import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; const workflowPath = ".github/workflows/openclaw-npm-release.yml"; type Step = { env?: Record; id?: string; if?: string; name?: string; run?: string; uses?: string; with?: Record; }; type Job = { environment?: string; steps?: Step[] }; type Workflow = { on?: { workflow_dispatch?: { inputs?: { bypass_extended_stable_guard?: { default?: boolean; type?: string }; npm_dist_tag?: { options?: string[] }; plugin_npm_run_id?: { required?: boolean; type?: string }; release_candidate_branch?: { default?: string; required?: boolean; type?: string }; }; }; }; jobs?: Record; }; function workflow(): Workflow { return parse(readFileSync(workflowPath, "utf8")) as Workflow; } function step(job: Job | undefined, name: string): Step { const found = job?.steps?.find((candidate) => candidate.name === name); if (!found) { throw new Error(`Missing workflow step: ${name}`); } return found; } describe("minimal npm extended-stable workflow", () => { it("bounds every git fetch operation", () => { const source = readFileSync(workflowPath, "utf8"); const gitFetchLines = source.split("\n").filter((line) => line.includes("git fetch")); expect(gitFetchLines).toHaveLength(7); expect( gitFetchLines.every((line) => line.includes("timeout --signal=TERM --kill-after=10s 120s")), ).toBe(true); }); it("adds extended-stable without adding policy or verifier contracts", () => { const raw = readFileSync(workflowPath, "utf8"); const parsed = workflow(); expect(parsed.on?.workflow_dispatch?.inputs?.npm_dist_tag?.options).toEqual([ "alpha", "beta", "latest", "extended-stable", ]); for (const forbidden of [ "release-policy", "policyMode", "release-operation-verifier", "external_contract_revision", "stable-lines.json", ]) { expect(raw).not.toContain(forbidden); } }); it("reuses the v1 preflight tarball and guards all three extended-stable gates", () => { const parsed = workflow(); const raw = readFileSync(workflowPath, "utf8"); expect(raw).toContain("version: 1"); expect(raw).toContain("openclaw-npm-preflight-${{ inputs.tag }}"); expect(raw.match(/openclaw-npm-extended-stable-release\.mjs validate-request/g)).toHaveLength( 3, ); expect(step(parsed.jobs?.preflight_openclaw_npm, "Validate npm release request").run).toContain( "openclaw-npm-extended-stable-release.mjs validate-request", ); expect( step(parsed.jobs?.preflight_openclaw_npm, "Validate npm release request").env?.PREFLIGHT_ONLY, ).toBe("${{ inputs.preflight_only }}"); expect( step(parsed.jobs?.validate_publish_request, "Validate npm release request").run, ).toContain("openclaw-npm-extended-stable-release.mjs validate-request"); expect(step(parsed.jobs?.publish_openclaw_npm, "Recheck npm release request").run).toContain( "openclaw-npm-extended-stable-release.mjs validate-request", ); expect( parsed.jobs?.validate_publish_request?.steps?.map((candidate) => candidate.name), ).not.toContain("Setup Node environment"); }); it("threads an explicit, default-off extended-stable bypass through every policy gate", () => { const parsed = workflow(); const input = parsed.on?.workflow_dispatch?.inputs?.bypass_extended_stable_guard; expect(input).toMatchObject({ default: false, type: "boolean" }); const policySteps = [ step(parsed.jobs?.preflight_openclaw_npm, "Validate npm release request"), step(parsed.jobs?.validate_publish_request, "Validate npm release request"), step(parsed.jobs?.publish_openclaw_npm, "Recheck npm release request"), step(parsed.jobs?.publish_openclaw_npm, "Publish"), ]; for (const policyStep of policySteps) { expect(policyStep.env?.BYPASS_EXTENDED_STABLE_GUARD).toBe( "${{ inputs.bypass_extended_stable_guard }}", ); } const trustedRef = step( parsed.jobs?.validate_publish_request, "Require trusted workflow ref for publish", ); expect(trustedRef.env?.BYPASS_EXTENDED_STABLE_GUARD).toBeUndefined(); expect(trustedRef.run).not.toContain("BYPASS_EXTENDED_STABLE_GUARD"); expect(trustedRef.run).toContain('"${WORKFLOW_REF}" == refs/heads/extended-stable/*'); const summary = step( parsed.jobs?.publish_openclaw_npm, "Summarize extended-stable npm publication", ); expect(summary.env?.BYPASS_EXTENDED_STABLE_GUARD).toBe( "${{ inputs.bypass_extended_stable_guard }}", ); expect(summary.run).toContain("Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}"); }); it("lets main promote only the canonical immutable extended-stable candidate", () => { const parsed = workflow(); const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); const input = parsed.on?.workflow_dispatch?.inputs?.release_candidate_branch; expect(input).toMatchObject({ default: "", required: false, type: "string" }); const validate = step(parsed.jobs?.validate_publish_request, "Validate npm release request"); expect(validate.env?.NPM_WORKFLOW_REF).toBe( "${{ inputs.release_candidate_branch != '' && format('refs/heads/{0}', inputs.release_candidate_branch) || github.ref }}", ); const checkout = step(parsed.jobs?.validate_publish_request, "Checkout"); expect(checkout.with?.ref).toBe( "${{ inputs.release_candidate_branch != '' && format('refs/tags/{0}', inputs.tag) || github.sha }}", ); const trustedRef = step( parsed.jobs?.validate_publish_request, "Require trusted workflow ref for publish", ); expect(trustedRef.env?.RELEASE_CANDIDATE_BRANCH).toBe("${{ inputs.release_candidate_branch }}"); expect(trustedRef.run).toContain('release_candidate_branch="${RELEASE_CANDIDATE_BRANCH:-}"'); expect(trustedRef.run).toContain('"${WORKFLOW_REF}" != "refs/heads/main"'); expect(trustedRef.run).toContain( 'expected_candidate_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"', ); const recheck = step(parsed.jobs?.publish_openclaw_npm, "Recheck npm release request"); expect(recheck.env?.NPM_WORKFLOW_REF).toBe(validate.env?.NPM_WORKFLOW_REF); expect(releaseDocs).toContain("--ref main"); expect(releaseDocs).toContain("-f release_candidate_branch=extended-stable/YYYY.M.33"); expect(releaseDocs).toContain("canonical candidate branch directly"); expect(releaseDocs).toContain("workflow SHA is reachable from current `main`"); expect(releaseDocs).toContain("trusted main-pinned harness"); }); it("accepts arbitrary SHA preflight targets and exercises every publishable plugin package", () => { const parsed = workflow(); const preflight = parsed.jobs?.preflight_openclaw_npm; const metadata = step(preflight, "Validate release metadata"); expect(metadata.run).toContain('RELEASE_BRANCH_REF="${RELEASE_SHA}"'); expect(metadata.run).not.toContain("Validation-only SHA mode only supports"); const plugins = step(preflight, "Exercise all extended-stable plugin npm packages"); expect(step(preflight, "Verify release contents").env).toMatchObject({ OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR: "${{ steps.core_package_tarballs.outputs.dir }}", }); expect(plugins.if).toBe("${{ inputs.npm_dist_tag == 'extended-stable' }}"); expect(plugins.env).toMatchObject({ OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: "extended-stable", }); expect(plugins.run).toContain("--selection-mode all-publishable"); expect(plugins.run).toContain("--npm-dist-tag extended-stable"); expect(plugins.run).toContain("scripts/check-plugin-npm-runtime-builds.mts"); expect(plugins.run).toContain("scripts/plugin-npm-publish.sh --pack"); expect(plugins.run).toContain("OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR"); expect(plugins.run).not.toContain("--publish"); expect(step(preflight, "Upload extended-stable plugin npm packages")).toBeDefined(); }); it("restores same-SHA preflight build outputs and keeps validation steps running", () => { const parsed = workflow(); const preflight = parsed.jobs?.preflight_openclaw_npm; const stepNames = preflight?.steps?.map((candidate) => candidate.name) ?? []; const cleanup = step(preflight, "Clean preflight build outputs before cache restore"); const restore = step(preflight, "Restore preflight build outputs"); expect(stepNames.indexOf(cleanup.name)).toBeLessThan(stepNames.indexOf(restore.name)); expect(cleanup.run).toContain("rm -rf -- dist dist-runtime packages/*/dist"); expect(cleanup.run).toContain("-path '*/src/host/*'"); expect(cleanup.run).toContain("-name '.bundle.hash'"); expect(cleanup.run).toContain("-name '*.bundle.js'"); expect(restore.uses).toContain("actions/cache/restore@"); expect(restore.with?.path).toContain("dist/"); expect(restore.with?.path).toContain("dist-runtime/"); expect(restore.with?.path).toContain("packages/*/dist/"); expect(restore.with?.path).toContain("extensions/*/src/host/**/.bundle.hash"); expect(restore.with?.path).toContain("extensions/*/src/host/**/*.bundle.js"); expect(restore.with?.key).toBe( "${{ runner.os }}-npm-preflight-dist-v1-${{ steps.preflight_cache_key.outputs.sha }}-${{ hashFiles('pnpm-lock.yaml') }}", ); // Only the build producers skip on a cache hit; every validation step // still runs against the restored artifacts. const build = step(preflight, "Build"); const buildControlUi = step(preflight, "Build Control UI"); expect(build.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); expect(build.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); expect(buildControlUi.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); expect(buildControlUi.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); expect(step(preflight, "Check").if).toBeUndefined(); expect(step(preflight, "Verify release contents").if).toBeUndefined(); expect(step(preflight, "Verify prepared npm tarball install").if).toBeUndefined(); const save = step(preflight, "Save preflight build outputs"); expect(save.uses).toContain("actions/cache/save@"); expect(save.with?.key).toBe("${{ steps.dist_build_cache.outputs.cache-primary-key }}"); }); it("uses the trusted Full Validation evidence verifier", () => { const parsed = workflow(); const raw = readFileSync(workflowPath, "utf8"); expect(raw).toContain("--json workflowName,headBranch,headSha,event,conclusion,url"); const verifier = step( parsed.jobs?.publish_openclaw_npm, "Checkout trusted validation verifier", ); expect(verifier.with?.ref).toBe("${{ github.workflow_sha }}"); expect(verifier.with?.path).toBe("trusted-workflow"); const fullValidation = step( parsed.jobs?.publish_openclaw_npm, "Verify full release validation evidence", ); expect(fullValidation.env?.EXPECTED_WORKFLOW_BRANCH).toBe( "${{ inputs.release_candidate_branch || github.ref_name }}", ); expect(fullValidation.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe( "${{ inputs.full_release_validation_run_attempt }}", ); expect(fullValidation.run).toContain( "actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}", ); expect(fullValidation.run).toContain("--filter=blob:none"); expect(fullValidation.run).toContain( "trusted-workflow/scripts/validate-full-release-validation-evidence.mjs", ); expect(raw.match(/openclaw-npm-extended-stable-release\.mjs verify-run/g)).toHaveLength(2); expect(raw).not.toContain("openclaw-npm-extended-stable-release.mjs verify-manifest"); }); it("requires and authenticates the plugin npm run before an extended-stable core publish", () => { const parsed = workflow(); expect(parsed.on?.workflow_dispatch?.inputs?.plugin_npm_run_id).toMatchObject({ required: false, type: "string", }); const required = step( parsed.jobs?.validate_publish_request, "Require preflight artifact promotion on real publish", ); expect(required.env?.PLUGIN_NPM_RUN_ID).toBe("${{ inputs.plugin_npm_run_id }}"); expect(required.run).toContain("Extended-stable publish requires plugin_npm_run_id"); const verify = step( parsed.jobs?.publish_openclaw_npm, "Verify plugin npm release run metadata", ); expect(verify.env?.RUN_KIND).toBe("plugin"); expect(verify.run).toContain( "--json workflowName,displayTitle,headBranch,headSha,event,status,conclusion,url", ); expect(verify.run).toContain("openclaw-npm-extended-stable-release.mjs verify-run"); }); it("captures selector fail closed, publishes extended-stable, retries, and summarizes", () => { const parsed = workflow(); const publish = parsed.jobs?.publish_openclaw_npm; const capture = step(publish, "Capture previous extended-stable selector"); const readback = step(publish, "Verify extended-stable registry readback"); const summary = step(publish, "Summarize extended-stable npm publication"); expect(capture.run).toContain("openclaw-npm-extended-stable-release.mjs capture-selector"); expect(step(publish, "Publish").run).toContain("openclaw-npm-publish.sh"); expect(readback.run).toContain("openclaw-npm-extended-stable-release.mjs verify-readback"); expect(summary.if).toContain("always()"); expect(summary.run).toContain("openclaw-npm-extended-stable-release.mjs repair-command"); expect(summary.run).toContain('EXPECTED_VERSION="$RELEASE_TAG"'); expect(summary.env?.EXTENDED_STABLE_BRANCH).toBe( "${{ inputs.release_candidate_branch || github.ref_name }}", ); expect(summary.env?.RELEASE_SHA).toBeUndefined(); expect(summary.run).toContain('release_sha="$(git rev-parse HEAD)"'); expect(publish?.environment).toBe("npm-release"); }); it("publishes only the tarball path verified from the preflight manifest", () => { const publish = workflow().jobs?.publish_openclaw_npm; const provenance = step(publish, "Verify prepared tarball provenance"); const publishStep = step(publish, "Publish"); expect(provenance.run).toContain( 'ARTIFACT_TARBALL_PATH="preflight-tarball/$ARTIFACT_TARBALL_NAME"', ); expect(provenance.run).toContain('has("corePackageTarballs")'); expect(provenance.run).toContain("CORE_PACKAGE_TARBALL_COUNT=0"); expect(provenance.run).toContain('echo "tarball_path=$ARTIFACT_TARBALL_PATH"'); expect(publishStep.env?.PUBLISH_TARBALL_PATH).toBe( "${{ steps.preflight_provenance.outputs.tarball_path }}", ); expect(publish?.steps?.map((candidate) => candidate.name)).not.toContain( "Resolve publish tarball", ); expect(readFileSync(workflowPath, "utf8")).not.toContain( "find preflight-tarball -type f -name '*.tgz'", ); }); it("publishes gateway packages in manifest order before the root package", () => { const parsed = workflow(); const preflightPack = step( parsed.jobs?.preflight_openclaw_npm, "Pack publishable core packages", ); const publish = step(parsed.jobs?.publish_openclaw_npm, "Publish"); expect(preflightPack.env?.CORE_PACKAGE_DIRS).toBe( "packages/ai packages/gateway-protocol packages/gateway-client", ); expect(readFileSync(workflowPath, "utf8")).toContain('packageName: "@openclaw/gateway-client"'); expect(publish.run).toContain("(.corePackageTarballs // [])[]"); expect(publish.run).toContain( 'bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"', ); }); });