




















1-name: Release QA Profile Evidence
1+name: QA Profile Evidence
223-run-name: ${{ format('Release QA Profile Evidence {0}', inputs.ref) }}
3+run-name: ${{ format('QA Profile Evidence {0} {1}', inputs.qa_profile, inputs.ref) }}
4455on:
66workflow_dispatch:
@@ -15,6 +15,11 @@ on:
1515required: false
1616default: ""
1717type: string
18+qa_profile:
19+description: Taxonomy QA profile id to run
20+required: true
21+default: release
22+type: string
1823workflow_call:
1924inputs:
2025ref:
@@ -26,25 +31,43 @@ on:
2631required: false
2732default: ""
2833type: string
34+qa_profile:
35+description: Taxonomy QA profile id to run
36+required: true
37+type: string
38+fail_on_qa_failure:
39+description: Fail the reusable workflow when the QA profile command exits non-zero
40+required: false
41+default: false
42+type: boolean
2943outputs:
3044artifact_name:
31-description: Uploaded release QA profile evidence artifact name
32-value: ${{ jobs.run_release_profile.outputs.artifact_name }}
45+description: Uploaded QA profile evidence artifact name
46+value: ${{ jobs.run_qa_profile.outputs.artifact_name }}
47+qa_profile:
48+description: Taxonomy QA profile id that produced the evidence
49+value: ${{ jobs.run_qa_profile.outputs.qa_profile }}
50+qa_exit_code:
51+description: Exit code from the QA profile run; non-zero evidence is still uploaded
52+value: ${{ jobs.run_qa_profile.outputs.qa_exit_code }}
53+qa_passed:
54+description: Whether the QA profile command exited successfully
55+value: ${{ jobs.run_qa_profile.outputs.qa_passed }}
3356target_sha:
3457description: Resolved OpenClaw SHA that produced the evidence
35-value: ${{ jobs.run_release_profile.outputs.target_sha }}
58+value: ${{ jobs.run_qa_profile.outputs.target_sha }}
3659trusted_reason:
3760description: Trust reason accepted before the secret-bearing QA job
38-value: ${{ jobs.run_release_profile.outputs.trusted_reason }}
61+value: ${{ jobs.run_qa_profile.outputs.trusted_reason }}
3962qa_evidence_path:
4063description: Path to qa-evidence.json inside the uploaded artifact
41-value: ${{ jobs.run_release_profile.outputs.qa_evidence_path }}
64+value: ${{ jobs.run_qa_profile.outputs.qa_evidence_path }}
42654366permissions:
4467contents: read
45684669concurrency:
47-group: release-qa-profile-evidence-${{ inputs.expected_sha || inputs.ref }}
70+group: qa-profile-evidence-${{ inputs.qa_profile }}-${{ inputs.expected_sha || inputs.ref }}
4871cancel-in-progress: false
49725073env:
@@ -158,15 +181,18 @@ jobs:
158181 echo "- Trust reason: \`$trusted_reason\`"
159182 } >> "$GITHUB_STEP_SUMMARY"
160183161- run_release_profile:
162-name: Generate release QA profile evidence
184+ run_qa_profile:
185+name: Generate QA profile evidence
163186needs: validate_selected_ref
164187runs-on: blacksmith-8vcpu-ubuntu-2404
165188timeout-minutes: 60
166189permissions:
167190contents: read
168191outputs:
169192artifact_name: ${{ steps.evidence.outputs.artifact_name }}
193+qa_profile: ${{ steps.profile.outputs.profile }}
194+qa_exit_code: ${{ steps.evidence.outputs.qa_exit_code }}
195+qa_passed: ${{ steps.evidence.outputs.qa_passed }}
170196target_sha: ${{ steps.evidence.outputs.target_sha }}
171197trusted_reason: ${{ steps.evidence.outputs.trusted_reason }}
172198qa_evidence_path: ${{ steps.evidence.outputs.qa_evidence_path }}
@@ -185,47 +211,67 @@ jobs:
185211node-version: ${{ env.NODE_VERSION }}
186212install-bun: "true"
187213188- - name: Validate required QA credential env
214+ - name: Validate QA profile input
215+id: profile
189216env:
190-OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
217+QA_PROFILE: ${{ inputs.qa_profile }}
191218shell: bash
192219run: |
193220 set -euo pipefail
194- if [[ -z "${OPENAI_API_KEY:-}" ]]; then
195- echo "Missing required OPENAI_API_KEY." >&2
196- exit 1
197- fi
221+ node --import tsx --input-type=module <<'NODE'
222+ import fs from "node:fs";
223+ import { readQaScorecardTaxonomyReport } from "./extensions/qa-lab/src/scorecard-taxonomy.ts";
224+225+ const requested = process.env.QA_PROFILE?.trim() ?? "";
226+ if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(requested)) {
227+ throw new Error(`qa_profile must use a taxonomy profile id, got ${JSON.stringify(process.env.QA_PROFILE)}`);
228+ }
229+230+ const taxonomy = readQaScorecardTaxonomyReport([]);
231+ const profile = taxonomy.profiles.find((entry) => entry.id === requested);
232+ if (!profile) {
233+ const available = taxonomy.profiles.map((entry) => entry.id).join(", ");
234+ throw new Error(`Unknown QA profile ${requested}. Available profiles: ${available}`);
235+ }
236+237+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `profile=${profile.id}\n`);
238+ NODE
239+240+ echo "QA profile: \`${QA_PROFILE}\`" >> "$GITHUB_STEP_SUMMARY"
198241199242 - name: Build private QA runtime
200243env:
201244NODE_OPTIONS: --max-old-space-size=8192
202245run: node scripts/build-all.mjs qaRuntime
203246204- - name: Run release QA profile
247+ - name: Run QA profile
205248id: run_profile
206249env:
250+QA_PROFILE: ${{ steps.profile.outputs.profile }}
207251OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
208252shell: bash
209253run: |
210254 set -euo pipefail
211255212- output_dir=".artifacts/qa-e2e/release-profile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
256+ output_dir=".artifacts/qa-e2e/profile-${QA_PROFILE}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
213257 echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
214258215259 qa_exit_code=0
216260 pnpm openclaw qa run \
217261 --repo-root . \
218- --qa-profile release \
262+ --qa-profile "${QA_PROFILE}" \
219263 --output-dir "${output_dir}" || qa_exit_code=$?
220264221265 echo "qa_exit_code=${qa_exit_code}" >> "$GITHUB_OUTPUT"
222266223- - name: Validate release QA evidence
267+ - name: Validate QA profile evidence
224268id: evidence
225269if: always()
226270env:
227-ARTIFACT_NAME: release-qa-profile-evidence-${{ needs.validate_selected_ref.outputs.selected_revision }}
271+ARTIFACT_NAME: qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}
228272OUTPUT_DIR: ${{ steps.run_profile.outputs.output_dir }}
273+QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
274+QA_PROFILE: ${{ steps.profile.outputs.profile }}
229275REQUESTED_REF: ${{ inputs.ref }}
230276TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_revision }}
231277TRUSTED_REASON: ${{ needs.validate_selected_ref.outputs.trusted_reason }}
@@ -241,22 +287,28 @@ jobs:
241287 if (!outputDir) {
242288 throw new Error("OUTPUT_DIR is required");
243289 }
290+ if (!process.env.QA_EXIT_CODE) {
291+ throw new Error("QA_EXIT_CODE is required");
292+ }
244293245294 const evidencePath = path.join(outputDir, "qa-evidence.json");
246295 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)}`);
296+ if (payload.profile !== process.env.QA_PROFILE) {
297+ throw new Error(`qa-evidence.json profile must be ${process.env.QA_PROFILE}, got ${JSON.stringify(payload.profile)}`);
249298 }
250299 if (!payload.scorecard || !Array.isArray(payload.scorecard.categoryReports)) {
251- throw new Error("release qa-evidence.json must include scorecard.categoryReports");
300+ throw new Error("QA profile qa-evidence.json must include scorecard.categoryReports");
252301 }
253302 if (payload.scorecard.categoryReports.length === 0) {
254- throw new Error("release qa-evidence.json scorecard has no category reports");
303+ throw new Error("QA profile qa-evidence.json scorecard has no category reports");
255304 }
256305257306 const manifest = {
258307 artifactName: process.env.ARTIFACT_NAME,
259308 generatedAt: new Date().toISOString(),
309+ qaProfile: process.env.QA_PROFILE,
310+ qaExitCode: Number(process.env.QA_EXIT_CODE),
311+ qaPassed: process.env.QA_EXIT_CODE === "0",
260312 requestedRef: process.env.REQUESTED_REF,
261313 targetSha: process.env.TARGET_SHA,
262314 trustedReason: process.env.TRUSTED_REASON,
@@ -269,45 +321,56 @@ jobs:
269321 },
270322 };
271323 fs.writeFileSync(
272- path.join(outputDir, "release-qa-profile-evidence-manifest.json"),
324+ path.join(outputDir, "qa-profile-evidence-manifest.json"),
273325 `${JSON.stringify(manifest, null, 2)}\n`,
274326 );
275327 NODE
276328277329 echo "artifact_name=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT"
330+ echo "qa_profile=${QA_PROFILE}" >> "$GITHUB_OUTPUT"
331+ echo "qa_exit_code=${QA_EXIT_CODE}" >> "$GITHUB_OUTPUT"
332+ if [[ "$QA_EXIT_CODE" == "0" ]]; then
333+ echo "qa_passed=true" >> "$GITHUB_OUTPUT"
334+ else
335+ echo "qa_passed=false" >> "$GITHUB_OUTPUT"
336+ echo "::warning::QA profile '${QA_PROFILE}' completed with exit code ${QA_EXIT_CODE}; evidence was still validated and uploaded."
337+ fi
278338 echo "target_sha=${TARGET_SHA}" >> "$GITHUB_OUTPUT"
279339 echo "trusted_reason=${TRUSTED_REASON}" >> "$GITHUB_OUTPUT"
280340 echo "qa_evidence_path=qa-evidence.json" >> "$GITHUB_OUTPUT"
281341 {
282- echo "### Release QA profile evidence"
342+ echo "### QA profile evidence"
283343 echo
284344 echo "- Artifact: \`${ARTIFACT_NAME}\`"
345+ echo "- QA profile: \`${QA_PROFILE}\`"
346+ echo "- QA exit code: \`${QA_EXIT_CODE}\`"
285347 echo "- Target SHA: \`${TARGET_SHA}\`"
286348 echo "- Evidence path: \`${OUTPUT_DIR}/qa-evidence.json\`"
287- echo "- Manifest: \`${OUTPUT_DIR}/release-qa-profile-evidence-manifest.json\`"
349+ echo "- Manifest: \`${OUTPUT_DIR}/qa-profile-evidence-manifest.json\`"
288350 } >> "$GITHUB_STEP_SUMMARY"
289351290- - name: Upload release QA profile evidence
352+ - name: Upload QA profile evidence
291353if: always()
292354uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
293355with:
294-name: release-qa-profile-evidence-${{ needs.validate_selected_ref.outputs.selected_revision }}
356+name: qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}
295357path: ${{ steps.run_profile.outputs.output_dir }}
296358retention-days: 30
297359if-no-files-found: error
298360299- - name: Fail if release QA profile failed
300-if: always()
361+ - name: Fail if configured QA gate failed
362+if: always() && (github.event_name == 'workflow_dispatch' || inputs.fail_on_qa_failure)
301363env:
302364QA_EXIT_CODE: ${{ steps.run_profile.outputs.qa_exit_code }}
365+QA_PROFILE: ${{ steps.profile.outputs.profile }}
303366shell: bash
304367run: |
305368 set -euo pipefail
306369 if [[ -z "${QA_EXIT_CODE:-}" ]]; then
307- echo "Release QA profile did not report an exit code." >&2
370+ echo "QA profile did not report an exit code." >&2
308371 exit 1
309372 fi
310373 if [[ "$QA_EXIT_CODE" != "0" ]]; then
311- echo "Release QA profile failed with exit code ${QA_EXIT_CODE}." >&2
374+ echo "QA profile '${QA_PROFILE}' failed with exit code ${QA_EXIT_CODE}." >&2
312375 exit "$QA_EXIT_CODE"
313376 fi
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。