mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 02:00:19 +08:00
567 lines
29 KiB
YAML
567 lines
29 KiB
YAML
name: Semantic Review
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: ["CI"]
|
|
types: [completed]
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
|
|
jobs:
|
|
pr-quality-summary:
|
|
if: github.event.workflow_run.event == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Verify workflow run and pull request for summary
|
|
id: pr
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
with:
|
|
script: |
|
|
const run = context.payload.workflow_run;
|
|
if (run.name !== "CI") throw new Error(`unexpected workflow name: ${run.name}`);
|
|
let workflowPath = run.path || "";
|
|
if (!workflowPath) {
|
|
const workflowId = Number(run.workflow_id || 0);
|
|
if (!Number.isInteger(workflowId) || workflowId <= 0) throw new Error("missing workflow id");
|
|
const { data: workflow } = await github.rest.actions.getWorkflow({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
workflow_id: workflowId,
|
|
});
|
|
workflowPath = workflow.path || "";
|
|
}
|
|
if (workflowPath !== ".github/workflows/ci.yml") throw new Error(`unexpected workflow path: ${workflowPath}`);
|
|
if (run.event !== "pull_request") throw new Error(`unexpected event: ${run.event}`);
|
|
if (run.repository.id !== context.payload.repository.id) throw new Error("repository id mismatch");
|
|
if (run.repository.full_name !== context.payload.repository.full_name) throw new Error("repository name mismatch");
|
|
if (typeof run.head_sha !== "string" || run.head_sha.length !== 40) throw new Error("invalid head sha");
|
|
const runPRs = Array.isArray(run.pull_requests) ? run.pull_requests : [];
|
|
if (runPRs.length > 1) {
|
|
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
|
|
}
|
|
let prNumber = Number(runPRs[0]?.number || 0);
|
|
const eventBaseSha = runPRs[0]?.base?.sha || "";
|
|
const eventHeadSha = runPRs[0]?.head?.sha || "";
|
|
const targetHeadSha = run.head_sha;
|
|
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
|
|
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
|
core.notice("PR quality summary using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
|
|
}
|
|
|
|
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
|
|
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: run.id,
|
|
per_page: 100,
|
|
});
|
|
const factsArtifacts = artifactData.artifacts.filter((artifact) => factsArtifactPattern.test(artifact.name));
|
|
let factsArtifactName = "";
|
|
let artifactBaseSha = "";
|
|
let artifactError = "";
|
|
if (factsArtifacts.length !== 1) {
|
|
artifactError = `expected exactly one base-bound quality gate facts artifact, got ${factsArtifacts.length}`;
|
|
} else {
|
|
factsArtifactName = factsArtifacts[0].name;
|
|
const [, parsedBaseSha, artifactHeadSha] = factsArtifactName.match(factsArtifactPattern);
|
|
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
|
artifactError = "facts artifact head sha does not match verified PR head sha";
|
|
factsArtifactName = "";
|
|
} else {
|
|
artifactBaseSha = parsedBaseSha;
|
|
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
|
core.notice("PR quality summary using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
|
|
}
|
|
}
|
|
}
|
|
if (!prNumber) {
|
|
const { data: associatedPRs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
commit_sha: targetHeadSha,
|
|
});
|
|
const candidatePRs = associatedPRs.filter((candidate) =>
|
|
candidate.state === "open" &&
|
|
candidate.base?.repo?.id === context.payload.repository.id &&
|
|
candidate.head?.sha === targetHeadSha
|
|
);
|
|
if (candidatePRs.length > 1) {
|
|
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${candidatePRs.length}`);
|
|
}
|
|
if (candidatePRs.length === 1) {
|
|
prNumber = candidatePRs[0].number;
|
|
}
|
|
}
|
|
if (!prNumber) {
|
|
const candidatePRs = await github.paginate(github.rest.pulls.list, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: "open",
|
|
per_page: 100,
|
|
}).then((prs) => prs.filter((candidate) =>
|
|
candidate.base?.repo?.id === context.payload.repository.id &&
|
|
candidate.head?.sha === targetHeadSha
|
|
));
|
|
if (candidatePRs.length !== 1) {
|
|
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
|
|
}
|
|
prNumber = candidatePRs[0].number;
|
|
}
|
|
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
});
|
|
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
|
|
if (pr.head.sha !== targetHeadSha) {
|
|
core.notice("PR quality summary skipped: workflow_run is stale for this PR head");
|
|
core.setOutput("stale", "true");
|
|
return;
|
|
}
|
|
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
|
|
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
|
|
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
|
|
core.notice("PR quality summary skipped: workflow_run is stale for this PR base");
|
|
core.setOutput("stale", "true");
|
|
return;
|
|
}
|
|
if (artifactError) {
|
|
core.warning(`quality gate facts artifact binding is unavailable: ${artifactError}`);
|
|
}
|
|
core.setOutput("pr_number", String(prNumber));
|
|
core.setOutput("head_sha", targetHeadSha);
|
|
core.setOutput("base_sha", baseSha);
|
|
core.setOutput("run_id", String(run.id));
|
|
core.setOutput("facts_artifact_name", factsArtifactName);
|
|
core.setOutput("artifact_error", artifactError);
|
|
core.setOutput("stale", "false");
|
|
|
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
id: checkout
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
with:
|
|
ref: ${{ steps.pr.outputs.base_sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Verify summary facts artifact metadata
|
|
id: artifact
|
|
if: ${{ steps.pr.outputs.stale != 'true' && steps.pr.outputs.facts_artifact_name != '' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
with:
|
|
script: |
|
|
const run = context.payload.workflow_run;
|
|
const factsArtifactName = "${{ steps.pr.outputs.facts_artifact_name }}";
|
|
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: run.id,
|
|
per_page: 100,
|
|
});
|
|
const artifacts = data.artifacts.filter(a => a.name === factsArtifactName);
|
|
if (artifacts.length !== 1) throw new Error(`expected exactly one quality-gate-facts artifact, got ${artifacts.length}`);
|
|
const artifact = artifacts[0];
|
|
if (artifact.expired) throw new Error("quality-gate-facts artifact expired");
|
|
if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > 5 * 1024 * 1024) {
|
|
throw new Error(`invalid artifact size: ${artifact.size_in_bytes}`);
|
|
}
|
|
if (!artifact.digest) throw new Error("facts artifact digest is missing from GitHub API response");
|
|
core.setOutput("artifact_id", String(artifact.id));
|
|
core.setOutput("artifact_digest", artifact.digest);
|
|
|
|
- name: Download facts artifact zip
|
|
if: ${{ steps.pr.outputs.stale != 'true' && steps.artifact.outputs.artifact_id != '' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
id: download
|
|
with:
|
|
script: |
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const artifactId = Number("${{ steps.artifact.outputs.artifact_id }}");
|
|
if (!Number.isInteger(artifactId) || artifactId <= 0) throw new Error("invalid artifact id");
|
|
const { data } = await github.rest.actions.downloadArtifact({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
artifact_id: artifactId,
|
|
archive_format: "zip",
|
|
});
|
|
const zipPath = path.join(process.env.RUNNER_TEMP, "quality-gate-facts.zip");
|
|
fs.writeFileSync(zipPath, Buffer.from(data));
|
|
core.setOutput("zip_path", zipPath);
|
|
|
|
- name: Verify and extract summary facts artifact
|
|
if: ${{ steps.pr.outputs.stale != 'true' && steps.download.outputs.zip_path != '' }}
|
|
env:
|
|
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
|
|
SEMANTIC_REVIEW_DECISION_OUT: decision.json
|
|
SEMANTIC_REVIEW_MARKDOWN_OUT: semantic-review.md
|
|
run: node scripts/semantic-review-verify-artifact.js '${{ steps.download.outputs.zip_path }}' facts.json '${{ steps.artifact.outputs.artifact_digest }}'
|
|
|
|
- name: Publish PR quality summary
|
|
if: ${{ always() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome == 'success' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
env:
|
|
CI_QUALITY_SUMMARY_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
|
CI_QUALITY_SUMMARY_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
|
|
CI_QUALITY_SUMMARY_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
|
CI_QUALITY_SUMMARY_RUN_ID: ${{ steps.pr.outputs.run_id }}
|
|
CI_QUALITY_SUMMARY_ARTIFACT_ERROR: ${{ steps.pr.outputs.artifact_error }}
|
|
with:
|
|
script: |
|
|
const { publish } = require("./scripts/ci-quality-summary-publish.js");
|
|
await publish({ github, context, core });
|
|
|
|
semantic-review:
|
|
needs: pr-quality-summary
|
|
if: always() && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: read
|
|
checks: write
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Verify workflow run and pull request
|
|
id: pr
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
with:
|
|
script: |
|
|
const run = context.payload.workflow_run;
|
|
if (run.name !== "CI") throw new Error(`unexpected workflow name: ${run.name}`);
|
|
let workflowPath = run.path || "";
|
|
if (!workflowPath) {
|
|
const workflowId = Number(run.workflow_id || 0);
|
|
if (!Number.isInteger(workflowId) || workflowId <= 0) throw new Error("missing workflow id");
|
|
const { data: workflow } = await github.rest.actions.getWorkflow({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
workflow_id: workflowId,
|
|
});
|
|
workflowPath = workflow.path || "";
|
|
}
|
|
if (workflowPath !== ".github/workflows/ci.yml") throw new Error(`unexpected workflow path: ${workflowPath}`);
|
|
if (run.event !== "pull_request") throw new Error(`unexpected event: ${run.event}`);
|
|
if (run.conclusion !== "success") throw new Error(`unexpected conclusion: ${run.conclusion}`);
|
|
if (run.repository.id !== context.payload.repository.id) throw new Error("repository id mismatch");
|
|
if (run.repository.full_name !== context.payload.repository.full_name) throw new Error("repository name mismatch");
|
|
if (typeof run.head_sha !== "string" || run.head_sha.length !== 40) throw new Error("invalid head sha");
|
|
const runPRs = Array.isArray(run.pull_requests) ? run.pull_requests : [];
|
|
if (runPRs.length > 1) {
|
|
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
|
|
}
|
|
let prNumber = Number(runPRs[0]?.number || 0);
|
|
const eventBaseSha = runPRs[0]?.base?.sha || "";
|
|
const eventHeadSha = runPRs[0]?.head?.sha || "";
|
|
const targetHeadSha = run.head_sha;
|
|
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
|
|
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
|
core.notice("semantic review using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
|
|
}
|
|
|
|
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
|
|
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: run.id,
|
|
per_page: 100,
|
|
});
|
|
const factsArtifacts = artifactData.artifacts.filter((artifact) => factsArtifactPattern.test(artifact.name));
|
|
let factsArtifactName = "";
|
|
let artifactBaseSha = "";
|
|
let artifactError = "";
|
|
if (factsArtifacts.length !== 1) {
|
|
artifactError = `expected exactly one base-bound quality gate facts artifact, got ${factsArtifacts.length}`;
|
|
} else {
|
|
factsArtifactName = factsArtifacts[0].name;
|
|
const [, parsedBaseSha, artifactHeadSha] = factsArtifactName.match(factsArtifactPattern);
|
|
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
|
artifactError = "facts artifact head sha does not match verified PR head sha";
|
|
factsArtifactName = "";
|
|
} else {
|
|
artifactBaseSha = parsedBaseSha;
|
|
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
|
core.notice("semantic review using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
|
|
}
|
|
}
|
|
}
|
|
if (!prNumber) {
|
|
const { data: associatedPRs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
commit_sha: targetHeadSha,
|
|
});
|
|
const candidatePRs = associatedPRs.filter((candidate) =>
|
|
candidate.state === "open" &&
|
|
candidate.base?.repo?.id === context.payload.repository.id &&
|
|
candidate.head?.sha === targetHeadSha
|
|
);
|
|
if (candidatePRs.length > 1) {
|
|
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${candidatePRs.length}`);
|
|
}
|
|
if (candidatePRs.length === 1) {
|
|
prNumber = candidatePRs[0].number;
|
|
}
|
|
}
|
|
if (!prNumber) {
|
|
const candidatePRs = await github.paginate(github.rest.pulls.list, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: "open",
|
|
per_page: 100,
|
|
}).then((prs) => prs.filter((candidate) =>
|
|
candidate.base?.repo?.id === context.payload.repository.id &&
|
|
candidate.head?.sha === targetHeadSha
|
|
));
|
|
if (candidatePRs.length !== 1) {
|
|
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
|
|
}
|
|
prNumber = candidatePRs[0].number;
|
|
}
|
|
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
});
|
|
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
|
|
if (pr.head.sha !== targetHeadSha) {
|
|
core.notice("semantic review skipped: workflow_run is stale for this PR head");
|
|
core.setOutput("stale", "true");
|
|
return;
|
|
}
|
|
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
|
|
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
|
|
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
|
|
core.notice("semantic review skipped: workflow_run is stale for this PR base");
|
|
core.setOutput("stale", "true");
|
|
return;
|
|
}
|
|
if (artifactError) {
|
|
core.warning(`semantic review facts artifact binding is unavailable: ${artifactError}`);
|
|
}
|
|
core.setOutput("pr_number", String(prNumber));
|
|
core.setOutput("head_sha", targetHeadSha);
|
|
core.setOutput("base_sha", baseSha);
|
|
core.setOutput("head_owner", pr.head.repo.owner.login);
|
|
core.setOutput("head_repo", pr.head.repo.name);
|
|
core.setOutput("head_repo_id", String(pr.head.repo.id));
|
|
core.setOutput("head_is_base_repo", pr.head.repo.id === context.payload.repository.id ? "true" : "false");
|
|
core.setOutput("run_id", String(run.id));
|
|
core.setOutput("facts_artifact_name", factsArtifactName);
|
|
core.setOutput("artifact_error", artifactError);
|
|
core.setOutput("stale", "false");
|
|
|
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
id: checkout
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
with:
|
|
ref: ${{ steps.pr.outputs.base_sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Publish pre-checkout semantic review failure
|
|
if: ${{ failure() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome != 'success' && steps.pr.outputs.head_sha != '' && steps.pr.outputs.pr_number != '' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
env:
|
|
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
|
|
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
|
SEMANTIC_REVIEW_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
|
|
SEMANTIC_REVIEW_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
|
SEMANTIC_REVIEW_RUN_ID: ${{ steps.pr.outputs.run_id }}
|
|
with:
|
|
script: |
|
|
const runtimeBlockMode = process.env.SEMANTIC_REVIEW_BLOCK === "true";
|
|
const pr = Number(process.env.SEMANTIC_REVIEW_PR_NUMBER || 0);
|
|
const headSha = process.env.SEMANTIC_REVIEW_HEAD_SHA || "";
|
|
const baseSha = process.env.SEMANTIC_REVIEW_BASE_SHA || "";
|
|
if (!Number.isInteger(pr) || pr <= 0 || !/^[a-f0-9]{40}$/i.test(headSha) || !/^[a-f0-9]{40}$/i.test(baseSha)) {
|
|
throw new Error("missing verified semantic review target");
|
|
}
|
|
const { data: pull } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: pr,
|
|
});
|
|
if (pull.head.sha !== headSha) {
|
|
core.notice("semantic review skipped infrastructure failure check: PR head changed");
|
|
return;
|
|
}
|
|
if (pull.base.sha !== baseSha) {
|
|
core.notice("semantic review skipped infrastructure failure check: PR base changed");
|
|
return;
|
|
}
|
|
if (pull.base.repo.id !== context.payload.repository.id) {
|
|
throw new Error("PR base repo mismatch before infrastructure failure check");
|
|
}
|
|
await github.rest.checks.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: runtimeBlockMode ? "semantic-review/result" : "semantic-review/observe",
|
|
head_sha: headSha,
|
|
status: "completed",
|
|
conclusion: runtimeBlockMode ? "failure" : "neutral",
|
|
output: {
|
|
title: "Semantic review infrastructure failure",
|
|
summary: "Semantic review could not checkout the verified base commit. Inspect the workflow logs before relying on semantic review output.",
|
|
},
|
|
});
|
|
|
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
with:
|
|
go-version-file: go.mod
|
|
|
|
- name: Verify semantic facts artifact metadata
|
|
id: artifact
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
with:
|
|
script: |
|
|
const run = context.payload.workflow_run;
|
|
const factsArtifactName = "${{ steps.pr.outputs.facts_artifact_name }}";
|
|
if (!/^quality-gate-facts-[a-f0-9]{40}-[a-f0-9]{40}$/i.test(factsArtifactName)) {
|
|
throw new Error("missing verified facts artifact binding");
|
|
}
|
|
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: run.id,
|
|
per_page: 100,
|
|
});
|
|
const artifacts = data.artifacts.filter(a => a.name === factsArtifactName);
|
|
if (artifacts.length !== 1) throw new Error(`expected exactly one quality-gate-facts artifact, got ${artifacts.length}`);
|
|
const artifact = artifacts[0];
|
|
if (artifact.expired) throw new Error("quality-gate-facts artifact expired");
|
|
if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > 5 * 1024 * 1024) {
|
|
throw new Error(`invalid artifact size: ${artifact.size_in_bytes}`);
|
|
}
|
|
if (!artifact.digest) throw new Error("facts artifact digest is missing from GitHub API response");
|
|
core.setOutput("artifact_id", String(artifact.id));
|
|
core.setOutput("artifact_digest", artifact.digest);
|
|
|
|
- name: Download facts artifact zip
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
id: download
|
|
with:
|
|
script: |
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const artifactId = Number("${{ steps.artifact.outputs.artifact_id }}");
|
|
if (!Number.isInteger(artifactId) || artifactId <= 0) throw new Error("invalid artifact id");
|
|
const { data } = await github.rest.actions.downloadArtifact({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
artifact_id: artifactId,
|
|
archive_format: "zip",
|
|
});
|
|
const zipPath = path.join(process.env.RUNNER_TEMP, "quality-gate-facts.zip");
|
|
fs.writeFileSync(zipPath, Buffer.from(data));
|
|
core.setOutput("zip_path", zipPath);
|
|
|
|
- name: Verify and extract semantic facts artifact
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
env:
|
|
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
|
|
SEMANTIC_REVIEW_DECISION_OUT: decision.json
|
|
SEMANTIC_REVIEW_MARKDOWN_OUT: semantic-review.md
|
|
run: node scripts/semantic-review-verify-artifact.js '${{ steps.download.outputs.zip_path }}' facts.json '${{ steps.artifact.outputs.artifact_digest }}'
|
|
|
|
- name: Download PR semantic waiver config
|
|
id: waiver_config
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
env:
|
|
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
|
SEMANTIC_REVIEW_HEAD_OWNER: ${{ steps.pr.outputs.head_owner }}
|
|
SEMANTIC_REVIEW_HEAD_REPO: ${{ steps.pr.outputs.head_repo }}
|
|
SEMANTIC_REVIEW_HEAD_IS_BASE_REPO: ${{ steps.pr.outputs.head_is_base_repo }}
|
|
with:
|
|
script: |
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const headSha = process.env.SEMANTIC_REVIEW_HEAD_SHA || "";
|
|
if (!/^[a-f0-9]{40}$/i.test(headSha)) {
|
|
throw new Error("missing verified semantic review target");
|
|
}
|
|
const headOwner = process.env.SEMANTIC_REVIEW_HEAD_OWNER || "";
|
|
const headRepo = process.env.SEMANTIC_REVIEW_HEAD_REPO || "";
|
|
if (!headOwner || !headRepo) {
|
|
throw new Error("missing verified semantic review head repository");
|
|
}
|
|
const waiverPath = "internal/qualitygate/config/semantic/waivers.txt";
|
|
const outPath = path.join(process.env.RUNNER_TEMP, "semantic-review-waivers.txt");
|
|
const headIsBaseRepo = process.env.SEMANTIC_REVIEW_HEAD_IS_BASE_REPO === "true";
|
|
if (!headIsBaseRepo) {
|
|
core.notice("fork PR semantic waiver config is ignored");
|
|
core.setOutput("path", "");
|
|
return;
|
|
}
|
|
let content = "";
|
|
try {
|
|
const { data } = await github.rest.repos.getContent({
|
|
owner: headOwner,
|
|
repo: headRepo,
|
|
path: waiverPath,
|
|
ref: headSha,
|
|
});
|
|
if (Array.isArray(data) || data.type !== "file" || data.encoding !== "base64") {
|
|
throw new Error(`${waiverPath} is not a base64 file at PR head`);
|
|
}
|
|
if (data.size > 256 * 1024) {
|
|
throw new Error(`${waiverPath} is too large: ${data.size} bytes`);
|
|
}
|
|
content = Buffer.from(data.content, "base64").toString("utf8");
|
|
} catch (err) {
|
|
if (err.status !== 404) {
|
|
throw err;
|
|
}
|
|
}
|
|
fs.writeFileSync(outPath, content);
|
|
core.setOutput("path", outPath);
|
|
|
|
- name: Run semantic review
|
|
id: semantic
|
|
if: ${{ steps.pr.outputs.stale != 'true' }}
|
|
env:
|
|
ARK_API_KEY: ${{ secrets.ARK_API_KEY }}
|
|
ARK_BASE_URL: ${{ vars.ARK_BASE_URL }}
|
|
ARK_MODEL: ${{ vars.ARK_MODEL }}
|
|
ARK_TIMEOUT_SECONDS: ${{ vars.ARK_TIMEOUT_SECONDS }}
|
|
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
|
|
run: |
|
|
args=(
|
|
--repo .
|
|
--facts facts.json
|
|
--decision-out decision.json
|
|
--markdown-out semantic-review.md
|
|
)
|
|
if [ -n "${{ steps.waiver_config.outputs.path }}" ]; then
|
|
args+=(--waivers-file '${{ steps.waiver_config.outputs.path }}')
|
|
fi
|
|
if [ "$SEMANTIC_REVIEW_BLOCK" = "true" ]; then
|
|
args+=(--block)
|
|
fi
|
|
go run ./internal/qualitygate/cmd/semantic-review "${args[@]}"
|
|
|
|
- name: Publish semantic review
|
|
if: ${{ always() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome == 'success' }}
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
env:
|
|
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
|
|
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
|
SEMANTIC_REVIEW_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
|
|
SEMANTIC_REVIEW_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
|
SEMANTIC_REVIEW_RUN_ID: ${{ steps.pr.outputs.run_id }}
|
|
with:
|
|
script: |
|
|
const { publish } = require("./scripts/semantic-review-publish.js");
|
|
await publish({ github, context, core });
|