fix(release): reject unrecoverable FRV continuation (#132711)

This commit is contained in:
Vincent Koc 2026-08-30 00:29:10 +08:00 • committed by GitHub
parent 50e1f091ee
commit 55abb43917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 64 additions and 2 deletions

View file

@ -58,6 +58,10 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
- Same-parent continuation requires the original root to have been dispatched - Same-parent continuation requires the original root to have been dispatched
with `fail_fast=false`. The controller verifies that exact logged input with `fail_fast=false`. The controller verifies that exact logged input
before any rerun mutation. before any rerun mutation.
- A parent that produced its own sealed candidate artifacts cannot be continued:
GitHub reruns make those prior-attempt artifacts unavailable. Keep the
candidate and Tooling SHAs frozen, supersede that parent, and start a fresh
all-group Full Release Validation.
- After dispatch, one immutable execution-plan artifact records the original - After dispatch, one immutable execution-plan artifact records the original
parent attempt, exact child tuples and titles, selected coverage, gates, and parent attempt, exact child tuples and titles, selected coverage, gates, and
reuse identity. The same bytes are saved under an exact run-ID cache key. reuse identity. The same bytes are saved under an exact run-ID cache key.

View file

@ -1674,8 +1674,8 @@ jobs:
DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json
run: node scripts/full-release-validation-state.mjs verify run: node scripts/full-release-validation-state.mjs verify
# Artifact v4 keeps prior attempts readable within the same workflow run. # Exact IDs from the sealed plan prevent verification from drifting to a newer upload.
# Exact IDs from the sealed plan prevent recovery from drifting to a newer upload. # Parent-owned candidate plans are rejected before same-parent continuation.
- name: Verify sealed release candidate - name: Verify sealed release candidate
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View file

@ -46,6 +46,10 @@ still-active child that owns the blocking failure.
Same-parent continuation requires the original root to have been dispatched Same-parent continuation requires the original root to have been dispatched
with `fail_fast=false`. The controller verifies that exact logged input before with `fail_fast=false`. The controller verifies that exact logged input before
any rerun mutation. any rerun mutation.
It is also unavailable when that parent produced the sealed candidate
artifacts, because GitHub reruns make those prior-attempt artifacts unavailable.
Keep the candidate and Tooling SHAs frozen, supersede the parent, and start a
fresh all-group Full Release Validation.
After dispatch, the parent writes one immutable After dispatch, the parent writes one immutable
`full-release-execution-plan-<run-id>` artifact and preserves the same bytes in `full-release-execution-plan-<run-id>` artifact and preserves the same bytes in

View file

@ -249,6 +249,11 @@ export async function preflightContinuation(
client, client,
repository = DEFAULT_REPOSITORY, repository = DEFAULT_REPOSITORY,
) { ) {
if (plan.candidate?.producer.runId === String(rootRunId)) {
throw new Error(
"parent-owned sealed candidate artifacts do not survive parent reruns; start a fresh all-group FRV",
);
}
if (plan.rerunGroup !== "all") { if (plan.rerunGroup !== "all") {
throw new Error("FRV continuation requires an all-group root"); throw new Error("FRV continuation requires an all-group root");
} }

View file

@ -418,6 +418,55 @@ describe("FRV immutable plan eligibility", () => {
}); });
describe("FRV continuation preflight", () => { describe("FRV continuation preflight", () => {
it("rejects parent-owned candidate artifacts before any GitHub access", async () => {
const selected = child("normalCi", "101");
const parentOwnedPlan = {
...plan([selected]),
candidate: { producer: { runId: "77" } },
};
let reads = 0;
let mutations = 0;
const read = async () => {
reads += 1;
throw new Error("unexpected GitHub read");
};
const mutate = async () => {
mutations += 1;
};
await expect(
continueFailed(parentOwnedPlan, "77", {
getAttemptJobs: read,
getJobLog: read,
getParentJobs: read,
getRun: read,
getRunAttempt: read,
repository: REPOSITORY,
rerunFailed: mutate,
rerunParent: mutate,
verify: mutate,
}),
).rejects.toThrow(
"parent-owned sealed candidate artifacts do not survive parent reruns; start a fresh all-group FRV",
);
expect(reads).toBe(0);
expect(mutations).toBe(0);
});
it.each([
["candidate-free", undefined],
["externally produced", { producer: { runId: "88" } }],
])("allows %s plans through candidate ownership preflight", async (_label, candidate) => {
const selected = child("normalCi", "101");
await expect(
preflightContinuation(
{ ...plan([selected]), candidate },
"77",
preflightMethods([selected], (entry) => runFor(entry, 1, "failure")),
),
).resolves.toMatchObject({ id: 77 });
});
it("rejects fail-fast roots before any rerun mutation", async () => { it("rejects fail-fast roots before any rerun mutation", async () => {
const selected = child("normalCi", "101"); const selected = child("normalCi", "101");
let mutations = 0; let mutations = 0;