Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3f60781
ci: report published package size and new runtime dependencies on eac…
cixzhang Oct 31, 2026
947e1ed
ci: write package sizes past 1 MB in MB
cixzhang Oct 31, 2026
4c9cfea
ci: per-suite ci:run-visual, ci:run-a11y and ci:run-rtl labels replac…
cixzhang Oct 31, 2026
207c6fa
ci: the PR report leads with one status table: this head's preview, s…
cixzhang Oct 31, 2026
d285fd0
Merge remote-tracking branch 'origin/ci/per-suite-run-labels' into ci…
cixzhang Oct 31, 2026
104373c
ci: the report's not-run sentence comes from run-labels.cjs
cixzhang Oct 31, 2026
b1dc73e
ci: the package budget is advisory; the merge-base lookups share one …
cixzhang Oct 31, 2026
1b07327
Merge branch 'ci/pack-size' into ci/pr-status-table
cixzhang Oct 31, 2026
a4d7aac
Merge the advisory package budget into the status table
cixzhang Oct 31, 2026
918fa69
ci: the status table reads suite jobs by display name, and a job's sk…
cixzhang Oct 31, 2026
197273f
ci: fold a skipped matrix's unexpanded job name into its job
cixzhang Oct 31, 2026
2b2736f
ci: the PR report follows the agreed mockup: preview first, one modif…
cixzhang Oct 31, 2026
6c19dca
ci: the frames step publishes only validated PNG/WebP frames to asset…
cixzhang Oct 31, 2026
219ccc5
ci: the pruner deletes only assets branches the PR report created (ma…
cixzhang Oct 31, 2026
a9980aa
Merge main (#9161 and #9165) into the PR report redesign
cixzhang Oct 31, 2026
7271897
ci: the PR report lists changed snapshots without thumbnails (frames …
cixzhang Oct 31, 2026
f242211
ci: the PR report reads fork previews too, keeps one preview line in …
cixzhang Oct 31, 2026
ef9b4bc
test: drop generator cases that repeat pr-status.test.mjs
cixzhang Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 80 additions & 317 deletions .github/scripts/generate-pr-comment.js

Large diffs are not rendered by default.

376 changes: 93 additions & 283 deletions .github/scripts/generate-pr-comment.test.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/scripts/lib/package-size.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ describe('renderPackageSizeSection', () => {
);
expect(markdown).toContain('### Published Package Size');
expect(markdown).toContain(
'| @astryxdesign/core | 97.7KB | 98.6KB | +1000B (+1.0%) |',
'| @astryxdesign/core | 97.7 kB | 98.6 kB | +1000 B (+1.0%) |',
);
expect(markdown).not.toContain('theme-neutral');
expect(markdown).toContain('2 other published packages are unchanged.');
Expand All @@ -279,7 +279,7 @@ describe('renderPackageSizeSection', () => {
}),
);
expect(markdown).toContain(
'| @astryxdesign/core | 8.58MB | 8.68MB | +97.7KB (+1.1%) |',
'| @astryxdesign/core | 8.58 MB | 8.68 MB | +97.7 kB (+1.1%) |',
);
});

Expand Down
164 changes: 125 additions & 39 deletions .github/scripts/lib/pr-preview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import {execFileSync} from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';

const {previewLine} = createRequire(import.meta.url)('./pr-status.js');

export const PR_ANALYSIS_MARKER = '<!-- astryx-pr-analysis -->';

function headMarker(sha) {
Expand Down Expand Up @@ -49,6 +53,97 @@ function refuse(message) {
throw new Error(`PR preview refused: ${message}`);
}

const VERCEL_DEPLOYMENT_URL = /^https:\/\/[a-z0-9-]+\.vercel\.app\/?$/;

/**
* This head's Vercel preview, from the deployment Vercel reports to GitHub for
* the exact commit: `ready` (that deployment's own address, so every link
* shows this head), `failed`, or `building` until Vercel reports it (the
* branch alias then serves the previous ready deployment). A fork's head is
* looked up the same way. Two reads in the comment job; no workflow runs on a
* deployment event.
*/
export async function headPreview({
github,
owner,
repo,
identity,
aliasOrigin,
}) {
const sha = identity.headSha;
try {
const {data: deployments} = await github.rest.repos.listDeployments({
owner,
repo,
sha,
per_page: 20,
});
const deployment = deployments
.filter(
candidate =>
candidate.creator?.login === 'vercel[bot]' &&
String(candidate.environment).toLowerCase() === 'preview',
)
.sort((a, b) => b.id - a.id)[0];
if (deployment) {
const {data: statuses} = await github.rest.repos.listDeploymentStatuses({
owner,
repo,
deployment_id: deployment.id,
per_page: 1,
});
const latest = statuses[0];
if (
latest?.state === 'success' &&
VERCEL_DEPLOYMENT_URL.test(String(latest.environment_url ?? ''))
)
return {
state: 'ready',
sha,
url: latest.environment_url.replace(/\/$/, ''),
};
if (latest?.state === 'failure' || latest?.state === 'error')
return {state: 'failed', sha};
}
} catch {
// An unreadable deployment list leaves the preview as still building.
}
return aliasOrigin
? {state: 'building', sha, aliasOrigin}
: {state: 'unknown', sha};
}

/**
* The source run's jobs (latest attempt) with each step's conclusion, so the
* report can say which jobs ran and which skipped their work by path. Null
* when the list cannot be read.
*/
export async function sourceRunJobs({github, owner, repo, runId}) {
try {
const jobs = await github.paginate(
github.rest.actions.listJobsForWorkflowRun,
{
owner,
repo,
run_id: runId,
filter: 'latest',
per_page: 100,
},
);
return jobs.map(job => ({
name: job.name,
status: job.status,
conclusion: job.conclusion,
steps: (job.steps || []).map(step => ({
name: step.name,
conclusion: step.conclusion,
})),
}));
} catch {
return null;
}
}

/**
* The pull request moved on from the source run: it closed, merged, or got a
* newer head. That is ordinary churn, not a forged identity. Nothing is
Expand Down Expand Up @@ -415,45 +510,21 @@ export function validateAnalysisMetadata(metadata, identity) {
return metadata;
}

function previewState(storybook, sandbox) {
if (storybook && sandbox) return 'both';
if (storybook) return 'storybook';
if (sandbox) return 'sandbox';
return 'none';
}

// The same preview wording as the generator's report: the branch alias
// serves the branch's latest deployment, which can trail a new push. A draft
// or an update-only reconcile (no alias attempted) says nothing about previews.
const PREVIEW_NOTE =
"> **Preview:** the links below open this branch's latest Vercel deployment, which can trail a new push until that push's deployment is ready.";
const PREVIEW_POINTER =
"> **Preview availability:** this branch's preview address cannot be known in advance; open the preview from Vercel's comment on this pull request.";

// Without a trusted analysis, the report keeps the same preview line as the
// full report (pr-status.js), then says why the analysis is missing.
function safeCurrentBody({
identity,
runUrl,
message: overrideMessage,
previewOrigin,
aliasAttempted,
preview,
}) {
const conclusion = identity.sourceConclusion;
const message =
overrideMessage ??
(conclusion === 'success'
? 'The current CI run completed, but its trusted analysis is unavailable.'
: `The current CI run concluded ${conclusion || 'without a result'}. Current analysis is unavailable.`);
const links = previewOrigin
? [
`[View Storybook for this PR](${previewOrigin}/storybook/)`,
`[View Sandbox for this PR](${previewOrigin}/sandbox/)`,
]
: [];
let previewMessage = '';
if (links.length)
previewMessage = `\n\n${PREVIEW_NOTE}\n\n${links.join(' · ')}`;
else if (aliasAttempted) previewMessage = `\n\n${PREVIEW_POINTER}`;
return `## PR Analysis Report\n${PR_ANALYSIS_MARKER}\n${headMarker(identity.headSha)}\n\n> **Current run:** ${message}${previewMessage}\n\n---\n\n<sub>Generated by PR Enrichment workflow | <a href="${runUrl}" target="_blank" rel="noopener noreferrer">View current CI run</a></sub>\n`;
return `## PR Analysis Report\n${PR_ANALYSIS_MARKER}\n${headMarker(identity.headSha)}\n\n${previewLine(preview)}> **Current run:** ${message}\n\n---\n\n<sub>Generated by PR Enrichment workflow | <a href="${runUrl}" target="_blank" rel="noopener noreferrer">View current CI run</a></sub>\n`;
}

function readJSON(file) {
Expand Down Expand Up @@ -545,8 +616,6 @@ export async function reconcilePrComment({
// preview and says nothing about one.
const aliasAttempted = createIfMissing && !identity.draft;
const previewOrigin = aliasAttempted ? branchPreviewOrigin(identity) : null;
const storybook = previewOrigin !== null;
const sandbox = storybook;
const comments = await allComments(github, owner, repo, identity.prNumber);
const botComments = comments.filter(comment => comment.user?.type === 'Bot');
const botComment =
Expand All @@ -559,8 +628,30 @@ export async function reconcilePrComment({
return {action: 'none', body: null, identity};
}

// This head's preview, for the full report and the fallback alike.
const preview = aliasAttempted
? await headPreview({
github,
owner,
repo,
identity,
aliasOrigin: previewOrigin,
})
: null;
let body;
if (analysisReady) {
// The comment job's own reads, handed to the generator as files.
const inputs = fs.mkdtempSync(path.join(os.tmpdir(), 'pr-report-'));
const jobs = await sourceRunJobs({
github,
owner,
repo,
runId: identity.sourceRunId,
});
const previewPath = path.join(inputs, 'preview.json');
const jobsPath = path.join(inputs, 'jobs.json');
if (preview) fs.writeFileSync(previewPath, JSON.stringify(preview));
if (jobs) fs.writeFileSync(jobsPath, JSON.stringify(jobs));
const args = [
generator,
'--analysis',
Expand All @@ -585,11 +676,8 @@ export async function reconcilePrComment({
// checks, or with the ci:run-a11y label); the comment says so.
...(fs.existsSync(a11yPath) ? ['--a11y', a11yPath] : []),
...(fs.existsSync(visualPath) ? ['--visual', visualPath] : []),
...(storybook ? ['--storybook-url', `${previewOrigin}/storybook/`] : []),
...(sandbox ? ['--sandbox-url', `${previewOrigin}/sandbox/`] : []),
...(aliasAttempted
? ['--preview-state', previewState(storybook, sandbox)]
: []),
...(preview ? ['--preview', previewPath] : []),
...(jobs ? ['--jobs', jobsPath] : []),
'--source-conclusion',
identity.sourceConclusion,
'--run-url',
Expand All @@ -608,17 +696,15 @@ export async function reconcilePrComment({
identity,
runUrl,
message: fallbackMessage,
previewOrigin,
aliasAttempted,
preview,
});
}
} else {
body = safeCurrentBody({
identity,
runUrl,
message: fallbackMessage,
previewOrigin,
aliasAttempted,
preview,
});
}

Expand Down
Loading
Loading