feat: add GSD_PROJECT_ID env var to override project hash (#1600)
Extract validateProjectId() and validate at startup in bootstrapAutoSession() so users get immediate feedback on invalid values. repoIdentity() returns the custom ID directly when set.
This commit is contained in:
parent
912dab1d81
commit
0bceb689a7
3 changed files with 52 additions and 5 deletions
|
|
@ -20,7 +20,7 @@ import {
|
|||
resolveSkillDiscoveryMode,
|
||||
getIsolationMode,
|
||||
} from "./preferences.js";
|
||||
import { ensureGsdSymlink } from "./repo-identity.js";
|
||||
import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
|
||||
import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
|
||||
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
||||
import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
|
||||
|
|
@ -130,6 +130,16 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
|
||||
try {
|
||||
// Validate GSD_PROJECT_ID early so the user gets immediate feedback
|
||||
const customProjectId = process.env.GSD_PROJECT_ID;
|
||||
if (customProjectId && !validateProjectId(customProjectId)) {
|
||||
ctx.ui.notify(
|
||||
`GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`,
|
||||
"error",
|
||||
);
|
||||
return releaseLockAndReturn();
|
||||
}
|
||||
|
||||
// Ensure git repo exists
|
||||
if (!nativeIsRepo(base)) {
|
||||
const mainBranch =
|
||||
|
|
|
|||
|
|
@ -92,14 +92,31 @@ function resolveGitRoot(basePath: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a GSD_PROJECT_ID value.
|
||||
*
|
||||
* Must contain only alphanumeric characters, hyphens, and underscores.
|
||||
* Call this once at startup so the user gets immediate feedback on bad values.
|
||||
*/
|
||||
export function validateProjectId(id: string): boolean {
|
||||
return /^[a-zA-Z0-9_-]+$/.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a stable identity for a repository.
|
||||
*
|
||||
* SHA-256 of `${remoteUrl}\n${resolvedRoot}`, truncated to 12 hex chars.
|
||||
* Deterministic: same repo always produces the same hash regardless of
|
||||
* which worktree the caller is inside.
|
||||
* If `GSD_PROJECT_ID` is set, returns it directly (validation is expected
|
||||
* to have already happened at startup via `validateProjectId`).
|
||||
*
|
||||
* Otherwise returns SHA-256 of `${remoteUrl}\n${resolvedRoot}`, truncated
|
||||
* to 12 hex chars. Deterministic: same repo always produces the same hash
|
||||
* regardless of which worktree the caller is inside.
|
||||
*/
|
||||
export function repoIdentity(basePath: string): string {
|
||||
const projectId = process.env.GSD_PROJECT_ID;
|
||||
if (projectId) {
|
||||
return projectId;
|
||||
}
|
||||
const remoteUrl = getRemoteUrl(basePath);
|
||||
const root = resolveGitRoot(basePath);
|
||||
const input = `${remoteUrl}\n${root}`;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { join } from "node:path";
|
|||
import { tmpdir } from "node:os";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
import { externalGsdRoot, ensureGsdSymlink } from "../repo-identity.ts";
|
||||
import { repoIdentity, externalGsdRoot, ensureGsdSymlink, validateProjectId } from "../repo-identity.ts";
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
|
@ -57,6 +57,26 @@ async function main(): Promise<void> {
|
|||
assertEq(preservedDirState, join(worktreePath, ".gsd"), "worktree .gsd directory is left in place for sync-based refresh");
|
||||
assertTrue(lstatSync(join(worktreePath, ".gsd")).isDirectory(), "worktree .gsd directory remains a directory");
|
||||
assertTrue(existsSync(join(worktreePath, ".gsd", "milestones", "stale.txt")), "existing worktree .gsd directory contents remain available for sync logic");
|
||||
|
||||
console.log("\n=== GSD_PROJECT_ID overrides computed repo hash ===");
|
||||
process.env.GSD_PROJECT_ID = "my-project";
|
||||
assertEq(repoIdentity(base), "my-project", "repoIdentity returns GSD_PROJECT_ID when set");
|
||||
assertEq(externalGsdRoot(base), join(stateDir, "projects", "my-project"), "externalGsdRoot uses GSD_PROJECT_ID");
|
||||
delete process.env.GSD_PROJECT_ID;
|
||||
|
||||
console.log("\n=== GSD_PROJECT_ID falls back to hash when unset ===");
|
||||
const hashIdentity = repoIdentity(base);
|
||||
assertTrue(/^[0-9a-f]{12}$/.test(hashIdentity), "repoIdentity returns 12-char hex hash when GSD_PROJECT_ID is unset");
|
||||
|
||||
console.log("\n=== validateProjectId rejects invalid values ===");
|
||||
for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
|
||||
assertTrue(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
|
||||
}
|
||||
|
||||
console.log("\n=== validateProjectId accepts valid values ===");
|
||||
for (const valid of ["my-project", "foo_bar", "abc123", "A-Z_0-9"]) {
|
||||
assertTrue(validateProjectId(valid), `validateProjectId accepts valid value: "${valid}"`);
|
||||
}
|
||||
} finally {
|
||||
delete process.env.GSD_STATE_DIR;
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue