import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { hashArtifactDirectory } from './release-artifact-hash.js'; const defaultEvidencePath = path.resolve(process.cwd(), 'docs/refactor/production-launch-evidence.json'); const defaultMaxAgeDays = 14; const defaultLiveTimeoutMs = 10_000; const h5PortalTargets = [ { portal: 'student', targetKey: 'studentH5Url' }, { portal: 'tenant-admin', targetKey: 'tenantAdminH5Url' }, { portal: 'platform-admin', targetKey: 'platformAdminH5Url' }, ]; const gateChecks = [ { id: 'readiness.production.env', label: 'Production environment readiness', commandIncludes: 'readiness:production', artifactSummary: nestedJsonSummaryArtifact, summary: { blocker: 0 }, }, { id: 'readiness.production.db', label: 'Production database readiness', commandIncludes: 'readiness:production:db', artifactSummary: nestedJsonSummaryArtifact, summary: { blocker: 0 }, }, { id: 'db.migration-history', label: 'Production database migration history', commandIncludes: 'db.migrations.current', artifactSummary: migrationHistoryArtifactSummary, summary: { status: 'pass', failed: 0, schemaVersion: 1, latestRepositoryMigration: { truthy: true }, latestAppliedMigration: { truthy: true }, missingMigrations: [], readinessArtifactSha256: { truthy: true }, }, }, { id: 'postgres.tuning-evidence', label: 'PostgreSQL 4c16g tuning evidence', commandIncludes: 'perf:postgres:evidence', artifactSummary: directJsonArtifactSummary, summary: { status: 'pass', profile: { oneOf: ['shared-host', 'dedicated-db'] }, failures: 0, pgStatStatementsAvailable: true, pendingRestart: 0, }, }, { id: 'auth.remote-smoke', label: 'Remote Supabase Auth/JWKS smoke', commandIncludes: 'smoke:auth:remote', summary: { failed: 0, requireAdminTokens: true }, }, { id: 'auth.platform-admin-bootstrap', label: 'First platform administrator bootstrap and Auth identity binding', commandIncludes: 'bootstrap:platform-admin', artifactSummary: platformAdminBootstrapArtifactSummary, summary: { status: 'pass', failed: 0, schemaVersion: 1, dryRunVerified: true, applied: true, adminUserIdSha256: { truthy: true }, authSmokeExpectedUserIdSha256: { truthy: true }, identityMatches: true, auditEvent: 'platform.admin.bootstrapped', auditVerified: true, dryRunArtifactSha256: { truthy: true }, applyArtifactSha256: { truthy: true }, authSmokeArtifactSha256: { truthy: true }, auditArtifactSha256: { truthy: true }, }, }, { id: 'auth.sms-pnvs-diagnostics', label: 'Aliyun PNVS SMS provider diagnostics', commandIncludes: 'diagnose:aliyun-pnvs', artifactSummary: directJsonArtifactSummary, summary: { ok: true, 'env.providerMatchesPnvs': true, 'provider.found': true, 'provider.provider': 'aliyun-pnvs', 'provider.status': { oneOf: ['active', 'testing'] }, 'provider.publicConfig.templateParamHasCodePlaceholder': true, 'secret.found': true, 'secret.accessKeyIdLength': { gt: 0 }, 'secret.accessKeySecretLength': { gt: 0 }, }, }, { id: 'auth.sms-pnvs-remote-smoke', label: 'Remote Aliyun PNVS SMS login smoke', commandIncludes: 'smoke:sms-login:remote', artifactSummary: directJsonArtifactSummary, summary: { failed: 0, provider: 'aliyun-pnvs', loginVerified: true, authMe: true, }, }, { id: 'rls.tenant-isolation', label: 'Runtime tenant RLS isolation', commandIncludes: 'test:rls', summary: { failed: 0 }, }, { id: 'db.tenant-foreign-key-audit', label: 'Tenant foreign key full-data integrity audit', commandIncludes: 'audit:tenant-foreign-keys', artifactSummary: directJsonArtifactSummary, summary: { status: 'pass', kind: 'tenant-foreign-key-audit', 'safety.databaseEnvironment': { oneOf: ['local', 'test', 'ci'] }, 'schema.schemaMatches': true, 'schema.relationCount': { gte: 189 }, 'schema.exceptionCount': 3, 'data.auditedRelations': { gte: 189 }, 'data.invalidRelations': 0, }, }, { id: 'migration.pb-production-dry-run', label: 'PocketBase production dry-run', commandIncludes: 'pb:import:dry-run', artifactSummary: pocketBaseDryRunArtifactSummary, summary: { blocker: 0, warning: 0, migrationProfile: 'production', requiredCollectionsMissing: 0, criticalFieldCoverageWarnings: 0, }, }, { id: 'migration.pb-import-validate', label: 'PocketBase import validation', commandIncludes: 'pb:import:validate', summary: { fail: 0 }, }, { id: 'migration.pb-import-sample', label: 'PocketBase imported business sampling', commandIncludes: 'pb:import:sample', summary: { fail: 0 }, }, { id: 'performance.api-real-data-read', label: 'Real-data API read benchmark', commandIncludes: 'perf:api:local', summary: { errors: 0, errorRate: { lte: 0.001 }, p95Ms: { lte: 300 }, p99Ms: { lte: 800 }, concurrency: { gte: 30 }, durationSeconds: { gte: 120 }, includeWrites: false, }, }, { id: 'performance.api-real-data-mixed', label: 'Real-data API mixed read/write benchmark', commandIncludes: 'perf:api:local', summary: { errors: 0, errorRate: { lte: 0.001 }, p95Ms: { lte: 500 }, p99Ms: { lte: 1200 }, concurrency: { gte: 50 }, durationSeconds: { gte: 60 }, includeWrites: true, }, }, { id: 'performance.tenant-students-100k', label: 'Single-tenant 100k student list capacity evidence', commandIncludes: 'perf:tenant-students:evidence', artifactSummary: tenantStudentCapacityArtifactSummary, summary: { databaseEnvironment: { oneOf: ['test', 'ci'] }, 'fixture.platformUsers': { gte: 100000 }, 'fixture.tenantMemberships': { gte: 100000 }, 'fixture.studentProfiles': { gte: 100000 }, caseCount: { gte: 5 }, deepCursorApproximateOffset: { gte: 90000 }, firstPageP95Ms: { lte: 100 }, deepCursorP95Ms: { lte: 100 }, searchP95MaxMs: { lte: 250 }, keysetIndexUsed: true, trigramIndexUsed: true, cleanupVerified: true, remainingManagedUsers: 0, }, }, { id: 'api.dynamic-tenant-cors-smoke', label: 'Dynamic active-tenant CORS production smoke', commandIncludes: 'smoke:tenant-cors:remote', artifactSummary: directJsonArtifactSummary, summary: { failed: 0, activeTenantOriginAllowed: true, unknownOriginDenied: true, disabledOriginDenied: true, noOriginHealthAllowed: true, }, }, { id: 'deploy.linux-systemd-verify', label: 'Linux systemd API, worker and timer installation verification', commandIncludes: 'systemd-analyze verify', summary: { failed: 0, apiServiceActive: true, workerTargetActive: true, workerJobServices: { gte: 9 }, enabledTimers: { gte: 6 }, }, }, { id: 'backup.restore-drill', label: 'Isolated production backup restore drill', commandIncludes: 'RESTORE_DRILL_VERIFY_COMMAND', artifactSummary: backupRestoreDrillArtifactSummary, summary: { status: 'pass', failed: 0, schemaVersion: 1, snapshotId: { truthy: true }, restoreTarget: { truthy: true }, isolated: true, integrityVerified: true, rtoMinutes: { gte: 0 }, rpoMinutes: { gte: 0 }, verificationArtifactSha256: { truthy: true }, }, }, { id: 'api.launch-persona-smoke', label: 'Real API launch persona journey smoke', commandIncludes: 'smoke:launch-persona', artifactSummary: directJsonArtifactSummary, summary: { status: 'pass', authMode: 'app_session', 'student.status': 'pass', 'student.result.entitlement.isSvip': true, 'student.result.practice.questionCount': { gte: 1 }, 'student.result.favorite.favoriteCount': { gte: 1 }, 'student.result.favorite.reviewSessionId': { truthy: true }, 'student.result.wrongReview.reviewPlanCount': { gte: 0 }, 'tenantAdmin.status': 'pass', 'tenantAdmin.result.guards.studentDashboardDenied': { oneOf: ['TENANT_ADMIN_REQUIRED', 'TENANT_MEMBER_REQUIRED', 'TENANT_PERMISSION_DENIED', 'FORBIDDEN'] }, 'tenantAdmin.result.guards.crossTenantDenied': { oneOf: ['AUTH_TENANT_MISMATCH', 'TENANT_MEMBER_REQUIRED', 'TENANT_PERMISSION_DENIED', 'FORBIDDEN'] }, 'platformAdmin.status': 'pass', 'platformAdmin.result.guards.studentPlatformDenied': { oneOf: ['PLATFORM_ADMIN_REQUIRED', 'FORBIDDEN'] }, }, }, { id: 'api.integration', label: 'API integration regression', commandIncludes: 'test:api', summary: { failed: 0 }, }, { id: 'worker.assets', label: 'Asset worker security regression', commandIncludes: 'test:worker:assets', summary: { failed: 0 }, }, { id: 'worker.commerce', label: 'Commerce/payment worker regression', commandIncludes: 'test:worker:commerce', summary: { failed: 0 }, }, { id: 'worker.platform-billing', label: 'Platform subscription billing worker regression', commandIncludes: 'test:worker:platform-billing', summary: { failed: 0 }, }, { id: 'worker.platform-dunning', label: 'Platform invoice dunning worker regression', commandIncludes: 'test:worker:platform-dunning', summary: { failed: 0 }, }, { id: 'worker.imports', label: 'Async import worker regression', commandIncludes: 'test:worker:imports', summary: { failed: 0 }, }, { id: 'worker.public-banks', label: 'Public question bank worker regression', commandIncludes: 'test:worker:public-banks', summary: { failed: 0 }, }, { id: 'taro.check', label: 'Taro type/security guardrails', commandIncludes: 'check:taro', summary: { failed: 0 }, }, { id: 'taro.supply-chain', label: 'Taro secured bundle dependencies and reviewed toolchain risk', commandIncludes: 'audit:taro:supply-chain', artifactSummary: taroSupplyChainArtifactSummary, summary: { schemaVersion: 1, status: 'pass-with-reviewed-toolchain-risk', 'securedBundleDependencies.swiper': '12.1.2', 'securedBundleDependencies.lodash-es': '4.18.1', 'audit.counts.critical': { lte: 3 }, 'audit.counts.high': { lte: 10 }, reviewedInvalidEdgeCount: 4, riskBoundaryPresent: true, riskControlCount: { gte: 1 }, }, }, { id: 'taro.build.student', label: 'Student H5 build', commandIncludes: 'build:taro:h5:student', summary: { failed: 0 }, }, { id: 'taro.build.tenant', label: 'Tenant admin H5 build', commandIncludes: 'build:taro:h5:tenant', summary: { failed: 0 }, }, { id: 'taro.build.platform', label: 'Platform admin H5 build', commandIncludes: 'build:taro:h5:platform', summary: { failed: 0 }, }, { id: 'taro.h5-static-smoke', label: 'Taro H5 static startup smoke', commandIncludes: 'smoke:taro:h5', artifactSummary: nestedJsonSummaryArtifact, summary: { fail: 0, portals: 3, tenantResolveRequests: 3, }, }, { id: 'taro.h5-interaction-smoke', label: 'Taro H5 real browser interaction smoke', commandIncludes: 'smoke:taro:h5:interaction', artifactSummary: h5InteractionArtifactSummary, summary: { fail: 0, pass: { gte: 32 }, 'mockApi.keyRequests.answers': { gte: 1 }, 'mockApi.keyRequests.favorites': { gte: 1 }, 'mockApi.keyRequests.ordersCreated': { gte: 1 }, 'mockApi.keyRequests.paymentsCreated': { gte: 1 }, 'mockApi.keyRequests.orderStatus': { gte: 1 }, 'mockApi.keyRequests.tenantResolve': { gte: 3 }, 'mockApi.keyRequests.tenantContentWrites': { gte: 1 }, 'mockApi.keyRequests.tenantAdminWrites': { gte: 1 }, 'mockApi.keyRequests.crmWrites': { gte: 1 }, 'mockApi.keyRequests.commissionWrites': { gte: 1 }, 'mockApi.keyRequests.platformAdminWrites': { gte: 1 }, }, }, { id: 'taro.h5-release-guardrails', label: 'Taro H5 release artifact guardrails', commandIncludes: 'taro-h5-release-guardrails-test.js', artifactSummary: nestedJsonSummaryArtifact, summary: { fail: 0, warn: 0, }, }, { id: 'taro.h5-release-manifest', label: 'Taro H5 release deployment manifest', commandIncludes: 'manifest:taro:h5', artifactSummary: nestedJsonSummaryArtifact, summary: { fail: 0, warn: 0, portals: 3, distReady: 3, runtimeConfigs: 3, treeHashes: 3, }, }, { id: 'audit.runtime', label: 'Runtime dependency audit', commandIncludes: 'audit:runtime', summary: { critical: 0, high: 0 }, }, { id: 'security.repo-scan', label: 'Repository static security scan', commandIncludes: 'security:repo', artifactSummary: nestedJsonSummaryArtifact, summary: { critical: 0, high: 0 }, }, { id: 'security.codex-scan', label: 'Codex Security scan', commandIncludes: 'codex-security', summary: { critical: 0, high: 0 }, }, ]; const requiredAttestations = [ { id: 'backup.snapshot', label: 'Production database backup/snapshot is prepared', }, { id: 'rollback.plan', label: 'Rollback plan and old PocketBase read-only snapshot are prepared', }, { id: 'migration.sampling', label: 'Real migrated users/questions/orders/assets were sampled', }, { id: 'provider.production-accounts', label: 'SMS/OAuth/payment production accounts and callback domains were verified', }, { id: 'object-storage.production-controls', label: 'Object storage AV/content scan, CDN boundary, watermark and lifecycle controls were verified', }, { id: 'payment.reconciliation-sampling', label: 'Real payment/refund bill samples were reconciled', }, { id: 'frontend.runtime-config-review', label: 'Three H5 runtime-config.json files were reviewed for public-only values', }, ]; const weappGateChecks = [ { id: 'taro.build.weapp-student', label: 'Student production WeApp build', commandIncludes: 'build:taro:weapp:student:production', summary: { failed: 0 }, }, { id: 'taro.weapp-release-guardrails', label: 'Student WeApp release artifact guardrails', commandIncludes: 'taro-weapp-release-guardrails.js --production', artifactSummary: nestedJsonSummaryArtifact, summary: { fail: 0, warn: 0 }, }, ]; const weappAttestations = [ { id: 'frontend.weapp-devtools-device-review', label: 'Student WeApp was verified in WeChat DevTools and on a real device', }, ]; const h5ReleaseDirectories = new Map([ ['student', 'apps/taro/dist/h5-student'], ['tenant-admin', 'apps/taro/dist/h5-tenant-admin'], ['platform-admin', 'apps/taro/dist/h5-platform-admin'], ]); function parseArgs(argv) { const options = { evidencePath: defaultEvidencePath, json: false, maxAgeDays: defaultMaxAgeDays, allowStale: false, verifyLiveH5: ['1', 'true', 'yes', 'on'].includes(String(process.env.LAUNCH_GATE_VERIFY_LIVE_H5 || '').trim().toLowerCase()), liveTimeoutMs: Number(process.env.LAUNCH_GATE_LIVE_TIMEOUT_MS || defaultLiveTimeoutMs), }; for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--json') options.json = true; else if (arg === '--allow-stale') options.allowStale = true; else if (arg === '--verify-live-h5') options.verifyLiveH5 = true; else if (arg === '--no-verify-live-h5') options.verifyLiveH5 = false; else if (arg === '--evidence') { options.evidencePath = path.resolve(process.cwd(), argv[index + 1] || ''); index += 1; } else if (arg.startsWith('--evidence=')) { options.evidencePath = path.resolve(process.cwd(), arg.slice('--evidence='.length)); } else if (arg === '--max-age-days') { options.maxAgeDays = Number(argv[index + 1]); index += 1; } else if (arg.startsWith('--max-age-days=')) { options.maxAgeDays = Number(arg.slice('--max-age-days='.length)); } else if (arg === '--live-timeout-ms') { options.liveTimeoutMs = Number(argv[index + 1]); index += 1; } else if (arg.startsWith('--live-timeout-ms=')) { options.liveTimeoutMs = Number(arg.slice('--live-timeout-ms='.length)); } } if (!Number.isFinite(options.maxAgeDays) || options.maxAgeDays <= 0) options.maxAgeDays = defaultMaxAgeDays; if (!Number.isFinite(options.liveTimeoutMs) || options.liveTimeoutMs < 1_000 || options.liveTimeoutMs > 60_000) { options.liveTimeoutMs = defaultLiveTimeoutMs; } return options; } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function sha256File(filePath) { return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); } function sha256Buffer(value) { return crypto.createHash('sha256').update(value).digest('hex'); } function productionUrlFailure(value) { const text = String(value || '').trim(); if (!text) return 'URL is missing'; let parsed; try { parsed = new URL(text); } catch { return 'URL is invalid'; } if (parsed.protocol !== 'https:') return 'URL must use HTTPS'; if (parsed.username || parsed.password) return 'URL must not include credentials'; if (parsed.hash) return 'URL must not include a fragment'; if (/[<>]|replace(?:-with)?|placeholder|your[-_. ]?(?:domain|host|url)/i.test(text)) { return 'URL contains a placeholder value'; } const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, ''); const placeholderHost = hostname === 'example.com' || hostname.endsWith('.example.com') || hostname === 'example' || hostname.endsWith('.example') || hostname.endsWith('.test') || hostname.endsWith('.invalid') || /(^|\.)(?:replace(?:-with)?|placeholder|your-domain|your-host)(?:\.|$)/i.test(hostname); if (placeholderHost) return 'URL uses a placeholder or reserved hostname'; const localHost = hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1' || hostname === '0.0.0.0' || hostname.startsWith('127.'); if (localHost) return 'URL must not resolve to a local development hostname'; return ''; } function normalizedUrl(value) { const parsed = new URL(String(value || '').trim()); parsed.hash = ''; parsed.search = ''; return parsed.href.replace(/\/+$/, ''); } function portalEvidence(items, portal) { if (Array.isArray(items)) return items.find(item => item?.portal === portal) || null; if (items && typeof items === 'object') return items[portal] || null; return null; } function appAssetPathFromIndex(indexHtml) { const sources = [...String(indexHtml || '').matchAll(/]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map(match => match[1]); return sources.find(source => /(?:^|\/)app(?:\.[^/]+)?\.js(?:\?|$)/i.test(source)) || sources.at(-1) || ''; } function normalizeAssetPath(value, baseUrl) { const resolved = new URL(String(value || ''), baseUrl); return `${resolved.pathname}${resolved.search}`; } function validateHash(value) { return /^[0-9a-f]{64}$/i.test(String(value || '')); } function normalizeStatus(value) { return String(value || '').trim().toLowerCase(); } function isApproved(value) { return ['approved', 'pass', 'passed', 'ok', 'complete', 'completed'].includes(normalizeStatus(value)); } function findById(items, id) { return (Array.isArray(items) ? items : []).find(item => item && item.id === id); } function resolveArtifact(evidencePath, artifact) { if (!artifact) return ''; if (path.isAbsolute(artifact)) return artifact; return path.resolve(path.dirname(evidencePath), artifact); } function daysSince(value, now = Date.now()) { const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return Number.POSITIVE_INFINITY; return (now - timestamp) / (24 * 60 * 60 * 1000); } function hasPath(object, keyPath) { const parts = keyPath.split('.'); let cursor = object; for (const part of parts) { if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return false; cursor = cursor[part]; } return true; } function valueAt(object, keyPath) { const parts = keyPath.split('.'); let cursor = object; for (const part of parts) cursor = cursor?.[part]; return cursor; } function compareSummary(actualSummary, expectedSummary) { const failures = []; for (const [key, expectedValue] of Object.entries(expectedSummary || {})) { if (!hasPath(actualSummary || {}, key)) { failures.push(`${key} is missing`); continue; } const actualValue = valueAt(actualSummary, key); const comparisonFailure = compareExpectedValue(actualValue, expectedValue); if (comparisonFailure) { failures.push(`${key} ${comparisonFailure}`); } } return failures; } function directJsonArtifactSummary(payload) { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new Error('artifact must contain a JSON object'); } return payload; } function nestedJsonSummaryArtifact(payload) { const artifact = directJsonArtifactSummary(payload); return directJsonArtifactSummary(artifact.summary); } function pocketBaseDryRunArtifactSummary(payload) { const artifact = directJsonArtifactSummary(payload); const summary = directJsonArtifactSummary(artifact.summary); const requiredCollections = artifact.migrationReadiness?.requiredCollections; const criticalFieldCoverage = artifact.migrationReadiness?.criticalFieldCoverage; if (!Array.isArray(requiredCollections) || !Array.isArray(criticalFieldCoverage)) { throw new Error('artifact must contain migrationReadiness requiredCollections and criticalFieldCoverage arrays'); } return { blocker: Number(summary.blockers), warning: Number(summary.warnings), migrationProfile: artifact.migrationProfile, requiredCollectionsMissing: requiredCollections.filter(item => item?.present !== true).length, criticalFieldCoverageWarnings: criticalFieldCoverage.filter(item => item?.present !== true).length, }; } function h5InteractionArtifactSummary(payload) { const artifact = directJsonArtifactSummary(payload); const summary = directJsonArtifactSummary(artifact.summary); return { ...summary, mockApi: artifact.mockApi, }; } function latestRepositoryMigrationVersion() { const migrationsDir = path.resolve(process.cwd(), 'supabase', 'migrations'); if (!fs.existsSync(migrationsDir)) throw new Error(`repository migration directory is missing: ${migrationsDir}`); const versions = fs.readdirSync(migrationsDir) .filter(file => file.endsWith('.sql')) .map(file => /^(\d+)_.*\.sql$/.exec(file)?.[1] || '') .filter(Boolean) .sort((left, right) => left.length - right.length || left.localeCompare(right)); if (versions.length === 0) throw new Error('repository contains no numeric Supabase migrations'); return versions.at(-1); } function migrationHistoryArtifactSummary(payload, context = {}) { const artifact = directJsonArtifactSummary(payload); const latestRepositoryMigration = String(artifact.latestRepositoryMigration || '').trim(); const latestAppliedMigration = String(artifact.latestAppliedMigration || '').trim(); const repositoryLatest = latestRepositoryMigrationVersion(); if (latestRepositoryMigration !== repositoryLatest) { throw new Error(`artifact repository migration ${latestRepositoryMigration || '(missing)'} does not match ${repositoryLatest}`); } if (!/^\d+$/.test(latestAppliedMigration) || compareMigrationVersion(latestAppliedMigration, repositoryLatest) < 0) { throw new Error(`artifact applied migration ${latestAppliedMigration || '(missing)'} is older than ${repositoryLatest}`); } if (!Array.isArray(artifact.missingMigrations)) throw new Error('artifact missingMigrations must be an array'); const readinessArtifactSha256 = requireSha256(artifact.readinessArtifactSha256, 'readinessArtifactSha256'); const readinessEvidence = findById(context.evidence?.checks, 'readiness.production.db'); if (!readinessEvidence?.artifact) throw new Error('readiness.production.db artifact reference is missing'); const readinessArtifactPath = resolveArtifact(context.options?.evidencePath || '', readinessEvidence.artifact); if (!fs.existsSync(readinessArtifactPath) || !fs.statSync(readinessArtifactPath).isFile()) { throw new Error('readiness.production.db artifact file is missing'); } const actualReadinessSha256 = sha256File(readinessArtifactPath); if (readinessArtifactSha256 !== actualReadinessSha256) { throw new Error('readinessArtifactSha256 does not match the readiness.production.db artifact'); } return { status: artifact.status, failed: Number(artifact.failed), schemaVersion: Number(artifact.schemaVersion), latestRepositoryMigration, latestAppliedMigration, missingMigrations: artifact.missingMigrations, readinessArtifactSha256, }; } function compareMigrationVersion(left, right) { const normalizedLeft = String(left || '').replace(/^0+(?=\d)/, ''); const normalizedRight = String(right || '').replace(/^0+(?=\d)/, ''); if (normalizedLeft.length !== normalizedRight.length) return normalizedLeft.length > normalizedRight.length ? 1 : -1; return normalizedLeft === normalizedRight ? 0 : normalizedLeft > normalizedRight ? 1 : -1; } function requireSha256(value, field) { const normalized = String(value || '').toLowerCase(); if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${field} must be a SHA-256 value`); return normalized; } function platformAdminBootstrapArtifactSummary(payload) { const artifact = directJsonArtifactSummary(payload); const adminUserIdSha256 = requireSha256(artifact.adminUserIdSha256, 'adminUserIdSha256'); const authSmokeExpectedUserIdSha256 = requireSha256( artifact.authSmokeExpectedUserIdSha256, 'authSmokeExpectedUserIdSha256', ); return { status: artifact.status, failed: Number(artifact.failed), schemaVersion: Number(artifact.schemaVersion), dryRunVerified: artifact.dryRunVerified === true, applied: artifact.applied === true, adminUserIdSha256, authSmokeExpectedUserIdSha256, identityMatches: adminUserIdSha256 === authSmokeExpectedUserIdSha256, auditEvent: artifact.auditEvent, auditVerified: artifact.auditVerified === true, dryRunArtifactSha256: requireSha256(artifact.dryRunArtifactSha256, 'dryRunArtifactSha256'), applyArtifactSha256: requireSha256(artifact.applyArtifactSha256, 'applyArtifactSha256'), authSmokeArtifactSha256: requireSha256(artifact.authSmokeArtifactSha256, 'authSmokeArtifactSha256'), auditArtifactSha256: requireSha256(artifact.auditArtifactSha256, 'auditArtifactSha256'), }; } function backupRestoreDrillArtifactSummary(payload) { const artifact = directJsonArtifactSummary(payload); const snapshotId = String(artifact.snapshotId || '').trim(); const restoreTarget = String(artifact.restoreTarget || '').trim(); if (!snapshotId || !restoreTarget) throw new Error('artifact must identify the snapshot and isolated restore target'); const rtoMinutes = Number(artifact.rtoMinutes); const rpoMinutes = Number(artifact.rpoMinutes); if (!Number.isFinite(rtoMinutes) || !Number.isFinite(rpoMinutes)) { throw new Error('artifact rtoMinutes and rpoMinutes must be numeric'); } return { status: artifact.status, failed: Number(artifact.failed), schemaVersion: Number(artifact.schemaVersion), snapshotId, restoreTarget, isolated: artifact.isolated === true, integrityVerified: artifact.integrityVerified === true, rtoMinutes, rpoMinutes, verificationArtifactSha256: requireSha256( artifact.verificationArtifactSha256, 'verificationArtifactSha256', ), }; } function taroSupplyChainArtifactSummary(payload) { const artifact = directJsonArtifactSummary(payload); const securedBundleDependencies = directJsonArtifactSummary(artifact.securedBundleDependencies); const audit = directJsonArtifactSummary(artifact.audit); const counts = directJsonArtifactSummary(audit.counts); const riskBoundary = directJsonArtifactSummary(artifact.riskBoundary); if (!Array.isArray(artifact.npmLs?.edges)) throw new Error('artifact npmLs.edges must be an array'); const expectedEdges = new Set([ '@tarojs/components>swiper@11.1.15', '@tarojs/components-react>swiper@11.1.15', '@tarojs/plugin-platform-h5>lodash-es@4.17.21', '@tarojs/taro-h5>lodash-es@4.17.21', ]); const actualEdges = new Set(artifact.npmLs.edges.map(edge => `${edge?.parent}>${edge?.dependency}@${edge?.declared}`)); if (actualEdges.size !== expectedEdges.size || [...expectedEdges].some(edge => !actualEdges.has(edge))) { throw new Error('artifact npmLs.edges does not match the four reviewed Taro exact-dependency edges'); } if (!String(riskBoundary.appliesTo || '').trim()) throw new Error('artifact riskBoundary.appliesTo is required'); if (!Array.isArray(riskBoundary.controls)) throw new Error('artifact riskBoundary.controls must be an array'); return { schemaVersion: Number(artifact.schemaVersion), status: artifact.status, securedBundleDependencies, audit: { counts }, reviewedInvalidEdgeCount: actualEdges.size, riskBoundaryPresent: true, riskControlCount: riskBoundary.controls.filter(item => String(item || '').trim()).length, }; } function tenantStudentCapacityArtifactSummary(payload) { if (payload?.schemaVersion !== 1 || payload?.kind !== 'tenant-student-capacity') { throw new Error('artifact is not tenant-student-capacity schemaVersion=1'); } const cases = Array.isArray(payload.cases) ? payload.cases : []; const byId = new Map(cases.map(item => [item?.id, item])); const firstPage = byId.get('first-page'); const deepCursor = byId.get('deep-cursor'); const searchCases = ['name-substring', 'phone-substring', 'email-substring'].map(id => byId.get(id)); if (!firstPage || !deepCursor || searchCases.some(item => !item)) { throw new Error('artifact must contain first-page, deep-cursor and three substring search cases'); } const caseIndexes = item => Array.isArray(item?.explain?.summary?.indexes) ? item.explain.summary.indexes : []; const searchP95Values = searchCases.map(item => Number(item?.latencyMs?.p95)); if (searchP95Values.some(value => !Number.isFinite(value))) { throw new Error('artifact search cases must contain numeric latencyMs.p95 values'); } const remaining = payload.cleanup?.remaining || {}; const remainingManagedUsers = [remaining.tenants, remaining.platformUsers, remaining.memberships, remaining.profiles] .reduce((sum, value) => sum + Number(value || 0), 0); return { databaseEnvironment: payload.safety?.databaseEnvironment || '', fixture: payload.fixture || {}, caseCount: cases.length, deepCursorApproximateOffset: Number(payload.config?.deepCursorApproximateOffset || 0), firstPageP95Ms: Number(firstPage?.latencyMs?.p95), deepCursorP95Ms: Number(deepCursor?.latencyMs?.p95), searchP95MaxMs: Math.max(...searchP95Values), keysetIndexUsed: [firstPage, deepCursor].every(item => caseIndexes(item).includes('idx_memberships_student_keyset_page')), trigramIndexUsed: searchCases.every(item => caseIndexes(item).includes('idx_platform_users_identity_search_trgm')), cleanupVerified: payload.cleanup?.cleanupVerified === true, remainingManagedUsers, }; } function validateStructuredArtifact(spec, item, artifactPath, evidence, options, collector) { if (typeof spec.artifactSummary !== 'function') return; let payload; try { payload = readJson(artifactPath); } catch (error) { collector.block(`check.${spec.id}.artifact_json`, `${spec.label} artifact must be valid JSON`, { artifact: item.artifact, error: error instanceof Error ? error.message : String(error), }); return; } let derived; try { derived = spec.artifactSummary(payload, { evidence, options, item, artifactPath }); } catch (error) { collector.block(`check.${spec.id}.artifact_summary`, `${spec.label} artifact structure is invalid`, { artifact: item.artifact, error: error instanceof Error ? error.message : String(error), }); return; } const gateFailures = compareSummary(derived, spec.summary); const evidenceFailures = []; for (const key of Object.keys(spec.summary || {})) { if (!hasPath(item.summary || {}, key)) { evidenceFailures.push(`${key} is missing from evidence summary`); continue; } if (!hasPath(derived, key) || JSON.stringify(valueAt(item.summary, key)) !== JSON.stringify(valueAt(derived, key))) { evidenceFailures.push(`${key} does not match the artifact`); } } const comparablePaths = new Set(Object.keys(spec.summary || {})); for (const key of leafPaths(item.summary || {})) { if ([...comparablePaths].some(pathKey => key === pathKey || key.startsWith(`${pathKey}.`))) continue; evidenceFailures.push(`${key} is not derived from an artifact-backed gate field`); } if (gateFailures.length || evidenceFailures.length) { collector.block(`check.${spec.id}.artifact_summary`, `${spec.label} artifact does not satisfy or match evidence`, { gateFailures, evidenceFailures, derived, }); } else { collector.pass(`check.${spec.id}.artifact_summary`, `${spec.label} artifact directly satisfies and matches evidence`); } } function leafPaths(object, prefix = '') { if (!object || typeof object !== 'object' || Array.isArray(object)) return prefix ? [prefix] : []; const paths = []; for (const [key, value] of Object.entries(object)) { const current = prefix ? `${prefix}.${key}` : key; if (value && typeof value === 'object' && !Array.isArray(value)) paths.push(...leafPaths(value, current)); else paths.push(current); } return paths; } function successSentinel(spec) { return `TIKU_LAUNCH_GATE_SUCCESS:${spec.id}`; } function validateLogArtifact(spec, item, artifactPath, collector) { if (typeof spec.artifactSummary === 'function') return; const sentinel = successSentinel(spec); const content = fs.readFileSync(artifactPath, 'utf8'); if (!content.split(/\r?\n/).some(line => line.trim() === sentinel)) { collector.block(`check.${spec.id}.artifact_success`, `${spec.label} artifact is missing its explicit success sentinel`, { artifact: item.artifact, expectedSentinel: sentinel, }); } else { collector.pass(`check.${spec.id}.artifact_success`, `${spec.label} artifact contains its explicit success sentinel`); } } function isComparatorSpec(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; return ['eq', 'lt', 'lte', 'gt', 'gte', 'oneOf', 'truthy'].some(key => Object.prototype.hasOwnProperty.call(value, key)); } function compareExpectedValue(actualValue, expectedValue) { if (!isComparatorSpec(expectedValue)) { if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) { return `expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`; } return ''; } if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq') && actualValue !== expectedValue.eq) { return `expected ${JSON.stringify(expectedValue.eq)} but got ${JSON.stringify(actualValue)}`; } if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) { const choices = Array.isArray(expectedValue.oneOf) ? expectedValue.oneOf : []; if (!choices.includes(actualValue)) { return `expected one of ${JSON.stringify(choices)} but got ${JSON.stringify(actualValue)}`; } } if (Object.prototype.hasOwnProperty.call(expectedValue, 'truthy')) { const shouldBeTruthy = Boolean(expectedValue.truthy); if (Boolean(actualValue) !== shouldBeTruthy) { return `expected ${shouldBeTruthy ? 'truthy' : 'falsy'} but got ${JSON.stringify(actualValue)}`; } } const numericChecks = [ ['lt', (actual, expected) => actual < expected, '<'], ['lte', (actual, expected) => actual <= expected, '<='], ['gt', (actual, expected) => actual > expected, '>'], ['gte', (actual, expected) => actual >= expected, '>='], ]; for (const [key, predicate, label] of numericChecks) { if (!Object.prototype.hasOwnProperty.call(expectedValue, key)) continue; const actualNumber = Number(actualValue); const expectedNumber = Number(expectedValue[key]); if (!Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || !predicate(actualNumber, expectedNumber)) { return `expected ${label} ${expectedNumber} but got ${JSON.stringify(actualValue)}`; } } return ''; } function resultCollector() { const checks = []; function pass(id, message, details = {}) { checks.push({ status: 'pass', id, message, details }); } function warn(id, message, details = {}) { checks.push({ status: 'warn', id, message, details }); } function block(id, message, details = {}) { checks.push({ status: 'blocker', id, message, details }); } return { checks, pass, warn, block }; } function validateTopLevel(evidence, collector) { if (evidence.schemaVersion !== 1) { collector.block('evidence.schema_version', 'Evidence schemaVersion must be 1', { actual: evidence.schemaVersion }); } else { collector.pass('evidence.schema_version', 'Evidence schema version is supported'); } if (evidence.environment !== 'production') { collector.block('evidence.environment', 'Evidence environment must be production', { actual: evidence.environment }); } else { collector.pass('evidence.environment', 'Evidence environment is production'); } if (!evidence.commit || !/^[0-9a-f]{7,40}$/i.test(String(evidence.commit))) { collector.block('evidence.commit', 'Evidence must include the reviewed deployment commit hash'); } else { collector.pass('evidence.commit', 'Deployment commit is recorded', { commit: evidence.commit }); } const currentCommit = String(process.env.DEPLOY_COMMIT_SHA || '').trim(); const deployReleaseRoot = String(process.env.DEPLOY_RELEASE_ROOT || '').trim(); if (currentCommit) { const evidenceCommit = String(evidence.commit || '').toLowerCase(); const deployedCommit = currentCommit.toLowerCase(); if (!evidenceCommit.startsWith(deployedCommit) && !deployedCommit.startsWith(evidenceCommit)) { collector.block('evidence.commit_match', 'Evidence commit does not match the commit being deployed', { evidenceCommit: evidence.commit, deployedCommit: currentCommit, }); } else { collector.pass('evidence.commit_match', 'Evidence commit matches the commit being deployed'); } if (!deployReleaseRoot) { collector.block('evidence.release_root', 'Deployment gate must provide DEPLOY_RELEASE_ROOT for artifact verification'); } else if (!fs.existsSync(deployReleaseRoot) || !fs.statSync(deployReleaseRoot).isDirectory()) { collector.block('evidence.release_root', 'DEPLOY_RELEASE_ROOT must be an existing candidate release directory', { deployReleaseRoot, }); } else { collector.pass('evidence.release_root', 'Candidate release directory is available for artifact verification', { deployReleaseRoot, }); } } const target = evidence.target || {}; const requiredTargets = ['apiBaseUrl', 'studentH5Url', 'tenantAdminH5Url', 'platformAdminH5Url']; for (const key of requiredTargets) { const value = String(target[key] || '').trim(); const failure = productionUrlFailure(value); if (failure) { collector.block(`target.${key}`, `${key} must be a real HTTPS production URL`, { value, failure }); } else { collector.pass(`target.${key}`, `${key} is a valid production HTTPS URL`); } } const releaseTargets = Array.isArray(evidence.releaseTargets) ? evidence.releaseTargets : ['h5']; const invalidTargets = releaseTargets.filter(item => !['h5', 'weapp'].includes(item)); if (!releaseTargets.includes('h5') || invalidTargets.length) { collector.block('evidence.release_targets', 'releaseTargets must include h5 and may optionally include weapp', { releaseTargets }); } else { collector.pass('evidence.release_targets', 'Release targets are explicit', { releaseTargets }); } } function validateH5ReleaseManifest(artifactPath, collector) { const deployReleaseRoot = String(process.env.DEPLOY_RELEASE_ROOT || '').trim(); if (!deployReleaseRoot) return; let manifest; try { manifest = readJson(artifactPath); } catch (error) { collector.block('check.taro.h5-release-manifest.release_tree', 'H5 release manifest artifact must be valid JSON', { artifactPath, error: error.message, }); return; } const failures = []; for (const [portal, relativeDir] of h5ReleaseDirectories) { const portalManifest = (Array.isArray(manifest.portals) ? manifest.portals : []) .find(item => item?.portal === portal); const expectedSha256 = String(portalManifest?.dist?.treeSha256 || '').toLowerCase(); const candidateDir = path.resolve(deployReleaseRoot, relativeDir); if (!/^[0-9a-f]{64}$/.test(expectedSha256)) { failures.push(`${portal}: manifest treeSha256 is missing or invalid`); continue; } if (!fs.existsSync(candidateDir) || !fs.statSync(candidateDir).isDirectory()) { failures.push(`${portal}: candidate directory is missing (${candidateDir})`); continue; } const actual = hashArtifactDirectory(candidateDir); if (actual.sha256 !== expectedSha256) { failures.push(`${portal}: expected ${expectedSha256}, got ${actual.sha256}`); } } if (failures.length > 0) { collector.block('check.taro.h5-release-manifest.release_tree', 'Candidate H5 files do not match the reviewed release manifest', { failures, }); } else { collector.pass('check.taro.h5-release-manifest.release_tree', 'Candidate H5 files match all reviewed release tree hashes'); } } async function fetchLiveResource(url, options) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), options.liveTimeoutMs); try { const fetchImpl = options.fetchImpl || fetch; return await fetchImpl(url, { redirect: 'follow', signal: controller.signal, headers: { accept: '*/*', 'cache-control': 'no-cache', 'user-agent': 'tiku-production-launch-gate/1', }, }); } finally { clearTimeout(timeout); } } async function liveResponseBytes(url, options, collector, id) { let response; try { response = await fetchLiveResource(url, options); } catch (error) { collector.block(id, 'Live H5 request failed', { url, error: error instanceof Error ? error.message : String(error) }); return null; } const finalUrl = response.url || url; const finalUrlFailure = productionUrlFailure(finalUrl); if (finalUrlFailure) { collector.block(id, 'Live H5 request redirected to a non-production URL', { requestedUrl: url, finalUrl, failure: finalUrlFailure, }); return null; } if (!response.ok) { collector.block(id, 'Live H5 request returned a non-success status', { url, finalUrl, status: response.status }); return null; } const bytes = Buffer.from(await response.arrayBuffer()); if (bytes.length === 0) { collector.block(id, 'Live H5 response is empty', { url, finalUrl }); return null; } collector.pass(id, 'Live H5 resource returned a non-empty success response', { url, finalUrl, status: response.status, bytes: bytes.length, }); return { bytes, response }; } function releaseManifestPortals(evidence, options, collector) { const liveH5 = evidence.liveH5 || {}; if (!liveH5.releaseManifestArtifact) return null; const manifestPath = resolveArtifact(options.evidencePath, liveH5.releaseManifestArtifact); if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile() || fs.statSync(manifestPath).size === 0) { collector.block('live_h5.release_manifest', 'Live H5 release manifest artifact is missing or empty', { artifact: liveH5.releaseManifestArtifact, }); return null; } if (!validateHash(liveH5.releaseManifestSha256)) { collector.block('live_h5.release_manifest_hash', 'Live H5 release manifest must record artifact SHA-256', { artifact: liveH5.releaseManifestArtifact, }); return null; } const actualHash = sha256File(manifestPath); if (actualHash !== String(liveH5.releaseManifestSha256).toLowerCase()) { collector.block('live_h5.release_manifest_hash', 'Live H5 release manifest hash does not match evidence', { artifact: liveH5.releaseManifestArtifact, expectedSha256: liveH5.releaseManifestSha256, actualSha256: actualHash, }); return null; } let manifest; try { manifest = readJson(manifestPath); } catch (error) { collector.block('live_h5.release_manifest', 'Live H5 release manifest is not valid JSON', { artifact: liveH5.releaseManifestArtifact, error: error instanceof Error ? error.message : String(error), }); return null; } if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.portals)) { collector.block('live_h5.release_manifest', 'Live H5 release manifest has an unsupported structure', { artifact: liveH5.releaseManifestArtifact, }); return null; } collector.pass('live_h5.release_manifest', 'Live H5 release manifest artifact is valid'); collector.pass('live_h5.release_manifest_hash', 'Live H5 release manifest hash matches evidence'); return manifest.portals; } function expectedPortalHashes(evidence, manifestPortals, portal, options, collector) { const direct = portalEvidence(evidence.liveH5?.portals, portal) || {}; const manifestPortal = portalEvidence(manifestPortals, portal) || {}; let indexSha256 = direct.indexSha256 || manifestPortal.dist?.indexSha256 || ''; let appSha256 = direct.appSha256 || ''; let appPath = direct.appPath || ''; if (manifestPortal.dist) { const deployReleaseRoot = String(options.deployReleaseRoot || process.env.DEPLOY_RELEASE_ROOT || '').trim(); const expectedRelativeDir = h5ReleaseDirectories.get(portal); const candidateDir = expectedRelativeDir ? path.resolve(deployReleaseRoot || process.cwd(), expectedRelativeDir) : ''; if (!candidateDir) { collector.block(`live_h5.${portal}.candidate_files`, 'Live H5 portal has no fixed candidate directory mapping', { portal, }); return null; } const candidateIndexPath = path.join(candidateDir, 'index.html'); if (!fs.existsSync(candidateIndexPath) || !fs.statSync(candidateIndexPath).isFile()) { collector.block(`live_h5.${portal}.candidate_files`, 'Release manifest candidate index is unavailable for strict live verification', { candidateIndexPath, }); return null; } const candidateIndexSha256 = sha256File(candidateIndexPath); if (indexSha256 && candidateIndexSha256 !== String(indexSha256).toLowerCase()) { collector.block(`live_h5.${portal}.candidate_files`, 'Release manifest index hash does not match the candidate file', { candidateIndexPath, expectedSha256: indexSha256, actualSha256: candidateIndexSha256, }); return null; } indexSha256 = candidateIndexSha256; const candidateIndexHtml = fs.readFileSync(candidateIndexPath, 'utf8'); const candidateAppPath = appAssetPathFromIndex(candidateIndexHtml); if (!candidateAppPath) { collector.block(`live_h5.${portal}.candidate_files`, 'Release manifest candidate index does not reference an app bundle', { candidateIndexPath, }); return null; } const candidateAppUrl = new URL(candidateAppPath, 'https://candidate.invalid/index.html'); const candidateAppFile = path.resolve(candidateDir, `.${candidateAppUrl.pathname}`); const relativeCandidateApp = path.relative(candidateDir, candidateAppFile); if (relativeCandidateApp.startsWith('..') || path.isAbsolute(relativeCandidateApp) || !fs.existsSync(candidateAppFile) || !fs.statSync(candidateAppFile).isFile()) { collector.block(`live_h5.${portal}.candidate_files`, 'Release manifest candidate app bundle is unavailable', { candidateAppFile, }); return null; } const candidateAppSha256 = sha256File(candidateAppFile); if (appSha256 && candidateAppSha256 !== String(appSha256).toLowerCase()) { collector.block(`live_h5.${portal}.candidate_files`, 'Direct app hash does not match the release manifest candidate bundle', { candidateAppFile, expectedSha256: appSha256, actualSha256: candidateAppSha256, }); return null; } appPath = candidateAppPath; appSha256 = candidateAppSha256; collector.pass(`live_h5.${portal}.candidate_files`, 'Release manifest is bound to the local candidate index and app bundle', { candidateIndexPath, candidateAppFile, }); } if (!validateHash(indexSha256) && !validateHash(appSha256)) { collector.block(`live_h5.${portal}.candidate_hash`, 'Live H5 evidence must record a candidate index or app SHA-256', { portal, source: manifestPortals ? 'release-manifest-or-direct' : 'direct', }); return null; } if (appSha256 && !validateHash(appSha256)) { collector.block(`live_h5.${portal}.app_hash`, 'Live H5 appSha256 must be a SHA-256 value', { portal }); return null; } if (indexSha256 && !validateHash(indexSha256)) { collector.block(`live_h5.${portal}.index_hash`, 'Live H5 indexSha256 must be a SHA-256 value', { portal }); return null; } return { indexSha256: String(indexSha256).toLowerCase(), appSha256: String(appSha256).toLowerCase(), appPath, }; } async function validateLiveH5(evidence, options, collector) { if (!options.verifyLiveH5) return; const target = evidence.target || {}; const apiBaseUrl = String(target.apiBaseUrl || '').trim(); if (productionUrlFailure(apiBaseUrl)) { collector.block('live_h5.target', 'Strict live H5 validation requires valid production target URLs'); return; } const manifestPortals = releaseManifestPortals(evidence, options, collector); for (const spec of h5PortalTargets) { const baseUrl = String(target[spec.targetKey] || '').trim(); if (productionUrlFailure(baseUrl)) { collector.block(`live_h5.${spec.portal}.target`, 'Strict live H5 validation requires a valid portal URL', { baseUrl }); continue; } const expected = expectedPortalHashes(evidence, manifestPortals, spec.portal, options, collector); const indexUrl = `${normalizedUrl(baseUrl)}/index.html`; const runtimeUrl = `${normalizedUrl(baseUrl)}/runtime-config.json`; const indexResult = await liveResponseBytes(indexUrl, options, collector, `live_h5.${spec.portal}.index_response`); const runtimeResult = await liveResponseBytes(runtimeUrl, options, collector, `live_h5.${spec.portal}.runtime_response`); if (!indexResult || !runtimeResult || !expected) continue; const indexContentType = indexResult.response.headers.get('content-type') || ''; const indexHtml = indexResult.bytes.toString('utf8'); if (!/text\/html/i.test(indexContentType) || !/ options.maxAgeDays) { collector.block(`check.${spec.id}.freshness`, `${spec.label} evidence is stale or missing completedAt`, { completedAt: item.completedAt || '', maxAgeDays: options.maxAgeDays, }); } else { collector.pass(`check.${spec.id}.freshness`, `${spec.label} evidence is fresh enough`, { completedAt: item.completedAt || '', }); } if (!item.artifact) { collector.block(`check.${spec.id}.artifact`, `${spec.label} must include a saved artifact/log path`); } else { const artifactPath = resolveArtifact(options.evidencePath, item.artifact); if (!fs.existsSync(artifactPath)) { collector.block(`check.${spec.id}.artifact`, `${spec.label} artifact file is missing`, { artifact: item.artifact }); } else { const stat = fs.statSync(artifactPath); if (!stat.isFile() || stat.size === 0) { collector.block(`check.${spec.id}.artifact`, `${spec.label} artifact must be a non-empty file`, { artifact: item.artifact }); } else { const actualSha256 = sha256File(artifactPath); if (!/^[0-9a-f]{64}$/i.test(String(item.artifactSha256 || ''))) { collector.block(`check.${spec.id}.artifact_hash`, `${spec.label} must record a SHA-256 for its artifact`, { artifact: item.artifact, }); } else if (actualSha256 !== String(item.artifactSha256).toLowerCase()) { collector.block(`check.${spec.id}.artifact_hash`, `${spec.label} artifact hash does not match evidence`, { artifact: item.artifact, expectedSha256: item.artifactSha256, actualSha256, }); } else { collector.pass(`check.${spec.id}.artifact`, `${spec.label} artifact is non-empty`, { artifact: item.artifact, bytes: stat.size }); collector.pass(`check.${spec.id}.artifact_hash`, `${spec.label} artifact hash matches evidence`); if (spec.id === 'taro.h5-release-manifest') validateH5ReleaseManifest(artifactPath, collector); validateStructuredArtifact(spec, item, artifactPath, evidence, options, collector); validateLogArtifact(spec, item, artifactPath, collector); } } } } const summaryFailures = compareSummary(item.summary || {}, spec.summary); if (summaryFailures.length > 0) { collector.block(`check.${spec.id}.summary`, `${spec.label} summary does not satisfy gate`, { failures: summaryFailures }); } else { collector.pass(`check.${spec.id}.summary`, `${spec.label} summary satisfies gate`); } } function validateAttestation(spec, evidence, options, collector) { const item = findById(evidence.attestations, spec.id); if (!item) { collector.block(`attestation.${spec.id}`, `${spec.label} attestation is missing`); return; } if (!isApproved(item.status)) { collector.block(`attestation.${spec.id}.status`, `${spec.label} must be approved`, { status: item.status }); } else { collector.pass(`attestation.${spec.id}.status`, `${spec.label} is approved`); } if (!String(item.approver || '').trim()) { collector.block(`attestation.${spec.id}.approver`, `${spec.label} must include an approver`); } else { collector.pass(`attestation.${spec.id}.approver`, `${spec.label} has an approver`); } const age = daysSince(item.approvedAt); if (!options.allowStale && age > options.maxAgeDays) { collector.block(`attestation.${spec.id}.freshness`, `${spec.label} approval is stale or missing approvedAt`, { approvedAt: item.approvedAt || '', maxAgeDays: options.maxAgeDays, }); } else { collector.pass(`attestation.${spec.id}.freshness`, `${spec.label} approval is fresh enough`, { approvedAt: item.approvedAt || '', }); } } async function validateEvidence(evidence, options) { const collector = resultCollector(); validateTopLevel(evidence, collector); for (const spec of gateChecks) validateGateCheck(spec, evidence, options, collector); for (const spec of requiredAttestations) validateAttestation(spec, evidence, options, collector); if (Array.isArray(evidence.releaseTargets) && evidence.releaseTargets.includes('weapp')) { for (const spec of weappGateChecks) validateGateCheck(spec, evidence, options, collector); for (const spec of weappAttestations) validateAttestation(spec, evidence, options, collector); } await validateLiveH5(evidence, options, collector); return collector.checks; } function summarize(checks) { return checks.reduce( (summary, item) => { summary[item.status] += 1; return summary; }, { blocker: 0, warn: 0, pass: 0 }, ); } function printHuman(options, evidence, checks) { const summary = summarize(checks); console.log('Production launch gate'); console.log(`Evidence: ${options.evidencePath}`); console.log(`Host: ${os.hostname()}`); console.log(`Target API: ${evidence.target?.apiBaseUrl || '(missing)'}`); console.log(`Summary: ${summary.blocker} blocker(s), ${summary.warn} warning(s), ${summary.pass} pass(es)`); for (const item of checks) { const marker = item.status === 'blocker' ? 'BLOCK' : item.status === 'warn' ? 'WARN ' : 'PASS '; console.log(`[${marker}] ${item.id}: ${item.message}`); } } async function main() { const options = parseArgs(process.argv); if (!fs.existsSync(options.evidencePath)) { const message = `Evidence file not found: ${options.evidencePath}`; if (options.json) console.log(JSON.stringify({ summary: { blocker: 1, warn: 0, pass: 0 }, checks: [{ status: 'blocker', id: 'evidence.file', message }] }, null, 2)); else console.error(`${message}\nCreate it from docs/refactor/production-launch-evidence.template.json after running real production checks.`); process.exitCode = 1; return; } const evidence = readJson(options.evidencePath); const checks = await validateEvidence(evidence, options); const summary = summarize(checks); if (options.json) console.log(JSON.stringify({ summary, checks }, null, 2)); else printHuman(options, evidence, checks); if (summary.blocker > 0) process.exitCode = 1; } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { main().catch(error => { const message = error instanceof Error ? error.message : String(error); if (process.argv.includes('--json')) { console.log(JSON.stringify({ summary: { blocker: 1, warn: 0, pass: 0 }, checks: [{ status: 'blocker', id: 'gate.unhandled_error', message }], }, null, 2)); } else { console.error(`Production launch gate failed: ${message}`); } process.exitCode = 1; }); } export { gateChecks, h5PortalTargets, parseArgs, productionUrlFailure, requiredAttestations, validateEvidence, weappGateChecks, weappAttestations, };