Skip to content

Commit bed590f

Browse files
committed
get started sticky
1 parent f08a566 commit bed590f

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

.circleci/config.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ commands:
118118
fi
119119
120120
jobs:
121+
post-pr-started:
122+
docker:
123+
- image: cimg/node:20.19.0-browsers
124+
resource_class: small
125+
working_directory: ~/remix-project
126+
steps:
127+
- checkout
128+
- run:
129+
name: Install @octokit/auth-app for PR commenter (started)
130+
command: |
131+
mkdir -p /tmp/pr-bot
132+
cd /tmp/pr-bot
133+
npm install --no-save @octokit/auth-app
134+
- run:
135+
name: Post sticky PR comment: CI started
136+
command: |
137+
NODE_PATH=/tmp/pr-bot/node_modules node scripts/post-pr-started.js || true
138+
121139
build:
122140
docker:
123141
- image: cimg/node:20.19.0-browsers
@@ -595,9 +613,13 @@ workflows:
595613
- build:
596614
requires:
597615
- check-flaky-or-pr-tests
616+
- post-pr-started:
617+
requires:
618+
- build
598619
- remix-ide-browser:
599620
requires:
600621
- build
622+
- post-pr-started
601623
matrix:
602624
parameters:
603625
browser: ["chrome"]
@@ -610,6 +632,7 @@ workflows:
610632
- post-failed-report:
611633
requires:
612634
- build
635+
- post-pr-started
613636

614637
run_file_keyword:
615638
when: << pipeline.parameters.run_file_tests_keyword >>
@@ -619,9 +642,13 @@ workflows:
619642
- build:
620643
requires:
621644
- check-flaky-or-pr-tests
645+
- post-pr-started:
646+
requires:
647+
- build
622648
- remix-ide-browser:
623649
requires:
624650
- build
651+
- post-pr-started
625652
matrix:
626653
parameters:
627654
browser: ["chrome"]
@@ -634,6 +661,7 @@ workflows:
634661
- post-failed-report:
635662
requires:
636663
- build
664+
- post-pr-started
637665

638666
run_pr_tests:
639667
when: << pipeline.parameters.run_pr_tests >>
@@ -643,9 +671,13 @@ workflows:
643671
- build:
644672
requires:
645673
- check-flaky-or-pr-tests
674+
- post-pr-started:
675+
requires:
676+
- build
646677
- remix-ide-browser:
647678
requires:
648679
- build
680+
- post-pr-started
649681
matrix:
650682
parameters:
651683
browser: ["chrome"]
@@ -658,6 +690,7 @@ workflows:
658690
- post-failed-report:
659691
requires:
660692
- build
693+
- post-pr-started
661694

662695
run_flaky_tests:
663696
when: << pipeline.parameters.run_flaky_tests >>
@@ -667,9 +700,13 @@ workflows:
667700
- build:
668701
requires:
669702
- check-flaky-or-pr-tests
703+
- post-pr-started:
704+
requires:
705+
- build
670706
- remix-ide-browser:
671707
requires:
672708
- build
709+
- post-pr-started
673710
matrix:
674711
parameters:
675712
browser: ["chrome"]
@@ -682,24 +719,30 @@ workflows:
682719
- post-failed-report:
683720
requires:
684721
- build
722+
- post-pr-started
685723

686724
web:
687725
when: << pipeline.parameters.run_all_tests >>
688726
jobs:
689727
- build
728+
- post-pr-started:
729+
requires:
730+
- build
690731
- build-plugin:
691732
matrix:
692733
parameters:
693734
plugin: ["plugin_api"]
694735
- lint:
695736
requires:
696737
- build
738+
- post-pr-started
697739
- remix-libs
698740
- remix-test-plugins:
699741
name: test-plugin-<< matrix.plugin >>
700742
requires:
701743
- build
702744
- build-plugin
745+
- post-pr-started
703746
matrix:
704747
alias: plugins
705748
parameters:
@@ -711,6 +754,7 @@ workflows:
711754
- remix-ide-browser:
712755
requires:
713756
- build
757+
- post-pr-started
714758
matrix:
715759
alias: chrome-tests
716760
parameters:
@@ -731,6 +775,7 @@ workflows:
731775
- post-failed-report:
732776
requires:
733777
- build
778+
- post-pr-started
734779

735780
lint_only:
736781
when: << pipeline.parameters.run_lint_only >>
@@ -758,12 +803,18 @@ workflows:
758803
when: << pipeline.parameters.run_rerun_failed >>
759804
jobs:
760805
- build
806+
- post-failed-report:
807+
name: post-pr-started
808+
requires:
809+
- build
761810
- rerun-failed-e2e:
762811
requires:
763812
- build
813+
- post-pr-started
764814
history_limit: << pipeline.parameters.rerun_failed_history >>
765815
selection_mode: << pipeline.parameters.rerun_failed_mode >>
766816
workflow_name: << pipeline.parameters.rerun_failed_workflow >>
767817
- post-failed-report:
768818
requires:
769819
- build
820+
- post-pr-started

scripts/post-pr-started.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env node
2+
/*
3+
Creates or updates the sticky PR comment (same marker as post-pr-report.js)
4+
to indicate that CI testing has started. Designed to run early in workflows
5+
so contributors immediately see progress.
6+
7+
Auth options (same as post-pr-report.js):
8+
- Preferred: GitHub App via env CI_PR_BOT_APP_ID, CI_PR_BOT_INSTALLATION_ID, CI_PR_BOT_PRIVATE_KEY
9+
- Fallback: GH_PR_COMMENT_TOKEN (or GITHUB_TOKEN/GH_TOKEN)
10+
11+
Env expected from CircleCI:
12+
- CIRCLE_PROJECT_SLUG (or CIRCLE_PROJECT_USERNAME + CIRCLE_PROJECT_REPONAME)
13+
- CIRCLE_BUILD_NUM (for context, optional)
14+
- CIRCLE_PULL_REQUESTS or CIRCLE_PULL_REQUEST (PR URL) OR CIRCLE_SHA1 (to resolve PR)
15+
*/
16+
17+
const { createAppAuth } = require('@octokit/auth-app');
18+
19+
// Prefer descriptive env var names; fall back to legacy
20+
const GH_TOKEN = process.env.GH_PR_COMMENT_TOKEN || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '';
21+
const APP_ID_ENV = process.env.CI_PR_BOT_APP_ID || process.env.APP_ID;
22+
const INSTALLATION_ID_ENV = process.env.CI_PR_BOT_INSTALLATION_ID || process.env.INSTALLATION_ID;
23+
const APP_PRIVATE_KEY_ENV = process.env.CI_PR_BOT_PRIVATE_KEY || process.env.APP_PRIVATE_KEY;
24+
const HAS_APP_CREDS = !!(APP_ID_ENV && INSTALLATION_ID_ENV && APP_PRIVATE_KEY_ENV);
25+
26+
const SLUG = process.env.CIRCLE_PROJECT_USERNAME && process.env.CIRCLE_PROJECT_REPONAME
27+
? `gh/${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`
28+
: (process.env.CIRCLE_PROJECT_SLUG || '');
29+
const PR_URLS = (process.env.CIRCLE_PULL_REQUESTS || process.env.CIRCLE_PULL_REQUEST || '').split(',').map(s=>s.trim()).filter(Boolean);
30+
const SHA = process.env.CIRCLE_SHA1 || '';
31+
const MARKER = '<!-- remix-e2e-report -->';
32+
const STATUS_CONTEXT = 'remix/e2e-report';
33+
const REPORT_SET_STATUS = process.env.REPORT_SET_STATUS === '1';
34+
35+
function exit(msg) { console.error(`[post-pr-started] ${msg}`); process.exit(2); }
36+
function log(...a){ console.log('[post-pr-started]', ...a); }
37+
38+
if (!HAS_APP_CREDS && !GH_TOKEN) exit('Missing GitHub auth: set GH_PR_COMMENT_TOKEN or CI_PR_BOT_* app credentials');
39+
if (!SLUG) exit('Missing CircleCI slug env');
40+
41+
function formatRunTime() {
42+
const now = new Date();
43+
return now.toLocaleString('en-US', {
44+
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric',
45+
hour: '2-digit', minute: '2-digit', timeZoneName: 'short'
46+
});
47+
}
48+
49+
(async () => {
50+
const { owner, repo } = parseSlug(SLUG);
51+
const prNumber = await resolvePrNumber(owner, repo, PR_URLS, SHA);
52+
if (!prNumber) {
53+
log('Cannot resolve PR number from env; skipping comment update.');
54+
process.exit(0);
55+
}
56+
57+
// Fetch existing comments to find sticky
58+
const existing = await gh(`GET /repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`);
59+
const mine = (existing || []).find(c => typeof c.body === 'string' && c.body.includes(MARKER));
60+
61+
const runTime = formatRunTime();
62+
const startedBody = [
63+
MARKER,
64+
'🟡 CI: tests have started. Waiting for results…',
65+
'',
66+
`_Last update: ${runTime}_`,
67+
'',
68+
'_This comment will be updated automatically once results are available._'
69+
].join('\n');
70+
71+
if (mine && mine.id) {
72+
await gh(`PATCH /repos/${owner}/${repo}/issues/comments/${mine.id}`, { body: startedBody });
73+
log(`Updated sticky PR comment #${mine.id} to "started" state`);
74+
} else {
75+
const created = await gh(`POST /repos/${owner}/${repo}/issues/${prNumber}/comments`, { body: startedBody });
76+
log(`Created sticky PR comment id=${created.id}`);
77+
}
78+
79+
if (REPORT_SET_STATUS && SHA) {
80+
await gh(`POST /repos/${owner}/${repo}/statuses/${SHA}`, {
81+
state: 'pending',
82+
description: 'E2E tests running',
83+
context: STATUS_CONTEXT
84+
});
85+
log(`Set commit status ${STATUS_CONTEXT}: pending`);
86+
}
87+
})().catch(e => { console.error(e); process.exit(1); });
88+
89+
function parseSlug(slug) {
90+
const m = String(slug).match(/^(?:gh|github)\/([^/]+)\/([^/]+)$/);
91+
if (!m) exit(`Bad slug: ${slug}`);
92+
return { owner: m[1], repo: m[2] };
93+
}
94+
95+
async function resolvePrNumber(owner, repo, prUrls, sha) {
96+
for (const u of prUrls) {
97+
const m = String(u).trim().match(/\/pull\/(\d+)/);
98+
if (m) return Number(m[1]);
99+
}
100+
if (!sha) return null;
101+
const res = await gh(`GET /repos/${owner}/${repo}/commits/${sha}/pulls`, null,
102+
{ accept: 'application/vnd.github.groot-preview+json' });
103+
if (Array.isArray(res) && res[0]?.number) return res[0].number;
104+
return null;
105+
}
106+
107+
async function gh(pathname, body, extraHeaders) {
108+
const [method, endpoint] = pathname.includes(' ') ? pathname.split(' ', 2) : ['GET', pathname];
109+
const authHeader = await getAuthHeader();
110+
const res = await fetch(`https://api.github.com${endpoint}`, {
111+
method,
112+
headers: {
113+
Authorization: authHeader,
114+
'Content-Type': 'application/json',
115+
...(extraHeaders || {})
116+
},
117+
body: body ? JSON.stringify(body) : undefined
118+
});
119+
if (!res.ok) {
120+
const t = await res.text();
121+
throw new Error(`GitHub ${res.status} ${endpoint}: ${t}`);
122+
}
123+
return res.json();
124+
}
125+
126+
async function getAuthHeader() {
127+
const appId = APP_ID_ENV;
128+
const instId = INSTALLATION_ID_ENV;
129+
let pk = APP_PRIVATE_KEY_ENV;
130+
131+
if (appId && instId && pk) {
132+
// Normalize private key newlines
133+
if (pk.includes('\\n') && !pk.includes('\n')) pk = pk.replace(/\\n/g, '\n');
134+
if (!pk.includes('-----BEGIN')) {
135+
throw new Error('Invalid private key format: missing PEM headers.');
136+
}
137+
const auth = createAppAuth({ appId: String(appId), privateKey: String(pk), installationId: String(instId) });
138+
const { token } = await auth({ type: 'installation' });
139+
return `token ${token}`;
140+
}
141+
if (!GH_TOKEN) throw new Error('GH_PR_COMMENT_TOKEN missing (or configure CI_PR_BOT_* app credentials)');
142+
return `token ${GH_TOKEN}`;
143+
}

0 commit comments

Comments
 (0)