|
882 | 882 | "expected_exit": 0 |
883 | 883 | } |
884 | 884 | ] |
| 885 | + }, |
| 886 | + { |
| 887 | + "id": "schema-sql-outside-migrations", |
| 888 | + "description": "Block schema-changing SQL (ALTER/CREATE/DROP/GRANT/REVOKE/CREATE POLICY) in .sql files outside supabase/migrations/", |
| 889 | + "applies_to": [ |
| 890 | + "Edit", |
| 891 | + "Write", |
| 892 | + "MultiEdit" |
| 893 | + ], |
| 894 | + "match": [ |
| 895 | + { |
| 896 | + "field": "file_path", |
| 897 | + "pattern": "\\.sql$", |
| 898 | + "not_pattern": "(supabase/migrations/|supabase/tests/|supabase/seed)" |
| 899 | + }, |
| 900 | + { |
| 901 | + "field": "content", |
| 902 | + "pattern": "\\b(ALTER TABLE|CREATE TABLE|DROP TABLE|CREATE POLICY|DROP POLICY|GRANT |REVOKE |CREATE FUNCTION|DROP FUNCTION|CREATE TYPE|ALTER TYPE)\\b", |
| 903 | + "flags": "i" |
| 904 | + } |
| 905 | + ], |
| 906 | + "action": "block", |
| 907 | + "bypass_marker": "schema-sql-outside-migrations", |
| 908 | + "memory_ref": "feedback_scripts_not_db.md", |
| 909 | + "message": "BLOCKED: schema-changing SQL in a file outside supabase/migrations/.\n\nEvery schema mutation (ALTER/CREATE/DROP/GRANT/POLICY) MUST live in a\ntimestamped migration file, never in ad-hoc scripts or runtime queries.\n\nCreate a new file:\n supabase/migrations/$(date -u +%Y%m%d%H%M%S)_<slug>.sql\n\nLegitimate exceptions (use the bypass marker):\n- Schema docs / examples in READMEs — should not be executable SQL\n- Test fixtures under supabase/tests/ — already exempt via not_pattern\n", |
| 910 | + "tests": [ |
| 911 | + { |
| 912 | + "name": "blocks-create-table-in-script", |
| 913 | + "input": { |
| 914 | + "tool_input": { |
| 915 | + "file_path": "scripts/setup.sql", |
| 916 | + "content": "CREATE TABLE foo (id int primary key);" |
| 917 | + } |
| 918 | + }, |
| 919 | + "expected_exit": 2 |
| 920 | + }, |
| 921 | + { |
| 922 | + "name": "blocks-grant-in-edge-function", |
| 923 | + "input": { |
| 924 | + "tool_input": { |
| 925 | + "file_path": "supabase/functions/foo/migrate.sql", |
| 926 | + "content": "GRANT SELECT ON foo TO authenticated;" |
| 927 | + } |
| 928 | + }, |
| 929 | + "expected_exit": 2 |
| 930 | + }, |
| 931 | + { |
| 932 | + "name": "blocks-alter-table-in-readme-example", |
| 933 | + "input": { |
| 934 | + "tool_input": { |
| 935 | + "file_path": "docs/example.sql", |
| 936 | + "content": "ALTER TABLE foo ADD COLUMN bar text;" |
| 937 | + } |
| 938 | + }, |
| 939 | + "expected_exit": 2 |
| 940 | + }, |
| 941 | + { |
| 942 | + "name": "allows-schema-in-migrations-folder", |
| 943 | + "input": { |
| 944 | + "tool_input": { |
| 945 | + "file_path": "supabase/migrations/20260509000000_foo.sql", |
| 946 | + "content": "CREATE TABLE foo (id int primary key);" |
| 947 | + } |
| 948 | + }, |
| 949 | + "expected_exit": 0 |
| 950 | + }, |
| 951 | + { |
| 952 | + "name": "allows-schema-in-tests-folder", |
| 953 | + "input": { |
| 954 | + "tool_input": { |
| 955 | + "file_path": "supabase/tests/foo_test.sql", |
| 956 | + "content": "CREATE TABLE temp_test_t (id int);" |
| 957 | + } |
| 958 | + }, |
| 959 | + "expected_exit": 0 |
| 960 | + }, |
| 961 | + { |
| 962 | + "name": "allows-non-schema-sql-anywhere", |
| 963 | + "input": { |
| 964 | + "tool_input": { |
| 965 | + "file_path": "scripts/query.sql", |
| 966 | + "content": "SELECT * FROM foo;" |
| 967 | + } |
| 968 | + }, |
| 969 | + "expected_exit": 0 |
| 970 | + }, |
| 971 | + { |
| 972 | + "name": "allows-bypass-marker", |
| 973 | + "input": { |
| 974 | + "tool_input": { |
| 975 | + "file_path": "scripts/oneshot.sql", |
| 976 | + "content": "-- # hook-bypass: schema-sql-outside-migrations\nCREATE TABLE foo (id int primary key);\n" |
| 977 | + } |
| 978 | + }, |
| 979 | + "expected_exit": 0 |
| 980 | + } |
| 981 | + ] |
| 982 | + }, |
| 983 | + { |
| 984 | + "id": "warn-psql-against-supabase-remote", |
| 985 | + "description": "Warn when psql/SQL is executed directly against a *.supabase.co host (drift risk vs versioned migrations)", |
| 986 | + "applies_to": [ |
| 987 | + "Bash" |
| 988 | + ], |
| 989 | + "match": [ |
| 990 | + { |
| 991 | + "field": "command", |
| 992 | + "pattern": "psql.*\\.supabase\\.co", |
| 993 | + "flags": "i" |
| 994 | + } |
| 995 | + ], |
| 996 | + "action": "warn", |
| 997 | + "bypass_marker": "psql-supabase-remote", |
| 998 | + "memory_ref": "feedback_scripts_not_db.md", |
| 999 | + "message": "WARNING: direct psql execution against a Supabase remote host.\n\nSQL Editor / psql-against-remote workflows create drift between the\nversioned migrations directory and other devs' local databases.\n\nCorrect workflow:\n 1. Add a migration in supabase/migrations/<timestamp>_<slug>.sql\n 2. Test locally: supabase db reset --local\n 3. Push to develop — CI applies the migration to staging automatically\n 4. For production: William runs the create-release SOP\n\nUse the bypass marker only for legitimate hotfixes where the PR cycle\nis too slow.\n", |
| 1000 | + "tests": [ |
| 1001 | + { |
| 1002 | + "name": "warns-on-psql-supabase-remote", |
| 1003 | + "input": { |
| 1004 | + "tool_input": { |
| 1005 | + "command": "psql postgresql://user:pass@db.xepaexmpawtpqtilhcpw.supabase.co:5432/postgres -c 'SELECT 1'" |
| 1006 | + } |
| 1007 | + }, |
| 1008 | + "expected_exit": 0, |
| 1009 | + "expected_stderr_contains": "warn-psql-against-supabase-remote" |
| 1010 | + }, |
| 1011 | + { |
| 1012 | + "name": "warns-on-psql-different-supabase-host", |
| 1013 | + "input": { |
| 1014 | + "tool_input": { |
| 1015 | + "command": "psql -h db.ukwovawzehnebuoowcec.supabase.co -d postgres -c 'CREATE INDEX foo_idx ON foo (id)'" |
| 1016 | + } |
| 1017 | + }, |
| 1018 | + "expected_exit": 0, |
| 1019 | + "expected_stderr_contains": "warn-psql-against-supabase-remote" |
| 1020 | + }, |
| 1021 | + { |
| 1022 | + "name": "allows-psql-localhost", |
| 1023 | + "input": { |
| 1024 | + "tool_input": { |
| 1025 | + "command": "psql -h localhost -p 54322 -d postgres -c 'SELECT 1'" |
| 1026 | + } |
| 1027 | + }, |
| 1028 | + "expected_exit": 0 |
| 1029 | + }, |
| 1030 | + { |
| 1031 | + "name": "allows-non-psql-supabase-references", |
| 1032 | + "input": { |
| 1033 | + "tool_input": { |
| 1034 | + "command": "curl https://xepaexmpawtpqtilhcpw.supabase.co/rest/v1/foo" |
| 1035 | + } |
| 1036 | + }, |
| 1037 | + "expected_exit": 0 |
| 1038 | + }, |
| 1039 | + { |
| 1040 | + "name": "allows-bypass-marker", |
| 1041 | + "input": { |
| 1042 | + "tool_input": { |
| 1043 | + "command": "psql -h db.xepaexmpawtpqtilhcpw.supabase.co -c 'SELECT 1' # hook-bypass: psql-supabase-remote" |
| 1044 | + } |
| 1045 | + }, |
| 1046 | + "expected_exit": 0 |
| 1047 | + } |
| 1048 | + ] |
| 1049 | + }, |
| 1050 | + { |
| 1051 | + "id": "pr-create-with-migrations-needs-deploy-note", |
| 1052 | + "description": "Warn when gh pr create body does not mention migrations (caller should verify the PR has no migration files)", |
| 1053 | + "applies_to": [ |
| 1054 | + "Bash" |
| 1055 | + ], |
| 1056 | + "match": [ |
| 1057 | + { |
| 1058 | + "field": "command", |
| 1059 | + "pattern": "gh[[:space:]]+pr[[:space:]]+create.*--body", |
| 1060 | + "flags": "i" |
| 1061 | + }, |
| 1062 | + { |
| 1063 | + "field": "command", |
| 1064 | + "not_pattern": "(migration|db push|supabase db|migration-deployment|requires.*staging|requires.*prod)", |
| 1065 | + "flags": "i" |
| 1066 | + } |
| 1067 | + ], |
| 1068 | + "action": "warn", |
| 1069 | + "bypass_marker": "pr-create-no-migration-note", |
| 1070 | + "memory_ref": "feedback_scripts_not_db.md", |
| 1071 | + "message": "WARNING: gh pr create body does not mention migrations.\n\nIf your PR modifies files under supabase/migrations/, the body MUST\ninclude a \"Migration deployment\" section that lists:\n 1. Which envs need supabase db push (staging auto via CI; prod via release)\n 2. Backfill order if the migrations depend on existing data\n 3. Post-merge verification steps\n\nThis rule does NOT inspect the diff — it only reads the command. Use\nthe bypass marker if your PR has no migrations.\n", |
| 1072 | + "tests": [ |
| 1073 | + { |
| 1074 | + "name": "warns-on-pr-create-body-without-migration-mention", |
| 1075 | + "input": { |
| 1076 | + "tool_input": { |
| 1077 | + "command": "gh pr create --base develop --title \"feat: foo\" --body \"Adds feature X\"" |
| 1078 | + } |
| 1079 | + }, |
| 1080 | + "expected_exit": 0, |
| 1081 | + "expected_stderr_contains": "pr-create-with-migrations-needs-deploy-note" |
| 1082 | + }, |
| 1083 | + { |
| 1084 | + "name": "allows-pr-create-body-mentioning-migration", |
| 1085 | + "input": { |
| 1086 | + "tool_input": { |
| 1087 | + "command": "gh pr create --base develop --title \"feat: foo\" --body \"Adds X. Migration deployment: requires supabase db push on staging\"" |
| 1088 | + } |
| 1089 | + }, |
| 1090 | + "expected_exit": 0 |
| 1091 | + }, |
| 1092 | + { |
| 1093 | + "name": "allows-pr-create-body-mentioning-db-push", |
| 1094 | + "input": { |
| 1095 | + "tool_input": { |
| 1096 | + "command": "gh pr create --body \"Use db push to apply\"" |
| 1097 | + } |
| 1098 | + }, |
| 1099 | + "expected_exit": 0 |
| 1100 | + }, |
| 1101 | + { |
| 1102 | + "name": "allows-bypass-marker", |
| 1103 | + "input": { |
| 1104 | + "tool_input": { |
| 1105 | + "command": "gh pr create --body \"release notes only # hook-bypass: pr-create-no-migration-note\"" |
| 1106 | + } |
| 1107 | + }, |
| 1108 | + "expected_exit": 0 |
| 1109 | + } |
| 1110 | + ] |
| 1111 | + }, |
| 1112 | + { |
| 1113 | + "id": "block-supabase-db-push-prod", |
| 1114 | + "description": "Block supabase db push targeting the PRODUCTION project ref without an explicit bypass marker", |
| 1115 | + "applies_to": [ |
| 1116 | + "Bash" |
| 1117 | + ], |
| 1118 | + "match": [ |
| 1119 | + { |
| 1120 | + "field": "command", |
| 1121 | + "pattern": "supabase[[:space:]]+db[[:space:]]+push.*ukwovawzehnebuoowcec" |
| 1122 | + } |
| 1123 | + ], |
| 1124 | + "action": "block", |
| 1125 | + "bypass_marker": "prod-db-push-approved", |
| 1126 | + "memory_ref": "feedback_never_touch_prod_without_approval.md", |
| 1127 | + "message": "BLOCKED: supabase db push targeting the PRODUCTION project ref\n(ukwovawzehnebuoowcec).\n\nNEVER run against production without explicit user approval — see\nfeedback_never_touch_prod_without_approval.md.\n\nCorrect workflow:\n 1. Merge to main\n 2. William runs the release per the SOP at\n https://github.com/DojoCodingLabs/dojo-os/blob/develop/.claude/commands/create-release.md\n\nUse the bypass marker ONLY when the user has verbally approved the\noperation.\n", |
| 1128 | + "tests": [ |
| 1129 | + { |
| 1130 | + "name": "blocks-supabase-db-push-prod", |
| 1131 | + "input": { |
| 1132 | + "tool_input": { |
| 1133 | + "command": "supabase db push --db-url postgresql://postgres:pwd@db.ukwovawzehnebuoowcec.supabase.co:5432/postgres" |
| 1134 | + } |
| 1135 | + }, |
| 1136 | + "expected_exit": 2 |
| 1137 | + }, |
| 1138 | + { |
| 1139 | + "name": "blocks-supabase-db-push-prod-with-project-ref-flag", |
| 1140 | + "input": { |
| 1141 | + "tool_input": { |
| 1142 | + "command": "supabase db push --project-ref ukwovawzehnebuoowcec" |
| 1143 | + } |
| 1144 | + }, |
| 1145 | + "expected_exit": 2 |
| 1146 | + }, |
| 1147 | + { |
| 1148 | + "name": "allows-supabase-db-push-staging", |
| 1149 | + "input": { |
| 1150 | + "tool_input": { |
| 1151 | + "command": "supabase db push --project-ref xepaexmpawtpqtilhcpw" |
| 1152 | + } |
| 1153 | + }, |
| 1154 | + "expected_exit": 0 |
| 1155 | + }, |
| 1156 | + { |
| 1157 | + "name": "allows-supabase-db-push-local", |
| 1158 | + "input": { |
| 1159 | + "tool_input": { |
| 1160 | + "command": "supabase db push" |
| 1161 | + } |
| 1162 | + }, |
| 1163 | + "expected_exit": 0 |
| 1164 | + }, |
| 1165 | + { |
| 1166 | + "name": "allows-bypass-marker", |
| 1167 | + "input": { |
| 1168 | + "tool_input": { |
| 1169 | + "command": "supabase db push --project-ref ukwovawzehnebuoowcec # hook-bypass: prod-db-push-approved" |
| 1170 | + } |
| 1171 | + }, |
| 1172 | + "expected_exit": 0 |
| 1173 | + } |
| 1174 | + ] |
885 | 1175 | } |
886 | 1176 | ] |
0 commit comments