From c6e9f3b30e8ba11ce6fc11c2c5f6a510cabf5e4b Mon Sep 17 00:00:00 2001 From: Seed LOOM Date: Mon, 2 Nov 2026 22:23:30 -0500 Subject: [PATCH] fix(verification): map transfer identity via repository_id --- .github/workflows/reusable-defensible-vertical-slice.yml | 5 ++++- .github/workflows/reusable-node-verify-v2.yml | 7 ++++--- schemas/repository-verification-profile.schema.json | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-defensible-vertical-slice.yml b/.github/workflows/reusable-defensible-vertical-slice.yml index 33dbc8f..4ec1506 100644 --- a/.github/workflows/reusable-defensible-vertical-slice.yml +++ b/.github/workflows/reusable-defensible-vertical-slice.yml @@ -50,7 +50,10 @@ jobs: const missing = required.filter(k => !(k in p)); if (missing.length) throw new Error(`Missing profile keys: ${missing.join(', ')}`); if (p.schema_version !== 1) throw new Error('Unsupported profile schema_version'); - if (p.repository !== process.env.GITHUB_REPOSITORY) throw new Error(`Profile repository ${p.repository} does not match ${process.env.GITHUB_REPOSITORY}`); + const idMatch = p.repository_id !== undefined && String(p.repository_id) === String(process.env.GITHUB_REPOSITORY_ID); + const formerNames = Array.isArray(p.former_repository_names) ? p.former_repository_names : []; + const nameMatch = p.repository === process.env.GITHUB_REPOSITORY || formerNames.includes(process.env.GITHUB_REPOSITORY); + if (!idMatch && !nameMatch) throw new Error(`Profile identity mismatch: profile repository=${p.repository} former_repository_names=${JSON.stringify(formerNames)} repository_id=${p.repository_id ?? 'unset'}; runtime GITHUB_REPOSITORY=${process.env.GITHUB_REPOSITORY} GITHUB_REPOSITORY_ID=${process.env.GITHUB_REPOSITORY_ID}`); if (!Array.isArray(p.promotion.required_gates) || !p.promotion.required_gates.length) throw new Error('At least one promotion gate is required'); console.log(JSON.stringify(p, null, 2)); NODE diff --git a/.github/workflows/reusable-node-verify-v2.yml b/.github/workflows/reusable-node-verify-v2.yml index 86bf78f..72c30da 100644 --- a/.github/workflows/reusable-node-verify-v2.yml +++ b/.github/workflows/reusable-node-verify-v2.yml @@ -98,9 +98,10 @@ jobs: const required = ['schema_version', 'repository', 'classification', 'runtime', 'commands', 'surfaces', 'promotion', 'evidence']; const missing = required.filter((key) => !(key in profile)); if (missing.length) throw new Error(`Missing profile keys: ${missing.join(', ')}`); - if (profile.repository !== process.env.GITHUB_REPOSITORY) { - throw new Error(`Profile repository ${profile.repository} does not match ${process.env.GITHUB_REPOSITORY}`); - } + const idMatch = profile.repository_id !== undefined && String(profile.repository_id) === String(process.env.GITHUB_REPOSITORY_ID); + const formerNames = Array.isArray(profile.former_repository_names) ? profile.former_repository_names : []; + const nameMatch = profile.repository === process.env.GITHUB_REPOSITORY || formerNames.includes(process.env.GITHUB_REPOSITORY); + if (!idMatch && !nameMatch) throw new Error(`Profile identity mismatch: profile repository=${profile.repository} former_repository_names=${JSON.stringify(formerNames)} repository_id=${profile.repository_id ?? 'unset'}; runtime GITHUB_REPOSITORY=${process.env.GITHUB_REPOSITORY} GITHUB_REPOSITORY_ID=${process.env.GITHUB_REPOSITORY_ID}`); if (!Array.isArray(profile.promotion?.required_gates) || profile.promotion.required_gates.length === 0) { throw new Error('promotion.required_gates must contain at least one gate'); } diff --git a/schemas/repository-verification-profile.schema.json b/schemas/repository-verification-profile.schema.json index 3d0e481..e31b018 100644 --- a/schemas/repository-verification-profile.schema.json +++ b/schemas/repository-verification-profile.schema.json @@ -8,6 +8,8 @@ "properties": { "schema_version": { "const": 1 }, "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" }, + "repository_id": { "type": "integer", "minimum": 1, "description": "Stable GitHub numeric repository id; survives transfers and renames." }, + "former_repository_names": { "type": "array", "items": { "type": "string", "pattern": "^[^/]+/[^/]+$" }, "uniqueItems": true, "description": "Historical owner/name values (e.g. pre-transfer); preserved so historical receipts remain attributable." }, "classification": { "type": "object", "additionalProperties": false,