Skip to content

M01 / setup-zig / Initial implementation#1

Merged
guysenpai merged 16 commits into
mainfrom
feat/M01-initial-implementation
May 10, 2026
Merged

M01 / setup-zig / Initial implementation#1
guysenpai merged 16 commits into
mainfrom
feat/M01-initial-implementation

Conversation

@guysenpai
Copy link
Copy Markdown
Contributor

Closes milestone M01 of weldengine/setup-zig. Brief: briefs/M01-initial-implementation.md.

Summary

From-scratch TypeScript reimplementation inspired by mlugg/setup-zig, with parallel mirror race via Promise.any + shared AbortController, ncc-bundled dist/, node24 runtime, and Weld-specific ergonomics (version-file, source, enforce-version-range inputs, zig-version output).

The minisign verifier is a near-verbatim port from mlugg with explicit attribution in src/minisign.ts and the double-copyright LICENSE.

Closing notes (from the brief)

  • What worked: Pure modules (version, cache, minisign, gc) layered cleanly on top of mlugg's logic; I/O modules (resolve, download) layered on top with vi.spyOn(globalThis, 'fetch') for testing. Parallel race via Promise.any + shared AbortController fell out naturally and is testable without ever hitting the network. The downloadTarball / downloadTarballWithKey split keeps the orchestration tied to the hardcoded Zig public key while still allowing tests to inject a generated test key.
  • What deviated from the original spec: Nothing in the FROZEN section. Three minor scaffolding paths added outside the explicit "Files to create" list, captured in Acted deviations: .nvmrc, .prettierignore, and the inherently-mandated briefs/ directory. One soft semantic decision documented in code: enforce-version-range is implemented as token-prefix match on the version's major.minor[.patch] (stripped of -dev suffix).
  • What needs explicit review attention:
    • src/minisign.ts:78 — preserves a known mlugg upstream quirk (stale offset in trailing-bytes guard). Carried verbatim per "minimal changes" mandate; flagged for visibility, out of scope to fix in M01.
    • The CI matrix uses Zig versions master, latest, 0.14.1, 0.15.1, 0.16.0. If 0.16.0 is not yet a real Zig release at run time, those rows will fail until either Zig ships 0.16.0 or the matrix is updated.
    • The cache-size-limit parser intentionally rejects bare floats ('1.5'); units are mandatory unless the value is a plain integer.
    • tests/fixtures/build.zig.zon declares minimum_zig_version = "0.16.0", used by the CI version-file job.
  • Final measurements:
    • 90 unit tests across 6 test files (brief required ≥ 36).
    • Coverage 97.86 % statements / 92.72 % branches / 100 % functions / 97.86 % lines (per-module floor: 95.45 % stmt, 87.5 % branches — both above the 90/85 thresholds).
    • dist/index.js 4 351 301 bytes; dist/post.js 4 433 650 bytes; dist/licenses.txt 60 139 bytes.
    • Total LOC under src/: 877.
  • Residual risks / intentional technical debt: the minisign-trailing-bytes upstream-mlugg quirk; dist/ reproducibility tied to a specific Node 24 patch version (mitigated by .nvmrc); hardcoded fallback mirror list will drift over time and need refreshing in future versions.

Validation checklist (Step 4 of the protocol)

  • All deliverables of "Scope" are present
  • No drift to "Out-of-scope"
  • All tests pass (npm test — 90 / 90)
  • Coverage ≥ 90 % statements and ≥ 85 % branches on every module under src/ (excluding entry points main.ts / post.ts per vitest exclude list — they are integration-tested via the CI matrix)
  • npm run build produces dist/index.js and dist/post.js with zero warnings
  • npm run lint clean (zero errors, zero warnings)
  • npm run format:check clean
  • Committed dist/ matches a freshly-rebuilt dist/ byte-for-byte (verified locally; enforced by .github/workflows/lint.yml)
  • CI workflow YAML files written and parse-valid (verified by gh accepting them in this push)
  • briefs/M01-initial-implementation.md Closing notes filled, Status: CLOSED, Closing date: 2026-05-10
  • Final commit docs(brief): close M01 present

Test plan after merge

  • Tag v0.1.0 on the squashed merge commit.
  • Set up a test consumer repository that uses weldengine/setup-zig@v0.1.0 to install Zig 0.16.0 and run zig build on a trivial build.zig.
  • After successful test consumer validation, post the mobile tag v1 on the same commit as v0.1.0.

guysenpai added 16 commits May 9, 2026 11:08
feat(gc): add zig-cache size limit parsing and purge logic

Both modules are pure (cache) or filesystem-only (gc), with no @actions/* dependencies. Includes tests for size parser unit suffixes (binary and decimal), filesystem fixtures, and disabled-state semantics.
Verbatim port to TypeScript with explicit attribution header. Uses node:crypto.subtle for Ed25519 verification and createHash('BLAKE2b512') for the hashed-mode prehash. Tests build round-trip vectors with node:crypto generateKeyPairSync to exercise valid, corrupted, and tampered scenarios without needing a real Zig signature fixture.
…ange enforcement

feat(download): add parallel mirror race with AbortController and minisign verification

resolve.ts handles input precedence (explicit > version-file > default build.zig.zon > latest), ZON regex extraction (mach_zig_version, minimum_zig_version), and prefix-based enforce-version-range.

download.ts implements parallel mirror race using Promise.any over fetch() with a shared AbortController, falls back to the 13-mirror hardcoded list when ziglang.org/download/community-mirrors.txt is unreachable, and last-resorts to the canonical ziglang.org URL when every mirror has failed. Tarball signatures and trusted comments are validated with the ported minisign module.
main.ts threads the full pipeline: resolve version, compute tarball name from os.arch/platform/endianness, restore tarball cache, download with mirror race on miss, save to cache, extract via @actions/tool-cache, exec 'zig version' to set the zig-version output, export ZIG_GLOBAL_CACHE_DIR/ZIG_LOCAL_CACHE_DIR, and restore the .zig-cache prefix when use-cache is true.

post.ts reads use-cache state, optionally GCs the .zig-cache via parseSizeLimit + maybeGc, and saves the cache under runId-runAttempt.

Also adds allowImportingTsExtensions to tsconfig (required by NodeNext module resolution for cross-module .ts imports), uses webcrypto.CryptoKey type explicitly in minisign.ts so 'lib: dom' is not needed, and simplifies the readdir typing in gc.ts.
ncc cannot use 'allowImportingTsExtensions' because it must emit. Switching imports from './foo.ts' to './foo.js' is the standard NodeNext convention (TypeScript resolves .js to the corresponding .ts source, ncc bundles into single CommonJS files).

dist/index.js and dist/post.js are committed (per supply-chain auditability decision in the brief). dist/* is marked linguist-generated in .gitattributes and ignored by ESLint/Prettier so reviewers see human-authored code only.
.github/workflows/test.yml: matrix 4 OS x 5 Zig versions (20 jobs) plus dedicated jobs for cache-hit, version-file, enforce-version-range (success and failure), custom mirror, and custom source query string.

.github/workflows/lint.yml: typecheck, eslint, prettier --check, plus a reproducibility check that verifies the committed dist/ matches a fresh 'npm run build'.

.forgejo/workflows/test.yml: minimal Codeberg runner verification on codeberg-tiny-lazy (no container.image override per the gotcha noted in the brief).
Adjusted eslint.config.js to disable typed-aware rules (require-await, no-base-to-string, restrict-template-expressions) for tests/ where mocked fetch and template-string assertions trip them, and reformatted source/tests with prettier --write. Switched a redundant ternary in src/version.ts to nullish coalescing.

Rebuilt dist/index.js and dist/post.js after the source touch-up. Same logical bundle, only minor diff from prettier's reflow.
Adds three minisign tests to push coverage from 89.87% to 97.46% on this module: a forged trusted-comment header (anchored on the preceding newline so we don't accidentally hit inside 'untrusted comment:'), an unsupported algorithm prefix, and the raw 'Ed' (unhashed) verification path that the action does not exercise but the verifier supports for spec completeness.
The workflow triggers via 'workflow_run' on the Lint workflow completing on main, reads .version from package.json, skips if v$version already exists as a tag, and otherwise creates an annotated tag and a GitHub Release with auto-generated notes (commits since the previous tag).

Post-close addendum to milestone M01: extends the brief's CI section beyond the originally-listed three workflows. Decision recorded as a Cas 3 verbal in 'Acted deviations' of briefs/M01-initial-implementation.md.
…ged entries)

The real index.json at https://ziglang.org/download/index.json only carries a string '.version' field on the 'master' entry (and a couple of very recent stable releases). Tagged entries like 0.15.1 / 0.14.1 have only 'date', 'docs', 'src', '<arch>-<os>' — and the key IS the version. The previous strict isVersionMap rejected this entirely with 'Malformed index.json' on every CI 'master' / 'latest' job.

Relax the type guard to require only object-of-objects; pull the string check up to the specific call sites (getMasterVersion, getMachVersion) where '.version' really must exist. getLatestVersion still uses the key as the version, which matches mlugg.

ci: also pin .nvmrc to the exact 24.15.0 patch and switch lint.yml to node-version-file: .nvmrc, so the dist/ reproducibility check no longer drifts between local 24.15.0 and CI's auto-resolved latest 24.x.
… on dist/*

Two combined fixes for the dist/ reproducibility check that was failing on CI:

1. dist/index.js and dist/post.js are now built in node:24.15.0-slim (Linux), matching CI's Ubuntu runners. ncc bakes platform-specific bits into the bundle, so a macOS-built dist will not match a Linux-built one byte-for-byte. The Docker build is deterministic across runs (verified by two consecutive builds producing identical output).

2. .gitattributes now sets 'dist/* -text' to opt the bundles out of git's CRLF/LF normalization. Without this, mixed line endings in transitive npm sources (some Windows-authored deps inside the bundle) get rewritten on commit but not on rebuild, causing spurious diffs.

Also adds .claude/ to .gitignore so Claude Code's local scheduled-task state is not tracked.
@guysenpai guysenpai merged commit 54499ec into main May 10, 2026
28 checks passed
@guysenpai guysenpai deleted the feat/M01-initial-implementation branch May 10, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant