@@ -5,21 +5,28 @@ name: examples
55# it, and destroys it in a `finally` block — so a green job means the example
66# worked against production.
77#
8- # Cost note: every run spawns real VMs and makes paid LLM API calls.
8+ # Runner: our own createos self-hosted runner (createos-sandbox-ghar). Every job
9+ # runs on `runs-on: [createos]`, so the controller boots a throwaway createos
10+ # microVM per job — the CI itself runs inside a sandbox. One example per matrix
11+ # job, one microVM per example; the runner autoscaler handles concurrency, so
12+ # there is no manual sharding here.
13+ #
14+ # Reaper constraint: ghar destroys any runner VM older than REAPER_MAX_AGE_MS
15+ # (30 min in prod). Each job must finish well under that, hence one example per
16+ # job + a job `timeout-minutes` below the cutoff — never batch examples serially
17+ # into a single job or the reaper kills the VM mid-run.
18+ #
19+ # Cost note: every run spawns real VMs and makes paid LLM API calls, and 42
20+ # example jobs boot 42 microVMs at once against a shared MAX_CONCURRENT pool.
921# Triggers are kept deliberate: nightly, manual, and on push to main.
1022#
1123# No `pull_request` trigger by design. Examples run arbitrary `bun index.ts`
12- # with real provider secrets on a self-hosted runner ; firing that on PRs
13- # would let any fork PR exfiltrate those secrets. Examples are verified on
14- # push to main (post-merge) instead.
24+ # with real provider secrets; firing that on PRs would let any fork PR
25+ # exfiltrate those secrets. Examples are verified on push to main (post-merge)
26+ # instead.
1527
1628on :
1729 workflow_dispatch :
18- inputs :
19- parallelism :
20- description : " Number of shards to split examples across (each shard is its own runner pod)"
21- type : string
22- default : " 10"
2330 schedule :
2431 - cron : " 17 3 * * *" # nightly 03:17 UTC
2532 push :
@@ -40,15 +47,13 @@ jobs:
4047 # Build the example matrix from the examples/ directory so new examples are
4148 # picked up automatically — no edits to this file when one is added.
4249 # Discover also builds the SDK and installs example deps ONCE, seeding a
43- # shared cache. The matrix jobs restore dist/ + examples/node_modules from
44- # it instead of repeating `bun install` + `bun run build` 37 times.
50+ # shared cache. The matrix jobs restore dist/ + the bun store from it instead
51+ # of repeating `bun install` + `bun run build` 42 times.
4552 discover :
46- runs-on : arc-runner-set
53+ runs-on : [createos]
4754 outputs :
4855 examples : ${{ steps.list.outputs.examples }}
4956 cache-key : ${{ steps.key.outputs.key }}
50- shards : ${{ steps.shards.outputs.shards }}
51- shard-count : ${{ steps.shards.outputs.shard-count }}
5257 steps :
5358 - uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5459 - id : list
6368 run : |
6469 echo "key=examples-deps-${{ hashFiles('src/**', 'tsconfig.json', 'bun.lock', 'examples/bun.lock', 'examples/package.json') }}" >> "$GITHUB_OUTPUT"
6570
66- # Parallelism is decided here, not hardcoded in the run job: N shards ->
67- # N independent runner pods in the matrix below, each handling its own
68- # slice of the example set instead of stacking concurrent `bun index.ts`
69- # processes on a single pod.
70- - id : shards
71- env :
72- PARALLELISM : ${{ github.event.inputs.parallelism }}
73- run : |
74- n="${PARALLELISM:-10}"
75- echo "shard-count=$n" >> "$GITHUB_OUTPUT"
76- echo "shards=$(jq -nc --argjson n "$n" '[range($n)]')" >> "$GITHUB_OUTPUT"
77-
7871 - uses : oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
7972 with :
8073 bun-version : latest
@@ -105,18 +98,16 @@ jobs:
10598
10699 run :
107100 needs : discover
108- runs-on : arc-runner-set
101+ runs-on : [createos]
109102 strategy :
110103 fail-fast : false
111104 matrix :
112- shard : ${{ fromJson(needs.discover.outputs.shards) }}
113- # worst case: ceil(examples / shard-count) examples run serially per shard,
114- # 20 min timeout each (42 examples / 10 shards = 5 x 20 min)
115- timeout-minutes : 120
105+ example : ${{ fromJson(needs.discover.outputs.examples) }}
106+ # One example per job; must stay under the ghar reaper's 30-min VM cutoff.
107+ # The example itself is capped at `timeout 1200` (20 min) below, leaving
108+ # setup headroom before this job timeout.
109+ timeout-minutes : 25
116110 env :
117- EXAMPLES_JSON : ${{ needs.discover.outputs.examples }}
118- SHARD : ${{ matrix.shard }}
119- SHARD_COUNT : ${{ needs.discover.outputs.shard-count }}
120111 # createos-sandbox control plane (read by the SDK + most examples)
121112 CREATEOS_SANDBOX_API_KEY : ${{ secrets.CREATEOS_SANDBOX_API_KEY }}
122113 CREATEOS_SANDBOX_BASE_URL : ${{ secrets.CREATEOS_SANDBOX_BASE_URL }}
@@ -173,72 +164,37 @@ jobs:
173164 working-directory : examples
174165 run : bun install
175166
176- # example 20 needs python3-venv; install once since the job runs all examples
177- - name : Set up Python
167+ # example 20 needs python3-venv; only that job pays for the setup.
168+ - name : Set up Python (example 20)
169+ if : startsWith(matrix.example, '20-')
178170 uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
179171 with :
180172 python-version : " 3.12"
181173
182- # example 38 mounts an S3 bucket; play.min.io purges buckets periodically
174+ # example 38 mounts an S3 bucket; play.min.io purges buckets periodically.
183175 - name : Ensure S3 bucket (example 38)
176+ if : startsWith(matrix.example, '38-')
184177 run : |
185178 curl -sSfL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
186179 chmod +x /tmp/mc
187180 /tmp/mc alias set ci "$S3_ENDPOINT" "$S3_ACCESS_KEY" "$S3_SECRET_KEY"
188181 /tmp/mc mb --ignore-existing "ci/$S3_BUCKET"
189182
190- - name : Run examples (shard ${{ matrix.shard }}/${{ needs.discover.outputs.shard-count }})
191- run : |
192- mkdir -p /tmp/example-logs /tmp/example-fails
193- mapfile -t ALL < <(echo "$EXAMPLES_JSON" | jq -r '.[]')
194- EXAMPLES=()
195- for i in "${!ALL[@]}"; do
196- (( i % SHARD_COUNT == SHARD )) && EXAMPLES+=("${ALL[$i]}")
197- done
198- printf '%s\n' "${EXAMPLES[@]}" > /tmp/shard-examples.txt
199-
200- # -P1: parallelism is now expressed as shard count (job/pod level),
201- # not concurrent processes within a single pod.
202- printf '%s\n' "${EXAMPLES[@]}" | xargs -n1 -P1 bash -c '
203- example="$1"
204- log="/tmp/example-logs/$example.log"
205- (cd "$GITHUB_WORKSPACE/examples/$example" && timeout 1200 bun index.ts) >"$log" 2>&1
206- rc=$?
207- [[ $rc -ne 0 ]] && echo "$rc" > "/tmp/example-fails/$example"
208- printf "[%s] %s\n" "$([ $rc -eq 0 ] && echo "OK " || echo "FAIL")" "$example"
209- exit $rc
210- ' _ || true
211-
212- - name : Report results
213- if : always()
183+ - name : Run example ${{ matrix.example }}
184+ env :
185+ EXAMPLE : ${{ matrix.example }}
214186 run : |
215- mapfile -t EXAMPLES < /tmp/shard-examples.txt
216- mapfile -t FAILED < <(ls /tmp/example-fails/ 2>/dev/null | sort)
217- total=${#EXAMPLES[@]}
218- nfailed=${#FAILED[@]}
219-
220- {
221- echo "## Examples (shard ${SHARD}): $(( total - nfailed ))/${total} passed"
222- echo ""
223- echo "| Example | Result |"
224- echo "| --- | --- |"
225- for ex in "${EXAMPLES[@]}"; do
226- if [[ -f "/tmp/example-fails/$ex" ]]; then
227- echo "| \`$ex\` | ❌ |"
228- else
229- echo "| \`$ex\` | ✅ |"
230- fi
231- done
232- } >> "$GITHUB_STEP_SUMMARY"
233-
234- # Example failures are advisory: flaky external deps (LLM APIs,
235- # sandbox quota/429s, play.min.io bucket purges) shouldn't red the
236- # run. Surface them as warnings + full logs, but do not exit non-zero.
237- if [[ $nfailed -gt 0 ]]; then
238- echo "::warning::${nfailed} of ${total} example(s) failed in shard ${SHARD}: ${FAILED[*]}"
239- for ex in "${FAILED[@]}"; do
240- echo "::group::FAILED: $ex"
241- cat "/tmp/example-logs/$ex.log" || true
242- echo "::endgroup::"
243- done
187+ set +e
188+ (cd "examples/$EXAMPLE" && timeout 1200 bun index.ts)
189+ rc=$?
190+ set -e
191+ if [ "$rc" -eq 0 ]; then
192+ echo "## ✅ $EXAMPLE passed" >> "$GITHUB_STEP_SUMMARY"
193+ else
194+ echo "## ❌ $EXAMPLE failed (rc=$rc)" >> "$GITHUB_STEP_SUMMARY"
195+ # Example failures are advisory: flaky external deps (LLM APIs,
196+ # sandbox quota/429s, play.min.io bucket purges) shouldn't red the
197+ # run. Surface as a warning (full log is already in the job output),
198+ # but exit 0 so the matrix stays green.
199+ echo "::warning::example $EXAMPLE failed (rc=$rc)"
244200 fi
0 commit comments