mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(ci): authenticate release validation fast paths
This commit is contained in:
@@ -33,6 +33,16 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
full_release_validation_run_id:
|
||||
description: Optional authenticated Full Release Validation parent run ID
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
full_release_validation_run_attempt:
|
||||
description: Optional authenticated Full Release Validation parent run attempt
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
historical_target_tag:
|
||||
description: Semver release tag authorizing compatibility fallbacks for its exact commit
|
||||
required: false
|
||||
@@ -76,6 +86,7 @@ jobs:
|
||||
# work fan out from a single source of truth.
|
||||
preflight:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid') && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association)) && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
@@ -132,11 +143,70 @@ jobs:
|
||||
run_openclawkit_tests: ${{ steps.manifest.outputs.run_openclawkit_tests }}
|
||||
run_ios_build: ${{ steps.manifest.outputs.run_ios_build }}
|
||||
run_ios_screenshots: ${{ steps.changed_scope.outputs.run_ios_screenshots }}
|
||||
trusted_frv_first_attempt: ${{ steps.frv_parent.outputs.trusted_first_attempt }}
|
||||
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
|
||||
use_compatible_android_ci: ${{ steps.manifest.outputs.use_compatible_android_ci }}
|
||||
run_protocol_event_coverage: ${{ steps.manifest.outputs.run_protocol_event_coverage }}
|
||||
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
|
||||
steps:
|
||||
- name: Validate Full Release Validation parent
|
||||
id: frv_parent
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PARENT_RUN_ATTEMPT: ${{ inputs.full_release_validation_run_attempt }}
|
||||
PARENT_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
TARGET_REF: ${{ inputs.target_ref }}
|
||||
WORKFLOW_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "trusted_first_attempt=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
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
|
||||
fi
|
||||
if [[ ! "$TARGET_REF" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "::error::Full Release Validation parent routing requires an exact target SHA." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
parent_run="$(
|
||||
gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${PARENT_RUN_ID}/attempts/${PARENT_RUN_ATTEMPT}"
|
||||
)"
|
||||
expected_title="Full Release Validation ${TARGET_REF}"
|
||||
if ! jq -e \
|
||||
--arg branch "$GITHUB_REF_NAME" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$WORKFLOW_SHA" \
|
||||
--arg title "$expected_title" \
|
||||
--argjson attempt "$PARENT_RUN_ATTEMPT" \
|
||||
--argjson run_id "$PARENT_RUN_ID" \
|
||||
'
|
||||
.id == $run_id
|
||||
and .run_attempt == $attempt
|
||||
and .event == "workflow_dispatch"
|
||||
and .path == ".github/workflows/full-release-validation.yml"
|
||||
and .head_sha == $sha
|
||||
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
|
||||
jq '{id, run_attempt, event, path, head_sha, head_branch, repository: .repository.full_name, display_title, status, conclusion}' \
|
||||
<<< "$parent_run" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$PARENT_RUN_ATTEMPT" == "1" && "$GITHUB_RUN_ATTEMPT" == "1" ]]; then
|
||||
echo "trusted_first_attempt=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Validate release-gate dispatch
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_gate
|
||||
env:
|
||||
@@ -3688,9 +3758,9 @@ jobs:
|
||||
name: "ios-build"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_ios_build == 'true'
|
||||
# The FRV child is a trusted exact-SHA dispatch with an identity-bound nonce.
|
||||
# Hosted preflight authenticates the exact parent run before this job routes.
|
||||
# Keep ordinary/manual/release-gate runs and every retry on hosted capacity.
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'macos-26' || github.run_attempt > 1 && 'macos-26' || (github.event_name == 'workflow_dispatch' && (inputs.release_gate || !startsWith(inputs.dispatch_id, 'full-release-validation-') || !endsWith(inputs.dispatch_id, '-ci'))) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association)) && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'macos-26' || github.run_attempt > 1 && 'macos-26' || (github.event_name == 'workflow_dispatch' && (inputs.release_gate || needs.preflight.outputs.trusted_frv_first_attempt != 'true')) && 'macos-26' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association)) && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
HISTORICAL_TARGET: ${{ needs.preflight.outputs.compatibility_target }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Full Release Validation
|
||||
run-name: Full Release Validation ${{ inputs.expected_sha || inputs.ref }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -991,7 +992,7 @@ jobs:
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci"
|
||||
dispatch_run_name="CI ${dispatch_id}"
|
||||
args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id")
|
||||
args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id" -f full_release_validation_run_id="$GITHUB_RUN_ID" -f full_release_validation_run_attempt="$GITHUB_RUN_ATTEMPT")
|
||||
if [[ "$TARGET_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
|
||||
args+=(-f historical_target_tag="$TARGET_REF")
|
||||
elif [[ "$TARGET_CONTEXT_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
|
||||
|
||||
@@ -2090,6 +2090,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.revision }}
|
||||
expected_sha: ${{ needs.resolve_target.outputs.revision }}
|
||||
lock_scope: matrix
|
||||
run_matrix: true
|
||||
fail_fast: ${{ fromJSON(needs.resolve_target.outputs.fail_fast) }}
|
||||
secrets:
|
||||
@@ -2108,6 +2109,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.revision }}
|
||||
expected_sha: ${{ needs.resolve_target.outputs.revision }}
|
||||
lock_scope: buzz
|
||||
run_buzz: true
|
||||
buzz_scenario: channel-canary,channel-mention-gating
|
||||
secrets:
|
||||
|
||||
@@ -13,6 +13,11 @@ on:
|
||||
description: Exact resolved SHA expected from the trusted caller
|
||||
required: true
|
||||
type: string
|
||||
lock_scope:
|
||||
description: Concurrency scope for a trusted single-lane reusable call
|
||||
required: false
|
||||
default: all
|
||||
type: string
|
||||
run_mock_parity:
|
||||
description: Run the mock parity lane
|
||||
required: false
|
||||
@@ -118,9 +123,9 @@ permissions:
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
# Release checks call Matrix and Buzz independently. Give those single-lane
|
||||
# calls separate locks while retaining the shared lock for aggregate runs.
|
||||
group: qa-lab-${{ github.event_name == 'workflow_call' && inputs.run_buzz && !inputs.run_matrix && 'buzz' || github.event_name == 'workflow_call' && inputs.run_matrix && !inputs.run_buzz && 'matrix' || 'all' }}-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}
|
||||
# Trusted reusable callers name their single-lane scope explicitly. Manual
|
||||
# and scheduled aggregate runs retain the shared all-lanes lock.
|
||||
group: qa-lab-${{ inputs.lock_scope || 'all' }}-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
@@ -438,6 +443,10 @@ jobs:
|
||||
if: inputs.expected_sha == '' || inputs.run_matrix
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 90
|
||||
concurrency:
|
||||
group: qa-live-matrix-${{ needs.validate_selected_ref.outputs.selected_revision }}
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
environment: qa-live-shared
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
@@ -502,6 +511,7 @@ jobs:
|
||||
concurrency:
|
||||
group: qa-live-buzz-shared
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
environment: qa-live-shared
|
||||
steps:
|
||||
- name: Checkout selected ref
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
The exact Full Release Validation child routes `ios-build` to Blacksmith on its first attempt. Ordinary manual dispatches, exact-head `release_gate` fallbacks, non-canonical repositories, the `github` backend override, and every 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -274,7 +274,7 @@ concurrent repositories, retries, and burst overlap.
|
||||
|
||||
The changed-target PR plan reduces the common Node test burst from 29 Blacksmith registrations to one. Broad-risk all-Blacksmith PRs keep the 32-registration metadata-complete compact fallback; canonical all-Blacksmith pushes use 24 nondist compact registrations. Hybrid currently uses 53 nondist fallback rows and 46 nondist push rows. The `github` and `hybrid` planner profiles remain capped at 96 compact rows; `github` rows are hosted, while hybrid rows consume Blacksmith registrations on attempt 1 and move to hosted capacity on retries. Even the 96-row cap plus roughly 25 other Blacksmith lanes is about 121 registrations per full run, or 484 for four admitted runs in a five-minute window, far below the 6,000-registration operating target.
|
||||
|
||||
Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs when the backend is unset or `blacksmith`. Hybrid keeps the heavy set plus the named critical-path plateau lanes on Blacksmith for attempt 1; other light lanes and every rerun Blacksmith lane use GitHub-hosted capacity. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu in every mode; push retries remain on their normal backend unless hybrid fallback applies. All `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. The [`github` backend](#runner-backend-modes) provides a manual repository-wide fallback; canonical runs do not probe Blacksmith queue health or mutate the variable automatically.
|
||||
Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs when the backend is unset or `blacksmith`. Hybrid keeps the heavy set plus the named critical-path plateau lanes on Blacksmith for attempt 1; other light lanes and every rerun Blacksmith lane use GitHub-hosted capacity. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu in every mode; push retries remain on their normal backend unless hybrid fallback applies. Ordinary `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. The authenticated first-attempt exact-SHA Full Release Validation iOS child is the sole manual-dispatch exception. The [`github` backend](#runner-backend-modes) provides a manual repository-wide fallback; canonical runs do not probe Blacksmith queue health or mutate the variable automatically.
|
||||
|
||||
## Surface ratchets
|
||||
|
||||
|
||||
@@ -93,8 +93,8 @@ function evaluateWorkflowExpression(
|
||||
repository: string;
|
||||
runnerBackend?: "" | "blacksmith" | "github" | "hybrid";
|
||||
runAttempt: number;
|
||||
trustedFrvFirstAttempt?: boolean;
|
||||
inputs?: {
|
||||
dispatchId?: string;
|
||||
releaseGate?: boolean;
|
||||
};
|
||||
},
|
||||
@@ -116,7 +116,6 @@ function evaluateWorkflowExpression(
|
||||
Array.isArray(haystack)
|
||||
? haystack.includes(needle)
|
||||
: String(haystack).includes(String(needle)),
|
||||
endsWith: (value: unknown, suffix: unknown) => String(value).endsWith(String(suffix)),
|
||||
fromJSON: (value: string) => JSON.parse(value) as unknown,
|
||||
github: {
|
||||
event_name: context.eventName,
|
||||
@@ -133,11 +132,16 @@ function evaluateWorkflowExpression(
|
||||
: {},
|
||||
},
|
||||
inputs: {
|
||||
dispatch_id: context.inputs?.dispatchId ?? "",
|
||||
release_gate: context.inputs?.releaseGate ?? false,
|
||||
},
|
||||
matrix: context.matrix ?? {},
|
||||
startsWith: (value: unknown, prefix: unknown) => String(value).startsWith(String(prefix)),
|
||||
needs: {
|
||||
preflight: {
|
||||
outputs: {
|
||||
trusted_frv_first_attempt: context.trustedFrvFirstAttempt ? "true" : "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
vars: {
|
||||
OPENCLAW_CI_RUNNER_BACKEND: context.runnerBackend ?? "",
|
||||
},
|
||||
@@ -1422,13 +1426,19 @@ describe("ci workflow guards", () => {
|
||||
|
||||
expect(workflow.concurrency).toEqual({
|
||||
group:
|
||||
"qa-lab-${{ github.event_name == 'workflow_call' && inputs.run_buzz && !inputs.run_matrix && 'buzz' || github.event_name == 'workflow_call' && inputs.run_matrix && !inputs.run_buzz && 'matrix' || 'all' }}-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}",
|
||||
"qa-lab-${{ inputs.lock_scope || 'all' }}-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}",
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
expect(workflow.jobs.run_live_matrix.concurrency).toEqual({
|
||||
group: "qa-live-matrix-${{ needs.validate_selected_ref.outputs.selected_revision }}",
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
expect(workflow.jobs.run_live_buzz.concurrency).toEqual({
|
||||
group: "qa-live-buzz-shared",
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1728,7 +1738,7 @@ NODE
|
||||
expect(changedScopeStep.run).toContain(
|
||||
'node scripts/ci-changed-scope.mjs --base "$BASE" --head "$HEAD_SHA"',
|
||||
);
|
||||
expect(workflow.jobs.preflight.permissions).toEqual({ contents: "read" });
|
||||
expect(workflow.jobs.preflight.permissions).toEqual({ actions: "read", contents: "read" });
|
||||
expect(workflow.jobs.preflight.outputs.run_ios_screenshots).toBe(
|
||||
"${{ steps.changed_scope.outputs.run_ios_screenshots }}",
|
||||
);
|
||||
@@ -1761,17 +1771,123 @@ NODE
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates the exact Full Release Validation parent before fast routing", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const validateStep = expectDefined(
|
||||
workflow.jobs.preflight.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Validate Full Release Validation parent",
|
||||
),
|
||||
"Full Release Validation parent validation",
|
||||
);
|
||||
const targetSha = "a".repeat(40);
|
||||
const workflowSha = "b".repeat(40);
|
||||
const baseRun = {
|
||||
conclusion: null,
|
||||
display_title: `Full Release Validation ${targetSha}`,
|
||||
event: "workflow_dispatch",
|
||||
head_branch: "main",
|
||||
head_sha: workflowSha,
|
||||
id: 32441524595,
|
||||
path: ".github/workflows/full-release-validation.yml",
|
||||
repository: { full_name: "openclaw/openclaw" },
|
||||
run_attempt: 1,
|
||||
status: "in_progress",
|
||||
};
|
||||
const runCase = (options?: {
|
||||
childAttempt?: number;
|
||||
parentAttempt?: number;
|
||||
parentRun?: Record<string, unknown>;
|
||||
provideParent?: boolean;
|
||||
}) => {
|
||||
const root = tempDirs.make("openclaw-frv-parent-");
|
||||
const binDir = path.join(root, "bin");
|
||||
const callsPath = path.join(root, "gh-calls");
|
||||
const outputPath = path.join(root, "github-output");
|
||||
mkdirSync(binDir);
|
||||
writeExecutable(path.join(binDir, "gh"), [
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'printf "%s\\n" "$*" >> "$GH_CALLS"',
|
||||
'printf "%s\\n" "$MOCK_GH_RESPONSE"',
|
||||
]);
|
||||
const provideParent = options?.provideParent ?? true;
|
||||
const parentAttempt = options?.parentAttempt ?? 1;
|
||||
const parentRun = options?.parentRun ?? { ...baseRun, run_attempt: parentAttempt };
|
||||
const result = runWorkflowShellScript(expectDefined(validateStep.run, "validation script"), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
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),
|
||||
PARENT_RUN_ATTEMPT: provideParent ? String(parentAttempt) : "",
|
||||
PARENT_RUN_ID: provideParent ? String(baseRun.id) : "",
|
||||
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
||||
TARGET_REF: targetSha,
|
||||
WORKFLOW_SHA: workflowSha,
|
||||
},
|
||||
});
|
||||
return {
|
||||
calls: existsSync(callsPath) ? readFileSync(callsPath, "utf8") : "",
|
||||
outputs: readWorkflowOutputs(outputPath),
|
||||
result,
|
||||
};
|
||||
};
|
||||
|
||||
expect(workflow.on.workflow_dispatch.inputs.full_release_validation_run_id).toMatchObject({
|
||||
default: "",
|
||||
required: false,
|
||||
type: "string",
|
||||
});
|
||||
expect(workflow.on.workflow_dispatch.inputs.full_release_validation_run_attempt).toMatchObject({
|
||||
default: "",
|
||||
required: false,
|
||||
type: "string",
|
||||
});
|
||||
expect(workflow.jobs.preflight.outputs.trusted_frv_first_attempt).toBe(
|
||||
"${{ steps.frv_parent.outputs.trusted_first_attempt }}",
|
||||
);
|
||||
|
||||
const trusted = runCase();
|
||||
expect(trusted.result.status, trusted.result.stderr).toBe(0);
|
||||
expect(trusted.outputs.trusted_first_attempt).toBe("true");
|
||||
expect(trusted.calls).toContain("repos/openclaw/openclaw/actions/runs/32441524595/attempts/1");
|
||||
|
||||
const generic = runCase({ provideParent: false });
|
||||
expect(generic.result.status, generic.result.stderr).toBe(0);
|
||||
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 parentRun of [
|
||||
{ ...baseRun, id: baseRun.id + 1 },
|
||||
{ ...baseRun, run_attempt: 2 },
|
||||
{ ...baseRun, head_sha: "c".repeat(40) },
|
||||
{ ...baseRun, display_title: "Full Release Validation main" },
|
||||
{ ...baseRun, path: ".github/workflows/ci.yml" },
|
||||
{ ...baseRun, status: "completed", conclusion: "success" },
|
||||
]) {
|
||||
const rejected = runCase({ parentRun });
|
||||
expect(rejected.result.status, JSON.stringify(parentRun)).toBe(1);
|
||||
expect(rejected.outputs.trusted_first_attempt).toBe("false");
|
||||
}
|
||||
});
|
||||
|
||||
it("routes only trusted first-attempt Full Release Validation iOS to Blacksmith", () => {
|
||||
const runsOn = readCiWorkflow().jobs["ios-build"]["runs-on"];
|
||||
const frvInputs = {
|
||||
dispatchId: "full-release-validation-32441524595-1-ci",
|
||||
releaseGate: false,
|
||||
};
|
||||
const canonicalDispatch = {
|
||||
eventName: "workflow_dispatch",
|
||||
inputs: frvInputs,
|
||||
inputs: { releaseGate: false },
|
||||
repository: "openclaw/openclaw",
|
||||
runAttempt: 1,
|
||||
trustedFrvFirstAttempt: true,
|
||||
} as const;
|
||||
|
||||
expect(evaluateWorkflowExpression(runsOn, canonicalDispatch)).toBe(
|
||||
@@ -1792,13 +1908,13 @@ NODE
|
||||
expect(
|
||||
evaluateWorkflowExpression(runsOn, {
|
||||
...canonicalDispatch,
|
||||
inputs: { dispatchId: "", releaseGate: false },
|
||||
trustedFrvFirstAttempt: false,
|
||||
}),
|
||||
).toBe("macos-26");
|
||||
expect(
|
||||
evaluateWorkflowExpression(runsOn, {
|
||||
...canonicalDispatch,
|
||||
inputs: { ...frvInputs, releaseGate: true },
|
||||
inputs: { releaseGate: true },
|
||||
}),
|
||||
).toBe("macos-26");
|
||||
expect(
|
||||
|
||||
@@ -2771,6 +2771,7 @@ describe("package acceptance workflow", () => {
|
||||
|
||||
it("requires full release child workflows to run at the parent workflow SHA", () => {
|
||||
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
|
||||
const parsedWorkflow = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW);
|
||||
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
||||
const performanceJob = workflowStep(
|
||||
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"),
|
||||
@@ -2778,6 +2779,9 @@ describe("package acceptance workflow", () => {
|
||||
).run;
|
||||
|
||||
expect(workflow).toContain("TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}");
|
||||
expect(parsedWorkflow["run-name"]).toBe(
|
||||
"Full Release Validation ${{ inputs.expected_sha || inputs.ref }}",
|
||||
);
|
||||
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
|
||||
expect(workflow).toContain("PARENT_WORKFLOW_SHA: ${{ github.sha }}");
|
||||
expect(workflow).toContain("release_package_spec:");
|
||||
@@ -2807,6 +2811,8 @@ describe("package acceptance workflow", () => {
|
||||
);
|
||||
expect(workflow).toContain('if [[ "$head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then');
|
||||
expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1');
|
||||
expect(workflow).toContain('-f full_release_validation_run_id="$GITHUB_RUN_ID"');
|
||||
expect(workflow).toContain('-f full_release_validation_run_attempt="$GITHUB_RUN_ATTEMPT"');
|
||||
expect(performanceJob).toContain(
|
||||
'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"',
|
||||
);
|
||||
@@ -4888,6 +4894,7 @@ describe("package artifact reuse", () => {
|
||||
expect(releaseJob.with).toMatchObject({
|
||||
expected_sha: "${{ needs.resolve_target.outputs.revision }}",
|
||||
fail_fast: "${{ fromJSON(needs.resolve_target.outputs.fail_fast) }}",
|
||||
lock_scope: "matrix",
|
||||
run_matrix: true,
|
||||
});
|
||||
for (const lane of ["mock_parity", "buzz", "telegram", "discord", "whatsapp", "slack"]) {
|
||||
@@ -4923,6 +4930,11 @@ describe("package artifact reuse", () => {
|
||||
expect(qaWorkflow).toContain('if [[ -n "${EXPECTED_SHA}" ]]; then');
|
||||
const matrixJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix");
|
||||
expect(matrixJob["timeout-minutes"]).toBe(90);
|
||||
expect(matrixJob.concurrency).toEqual({
|
||||
group: "qa-live-matrix-${{ needs.validate_selected_ref.outputs.selected_revision }}",
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
expect(workflowStep(matrixJob, "Run Matrix live lane").run).toContain(
|
||||
"--provider-mode mock-openai",
|
||||
);
|
||||
@@ -4962,10 +4974,16 @@ describe("package artifact reuse", () => {
|
||||
expect(releaseJob.with).toMatchObject({
|
||||
buzz_scenario: "channel-canary,channel-mention-gating",
|
||||
expected_sha: "${{ needs.resolve_target.outputs.revision }}",
|
||||
lock_scope: "buzz",
|
||||
run_buzz: true,
|
||||
});
|
||||
const buzzJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_buzz");
|
||||
expect(buzzJob.if).toBe("inputs.run_buzz");
|
||||
expect(buzzJob.concurrency).toEqual({
|
||||
group: "qa-live-buzz-shared",
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
const resolveBuzz = workflowStep(buzzJob, "Resolve Buzz QA runner");
|
||||
expect(resolveBuzz.run).toContain('runner?.commandName === "buzz"');
|
||||
expect(resolveBuzz.run).toContain("selected ref does not declare the Buzz QA runner");
|
||||
|
||||
Reference in New Issue
Block a user