Skip to content

Build Explainer

Build Explainer #49

name: Build Explainer
# Hosted runner for explainmyrepo: given a GitHub repo, build its explainer with the owner's
# keys (repo secrets), deploy it to its own Netlify site, and write progress back to a status
# gist that the website polls. Triggered by the /build Netlify function.
#
# ARCHITECTURE (2026-07-04 rebuild — see project memory "hosted-agentic-architecture", approved
# 2026-06-30 but never actually wired until now): this invokes bin/agentic-runner.mjs, which runs
# the real explainmyrepo SKILL as a headless Claude Code agent — the brain decides, tools/*.mjs do
# the mechanics — instead of the old deterministic orchestrator.mjs pipeline. GitHub Actions is
# still the right HOST for this (ADR-033, agent-harness-generator: triggered, autonomous, no human
# at the keyboard); what changed is that it now runs the AGENT, which adapts per-repo, instead of a
# fixed script that broke on off-mold repos (empty dep-graphs, oversized monorepos).
#
# RAILS: timeout-minutes is COMPUTED from budget_min (a pre-flight repo-size signal from build.js)
# instead of one fixed ceiling for every repo · serialized concurrency (no cost fan-out) · the
# runner enforces its own wall-clock + $ budget and stops cleanly before the outer timeout would
# SIGKILL it · on any failure it alerts the owner directly with full particulars (never silent).
on:
workflow_dispatch:
inputs:
target_repo:
description: "owner/repo to explain"
required: true
build_id:
description: "correlation id (matches the status gist)"
required: false
gist_id:
description: "status gist to write progress into"
required: false
submitter_email:
description: "email to notify when done (optional)"
required: false
budget_min:
description: "wall-clock budget in minutes, sized to repo complexity by build.js"
required: false
default: "20"
budget_usd:
description: "$ budget for this build, sized to repo complexity by build.js"
required: false
default: "6"
timeout_min:
description: "budget_min + overhead buffer, pre-summed by build.js (GH Actions expressions don't do arithmetic on inputs)"
required: false
default: "30"
concurrency:
group: build-explainer
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
# The agent enforces its OWN budget (budget_min) internally and exits cleanly well before this
# outer ceiling would ever need to fire — this is just the backstop. timeout_min is budget_min +
# a fixed overhead buffer, computed in build.js (plain JS), not here: GitHub Actions expressions
# don't support arithmetic on inputs the way `fromJSON(x) + 10` implies — learned this the hard
# way, it rejects the whole dispatch at parse time, which a YAML-structure check doesn't catch
# since it's an expression-syntax error, not a YAML one.
timeout-minutes: ${{ fromJSON(inputs.timeout_min || '30') }}
steps:
- name: Checkout the explainer tool
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Validate input (abuse guard)
env:
# Template-expanded into the env table, not spliced into the script — the raw value never
# touches the shell before validation (a target_repo containing a stray quote could
# otherwise break out of the single-quoted context at YAML-expansion time).
REPO: ${{ github.event.inputs.target_repo }}
run: |
echo "target: $REPO"
if ! printf '%s' "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
echo "::error::invalid repo format (expected owner/name): $REPO"; exit 1
fi
- name: Install dependencies
run: |
npm ci || npm install
# Belt-and-suspenders: guarantee the 3 runtime deps are present (CI 'npm ci' was leaving
# playwright unresolved -> quality-grade failed). EXACT pinned versions + --no-save so this
# cannot drift to a newer/compromised patch or rewrite the committed lockfile (no supply-
# chain bypass; these match package-lock.json).
npm install --no-save playwright@1.61.1 @xenova/transformers@2.17.2 @ruvector/rvf@0.2.2
npx playwright install --with-deps chromium
# System tools the pipeline shells out to (scanned from tools/*.mjs), not all preinstalled
# on the runner: ImageMagick (magick/convert/identify — favicon, social card), xmllint
# (libxml2-utils — diagram SVG validation), dot (graphviz — diagram layout), zip (AI pack).
# Fonts help text render in generated imagery.
sudo apt-get update && sudo apt-get install -y imagemagick libxml2-utils graphviz zip fonts-liberation fonts-dejavu-core
# The agentic kernel itself (bin/agentic-runner.mjs spawns this headless) — pinned exact
# version, same rationale as the runtime deps above: no supply-chain drift on a bare runner.
npm install -g @anthropic-ai/claude-code@2.1.201
claude --version
- name: Build, deploy, and report status
env:
# These reach agentic-runner.mjs's OWN env (used for gist-patching, notify, and the
# failure alert) — the runner builds a SEPARATE, much narrower allowlist for the spawned
# agent process itself (see bin/agentic-runner.mjs), which never sees GH/SMTP creds.
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
SMTP_USER: ${{ secrets.GMAIL_USER }}
SMTP_PASS: ${{ secrets.GMAIL_APP_PASSWORD }}
EXPLAINER_GH_TOKEN: ${{ secrets.EXPLAINER_GH_TOKEN }}
BUILD_ID: ${{ github.event.inputs.build_id }}
GIST_ID: ${{ github.event.inputs.gist_id }}
REPO: ${{ github.event.inputs.target_repo }}
SUBMITTER: ${{ github.event.inputs.submitter_email }}
BUDGET_MIN: ${{ github.event.inputs.budget_min }}
BUDGET_USD: ${{ github.event.inputs.budget_usd }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set +e
set -o pipefail
node bin/agentic-runner.mjs "$REPO" \
--build-dir "$RUNNER_TEMP/hosted-build" \
--budget-min "${BUDGET_MIN:-20}" \
--budget-usd "${BUDGET_USD:-6}" \
--submitter "$SUBMITTER" \
--run-url "$RUN_URL" \
--build-id "$BUILD_ID" \
--gist-id "$GIST_ID" \
2>&1 | tee /tmp/build.log
RC=${PIPESTATUS[0]}
URL=$(grep -oE '^LIVE: https://\S+' /tmp/build.log | tail -1 | sed 's/^LIVE: //')
if [ "$RC" -eq 0 ] && [ -n "$URL" ]; then
echo "LIVE: $URL"
echo "### Explainer ready → $URL" >> "$GITHUB_STEP_SUMMARY"
else
# agentic-runner.mjs already patched the gist + emailed the owner before exiting non-zero.
echo "::error::build failed (rc=$RC, url=$URL)"
echo "---- build.log tail (Actions run log only — NOT published) ----"
tail -c 2000 /tmp/build.log || true
exit 1
fi