Skip to content

feat: pkg 6.21 build hooks — default bump, trust-boundary docs, and the fixes review surfaced - #22

Merged
robertsLando merged 9 commits into
mainfrom
feat/pkg-621-hooks
Jul 27, 2026
Merged

feat: pkg 6.21 build hooks — default bump, trust-boundary docs, and the fixes review surfaced#22
robertsLando merged 9 commits into
mainfrom
feat/pkg-621-hooks

Conversation

@robertsLando

@robertsLando robertsLando commented Jul 27, 2026

Copy link
Copy Markdown
Member

What

pkg 6.21.0 shipped build hooks (yao-pkg/pkg#273). They need no new action surface — config keys, no CLI flags, so the existing --config pass-through already carries them. The default pin ~6.19.0 was what made them unreachable.

A deep review of the first draft turned up two blockers and a set of real defects, all fixed here. Four commits, separable by concern.

250ebb9 — standalone --config was broken

pkg refuses --config <file> together with a package.json input (Specify either 'package.json' or config. Not both), and the action's default positional . resolves to exactly that. Every run using config-inline, or config pointing at a standalone pkg config, failed outright unless the caller also set entry.

Reproduced against 6.21.0 and 6.19.0 alike — this predates the bump; the first draft of this PR merely documented it as working. Fixed by resolving package.json bin and passing it as the entry, mirroring pkg's own precedence.

0b132e5 — shell injection + a drifted filename list

npm i -g @yao-pkg/pkg@${{ inputs.pkg-version }} pasted the input into the run block; a crafted specifier executed shell in a job that can hold signing certs. Now passed via env:, quoted, and validated.

The pkg config filename list had drifted into four copies with two different contents. The cache-key hashFiles glob still matched pkg.config.{js,ts,json} — so a pkg.config.mjs, the file the new hooks docs tell you to write, never invalidated the pkg-fetch cache. Collapsed to one exported constant.

Also drops two ?? '<literal>' fallbacks that were unreachable duplicates of INPUT_SPECS defaults.

5e76562 — diagnosability

~6.21.0 floats patches at CI runtime, so the specifier can't identify the build that ran. Now reports the resolved pkg --version. A non-zero pkg exit names the config file, because a failing hook is otherwise indistinguishable from a pkg-internal failure; the output-map miss suggests a postBuild rename.

013d576 — the trust boundary, and the docs

The default bump silently turned pkg configs from data into executable code. pkg picks a config up from the checked-out tree, so a workflow building an untrusted ref now grants that ref command execution in a job that may hold signing credentials. No such capability existed at 6.19. Documented in docs/inputs.md, README and STATUS.yaml, with the release-note obligation and the pin-vs-floor rationale recorded.

Hook-reference corrections: postBuild runs after pkg's own macOS ad-hoc signing (not the action's, and not at all on Windows); the output-map fallback is exact → case-insensitive → <os>-<arch> substring; the transform example scopes to project sources, uses a replacer function so $& isn't interpreted, and no longer bakes "undefined" outside Actions.

New e2e job asserts all three hooks fire through the --config pass-through. Generator tests cover the cache glob, the injection guard, and the trust warning.

Verification

  • yarn typecheck, yarn lint clean
  • yarn test — 245/245 pass (was 231; +14 covering bin resolution, default consistency, and the generated surfaces)
  • yarn gen verified idempotent, so the codegen drift gate passes
  • The --config failure and its fix reproduced directly against both pkg 6.19.0 and 6.21.0

Note

Also fixes a pre-existing YAML syntax error in STATUS.yaml that kept the whole file from parsing — unrelated, but the file could not be validated while it was there.

🤖 Generated with Claude Code

pkg 6.21.0 added preBuild / postBuild / transform hooks. They are
config-only (no CLI flags), so the action's --config pass-through already
carries them — but the default pkg-version pin of ~6.19.0 resolved to
6.19.x, which does not have them.

- default pkg-version ~6.19.0 -> ~6.21.0
- docs/inputs.md gains a Build hooks section covering the three keys,
  the PKG_OUTPUT contract, the config-inline JSON limitation, and a
  warning against renaming the binary from postBuild (pkg-output-map
  locates outputs by predicted name + basename-prefix scan)
- fix the `config` input description: pkg auto-detects .pkgrc,
  .pkgrc.json and pkg.config.{js,cjs,mjs} — never .ts, never
  pkg.config.json
Copilot AI review requested due to automatic review settings July 27, 2026 08:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the action’s default @yao-pkg/pkg version pin to surface pkg 6.21.0 build hooks by default, and regenerates action/docs artifacts to reflect the new defaults and corrected config auto-detection documentation.

Changes:

  • Bump default pkg-version from ~6.19.0 to ~6.21.0 across core parsing, generated action manifests, dist bundles, and unit tests.
  • Document pkg 6.21.0 build hooks in generated docs/inputs.md via a new static section in scripts/gen-action-yml.ts.
  • Correct config / config-inline input descriptions to match pkg’s actual config filename auto-detection and hook limitations.

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
STATUS.yaml Updates minimum/default pkg-version notes and adds a build-hooks status note.
scripts/gen-action-yml.ts Adds a generated “Build hooks” documentation block into docs/inputs.md.
packages/windows-metadata/dist/index.mjs Regenerated dist bundle reflecting updated input descriptions and defaults.
packages/core/test/unit/pkg-runner.test.ts Updates test baseline default pkgVersion to ~6.21.0.
packages/core/test/unit/inputs.test.ts Updates default-value assertion for pkgVersion.
packages/core/src/inputs.ts Updates input descriptions and default/fallback for pkg-version.
packages/build/dist/index.mjs Regenerated dist bundle reflecting updated input descriptions and defaults.
packages/build/action.yml Regenerated action manifest with updated descriptions and default pkg-version.
docs/inputs.md Regenerated inputs docs: updated config descriptions and new Build hooks section.
action.yml Regenerated composite action manifest with updated descriptions and default pkg-version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/gen-action-yml.ts
pkg refuses `--config <file>` together with a package.json input
("Specify either 'package.json' or config. Not both"), and the action's
default positional `.` resolves to exactly that. So every run using
`config-inline`, or `config` pointing at a standalone pkg config, failed
outright unless the caller also set `entry`.

Reproduced against 6.21.0 and 6.19.0 alike — this predates the version
bump. Resolve package.json `bin` and pass it as the entry, mirroring
pkg's own precedence (string bin; object keyed by unscoped package name,
else first value). Actionable error when there is no bin.

tokensForTarget narrows to Pick<ProjectInfo, 'name'|'version'> — it never
needed the new field.
`npm i -g @yao-pkg/pkg@${{ inputs.pkg-version }}` pasted the input
straight into the run block, so a crafted specifier executed shell in a
job that can hold signing certs and notarization credentials. Pass it
through `env:`, reference it quoted, and reject anything outside npm
specifier characters before installing.

Also collapses the pkg config filename list to one exported constant.
It had drifted into four independent copies with two different contents:
the cache-key `hashFiles` glob still matched `pkg.config.{js,ts,json}`,
so a `pkg.config.mjs` — the file the hooks docs tell you to write — never
invalidated the pkg-fetch cache, while `.ts` was hashed despite pkg never
auto-detecting it.

Drops the `?? '<literal>'` fallbacks in parseInputs: readInput already
resolves the INPUT_SPECS default, so those were unreachable copies of a
value that has to stay in sync by hand.
`~6.21.0` floats patch releases at CI runtime, so the specifier can't
identify the build that ran — exactly what you need when a patch
regresses. Query `pkg --version` and surface it in the log and the step
summary alongside the specifier. Failing to label a summary never fails
a build.

Two error messages gain the context the new hooks make necessary: a
non-zero pkg exit now names the config file, because a failing
preBuild/postBuild/transform hook is indistinguishable from a
pkg-internal failure in the log; and the output-map miss suggests a
postBuild rename, which is the newly-likely cause.
The default bump to 6.21 silently turned pkg configs from data into
executable code: pkg picks a config up from the checked-out tree, so a
workflow building an untrusted ref now grants that ref command execution
in a job that may hold signing credentials. No such capability existed at
6.19. Documented in docs/inputs.md, README and STATUS.yaml, with the
release-note obligation and the pin-vs-floor rationale recorded.

Corrections to the hooks reference:
- postBuild runs after pkg's own macOS ad-hoc signing, not the action's
  signing stage, and not at all on Windows
- the output-map fallback is exact -> case-insensitive -> <os>-<arch>
  substring, not a basename-prefix scan
- config-inline needs an entry script; say so
- the transform example scopes to project sources (a bare .endsWith('.js')
  also rewrites node_modules), uses a replacer function so `$&` is not
  interpreted, and no longer bakes "undefined" outside Actions
- stop telling readers to raise pkg-version to a value that is the default
- setSecret does not mask what a hook prints

Adds an e2e job asserting all three hooks fire through the --config
pass-through, and generator tests covering the cache glob, the install
step's injection guard, and the trust warning.

Fixes an unrelated pre-existing YAML syntax error in STATUS.yaml that
kept the whole file from parsing.
@robertsLando robertsLando changed the title feat: default pkg-version to ~6.21.0 and document build hooks feat: pkg 6.21 build hooks — default bump, trust-boundary docs, and the fixes review surfaced Jul 27, 2026
Actions expands ${{ }} anywhere inside a run block, shell comments
included, so the comment explaining why pkg-version is passed via env
was itself parsed as an empty expression: "Line 152: An expression was
expected". action.yml failed to load, so every job that invokes the
action failed — including ones this branch never touched.

Rewords the comment and adds a generator test that scans every run
block for workflow expressions. Verified it fails on the exact bug.
…output

`[[ ! "$X" =~ ^[...><...]+$ ]]` is a bash syntax error — [[ ]] parses a
bare > as an operator before the regex engine ever sees it, so the
install step died with "unexpected token `>`" on every job.

The pattern now lives in `specifier_re` and is referenced unquoted, the
standard idiom. Verified against real specifiers (~6.21.0, ^6.21.0,
latest, ">=6.21.0 <7.0.0", 6.x, *, "6.20.0 || 6.21.0") and injection
attempts ("6.21.0; curl …|sh", "$(id)", backticks, "&&whoami").

Root cause of both CI failures is the same gap: shell embedded in a YAML
string is invisible to eslint and to the unit tests, so it could only
fail in CI. Added a test that extracts every `run: |` block from the
generated action.yml and runs `bash -n` over it. Confirmed it fails on
the exact broken conditional.
The entry fix read package.json from GITHUB_WORKSPACE, but a standalone
config usually sits beside the project it configures — not at the repo
root. The hooks e2e caught it: bin was looked up in pkg-action's own root
package.json, which has none.

An explicit `config` now names the project — its parent directory is the
root whether it is a package.json or a standalone config next to one.
A config kept away from its project (configs/pkg.config.mjs) has no
package.json beside it, so that still falls back to the workspace, as
does an unset config.

No back-compat risk: standalone configs never worked at all before
250ebb9, so nothing was relying on the old resolution.

Verified the full flow against real pkg 6.21 locally — all three hooks
fire, the transform rewrites only packed bytes, and the on-disk source
keeps its placeholder.
pkg derives the output base name as --output > package.json name >
config `name` > entry basename. Supplying an entry for a standalone
config moved it off package.json name, so the prediction ("hooks-app")
missed what pkg wrote ("index"), and a single target has no os/arch
suffix for the triple fallback to match on.

The action cannot know the config's `name` without executing the user's
pkg.config.mjs, so prediction here is inherently best-effort — hence the
existing fallback chain. Extends it with the one case that is
unambiguous: a sole target and a sole file in an output directory the
action created empty for this run.

Still refuses to guess when several files are present, or for
multi-target builds where the os/arch fallback already applies. Verified
against the real pkg output dir from the failing CI scenario.
@robertsLando
robertsLando merged commit 806999a into main Jul 27, 2026
20 checks passed
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.

2 participants