Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions .archon/scripts/ci-wait.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bun
/**
* Wait for GitHub CI on a PR to finish, with a hard wall-clock timeout.
*
* Usage: bun .archon/scripts/ci-wait.js <pr-number-or-url> [timeout-ms]
*
* Exit codes:
* 0 — all required checks passed
* 1 — at least one required check failed
* 3 — timeout reached before CI finished
* 2 — bad args / missing gh
*
* Used by archon-slack-feature-to-review-app to gate review-app deploy.
*/
import { spawn } from 'node:child_process';

const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;

function main() {
const [pr, timeoutArg] = process.argv.slice(2);

if (!pr) {
console.error('Usage: ci-wait.js <pr-number-or-url> [timeout-ms]');
process.exit(2);
}

const timeoutMs = timeoutArg ? Number(timeoutArg) : DEFAULT_TIMEOUT_MS;
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
console.error(`Invalid timeout-ms: ${timeoutArg}`);
process.exit(2);
}

console.log(
`Waiting for CI on PR ${pr} (timeout: ${Math.round(timeoutMs / 1000)}s)...`
);

const child = spawn(
'gh',
['pr', 'checks', pr, '--watch', '--fail-fast', '--interval', '30'],
{ stdio: 'inherit' }
);

let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
console.error(`\nCI wait timed out after ${Math.round(timeoutMs / 1000)}s`);
child.kill('SIGTERM');
setTimeout(() => process.exit(3), 2000).unref();
}, timeoutMs);
timer.unref();

child.on('exit', (code, _signal) => {
clearTimeout(timer);
if (timedOut) return;
if (code === 0) {
console.log('CI passed.');
process.exit(0);
}
console.error(`CI failed (gh exit code ${code ?? 'null'})`);
process.exit(1);
});

child.on('error', err => {
clearTimeout(timer);
console.error(`Failed to spawn gh: ${err.message}`);
process.exit(2);
});
}

main();
47 changes: 47 additions & 0 deletions .archon/scripts/dispatch-review-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bun
/**
* Dispatch a GitHub Actions workflow_dispatch event on the given ref.
*
* Usage: bun .archon/scripts/dispatch-review-app.js <workflow-file> <ref>
*
* Exits 0 on successful dispatch. Exits non-zero with a human-readable stderr
* message on any failure (missing args, gh not installed, gh call failed).
*
* Used by the archon-slack-feature-to-review-app workflow after CI passes
* to deploy a review app for the PR branch.
*/
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

async function main() {
const [workflowFile, ref] = process.argv.slice(2);

if (!workflowFile || !ref) {
console.error('Usage: dispatch-review-app.js <workflow-file> <ref>');
process.exit(2);
}

try {
const { stdout, stderr } = await execFileAsync('gh', [
'workflow',
'run',
workflowFile,
'--ref',
ref,
]);
if (stdout.trim()) console.log(stdout.trim());
if (stderr.trim()) console.log(stderr.trim());
console.log(
JSON.stringify({ dispatched: true, workflow: workflowFile, ref })
);
} catch (err) {
console.error(
`Failed to dispatch ${workflowFile} on ref ${ref}: ${err.stderr ?? err.message}`
);
process.exit(1);
}
}

void main();
107 changes: 107 additions & 0 deletions .archon/scripts/fetch-review-app-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bun
/**
* Poll a GitHub PR's comments for a review-app URL matching a regex.
*
* Usage:
* bun .archon/scripts/fetch-review-app-url.js <pr> <regex> [timeout-ms] [interval-ms]
*
* Exit codes:
* 0 — URL found; printed to stdout as the only stdout line
* 3 — timeout reached without a match
* 2 — bad args / gh failure / invalid regex / bad comments JSON
*
* The workflow consumes the trimmed stdout via $<node-id>.output.
* All log lines go to stderr so the URL is the only stdout content.
*/
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
const DEFAULT_INTERVAL_MS = 20 * 1000;

async function pollOnce(pr, regex) {
const { stdout } = await execFileAsync('gh', [
'pr',
'view',
pr,
'--json',
'comments',
]);
let parsed;
try {
parsed = JSON.parse(stdout);
} catch {
throw new Error(`gh returned non-JSON stdout: ${stdout.slice(0, 200)}`);
}
const comments = parsed.comments ?? [];
for (const c of comments) {
const match = typeof c.body === 'string' ? c.body.match(regex) : null;
if (match) return match[0];
}
return null;
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
const [pr, regexStr, timeoutArg, intervalArg] = process.argv.slice(2);

if (!pr || !regexStr) {
console.error(
'Usage: fetch-review-app-url.js <pr> <regex> [timeout-ms] [interval-ms]'
);
process.exit(2);
}

let regex;
try {
regex = new RegExp(regexStr);
} catch (err) {
console.error(
`Invalid regex ${JSON.stringify(regexStr)}: ${err.message}`
);
process.exit(2);
}

const timeoutMs = timeoutArg ? Number(timeoutArg) : DEFAULT_TIMEOUT_MS;
const intervalMs = intervalArg ? Number(intervalArg) : DEFAULT_INTERVAL_MS;
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
console.error(`Invalid timeout-ms: ${timeoutArg}`);
process.exit(2);
}
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
console.error(`Invalid interval-ms: ${intervalArg}`);
process.exit(2);
}

const deadline = Date.now() + timeoutMs;
console.error(
`Polling PR ${pr} for pattern ${regex} every ${Math.round(intervalMs / 1000)}s, up to ${Math.round(timeoutMs / 1000)}s total...`
);

while (Date.now() < deadline) {
try {
const match = await pollOnce(pr, regex);
if (match) {
console.log(match);
return;
}
} catch (err) {
console.error(`Poll error (will retry): ${err.message}`);
}
const remaining = deadline - Date.now();
if (remaining <= 0) break;
await sleep(Math.min(intervalMs, remaining));
}

console.error(
`No matching comment found on PR ${pr} within ${Math.round(timeoutMs / 1000)}s.`
);
process.exit(3);
}

void main();
Loading