mirror of
https://github.com/we-promise/sure.git
synced 2026-06-04 10:19:03 +00:00
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:
198
workers/preview/deploy/resolve_preview_request.cjs
Normal file
198
workers/preview/deploy/resolve_preview_request.cjs
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user