From a2ad9b8874c86c75a52514a8d9f9e38674ee32bc Mon Sep 17 00:00:00 2001 From: KeiKurosawa Date: Fri, 10 Jul 2026 16:09:11 +0800 Subject: [PATCH] fix(dev): guard driver submodule checkout --- CONTRIBUTING.md | 4 ++ justfile | 10 ++- package.json | 12 ++-- scripts/check-driver-submodule-state.test.ts | 30 ++++++++ scripts/check-driver-submodule-state.ts | 76 ++++++++++++++++++++ 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 scripts/check-driver-submodule-state.test.ts create mode 100644 scripts/check-driver-submodule-state.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54352c07..968488f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,6 +99,10 @@ just dev This command runs local D1 migration first, then starts the driver build, API Worker, and web app. +`just dev`, `just build`, and `just check` fail fast when `apps/driver` is not initialized or its checked-out commit differs from the gitlink recorded by the main repository. Run `git submodule update --init --checkout apps/driver` to restore the pinned Driver version. The guard never updates the submodule automatically, so intentional Driver work remains explicit and reviewable. + +`just setup` also enables `submodule.recurse` in the current checkout so later Git operations keep submodule state visible and synchronized where Git supports recursive behavior. The fail-fast guard remains authoritative because local Git configuration can be changed or bypassed. + Default local URLs: - Web: `http://localhost:5173` diff --git a/justfile b/justfile index ad74062b..30b7b4a3 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,9 @@ default: # Prepare a new checkout for local development. setup: - git submodule update --init + git config submodule.recurse true + git submodule update --init --recursive --checkout + just driver-submodule-check bun install --frozen-lockfile just env-init just hooks-install @@ -23,7 +25,7 @@ commit-check: bun run commit:check # Start the local development stack after applying local migrations. -dev: +dev: driver-submodule-check just db-migrate bun run dev @@ -128,6 +130,10 @@ e2e *args: driver-submodule-smoke: bun run driver:submodule:smoke +# Verify that the Agent Driver checkout matches the repository gitlink. +driver-submodule-check: + bun run driver:submodule:check + # Update the Agent Driver submodule to upstream HEAD. driver-update: git submodule update --init --remote --checkout apps/driver diff --git a/package.json b/package.json index 6a4fac0e..f5ebeded 100644 --- a/package.json +++ b/package.json @@ -8,16 +8,18 @@ ], "type": "module", "scripts": { - "build": "vp run --filter agent-driver build && vp run --filter @mosoo/web build", + "build": "bun run driver:submodule:check && vp run --filter agent-driver build && vp run --filter @mosoo/web build", "cf:types": "vp run --filter @mosoo/api cf:types", - "check": "vp run -w fmt:check && vp run -w docs:check && vp run -w lint && vp run -w tc && vp run -w test", + "check": "bun run driver:submodule:check && vp run -w fmt:check && vp run -w docs:check && vp run -w lint && vp run -w tc && vp run -w test", "db:migrate:local": "vp run --filter @mosoo/api db:migrate:local", "db:regen": "vp run --filter @mosoo/db db:regen", "deploy": "vp run -w deploy:api && vp run -w deploy:web", - "deploy:api": "vp run --filter @mosoo/api deploy", + "deploy:api": "bun run driver:submodule:check && vp run --filter @mosoo/api deploy", "deploy:web": "vp run --filter @mosoo/web deploy", - "dev": "vp run --filter @mosoo/api --filter @mosoo/web --parallel dev", + "dev": "bun run driver:submodule:check && vp run --filter @mosoo/api --filter @mosoo/web --parallel dev", "docs:check": "vp exec bun scripts/check-doc-links.ts", + "driver:repo:export": "vp exec bun scripts/export-driver-standalone-repo.ts", + "driver:submodule:check": "bun scripts/check-driver-submodule-state.ts", "driver:submodule:smoke": "vp exec bun scripts/check-driver-submodule-cutover.ts", "e2e": "bun e2e/cli.ts", "env:init": "vp run --filter @mosoo/api env:init", @@ -35,7 +37,7 @@ "react-doctor:diff": "vp exec react-doctor apps/web --verbose --diff", "react-doctor:report": "vp exec react-doctor apps/web --json --json-compact --full --fail-on none", "tc": "vp run -r tc", - "test": "vp exec bun test config/commit-policy.test.ts scripts/validate-commit-message.test.ts && vp run --filter @mosoo/api --filter agent-driver --filter @mosoo/web --filter @mosoo/ag-ui-session --filter @mosoo/agent-package --filter @mosoo/contracts --filter @mosoo/db --filter @mosoo/id --filter @mosoo/public-api-client --filter @mosoo/runtime-catalog --filter @mosoo/runtime-events --filter @mosoo/session-policy --filter @mosoo/skill-package test && vp run -w graphql:codegen:check" + "test": "vp exec bun test config/commit-policy.test.ts scripts/check-driver-submodule-state.test.ts scripts/validate-commit-message.test.ts && vp run --filter @mosoo/api --filter agent-driver --filter @mosoo/web --filter @mosoo/ag-ui-session --filter @mosoo/agent-package --filter @mosoo/contracts --filter @mosoo/db --filter @mosoo/id --filter @mosoo/public-api-client --filter @mosoo/runtime-catalog --filter @mosoo/runtime-events --filter @mosoo/session-policy --filter @mosoo/skill-package test && vp run -w graphql:codegen:check" }, "devDependencies": { "@graphql-codegen/cli": "^7.2.0", diff --git a/scripts/check-driver-submodule-state.test.ts b/scripts/check-driver-submodule-state.test.ts new file mode 100644 index 00000000..8ecf0422 --- /dev/null +++ b/scripts/check-driver-submodule-state.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; + +import { validateDriverSubmoduleCheckout } from "./check-driver-submodule-state"; + +describe("driver submodule checkout guard", () => { + test("accepts the commit recorded by the repository index", () => { + expect(() => + validateDriverSubmoduleCheckout({ + actualCommit: "expected-commit", + expectedCommit: "expected-commit", + }), + ).not.toThrow(); + }); + + test("rejects a stale driver checkout with an actionable command", () => { + expect(() => + validateDriverSubmoduleCheckout({ + actualCommit: "stale-commit", + expectedCommit: "expected-commit", + }), + ).toThrow( + [ + "apps/driver is checked out at the wrong commit.", + "Expected: expected-commit", + "Actual: stale-commit", + "Run: git submodule update --init --checkout apps/driver", + ].join("\n"), + ); + }); +}); diff --git a/scripts/check-driver-submodule-state.ts b/scripts/check-driver-submodule-state.ts new file mode 100644 index 00000000..b770eae3 --- /dev/null +++ b/scripts/check-driver-submodule-state.ts @@ -0,0 +1,76 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const driverSubmodulePath = "apps/driver"; +const updateCommand = `git submodule update --init --checkout ${driverSubmodulePath}`; + +export interface DriverSubmoduleCheckout { + readonly actualCommit: string; + readonly expectedCommit: string; +} + +function fail(message: string): never { + throw new Error(`Driver submodule check failed: ${message}`); +} + +function readGitOutput(repoRoot: string, args: readonly string[], failureMessage: string): string { + const result = spawnSync("git", [...args], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.status !== 0) { + const detail = result.stderr.trim(); + fail(`${failureMessage}${detail.length > 0 ? `\n${detail}` : ""}`); + } + + const output = result.stdout.trim(); + + if (output.length === 0) { + fail(failureMessage); + } + + return output; +} + +export function validateDriverSubmoduleCheckout(input: DriverSubmoduleCheckout): void { + if (input.actualCommit === input.expectedCommit) { + return; + } + + fail( + [ + `${driverSubmodulePath} is checked out at the wrong commit.`, + `Expected: ${input.expectedCommit}`, + `Actual: ${input.actualCommit}`, + `Run: ${updateCommand}`, + ].join("\n"), + ); +} + +export function checkDriverSubmoduleCheckout(repoRoot: string): DriverSubmoduleCheckout { + const expectedCommit = readGitOutput( + repoRoot, + ["rev-parse", `:${driverSubmodulePath}`], + `${driverSubmodulePath} is not recorded as an initialized gitlink in the repository index.`, + ); + const actualCommit = readGitOutput( + repoRoot, + ["-C", driverSubmodulePath, "rev-parse", "HEAD"], + `${driverSubmodulePath} is not initialized. Run: ${updateCommand}`, + ); + const checkout = { + actualCommit, + expectedCommit, + } satisfies DriverSubmoduleCheckout; + + validateDriverSubmoduleCheckout(checkout); + return checkout; +} + +if (import.meta.main) { + const repoRoot = fileURLToPath(new URL("..", import.meta.url)); + const checkout = checkDriverSubmoduleCheckout(repoRoot); + console.log(`Driver submodule checkout matches ${checkout.expectedCommit}.`); +}