Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions scripts/migration-pr-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,10 @@ async function main() {
}
const collectorArgs = {
env, collectorSha256: hashBytes(readFileSync(process.argv[1])), event: JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf8')),
git: (args, binary = false, input) => execFileSync('git', ['--no-replace-objects', ...args], { input, encoding: binary ? undefined : 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }),
api: (path, options) => request(`https://api.github.com${path}`, env.GITHUB_TOKEN, options),
git: (args, binary = false, input) => {
try { return execFileSync('git', ['--no-replace-objects', ...args], { input, encoding: binary ? undefined : 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }) } catch (error) { throw Object.assign(error, { stage: `git-${/^[a-z-]{1,20}$/.test(args[0]) ? args[0] : 'cmd'}` }) }
},
api: async (path, options) => { try { return await request(`https://api.github.com${path}`, env.GITHUB_TOKEN, options) } catch (error) { throw error?.safe ? error : Object.assign(error, { stage: 'github-api' }) } },
ledger: async () => {
requireFact(env.SUPABASE_ACCESS_TOKEN && /^[a-z0-9]+$/.test(env.SUPABASE_PROJECT_REF ?? ''), 'live comparison credentials required')
const payload = await request(`https://api.supabase.com/v1/projects/${env.SUPABASE_PROJECT_REF}/database/query`, env.SUPABASE_ACCESS_TOKEN, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: 'select version, name from supabase_migrations.schema_migrations order by version' }) })
Expand All @@ -230,7 +232,7 @@ async function main() {
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(error => {
if (error?.safe === true) console.error(`Migration readiness rule refused: ${error.message}`)
import('./publish-migration-readiness.mjs').then(({ describeFailure }) => console.error(`Migration readiness ${describeFailure(error)}`)).catch(() => {})
// 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
Expand Down
9 changes: 8 additions & 1 deletion scripts/publish-migration-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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 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']
// A log-safe label for any failure: our own rule text, or only the error class, code, exit status and the stage that
// raised it. Never the message, stderr, command arguments or any provider body.
export function describeFailure(error) {
if (error?.safe === true) return `rule refused: ${error.message}`
const part = value => ((typeof value === 'string' || typeof value === 'number') && /^[A-Za-z0-9_.:-]{1,40}$/.test(String(value)) ? String(value) : '?')
return `error ${part(error?.name)} code=${part(error?.code)} status=${part(error?.status)} stage=${part(error?.stage)}`
}
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')
requireFact(Number.isSafeInteger(event?.number) && event.number > 0 && sha.test(event.pull_request?.head?.sha) && /^\d+$/.test(env.GITHUB_RUN_ID ?? '') && /^\d+$/.test(env.GITHUB_RUN_ATTEMPT ?? ''), 'unknown PR/run identity')
Expand Down Expand Up @@ -45,7 +52,7 @@ export async function publishReadiness({ env, event, api, git, publisherSha256,
return result
} 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}`)
console.error(`Migration readiness ${describeFailure(error)}`)
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')
}
Expand Down
Loading