























1+name: Release QA Profile Evidence
2+3+run-name: ${{ format('Release QA Profile Evidence {0}', inputs.ref) }}
4+5+on:
6+workflow_dispatch:
7+inputs:
8+ref:
9+description: OpenClaw branch, tag, or SHA to run
10+required: true
11+default: main
12+type: string
13+expected_sha:
14+description: Optional full SHA that ref must resolve to
15+required: false
16+default: ""
17+type: string
18+workflow_call:
19+inputs:
20+ref:
21+description: OpenClaw branch, tag, or SHA to run
22+required: true
23+type: string
24+expected_sha:
25+description: Optional full SHA that ref must resolve to
26+required: false
27+default: ""
28+type: string
29+outputs:
30+artifact_name:
31+description: Uploaded release QA profile evidence artifact name
32+value: ${{ jobs.run_release_profile.outputs.artifact_name }}
33+target_sha:
34+description: Resolved OpenClaw SHA that produced the evidence
35+value: ${{ jobs.run_release_profile.outputs.target_sha }}
36+trusted_reason:
37+description: Trust reason accepted before the secret-bearing QA job
38+value: ${{ jobs.run_release_profile.outputs.trusted_reason }}
39+qa_evidence_path:
40+description: Path to qa-evidence.json inside the uploaded artifact
41+value: ${{ jobs.run_release_profile.outputs.qa_evidence_path }}
42+43+permissions:
44+contents: read
45+46+concurrency:
47+group: release-qa-profile-evidence-${{ inputs.expected_sha || inputs.ref }}
48+cancel-in-progress: false
49+50+env:
51+FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
52+NODE_VERSION: "24.x"
53+OPENCLAW_BUILD_PRIVATE_QA: "1"
54+OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
55+OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
56+OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
57+58+jobs:
59+authorize_actor:
60+name: Authorize workflow actor
61+runs-on: blacksmith-8vcpu-ubuntu-2404
62+outputs:
63+authorized: ${{ steps.permission.outputs.authorized }}
64+steps:
65+ - name: Require maintainer-level repository access
66+id: permission
67+uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
68+with:
69+script: |
70+ if (context.eventName !== "workflow_dispatch") {
71+ core.info(`Skipping manual actor permission check for ${context.eventName}.`);
72+ core.setOutput("authorized", "true");
73+ return;
74+ }
75+ const allowed = new Set(["admin", "maintain", "write"]);
76+ const { owner, repo } = context.repo;
77+ const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
78+ owner,
79+ repo,
80+ username: context.actor,
81+ });
82+ const permission = data.permission;
83+ core.info(`Actor ${context.actor} permission: ${permission}`);
84+ if (!allowed.has(permission)) {
85+ core.notice(
86+ `Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
87+ );
88+ core.setOutput("authorized", "false");
89+ return;
90+ }
91+ core.setOutput("authorized", "true");
92+93+ validate_selected_ref:
94+name: Validate selected ref
95+needs: authorize_actor
96+if: needs.authorize_actor.outputs.authorized == 'true'
97+runs-on: blacksmith-8vcpu-ubuntu-2404
98+outputs:
99+selected_revision: ${{ steps.validate.outputs.selected_revision }}
100+trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
101+steps:
102+ - name: Checkout selected ref
103+uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
104+with:
105+persist-credentials: false
106+ref: ${{ inputs.ref }}
107+fetch-depth: 0
108+109+ - name: Validate selected ref
110+id: validate
111+env:
112+EXPECTED_SHA: ${{ inputs.expected_sha }}
113+INPUT_REF: ${{ inputs.ref }}
114+shell: bash
115+run: |
116+ set -euo pipefail
117+118+ selected_revision="$(git rev-parse HEAD)"
119+ expected_sha="${EXPECTED_SHA,,}"
120+ trusted_reason=""
121+122+ if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
123+ echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
124+ exit 1
125+ fi
126+ if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
127+ echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
128+ exit 1
129+ fi
130+131+ git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
132+133+ if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
134+ trusted_reason="main-ancestor"
135+ elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
136+ trusted_reason="release-tag"
137+ elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then
138+ git fetch --no-tags origin "+refs/heads/${INPUT_REF}:refs/remotes/origin/${INPUT_REF}"
139+ release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"
140+ if [[ "$selected_revision" == "$release_branch_sha" ]]; then
141+ trusted_reason="release-branch-head"
142+ fi
143+ fi
144+145+ if [[ -z "$trusted_reason" ]]; then
146+ echo "Ref '${INPUT_REF}' resolved to $selected_revision, which is not trusted for this secret-bearing QA evidence run." >&2
147+ echo "Allowed refs must be on main, point to a release tag, or match a release branch head." >&2
148+ exit 1
149+ fi
150+151+ echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"
152+ echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
153+ {
154+ echo "### Target"
155+ echo
156+ echo "- Requested ref: \`${INPUT_REF}\`"
157+ echo "- Resolved SHA: \`$selected_revision\`"
158+ echo "- Trust reason: \`$trusted_reason\`"
159+ } >> "$GITHUB_STEP_SUMMARY"
160+161+ run_release_profile:
162+name: Generate release QA profile evidence
163+needs: validate_selected_ref
164+runs-on: blacksmith-8vcpu-ubuntu-2404
165+timeout-minutes: 60
166+permissions:
167+contents: read
168+outputs:
169+artifact_name: ${{ steps.evidence.outputs.artifact_name }}
170+target_sha: ${{ steps.evidence.outputs.target_sha }}
171+trusted_reason: ${{ steps.evidence.outputs.trusted_reason }}
172+qa_evidence_path: ${{ steps.evidence.outputs.qa_evidence_path }}
173+environment: qa-live-shared
174+steps:
175+ - name: Checkout selected ref
176+uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
177+with:
178+persist-credentials: false
179+ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
180+fetch-depth: 1
181+182+ - name: Setup Node environment
183+uses: ./.github/actions/setup-node-env
184+with:
185+node-version: ${{ env.NODE_VERSION }}
186+install-bun: "true"
187+188+ - name: Validate required QA credential env
189+env:
190+OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
191+shell: bash
192+run: |
193+ set -euo pipefail
194+ if [[ -z "${OPENAI_API_KEY:-}" ]]; then
195+ echo "Missing required OPENAI_API_KEY." >&2
196+ exit 1
197+ fi
198+199+ - name: Build private QA runtime
200+env:
201+NODE_OPTIONS: --max-old-space-size=8192
202+run: node scripts/build-all.mjs qaRuntime
203+204+ - name: Run release QA profile
205+id: run_profile
206+env:
207+OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
208+shell: bash
209+run: |
210+ set -euo pipefail
211+212+ output_dir=".artifacts/qa-e2e/release-profile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
213+ echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
214+215+ qa_exit_code=0
216+ pnpm openclaw qa run \
217+ --repo-root . \
218+ --qa-profile release \
219+ --output-dir "${output_dir}" || qa_exit_code=$?
220+221+ echo "qa_exit_code=${qa_exit_code}" >> "$GITHUB_OUTPUT"
222+223+ - name: Validate release QA evidence
224+id: evidence
225+if: always()
226+env:
227+ARTIFACT_NAME: release-qa-profile-evidence-${{ needs.validate_selected_ref.outputs.selected_revision }}
228+OUTPUT_DIR: ${{ steps.run_profile.outputs.output_dir }}
229+REQUESTED_REF: ${{ inputs.ref }}
230+TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_revision }}
231+TRUSTED_REASON: ${{ needs.validate_selected_ref.outputs.trusted_reason }}
232+shell: bash
233+run: |
234+ set -euo pipefail
235+236+ node --input-type=module <<'NODE'
237+ import fs from "node:fs";
238+ import path from "node:path";
239+240+ const outputDir = process.env.OUTPUT_DIR;
241+ if (!outputDir) {
242+ throw new Error("OUTPUT_DIR is required");
243+ }
244+245+ const evidencePath = path.join(outputDir, "qa-evidence.json");
246+ const payload = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
247+ if (payload.profile !== "release") {
248+ throw new Error(`qa-evidence.json profile must be release, got ${JSON.stringify(payload.profile)}`);
249+ }
250+ if (!payload.scorecard || !Array.isArray(payload.scorecard.categoryReports)) {
251+ throw new Error("release qa-evidence.json must include scorecard.categoryReports");
252+ }
253+ if (payload.scorecard.categoryReports.length === 0) {
254+ throw new Error("release qa-evidence.json scorecard has no category reports");
255+ }
256+257+ const manifest = {
258+ artifactName: process.env.ARTIFACT_NAME,
259+ generatedAt: new Date().toISOString(),
260+ requestedRef: process.env.REQUESTED_REF,
261+ targetSha: process.env.TARGET_SHA,
262+ trustedReason: process.env.TRUSTED_REASON,
263+ evidenceMode: payload.evidenceMode,
264+ qaEvidencePath: "qa-evidence.json",
265+ scorecard: {
266+ categories: payload.scorecard.categories,
267+ features: payload.scorecard.features,
268+ categoryReports: payload.scorecard.categoryReports.length,
269+ },
270+ };
271+ fs.writeFileSync(
272+ path.join(outputDir, "release-qa-profile-evidence-manifest.json"),
273+ `${JSON.stringify(manifest, null, 2)}\n`,
274+ );
275+ NODE
276+277+ echo "artifact_name=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT"
278+ echo "target_sha=${TARGET_SHA}" >> "$GITHUB_OUTPUT"
279+ echo "trusted_reason=${TRUSTED_REASON}" >> "$GITHUB_OUTPUT"
280+ echo "qa_evidence_path=qa-evidence.json" >> "$GITHUB_OUTPUT"
281+ {
282+ echo "### Release QA profile evidence"
283+ echo
284+ echo "- Artifact: \`${ARTIFACT_NAME}\`"
285+ echo "- Target SHA: \`${TARGET_SHA}\`"
286+ echo "- Evidence path: \`${OUTPUT_DIR}/qa-evidence.json\`"
287+ echo "- Manifest: \`${OUTPUT_DIR}/release-qa-profile-evidence-manifest.json\`"
288+ } >> "$GITHUB_STEP_SUMMARY"
289+290+ - name: Upload release QA profile evidence
291+if: always()
292+uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
293+with:
294+name: release-qa-profile-evidence-${{ needs.validate_selected_ref.outputs.selected_revision }}
295+path: ${{ steps.run_profile.outputs.output_dir }}
296+retention-days: 30
297+if-no-files-found: error
298+299+ - name: Fail if release QA profile failed
300+if: always()
301+env:
302+QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
303+shell: bash
304+run: |
305+ set -euo pipefail
306+ if [[ -z "${QA_EXIT_CODE:-}" ]]; then
307+ echo "Release QA profile did not report an exit code." >&2
308+ exit 1
309+ fi
310+ if [[ "$QA_EXIT_CODE" != "0" ]]; then
311+ echo "Release QA profile failed with exit code ${QA_EXIT_CODE}." >&2
312+ exit "$QA_EXIT_CODE"
313+ fi
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。