tenseleyflow/shithub / 970ad46

Browse files

Fix duplicate migration versions

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
970ad46788501ba6414ee149f0404134a2bdbbcd
Parents
24ab250
Tree
667ddfd

9 changed files

StatusFile+-
M .github/workflows/ci.yml 3 0
M Makefile 5 2
M docs/internal/actions-schema.md 4 2
M docs/internal/db.md 3 1
M docs/internal/runbooks/actions-runner.md 1 1
R internal/migrationsfs/migrations/0052_runner_jwt_used.sqlinternal/migrationsfs/migrations/0053_runner_jwt_used.sql 0 0
R internal/migrationsfs/migrations/0052_push_events_web_protocol.sqlinternal/migrationsfs/migrations/0054_push_events_web_protocol.sql 0 0
A internal/migrationsfs/migrationsfs_test.go 54 0
A scripts/lint-migration-versions.sh 29 0
.github/workflows/ci.ymlmodified
@@ -42,6 +42,9 @@ jobs:
4242
         # accumulated during S00–S40 refactors (audit 2026-05-10 M1).
4343
         run: scripts/lint-unused.sh
4444
 
45
+      - name: Lint - migration versions
46
+        run: scripts/lint-migration-versions.sh
47
+
4548
       - name: Test
4649
         run: go test -trimpath ./...
4750
 
Makefilemodified
@@ -2,7 +2,7 @@
22
 # Targets mirror what CI runs. The Makefile is the source of truth.
33
 
44
 .DEFAULT_GOAL := help
5
-.PHONY: help dev build test test-race lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused verify-api-docs fmt tidy clean ci assets install-tools version deploy deploy-check restore-drill bench-staging docs docs-serve docs-verify gen-third-party-notices audit-a11y audit-a11y-pa11y audit-a11y-axe load-test
5
+.PHONY: help dev build test test-race lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs fmt tidy clean ci assets install-tools version deploy deploy-check restore-drill bench-staging docs docs-serve docs-verify gen-third-party-notices audit-a11y audit-a11y-pa11y audit-a11y-axe load-test
66
 
77
 # Build metadata embedded into the binary via -ldflags.
88
 VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
@@ -70,7 +70,7 @@ assets: ## Copy Primer CSS into internal/web/static/ for embedding.
7070
 		echo "warn: .refs/primer-css/dist not found; run 'git clone https://github.com/primer/css .refs/primer-css' first"; \
7171
 	fi
7272
 
73
-ci: lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused verify-api-docs test build ## Full CI pipeline (matches .github/workflows/ci.yml).
73
+ci: lint lint-policy lint-markdown lint-secret-logs lint-spdx lint-unused lint-migrations verify-api-docs test build ## Full CI pipeline (matches .github/workflows/ci.yml).
7474
 	@echo "ci: ok"
7575
 
7676
 lint-policy: ## Enforce policy-package boundary (no inline auth checks in handlers/git/cmd).
@@ -88,6 +88,9 @@ lint-spdx: ## Verify every Go + shell source carries the SPDX license header.
8888
 lint-unused: ## Fail when source carries dead-code 'silence unused import' shims (var _ = symbol).
8989
 	@scripts/lint-unused.sh
9090
 
91
+lint-migrations: ## Fail when goose migration numeric versions collide.
92
+	@scripts/lint-migration-versions.sh
93
+
9194
 verify-api-docs: ## Fail when an /api/v1 route in code is missing from docs/public/api/.
9295
 	@scripts/verify-api-docs.sh
9396
 
docs/internal/actions-schema.mdmodified
@@ -12,7 +12,9 @@ without churning under them.
1212
 
1313
 ## SQL schema
1414
 
15
-Migrations 0042–0052, in dependency order:
15
+Actions migrations currently span 0042–0051 and 0053. Migration 0052 belongs to
16
+the repo source-remotes feature and was already deployed before the runner JWT
17
+replay table landed.
1618
 
1719
 | #     | Table                       | Purpose                                                       |
1820
 | ----- | --------------------------- | ------------------------------------------------------------- |
@@ -26,7 +28,7 @@ Migrations 0042–0052, in dependency order:
2628
 | 0049  | `actions_variables`         | Non-secret per-repo/org config (Forgejo parity)               |
2729
 | 0050  | `workflow_steps.step_with`  | Parsed `with:` inputs for magic `uses:` aliases               |
2830
 | 0051  | `workflow_runs.trigger_event_id` | Trigger idempotency for retries/admin replays            |
29
-| 0052  | `runner_jwt_used`           | Single-use replay gate for runner job JWTs                    |
31
+| 0053  | `runner_jwt_used`           | Single-use replay gate for runner job JWTs                    |
3032
 
3133
 A few load-bearing choices, called out so they're easy to spot in a
3234
 later schema diff:
docs/internal/db.mdmodified
@@ -75,7 +75,9 @@ conventions. Every domain sprint (S05 onwards) follows these.
7575
   convenience but are immutable post-deploy. Corrections come as new
7676
   migrations.
7777
 - One change per migration. Filename: `NNNN_short_purpose.sql` where `NNNN`
78
-  is monotonically increasing.
78
+  is monotonically increasing and globally unique. `scripts/lint-migration-versions.sh`
79
+  enforces this in CI because goose panics on duplicate numeric versions before
80
+  it can run any migration.
7981
 - Goose markers:
8082
 
8183
   ```
docs/internal/runbooks/actions-runner.mdmodified
@@ -5,7 +5,7 @@ operator validation before the real `shithubd-runner` binary lands.
55
 
66
 Prereqs:
77
 
8
-- Database migrations are current through `0052_runner_jwt_used.sql`.
8
+- Database migrations are current through `0053_runner_jwt_used.sql`.
99
 - `SHITHUB_TOTP_KEY` or `auth.totp_key_b64` is set on the web process.
1010
 - Object storage is configured if testing artifact upload.
1111
 - A repo has a workflow under `.shithub/workflows/*.yml` with
internal/migrationsfs/migrations/0052_runner_jwt_used.sql → internal/migrationsfs/migrations/0053_runner_jwt_used.sqlrenamed (100% similarity)
internal/migrationsfs/migrations/0052_push_events_web_protocol.sql → internal/migrationsfs/migrations/0054_push_events_web_protocol.sqlrenamed (100% similarity)
internal/migrationsfs/migrationsfs_test.goadded
@@ -0,0 +1,54 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package migrationsfs
4
+
5
+import (
6
+	"fmt"
7
+	"io/fs"
8
+	"sort"
9
+	"strings"
10
+	"testing"
11
+)
12
+
13
+func TestMigrationVersionsAreUnique(t *testing.T) {
14
+	t.Parallel()
15
+
16
+	entries, err := fs.ReadDir(FS(), ".")
17
+	if err != nil {
18
+		t.Fatalf("ReadDir migrations: %v", err)
19
+	}
20
+
21
+	byVersion := make(map[string][]string)
22
+	for _, entry := range entries {
23
+		if entry.IsDir() || !isGooseMigrationName(entry.Name()) {
24
+			continue
25
+		}
26
+		version := entry.Name()[:4]
27
+		byVersion[version] = append(byVersion[version], entry.Name())
28
+	}
29
+
30
+	duplicates := make([]string, 0)
31
+	for version, names := range byVersion {
32
+		if len(names) <= 1 {
33
+			continue
34
+		}
35
+		sort.Strings(names)
36
+		duplicates = append(duplicates, fmt.Sprintf("%s: %s", version, strings.Join(names, ", ")))
37
+	}
38
+	sort.Strings(duplicates)
39
+	if len(duplicates) > 0 {
40
+		t.Fatalf("duplicate goose migration versions:\n%s", strings.Join(duplicates, "\n"))
41
+	}
42
+}
43
+
44
+func isGooseMigrationName(name string) bool {
45
+	if len(name) < len("0000_.sql") || !strings.HasSuffix(name, ".sql") || name[4] != '_' {
46
+		return false
47
+	}
48
+	for _, r := range name[:4] {
49
+		if r < '0' || r > '9' {
50
+			return false
51
+		}
52
+	}
53
+	return true
54
+}
scripts/lint-migration-versions.shadded
@@ -0,0 +1,29 @@
1
+#!/usr/bin/env bash
2
+# SPDX-License-Identifier: AGPL-3.0-or-later
3
+#
4
+# Goose keys migrations by numeric version, not the rest of the filename.
5
+# Duplicate NNNN prefixes panic at runtime before any migration runs, so
6
+# catch them in CI before a deploy has to discover the conflict.
7
+
8
+set -euo pipefail
9
+
10
+cd "$(git rev-parse --show-toplevel)"
11
+
12
+migrations=$(find internal/migrationsfs/migrations -maxdepth 1 -type f -name '[0-9][0-9][0-9][0-9]_*.sql' | sort)
13
+versions=$(printf '%s\n' "$migrations" | sed -E 's#^.*/([0-9][0-9][0-9][0-9])_.*$#\1#')
14
+duplicates=$(printf '%s\n' "$versions" | sort | uniq -d)
15
+
16
+if [ -n "$duplicates" ]; then
17
+  echo "lint-migration-versions: duplicate migration versions found:" >&2
18
+  echo >&2
19
+  while IFS= read -r version; do
20
+    [ -z "$version" ] && continue
21
+    echo "  version $version:" >&2
22
+    printf '%s\n' "$migrations" | grep "/${version}_" | sed 's/^/    /' >&2
23
+  done <<< "$duplicates"
24
+  echo >&2
25
+  echo "Goose requires each numeric NNNN prefix to be globally unique." >&2
26
+  exit 1
27
+fi
28
+
29
+echo "lint-migration-versions: ok"