mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
train S3: merge #3617 at 18f702ab3e v1.91.370.0
This commit is contained in:
commit
c6055fceaa
8 files changed
+146
-9
No files matched your search
@@ -23,6 +23,22 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
free_runner:
|
||||
description: 'Runner-sizing experiments only: Ubicloud runner for the free-suite shards (ubicloud-standard-2, -4 or -8; empty keeps FREE_SUITE_RUNNER). Never cite such a run as a PR or train Free Tests result.'
|
||||
type: string
|
||||
default: ''
|
||||
free_shards:
|
||||
description: 'Runner-sizing experiments only: shard count, 1-40 (empty keeps FREE_SUITE_SHARDS)'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
# The one source of the free-suite runner and shard count. pull_request and
|
||||
# push runs never see workflow_dispatch inputs, so free-plan resolves these
|
||||
# (or a dispatch override) into outputs that free-suite reads.
|
||||
env:
|
||||
FREE_SUITE_RUNNER: ubicloud-standard-2
|
||||
FREE_SUITE_SHARDS: '20'
|
||||
|
||||
# Keyed on the PR number, not head_ref: a bare branch name carries no fork
|
||||
# prefix, so same-name branches from two forks would share one group and a
|
||||
@@ -43,6 +59,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
runner: ${{ steps.plan.outputs.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -56,9 +73,27 @@ jobs:
|
||||
- id: plan
|
||||
name: Inventory and balance the complete free suite
|
||||
# Warns in the job summary when more than 5 files are unseeded.
|
||||
env:
|
||||
RUNNER_OVERRIDE: ${{ inputs.free_runner }}
|
||||
SHARDS_OVERRIDE: ${{ inputs.free_shards }}
|
||||
run: |
|
||||
matrix=$(bun run scripts/test-free-shards.ts --ci-plan "$RUNNER_TEMP/free-plan.json" --shards 20)
|
||||
set -euo pipefail
|
||||
runner="${RUNNER_OVERRIDE:-$FREE_SUITE_RUNNER}"
|
||||
shards="${SHARDS_OVERRIDE:-$FREE_SUITE_SHARDS}"
|
||||
case "$runner" in
|
||||
ubicloud-standard-2|ubicloud-standard-4|ubicloud-standard-8) ;;
|
||||
*) echo "::error::free_runner '$runner' is not ubicloud-standard-2, -4 or -8"; exit 1 ;;
|
||||
esac
|
||||
case "$shards" in
|
||||
''|*[!0-9]*) echo "::error::free_shards '$shards' is not a whole number"; exit 1 ;;
|
||||
esac
|
||||
if [ "$shards" -lt 1 ] || [ "$shards" -gt 40 ]; then echo "::error::free_shards $shards is outside 1-40"; exit 1; fi
|
||||
if [ "$runner" != "$FREE_SUITE_RUNNER" ] || [ "$shards" != "$FREE_SUITE_SHARDS" ]; then
|
||||
echo "- Runner-sizing experiment: $shards shards on $runner (defaults $FREE_SUITE_SHARDS on $FREE_SUITE_RUNNER); not a PR or train Free Tests result" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
matrix=$(bun run scripts/test-free-shards.ts --ci-plan "$RUNNER_TEMP/free-plan.json" --shards "$shards")
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
echo "runner=$runner" >> "$GITHUB_OUTPUT"
|
||||
- name: Seed growth ratchet (60 s ceiling, shrink-only allowlist)
|
||||
run: |
|
||||
GSTACK_FREE_SEED_BASE="$(git merge-base HEAD origin/main)" bun test test/free-seed-ratchet.test.ts
|
||||
@@ -329,7 +364,7 @@ jobs:
|
||||
|
||||
free-suite:
|
||||
needs: free-plan
|
||||
runs-on: ubicloud-standard-8
|
||||
runs-on: ${{ needs.free-plan.outputs.runner }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -476,6 +511,18 @@ jobs:
|
||||
- name: Build trusted CSO helper used by free contract tests
|
||||
run: bun run build:cso
|
||||
|
||||
# Runner sizing: the suite runs one test at a time, so a shard needs RAM
|
||||
# and disk more than cores. Sample both while it runs and report the peak.
|
||||
- name: Start the memory and disk sampler
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/sample-resources.sh" <<'EOF'
|
||||
while :; do
|
||||
printf '%s %s %s\n' "$(date +%s)" "$(free -k | awk 'NR == 2 { print $3 }')" "$(df -Pk / | awk 'NR == 2 { print $4 }')"
|
||||
sleep 2
|
||||
done
|
||||
EOF
|
||||
nohup sh "$RUNNER_TEMP/sample-resources.sh" > "$RUNNER_TEMP/resources.log" 2>&1 &
|
||||
|
||||
# GSTACK_EXPECT_BINARIES=1 arms make-pdf/test/e2e/ci-prereqs.test.ts:
|
||||
# if a future edit drops the gate build (or poppler), the lane FAILS
|
||||
# instead of the gates silently self-skipping back to false green.
|
||||
@@ -494,6 +541,12 @@ jobs:
|
||||
GSTACK_FREE_RETRY_FLAKY: "1"
|
||||
GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl
|
||||
|
||||
- name: Report peak memory and minimum free disk
|
||||
if: always()
|
||||
run: |
|
||||
if [ ! -s "$RUNNER_TEMP/resources.log" ]; then echo "RESOURCES: no samples"; exit 0; fi
|
||||
awk '{ if ($2 > used) used = $2; if (free == "" || $3 < free) free = $3 } END { printf "RESOURCES: peak_used_mib=%d min_free_disk_mib=%d samples=%d\n", used / 1024, free / 1024, NR }' "$RUNNER_TEMP/resources.log" | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload strict shard result
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.370.0] - 2026-10-31
|
||||
|
||||
**The free suite now runs on `ubicloud-standard-2` instead of `ubicloud-standard-8`, at the same 20 shards, for about a quarter of the runner cost.** Each shard runs its files one at a time, so it waits on disk and processes rather than CPU. In three paired rounds at one commit, the median per-shard time rose by 3 to 8 s, inside the round-to-round spread of −90 to +65 s, while runner-minutes stayed about equal (464 vs 475). Peak memory stayed under 2.5 GiB and free disk above 15 GiB. A 16-shard arm was slower beyond that spread, so the shard count stays at 20.
|
||||
|
||||
### Changed
|
||||
- **`.github/workflows/free-tests.yml`**: one workflow-level `FREE_SUITE_RUNNER` (`ubicloud-standard-2`) and `FREE_SUITE_SHARDS` (`20`) set the free-suite size for pull_request, push and dispatch runs alike. `free-plan` resolves them into its `runner` output and the plan's shard count, and `free-suite` reads `runs-on` from that output. The duration-seed recorder stays on `ubicloud-standard-8`.
|
||||
- **Sizing experiments without a workflow edit**: `workflow_dispatch` takes `free_runner` (`ubicloud-standard-2`, `-4` or `-8` only) and `free_shards` (1–40). `free-plan` refuses any other value before planning, and a run that overrides either one says in its job summary that it is a sizing experiment, not a PR or train Free Tests result.
|
||||
- **Every free-suite shard prints `RESOURCES: peak_used_mib=… min_free_disk_mib=… samples=…`** from a memory and disk sample every 2 s, even when the suite fails. If a shard shows OOM, ENOSPC or an unexplained Chromium launch failure, move `FREE_SUITE_RUNNER` to `ubicloud-standard-4`.
|
||||
|
||||
### Tests
|
||||
- `test/free-tests-workflow-wiring.test.ts`: runs the real `free-plan` step with a stubbed `bun`. It checks that the constant passes through when no override is given, that a valid override is honored, and that `ubuntu-24.04`, `ubicloud-standard-8 x`, `0`, `41` and `16; true` are refused before planning. It also checks that the sampler starts before the suite and that the report step runs `always()`.
|
||||
- `test/free-durations-record-workflow.test.ts`: the recorder and `FREE_SUITE_RUNNER` must both be Ubicloud standard runners. They no longer have to be the same size, because the recorder stays on standard-8.
|
||||
|
||||
## [1.91.369.0] - 2026-10-31
|
||||
|
||||
**`gstack-wtree` works again in a repository that gitignores `.bun/`.** Since GST-421, its `git add -A` carried an exclude pathspec naming `.bun`, and git refuses a pathspec naming an ignored path with exit 1. Every fingerprint call then failed, which broke `gstack-review-log --start`, the evidence checks and /ship in harness-evals (HARN-1, HARN-7, harness-evals#148; GST-449).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.369.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.370.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -197,7 +197,15 @@ shards get duration-aware walls (`max(base, predicted × 3, files × 5s)`). The
|
||||
legacy `--shards N --shard i` path keeps stable hash indices. Required CI uses
|
||||
one duration-packed `--ci-plan`, 20 ordinary `--ci-run` shards plus a separate
|
||||
exclusive-fixture shard, and a `--ci-verify` aggregate. CI shards still run on
|
||||
independent machines without ordering unrelated jobs. The public
|
||||
independent machines without ordering unrelated jobs. Two workflow-level
|
||||
constants in `free-tests.yml` set that size for every event: `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-2`) and `FREE_SUITE_SHARDS` (`'20'`), which `free-plan`
|
||||
resolves into the shards' runner and shard count. A `workflow_dispatch` run may
|
||||
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`) and
|
||||
`free_shards` (1–40) for runner-sizing experiments only; its job summary says
|
||||
so, and it is never cited as a PR or train Free Tests result. Each shard prints
|
||||
`RESOURCES: peak_used_mib=… min_free_disk_mib=… samples=…` from a memory and
|
||||
disk sample every 2 seconds, even when the suite fails. The public
|
||||
`TREE_MUTATING` map now classifies exclusive host-state fixtures; its sole
|
||||
entry is `test/bootstrap-retention.test.ts`, whose same-UID nondumpable actors
|
||||
affect host-wide procfs permission checks. Locally, this file runs only after
|
||||
@@ -1504,7 +1512,9 @@ refresh to a maintainer.
|
||||
- `free-durations-record.yml` (Mondays 04:00 UTC, before test-health, and on
|
||||
dispatch) records the Linux seed on `ubicloud-standard-8` with
|
||||
`scripts/ubicloud/setup-free-suite.sh` and the Windows seed through
|
||||
`windows-free-tests.yml`'s `record_durations` path. When either recording adds
|
||||
`windows-free-tests.yml`'s `record_durations` path. The recorder stays on
|
||||
`ubicloud-standard-8` while the free-suite shards run on `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-2`); both are Ubicloud standard runners. When either recording adds
|
||||
or drops a test file, it force-updates the `bot/free-duration-seeds` branch,
|
||||
opens or refreshes one seed PR whose body states the ratchet result, and
|
||||
dispatches Free Tests, Windows Free Tests and Quality gate on it. Those
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.369",
|
||||
"version": "1.91.370",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -29,9 +29,10 @@ test('the recorder runs weekly before test-health and on dispatch, read-only exc
|
||||
expect(recorder.jobs.propose.permissions).toEqual({ contents: 'write', 'pull-requests': 'write', actions: 'write' });
|
||||
});
|
||||
|
||||
test('Linux records on the free-suite runner, turns red on any red file, and uploads only a complete recording', () => {
|
||||
test('Linux records on the free-suite runner image, turns red on any red file, and uploads only a complete recording', () => {
|
||||
const linux = recorder.jobs.linux;
|
||||
expect(linux['runs-on']).toBe(freeTests.jobs['free-suite']['runs-on']);
|
||||
expect(linux['runs-on']).toMatch(/^ubicloud-standard-\d+$/);
|
||||
expect(freeTests.env.FREE_SUITE_RUNNER).toMatch(/^ubicloud-standard-\d+$/);
|
||||
const runs = linux.steps.map((step: any) => step.run).filter(Boolean).join('\n');
|
||||
expect(runs).toContain('bash scripts/ubicloud/setup-free-suite.sh');
|
||||
const record = linux.steps.find((step: any) => step.name === 'Time every free file alone');
|
||||
|
||||
@@ -166,6 +166,66 @@ describe('free-tests workflow wiring', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(!Bun.which('bash'))('free-plan resolves the one runner and shard constant for every event, and validates dispatch overrides', () => {
|
||||
const workflow = Bun.YAML.parse(source) as any;
|
||||
const planner = workflow.jobs['free-plan'];
|
||||
const plan = planner.steps.find((step: any) => step.id === 'plan');
|
||||
expect(workflow.env.FREE_SUITE_RUNNER).toMatch(/^ubicloud-standard-(2|4|8)$/);
|
||||
expect(workflow.env.FREE_SUITE_SHARDS).toMatch(/^[1-9][0-9]?$/);
|
||||
expect(planner.outputs.runner).toBe('${{ steps.plan.outputs.runner }}');
|
||||
expect(workflow.jobs['free-suite']['runs-on']).toBe('${{ needs.free-plan.outputs.runner }}');
|
||||
expect(plan.env).toEqual({ RUNNER_OVERRIDE: '${{ inputs.free_runner }}', SHARDS_OVERRIDE: '${{ inputs.free_shards }}' });
|
||||
expect(plan.run).not.toContain('${{');
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'free plan '));
|
||||
const bin = path.join(directory, 'bin');
|
||||
fs.mkdirSync(bin);
|
||||
fs.writeFileSync(path.join(bin, 'bun'), '#!/bin/sh\nprintf "%s\\n" "$*" > "$RUNNER_TEMP/bun-args"\necho \'{"shard":[1]}\'\n', { mode: 0o755 });
|
||||
const toShell = (p: string) => p.split(path.sep).join('/');
|
||||
const resolve = (overrides: Record<string, string>) => {
|
||||
const output = path.join(directory, 'output');
|
||||
fs.writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-c', plan.run], {
|
||||
env: { ...process.env, ...workflow.env, RUNNER_OVERRIDE: '', SHARDS_OVERRIDE: '', ...overrides,
|
||||
PATH: `${toShell(bin)}:${process.env.PATH}`, RUNNER_TEMP: toShell(directory),
|
||||
GITHUB_OUTPUT: toShell(output), GITHUB_STEP_SUMMARY: toShell(path.join(directory, 'summary')) },
|
||||
encoding: 'utf8', timeout: 5000,
|
||||
});
|
||||
const outputs = Object.fromEntries(fs.readFileSync(output, 'utf8').trim().split('\n').filter(Boolean).map(line => line.split(/=(.*)/s).slice(0, 2)));
|
||||
const args = fs.existsSync(path.join(directory, 'bun-args')) ? fs.readFileSync(path.join(directory, 'bun-args'), 'utf8') : '';
|
||||
fs.rmSync(path.join(directory, 'bun-args'), { force: true });
|
||||
return { status: result.status, outputs, args };
|
||||
};
|
||||
try {
|
||||
const unset = resolve({});
|
||||
expect(unset.status).toBe(0);
|
||||
expect(unset.outputs.runner).toBe(workflow.env.FREE_SUITE_RUNNER);
|
||||
expect(unset.args).toContain(`--shards ${workflow.env.FREE_SUITE_SHARDS}`);
|
||||
const override = resolve({ RUNNER_OVERRIDE: 'ubicloud-standard-4', SHARDS_OVERRIDE: '16' });
|
||||
expect(override.status).toBe(0);
|
||||
expect(override.outputs.runner).toBe('ubicloud-standard-4');
|
||||
expect(override.args).toContain('--shards 16');
|
||||
const refusals: Record<string, string>[] = [{ RUNNER_OVERRIDE: 'ubuntu-24.04' }, { RUNNER_OVERRIDE: 'ubicloud-standard-8 x' }, { SHARDS_OVERRIDE: '0' }, { SHARDS_OVERRIDE: '41' }, { SHARDS_OVERRIDE: '16; true' }];
|
||||
for (const bad of refusals) {
|
||||
const refused = resolve(bad);
|
||||
expect(refused.status, JSON.stringify(bad)).not.toBe(0);
|
||||
expect(refused.outputs.runner, JSON.stringify(bad)).toBeUndefined();
|
||||
expect(refused.args, JSON.stringify(bad)).toBe('');
|
||||
}
|
||||
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('every shard reports its peak memory and minimum free disk, even when the suite fails', () => {
|
||||
const steps = (Bun.YAML.parse(source) as any).jobs['free-suite'].steps;
|
||||
const start = steps.findIndex((step: any) => step.name === 'Start the memory and disk sampler');
|
||||
const suite = steps.findIndex((step: any) => step.name === 'Run free suite');
|
||||
const report = steps.findIndex((step: any) => step.name === 'Report peak memory and minimum free disk');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
expect(start).toBeLessThan(suite);
|
||||
expect(report).toBeGreaterThan(suite);
|
||||
expect(steps[report].if).toBe('always()');
|
||||
expect(steps[report].run).toContain('RESOURCES: peak_used_mib=');
|
||||
});
|
||||
|
||||
test('least-privilege token: contents read-only, credentials not persisted', () => {
|
||||
// The job executes PR-controlled code (install lifecycle scripts + the
|
||||
// suite itself). A default-grant GITHUB_TOKEN persisted into .git/config
|
||||
|
||||
Reference in new issue
Block a user