diff --git a/scripts/migration-pr-readiness.mjs b/scripts/migration-pr-readiness.mjs index db22aafcd7..d7ab27fc67 100644 --- a/scripts/migration-pr-readiness.mjs +++ b/scripts/migration-pr-readiness.mjs @@ -61,7 +61,8 @@ const POLICY_PATH = 'scripts/migration-readiness-policy.json' const SQL_PATH = /^supabase\/migrations\/(\d{14})_([A-Za-z0-9_.-]+)\.sql$/ const sha = /^[a-f0-9]{40}$/ const hashBytes = bytes => createHash('sha256').update(bytes).digest('hex') -const requireFact = (condition, message) => { if (!condition) throw new Error(message) } +// `safe` marks our own fixed-text refusals so the job log can name the failed rule; provider/Git errors never carry it. +const requireFact = (condition, message) => { if (!condition) throw Object.assign(new Error(message), { safe: true }) } // One native Git process reads the corpus; never spawn a process per historical migration. export function readGitBlobs(git, revision, paths) { @@ -228,7 +229,8 @@ async function main() { if (result.status === 'strict_parity') { console.log('Migration PR historical SQL unchanged; current main/production strict parity VERIFIED.'); return } console.log(`Migration PR staging VERIFIED; ${result.pending.length} exact registered migration(s) PENDING APPLY. Production was NOT changed. Main remains strict.`) } -if (invokedDirectly(import.meta.url)) main().catch(() => { +if (invokedDirectly(import.meta.url)) main().catch(error => { + if (error?.safe === true) console.error(`Migration readiness rule refused: ${error.message}`) // No API body, token, SQL, state or Git command arguments are copied into CI diagnostics. console.error('Migration PR readiness failed closed; registration, current base/ledger or exact-head staging proof was not satisfied.') process.exitCode = 1 diff --git a/scripts/publish-migration-readiness.mjs b/scripts/publish-migration-readiness.mjs index b4b7d34c38..a81bba84cd 100644 --- a/scripts/publish-migration-readiness.mjs +++ b/scripts/publish-migration-readiness.mjs @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto' const repo = 'hellofrequencylab/frequency-web' const sha = /^[a-f0-9]{40}$/, digest = /^[a-f0-9]{64}$/ const hash = value => createHash('sha256').update(value).digest('hex') -const requireFact = (condition, message) => { if (!condition) throw Error(message) } +const requireFact = (condition, message) => { if (!condition) throw Object.assign(Error(message), { safe: true }) } const files = ['scripts/migration-pr-readiness.mjs', 'scripts/publish-migration-readiness.mjs', '.github/workflows/migration-readiness.yml', 'scripts/migration-readiness-policy.json'] export async function publishReadiness({ env, event, api, git, publisherSha256, evaluate }) { requireFact(env.GITHUB_ACTIONS === 'true' && env.GITHUB_EVENT_NAME === 'pull_request_target' && env.GITHUB_REPOSITORY === repo && env.GITHUB_WORKFLOW_REF === `${repo}/.github/workflows/migration-readiness.yml@refs/heads/main`, 'trusted workflow required') @@ -43,7 +43,9 @@ export async function publishReadiness({ env, event, api, git, publisherSha256, // A branch movement during the API write invalidates even the old-head result. await current(result.status !== 'not_applicable') return result - } catch { + } catch (error) { + // Only our own fixed-text rule names are printed; provider, Git and API errors are never serialized. + if (error?.safe === true) console.error(`Migration readiness rule refused: ${error.message}`) await finish('failure', { title: 'Trusted migration readiness failed closed', summary: 'Evaluation, identity or proof was refused, unavailable or moved. No production SQL was applied. Rerun against the current PR head and main.' }) throw Error('trusted candidate-head readiness failed') }