From 53fbe2eb33adea9eadfa51c01df2d88014990c81 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 21 Aug 2026 19:08:32 -0700 Subject: [PATCH] fix(release): preserve validation plan across reruns (#127343) * fix(release): preserve validation plan across reruns * test(release): align rerun plan assertions * refactor(release): use canonical plan cache action * style(test): format release plan cache assertion --- .agents/skills/openclaw-testing/SKILL.md | 7 ++++--- .agents/skills/release-openclaw-ci/SKILL.md | 13 ++++++++----- .github/workflows/full-release-validation.yml | 12 +++++++----- docs/reference/full-release-validation.md | 10 +++++++--- test/scripts/package-acceptance-workflow.test.ts | 11 +++++++++-- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index 3f5899457966..188b91fd2ece 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -350,9 +350,10 @@ the failure to that exact active run. The child-dispatch jobs record run ID, run attempt, and URL, then finish. The parent seals those tuples, original dispatch titles, gate coverage, reuse policy, and original parent attempt in one immutable -`full-release-execution-plan-` artifact. Collector retries restore that -artifact and adopt its children; they never reconstruct the plan or redispatch -tests. +`full-release-execution-plan-` artifact and exact run-ID cache entry. +Collector retries restore the cached bytes, validate them, and re-upload the +artifact for their attempt before adopting its children; they never reconstruct +the plan or redispatch tests. `Release Decision` polls those exact identities and can report `blocked_diagnostics_running` before unrelated children finish. For reused evidence, it also repeats the canonical target, policy, changed-path, diff --git a/.agents/skills/release-openclaw-ci/SKILL.md b/.agents/skills/release-openclaw-ci/SKILL.md index 6ccf9e1d8ef9..6452a8bf4d37 100644 --- a/.agents/skills/release-openclaw-ci/SKILL.md +++ b/.agents/skills/release-openclaw-ci/SKILL.md @@ -57,10 +57,12 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele child that owns a blocking failure. - After dispatch, one immutable execution-plan artifact records the original parent attempt, exact child tuples and titles, selected coverage, gates, and - reuse identity. Decision, Drain, manifest writing, evidence validation, and - final verification consume that plan. A collector retry restores it and - adopts the same children; missing plan state is an orchestration failure, not - permission to redispatch. + reuse identity. The same bytes are saved under an exact run-ID cache key. + Decision, Drain, manifest writing, evidence validation, and final verification + consume the artifact for their current attempt. A collector retry restores + the cached plan, validates it, re-uploads its artifact, and adopts the same + children; missing plan state is an orchestration failure, not permission to + reconstruct the plan or redispatch. - Reused evidence is not trusted merely because plan sealing found it. Release Decision repeats the sealed target SHA, evidence SHA, policy, changed paths, selected run, root run, source manifest, trusted tooling identity, and @@ -321,7 +323,8 @@ The `full-release-diagnostics--` artifact is the terminal failure and timing manifest. Use it after an early blocker instead of restarting `all` merely to discover what the still-running children found. The stable `full-release-execution-plan-` artifact is the identity -source for every collector attempt. +source within each collector attempt; retry attempts restore its immutable +run-ID-cached bytes first. ## Failure Triage diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index c846b1ad6181..3a3c4216ba38 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -1096,12 +1096,14 @@ jobs: sparse-checkout-cone-mode: false persist-credentials: false - - name: Restore immutable release execution plan - if: ${{ github.run_attempt != 1 }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + # Parent reruns hide prior-attempt artifacts. This exact-key cache is immutable; + # a miss fails closed instead of reconstructing or redispatching child identities. + - name: Cache immutable release execution plan + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: - name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan + key: full-release-execution-plan-v1-${{ github.run_id }} + fail-on-cache-miss: ${{ github.run_attempt != 1 }} - name: Seal immutable release execution plan id: plan @@ -1226,7 +1228,7 @@ jobs: node scripts/full-release-validation-state.mjs plan - name: Upload immutable release execution plan - if: ${{ always() && github.run_attempt == 1 }} + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-execution-plan-${{ github.run_id }} diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index df9c909584e3..c686e6fa0a30 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -45,12 +45,16 @@ first-failure path is preferable; Release Decision then cancels only the exact still-active child that owns the blocking failure. After dispatch, the parent writes one immutable -`full-release-execution-plan-` artifact. It records selected and +`full-release-execution-plan-` artifact and preserves the same bytes in +an exact run-ID Actions cache. It records selected and required coverage, gate results, reuse identity, the original parent attempt, and every exact child run ID, attempt, title, workflow ref, and Tooling SHA. Decision, Drain, manifest generation, evidence verification, and the final -verifier consume this artifact. Collector retries restore it and adopt the -same children; they never rebuild the plan or redispatch tests. +verifier consume the artifact for their current attempt. Collector retries +restore the immutable cached copy, validate it, and upload the artifact again +for the retry; they never rebuild the plan or redispatch tests. A missing or +evicted cache fails closed, so start a new validation instead of retrying that +stale parent. Release Decision also repeats canonical reuse-chain validation before a reused run can pass. The sealed target SHA, evidence SHA, policy, changed-path set, selected run, root run, source manifest, trusted tooling identity, and child diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index f21c2e651042..72d217230127 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -54,6 +54,7 @@ const ANDROID_RELEASE_WORKFLOW = ".github/workflows/android-release.yml"; const STABLE_MAIN_CLOSEOUT_WORKFLOW = ".github/workflows/openclaw-stable-main-closeout.yml"; const WINDOWS_NODE_RELEASE_WORKFLOW = ".github/workflows/windows-node-release.yml"; const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml"; +const ACTIONS_CACHE_V5 = "actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae"; const CI_WORKFLOW = ".github/workflows/ci.yml"; const PERFORMANCE_WORKFLOW = ".github/workflows/openclaw-performance.yml"; const FULL_RELEASE_CHILD_DISPATCHES = [ @@ -2967,6 +2968,7 @@ describe("package acceptance workflow", () => { const decisionUpload = workflowStep(decision, "Upload release decision"); const drainUpload = workflowStep(drain, "Upload diagnostic drain manifest"); const planStep = workflowStep(executionPlan, "Seal immutable release execution plan"); + const planCache = workflowStep(executionPlan, "Cache immutable release execution plan"); const planUpload = workflowStep(executionPlan, "Upload immutable release execution plan"); const manifestStep = workflowStep(summary, "Write release validation manifest"); const selectState = workflowStep(summary, "Select newest compatible release state artifacts"); @@ -2999,7 +3001,12 @@ describe("package acceptance workflow", () => { expect(planStep.run).not.toContain("EVIDENCE_MANIFEST"); expect(planStep.run).toContain('--arg evidenceRunId "$EVIDENCE_RUN_ID"'); expect(planStep.run).toContain('--argjson trustedWorkflow "$TRUSTED_WORKFLOW_JSON"'); - expect(planUpload.if).toBe("${{ always() && github.run_attempt == 1 }}"); + expect(planCache.uses).toBe(ACTIONS_CACHE_V5); + expect(planCache.with).toMatchObject({ + "fail-on-cache-miss": "${{ github.run_attempt != 1 }}", + key: "full-release-execution-plan-v1-${{ github.run_id }}", + }); + expect(planUpload.if).toBe("always()"); expect(planUpload.with?.name).toBe("full-release-execution-plan-${{ github.run_id }}"); expect(manifestStep.env).not.toHaveProperty("EVIDENCE_MANIFEST"); expect(manifestStep.run).toContain( @@ -3065,7 +3072,7 @@ describe("package acceptance workflow", () => { (job.steps ?? []).filter((step) => step.uses?.startsWith("actions/download-artifact@")), ); - expect(downloadSteps).toHaveLength(6); + expect(downloadSteps).toHaveLength(5); for (const step of downloadSteps) { expect(step.uses).toBe(DOWNLOAD_ARTIFACT_V8); }