#!/usr/bin/env node const assert = require("assert"); const fs = require("fs"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443"; const serviceHost = "disasmer.michelpaulissen.com"; const forgejoHost = "git.michelpaulissen.com"; function read(relativePath) { return fs.readFileSync(path.join(repo, relativePath), "utf8"); } function maybeRead(relativePath) { const file = path.join(repo, relativePath); return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null; } function expect(source, name, pattern) { assert(source !== null && source !== undefined, `missing source for ${name}`); assert.match(source, pattern, `missing public release dry-run evidence: ${name}`); } const phase2 = maybeRead("acceptance_criteria_phase2.md"); const readme = maybeRead("README.md"); const cliSource = read("crates/disasmer-cli/src/main.rs"); const cliLoginSmoke = read("scripts/cli-login-smoke.js"); const vscodePackage = JSON.parse(read("vscode-extension/package.json")); const vscodeExtension = read("vscode-extension/extension.js"); const vscodeSmoke = read("scripts/vscode-extension-smoke.js"); const prepScript = read("scripts/prepare-public-release-dryrun.js"); const publishScript = read("scripts/publish-public-release-dryrun.js"); const preflightScript = read("scripts/public-release-dryrun-preflight.js"); const e2eScript = read("scripts/public-release-dryrun-e2e.js"); const finalEvidenceScript = read("scripts/public-release-dryrun-final-evidence.js"); const workflow = maybeRead(".forgejo/workflows/public-release-dryrun.yml"); const publicAcceptance = read("scripts/acceptance-public.sh"); const privateAcceptance = read("scripts/acceptance-private.sh"); const publicSplit = read("scripts/verify-public-split.sh"); for (const [name, source] of [ ["phase 2 criteria", phase2], ["README", readme], ]) { if (source) { expect(source, name, new RegExp(serviceHost.replaceAll(".", "\\."))); expect(source, `${name} DNS publication state`, /DNS record|public DNS|dns[-_]publication/i); expect(source, `${name} resolver fallback`, /no resolver override|required resolver override|hosts entry|controlled resolution|fallback/i); expect(source, `${name} Forgejo host`, new RegExp(forgejoHost.replaceAll(".", "\\."))); expect(source, `${name} Forgejo Release`, /Forgejo Release/); expect(source, `${name} compiled assets`, /compiled\s+(release\s+)?assets|compiled release assets/); expect(source, `${name} filtered tree`, /private\/\*\*[\s\S]*experiments\/\*\*/); expect(source, `${name} root Markdown export filter`, /root `\*\.md`|root Markdown/); expect(source, `${name} host-neutral workflow filter`, /\.forgejo[\s\S]*--include-forgejo-workflows/); expect(source, `${name} GitHub release out of scope`, /GitHub[\s\S]*outside this dry run|public GitHub-release[\s\S]*out of scope/); } } expect(cliSource, "CLI default operator constant", new RegExp(`DEFAULT_OPERATOR_ENDPOINT: &str = "${serviceEndpoint.replaceAll(".", "\\.")}"`)); expect(cliSource, "login uses default operator", /default_value_t = default_operator_endpoint\(\)/); expect(cliSource, "hosted run records operator endpoint", /operator_endpoint[\s\S]*Some\(default_operator_endpoint\(\)\)/); expect(cliSource, "hosted URL maps to JSON-line transport", /fn json_line_transport_addr[\s\S]*\("https:\/\/", 443\)/); expect(cliSource, "JSON-line session uses mapped transport", /TcpStream::connect\(&transport_addr\)/); expect(cliSource, "default operator maps to port 9443", /json_line_transport_addr\(DEFAULT_OPERATOR_ENDPOINT\)[\s\S]*"disasmer\.michelpaulissen\.com:9443"/); assert.doesNotMatch(cliSource, /coord\.disasmer\.invalid/); expect(cliLoginSmoke, "CLI login smoke covers default operator", new RegExp(`defaultOperatorEndpoint = "${serviceEndpoint.replaceAll(".", "\\.")}"`)); expect(vscodeExtension, "VS Code default operator constant", new RegExp(`DEFAULT_OPERATOR_ENDPOINT = "${serviceEndpoint.replaceAll(".", "\\.")}"`)); assert.strictEqual( vscodePackage.contributes.debuggers[0].configurationAttributes.launch.properties.operatorEndpoint.default, serviceEndpoint ); expect(vscodeSmoke, "VS Code smoke covers operator endpoint", /operatorEndpoint\.default/); for (const binary of [ "disasmer", "disasmer-coordinator", "disasmer-node", "disasmer-debug-dap", ]) { expect(prepScript, `release prep includes ${binary}`, new RegExp(`"${binary}"`)); } for (const [name, pattern] of [ ["release prep filters private and experiments", /filteredTopLevel = \["private", "experiments", "\.git", "target"\]/], ["release prep filters generated view state", /filteredDirectoryNames = \["\.disasmer"\]/], ["release prep filters root Markdown", /isRootMarkdown[\s\S]*path\.extname\(entry\.name\)\.toLowerCase\(\) === "\.md"/], ["release prep filters .forgejo by default", /topLevel === "\.forgejo" && !includeForgejoWorkflows/], ["release prep has Forgejo workflow opt-in argument", /--include-forgejo-workflows/], ["release prep builds public release bins", /cargo"[\s\S]*"build"[\s\S]*"--workspace"[\s\S]*"--bins"[\s\S]*"--release"/], ["release prep writes binary archive", /disasmer-public-binaries-\$\{releaseName\}-\$\{platformName\(\)\}\.tar\.gz/], ["release prep writes source archive", /disasmer-public-source-\$\{releaseName\}\.tar\.gz/], ["release prep writes extension VSIX", /\$\{packageJson\.name\}-\$\{packageJson\.version\}\.vsix[\s\S]*@vscode\/vsce[\s\S]*package/], ["release prep writes selected-user guide", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\$\{releaseName\}\.md/], ["release prep writes selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\$\{releaseName\}\.md/], ["release prep accepts resolver instructions", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS/], ["release prep accepts hosts entry", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY/], ["release prep accepts deployment IP", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP/], ["selected-user guide documents DNS or fallback", /public DNS[\s\S]*(fallback|hosts entry)|DNS record[\s\S]*controlled resolution/], ["selected-user guide says private hosted coordinator", /default operator is the private hosted coordinator/], ["selected-user guide rejects standalone public coordinator", /standalone open-source\/public coordinator/], ["selected-user invite documents friends dry run", /friends helping test[\s\S]*not broadly advertised/], ["selected-user invite says hosted server is private", /hosted server is not the standalone public\s+coordinator/], ["selected-user guide documents default login", /disasmer login --browser/], ["selected-user guide documents VSIX install", /code --install-extension disasmer-vscode-\*\.vsix/], ["selected-user guide documents node attach", /disasmer node attach --coordinator https:\/\/disasmer\.michelpaulissen\.com:9443/], ["release prep writes checksums", /SHA256SUMS/], ["release prep writes manifest", /public-release-manifest\.json/], ["release prep writes public tree provenance", /DISASMER_PUBLIC_TREE\.json/], ["release prep records public tree identity", /public_tree_identity: publicTreeIdentity/], ["release prep records DNS state", /dns_publication_state:/], ["release prep records resolver override", /resolver_override:/], ["release prep records Forgejo URLs", /public_repo_url:[\s\S]*public_repo_remote:[\s\S]*forgejo_release_url:/], ["release prep requires publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE/], ["release prep requires Forgejo remote", /remote\.includes\(forgejoHost\)/], ["release prep can push filtered tree", /git"[\s\S]*"push"[\s\S]*"public"[\s\S]*`HEAD:\$\{publicRepoBranch\}`/], ["release prep records publish result", /public_tree_publish: publicTreePublish/], ["release prep publishes tree before building target", /const publicTreePublish = publishPublicTree[\s\S]*buildPublicBinaries\(\)/], ]) { expect(prepScript, name, pattern); } for (const [name, pattern] of [ ["release publisher requires Forgejo token", /DISASMER_FORGEJO_TOKEN/], ["release publisher infers Forgejo repo identity", /function resolveRepoIdentity\(manifest\)[\s\S]*parseForgejoRepoIdentity/], ["release publisher uses manifest public repo URL", /manifest\.public_repo_url[\s\S]*manifest\.public_repo_remote/], ["release publisher uses token auth", /Authorization: `token \$\{token\}`/], ["release publisher lists releases", /\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases\?limit=100/], ["release publisher creates release", /POST[\s\S]*\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases/], ["release publisher reloads release details", /function loadRelease\(releaseId\)[\s\S]*\/releases\/\$\{releaseId\}/], ["release publisher uploads assets", /releases\/\$\{release\.id\}\/assets\?name=\$\{encodeURIComponent\(asset\.name\)\}/], ["release publisher uses attachment multipart field", /multipartFile\("attachment", asset\.file\)/], ["release publisher skips already attached assets", /existingAssetByName\(release, asset\.name\)/], ["release publisher rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["release publisher checks manifest kind", /manifest\.kind !== "disasmer-public-release-dryrun"/], ["release publisher requires public tree push", /public tree must be pushed before publishing the Forgejo Release/], ["release publisher accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["release publisher targets external public tree commit", /function publicTreeCommit\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_COMMIT/], ["release publisher writes evidence report", /public-release-dryrun-forgejo-release\.json/], ["release publisher records reused assets", /reused_assets:/], ]) { expect(publishScript, name, pattern); } for (const [name, pattern] of [ ["preflight rejects stale release manifests", /manifest\.source_commit[\s\S]*currentSourceCommit/], ["preflight disables interactive remote prompts", /function nonInteractiveEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*git", \["ls-remote"[\s\S]*timeout/], ["preflight accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["preflight verifies public branch commit", /remoteMain[\s\S]*publicTreeCommit/], ["preflight verifies local asset checksums", /parseSha256Sums[\s\S]*checksum mismatch/], ["preflight records public tree push source", /public_tree_push_source/], ["preflight records pending external gates", /external_gates:[\s\S]*forgejo_release_publication[\s\S]*public_release_e2e/], ["preflight writes evidence report", /public-release-dryrun-preflight\.json/], ]) { expect(preflightScript, name, pattern); } for (const [name, pattern] of [ ["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/], ["e2e runner rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["e2e runner disables interactive git prompts", /function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/], ["e2e runner accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["e2e runner downloads Forgejo Release assets", /downloadReleaseAssets/], ["e2e runner verifies SHA256SUMS", /verifyChecksums/], ["e2e runner clones public repo", /"git", \["clone", "--depth", "1"/], ["e2e runner rejects private tree leaks", /private[\s\S]*experiments[\s\S]*target/], ["e2e runner rejects root Markdown leaks", /root Markdown file leaked into public repo/], ["e2e runner rejects .forgejo leaks by default", /\.forgejo\/ leaked into public repo/], ["e2e runner rejects generated view state", /generated \.disasmer view state leaked into public repo/], ["e2e runner checks public tree identity", /hashTree\(checkout\)[\s\S]*manifest\.public_tree_identity/], ["e2e runner extracts public binaries", /tar"[\s\S]*"-xzf"/], ["e2e runner loads public coordinator binary", /executable\(installDir, "disasmer-coordinator"\)/], ["e2e runner checks default operator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/], ["e2e runner completes browser login", /--complete-browser-code/], ["e2e runner uses public CLI node attach", /"node"[\s\S]*"attach"[\s\S]*serviceEndpoint/], ["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/], ["e2e runner launches task through coordinator", /type: "launch_task"/], ["e2e runner verifies assignment polling", /worker_assignment_poll_verified/], ["e2e runner validates standalone public coordinator", /validateStandalonePublicCoordinator/], ["e2e runner records public coordinator validation", /public_coordinator_validated/], ["e2e runner validates released live DAP", /validateReleasedLiveDap/], ["e2e runner starts live DAP worker after coordinator-side breakpoint", /worker_started_after_coordinator_side_breakpoint/], ["e2e runner verifies task events", /type: "list_task_events"/], ["e2e runner verifies download link", /type: "create_artifact_download_link"/], ["e2e runner verifies VS Code debugger", /scripts\/vscode-f5-smoke\.js/], ["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/], ]) { expect(e2eScript, name, pattern); } for (const [name, pattern] of [ ["final evidence reads manifest", /public-release-manifest\.json/], ["final evidence rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/], ["final evidence verifies filtered export", /function assertFilteredOut\(manifest\)/], ["final evidence reads Forgejo Release report", /public-release-dryrun-forgejo-release\.json/], ["final evidence reads deployment manifest", /deployment-manifest\.json/], ["final evidence reads service smoke report", /public-release-dryrun-service\.json/], ["final evidence reads public operator compatibility report", /public-operator-compat\.json/], ["final evidence reads public coordinator compatibility report", /public-coordinator-compat\.json/], ["final evidence requires public e2e report", /public-release-dryrun-e2e\.json/], ["final evidence verifies Forgejo host", /forgejoHost = "git\.michelpaulissen\.com"/], ["final evidence verifies default operator", /serviceEndpoint = "https:\/\/disasmer\.michelpaulissen\.com:9443"/], ["final evidence accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/], ["final evidence records resolved public repo", /resolvedPublicRepoRemote[\s\S]*public_repository_url: resolvedPublicRepoRemote/], ["final evidence requires private hosted coordinator", /operator_implementation[\s\S]*"private-hosted-coordinator"/], ["final evidence requires service private coordinator marker", /service\.operator_implementation[\s\S]*"private-hosted-coordinator"/], ["final evidence requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires current public operator source", /compat\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current public operator release", /compat\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires current public coordinator source", /publicCoordinator\.source_commit[\s\S]*manifest\.source_commit/], ["final evidence requires current public coordinator release", /publicCoordinator\.release_name[\s\S]*manifest\.release_name/], ["final evidence requires service launch task", /service\.evidence\.public_launch_task[\s\S]*"task_launched"/], ["final evidence requires launch task", /launch_task_response[\s\S]*"task_launched"/], ["final evidence requires assignment polling", /worker_assignment_poll_verified/], ["final evidence requires public coordinator validation", /public_coordinator_validated/], ["final evidence requires standalone public coordinator", /standalone-public-coordinator/], ["final evidence requires released live DAP", /released_live_dap_verified/], ["final evidence requires pushed public tree", /publicTreeAlreadyPushed\(manifest\)/], ["final evidence requires release assets", /downloaded_release_assets/], ["final evidence requires VS Code debugger", /vscode_debugger_verified/], ["final evidence writes final report", /public-release-dryrun-final\.json/], ]) { expect(finalEvidenceScript, name, pattern); } if (workflow) { for (const [name, pattern] of [ ["manual Forgejo workflow", /workflow_dispatch:/], ["Linux Forgejo asset job", /linux-assets:[\s\S]*runs-on: docker/], ["Windows Forgejo asset job", /windows-assets:[\s\S]*runs-on: windows/], ["Windows runner caveat", /intermittently online/], ["workflow runs release prep", /node scripts\/prepare-public-release-dryrun\.js/], ["workflow uploads assets", /actions\/upload-artifact@v4[\s\S]*target\/public-release-dryrun\/assets\/\*/], ]) { expect(workflow, name, pattern); } } expect( prepScript, "public tree prep disables interactive git prompts", /function nonInteractiveGitEnv[\s\S]*GIT_TERMINAL_PROMPT[\s\S]*GIT_ASKPASS[\s\S]*SSH_ASKPASS[\s\S]*GIT_SSH_COMMAND[\s\S]*BatchMode=yes[\s\S]*NumberOfPasswordPrompts=0[\s\S]*function commandEnv/ ); for (const [scriptName, script] of [ ["public acceptance", publicAcceptance], ["public split", publicSplit], ]) { assert( script.includes("node scripts/public-release-dryrun-contract-smoke.js"), `${scriptName} must run public-release-dryrun-contract-smoke.js` ); assert( script.includes("node scripts/prepare-public-release-dryrun.js"), `${scriptName} must run prepare-public-release-dryrun.js` ); assert( script.includes("node scripts/publish-public-release-dryrun.js"), `${scriptName} must be able to publish the Forgejo Release` ); assert( script.includes("DISASMER_FORGEJO_TOKEN"), `${scriptName} must gate Forgejo Release publishing on a token` ); assert( script.includes('if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]'), `${scriptName} must not require owner/repo env when the manifest can infer the Forgejo repository` ); } for (const [scriptName, script] of [ ["public acceptance", publicAcceptance], ["private acceptance", privateAcceptance], ]) { if (scriptName === "public acceptance") { assert( script.includes("node scripts/public-release-dryrun-e2e.js"), "public acceptance must be able to run public-release-dryrun-e2e.js" ); assert( script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"), "public acceptance must gate public-release-dryrun-e2e.js" ); } assert( script.includes("node scripts/public-release-dryrun-final-evidence.js"), `${scriptName} must be able to run public-release-dryrun-final-evidence.js` ); assert( script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"), `${scriptName} must gate final public release dry-run evidence` ); } console.log("Public release dry-run contract smoke passed");