Build Explainer #49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |