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
This commit is contained in:
Dallin Romney
2026-08-21 19:08:32 -07:00
committed by GitHub
parent b3d3404ae6
commit 53fbe2eb33
5 changed files with 35 additions and 18 deletions
+4 -3
View File
@@ -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-<run-id>` artifact. Collector retries restore that
artifact and adopt its children; they never reconstruct the plan or redispatch
tests.
`full-release-execution-plan-<run-id>` 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,
+8 -5
View File
@@ -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-<run-id>-<attempt>` 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-<run-id>` 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
@@ -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 }}
+7 -3
View File
@@ -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-<run-id>` artifact. It records selected and
`full-release-execution-plan-<run-id>` 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
@@ -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);
}