fix(ci): fall back when FRV routing auth is unavailable

This commit is contained in:
Vincent Koc
2026-08-21 02:45:33 -07:00
parent 8824dcc497
commit d68f7ec77c
3 changed files with 52 additions and 18 deletions
+28 -13
View File
@@ -152,31 +152,44 @@ jobs:
- name: Validate Full Release Validation parent
id: frv_parent
env:
CI_RUNNER_BACKEND: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND }}
GH_TOKEN: ${{ github.token }}
PARENT_RUN_ATTEMPT: ${{ inputs.full_release_validation_run_attempt }}
PARENT_RUN_ID: ${{ inputs.full_release_validation_run_id }}
RELEASE_GATE: ${{ inputs.release_gate }}
TARGET_REF: ${{ inputs.target_ref }}
WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
echo "trusted_first_attempt=false" >> "$GITHUB_OUTPUT"
# Parent authentication only unlocks the attempt-one fast route. Any
# hosted-policy case must remain independent of the parent API.
if [[ "$GITHUB_RUN_ATTEMPT" != "1" || "$CI_RUNNER_BACKEND" == "github" || "$RELEASE_GATE" == "true" ]]; then
exit 0
fi
if [[ -z "$PARENT_RUN_ID" && -z "$PARENT_RUN_ATTEMPT" ]]; then
exit 0
fi
if [[ ! "$PARENT_RUN_ID" =~ ^[1-9][0-9]*$ || ! "$PARENT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::Full Release Validation parent run ID and attempt must be positive integers." >&2
exit 1
echo "::warning::Full Release Validation parent identity is malformed; routing iOS to GitHub-hosted macOS." >&2
exit 0
fi
if [[ "$PARENT_RUN_ATTEMPT" != "1" ]]; then
exit 0
fi
if [[ ! "$TARGET_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Full Release Validation parent routing requires an exact target SHA." >&2
exit 1
echo "::warning::Full Release Validation parent routing requires an exact target SHA; routing iOS to GitHub-hosted macOS." >&2
exit 0
fi
parent_run="$(
if ! parent_run="$(
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${PARENT_RUN_ID}/attempts/${PARENT_RUN_ATTEMPT}"
)"
"repos/${GITHUB_REPOSITORY}/actions/runs/${PARENT_RUN_ID}/attempts/${PARENT_RUN_ATTEMPT}" 2>&1
)"; then
echo "::warning::Could not authenticate the Full Release Validation parent through the Actions API; routing iOS to GitHub-hosted macOS." >&2
exit 0
fi
expected_title="Full Release Validation ${TARGET_REF}"
if ! jq -e \
--arg branch "$GITHUB_REF_NAME" \
@@ -194,19 +207,21 @@ jobs:
and .head_branch == $branch
and .repository.full_name == $repository
and .display_title == $title
and .status == "in_progress"
and .conclusion == null
' <<< "$parent_run" >/dev/null; then
echo "::error::Full Release Validation parent identity did not match this CI target and workflow." >&2
echo "::warning::Full Release Validation parent identity did not match this CI target and workflow; routing iOS to GitHub-hosted macOS." >&2
jq '{id, run_attempt, event, path, head_sha, head_branch, repository: .repository.full_name, display_title, status, conclusion}' \
<<< "$parent_run" >&2 || true
exit 1
exit 0
fi
if [[ "$PARENT_RUN_ATTEMPT" == "1" && "$GITHUB_RUN_ATTEMPT" == "1" ]]; then
echo "trusted_first_attempt=true" >> "$GITHUB_OUTPUT"
if ! jq -e '.status == "in_progress" and .conclusion == null' \
<<< "$parent_run" >/dev/null; then
echo "::warning::Full Release Validation parent is no longer active; routing iOS to GitHub-hosted macOS." >&2
exit 0
fi
echo "trusted_first_attempt=true" >> "$GITHUB_OUTPUT"
- name: Validate release-gate dispatch
if: github.event_name == 'workflow_dispatch' && inputs.release_gate
env:
+1 -1
View File
@@ -182,7 +182,7 @@ Barnacle treats bug-labeled issues as verification candidates rather than inacti
Ordinary manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI/native app i18n. Node 22 compatibility runs in Full Release Validation and manual dispatches only; push and pull request CI skip it. The exact-head `release_gate` fallback instead keeps the pull request's macOS and iOS scope, including conservative release screenshot capture for screenshot-pipeline owners. Automatic source PRs verify native extraction inventory and Android/Apple localization safety without requiring translated or platform-generated output in the same PR. The serialized Native App Locale Refresh workflow rebuilds those artifacts in one isolated PR and enables exact-head auto-merge after required checks pass. Full native parity remains blocking for generated-artifact PRs, manual CI, Full Release Validation, and release prep. Control UI locale parity remains advisory on automatic PR and `main` runs and blocking on manual/release CI. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
An exact-SHA Full Release Validation child routes `ios-build` to Blacksmith only when hosted preflight authenticates the live parent run ID, first attempt, workflow SHA, workflow ref, and target SHA through the Actions API. Ordinary manual dispatches, exact-head `release_gate` fallbacks, non-canonical repositories, the `github` backend override, and every parent or child rerun stay on GitHub-hosted macOS.
An exact-SHA Full Release Validation child routes `ios-build` to Blacksmith only when hosted preflight authenticates the live parent run ID, first attempt, workflow SHA, workflow ref, and target SHA through the Actions API. Ordinary manual dispatches, exact-head `release_gate` fallbacks, non-canonical repositories, the `github` backend override, and every parent or child rerun stay on GitHub-hosted macOS. Parent API errors, identity mismatches, and parents that finish before child preflight also fall back to hosted macOS without failing CI.
PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. The max-lines entry chains the environment-variable budget with the same fork-point ref before the assertion-safety check, so production source growth cannot first surface on `main`. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated.
+23 -4
View File
@@ -1795,9 +1795,12 @@ NODE
};
const runCase = (options?: {
childAttempt?: number;
ghStatus?: number;
parentAttempt?: number;
parentRun?: Record<string, unknown>;
provideParent?: boolean;
releaseGate?: boolean;
runnerBackend?: string;
}) => {
const root = tempDirs.make("openclaw-frv-parent-");
const binDir = path.join(root, "bin");
@@ -1809,6 +1812,7 @@ NODE
"set -euo pipefail",
'printf "%s\\n" "$*" >> "$GH_CALLS"',
'printf "%s\\n" "$MOCK_GH_RESPONSE"',
'exit "${MOCK_GH_STATUS:-0}"',
]);
const provideParent = options?.provideParent ?? true;
const parentAttempt = options?.parentAttempt ?? 1;
@@ -1817,15 +1821,18 @@ NODE
cwd: root,
env: {
...process.env,
CI_RUNNER_BACKEND: options?.runnerBackend ?? "",
GH_CALLS: callsPath,
GITHUB_OUTPUT: outputPath,
GITHUB_REF_NAME: "main",
GITHUB_REPOSITORY: "openclaw/openclaw",
GITHUB_RUN_ATTEMPT: String(options?.childAttempt ?? 1),
MOCK_GH_RESPONSE: JSON.stringify(parentRun),
MOCK_GH_STATUS: String(options?.ghStatus ?? 0),
PARENT_RUN_ATTEMPT: provideParent ? String(parentAttempt) : "",
PARENT_RUN_ID: provideParent ? String(baseRun.id) : "",
PATH: `${binDir}:${process.env.PATH ?? ""}`,
RELEASE_GATE: options?.releaseGate ? "true" : "false",
TARGET_REF: targetSha,
WORKFLOW_SHA: workflowSha,
},
@@ -1861,9 +1868,15 @@ NODE
expect(generic.outputs.trusted_first_attempt).toBe("false");
expect(generic.calls).toBe("");
for (const rerun of [runCase({ childAttempt: 2 }), runCase({ parentAttempt: 2 })]) {
expect(rerun.result.status, rerun.result.stderr).toBe(0);
expect(rerun.outputs.trusted_first_attempt).toBe("false");
for (const hostedFallback of [
runCase({ childAttempt: 2 }),
runCase({ parentAttempt: 2 }),
runCase({ releaseGate: true }),
runCase({ runnerBackend: "github" }),
]) {
expect(hostedFallback.result.status, hostedFallback.result.stderr).toBe(0);
expect(hostedFallback.outputs.trusted_first_attempt).toBe("false");
expect(hostedFallback.calls).toBe("");
}
for (const parentRun of [
@@ -1875,9 +1888,15 @@ NODE
{ ...baseRun, status: "completed", conclusion: "success" },
]) {
const rejected = runCase({ parentRun });
expect(rejected.result.status, JSON.stringify(parentRun)).toBe(1);
expect(rejected.result.status, JSON.stringify(parentRun)).toBe(0);
expect(rejected.outputs.trusted_first_attempt).toBe("false");
expect(rejected.result.stderr).toContain("::warning::");
}
const apiFailure = runCase({ ghStatus: 1 });
expect(apiFailure.result.status, apiFailure.result.stderr).toBe(0);
expect(apiFailure.outputs.trusted_first_attempt).toBe("false");
expect(apiFailure.result.stderr).toContain("::warning::");
});
it("routes only trusted first-attempt Full Release Validation iOS to Blacksmith", () => {