Skip to content

Commit 238a361

Browse files
themr0cclaude
andcommitted
refactor: simplify deploy-gh-pages.js
- Replace node:https callback API with fetch() (Node 18+) - Remove unused branchDir parameter from applyContent() - Extract cleanup from stageAndCommit into pushWithRetry (runs once, not per retry) - Deduplicate readdirSync call for diagnostics - Add flow overview and inline comments for non-obvious decisions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aa4370c commit 238a361

1 file changed

Lines changed: 49 additions & 69 deletions

File tree

build/scripts/deploy-gh-pages.js

Lines changed: 49 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@
88
*
99
* SPDX-License-Identifier: EPL-2.0
1010
*
11-
* Replaces deploy-gh-pages.sh. Deploys content to the gh-pages branch
12-
* with retry on push rejection, PR/branch cleanup, and index regeneration.
11+
* Deploys build output (titles-generated/) to the gh-pages branch.
12+
*
13+
* Flow:
14+
* 1. Create a temp git repo, fetch gh-pages (shallow)
15+
* 2. Copy --publish-dir content into the working tree
16+
* 3. For branch deploys: clean up stale PR and branch directories
17+
* 4. Regenerate index.html (branch list) and pulls.html (PR list)
18+
* 5. Commit everything (content + cleanup + indexes) and push
19+
* 6. On push rejection: rebase and retry (max 3 attempts)
20+
*
21+
* Branch deploys clean up merged/closed PR dirs and deleted branch dirs.
22+
* PR deploys only update content and pulls.html — no cleanup.
1323
*
1424
* Usage:
1525
* node deploy-gh-pages.js --publish-dir <dir> [--message <msg>]
@@ -21,15 +31,10 @@ import { readdirSync, statSync, writeFileSync, existsSync, cpSync, rmSync, mkdte
2131
import { join, resolve } from 'node:path';
2232
import { tmpdir } from 'node:os';
2333
import { execFileSync } from 'node:child_process';
24-
import { get as httpsGet } from 'node:https';
25-
26-
// ── Constants ────────────────────────────────────────────────────────────────
2734

2835
const MAX_RETRIES = 3;
2936
const RELEASE_NOTES_BASE = 'https://red-hat-developers-documentation.pages.redhat.com/red-hat-developer-hub-release-notes';
3037

31-
// ── Helpers ──────────────────────────────────────────────────────────────────
32-
3338
function git(cwd, ...args) {
3439
const result = execFileSync('git', args, { // NOSONAR: git is resolved from PATH in a controlled CI environment
3540
cwd,
@@ -49,39 +54,27 @@ function noStagedChanges(cwd) {
4954
}
5055
}
5156

52-
function getPRState(owner, repo, prNumber) {
53-
return new Promise((resolve, reject) => {
54-
const options = {
55-
hostname: 'api.github.com',
56-
path: `/repos/${owner}/${repo}/pulls/${prNumber}`,
57+
async function getPRState(owner, repo, prNumber) {
58+
try {
59+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
5760
headers: {
5861
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
5962
'User-Agent': 'deploy-gh-pages',
6063
'Accept': 'application/vnd.github+json',
6164
},
62-
};
63-
httpsGet(options, (res) => {
64-
let data = '';
65-
res.on('data', chunk => { data += chunk; });
66-
res.on('end', () => {
67-
if (res.statusCode !== 200) {
68-
console.log(`GitHub API returned ${res.statusCode} for PR ${prNumber}`);
69-
resolve('unknown');
70-
return;
71-
}
72-
try {
73-
const json = JSON.parse(data);
74-
const closedState = json.merged ? 'merged' : 'closed';
75-
resolve(json.state === 'closed' ? closedState : 'open');
76-
} catch { resolve('unknown'); }
77-
});
78-
res.on('error', () => resolve('unknown'));
79-
}).on('error', () => resolve('unknown'));
80-
});
65+
});
66+
if (!res.ok) {
67+
console.log(`GitHub API returned ${res.status} for PR ${prNumber}`);
68+
return 'unknown';
69+
}
70+
const json = await res.json();
71+
if (json.state !== 'closed') return 'open';
72+
return json.merged ? 'merged' : 'closed';
73+
} catch {
74+
return 'unknown';
75+
}
8176
}
8277

83-
// ── gh-pages branch setup ────────────────────────────────────────────────────
84-
8578
function fetchOrCreateGhPages(deployDir) {
8679
try {
8780
git(deployDir, 'fetch', 'origin', 'gh-pages', '--depth=1');
@@ -97,20 +90,16 @@ function fetchOrCreateGhPages(deployDir) {
9790
}
9891
}
9992

100-
// ── Content application ──────────────────────────────────────────────────────
101-
102-
function applyContent(deployDir, publishDir, branchDir) {
93+
function applyContent(deployDir, publishDir) {
10394
cpSync(publishDir, deployDir, { recursive: true });
10495
}
10596

106-
// ── Cleanup ──────────────────────────────────────────────────────────────────
107-
10897
async function cleanup(deployDir) {
10998
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
11099

111100
// PR cleanup: remove directories for merged/closed PRs
112101
const prDirs = readdirSync(deployDir).filter(d =>
113-
d.startsWith('pr-') && !d.startsWith('.') && statSync(join(deployDir, d)).isDirectory()
102+
d.startsWith('pr-') && statSync(join(deployDir, d)).isDirectory()
114103
);
115104

116105
for (const dir of prDirs) {
@@ -142,8 +131,6 @@ async function cleanup(deployDir) {
142131
}
143132
}
144133

145-
// ── Index generation ─────────────────────────────────────────────────────────
146-
147134
function getReleaseNotesUrl(branch) {
148135
if (branch === 'main') return `${RELEASE_NOTES_BASE}/main/index.html`;
149136
const match = branch.match(/^release-(\d+)\.(\d+)$/);
@@ -180,19 +167,16 @@ function regenerateIndex(deployDir, branchDir) {
180167
writeIndex(deployDir, allDirs.filter(d => d.startsWith('pr-')), 'pulls.html', 'PR Previews', true);
181168
}
182169

183-
// ── Push with retry ──────────────────────────────────────────────────────────
184-
185-
async function stageAndCommit(deployDir, publishDir, branchDir, message) {
186-
if (!branchDir.startsWith('pr-')) {
187-
await cleanup(deployDir);
188-
}
170+
function stageAndCommit(deployDir, publishDir, branchDir, message) {
189171
regenerateIndex(deployDir, branchDir);
190172

191-
const publishEntries = readdirSync(publishDir).filter(e => !e.startsWith('.'));
192-
const toStage = [...publishEntries];
193-
if (existsSync(join(deployDir, 'index.html'))) toStage.push('index.html');
194-
if (existsSync(join(deployDir, 'pulls.html'))) toStage.push('pulls.html');
195-
git(deployDir, 'add', '--force', '--', ...new Set(toStage));
173+
// Force-add publish entries and index files (.gitignore may exclude them)
174+
const forceEntries = new Set(readdirSync(publishDir).filter(e => !e.startsWith('.')));
175+
for (const f of ['index.html', 'pulls.html']) {
176+
if (existsSync(join(deployDir, f))) forceEntries.add(f);
177+
}
178+
git(deployDir, 'add', '--force', '--', ...forceEntries);
179+
// Stage deletions from cleanup
196180
git(deployDir, 'add', '-A');
197181

198182
if (noStagedChanges(deployDir)) {
@@ -210,6 +194,9 @@ async function stageAndCommit(deployDir, publishDir, branchDir, message) {
210194
return true;
211195
}
212196

197+
// On push rejection (concurrent deploy from another branch/PR), try rebase first.
198+
// If rebase conflicts (different branch touched same index files), reset and let
199+
// the retry loop rebuild from a fresh fetch.
213200
function tryRebaseAndPush(deployDir, attempt) {
214201
try {
215202
git(deployDir, 'pull', '--rebase', 'origin', 'gh-pages');
@@ -231,12 +218,17 @@ function tryRebaseAndPush(deployDir, attempt) {
231218
}
232219

233220
async function pushWithRetry(deployDir, publishDir, branchDir, message) {
221+
// Cleanup runs once before retries so we don't make redundant API calls
222+
if (!branchDir.startsWith('pr-')) {
223+
await cleanup(deployDir);
224+
}
225+
234226
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
235227
if (attempt > 1) {
236-
applyContent(deployDir, publishDir, branchDir);
228+
applyContent(deployDir, publishDir);
237229
}
238230

239-
const hasChanges = await stageAndCommit(deployDir, publishDir, branchDir, message);
231+
const hasChanges = stageAndCommit(deployDir, publishDir, branchDir, message);
240232
if (!hasChanges) return;
241233

242234
try {
@@ -251,10 +243,7 @@ async function pushWithRetry(deployDir, publishDir, branchDir, message) {
251243
throw new Error(`Deploy failed after ${MAX_RETRIES} attempts`);
252244
}
253245

254-
// ── Main ─────────────────────────────────────────────────────────────────────
255-
256246
async function deploy() {
257-
// Parse args
258247
const args = process.argv.slice(2);
259248
let publishDir = null;
260249
let message = 'Deploy to GitHub Pages';
@@ -280,7 +269,6 @@ async function deploy() {
280269

281270
publishDir = resolve(publishDir);
282271

283-
// Validate env
284272
if (!process.env.GITHUB_TOKEN) {
285273
console.error('GITHUB_TOKEN is required (set by GitHub Actions)');
286274
process.exit(1);
@@ -290,7 +278,6 @@ async function deploy() {
290278
process.exit(1);
291279
}
292280

293-
// Detect branchDir from publishDir
294281
const entries = readdirSync(publishDir).filter(e =>
295282
!e.startsWith('.') && statSync(join(publishDir, e)).isDirectory()
296283
);
@@ -305,15 +292,11 @@ async function deploy() {
305292

306293
const branchDir = entries[0];
307294

308-
// Diagnostics
309295
console.log(`PUBLISH_DIR: ${publishDir}`);
310296
console.log('Top-level entries in PUBLISH_DIR:');
311-
readdirSync(publishDir)
312-
.filter(e => !e.startsWith('.'))
313-
.forEach(e => console.log(` ${e}`));
297+
entries.forEach(e => console.log(` ${e}`));
314298
console.log(`Branch directory: ${branchDir}`);
315299

316-
// Create temp git repo
317300
const deployDir = mkdtempSync(join(tmpdir(), 'deploy-'));
318301
process.on('exit', () => {
319302
try { rmSync(deployDir, { recursive: true, force: true }); } catch {}
@@ -322,18 +305,15 @@ async function deploy() {
322305
git(deployDir, 'init', '-q');
323306
git(deployDir, 'config', 'user.name', 'github-actions[bot]');
324307
git(deployDir, 'config', 'user.email', 'github-actions[bot]@users.noreply.github.com');
308+
// Auth via http.extraHeader keeps the token out of the remote URL (avoids leaking in logs)
325309
const repoUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}.git`;
326310
git(deployDir, 'remote', 'add', 'origin', repoUrl);
327311
const credentials = Buffer.from('x-access-token:' + process.env.GITHUB_TOKEN).toString('base64');
328312
git(deployDir, 'config', `http.${repoUrl}.extraHeader`, `Authorization: Basic ${credentials}`);
329313

330-
// Fetch gh-pages
331314
fetchOrCreateGhPages(deployDir);
332315

333-
// Apply content on first pass
334-
applyContent(deployDir, publishDir, branchDir);
335-
336-
// Push with retry handles cleanup, index regeneration, staging, commit, and push
316+
applyContent(deployDir, publishDir);
337317
await pushWithRetry(deployDir, publishDir, branchDir, message);
338318
}
339319

0 commit comments

Comments
 (0)