ci(preview): stabilize Cloudflare preview deployments (#2062)

* ci(preview): stabilize Cloudflare preview deployments

* ci(preview): bound diagnostics and cover artifact fallback

* ci(preview): isolate artifact deploy permissions

* ci(preview): tidy deployment comment rendering

* ci(preview): harden preview manifest generation

* ci(preview): fail on preview diagnostics failure
This commit is contained in:
ghost
2026-05-31 04:30:03 -07:00
committed by GitHub
parent ca8cbaa201
commit 5f8452d63b
6 changed files with 845 additions and 138 deletions

View File

@@ -0,0 +1,198 @@
const PREVIEW_ARTIFACT_PATTERN = /^preview-image-pr-([1-9][0-9]*)-([a-f0-9]{40})$/;
function parsePreviewArtifactName(name) {
const match = PREVIEW_ARTIFACT_PATTERN.exec(name);
if (!match) return null;
return {
prNumber: Number(match[1]),
headSha: match[2],
};
}
function repoFullName(context) {
return `${context.repo.owner}/${context.repo.repo}`;
}
function labelsIncludePreview(pullRequest) {
return pullRequest.labels.some((label) => label.name === "preview-cf");
}
function artifactCandidates(artifacts, headSha) {
return artifacts
.filter((artifact) => !artifact.expired)
.map((artifact) => ({
artifact,
parsed: parsePreviewArtifactName(artifact.name),
}))
.filter((candidate) => candidate.parsed?.headSha === headSha);
}
function uniqueNumbers(candidates) {
return [...new Set(candidates.map((candidate) => candidate.parsed.prNumber))];
}
function associatedPullRequestsForHead(associatedPullRequests, context, headSha) {
const baseRepo = repoFullName(context);
return associatedPullRequests.filter((pullRequest) => (
pullRequest.state === "open" &&
pullRequest.head?.sha === headSha &&
pullRequest.base?.repo?.full_name === baseRepo
));
}
function selectPullRequestNumber({ runPullRequest, artifacts, associatedPullRequests, context, headSha }) {
if (runPullRequest?.number) {
return {
prNumber: runPullRequest.number,
source: "workflow_run",
};
}
const associatedHeadPullRequests = associatedPullRequestsForHead(associatedPullRequests, context, headSha);
const artifactPullRequestNumbers = uniqueNumbers(artifactCandidates(artifacts, headSha));
if (associatedHeadPullRequests.length === 1) {
return {
prNumber: associatedHeadPullRequests[0].number,
source: "commit_association",
};
}
if (associatedHeadPullRequests.length > 1) {
const associatedNumbers = new Set(associatedHeadPullRequests.map((pullRequest) => pullRequest.number));
const artifactMatches = artifactPullRequestNumbers.filter((number) => associatedNumbers.has(number));
if (artifactMatches.length === 1) {
return {
prNumber: artifactMatches[0],
source: "artifact_and_commit_association",
};
}
return {
error: `Workflow run head SHA ${headSha} is associated with multiple open pull requests and no single preview artifact matched`,
};
}
if (artifactPullRequestNumbers.length === 1) {
return {
prNumber: artifactPullRequestNumbers[0],
source: "artifact_name",
};
}
if (artifactPullRequestNumbers.length > 1) {
return {
error: `Workflow run ${headSha} published preview artifacts for multiple pull requests`,
};
}
return {
prNumber: null,
source: "none",
};
}
async function resolvePreviewRequest({ github, context, core }) {
const workflowRun = context.payload.workflow_run;
const runPullRequest = workflowRun.pull_requests?.[0];
const headSha = workflowRun.head_sha;
core.setOutput("should_deploy", "false");
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: workflowRun.id,
per_page: 100,
});
const { data: associatedPullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: headSha,
});
const selected = selectPullRequestNumber({
runPullRequest,
artifacts,
associatedPullRequests,
context,
headSha,
});
if (selected.error) {
core.setFailed(selected.error);
return;
}
if (!selected.prNumber) {
core.info("Workflow run is not associated with an open pull request");
return;
}
const prNumber = selected.prNumber;
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
if (pullRequest.state !== "open") {
core.info(`PR ${prNumber} is ${pullRequest.state}; skipping preview deploy`);
return;
}
if (pullRequest.head.sha !== headSha) {
core.info(`Workflow run head SHA ${headSha} is stale for PR ${prNumber}; current head is ${pullRequest.head.sha}`);
return;
}
const hasPreviewLabel = labelsIncludePreview(pullRequest);
if (!hasPreviewLabel) {
core.info(`PR ${prNumber} does not have the preview-cf label`);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
});
const workflowChanges = files
.map((file) => file.filename)
.filter((filename) => filename.startsWith(".github/workflows/"));
if (workflowChanges.length > 0) {
core.setFailed(`Preview deployment requires base-trusted workflow definitions; changed workflow files: ${workflowChanges.join(", ")}`);
return;
}
const artifactName = `preview-image-pr-${prNumber}-${headSha}`;
const artifact = artifacts.find((item) => item.name === artifactName && !item.expired);
if (!artifact) {
core.setFailed(`Pull Request workflow run ${workflowRun.id} did not publish ${artifactName}`);
return;
}
const isFork = pullRequest.head.repo?.full_name !== repoFullName(context);
core.info(`Resolved PR ${prNumber} from ${selected.source}; fork=${isFork}`);
core.setOutput("artifact_name", artifactName);
core.setOutput("head_sha", headSha);
core.setOutput("is_fork", String(isFork));
core.setOutput("pr_number", String(prNumber));
core.setOutput("should_deploy", "true");
}
module.exports = {
artifactCandidates,
associatedPullRequestsForHead,
parsePreviewArtifactName,
resolvePreviewRequest,
selectPullRequestNumber,
};