From 4ffb12e489f4cd5c764d3fb2c7ec669364a73938 Mon Sep 17 00:00:00 2001 From: Tara X V01D <1711810+taraxvoid@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:16:55 -0500 Subject: [PATCH 1/5] docs: drop stray conflict marker, fix lefthook remote ref comment Co-Authored-By: Claude Sonnet 5.5 --- README.md | 1 - lefthook/site.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f63a60..3d9226e 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,6 @@ remote's `ref` tag, and tool versions in a `justfile` (annotate the line above with `# renovate: datasource=... depName=...`). Requires the Renovate GitHub App on the repo. -||||||| 4041a59 ## Git hooks (lefthook) [`lefthook/site.yml`](lefthook/site.yml) is the shared hook set for the site diff --git a/lefthook/site.yml b/lefthook/site.yml index b1a9c94..728ed62 100644 --- a/lefthook/site.yml +++ b/lefthook/site.yml @@ -5,7 +5,7 @@ # # lefthook.yml in the site # remotes: # - git_url: https://github.com/taraxvoid/voidflow -# ref: # pin; bump deliberately +# ref: # a tag or branch (not a SHA); bump deliberately # configs: # - lefthook/site.yml # From cc14d77befdb4532e8ba448d37b0f4087ca99e1d Mon Sep 17 00:00:00 2001 From: "Tara \\X/ Void" <1711810+taraxvoid@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:22:51 -0500 Subject: [PATCH 2/5] fix(resolve-env): inline the env mapping so the reusable workflow works cross-repo (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolve-env.yml` does `actions/checkout` (which checks out the **caller's** repo) and then `uses: ./actions/branch-env-map`, a path that only exists in voidflow's own repo. Every cross-repo caller therefore fails at that step with the action not found. Found rolling resolve-env into taraxvoid.net (`resolve-env / Resolve env: Run ./actions/branch-env-map` failed on the PR run). Fix: inline the mapping in the workflow (no checkout, no local action). A reusable workflow has no way to reference its own repo's composite action at a matching ref, so the script is duplicated; `test/branch-env-map.test.ts` asserts the two scripts are byte-identical and runs the mapping for every ref/event case in both modes (default `main/next/live` to `dev/staging/prod`, and `single-environment`), against both copies. Also asserts the workflow has no `uses` steps, so this cannot regress. Validating on a real runner via taraxvoid.net#20 pointed at this branch; result to follow here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 5.5 --- .github/workflows/resolve-env.yml | 37 ++++++++++++---- test/branch-env-map.test.ts | 71 +++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 test/branch-env-map.test.ts diff --git a/.github/workflows/resolve-env.yml b/.github/workflows/resolve-env.yml index 7d20fc3..49af8e6 100644 --- a/.github/workflows/resolve-env.yml +++ b/.github/workflows/resolve-env.yml @@ -1,8 +1,9 @@ name: Resolve env # Resolves the deploy environment (dev/staging/prod) for the current branch -# via the branch-env-map action, exposing it as a job output so callers can -# reference it in a downstream job's `name:` (e.g. `Deploy [${{ needs.resolve-env.outputs.env }}]`). +# using the same mapping as the branch-env-map action, exposing it as a job +# output so callers can reference it in a downstream job's `name:` (e.g. +# `Deploy [${{ needs.resolve-env.outputs.env }}]`). # Job names may use the `needs` context, but not composite action outputs # directly, so this indirection is what makes that possible. @@ -34,10 +35,30 @@ jobs: outputs: env: ${{ steps.map.outputs.env }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false + # Same mapping as actions/branch-env-map (a test keeps them identical). + # Inlined because a reusable workflow can't reach `./actions/...` in its + # own repo: checkout gives it the caller's tree, not voidflow's. - id: map - uses: ./actions/branch-env-map - with: - single-environment: ${{ inputs.single-environment }} + shell: bash + env: + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + SINGLE: ${{ inputs.single-environment }} + run: | + if [[ "$SINGLE" == "true" ]]; then + if [[ "$EVENT_NAME" == pull_request* ]]; then + echo "env=preview" >> "$GITHUB_OUTPUT" + elif [[ "$REF_NAME" == main ]]; then + echo "env=prod" >> "$GITHUB_OUTPUT" + else + echo "::error::Refusing to deploy unknown ref: $REF_NAME (expected main or a pull request)" >&2 + exit 1 + fi + exit 0 + fi + case "$REF_NAME" in + main) echo "env=dev" >> "$GITHUB_OUTPUT" ;; + next) echo "env=staging" >> "$GITHUB_OUTPUT" ;; + live) echo "env=prod" >> "$GITHUB_OUTPUT" ;; + *) echo "::error::Refusing to deploy unknown branch: $REF_NAME (expected main, next, or live)" >&2; exit 1 ;; + esac diff --git a/test/branch-env-map.test.ts b/test/branch-env-map.test.ts new file mode 100644 index 0000000..6f0fa7a --- /dev/null +++ b/test/branch-env-map.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from 'bun:test' +import { mkdtempSync, readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), 'utf8') + +const actionScript: string = Bun.YAML.parse(read('actions/branch-env-map/action.yml')).runs.steps.find( + (s: any) => s.id === 'map', +).run +const workflowScript: string = Bun.YAML.parse(read('.github/workflows/resolve-env.yml')).jobs[ + 'resolve-env' +].steps.find((s: any) => s.id === 'map').run + +async function map(script: string, ref: string, event: string, single: boolean) { + const out = join(mkdtempSync(join(tmpdir(), 'voidflow-env-')), 'out') + await Bun.write(out, '') + const proc = Bun.spawn(['bash', '-c', script], { + env: { + PATH: process.env.PATH, + REF_NAME: ref, + EVENT_NAME: event, + SINGLE: String(single), + GITHUB_OUTPUT: out, + }, + stdout: 'pipe', + stderr: 'pipe', + }) + const code = await proc.exited + return { code, env: readFileSync(out, 'utf8').trim() } +} + +describe('resolve-env.yml carries the same mapping as branch-env-map', () => { + test('scripts are identical (no drift)', () => { + expect(workflowScript).toBe(actionScript) + }) + + test('resolve-env.yml has no `uses` steps (a local ./actions path or checkout cannot work cross-repo)', () => { + const steps = Bun.YAML.parse(read('.github/workflows/resolve-env.yml')).jobs['resolve-env'].steps + expect(steps.filter((s: any) => s.uses)).toEqual([]) + }) +}) + +describe.each([ + ['action', actionScript], + ['workflow', workflowScript], +])('%s script', (_name, script) => { + test.each([ + ['main', 'push', 'env=dev'], + ['next', 'push', 'env=staging'], + ['live', 'push', 'env=prod'], + ])('default mode: %s on %s', async (ref, event, expected) => { + expect(await map(script, ref, event, false)).toEqual({ code: 0, env: expected }) + }) + + test('default mode rejects unknown branches', async () => { + expect((await map(script, 'feature/x', 'push', false)).code).toBe(1) + }) + + test.each([ + ['main', 'push', 'env=prod'], + ['123/merge', 'pull_request', 'env=preview'], + ['feature/x', 'pull_request', 'env=preview'], + ])('single-environment: %s on %s', async (ref, event, expected) => { + expect(await map(script, ref, event, true)).toEqual({ code: 0, env: expected }) + }) + + test('single-environment rejects non-main pushes', async () => { + expect((await map(script, 'feature/x', 'push', true)).code).toBe(1) + }) +}) From 8bcb8a18656717e98718ea6a47f8a1bdcd92ef49 Mon Sep 17 00:00:00 2001 From: "Tara \\X/ Void" <1711810+taraxvoid@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:18:40 -0500 Subject: [PATCH 3/5] docs: tell sites to run voidflow bins from the devDependency, not bunx (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README told sites to wire `check:licenses` and `test:e2e:lighthouse` through `bunx --package @taraxvoid/voidflow@ …`. `bunx` reads the **global** `~/.bunfig.toml`, so a global `minimumReleaseAge` that doesn't exclude voidflow blocks it locally (pre-commit hook included) for 3 days after each release, and the pinned version lives outside the lockfile and Renovate. Sites already have voidflow as a devDependency, so the bins are in `node_modules/.bin`. queeromaha and soundry already call them directly; synthomaha is switching in taraxvoid/synthomaha#100. This updates the README to match and drops the stale 0.8.1 pin. Not verified: why synthomaha CI run 37051879514 failed with `No version matching "0.11.1"` — I couldn't reproduce it now that 0.11.1 is past the age gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 5.5 --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d9226e..fa8ca3d 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,11 @@ static directory on a free port and runs [Unlighthouse](https://unlighthouse.dev/) against it, reusing Playwright's Chromium. It exits non-zero if a category budget in the site's config fails. -In the site's `package.json` (pin the tag): +Add `@taraxvoid/voidflow` as a devDependency and call the bin from the site's +`package.json`: ```json -"test:e2e:lighthouse": "bun run build && bunx --package @taraxvoid/voidflow@0.8.1 unlighthouse-runner --dir dist" +"test:e2e:lighthouse": "bun run build && unlighthouse-runner --dir dist" ``` Options: `--dir` (default `dist`, use `dist/client` for Cloudflare adapter @@ -126,12 +127,20 @@ change. Sites implement that script with the shared check, which runs `license-checker` in the site's repo root and fails on GPL, AGPL, SSPL and similar copyleft licenses (LGPL is allowed). -In the site's `package.json` (pin the tag): +With `@taraxvoid/voidflow` as a devDependency, call the bin from the site's +`package.json`: ```json -"check:licenses": "bunx --package @taraxvoid/voidflow@ check-licenses" +"check:licenses": "check-licenses" ``` +Don't use `bunx --package @taraxvoid/voidflow@ ...` for these. It +duplicates the version pin outside the lockfile and Renovate, and `bunx` reads +the global `~/.bunfig.toml` rather than the site's, so a global +`minimumReleaseAge` that doesn't exclude `@taraxvoid/voidflow` blocks it +locally (including in the pre-commit hook) for 3 days after every release. +The devDependency's bins are installed under the site's own `bunfig.toml`. + ### Playwright preview config Shared `playwright.config` for Astro sites that run e2e against From fc33ffd2c3314ed8e4a3be6a2d006789b832d1fd Mon Sep 17 00:00:00 2001 From: "Tara \\X/ Void" <1711810+taraxvoid@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:20:07 -0500 Subject: [PATCH 4/5] feat(lefthook): pick bun or pnpm from the lockfile in the shared site hooks (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared site hooks hardcoded `bun run --if-present`, which blocks the pnpm sites (rvnflt, voidsite) from adopting them. A top-level lefthook `templates:` entry `pm` resolves to `pnpm` when the repo has a `pnpm-lock.yaml`, else `bun`, and every command uses `{pm} run --if-present`. The install hints use `{pm} install` too. Also fixes the header comment (`ref` is a tag or branch, not a SHA). Tested as a `file://` remote in throwaway repos: the bun repo runs `bun run` for each step, the pnpm repo (with a stub `pnpm` on PATH) runs `pnpm run --if-present` for each step. `lefthook validate` passes and the unit test now asserts the lockfile detection and that no `bun run` remains. Note: `bun run --if-present audit` falls through to macOS's `/usr/sbin/audit` when a site defines no `audit` script. Pre-existing and harmless for sites that define one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5.5 --- README.md | 3 ++- lefthook/site.yml | 37 ++++++++++++++++++++++++------------- test/lefthook-site.test.ts | 16 ++++++++++++++-- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fa8ca3d..5dccc88 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,8 @@ on the repo. [`lefthook/site.yml`](lefthook/site.yml) is the shared hook set for the site stack, consumed as a lefthook remote. It calls the site's own `package.json` -scripts (`bun run --if-present`, so a missing script is skipped): +scripts (` run --if-present`, so a missing script is skipped; `` is pnpm +when the repo has a `pnpm-lock.yaml`, bun otherwise): - `pre-commit`: dependency guard, `format` (formatted files are re-staged), `lint:actions`, `check`, `test:unit`, and `check:licenses` / `audit` when the diff --git a/lefthook/site.yml b/lefthook/site.yml index 728ed62..47dc8e6 100644 --- a/lefthook/site.yml +++ b/lefthook/site.yml @@ -1,6 +1,7 @@ -# Shared git hooks for the Astro/bun site stack. Sites pull this in as a -# lefthook remote and call their own package.json scripts, so a script a site -# does not define is skipped (`bun run --if-present`). +# Shared git hooks for the Astro site stack (bun, or pnpm when the repo has a +# pnpm-lock.yaml). Sites pull this in as a lefthook remote and call their own +# package.json scripts, so a script a site does not define is skipped +# (` run --if-present`). # # # lefthook.yml in the site # remotes: @@ -14,6 +15,10 @@ # one defined here. Differences go through package.json scripts instead, e.g. # `test:unit:precommit` replaces `test:unit` for the pre-commit unit step. +templates: + # Package manager: pnpm when the repo is a pnpm project, bun otherwise. + pm: "$(if [ -f pnpm-lock.yaml ]; then echo pnpm; else echo bun; fi)" + pre-commit: skip: [merge, rebase] commands: @@ -22,39 +27,45 @@ pre-commit: run: | if [ ! -d node_modules ]; then echo "pre-commit: 'node_modules' is missing, dependencies aren't installed." - echo " Run 'bun install' before committing, then commit again." + echo " Run '{pm} install' before committing, then commit again." exit 1 fi format: priority: 2 - run: bun run --if-present format + run: "{pm} run --if-present format" stage_fixed: true lint-actions: priority: 3 - run: bun run --if-present lint:actions + run: "{pm} run --if-present lint:actions" typecheck: priority: 3 - run: bun run --if-present check + run: "{pm} run --if-present check" unit: priority: 3 # A site that can't run its whole suite pre-commit (e.g. tests that need # a built dist/) defines `test:unit:precommit`; otherwise test:unit runs. run: | if grep -q '"test:unit:precommit"' package.json; then - bun run test:unit:precommit + {pm} run test:unit:precommit else - bun run --if-present test:unit + {pm} run --if-present test:unit fi licenses: priority: 3 glob: "{package.json,bun.lock,pnpm-lock.yaml}" - run: bun run --if-present check:licenses + run: "{pm} run --if-present check:licenses" audit: # Local only: hits a remote vulnerability database and can hang offline, # so it runs when dependencies changed and fails closed. priority: 3 glob: "{package.json,bun.lock,pnpm-lock.yaml}" - run: bun run --if-present audit + # Checked by hand: under bun, `--if-present` falls through to + # the system `audit` binary (macOS /usr/sbin/audit) when the site has no + # audit script, and that exits non-zero. + run: | + if grep -q '"audit"' package.json; then + {pm} run audit + fi commit-msg: commands: @@ -109,8 +120,8 @@ pre-push: if [ ! -d node_modules ]; then echo "pre-push: 'node_modules' is missing, dependencies aren't installed." - echo " Run 'bun install' before pushing, then push again." + echo " Run '{pm} install' before pushing, then push again." exit 1 fi - bun run --if-present test:push + {pm} run --if-present test:push diff --git a/test/lefthook-site.test.ts b/test/lefthook-site.test.ts index 574c31d..f44c796 100644 --- a/test/lefthook-site.test.ts +++ b/test/lefthook-site.test.ts @@ -28,7 +28,19 @@ describe('lefthook/site.yml', () => { test('pre-commit unit step prefers test:unit:precommit when a site defines it', async () => { const text = await Bun.file(config).text() expect(text).toContain('grep -q \'"test:unit:precommit"\' package.json') - expect(text).toContain('bun run test:unit:precommit') - expect(text).toContain('bun run --if-present test:unit') + expect(text).toContain('{pm} run test:unit:precommit') + expect(text).toContain('{pm} run --if-present test:unit') + }) + + test('picks the package manager from the lockfile instead of hardcoding bun', async () => { + const text = await Bun.file(config).text() + expect(text).toContain('pnpm-lock.yaml ]; then echo pnpm; else echo bun') + expect(text).not.toMatch(/\bbun run\b/) + }) + + test('audit step only runs when the site defines an audit script', async () => { + const text = await Bun.file(config).text() + expect(text).toContain('grep -q \'"audit"\' package.json') + expect(text).not.toContain('--if-present audit') }) }) From e7b76d040cba8d5c28925b8de81532de6537583d Mon Sep 17 00:00:00 2001 From: "ravenflight-releases[bot]" <330165707+ravenflight-releases[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:20:33 +0000 Subject: [PATCH 5/5] chore(main): release 0.12.0 --- .release-please/main-manifest.json | 2 +- CHANGELOG.md | 12 ++++++++++++ jsr.json | 2 +- package.json | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.release-please/main-manifest.json b/.release-please/main-manifest.json index ddfa3e3..8032c17 100644 --- a/.release-please/main-manifest.json +++ b/.release-please/main-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.1" + ".": "0.12.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b724b..f48bbfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project are documented here, generated by [release-please](https://github.com/googleapis/release-please) from [Conventional Commits](https://www.conventionalcommits.org). +## [0.12.0](https://github.com/taraxvoid/voidflow/compare/v0.11.1...v0.12.0) (2026-10-06) + + +### Features + +* **lefthook:** pick bun or pnpm from the lockfile in the shared site hooks ([#119](https://github.com/taraxvoid/voidflow/issues/119)) ([fc33ffd](https://github.com/taraxvoid/voidflow/commit/fc33ffd2c3314ed8e4a3be6a2d006789b832d1fd)) + + +### Bug Fixes + +* **resolve-env:** inline the env mapping so the reusable workflow works cross-repo ([#118](https://github.com/taraxvoid/voidflow/issues/118)) ([cc14d77](https://github.com/taraxvoid/voidflow/commit/cc14d77befdb4532e8ba448d37b0f4087ca99e1d)) + ## [0.11.1](https://github.com/taraxvoid/voidflow/compare/v0.11.0...v0.11.1) (2026-10-02) diff --git a/jsr.json b/jsr.json index 085437a..336260e 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@taraxvoid/voidflow", - "version": "0.11.1", + "version": "0.12.0", "license": "MIT", "exports": { "./playwright": "./playwright/index.mjs" diff --git a/package.json b/package.json index be4a64d..d9c4048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@taraxvoid/voidflow", - "version": "0.11.1", + "version": "0.12.0", "description": "Shared workflows, actions and tooling for @taraxvoid sites", "license": "MIT", "keywords": [