diff --git a/.github/workflows/deploy-strategy-switch-console.yml b/.github/workflows/deploy-strategy-switch-console.yml index 62626e99..fd9371c7 100644 --- a/.github/workflows/deploy-strategy-switch-console.yml +++ b/.github/workflows/deploy-strategy-switch-console.yml @@ -1040,6 +1040,23 @@ jobs: if [ "$(git rev-parse HEAD)" != "$RELEASE_SOURCE_SHA" ] || [ "$(git rev-parse 'HEAD^{tree}')" != "$expected_tree" ]; then exit 2; fi exit 0 fi + # Reviewed merged main release: keep all receiver checks and live configuration. + if [ "$RELEASE_SOURCE_SHA" = "c1b737d6e721eb9fe71a12c613fbb49d3fa51563" ]; then + [ "$GITHUB_REF" = "refs/heads/main" ] && [ "$frontend_mode" = true ] \ + && [ "$SYNC_SECRETS" = false ] && [ "$SYNC_STRATEGY_PROFILES" = false ] \ + && [ "$SKIP_BINANCE_RECEIVER_CHECKS" = false ] || exit 2 + control_sha="$(git rev-parse HEAD)" + [ "$control_sha" = "$GITHUB_SHA" ] || exit 2 + git cat-file -e "${RELEASE_SOURCE_SHA}^{commit}" + git merge-base --is-ancestor "$RELEASE_SOURCE_SHA" "$control_sha" + expected_tree="5dcdab977ec72e32f99cd3ef4956cebff90ba107" + [ "$(git rev-parse "${RELEASE_SOURCE_SHA}^{tree}")" = "$expected_tree" ] || exit 2 + [ "$(git rev-parse "${RELEASE_SOURCE_SHA}:web/strategy-switch-console/v2_asset_map.js")" = "b4ef57a6830011f78d0aa0270729e17d05a4d230" ] || exit 2 + git -c advice.detachedHead=false checkout --detach "$RELEASE_SOURCE_SHA" + [ "$(git rev-parse HEAD)" = "$RELEASE_SOURCE_SHA" ] \ + && [ "$(git rev-parse 'HEAD^{tree}')" = "$expected_tree" ] || exit 2 + exit 0 + fi if [ "$frontend_mode" = "true" ]; then if [ "$GITHUB_REF" != "refs/heads/main" ] || [ "$RELEASE_SOURCE_SHA" != "9722414a99298cd7079a88eb8e3e703d50e986de" ] \ || [ "$SYNC_SECRETS" != "false" ] || [ "$SYNC_STRATEGY_PROFILES" != "false" ] \ @@ -1127,6 +1144,15 @@ jobs: if: inputs.release_frontend_assets && inputs.source_sha != '20dad166be317dd0d57e492ed06e56dfb82b7aef' && !inputs.render_and_compare_only run: | set -euo pipefail + reviewed_main="c1b737d6e721eb9fe71a12c613fbb49d3fa51563" + if [ "$(git rev-parse HEAD)" = "$reviewed_main" ]; then + [ "$GITHUB_REF" = "refs/heads/main" ] \ + && [ "$(git rev-parse 'HEAD^{tree}')" = "5dcdab977ec72e32f99cd3ef4956cebff90ba107" ] || exit 2 + [ "$(git hash-object web/strategy-switch-console/v2_asset_map.js)" = "b4ef57a6830011f78d0aa0270729e17d05a4d230" ] || exit 2 + [ -z "$(git diff --name-only "$reviewed_main")" ] \ + && [ -z "$(git ls-files --others --exclude-standard)" ] || exit 2 + exit 0 + fi integrated="0dd2c0b3e73e890bc2c39ab0bae0eeb48c58470a" cpu_candidate="974354b98ebe0cd55931baa03034d887d4fac560" status_candidate="68e3d4b69e706e2100f860c473ec657e9baf05ba" @@ -1603,7 +1629,8 @@ jobs: const statusCandidate = process.env.RELEASE_SOURCE_SHA === '68e3d4b69e706e2100f860c473ec657e9baf05ba'; const cpuCandidate = process.env.RELEASE_SOURCE_SHA === '974354b98ebe0cd55931baa03034d887d4fac560'; const integrated = controlRuntimeCandidate || controlAdminCandidate || historyCandidate || declarationCandidate || returnsCandidate || settingsCandidate || dailyCandidate || statusCandidate || cpuCandidate || process.env.RELEASE_SOURCE_SHA === '0dd2c0b3e73e890bc2c39ab0bae0eeb48c58470a'; - const betaRelease = ibkrDaily || integrated; + const reviewedMain = process.env.RELEASE_SOURCE_SHA === 'c1b737d6e721eb9fe71a12c613fbb49d3fa51563'; + const betaRelease = ibkrDaily || integrated || reviewedMain; const flowMode = process.env.EXTERNAL_FLOW_ENABLE === 'true'; if (flowMode && process.env.RELEASE_SOURCE_SHA !== '1174db8e143f2a3689ca8e86cd547dc08461ff29') fail('external_flow_source_invalid'); const resume = process.env.IBKR_CANDIDATE_RESUME ?? 'off'; @@ -1810,7 +1837,7 @@ jobs: // version; the older script-version API only supports latest. const metadata = { main_module: 'worker.js', bindings: old.bindings.map(b => ({ type: 'inherit', name: b.name, ...(betaRelease ? {version_id: prior} : {}) })), compatibility_date: old.script_runtime.compatibility_date, usage_model: old.script_runtime.usage_model, - annotations: { 'workers/message': controlRuntimeCandidate ? 'Reviewed runtime control candidate; retained configuration' : controlAdminCandidate ? 'Reviewed runtime control admin candidate 5db5153e; retained configuration' : historyCandidate ? 'Reviewed history return candidate 21829dda; retained configuration' : declarationCandidate ? 'Reviewed account declaration candidate 411356d3; retained configuration' : returnsCandidate ? 'Reviewed account returns candidate b051154d; retained configuration' : settingsCandidate ? 'Reviewed settings candidate 3782a3e8; retained configuration' : dailyCandidate ? 'Reviewed daily candidate 4b6cbedc; retained configuration' : statusCandidate ? 'Reviewed status candidate 68e3d4b6; retained configuration' : cpuCandidate ? 'Reviewed CPU candidate 974354b9; retained configuration' : integrated ? 'Reviewed integrated candidate 0dd2c0b3; retained configuration' : pr764 ? 'Reviewed PR764 20dad166 full source; retained configuration' : ibkrDaily ? 'Reviewed IBKR daily candidate aaa46a81; retained configuration' : 'Reviewed website backend 9722414a with fixed frontend 96363633; retained bindings' } }; + annotations: { 'workers/message': reviewedMain ? 'Reviewed merged main frontend; retained configuration' : controlRuntimeCandidate ? 'Reviewed runtime control candidate; retained configuration' : controlAdminCandidate ? 'Reviewed runtime control admin candidate 5db5153e; retained configuration' : historyCandidate ? 'Reviewed history return candidate 21829dda; retained configuration' : declarationCandidate ? 'Reviewed account declaration candidate 411356d3; retained configuration' : returnsCandidate ? 'Reviewed account returns candidate b051154d; retained configuration' : settingsCandidate ? 'Reviewed settings candidate 3782a3e8; retained configuration' : dailyCandidate ? 'Reviewed daily candidate 4b6cbedc; retained configuration' : statusCandidate ? 'Reviewed status candidate 68e3d4b6; retained configuration' : cpuCandidate ? 'Reviewed CPU candidate 974354b9; retained configuration' : integrated ? 'Reviewed integrated candidate 0dd2c0b3; retained configuration' : pr764 ? 'Reviewed PR764 20dad166 full source; retained configuration' : ibkrDaily ? 'Reviewed IBKR daily candidate aaa46a81; retained configuration' : 'Reviewed website backend 9722414a with fixed frontend 96363633; retained bindings' } }; if (flowMode) { metadata.bindings = metadata.bindings.filter(binding => binding.name !== 'CONSOLE_EXTERNAL_FLOW_ENABLED'); metadata.bindings.push({ name: 'CONSOLE_EXTERNAL_FLOW_ENABLED', type: 'plain_text', text: 'true' }); @@ -1835,7 +1862,7 @@ jobs: } else { uploaded = '2b097d2e-ffb5-4f08-af3b-9cc1d4b9e9ee'; } - if (resume !== 'off' || integrated) { + if (resume !== 'off' || integrated || reviewedMain) { stage = 'candidate_module'; const candidate = await request(`/versions/${uploaded}?include=modules`, 'GET', undefined, true); if (candidate?.id !== uploaded) fail('candidate_id_mismatch'); @@ -1862,7 +1889,7 @@ jobs: desired.bindings.push({ name: 'CONSOLE_EXTERNAL_FLOW_ENABLED', type: 'plain_text', text: 'true' }); } const configBefore = configHash(desired), configUploaded = configHash(next); - if (resume !== 'off' || integrated) { + if (resume !== 'off' || integrated || reviewedMain) { const oldTag = old.script_runtime.migration_tag, newTag = next.script_runtime.migration_tag; if (typeof oldTag !== 'string' || !oldTag || (newTag !== undefined && newTag !== oldTag)) fail('migration_tag_mismatch'); const normalized = structuredClone(next); normalized.script_runtime.migration_tag = oldTag; @@ -1891,7 +1918,7 @@ jobs: } else { stage = 'activate'; deployAttempted = true; const deployment = await request('/deployments?force=false', 'POST', JSON.stringify({ strategy: 'percentage', - versions: [{ version_id: uploaded, percentage: 100 }], annotations: { 'workers/message': controlRuntimeCandidate ? 'Reviewed runtime control candidate; retained configuration' : controlAdminCandidate ? 'Reviewed runtime control admin candidate 5db5153e; retained configuration' : historyCandidate ? 'Reviewed history return candidate 21829dda; retained configuration' : declarationCandidate ? 'Reviewed account declaration candidate 411356d3; retained configuration' : returnsCandidate ? 'Reviewed account returns candidate b051154d; retained configuration' : settingsCandidate ? 'Reviewed settings candidate 3782a3e8; retained configuration' : dailyCandidate ? 'Reviewed daily candidate 4b6cbedc; retained configuration' : statusCandidate ? 'Reviewed status candidate 68e3d4b6; retained configuration' : cpuCandidate ? 'Reviewed CPU candidate 974354b9; retained configuration' : integrated ? 'Reviewed integrated candidate 0dd2c0b3; retained configuration' : pr764 ? 'Reviewed PR764 full source; retained configuration' : ibkrDaily ? 'Reviewed IBKR daily candidate aaa46a81; retained configuration' : 'Reviewed website backend 9722414a and frontend 96363633; retained configuration' } })); + versions: [{ version_id: uploaded, percentage: 100 }], annotations: { 'workers/message': reviewedMain ? 'Reviewed merged main frontend; retained configuration' : controlRuntimeCandidate ? 'Reviewed runtime control candidate; retained configuration' : controlAdminCandidate ? 'Reviewed runtime control admin candidate 5db5153e; retained configuration' : historyCandidate ? 'Reviewed history return candidate 21829dda; retained configuration' : declarationCandidate ? 'Reviewed account declaration candidate 411356d3; retained configuration' : returnsCandidate ? 'Reviewed account returns candidate b051154d; retained configuration' : settingsCandidate ? 'Reviewed settings candidate 3782a3e8; retained configuration' : dailyCandidate ? 'Reviewed daily candidate 4b6cbedc; retained configuration' : statusCandidate ? 'Reviewed status candidate 68e3d4b6; retained configuration' : cpuCandidate ? 'Reviewed CPU candidate 974354b9; retained configuration' : integrated ? 'Reviewed integrated candidate 0dd2c0b3; retained configuration' : pr764 ? 'Reviewed PR764 full source; retained configuration' : ibkrDaily ? 'Reviewed IBKR daily candidate aaa46a81; retained configuration' : 'Reviewed website backend 9722414a and frontend 96363633; retained configuration' } })); if (!object(deployment) || !Object.hasOwn(deployment, 'id')) fail('missing_id'); if (!isUuid(deployment.id)) fail('invalid_id'); const deploymentId = deployment.id; @@ -1906,14 +1933,14 @@ jobs: if (deployment.versions[0]?.percentage !== 100) fail('percentage_mismatch'); } stage = 'readback'; await active(deploymentId, uploaded); - if (pr764 || integrated || resume !== 'off') { + if (pr764 || integrated || reviewedMain || resume !== 'off') { stage = 'post_release_configuration'; const configAfter = configHash(resources(await request(`/versions/${uploaded}`), uploaded)); if (configAfter !== configBefore) fail('post_release_configuration_hash_mismatch'); // Version metadata hides secret values. Hash all comparable bindings/runtime/ // handlers/placement, never code etag or provider bookkeeping. No KV writes. if (controlRuntimeCandidate) console.log(JSON.stringify({ status: 'integrated_configuration_verified', bindings_equal: true, runtime_equal: true, secret_values_read: false })); - else console.log(JSON.stringify({ status: integrated ? 'integrated_configuration_verified' : resume !== 'off' ? 'ibkr_candidate_configuration_verified' : 'pr764_configuration_verified', config_before_sha256: configBefore, + else console.log(JSON.stringify({ status: reviewedMain ? 'reviewed_main_configuration_verified' : integrated ? 'integrated_configuration_verified' : resume !== 'off' ? 'ibkr_candidate_configuration_verified' : 'pr764_configuration_verified', config_before_sha256: configBefore, config_uploaded_sha256: configUploaded, config_after_sha256: configAfter, secret_values_read: false })); } if (controlRuntimeCandidate) console.log(JSON.stringify({ status: 'frontend_release_verified', matched: true, diff --git a/docs/console-execution-evidence.md b/docs/console-execution-evidence.md index 55a5464b..69140214 100644 --- a/docs/console-execution-evidence.md +++ b/docs/console-execution-evidence.md @@ -25,3 +25,22 @@ The account and overview cards display the reason for unknown, broker environmen 页面展示未知原因、券商环境、配置来源与检查时间。确认配置仅表示已读到明确配置,不能证明当前生效、真实运行或成交。此变更不授予执行权限。 未获准的会话读取 `/api/config`,可能得到 HTTP 200 和 `accountOptions=null`;这是账户资料不可见,不能当作账户目录为空。 + +## Reviewed main frontend release source + +The manual console deployment entry accepts the pinned, reviewed merged main +frontend release with receiver checks retained. The selector verifies the exact +commit, its ancestry in the workflow control revision, its tree and its embedded +asset map before checking out the source. The build must reproduce that map and +leave no tracked or untracked changes; the legacy frontend overlay is not applied. + +Publication uses version-pinned inheritance from the active Worker version for +every binding, including secrets. Latest-version lineage checks reject concurrent +uploads; module and configuration readbacks must match before activation. + +This mode requires `release_frontend_assets=true`, `sync_secrets=false`, +`sync_strategy_profiles=false`, `skip_binance_receiver_checks=false`, +`external_flow_enable=false` and `ibkr_candidate_resume=off`. Existing job gates +exclude backend-only, rollback, private-release and read-only modes. An arbitrary +main commit is not an accepted release source. Deployment still requires separate +scoped authorization; accepting a source does not grant deployment authority. diff --git a/tests/console_deploy_safety_validation.mjs b/tests/console_deploy_safety_validation.mjs index d0ecd195..c9bc63cd 100644 --- a/tests/console_deploy_safety_validation.mjs +++ b/tests/console_deploy_safety_validation.mjs @@ -245,17 +245,17 @@ const fakeGit = `git() { 'fetch --no-tags origin refs/heads/fix/runtime-daily-multi-release-20261030'|'fetch --no-tags origin refs/heads/fix/qrs-runtime-daily-serving-20261030'|'fetch --no-tags origin refs/heads/codex/website-account-settings-reviewed-20261029'|'fetch --no-tags origin refs/heads/codex/website-native-daily-fixed-clock-20261029'|'fetch --no-tags origin refs/heads/codex/website-integrated-candidate-20261029'|'fetch --no-tags origin refs/heads/codex/website-history-cpu-candidate-20261029'|'fetch --no-tags origin refs/heads/codex/website-incomplete-release-candidate-20261029') return 0 ;; 'rev-parse FETCH_HEAD') printf '%s' "$FAKE_FETCH_HEAD" ;; 'rev-parse HEAD') if [ -f "$SELECTED" ]; then printf '%s' "$FAKE_SELECTED_HEAD"; else printf '%s' "$FAKE_CONTROL_HEAD"; fi ;; - 'cat-file -e ${approved}^{commit}'|'cat-file -e ${pr764Source}^{commit}'|'cat-file -e ${ibkrSource}^{commit}') return "$FAKE_OBJECT_EXIT" ;; + 'cat-file -e c1b737d6e721eb9fe71a12c613fbb49d3fa51563^{commit}'|'cat-file -e ${approved}^{commit}'|'cat-file -e ${pr764Source}^{commit}'|'cat-file -e ${ibkrSource}^{commit}') return "$FAKE_OBJECT_EXIT" ;; 'cat-file -e ${frontend}^{commit}') return "$FAKE_FRONTEND_OBJECT_EXIT" ;; - 'merge-base --is-ancestor ${approved} ${control}'|'merge-base --is-ancestor ${pr764Source} ${control}') return "$FAKE_ANCESTRY_EXIT" ;; + 'merge-base --is-ancestor c1b737d6e721eb9fe71a12c613fbb49d3fa51563 ${control}'|'merge-base --is-ancestor ${approved} ${control}'|'merge-base --is-ancestor ${pr764Source} ${control}') return "$FAKE_ANCESTRY_EXIT" ;; 'merge-base --is-ancestor ${integratedSource} ${cpuSource}') return "$FAKE_ANCESTRY_EXIT" ;; 'merge-base --is-ancestor ${frontend} ${control}') return "$FAKE_FRONTEND_ANCESTRY_EXIT" ;; - 'rev-parse ${runtimeDailyMultiSource}^{tree}'|'rev-parse ${runtimeDailySource}^{tree}'|'rev-parse ${approved}^{tree}'|'rev-parse ${pr764Source}^{tree}'|'rev-parse ${ibkrSource}^{tree}'|'rev-parse ${integratedSource}^{tree}'|'rev-parse ${cpuSource}^{tree}'|'rev-parse ${settingsSource}^{tree}'|'rev-parse ${dailySource}^{tree}'|'rev-parse ${statusSource}^{tree}'|'rev-parse HEAD^{tree}') printf '%s' "$FAKE_TREE" ;; + 'rev-parse c1b737d6e721eb9fe71a12c613fbb49d3fa51563^{tree}'|'rev-parse ${runtimeDailyMultiSource}^{tree}'|'rev-parse ${runtimeDailySource}^{tree}'|'rev-parse ${approved}^{tree}'|'rev-parse ${pr764Source}^{tree}'|'rev-parse ${ibkrSource}^{tree}'|'rev-parse ${integratedSource}^{tree}'|'rev-parse ${cpuSource}^{tree}'|'rev-parse ${settingsSource}^{tree}'|'rev-parse ${dailySource}^{tree}'|'rev-parse ${statusSource}^{tree}'|'rev-parse HEAD^{tree}') printf '%s' "$FAKE_TREE" ;; 'rev-parse ${runtimeDailyMultiSource}^'|'rev-parse ${runtimeDailySource}^'|'rev-parse ${ibkrSource}^'|'rev-parse ${integratedSource}^'|'rev-parse ${settingsSource}^'|'rev-parse ${dailySource}^'|'rev-parse ${statusSource}^') printf '%s' "$FAKE_PARENT" ;; 'rev-parse ${frontend}^{tree}') printf '%s' "$FAKE_FRONTEND_TREE" ;; - 'rev-parse ${runtimeDailyMultiSource}:${assetPath}'|'rev-parse ${runtimeDailySource}:${assetPath}'|'rev-parse ${pr764Source}:${assetPath}'|'rev-parse ${ibkrSource}:${assetPath}'|'rev-parse ${integratedSource}:${assetPath}'|'rev-parse ${cpuSource}:${assetPath}'|'rev-parse ${settingsSource}:${assetPath}'|'rev-parse ${dailySource}:${assetPath}'|'rev-parse ${statusSource}:${assetPath}') printf '%s' "$FAKE_PR764_BLOB" ;; + 'rev-parse c1b737d6e721eb9fe71a12c613fbb49d3fa51563:${assetPath}'|'rev-parse ${runtimeDailyMultiSource}:${assetPath}'|'rev-parse ${runtimeDailySource}:${assetPath}'|'rev-parse ${pr764Source}:${assetPath}'|'rev-parse ${ibkrSource}:${assetPath}'|'rev-parse ${integratedSource}:${assetPath}'|'rev-parse ${cpuSource}:${assetPath}'|'rev-parse ${settingsSource}:${assetPath}'|'rev-parse ${dailySource}:${assetPath}'|'rev-parse ${statusSource}:${assetPath}') printf '%s' "$FAKE_PR764_BLOB" ;; 'rev-parse ${frontend}:${assetPath}') printf '%s' "$FAKE_FRONTEND_BLOB" ;; - '-c advice.detachedHead=false checkout --detach ${runtimeDailyMultiSource}'|'-c advice.detachedHead=false checkout --detach ${runtimeDailySource}'|'-c advice.detachedHead=false checkout --detach ${approved}'|'-c advice.detachedHead=false checkout --detach ${pr764Source}'|'-c advice.detachedHead=false checkout --detach ${ibkrSource}'|'-c advice.detachedHead=false checkout --detach ${integratedSource}'|'-c advice.detachedHead=false checkout --detach ${cpuSource}'|'-c advice.detachedHead=false checkout --detach ${statusSource}'|'-c advice.detachedHead=false checkout --detach ${settingsSource}'|'-c advice.detachedHead=false checkout --detach ${dailySource}') touch "$SELECTED" ;; + '-c advice.detachedHead=false checkout --detach c1b737d6e721eb9fe71a12c613fbb49d3fa51563'|'-c advice.detachedHead=false checkout --detach ${runtimeDailyMultiSource}'|'-c advice.detachedHead=false checkout --detach ${runtimeDailySource}'|'-c advice.detachedHead=false checkout --detach ${approved}'|'-c advice.detachedHead=false checkout --detach ${pr764Source}'|'-c advice.detachedHead=false checkout --detach ${ibkrSource}'|'-c advice.detachedHead=false checkout --detach ${integratedSource}'|'-c advice.detachedHead=false checkout --detach ${cpuSource}'|'-c advice.detachedHead=false checkout --detach ${statusSource}'|'-c advice.detachedHead=false checkout --detach ${settingsSource}'|'-c advice.detachedHead=false checkout --detach ${dailySource}') touch "$SELECTED" ;; *) return 91 ;; esac };\n`; @@ -277,6 +277,25 @@ function sourceCase(delta, expected) { return { calls, selected: existsSync(selected) }; } finally { rmSync(temp, { recursive: true, force: true }); } } +// The merged reviewed main release retains receiver checks and never syncs secrets. +const mergedSource = "c1b737d6e721eb9fe71a12c613fbb49d3fa51563"; +const mergedTree = "5dcdab977ec72e32f99cd3ef4956cebff90ba107"; +const mergedBlob = "b4ef57a6830011f78d0aa0270729e17d05a4d230"; +const mergedEnv = { RELEASE_SOURCE_SHA: mergedSource, RELEASE_FRONTEND_ASSETS: "true", + SKIP_BINANCE_RECEIVER_CHECKS: "false", FAKE_SELECTED_HEAD: mergedSource, + FAKE_TREE: mergedTree, FAKE_PR764_BLOB: mergedBlob }; +assert.equal(sourceCase(mergedEnv, true).selected, true); +for (const delta of [{RELEASE_FRONTEND_ASSETS:"false"}, {SYNC_SECRETS:"true"}, + {SYNC_STRATEGY_PROFILES:"true"}, {SKIP_BINANCE_RECEIVER_CHECKS:"true"}, + {GITHUB_REF:"refs/heads/other"}, {IBKR_CANDIDATE_RESUME:"verify"}, {EXTERNAL_FLOW_ENABLE:"true"}]) { + assert.deepEqual(sourceCase({...mergedEnv,...delta}, false).calls, []); +} +for (const delta of [{FAKE_CONTROL_HEAD:"b".repeat(40)}, {FAKE_OBJECT_EXIT:"1"}, + {FAKE_ANCESTRY_EXIT:"1"}, {FAKE_TREE:"b".repeat(40)}, {FAKE_PR764_BLOB:"b".repeat(40)}]) { + assert.equal(sourceCase({...mergedEnv,...delta}, false).selected, false); +} +assert.equal(sourceCase({...mergedEnv,FAKE_SELECTED_HEAD:"b".repeat(40)}, false).selected, true); + const validSource = sourceCase({}, true); assert.equal(validSource.selected, true); assert.ok(validSource.calls.indexOf(`merge-base --is-ancestor ${approved} ${control}`) < validSource.calls.findIndex(line => line.includes("checkout --detach"))); @@ -432,7 +451,7 @@ const fakeOverlayGit = `git() { 'show ${ibkrSource}:${assetPath}') cat "$FROZEN_MAP" ;; hash-object*) command git "$@" ;; 'diff --name-only ${approved}') printf '%s' "$FAKE_DIFF" ;; - 'diff --name-only ${ibkrSource}'|'diff --name-only ${cpuSource}'|'diff --name-only ${settingsSource}'|'diff --name-only ${dailySource}'|'diff --name-only ${statusSource}') printf '%s' "$FAKE_DIFF" ;; + 'diff --name-only ${mergedSource}'|'diff --name-only ${ibkrSource}'|'diff --name-only ${cpuSource}'|'diff --name-only ${settingsSource}'|'diff --name-only ${dailySource}'|'diff --name-only ${statusSource}') printf '%s' "$FAKE_DIFF" ;; 'ls-files --others --exclude-standard') printf '%s' "$FAKE_UNTRACKED" ;; *) return 91 ;; esac @@ -452,6 +471,29 @@ function overlayCase(delta = {}, expected = true, bytes = reviewedMap) { overlayCases++; } finally { rmSync(temp, { recursive: true, force: true }); } } +// The merged release must preserve its own map, reject tampering and dirty output. +{ + const assetResult = spawnSync("git", ["show", `${mergedSource}:${assetPath}`], {maxBuffer:16*1024*1024}); + assert.equal(assetResult.status, 0); + const asset = assetResult.stdout; + assert.equal(spawnSync("git", ["hash-object", "--stdin"], {input:asset,encoding:"utf8",maxBuffer:16*1024*1024}).stdout.trim(), mergedBlob); + const temp = mkdtempSync(join(tmpdir(), "console-merged-overlay-")); + try { + const dest = join(temp, assetPath); + mkdirSync(join(temp, "web/strategy-switch-console"), {recursive:true}); + const env = {PATH:process.env.PATH, GITHUB_REF:"refs/heads/main", FAKE_HEAD:mergedSource, + FAKE_TREE:mergedTree, FAKE_DIFF:"", FAKE_UNTRACKED:""}; + for (const delta of [{}, {FAKE_DIFF:"fake-file"}, {FAKE_UNTRACKED:"fake-file"}, + {FAKE_TREE:"a".repeat(40)}, {GITHUB_REF:"refs/heads/other"}]) { + writeFileSync(dest, asset); + const result = spawnSync("bash", ["-c", fakeOverlayGit + overlayShell], {cwd:temp,env:{...env,...delta}}); + assert.equal(result.status === 0, Object.keys(delta).length === 0); + assert.deepEqual(readFileSync(dest), asset, "never overlay an older asset"); + } + writeFileSync(dest, "fake modified asset"); + assert.notEqual(spawnSync("bash", ["-c", fakeOverlayGit + overlayShell], {cwd:temp,env}).status, 0); + } finally {rmSync(temp, {recursive:true,force:true});} +} // The CPU candidate carries its own exact generated map, never the older overlay. { const fixedAsset = spawnSync("git", ["show", `${cpuSource}:${assetPath}`], { cwd: new URL("..", import.meta.url) }); @@ -663,13 +705,13 @@ async function releaseCase({pr764=false,ibkrDaily=false,activeOverride=null,inhe releaseCases++;return {calls,stdout,stderr}; } // Exact integrated candidate: explicit inheritance, verified module, one non-forced activation. -async function integratedReleaseCase({ settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false } = {}, expected=true) { +async function integratedReleaseCase({ reviewedMain=false, lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false } = {}, expected=true) { const calls=[], stdout=[], stderr=[], prior=settings?"7ffe9227-686a-48ad-baa9-72665969d918":daily?"14f0c9c2-09f9-40e9-bfbe-17165f499495":statusCandidate?"175ba768-484a-4fe6-9cea-e5a101bc1618":cpu?"90a0c8e9-b5ad-447a-998f-5d585ec28063":"600ec84d-a804-4ede-8d1d-0a34819ce46a"; active ??= prior; latest ??= active; const responses=responseSequence(false,active); responses.splice(3,0,wrap({items:[{id:latest}]})); responses.splice(6,0,wrap({id:newVersion,modules:[{name:"worker.js",content_type:"application/javascript+module",content_base64:moduleContent}]})); - responses.splice(8,0,wrap({items:[{id:newVersion},{id:latest}]})); + responses.splice(8,0,wrap({items:[{id:newVersion},{id:lineage ?? latest}]})); if (flow) { if (oldFlag) responses[1].result.resources.bindings.push(oldFlag); for (const i of [7, 10]) responses[i].result.resources.bindings.push({name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"}); @@ -684,7 +726,7 @@ async function integratedReleaseCase({ settings=false, daily=false, statusCandid else postResources.script_runtime.migration_tag=postTag; responses.push(wrap({id:newVersion,resources:postResources})); const process={env:{GITHUB_REF:"refs/heads/main",CLOUDFLARE_ACCOUNT_ID:"a".repeat(32),CLOUDFLARE_API_TOKEN:privateRelease, - RUNNER_TEMP:"/synthetic",WORKER_DIR:"web/strategy-switch-console",RELEASE_SOURCE_SHA:flow?"1174db8e143f2a3689ca8e86cd547dc08461ff29":settings?settingsSource:daily?dailySource:statusCandidate?statusSource:cpu?cpuSource:integratedSource,...(flow?{EXTERNAL_FLOW_ENABLE:"true",PLATFORM_CONTROL_EXPECTED_VERSION:prior}:{})},exitCode:0}; + RUNNER_TEMP:"/synthetic",WORKER_DIR:"web/strategy-switch-console",RELEASE_SOURCE_SHA:reviewedMain?"c1b737d6e721eb9fe71a12c613fbb49d3fa51563":flow?"1174db8e143f2a3689ca8e86cd547dc08461ff29":settings?settingsSource:daily?dailySource:statusCandidate?statusSource:cpu?cpuSource:integratedSource,...(flow?{EXTERNAL_FLOW_ENABLE:"true",PLATFORM_CONTROL_EXPECTED_VERSION:prior}:{})},exitCode:0}; await runRelease(process,async(url,options)=>{calls.push({url,options}); const response=responses[calls.length-1]; assert.ok(response,"no retry"); return response instanceof Response ? response : new Response(JSON.stringify(response),{headers:{"content-type":"application/json"}});}, {log:x=>stdout.push(x),error:x=>stderr.push(x)},async path=>path.endsWith("v2_asset_map.js")?syntheticAssetMap:syntheticBundle, @@ -692,11 +734,19 @@ async function integratedReleaseCase({ settings=false, daily=false, statusCandid assert.equal(process.exitCode===0,expected,stderr.join("\n")); const posts=calls.filter(x=>x.options.method==="POST"); if(expected){assert.equal(posts.length,2);assert.ok(posts[0].url.endsWith("/versions?deploy=false")); + assert.ok(!calls.some(c=>c.url.includes("bindings_inherit=strict")), "never use latest-version secret inheritance"); const upload=JSON.parse(posts[0].options.body);assert.deepEqual(upload.bindings,[...remoteBindings.map(b=>({type:"inherit",name:b.name,version_id:prior})),...(flow?[{name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"}]:[])]); assert.ok(posts[1].url.endsWith("/deployments?force=false"));if (flow) assert.equal(JSON.parse(stdout.at(-1)).external_flow_enabled,true); else assert.equal(JSON.parse(stdout.at(-1)).version_id,newVersion); } else {if (postTag !== null) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false);} assert.ok(!JSON.stringify({stdout,stderr}).includes(privateRelease)); } +// Execute the actual publisher for the exact merged source, with fake provider responses. +await integratedReleaseCase({reviewedMain:true}); +await integratedReleaseCase({reviewedMain:true,latest:priorVersion},false); +await integratedReleaseCase({reviewedMain:true,lineage:priorVersion},false); +await integratedReleaseCase({reviewedMain:true,moduleContent:Buffer.from("wrong").toString("base64")},false); +await integratedReleaseCase({reviewedMain:true,uploadedTag:"wrong-tag"},false); +await integratedReleaseCase({reviewedMain:true,postTag:null},false); await integratedReleaseCase(); await integratedReleaseCase({flow:true}); await integratedReleaseCase({flow:true,oldFlag:{name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"false"}});