-
Notifications
You must be signed in to change notification settings - Fork 6
147 lines (137 loc) · 7.29 KB
/
Copy pathbuild-explainer.yml
File metadata and controls
147 lines (137 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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