Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .ai/skills/hack-repo-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,23 @@ Read [CI](../../../.github/workflows/ci.yml) for current hosted gates. A local p
does not mean CI ran.

Hosted CI runs the `bun run test` file set on eight `test shard i/8` jobs;
the required `test` check is their fan-in. It accepts success and cancelled-only
results; failure or skipped shard results still fail. Prerelease gates separately
require every shard to succeed. The aggregate cannot distinguish cancellation
causes from the matrix result alone; it accepts every `cancelled` result.
light PRs run only the five Linux shards and report selected macOS paths as
deferred to the integration run. `ci:mac`/`ci:e2e` opt into selected tiers;
`ci:full` opts into all ordinary PR tiers. Platform and Docker integration
evidence comes from next pushes. Lead readiness requires green Linux jobs,
clean persistent review and handled bot findings; list pending Mac/Docker jobs.
The required `test` check is their fan-in. Every planned shard must succeed;
failure, cancellation or skipped shards fail. Drafts deliberately fail with
`skipped: draft`. Next pushes can select tests only after a successful predecessor;
superseded pushes cancel. Main and explicit full dispatches retain release-host
coverage. Prerelease plan/publish require exact-head `release-full` evidence from
the selected run's latest attempt, including every shard and compiler host.
[`scripts/test-shard-plan.ts`](../../../scripts/test-shard-plan.ts) splits the files
by the durations in `scripts/test-shard-durations.json`. A file with no recorded
duration is planned at 5 s. Each shard runs each of its files in its own
`bun test ./<file>` process and prints a start line and a result line per file.
It stops a file after 12 min, and stops the shard at 30 min or on cancellation.
It stops a file after 12 min (temporarily 20 min for storage activation command,
run first in its shard), and stops the shard at 30 min or on cancellation.
When it stops early it names the running file, the files not started and the
slowest files. Each shard first checks that the eight shards cover the files
`git ls-files` lists exactly once. To reproduce one shard locally, run
Expand Down
49 changes: 49 additions & 0 deletions .github/actions/ci-dependency-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI dependency cache
description: Cache downloaded packages and native build dependencies without skipping verification
inputs:
mode:
description: bun or rust
default: bun
kind:
description: Separate compiler and candidate target trees
default: compiler
target:
description: Cargo target directory to retain
default: .hack-local/config-compiler-target
runs:
using: composite
steps:
- name: Cache Bun downloads
if: inputs.mode == 'bun'
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ci-bun-v1-${{ runner.os }}-${{ runner.arch }}-1.4.4-${{ hashFiles('bun.lock') }}
restore-keys: ci-bun-v1-${{ runner.os }}-${{ runner.arch }}-1.4.4-
- name: Identify native cache toolchain
if: inputs.mode == 'rust'
id: image
shell: bash
run: |
printf 'image=%s\n' "${ImageOS:-unknown}" >> "$GITHUB_OUTPUT"
printf 'CARGO_INCREMENTAL=0\nZIG_GLOBAL_CACHE_DIR=%s/ci-zig-cache\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
- name: Restore Cargo and Zig build data on pull requests
if: inputs.mode == 'rust' && github.event_name == 'pull_request'
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git/db
${{ inputs.target }}
${{ runner.temp }}/ci-zig-cache
key: ci-native-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.image.outputs.image }}-${{ inputs.kind }}-rust1.97.1-zig0.15.2-${{ hashFiles('mise.toml', 'packages/**/Cargo.lock') }}
- name: Cache Cargo and Zig build data on trusted events
if: inputs.mode == 'rust' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/next' || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch')
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git/db
${{ inputs.target }}
${{ runner.temp }}/ci-zig-cache
key: ci-native-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.image.outputs.image }}-${{ inputs.kind }}-rust1.97.1-zig0.15.2-${{ hashFiles('mise.toml', 'packages/**/Cargo.lock') }}
128 changes: 128 additions & 0 deletions .github/actions/docker-e2e-shard/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Docker E2E shard
description: Build and run one complete deterministic Docker E2E shard with allowlisted timings.
inputs:
shard:
description: One-based shard index.
required: true
count:
description: Total number of shards.
required: true
extra-shard:
description: Shard that runs the supplemental checks once per layout.
required: true
runs:
using: composite
steps:
- name: Start shard elapsed timer
shell: bash
run: echo "E2E_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.4.4"
- name: Install tmux
shell: bash
run: sudo apt-get update && sudo apt-get install --yes tmux
- name: Install dependencies
shell: bash
run: bun install --frozen-lockfile
- name: Create isolated Hack network
shell: bash
run: docker network create --subnet 172.30.0.0/16 hack-dev
# The Linux test pool may lack tmux and skip this file.
# Require one real pass here, where tmux is explicitly installed.
- name: Verify private tmux input on Linux
if: inputs.shard == inputs.extra-shard
shell: bash
run: |
command -v tmux
SHELL=/bin/bash bun test tests/tmux-private-input.test.ts 2>&1 | tee "$RUNNER_TEMP/tmux-private-input.log"
grep -Eq '^ *1 pass$' "$RUNNER_TEMP/tmux-private-input.log"
- name: Prepare offline cache fixture images
shell: bash
run: |
docker pull alpine:3.20
docker pull alpine:3.22
docker pull node:24.22.1-bookworm-slim
# Also the cataloged native file and storage helper image
# (src/lib/native-compose-storage-witness-docker-artifact.ts).
docker pull oven/bun:1.4.4-slim
docker pull postgres:17.6-alpine
docker pull lucaslorentz/caddy-docker-proxy:2.10.0-alpine
- uses: dtolnay/rust-toolchain@1.97.1
# Each shard builds its own binaries (about 20 s), so the hashes it records match what it runs.
- name: Build current-branch CLI and native config compiler
shell: bash
run: |
bun run build
bun run build:config-compiler
# The one-off compiler checks run once, on the lightest shard, off the longest shard's path.
- name: Verify literal argv through real compiler and Compose normalization
if: inputs.shard == inputs.extra-shard
shell: bash
run: HACK_TEST_COMPOSE_CONFIG=1 bun test tests/native-compose-renderer-config.test.ts --test-name-pattern '^legacy dollar-pair argv matches real compiler plan and Compose normalization$'
- name: Verify Compose string argv through real compiler and Compose normalization
if: inputs.shard == inputs.extra-shard
shell: bash
run: HACK_TEST_COMPOSE_CONFIG=1 bun test tests/native-compose-renderer-config.test.ts --test-name-pattern '^(Compose string words match real compiler and renderer exec argv for selected profiles|raw unpaired dollars cannot become accepted pairs after Compose word splitting)$'
- name: Verify owned bridge policy and aliases through real compiler and Compose normalization
if: inputs.shard == inputs.extra-shard
shell: bash
run: HACK_TEST_COMPOSE_CONFIG=1 bun test tests/native-compose-renderer-config.test.ts --test-name-pattern '^one authored bridge keeps internal policy and static aliases through compiler and Compose normalization$'
# Real Docker CLI against two fake Unix engines (isolated HOME and config; no real engine).
- name: Verify Docker context endpoint selection against the real Docker CLI
if: inputs.shard == inputs.extra-shard
shell: bash
run: HACK_TEST_DOCKER_CLI=1 bun test tests/docker-context.test.ts
- name: Verify recipe and config examples with docker compose config
if: inputs.shard == inputs.extra-shard
shell: bash
run: |
HACK_TEST_COMPOSE_CONFIG=1 bun test tests/recipe-examples.test.ts --test-name-pattern '^docker compose config accepts every example Compose file$' 2>&1 | tee "$RUNNER_TEMP/recipe-examples-compose.log"
grep -Eq '^ *1 pass$' "$RUNNER_TEMP/recipe-examples-compose.log"
- name: Run local and Docker E2E shard
id: e2e
shell: bash
env:
HACK_E2E_PROCESS_POLICY_REFUSAL_CAPSULE: "1"
HACK_E2E_CAPSULE_DIR: ${{ runner.temp }}/process-policy-capsules
HACK_E2E_CLI_BIN: ${{ github.workspace }}/dist/hack
HACK_E2E_SHARD: ${{ inputs.shard }}/${{ inputs.count }}
run: |
HACK_E2E_DOCKER_HOST="${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}')}"
HACK_E2E_NETWORK_SOURCE_REVISION="$(git rev-parse HEAD)"
HACK_E2E_NETWORK_CLI_SHA256="$(sha256sum dist/hack | cut -d ' ' -f1)"
HACK_E2E_NETWORK_COMPILER_SHA256="$(sha256sum dist/hack-config-compiler | cut -d ' ' -f1)"
HACK_E2E_COMPOSE_PLUGIN_PATH="$(docker info --format '{{json .ClientInfo.Plugins}}' | bun -e 'import { realpath } from "node:fs/promises"; const value = await Bun.stdin.json(); const plugin = Array.isArray(value) ? value.find((entry) => entry && Object.hasOwn(entry, "Name") && entry.Name === "compose") : null; if (!(plugin && Object.hasOwn(plugin, "Path") && typeof plugin.Path === "string" && plugin.Path.startsWith("/"))) throw new Error("Installed Compose plugin path is unavailable"); process.stdout.write(await realpath(plugin.Path));')"
HACK_E2E_COMPOSE_PLUGIN_SHA256="$(sha256sum "$HACK_E2E_COMPOSE_PLUGIN_PATH" | cut -d ' ' -f1)"
export HACK_E2E_DOCKER_HOST HACK_E2E_NETWORK_SOURCE_REVISION HACK_E2E_NETWORK_CLI_SHA256 HACK_E2E_NETWORK_COMPILER_SHA256 HACK_E2E_COMPOSE_PLUGIN_PATH HACK_E2E_COMPOSE_PLUGIN_SHA256
HACK_E2E_REQUIRE_DOCKER=1 HACK_E2E_REQUIRE_TMUX=1 bun run test:e2e:local:docker --shard="$HACK_E2E_SHARD" 2>&1 | tee "$RUNNER_TEMP/docker-e2e.log"
- name: Preserve process-policy capsule and startup diagnostic
# Closed fixed-field JSON only (counts, enums, booleans); the gh log viewer truncates it inline.
if: failure()
uses: actions/upload-artifact@v4
with:
name: process-policy-capsules-${{ inputs.count }}-shards-${{ inputs.shard }}
path: ${{ runner.temp }}/process-policy-capsules
if-no-files-found: ignore
retention-days: 14
- name: Smoke container resource metadata without optional fields
id: smoke
if: inputs.shard == inputs.extra-shard
shell: bash
run: |
probe_id=$(docker create --network none --read-only alpine:3.20 true)
trap 'docker rm -f "$probe_id" >/dev/null' EXIT
bun scripts/inspect-container-resources.ts --container "$probe_id" > "$RUNNER_TEMP/resource-probe.json"
bun -e 'const r = await Bun.stdin.json(); if (r.probeStatus !== "not_running" || r.container.healthcheckIntervalNs !== null || r.container.writableLayerBytes !== null) throw new Error("Invalid optional resource metadata");' < "$RUNNER_TEMP/resource-probe.json"
bun scripts/inspect-container-resources.ts --container "$probe_id" --storage > "$RUNNER_TEMP/resource-probe.json"
bun -e 'const r = await Bun.stdin.json(); if (typeof r.container.writableLayerBytes !== "number") throw new Error("Missing writable-layer accounting");' < "$RUNNER_TEMP/resource-probe.json"

- name: Summarize shard elapsed time and scenario results
if: always()
continue-on-error: true
env:
HACK_E2E_SHARD: ${{ inputs.shard }}/${{ inputs.count }}
E2E_SUCCEEDED: ${{ steps.e2e.outcome == 'success' && (inputs.shard != inputs.extra-shard || steps.smoke.outcome == 'success') }}
shell: bash
run: bun scripts/docker-e2e-summary.ts
Loading
Loading