* feat: add GitHub Workflows skill with CI workflow and ci_monitor tool - Runs on push to main and feature branches - Runs on pull requests to main - Build + test pipeline using Node 22 Cross-platform CI monitoring tool for debugging GitHub Actions: - `runs` - List recent workflow runs - `watch` - Monitor running workflow - `fail-fast` - Exit 1 on first failure (for scripts) - `log-failed` - Show failed job logs - `test-summary` - Extract test pass/fail counts - `check-actions` - GraphQL query for action versions - `grep` - Search logs with context - `wait-for` - Block until deployment keyword appears Pure Node.js - no shell interpolation, works on macOS/Windows/Linux. Drift-immune skill that: - Routes all CI operations through ci_monitor.cjs - Fetches live docs from docs.github.com (no stale training data) - Provides validation constraints (BEFORE/AFTER/EVIDENCE) - Split tests into test:unit (141 tests, ~12s) and test:integration (5 tests) - Fixed idle-recovery.test.ts for current implementation - Removed AGENTS.md dead code from resource-loader.ts - Moved npm run build out of tests (fixes ENOBUFS) When CI fails, you need observable diagnostics: - `gh run` output is not script-friendly - ci_monitor.cjs provides structured output for automation - The skill ensures AI uses the tool, not stale training data * fix: resolve imports and path for current upstream version - Updated imports from @mariozechner/pi-coding-agent to @gsd/pi-coding-agent - Fixed integration test path calculation to use process.cwd() - Kept test:unit and test:integration scripts * fix: replace search provider preference instead of accumulating AuthStorage.set() for api_key credentials appends to the existing list rather than replacing. When setSearchProviderPreference was called twice with different values, the second call appended the new value, leaving the first value at index 0, which get() returned. Fix: call auth.remove() before auth.set() to ensure only the latest preference is stored. https://claude.ai/code/session_01Qx7HRSDb117KzDZzdKk1KB * fix: address all 10 open PR review comments - package.json: run build before test:integration so a fresh checkout works - pack-install.test.ts: replace execSync+shell redirects with execFileSync argument arrays (portable, no shell parsing, paths with spaces safe) - ci_monitor.test.ts: remove unconditional passed++ after assert; move success message after the failed > 0 check so it only prints on success - setup_gh.cjs: replace unzip/tar shell-outs with platform-specific execFileSync calls (unzip on macOS, PowerShell Expand-Archive on Windows); add compareVersions() for correct element-by-element semver comparison - ci_monitor.cjs: add --repo/-R global option so repo is overrideable; fix getLogs() to use gh run view --log --job instead of binary REST endpoint https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: make all changed files fully cross-platform (Windows/macOS/Linux) - pack-install.test.ts: use tar npm package instead of tar CLI; resolve gsd binary as gsd.cmd on Windows; skip shebang check on Windows - setup_gh.cjs: use execFileSync for all binary invocations; replace which with where on Windows; add Windows PATH guidance; filter preferred install dirs by platform; unify ZIP extraction to use process.platform consistently; escape single quotes in PowerShell Expand-Archive args - ci_monitor.cjs: use path.join for .github/workflows paths; replace all split('\n') with split(/\r?\n/) to handle Windows CRLF output https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * refactor: simplify and deduplicate changed files - ci_monitor.cjs: memoize getRepo() so gh repo view subprocess runs at most once per invocation instead of once per command call in watch loops - pack-install.test.ts: extract packTarball() helper to eliminate duplicate npm pack logic across two tests; remove unused contents variable https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * refactor: remove redundant existsSync before canWrite() in findInstallDir canWrite() already returns false for non-existent directories, so the pre-check was a TOCTOU-style redundancy with no behavioral value. https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: replace tar npm package with Node built-ins (zlib + manual tar parsing) tar is not in the dependency tree. listTarEntries() decompresses via createGunzip() and parses the 512-byte tar block format directly, reading name/prefix/type/size fields per POSIX ustar spec. No external dependency required. Also fixes the broken tarball variable reference left over from the packTarball() refactor. https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * remove: drop setup_gh scripts in favour of ci_monitor setup_gh.cjs and setup_gh.py were one-shot gh CLI installers. ci_monitor.cjs covers the day-to-day CI use case and is the tool the skill routes through. Environments that need gh installed can use brew/winget/distro packages directly. https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: run only unit tests in CI — integration tests cause ENOBUFS The integration tests (npm pack → npm install → spawn node) exceed the buffer limits of the CI runner environment. They are documented as requiring a manual build+run step. CI now runs test:unit only. https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: run all tests in CI without ENOBUFS - ci.yml: run unit and integration as separate steps; build is already its own step so test:integration doesn't need to rebuild - package.json: remove npm run build from test:integration script - pack-install.test.ts: npm install uses stdio:'ignore' to avoid piping large output through Node buffers (root cause of ENOBUFS); add early dist/ check with clear error message instead of rebuilding https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: resolve ENOBUFS and clean up setup_gh references - pack-install.test.ts: derive tarball filename from package.json instead of piping npm pack --json stdout; use stdio:ignore throughout to avoid exhausting OS pipe buffers on CI runners - SKILL.md: remove setup_gh install instructions; assume gh is pre-installed via system package manager; point to ci_monitor.cjs - github_project_setup.py: remove setup_gh.py reference from error message https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM * fix: address Copilot review comments on pack-install.test.ts - listTarEntries: collect chunks in array, Buffer.concat once on end instead of O(n²) repeated concat in data handler - listTarEntries: attach error handler to createReadStream so read errors reject the Promise instead of crashing the process - npm pack: use stdio:['ignore','ignore','pipe'] to preserve stderr for diagnostics while still avoiding ENOBUFS on stdout - npm install: same — pipe stderr so failures include error output https://claude.ai/code/session_01AT6CgcAB62kWcDsTJg9HZM --------- Co-authored-by: Claude <noreply@anthropic.com>
98 lines
4.4 KiB
TypeScript
98 lines
4.4 KiB
TypeScript
// Tests for ci_monitor.cjs — cross-platform CI monitoring tool
|
|
//
|
|
// Sections:
|
|
// (a) Script exists and is executable
|
|
// (b) --help shows all commands
|
|
// (c) list-workflows finds workflow files
|
|
// (d) check-actions parses actions from workflow
|
|
// (e) Commands validate required arguments
|
|
|
|
import { existsSync } from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, '..', '..');
|
|
const SCRIPT_PATH = join(ROOT, 'scripts', 'ci_monitor.cjs');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition: boolean, message: string): void {
|
|
if (condition) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
console.error(` FAIL: ${message}`);
|
|
}
|
|
}
|
|
|
|
function runScript(args: string[]): { stdout: string; stderr: string; status: number | null } {
|
|
const result = spawnSync('node', [SCRIPT_PATH, ...args], {
|
|
encoding: 'utf-8',
|
|
timeout: 30000,
|
|
});
|
|
return {
|
|
stdout: result.stdout || '',
|
|
stderr: result.stderr || '',
|
|
status: result.status,
|
|
};
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
|
|
console.log('# === (a) Script exists and is executable ===');
|
|
assert(existsSync(SCRIPT_PATH), 'ci_monitor.cjs exists');
|
|
const scriptStat = spawnSync('node', ['--check', SCRIPT_PATH], { encoding: 'utf-8' });
|
|
assert(scriptStat.status === 0, 'ci_monitor.cjs has valid JavaScript syntax');
|
|
|
|
console.log('\n# === (b) --help shows all commands ===');
|
|
const help = runScript(['--help']);
|
|
assert(help.status === 0, '--help exits with code 0');
|
|
assert(help.stdout.includes('runs'), 'help shows runs command');
|
|
assert(help.stdout.includes('watch'), 'help shows watch command');
|
|
assert(help.stdout.includes('fail-fast'), 'help shows fail-fast command');
|
|
assert(help.stdout.includes('log-failed'), 'help shows log-failed command');
|
|
assert(help.stdout.includes('test-summary'), 'help shows test-summary command');
|
|
assert(help.stdout.includes('check-actions'), 'help shows check-actions command');
|
|
assert(help.stdout.includes('grep'), 'help shows grep command');
|
|
assert(help.stdout.includes('wait-for'), 'help shows wait-for command');
|
|
|
|
console.log('\n# === (c) list-workflows finds workflow files ===');
|
|
const workflows = runScript(['list-workflows']);
|
|
// May fail if no .github/workflows exists, that's OK
|
|
if (workflows.status === 0) {
|
|
assert(workflows.stdout.includes('.yml') || workflows.stdout.includes('No workflow files') || workflows.stdout.includes('No .github'), 'list-workflows output mentions yml files or none found');
|
|
} else {
|
|
// If it fails, should be due to missing directory
|
|
assert(workflows.stderr.includes('No .github/workflows'), 'list-workflows fails gracefully when no workflows dir');
|
|
}
|
|
|
|
console.log('\n# === (d) check-actions validates workflow file ===');
|
|
const checkMissing = runScript(['check-actions', '.github/workflows/nonexistent.yml']);
|
|
assert(checkMissing.status !== 0, 'check-actions fails for missing file');
|
|
assert(checkMissing.stderr.includes('not found') || checkMissing.stderr.includes('File not found'), 'check-actions reports missing file');
|
|
|
|
console.log('\n# === (e) Commands validate required arguments ===');
|
|
const grepNoPattern = runScript(['grep', '12345']);
|
|
assert(grepNoPattern.status !== 0, 'grep fails without --pattern');
|
|
assert(grepNoPattern.stderr.includes('--pattern') || grepNoPattern.stderr.includes('required'), 'grep reports missing pattern');
|
|
|
|
const waitNoKeyword = runScript(['wait-for', '12345', 'build']);
|
|
assert(waitNoKeyword.status !== 0, 'wait-for fails without --keyword');
|
|
assert(waitNoKeyword.stderr.includes('--keyword') || waitNoKeyword.stderr.includes('required'), 'wait-for reports missing keyword');
|
|
|
|
const compareMissing = runScript(['compare', '12345']);
|
|
assert(compareMissing.status !== 0, 'compare fails with only one run-id');
|
|
|
|
// ─── Summary ───────────────────────────────────────────────────────────────
|
|
|
|
console.log('\n# ========================================');
|
|
console.log(`# Results: ${passed} passed, ${failed} failed`);
|
|
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('# All tests passed ✓');
|