From 807bb312fd4c74e9100ba3b60f119d847ee48851 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:09:33 +0200 Subject: [PATCH 01/10] refactor!: drop pkg-CLI mirror inputs, own only action-layer surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pkg-specific knobs (mode, node-version, compress-node, fallback-to-source, public, public-packages, options, no-bytecode, no-dict, debug, extra-args) are removed from the action input surface. Users express them in their pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json pkg field) instead. The action keeps only action-layer concerns that do not exist in pkg config: config path, entry, targets, pkg-version/pkg-path, plus the archive, signing, windows-metadata, and performance groups. Motivation: mirroring pkg's CLI forced the action to grow a new input each time pkg added a flag, and to maintain back-compat on every pkg release. By letting pkg own its own schema we decouple the action from pkg's evolution. Docs updated: README gets a new "pkg configuration" section with a migration example; architecture.md §3/§6 note the scoped surface; STATUS.yaml records the dropped-inputs list and migration path. BREAKING CHANGE: the listed inputs are no longer accepted. Setting them now triggers the unknown-input warning and the value is ignored. Migrate into your pkg config file. --- README.md | 34 ++++++++ STATUS.yaml | 21 +++++ action.yml | 42 +--------- docs/architecture.md | 15 +++- docs/inputs.md | 11 --- packages/build/action.yml | 29 ------- packages/build/dist/index.mjs | 76 +----------------- packages/core/src/inputs.ts | 93 +--------------------- packages/core/src/pkg-runner.ts | 41 ++-------- packages/core/src/targets.ts | 4 +- packages/core/test/unit/inputs.test.ts | 42 +++++----- packages/core/test/unit/pkg-runner.test.ts | 77 ++++-------------- packages/matrix/dist/index.mjs | 2 +- packages/windows-metadata/dist/index.mjs | 57 ------------- scripts/gen-action-yml.ts | 2 +- 15 files changed, 123 insertions(+), 423 deletions(-) diff --git a/README.md b/README.md index 19bbbfb..cd75097 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,40 @@ Tracking issue: [yao-pkg/pkg#248](https://github.com/yao-pkg/pkg/issues/248). path: "${{ join(fromJson(steps.build.outputs.artifacts), '\n') }}" ``` +## pkg configuration + +The action does **not** mirror pkg's CLI flags as inputs. Pkg-specific knobs +— SEA mode, bundled Node compression, `public` / `publicPackages`, V8 +`options`, `noBytecode`, `noDict`, `debug`, bytecode-fabricator fallback — +live in your pkg config file (`.pkgrc.json`, `pkg.config.{js,ts,json}`, or +the `pkg` field of `package.json`). See +[yao-pkg/pkg's README](https://github.com/yao-pkg/pkg#config) for the full +schema. + +Example — SEA mode with Brotli-compressed Node + fallback: + +```jsonc +// .pkgrc.json +{ + "bin": "src/main.js", + "mode": "sea", + "compressNode": "Brotli", + "fallbackToSource": true, +} +``` + +```yaml +- uses: yao-pkg/pkg-action@v1 + with: + config: .pkgrc.json + targets: node22-linux-x64 +``` + +The action's inputs cover only concerns pkg config cannot express: matrix +targets, pkg install version/path, archive format, filename template, +checksum algorithms, Windows-metadata resedit patch, macOS/Windows signing, +cache, step summary. + ## Outputs | Output | Shape | diff --git a/STATUS.yaml b/STATUS.yaml index 196e934..9037326 100644 --- a/STATUS.yaml +++ b/STATUS.yaml @@ -48,6 +48,27 @@ recent-hardening: - '`yamlString` codegen helper throws on embedded control chars — prevents silent action.yml corruption (S2).' - 'CI coverage gate at 85% via scripts/check-coverage.ts parsing lcov (S6).' +# ─── Input-surface slim (2026-04-23) ───────────────────────────────────── +# Stop mirroring pkg's CLI. Pkg-specific knobs live in the user's pkg config; +# the action owns only concerns pkg config cannot express (CI-matrix targets, +# pkg install version, archive, checksum, windows-metadata, signing, cache). + +input-surface-slim: + dropped-inputs: + - mode + - node-version + - compress-node + - fallback-to-source + - public + - public-packages + - options + - no-bytecode + - no-dict + - debug + - extra-args + migration: 'Move each value into .pkgrc / pkg.config.{js,ts,json} or the `pkg` field of package.json. `buildPkgArgs` forwards the config path to pkg.' + breaking: 'yes — users setting any dropped input will see the unknown-input warning and the value will be ignored. Release note required.' + # ─── Removed (2026-04-23) ──────────────────────────────────────────────── # Distribution scope reset. Use dedicated downstream actions instead. diff --git a/action.yml b/action.yml index 8b061c4..ce08bd6 100644 --- a/action.yml +++ b/action.yml @@ -15,35 +15,6 @@ inputs: description: 'Entry script when not specified in the config.' targets: description: 'Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target.' - mode: - description: 'standard | sea — selects pkg Standard or SEA mode.' - default: 'standard' - node-version: - description: 'pkg''s bundled Node.js major (e.g. 22, 24). Does not affect the action''s own Node runtime.' - default: '22' - compress-node: - description: 'pkg''s bundled-binary compression: Brotli | GZip | None.' - default: 'None' - fallback-to-source: - description: 'Pass pkg --fallback-to-source for bytecode-fabricator failures.' - default: 'false' - public: - description: 'Pass pkg --public (ships sources as plaintext).' - default: 'false' - public-packages: - description: 'Comma-separated package names to mark public (pkg --public-packages).' - options: - description: 'Comma-separated V8 options baked into the binary (pkg --options).' - no-bytecode: - description: 'Pass pkg --no-bytecode.' - default: 'false' - no-dict: - description: 'Comma-separated list of packages for pkg --no-dict (or * for all).' - debug: - description: 'Pass pkg --debug.' - default: 'false' - extra-args: - description: 'Raw extra flags appended to the pkg CLI invocation.' pkg-version: description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.' default: '~6.16.0' @@ -169,7 +140,7 @@ runs: uses: actions/cache@v5 with: path: ${{ runner.temp }}/pkg-cache - key: ${{ inputs.cache-key || format('pkg-fetch-{0}-{1}-node{2}-{3}', runner.os, runner.arch, inputs.node-version, hashFiles('**/package.json', '.pkgrc*', '**/pkg.config.{js,ts,json}')) }} + key: ${{ inputs.cache-key || format('pkg-fetch-{0}-{1}-{2}', runner.os, runner.arch, hashFiles('**/package.json', '.pkgrc*', '**/pkg.config.{js,ts,json}')) }} - name: Install @yao-pkg/pkg if: ${{ inputs.pkg-path == '' }} @@ -184,17 +155,6 @@ runs: config: ${{ inputs.config }} entry: ${{ inputs.entry }} targets: ${{ inputs.targets }} - mode: ${{ inputs.mode }} - node-version: ${{ inputs.node-version }} - compress-node: ${{ inputs.compress-node }} - fallback-to-source: ${{ inputs.fallback-to-source }} - public: ${{ inputs.public }} - public-packages: ${{ inputs.public-packages }} - options: ${{ inputs.options }} - no-bytecode: ${{ inputs.no-bytecode }} - no-dict: ${{ inputs.no-dict }} - debug: ${{ inputs.debug }} - extra-args: ${{ inputs.extra-args }} pkg-version: ${{ inputs.pkg-version }} pkg-path: ${{ inputs.pkg-path }} strip: ${{ inputs.strip }} diff --git a/docs/architecture.md b/docs/architecture.md index 5b19bcb..a3159e9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,8 +69,8 @@ directly). | `targets.ts` | `Target` type, `parseTarget`, `hostTarget`, `formatTarget` | | `templates.ts` | `{name}/{version}/{os}/{arch}/…` filename renderer + token bag | | `checksum.ts` | sha256/sha512/md5 streaming, `writeShasumsFile`, `writeSidecar` | -| `inputs.ts` | `INPUT_SPECS` (source of truth for every input) + `parseInputs` | -| `pkg-runner.ts` | `@yao-pkg/pkg` CLI bridge + `buildPkgArgs` | +| `inputs.ts` | `INPUT_SPECS` (action-layer inputs only) + `parseInputs` | +| `pkg-runner.ts` | `@yao-pkg/pkg` CLI bridge + `buildPkgArgs` (no pkg-flag mirroring) | | `pkg-output-map.ts` | Reconciles pkg on-disk outputs to `Target[]` | | `archive.ts` | tar.gz / tar.xz / zip / 7z writers (yazl for zip) | | `summary.ts` | Markdown table for `GITHUB_STEP_SUMMARY` | @@ -156,6 +156,17 @@ Source of truth: `packages/core/src/inputs.ts::INPUT_SPECS`. One `InputSpec` record per input with `name / description / default? / required? / category / deprecated? / secret?`. +**Input-surface scope** (2026-04-23): the action intentionally does not +mirror pkg's CLI. Pkg-specific knobs (`mode`, `compress-node`, +`fallback-to-source`, `public`, `public-packages`, `options`, `no-bytecode`, +`no-dict`, `debug`, `extra-args`, `node-version`) are expressed via the user's +pkg config file, which `buildPkgArgs` forwards through `--config`. The action +owns only concerns that pkg config cannot express: CI-matrix `targets`, the +`pkg-version` / `pkg-path` install choice, archive format, filename template, +checksum algorithms, Windows-metadata resedit patch, signing, cache, and step +summary. Rationale: decouple the action from pkg's CLI evolution — each new +pkg flag otherwise forced a back-compat-preserving input bump here. + Emitted: - `/action.yml` — top-level composite. Every input forwarded explicitly to the diff --git a/docs/inputs.md b/docs/inputs.md index 205ca8f..f0556a6 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -11,17 +11,6 @@ Every `pkg-action` input, grouped by category. | `config` | — | no | no | Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. | | `entry` | — | no | no | Entry script when not specified in the config. | | `targets` | — | no | no | Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target. | -| `mode` | `standard` | no | no | standard \| sea — selects pkg Standard or SEA mode. | -| `node-version` | `22` | no | no | pkg's bundled Node.js major (e.g. 22, 24). Does not affect the action's own Node runtime. | -| `compress-node` | `None` | no | no | pkg's bundled-binary compression: Brotli \| GZip \| None. | -| `fallback-to-source` | `false` | no | no | Pass pkg --fallback-to-source for bytecode-fabricator failures. | -| `public` | `false` | no | no | Pass pkg --public (ships sources as plaintext). | -| `public-packages` | — | no | no | Comma-separated package names to mark public (pkg --public-packages). | -| `options` | — | no | no | Comma-separated V8 options baked into the binary (pkg --options). | -| `no-bytecode` | `false` | no | no | Pass pkg --no-bytecode. | -| `no-dict` | — | no | no | Comma-separated list of packages for pkg --no-dict (or * for all). | -| `debug` | `false` | no | no | Pass pkg --debug. | -| `extra-args` | — | no | no | Raw extra flags appended to the pkg CLI invocation. | | `pkg-version` | `~6.16.0` | no | no | npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set. | | `pkg-path` | — | no | no | Absolute path to a pre-installed pkg binary. Skips the implicit npm i -g. | diff --git a/packages/build/action.yml b/packages/build/action.yml index e7d57ad..210c5a4 100644 --- a/packages/build/action.yml +++ b/packages/build/action.yml @@ -12,35 +12,6 @@ inputs: description: 'Entry script when not specified in the config.' targets: description: 'Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target.' - mode: - description: 'standard | sea — selects pkg Standard or SEA mode.' - default: 'standard' - node-version: - description: 'pkg''s bundled Node.js major (e.g. 22, 24). Does not affect the action''s own Node runtime.' - default: '22' - compress-node: - description: 'pkg''s bundled-binary compression: Brotli | GZip | None.' - default: 'None' - fallback-to-source: - description: 'Pass pkg --fallback-to-source for bytecode-fabricator failures.' - default: 'false' - public: - description: 'Pass pkg --public (ships sources as plaintext).' - default: 'false' - public-packages: - description: 'Comma-separated package names to mark public (pkg --public-packages).' - options: - description: 'Comma-separated V8 options baked into the binary (pkg --options).' - no-bytecode: - description: 'Pass pkg --no-bytecode.' - default: 'false' - no-dict: - description: 'Comma-separated list of packages for pkg --no-dict (or * for all).' - debug: - description: 'Pass pkg --debug.' - default: 'false' - extra-args: - description: 'Raw extra flags appended to the pkg CLI invocation.' pkg-version: description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.' default: '~6.16.0' diff --git a/packages/build/dist/index.mjs b/packages/build/dist/index.mjs index 8e63559..5950a23 100644 --- a/packages/build/dist/index.mjs +++ b/packages/build/dist/index.mjs @@ -14776,63 +14776,6 @@ var INPUT_SPECS = [ category: "build", description: "Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target." }, - { - name: "mode", - category: "build", - description: "standard | sea \u2014 selects pkg Standard or SEA mode.", - default: "standard" - }, - { - name: "node-version", - category: "build", - description: "pkg's bundled Node.js major (e.g. 22, 24). Does not affect the action's own Node runtime.", - default: "22" - }, - { - name: "compress-node", - category: "build", - description: "pkg's bundled-binary compression: Brotli | GZip | None.", - default: "None" - }, - { - name: "fallback-to-source", - category: "build", - description: "Pass pkg --fallback-to-source for bytecode-fabricator failures.", - default: "false" - }, - { - name: "public", - category: "build", - description: "Pass pkg --public (ships sources as plaintext).", - default: "false" - }, - { - name: "public-packages", - category: "build", - description: "Comma-separated package names to mark public (pkg --public-packages)." - }, - { - name: "options", - category: "build", - description: "Comma-separated V8 options baked into the binary (pkg --options)." - }, - { - name: "no-bytecode", - category: "build", - description: "Pass pkg --no-bytecode.", - default: "false" - }, - { - name: "no-dict", - category: "build", - description: "Comma-separated list of packages for pkg --no-dict (or * for all)." - }, - { name: "debug", category: "build", description: "Pass pkg --debug.", default: "false" }, - { - name: "extra-args", - category: "build", - description: "Raw extra flags appended to the pkg CLI invocation." - }, { name: "pkg-version", category: "build", @@ -15124,21 +15067,6 @@ function parseInputs(opts = {}) { config: readInput(env, "config"), entry: readInput(env, "entry"), targets, - mode: parseEnum(readInput(env, "mode"), "mode", ["standard", "sea"]), - nodeVersion: readInput(env, "node-version") ?? "22", - compressNode: parseEnum(readInput(env, "compress-node"), "compress-node", [ - "Brotli", - "GZip", - "None" - ]), - fallbackToSource: parseBoolean(readInput(env, "fallback-to-source"), "fallback-to-source"), - public: parseBoolean(readInput(env, "public"), "public"), - publicPackages: parseList(readInput(env, "public-packages")), - options: parseList(readInput(env, "options")), - noBytecode: parseBoolean(readInput(env, "no-bytecode"), "no-bytecode"), - noDict: parseList(readInput(env, "no-dict")), - debug: parseBoolean(readInput(env, "debug"), "debug"), - extraArgs: readInput(env, "extra-args"), pkgVersion: readInput(env, "pkg-version") ?? "~6.16.0", pkgPath: readInput(env, "pkg-path") }, postBuild = { @@ -15193,9 +15121,7 @@ function levenshtein2(a, b) { // packages/core/src/pkg-runner.ts function buildPkgArgs(inv) { let args = []; - if (inv.targets.length > 0 && args.push("--targets", inv.targets.map(formatTarget).join(",")), inv.build.config !== void 0 && args.push("--config", inv.build.config), inv.build.mode === "sea" && args.push("--sea"), inv.build.compressNode !== "None" && args.push("--compress", inv.build.compressNode), inv.build.fallbackToSource && args.push("--fallback-to-source"), inv.build.public && args.push("--public"), inv.build.publicPackages.length > 0 && args.push("--public-packages", inv.build.publicPackages.join(",")), inv.build.options.length > 0 && args.push("--options", inv.build.options.join(",")), inv.build.noBytecode && args.push("--no-bytecode"), inv.build.noDict.length > 0 && args.push("--no-dict", inv.build.noDict.join(",")), inv.build.debug && args.push("--debug"), args.push("--out-path", inv.outputDir), inv.build.extraArgs !== void 0 && inv.build.extraArgs.trim() !== "") - for (let tok of inv.build.extraArgs.split(/\s+/).filter((s) => s.length > 0)) - args.push(tok); + inv.targets.length > 0 && args.push("--targets", inv.targets.map(formatTarget).join(",")), inv.build.config !== void 0 && args.push("--config", inv.build.config), args.push("--out-path", inv.outputDir); let entry = inv.build.entry ?? "."; return args.push(entry), args; } diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts index 0489509..b3a705e 100644 --- a/packages/core/src/inputs.ts +++ b/packages/core/src/inputs.ts @@ -49,64 +49,6 @@ export const INPUT_SPECS: readonly InputSpec[] = [ description: 'Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target.', }, - { - name: 'mode', - category: 'build', - description: 'standard | sea — selects pkg Standard or SEA mode.', - default: 'standard', - }, - { - name: 'node-version', - category: 'build', - description: - "pkg's bundled Node.js major (e.g. 22, 24). Does not affect the action's own Node runtime.", - default: '22', - }, - { - name: 'compress-node', - category: 'build', - description: "pkg's bundled-binary compression: Brotli | GZip | None.", - default: 'None', - }, - { - name: 'fallback-to-source', - category: 'build', - description: 'Pass pkg --fallback-to-source for bytecode-fabricator failures.', - default: 'false', - }, - { - name: 'public', - category: 'build', - description: 'Pass pkg --public (ships sources as plaintext).', - default: 'false', - }, - { - name: 'public-packages', - category: 'build', - description: 'Comma-separated package names to mark public (pkg --public-packages).', - }, - { - name: 'options', - category: 'build', - description: 'Comma-separated V8 options baked into the binary (pkg --options).', - }, - { - name: 'no-bytecode', - category: 'build', - description: 'Pass pkg --no-bytecode.', - default: 'false', - }, - { - name: 'no-dict', - category: 'build', - description: 'Comma-separated list of packages for pkg --no-dict (or * for all).', - }, - { name: 'debug', category: 'build', description: 'Pass pkg --debug.', default: 'false' }, - { - name: 'extra-args', - category: 'build', - description: 'Raw extra flags appended to the pkg CLI invocation.', - }, { name: 'pkg-version', category: 'build', @@ -354,32 +296,20 @@ export function specFor(name: string): InputSpec | undefined { // ─── Typed parsed inputs ────────────────────────────────────────────────── -export type CompressionMode = 'Brotli' | 'GZip' | 'None'; /** Archive format as seen by the input layer — extends archive.ts's set with 'none' (skip archiving). */ export type ArchiveFormatInput = 'tar.gz' | 'tar.xz' | 'zip' | '7z' | 'none'; -export type PkgMode = 'standard' | 'sea'; /** - * Parsed inputs — the M1-active subset. Later milestones extend this shape - * with Windows/signing/publishing fields. We keep separate interfaces per - * category so M3/M4 can add their own without perturbing the base. + * Parsed inputs. The build surface is intentionally thin: pkg-specific knobs + * (mode, compression, public, bytecode, v8 options, debug, raw extra-args, …) + * are expressed in the user's pkg config file, not mirrored as action inputs. + * This keeps the action decoupled from pkg's CLI evolution. */ export interface BuildInputs { readonly config: string | undefined; readonly entry: string | undefined; /** 'host' = use the host target; otherwise a non-empty list. */ readonly targets: Target[] | 'host'; - readonly mode: PkgMode; - readonly nodeVersion: string; - readonly compressNode: CompressionMode; - readonly fallbackToSource: boolean; - readonly public: boolean; - readonly publicPackages: string[]; - readonly options: string[]; - readonly noBytecode: boolean; - readonly noDict: string[]; - readonly debug: boolean; - readonly extraArgs: string | undefined; readonly pkgVersion: string; readonly pkgPath: string | undefined; } @@ -526,21 +456,6 @@ export function parseInputs(opts: ParseInputsOptions = {}): ActionInputs { config: readInput(env, 'config'), entry: readInput(env, 'entry'), targets, - mode: parseEnum(readInput(env, 'mode'), 'mode', ['standard', 'sea'] as const), - nodeVersion: readInput(env, 'node-version') ?? '22', - compressNode: parseEnum(readInput(env, 'compress-node'), 'compress-node', [ - 'Brotli', - 'GZip', - 'None', - ] as const), - fallbackToSource: parseBoolean(readInput(env, 'fallback-to-source'), 'fallback-to-source'), - public: parseBoolean(readInput(env, 'public'), 'public'), - publicPackages: parseList(readInput(env, 'public-packages')), - options: parseList(readInput(env, 'options')), - noBytecode: parseBoolean(readInput(env, 'no-bytecode'), 'no-bytecode'), - noDict: parseList(readInput(env, 'no-dict')), - debug: parseBoolean(readInput(env, 'debug'), 'debug'), - extraArgs: readInput(env, 'extra-args'), pkgVersion: readInput(env, 'pkg-version') ?? '~6.16.0', pkgPath: readInput(env, 'pkg-path'), }; diff --git a/packages/core/src/pkg-runner.ts b/packages/core/src/pkg-runner.ts index 4ff73ab..0306ad9 100644 --- a/packages/core/src/pkg-runner.ts +++ b/packages/core/src/pkg-runner.ts @@ -47,6 +47,11 @@ export interface PkgInvocation { /** * Build the argv passed to pkg from a `PkgInvocation`. Exposed for unit tests. + * + * The action deliberately owns only a thin action-layer surface: targets (CI + * matrix concern), config path, output directory, entry. Pkg-specific knobs + * (mode, compression, public, bytecode, v8 options, debug, …) belong in the + * user's pkg config file so the action doesn't have to track pkg's CLI. */ export function buildPkgArgs(inv: PkgInvocation): string[] { const args: string[] = []; @@ -57,45 +62,9 @@ export function buildPkgArgs(inv: PkgInvocation): string[] { if (inv.build.config !== undefined) { args.push('--config', inv.build.config); } - if (inv.build.mode === 'sea') { - args.push('--sea'); - } - if (inv.build.compressNode !== 'None') { - args.push('--compress', inv.build.compressNode); - } - if (inv.build.fallbackToSource) { - args.push('--fallback-to-source'); - } - if (inv.build.public) { - args.push('--public'); - } - if (inv.build.publicPackages.length > 0) { - args.push('--public-packages', inv.build.publicPackages.join(',')); - } - if (inv.build.options.length > 0) { - args.push('--options', inv.build.options.join(',')); - } - if (inv.build.noBytecode) { - args.push('--no-bytecode'); - } - if (inv.build.noDict.length > 0) { - args.push('--no-dict', inv.build.noDict.join(',')); - } - if (inv.build.debug) { - args.push('--debug'); - } args.push('--out-path', inv.outputDir); - // Extra raw flags (escape hatch). Split on whitespace for a naive but usable - // tokenization — users needing quoted args can set them via env. The pkg CLI - // itself doesn't accept quoted bundled tokens. - if (inv.build.extraArgs !== undefined && inv.build.extraArgs.trim() !== '') { - for (const tok of inv.build.extraArgs.split(/\s+/).filter((s) => s.length > 0)) { - args.push(tok); - } - } - // Positional entry / project root — must come LAST, after flags. const entry = inv.build.entry ?? '.'; args.push(entry); diff --git a/packages/core/src/targets.ts b/packages/core/src/targets.ts index ce2cfc1..67b1bef 100644 --- a/packages/core/src/targets.ts +++ b/packages/core/src/targets.ts @@ -213,11 +213,11 @@ export function crossCompileRisk(host: Target, target: Target): string | null { } // Non-Linux → Linux-arm64 without QEMU hits the fabrication bug on Node 22. if (target.os === 'linux' && target.arch === 'arm64' && host.arch !== 'arm64') { - return 'Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or pass --fallback-to-source.'; + return 'Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or enable fallback-to-source in your pkg config.'; } // Non-Windows → win-x64 on Node 22 also hits the fabricator bug. if (target.os === 'win' && target.arch === 'x64' && host.os !== 'win') { - return 'win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or pass --fallback-to-source.'; + return 'win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or enable fallback-to-source in your pkg config.'; } // macOS-arm64 requires signing. if (target.os === 'macos' && target.arch === 'arm64' && host.os !== 'macos') { diff --git a/packages/core/test/unit/inputs.test.ts b/packages/core/test/unit/inputs.test.ts index b565db3..711c04e 100644 --- a/packages/core/test/unit/inputs.test.ts +++ b/packages/core/test/unit/inputs.test.ts @@ -61,11 +61,8 @@ test('readInputRaw preserves dashes in env keys — matches @actions/core', () = test('parseInputs with no env uses defaults', () => { const inputs = parseInputs({ env: {} }); strictEqual(inputs.build.targets, 'host'); - strictEqual(inputs.build.mode, 'standard'); - strictEqual(inputs.build.nodeVersion, '22'); - strictEqual(inputs.build.compressNode, 'None'); - strictEqual(inputs.build.fallbackToSource, false); - strictEqual(inputs.build.public, false); + strictEqual(inputs.build.config, undefined); + strictEqual(inputs.build.entry, undefined); strictEqual(inputs.build.pkgVersion, '~6.16.0'); strictEqual(inputs.postBuild.compress, 'none'); @@ -81,28 +78,37 @@ test('parseInputs parses a realistic build config', () => { const inputs = parseInputs({ env: env( ['targets', 'node22-linux-x64,node22-macos-arm64'], - ['mode', 'sea'], - ['compress-node', 'Brotli'], + ['config', '.pkgrc.json'], + ['entry', 'src/main.js'], ['compress', 'tar.gz'], ['checksum', 'sha256,sha512'], ['strip', 'true'], - ['fallback-to-source', 'true'], - ['public', 'true'], - ['public-packages', 'express,lodash'], - ['no-dict', '*'], ), }); ok(Array.isArray(inputs.build.targets)); strictEqual((inputs.build.targets as never[]).length, 2); - strictEqual(inputs.build.mode, 'sea'); - strictEqual(inputs.build.compressNode, 'Brotli'); + strictEqual(inputs.build.config, '.pkgrc.json'); + strictEqual(inputs.build.entry, 'src/main.js'); strictEqual(inputs.postBuild.compress, 'tar.gz'); deepStrictEqual(inputs.postBuild.checksum, ['sha256', 'sha512']); strictEqual(inputs.postBuild.strip, true); - strictEqual(inputs.build.fallbackToSource, true); - strictEqual(inputs.build.public, true); - deepStrictEqual(inputs.build.publicPackages, ['express', 'lodash']); - deepStrictEqual(inputs.build.noDict, ['*']); +}); + +test('parseInputs flags removed pkg-layer inputs as unknown', () => { + const unknown: string[] = []; + parseInputs({ + env: env( + ['mode', 'sea'], + ['compress-node', 'Brotli'], + ['public', 'true'], + ['no-bytecode', 'true'], + ['extra-args', '--foo'], + ), + onUnknownInput: (n) => unknown.push(n), + }); + for (const n of ['mode', 'compress-node', 'public', 'no-bytecode', 'extra-args']) { + ok(unknown.includes(n), `expected "${n}" to be flagged as unknown`); + } }); test('parseInputs coerces multiple boolean spellings', () => { @@ -119,9 +125,7 @@ test('parseInputs rejects invalid boolean', () => { }); test('parseInputs rejects invalid enum value', () => { - throws(() => parseInputs({ env: env(['mode', 'fast']) }), ValidationError); throws(() => parseInputs({ env: env(['compress', 'rar']) }), ValidationError); - throws(() => parseInputs({ env: env(['compress-node', 'zstd']) }), ValidationError); }); test('parseInputs checksum accepts "none" and drops to empty list', () => { diff --git a/packages/core/test/unit/pkg-runner.test.ts b/packages/core/test/unit/pkg-runner.test.ts index 86a5907..61a0e00 100644 --- a/packages/core/test/unit/pkg-runner.test.ts +++ b/packages/core/test/unit/pkg-runner.test.ts @@ -9,21 +9,22 @@ const BASE_BUILD: BuildInputs = { config: undefined, entry: undefined, targets: 'host', - mode: 'standard', - nodeVersion: '22', - compressNode: 'None', - fallbackToSource: false, - public: false, - publicPackages: [], - options: [], - noBytecode: false, - noDict: [], - debug: false, - extraArgs: undefined, pkgVersion: '~6.16.0', pkgPath: undefined, }; +const PKG_FLAGS_OWNED_BY_CONFIG = [ + '--sea', + '--compress', + '--fallback-to-source', + '--public', + '--public-packages', + '--options', + '--no-bytecode', + '--no-dict', + '--debug', +]; + test('buildPkgArgs: minimal invocation', () => { const args = buildPkgArgs({ build: BASE_BUILD, @@ -33,43 +34,15 @@ test('buildPkgArgs: minimal invocation', () => { deepStrictEqual(args, ['--targets', 'node22-linux-x64', '--out-path', '/tmp/out', '.']); }); -test('buildPkgArgs: SEA mode + compression + fallback', () => { +test('buildPkgArgs: never emits pkg-layer flags (those belong in config)', () => { const args = buildPkgArgs({ - build: { ...BASE_BUILD, mode: 'sea', compressNode: 'Brotli', fallbackToSource: true }, - targets: [{ node: 22, os: 'linux', arch: 'x64' }], - outputDir: '/tmp/out', - }); - const joined = args.join(' '); - strictEqual(joined.includes('--sea'), true); - strictEqual(joined.includes('--compress Brotli'), true); - strictEqual(joined.includes('--fallback-to-source'), true); -}); - -test('buildPkgArgs: public + public-packages + options', () => { - const args = buildPkgArgs({ - build: { - ...BASE_BUILD, - public: true, - publicPackages: ['express', 'lodash'], - options: ['expose-gc', 'max-old-space-size=4096'], - }, - targets: [{ node: 22, os: 'linux', arch: 'x64' }], - outputDir: '/tmp/out', - }); - strictEqual(args.includes('--public'), true); - strictEqual(args.indexOf('--public-packages') + 1, args.indexOf('express,lodash')); - strictEqual(args.indexOf('--options') + 1, args.indexOf('expose-gc,max-old-space-size=4096')); -}); - -test('buildPkgArgs: no-bytecode + no-dict + debug', () => { - const args = buildPkgArgs({ - build: { ...BASE_BUILD, noBytecode: true, noDict: ['*'], debug: true }, + build: BASE_BUILD, targets: [{ node: 22, os: 'linux', arch: 'x64' }], outputDir: '/tmp/out', }); - strictEqual(args.includes('--no-bytecode'), true); - strictEqual(args.indexOf('--no-dict') + 1, args.indexOf('*')); - strictEqual(args.includes('--debug'), true); + for (const flag of PKG_FLAGS_OWNED_BY_CONFIG) { + strictEqual(args.includes(flag), false, `unexpected flag ${flag}`); + } }); test('buildPkgArgs: custom config + entry', () => { @@ -97,22 +70,6 @@ test('buildPkgArgs: multi-target list is comma-joined', () => { strictEqual(args[i + 1], 'node22-linux-x64,node22-macos-arm64,node22-win-x64'); }); -test('buildPkgArgs: extra-args tokens get appended before entry', () => { - const args = buildPkgArgs({ - build: { ...BASE_BUILD, extraArgs: '--foo bar --baz' }, - targets: [{ node: 22, os: 'linux', arch: 'x64' }], - outputDir: '/tmp/out', - }); - // Extra args appear after --out-path but before entry. - const outIdx = args.indexOf('--out-path'); - const fooIdx = args.indexOf('--foo'); - const bazIdx = args.indexOf('--baz'); - const entryIdx = args.indexOf('.'); - strictEqual(outIdx < fooIdx, true); - strictEqual(fooIdx < bazIdx, true); - strictEqual(bazIdx < entryIdx, true); -}); - test('runPkg passes through a successful exec', async () => { const calls: Array<[string, readonly string[]]> = []; const exec: ExecFn = async (command, args): Promise => { diff --git a/packages/matrix/dist/index.mjs b/packages/matrix/dist/index.mjs index 649fa07..cb2bc98 100644 --- a/packages/matrix/dist/index.mjs +++ b/packages/matrix/dist/index.mjs @@ -13434,7 +13434,7 @@ function expandMatrix(targets, overrides = {}) { } function crossCompileRisk(host, target) { let sameOs = host.os === target.os, sameArch = host.arch === target.arch; - return sameOs && sameArch ? null : host.os === "linux" && target.os === "macos" ? "Linux host \u2192 macOS target produces non-functional binaries (see yao-pkg/pkg#183). Use a macOS runner." : target.os === "linux" && target.arch === "arm64" && host.arch !== "arm64" ? "Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or pass --fallback-to-source." : target.os === "win" && target.arch === "x64" && host.os !== "win" ? "win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or pass --fallback-to-source." : target.os === "macos" && target.arch === "arm64" && host.os !== "macos" ? "macos-arm64 binaries must be signed to run; cross-compiling without a codesign toolchain will produce an unusable binary." : null; + return sameOs && sameArch ? null : host.os === "linux" && target.os === "macos" ? "Linux host \u2192 macOS target produces non-functional binaries (see yao-pkg/pkg#183). Use a macOS runner." : target.os === "linux" && target.arch === "arm64" && host.arch !== "arm64" ? "Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or enable fallback-to-source in your pkg config." : target.os === "win" && target.arch === "x64" && host.os !== "win" ? "win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or enable fallback-to-source in your pkg config." : target.os === "macos" && target.arch === "arm64" && host.os !== "macos" ? "macos-arm64 binaries must be signed to run; cross-compiling without a codesign toolchain will produce an unusable binary." : null; } // packages/core/src/version.ts diff --git a/packages/windows-metadata/dist/index.mjs b/packages/windows-metadata/dist/index.mjs index 0801c15..7cda528 100644 --- a/packages/windows-metadata/dist/index.mjs +++ b/packages/windows-metadata/dist/index.mjs @@ -13375,63 +13375,6 @@ var INPUT_SPECS = [ category: "build", description: "Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target." }, - { - name: "mode", - category: "build", - description: "standard | sea \u2014 selects pkg Standard or SEA mode.", - default: "standard" - }, - { - name: "node-version", - category: "build", - description: "pkg's bundled Node.js major (e.g. 22, 24). Does not affect the action's own Node runtime.", - default: "22" - }, - { - name: "compress-node", - category: "build", - description: "pkg's bundled-binary compression: Brotli | GZip | None.", - default: "None" - }, - { - name: "fallback-to-source", - category: "build", - description: "Pass pkg --fallback-to-source for bytecode-fabricator failures.", - default: "false" - }, - { - name: "public", - category: "build", - description: "Pass pkg --public (ships sources as plaintext).", - default: "false" - }, - { - name: "public-packages", - category: "build", - description: "Comma-separated package names to mark public (pkg --public-packages)." - }, - { - name: "options", - category: "build", - description: "Comma-separated V8 options baked into the binary (pkg --options)." - }, - { - name: "no-bytecode", - category: "build", - description: "Pass pkg --no-bytecode.", - default: "false" - }, - { - name: "no-dict", - category: "build", - description: "Comma-separated list of packages for pkg --no-dict (or * for all)." - }, - { name: "debug", category: "build", description: "Pass pkg --debug.", default: "false" }, - { - name: "extra-args", - category: "build", - description: "Raw extra flags appended to the pkg CLI invocation." - }, { name: "pkg-version", category: "build", diff --git a/scripts/gen-action-yml.ts b/scripts/gen-action-yml.ts index 354e853..a01e809 100644 --- a/scripts/gen-action-yml.ts +++ b/scripts/gen-action-yml.ts @@ -125,7 +125,7 @@ runs: uses: actions/cache@v5 with: path: \${{ runner.temp }}/pkg-cache - key: \${{ inputs.cache-key || format('pkg-fetch-{0}-{1}-node{2}-{3}', runner.os, runner.arch, inputs.node-version, hashFiles('**/package.json', '.pkgrc*', '**/pkg.config.{js,ts,json}')) }} + key: \${{ inputs.cache-key || format('pkg-fetch-{0}-{1}-{2}', runner.os, runner.arch, hashFiles('**/package.json', '.pkgrc*', '**/pkg.config.{js,ts,json}')) }} - name: Install @yao-pkg/pkg if: \${{ inputs.pkg-path == '' }} From 417a1beafa0cd27fdc89dd9e1d4f8d5e04c99ba8 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:18:13 +0200 Subject: [PATCH 02/10] review: link STATUS from architecture, cover pkg-version/pkg-path in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR-review suggestions: - architecture.md §6 no longer duplicates the dropped-input list; points readers at STATUS.yaml#input-surface-slim as the single source. - inputs.test.ts: extend the defaults test with `pkgPath` and add a positive round-trip test for `pkg-version` + `pkg-path`. These remain live inputs and deserve the same explicit coverage the rest of BuildInputs has. 223/223 unit tests, lint clean. --- docs/architecture.md | 10 +++++----- packages/core/test/unit/inputs.test.ts | 9 +++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a3159e9..fd4dc4c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -157,15 +157,15 @@ record per input with `name / description / default? / required? / category / deprecated? / secret?`. **Input-surface scope** (2026-04-23): the action intentionally does not -mirror pkg's CLI. Pkg-specific knobs (`mode`, `compress-node`, -`fallback-to-source`, `public`, `public-packages`, `options`, `no-bytecode`, -`no-dict`, `debug`, `extra-args`, `node-version`) are expressed via the user's -pkg config file, which `buildPkgArgs` forwards through `--config`. The action -owns only concerns that pkg config cannot express: CI-matrix `targets`, the +mirror pkg's CLI. Pkg-specific knobs are expressed via the user's pkg config +file, which `buildPkgArgs` forwards through `--config`. The action owns only +concerns that pkg config cannot express: CI-matrix `targets`, the `pkg-version` / `pkg-path` install choice, archive format, filename template, checksum algorithms, Windows-metadata resedit patch, signing, cache, and step summary. Rationale: decouple the action from pkg's CLI evolution — each new pkg flag otherwise forced a back-compat-preserving input bump here. +Authoritative list of dropped inputs + migration note lives in +[`STATUS.yaml`](../STATUS.yaml) under `input-surface-slim`. Emitted: diff --git a/packages/core/test/unit/inputs.test.ts b/packages/core/test/unit/inputs.test.ts index 711c04e..e7bf153 100644 --- a/packages/core/test/unit/inputs.test.ts +++ b/packages/core/test/unit/inputs.test.ts @@ -64,6 +64,7 @@ test('parseInputs with no env uses defaults', () => { strictEqual(inputs.build.config, undefined); strictEqual(inputs.build.entry, undefined); strictEqual(inputs.build.pkgVersion, '~6.16.0'); + strictEqual(inputs.build.pkgPath, undefined); strictEqual(inputs.postBuild.compress, 'none'); strictEqual(inputs.postBuild.strip, false); @@ -74,6 +75,14 @@ test('parseInputs with no env uses defaults', () => { strictEqual(inputs.performance.stepSummary, true); }); +test('parseInputs threads pkg-version + pkg-path through', () => { + const inputs = parseInputs({ + env: env(['pkg-version', '~6.99.0'], ['pkg-path', '/opt/pkg/bin/pkg']), + }); + strictEqual(inputs.build.pkgVersion, '~6.99.0'); + strictEqual(inputs.build.pkgPath, '/opt/pkg/bin/pkg'); +}); + test('parseInputs parses a realistic build config', () => { const inputs = parseInputs({ env: env( From 5a2d84f2a787397f2d3a60aa30c89e1c39ffce65 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:22:42 +0200 Subject: [PATCH 03/10] feat: add config-inline input for file-less pkg config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New optional input `config-inline` accepts a JSON string carrying the pkg config directly in the workflow. The orchestrator writes it to `${invocationDir}/pkg-config.inline.json` and forwards that path to pkg via `--config`. Mutually exclusive with `config`; the pair is validated at parse time alongside a JSON-object syntax check so a typo fails before pkg runs. Motivation: users with a trivial pkg config (entry + a couple of knobs) no longer need to commit a separate .pkgrc.json just to customize the build. Keeps the "pkg owns its schema" property — users still write pkg config keys (camelCase), we just let them write them inline. Not masked as a secret — README adds a note warning users against embedding secrets in the value. Docs: README gains an "Inline config" subsection with an example; action.yml + docs/inputs.md regenerated. Tests: four new unit tests cover the happy path, mutual exclusion, invalid JSON, and non-object JSON (string/array/null). 227/227 pass. --- README.md | 22 ++++++++++++ action.yml | 5 ++- docs/inputs.md | 3 +- packages/build/action.yml | 4 ++- packages/build/dist/index.mjs | 39 +++++++++++++++++--- packages/build/src/main.ts | 22 +++++++++--- packages/core/src/inputs.ts | 42 ++++++++++++++++++++-- packages/core/test/unit/inputs.test.ts | 41 +++++++++++++++++++++ packages/core/test/unit/pkg-runner.test.ts | 1 + packages/windows-metadata/dist/index.mjs | 7 +++- 10 files changed, 172 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cd75097..e67cd45 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,28 @@ Example — SEA mode with Brotli-compressed Node + fallback: targets: node22-linux-x64 ``` +### Inline config + +For trivial setups you can skip the file and pass the config as a JSON string +via `config-inline` — the action writes it to a temp file and points pkg at +it. Mutually exclusive with `config`. + +```yaml +- uses: yao-pkg/pkg-action@v1 + with: + targets: node22-linux-x64 + config-inline: | + { + "bin": "src/main.js", + "mode": "sea", + "compressNode": "Brotli" + } +``` + +> **Do not embed secrets in `config-inline`** — the value is written to disk +> and echoed into the build log. It is not registered with +> `core.setSecret`. + The action's inputs cover only concerns pkg config cannot express: matrix targets, pkg install version/path, archive format, filename template, checksum algorithms, Windows-metadata resedit patch, macOS/Windows signing, diff --git a/action.yml b/action.yml index ce08bd6..452ad04 100644 --- a/action.yml +++ b/action.yml @@ -10,7 +10,9 @@ branding: inputs: config: - description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted.' + description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline.' + config-inline: + description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.' entry: description: 'Entry script when not specified in the config.' targets: @@ -153,6 +155,7 @@ runs: uses: ./packages/build with: config: ${{ inputs.config }} + config-inline: ${{ inputs.config-inline }} entry: ${{ inputs.entry }} targets: ${{ inputs.targets }} pkg-version: ${{ inputs.pkg-version }} diff --git a/docs/inputs.md b/docs/inputs.md index f0556a6..afe18f5 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -8,7 +8,8 @@ Every `pkg-action` input, grouped by category. | Input | Default | Required | Secret | Description | | --- | --- | --- | --- | --- | -| `config` | — | no | no | Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. | +| `config` | — | no | no | Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline. | +| `config-inline` | — | no | no | Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked. | | `entry` | — | no | no | Entry script when not specified in the config. | | `targets` | — | no | no | Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target. | | `pkg-version` | `~6.16.0` | no | no | npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set. | diff --git a/packages/build/action.yml b/packages/build/action.yml index 210c5a4..33cb4fa 100644 --- a/packages/build/action.yml +++ b/packages/build/action.yml @@ -7,7 +7,9 @@ author: 'yao-pkg contributors' inputs: config: - description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted.' + description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline.' + config-inline: + description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.' entry: description: 'Entry script when not specified in the config.' targets: diff --git a/packages/build/dist/index.mjs b/packages/build/dist/index.mjs index 5950a23..5b52c37 100644 --- a/packages/build/dist/index.mjs +++ b/packages/build/dist/index.mjs @@ -14497,7 +14497,7 @@ function getState(name) { } // packages/build/src/main.ts -import { mkdir as mkdir3, rename as rename3, stat as stat4 } from "node:fs/promises"; +import { mkdir as mkdir3, rename as rename3, stat as stat4, writeFile as writeFile5 } from "node:fs/promises"; import { tmpdir as tmpdir2 } from "node:os"; import { basename as pathBasename, dirname as dirname5, join as join7, resolve as pathResolve } from "node:path"; @@ -14764,7 +14764,12 @@ var INPUT_SPECS = [ { name: "config", category: "build", - description: "Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted." + description: "Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline." + }, + { + name: "config-inline", + category: "build", + description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets \u2014 this input is not masked." }, { name: "entry", @@ -15063,8 +15068,26 @@ function parseInputs(opts = {}) { let targetsRaw = readInput(env, "targets"), targets = targetsRaw === void 0 ? "host" : parseTargetList(targetsRaw); if (Array.isArray(targets) && targets.length === 0) throw new ValidationError('Input "targets" was set but resolved to an empty list.'); + let configPath = readInput(env, "config"), configInline = readInput(env, "config-inline"); + if (configPath !== void 0 && configInline !== void 0) + throw new ValidationError( + 'Inputs "config" and "config-inline" are mutually exclusive \u2014 pick one.' + ); + if (configInline !== void 0) + try { + let parsed = JSON.parse(configInline); + if (parsed === null || typeof parsed != "object" || Array.isArray(parsed)) + throw new ValidationError( + 'Input "config-inline" must parse to a JSON object (got ' + (parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed) + ")." + ); + } catch (err) { + throw err instanceof ValidationError ? err : new ValidationError( + `Input "config-inline" is not valid JSON: ${err instanceof Error ? err.message : String(err)}` + ); + } let build = { - config: readInput(env, "config"), + config: configPath, + configInline, entry: readInput(env, "entry"), targets, pkgVersion: readInput(env, "pkg-version") ?? "~6.16.0", @@ -19237,7 +19260,15 @@ async function main() { saveState("invocationDir", invocationDir); let pkgOutputDir = join7(invocationDir, "pkg-out"); await mkdir3(pkgOutputDir, { recursive: !0 }); - let pkgCommand = inputs.build.pkgPath ?? "pkg", pkgBuildInputs = inputs.build.config !== void 0 && pathBasename(inputs.build.config).toLowerCase() === "package.json" ? { ...inputs.build, config: void 0 } : inputs.build, pkgArgs = buildPkgArgs({ + let effectiveConfig = inputs.build.config; + if (inputs.build.configInline !== void 0) { + let inlinePath = join7(invocationDir, "pkg-config.inline.json"); + await writeFile5(inlinePath, inputs.build.configInline, "utf8"), effectiveConfig = inlinePath, logger.info(`[pkg-action] materialized config-inline \u2192 ${inlinePath}`); + } + let pkgCommand = inputs.build.pkgPath ?? "pkg", cfgIsPackageJson = effectiveConfig !== void 0 && pathBasename(effectiveConfig).toLowerCase() === "package.json", pkgBuildInputs = { + ...inputs.build, + config: cfgIsPackageJson ? void 0 : effectiveConfig + }, pkgArgs = buildPkgArgs({ build: pkgBuildInputs, targets: resolvedTargets, outputDir: pkgOutputDir diff --git a/packages/build/src/main.ts b/packages/build/src/main.ts index 981eb2b..14525f3 100644 --- a/packages/build/src/main.ts +++ b/packages/build/src/main.ts @@ -25,7 +25,7 @@ // `checksums` outputs. import * as core from '@actions/core'; -import { mkdir, rename, stat } from 'node:fs/promises'; +import { mkdir, rename, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename as pathBasename, dirname, join, resolve as pathResolve } from 'node:path'; import { @@ -151,6 +151,18 @@ async function main(): Promise { const pkgOutputDir = join(invocationDir, 'pkg-out'); await mkdir(pkgOutputDir, { recursive: true }); + // 4.5. Materialize `config-inline` to disk, if set. parseInputs already + // validated it as a JSON object and enforced mutual exclusion with + // `config`, so this step just writes the bytes and threads the resulting + // path through as the effective config. + let effectiveConfig = inputs.build.config; + if (inputs.build.configInline !== undefined) { + const inlinePath = join(invocationDir, 'pkg-config.inline.json'); + await writeFile(inlinePath, inputs.build.configInline, 'utf8'); + effectiveConfig = inlinePath; + logger.info(`[pkg-action] materialized config-inline → ${inlinePath}`); + } + // 5. Run pkg from the project directory. // // When a package.json was used to locate the project, drop the explicit @@ -159,9 +171,11 @@ async function main(): Promise { // standalone pkg config like .pkgrc.json). const pkgCommand = inputs.build.pkgPath ?? 'pkg'; const cfgIsPackageJson = - inputs.build.config !== undefined && - pathBasename(inputs.build.config).toLowerCase() === 'package.json'; - const pkgBuildInputs = cfgIsPackageJson ? { ...inputs.build, config: undefined } : inputs.build; + effectiveConfig !== undefined && pathBasename(effectiveConfig).toLowerCase() === 'package.json'; + const pkgBuildInputs = { + ...inputs.build, + config: cfgIsPackageJson ? undefined : effectiveConfig, + }; const pkgArgs = buildPkgArgs({ build: pkgBuildInputs, targets: resolvedTargets, diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts index b3a705e..c2950fd 100644 --- a/packages/core/src/inputs.ts +++ b/packages/core/src/inputs.ts @@ -36,7 +36,13 @@ export const INPUT_SPECS: readonly InputSpec[] = [ name: 'config', category: 'build', description: - 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted.', + 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline.', + }, + { + name: 'config-inline', + category: 'build', + description: + 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.', }, { name: 'entry', @@ -307,6 +313,12 @@ export type ArchiveFormatInput = 'tar.gz' | 'tar.xz' | 'zip' | '7z' | 'none'; */ export interface BuildInputs { readonly config: string | undefined; + /** + * Inline JSON pkg config. Mutually exclusive with `config`. Orchestrator + * materializes this to a file under the invocation temp dir and forwards + * the resulting path to pkg via `--config`. + */ + readonly configInline: string | undefined; readonly entry: string | undefined; /** 'host' = use the host target; otherwise a non-empty list. */ readonly targets: Target[] | 'host'; @@ -452,8 +464,34 @@ export function parseInputs(opts: ParseInputsOptions = {}): ActionInputs { throw new ValidationError(`Input "targets" was set but resolved to an empty list.`); } + const configPath = readInput(env, 'config'); + const configInline = readInput(env, 'config-inline'); + if (configPath !== undefined && configInline !== undefined) { + throw new ValidationError( + 'Inputs "config" and "config-inline" are mutually exclusive — pick one.', + ); + } + if (configInline !== undefined) { + try { + const parsed: unknown = JSON.parse(configInline); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new ValidationError( + 'Input "config-inline" must parse to a JSON object (got ' + + (parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed) + + ').', + ); + } + } catch (err) { + if (err instanceof ValidationError) throw err; + throw new ValidationError( + `Input "config-inline" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + const build: BuildInputs = { - config: readInput(env, 'config'), + config: configPath, + configInline, entry: readInput(env, 'entry'), targets, pkgVersion: readInput(env, 'pkg-version') ?? '~6.16.0', diff --git a/packages/core/test/unit/inputs.test.ts b/packages/core/test/unit/inputs.test.ts index e7bf153..8f4a4c7 100644 --- a/packages/core/test/unit/inputs.test.ts +++ b/packages/core/test/unit/inputs.test.ts @@ -62,6 +62,7 @@ test('parseInputs with no env uses defaults', () => { const inputs = parseInputs({ env: {} }); strictEqual(inputs.build.targets, 'host'); strictEqual(inputs.build.config, undefined); + strictEqual(inputs.build.configInline, undefined); strictEqual(inputs.build.entry, undefined); strictEqual(inputs.build.pkgVersion, '~6.16.0'); strictEqual(inputs.build.pkgPath, undefined); @@ -103,6 +104,46 @@ test('parseInputs parses a realistic build config', () => { strictEqual(inputs.postBuild.strip, true); }); +test('parseInputs accepts config-inline with valid JSON object', () => { + const inputs = parseInputs({ + env: env(['config-inline', '{"bin":"src/main.js","mode":"sea"}']), + }); + strictEqual(inputs.build.config, undefined); + strictEqual(inputs.build.configInline, '{"bin":"src/main.js","mode":"sea"}'); +}); + +test('parseInputs rejects config + config-inline set together', () => { + throws( + () => + parseInputs({ + env: env(['config', '.pkgrc.json'], ['config-inline', '{"bin":"x.js"}']), + }), + ValidationError, + ); +}); + +test('parseInputs rejects config-inline with invalid JSON', () => { + throws( + () => parseInputs({ env: env(['config-inline', '{not json']) }), + (err: unknown) => err instanceof ValidationError && /not valid JSON/.test(err.message), + ); +}); + +test('parseInputs rejects config-inline that is not a JSON object', () => { + throws( + () => parseInputs({ env: env(['config-inline', '"bare-string"']) }), + (err: unknown) => err instanceof ValidationError && /JSON object/.test(err.message), + ); + throws( + () => parseInputs({ env: env(['config-inline', '[1,2,3]']) }), + (err: unknown) => err instanceof ValidationError && /JSON object/.test(err.message), + ); + throws( + () => parseInputs({ env: env(['config-inline', 'null']) }), + (err: unknown) => err instanceof ValidationError && /JSON object/.test(err.message), + ); +}); + test('parseInputs flags removed pkg-layer inputs as unknown', () => { const unknown: string[] = []; parseInputs({ diff --git a/packages/core/test/unit/pkg-runner.test.ts b/packages/core/test/unit/pkg-runner.test.ts index 61a0e00..b73da1e 100644 --- a/packages/core/test/unit/pkg-runner.test.ts +++ b/packages/core/test/unit/pkg-runner.test.ts @@ -7,6 +7,7 @@ import { PkgRunError } from '../../src/errors.ts'; const BASE_BUILD: BuildInputs = { config: undefined, + configInline: undefined, entry: undefined, targets: 'host', pkgVersion: '~6.16.0', diff --git a/packages/windows-metadata/dist/index.mjs b/packages/windows-metadata/dist/index.mjs index 7cda528..06792e1 100644 --- a/packages/windows-metadata/dist/index.mjs +++ b/packages/windows-metadata/dist/index.mjs @@ -13363,7 +13363,12 @@ var INPUT_SPECS = [ { name: "config", category: "build", - description: "Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted." + description: "Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline." + }, + { + name: "config-inline", + category: "build", + description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets \u2014 this input is not masked." }, { name: "entry", From cfbbdb62ffe28e4cd4966ed0cd42e98fb86b57db Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:26:34 +0200 Subject: [PATCH 04/10] review(copilot): use correct camelCase pkg-config keys in warnings/docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three nits from the Copilot review on PR #13: - targets.ts cross-compile warnings referenced `fallback-to-source` (kebab, like the retired CLI flag); pkg config uses `fallbackToSource` (camelCase). Reword both messages to point at the actual config key. - README "pkg configuration" example was fenced as jsonc with a `//` comment and trailing comma but labeled `.pkgrc.json` — .pkgrc.json is strict JSON, so a copy/paste would fail. Move the filename into the prose, fence as json, drop the comment + trailing comma. Unit tests + lint green. Matrix bundle rebuilt to pick up the new warning strings. --- README.md | 7 +++---- packages/core/src/targets.ts | 4 ++-- packages/matrix/dist/index.mjs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e67cd45..00284b4 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,14 @@ the `pkg` field of `package.json`). See [yao-pkg/pkg's README](https://github.com/yao-pkg/pkg#config) for the full schema. -Example — SEA mode with Brotli-compressed Node + fallback: +Example — SEA mode with Brotli-compressed Node + fallback, saved as `.pkgrc.json`: -```jsonc -// .pkgrc.json +```json { "bin": "src/main.js", "mode": "sea", "compressNode": "Brotli", - "fallbackToSource": true, + "fallbackToSource": true } ``` diff --git a/packages/core/src/targets.ts b/packages/core/src/targets.ts index 67b1bef..b1d8467 100644 --- a/packages/core/src/targets.ts +++ b/packages/core/src/targets.ts @@ -213,11 +213,11 @@ export function crossCompileRisk(host: Target, target: Target): string | null { } // Non-Linux → Linux-arm64 without QEMU hits the fabrication bug on Node 22. if (target.os === 'linux' && target.arch === 'arm64' && host.arch !== 'arm64') { - return 'Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or enable fallback-to-source in your pkg config.'; + return 'Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or set `fallbackToSource: true` in your pkg config.'; } // Non-Windows → win-x64 on Node 22 also hits the fabricator bug. if (target.os === 'win' && target.arch === 'x64' && host.os !== 'win') { - return 'win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or enable fallback-to-source in your pkg config.'; + return 'win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or set `fallbackToSource: true` in your pkg config.'; } // macOS-arm64 requires signing. if (target.os === 'macos' && target.arch === 'arm64' && host.os !== 'macos') { diff --git a/packages/matrix/dist/index.mjs b/packages/matrix/dist/index.mjs index cb2bc98..5070e70 100644 --- a/packages/matrix/dist/index.mjs +++ b/packages/matrix/dist/index.mjs @@ -13434,7 +13434,7 @@ function expandMatrix(targets, overrides = {}) { } function crossCompileRisk(host, target) { let sameOs = host.os === target.os, sameArch = host.arch === target.arch; - return sameOs && sameArch ? null : host.os === "linux" && target.os === "macos" ? "Linux host \u2192 macOS target produces non-functional binaries (see yao-pkg/pkg#183). Use a macOS runner." : target.os === "linux" && target.arch === "arm64" && host.arch !== "arm64" ? "Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or enable fallback-to-source in your pkg config." : target.os === "win" && target.arch === "x64" && host.os !== "win" ? "win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or enable fallback-to-source in your pkg config." : target.os === "macos" && target.arch === "arm64" && host.os !== "macos" ? "macos-arm64 binaries must be signed to run; cross-compiling without a codesign toolchain will produce an unusable binary." : null; + return sameOs && sameArch ? null : host.os === "linux" && target.os === "macos" ? "Linux host \u2192 macOS target produces non-functional binaries (see yao-pkg/pkg#183). Use a macOS runner." : target.os === "linux" && target.arch === "arm64" && host.arch !== "arm64" ? "Linux-arm64 cross-compile from a non-arm64 host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a linux/arm64 runner or set `fallbackToSource: true` in your pkg config." : target.os === "win" && target.arch === "x64" && host.os !== "win" ? "win-x64 cross-compile from a non-Windows host hits the pkg bytecode fabricator bug (#87/#181) on Node 22. Use a windows runner or set `fallbackToSource: true` in your pkg config." : target.os === "macos" && target.arch === "arm64" && host.os !== "macos" ? "macos-arm64 binaries must be signed to run; cross-compiling without a codesign toolchain will produce an unusable binary." : null; } // packages/core/src/version.ts From dce899e1ad6ce1539928e996f08f59a3a7b22a90 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:38:28 +0200 Subject: [PATCH 05/10] e2e(claude-code): move pkg-layer knobs from action inputs to pkg config Main (PR #12) wired the claude-code-smoke job through the action using `mode: sea` and `compress-node: Zstd` inputs. Those inputs no longer exist on this branch, so move both values into the package.json#pkg field that the fixture-prep step already injects. No behavior change on main; on this branch the e2e now exercises the pkg-config path the slim input surface forces users onto. --- .github/workflows/e2e.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6d7de12..232f6ca 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -332,8 +332,12 @@ jobs: const p = './package.json'; const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); pkg.bin = './' + process.argv[1]; + // pkg-layer knobs (mode, compressNode) live in the pkg config now + // that the action stopped mirroring pkg's CLI flags as inputs. pkg.pkg = { - assets: ['node_modules/@anthropic-ai/claude-code/**/*'] + assets: ['node_modules/@anthropic-ai/claude-code/**/*'], + mode: 'sea', + compressNode: 'Zstd', }; fs.writeFileSync(p, JSON.stringify(pkg, null, 2)); " "$entry" @@ -350,10 +354,10 @@ jobs: with: config: ${{ steps.fix.outputs.fixture }}/package.json targets: ${{ matrix.target }} - mode: sea - compress-node: Zstd # Zstd landed in @yao-pkg/pkg 6.17. The action's default # (~6.16.0) predates it, so pin a Zstd-capable version. + # pkg-layer knobs (mode: sea, compressNode: Zstd) are set in the + # package.json#pkg field, not as action inputs. pkg-version: ~6.18.0 compress: tar.gz checksum: sha256 From 9202af6d19ee5f5a70131995b6822a64ff3b413f Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:46:58 +0200 Subject: [PATCH 06/10] e2e(claude-code): use sea:true config, acknowledge CLI-only flag gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge from main brought in the claude-code-smoke job, which previously drove pkg via `mode: sea` + `compress-node: Zstd` action inputs. On this branch those inputs are dropped. The prior merge-conflict resolution moved both into `package.json#pkg` under the assumption that pkg config would pick them up. That assumption was wrong: - `mode: 'sea'` is not a pkg config key — the correct key is `sea: true`. (This was a user-visible bug; pkg silently ignored `mode` and ran standard-mode, which OOM'd trying to bytecode-compile claude-code's ESM bundle on macos-arm64.) - `compressNode` / `compress` is not a pkg config key at all. `--compress` is CLI-only today on @yao-pkg/pkg. No config equivalent exists. Fix the e2e: - Write `sea: true` into package.json#pkg so SEA is actually enabled. - Drop the Zstd request — there is no way to express it without the dropped CLI input. The job now exercises SEA + tar.gz; the Zstd branch is intentionally deferred until upstream lands config support. - Rename + retone the job's comment block accordingly. Also add an upstream-dependency note to STATUS.yaml and a draft issue body at docs/upstream-pkg-config-issue.md. The draft asks yao-pkg/pkg to accept the currently-CLI-only build flags (compress, fallbackToSource, public, publicPackages, options, noBytecode, noDict, debug, signature) in the config file — that's the piece that makes this PR's scope defensible for the full input set, not just for SEA. 227 unit tests pass; lint + prettier clean. --- .github/workflows/e2e.yml | 38 ++++++++++++++---------- STATUS.yaml | 11 +++++++ docs/upstream-pkg-config-issue.md | 49 +++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 docs/upstream-pkg-config-issue.md diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 232f6ca..cc67850 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -253,16 +253,22 @@ jobs: # ────────────────────────────────────────────────────────────────────── # Real-world smoke: pull @anthropic-ai/claude-code from npm, build a - # native binary per runner OS/arch with Zstd-compressed pkg payload + - # tar.gz archive, execute the binary with --version, then verify the - # archive round-trips and the sha256 sidecar matches the archive bytes. + # native binary per runner OS/arch in SEA mode, tar.gz archive, execute + # the binary with --version, then verify the archive round-trips and + # the sha256 sidecar matches the archive bytes. # - # claude-code is ESM, so mode: sea (standard pkg can't bytecode-compile - # ESM). Each matrix entry builds for the runner's native target so the - # produced binary can be launched in-place for the smoke assertion, + # claude-code is ESM, so SEA mode is required (standard pkg can't + # bytecode-compile ESM). SEA is expressed via `sea: true` in the pkg + # config (package.json#pkg). Each matrix entry builds for the runner's + # native target so the produced binary can be launched in-place, # giving real cross-OS + cross-arch coverage (x64 and arm64 on Linux, - # arm64 on macOS, x64 on Windows). Zstd bundled-binary compression - # requires Node.js >= 22.15 on the build host; .node-version is 22.22. + # arm64 on macOS, x64 on Windows). + # + # NOTE: Zstd bundled-binary compression (`--compress Zstd`) is a + # CLI-only flag on @yao-pkg/pkg today; there is no pkg-config key for + # it yet. Since this branch doesn't mirror CLI-only flags as action + # inputs, the job can't exercise Zstd until the upstream pkg issue + # lands config support. See docs/upstream-pkg-config-issue.md. claude-code-smoke: name: claude-code-smoke / ${{ matrix.os }} / ${{ matrix.target }} runs-on: ${{ matrix.os }} @@ -332,12 +338,13 @@ jobs: const p = './package.json'; const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); pkg.bin = './' + process.argv[1]; - // pkg-layer knobs (mode, compressNode) live in the pkg config now - // that the action stopped mirroring pkg's CLI flags as inputs. + // SEA mode is expressible in pkg config (sea: true). Other pkg + // build knobs (compress, fallbackToSource, …) are CLI-only + // today — this branch waits on upstream config support rather + // than mirror them as action inputs. pkg.pkg = { assets: ['node_modules/@anthropic-ai/claude-code/**/*'], - mode: 'sea', - compressNode: 'Zstd', + sea: true, }; fs.writeFileSync(p, JSON.stringify(pkg, null, 2)); " "$entry" @@ -354,10 +361,9 @@ jobs: with: config: ${{ steps.fix.outputs.fixture }}/package.json targets: ${{ matrix.target }} - # Zstd landed in @yao-pkg/pkg 6.17. The action's default - # (~6.16.0) predates it, so pin a Zstd-capable version. - # pkg-layer knobs (mode: sea, compressNode: Zstd) are set in the - # package.json#pkg field, not as action inputs. + # Pin a recent pkg — 6.18 line is what main's e2e was validated + # against. SEA (`sea: true`) is set in the package.json#pkg field, + # not as an action input. pkg-version: ~6.18.0 compress: tar.gz checksum: sha256 diff --git a/STATUS.yaml b/STATUS.yaml index 9037326..0bbe8c1 100644 --- a/STATUS.yaml +++ b/STATUS.yaml @@ -68,6 +68,17 @@ input-surface-slim: - extra-args migration: 'Move each value into .pkgrc / pkg.config.{js,ts,json} or the `pkg` field of package.json. `buildPkgArgs` forwards the config path to pkg.' breaking: 'yes — users setting any dropped input will see the unknown-input warning and the value will be ignored. Release note required.' + upstream-dependency: | + @yao-pkg/pkg currently accepts only a subset of build flags in its config + file (scripts, assets, ignore, targets, outputPath, patches, sea, + seaConfig, deployAssets). The rest (--compress, --fallback-to-source, + --public, --public-packages, --options, --no-bytecode, --no-dict, + --debug, --signature) are CLI-only. Until pkg lands config support for + those, users of this action lose access to them. Draft of the upstream + issue lives at docs/upstream-pkg-config-issue.md — file against + yao-pkg/pkg and link back here once opened. + known-gaps: + - 'Zstd bundled-binary compression — unreachable without the --compress CLI flag. The claude-code-smoke e2e job exercises SEA + tar.gz only until upstream lands pkg-config `compress` support.' # ─── Removed (2026-04-23) ──────────────────────────────────────────────── # Distribution scope reset. Use dedicated downstream actions instead. diff --git a/docs/upstream-pkg-config-issue.md b/docs/upstream-pkg-config-issue.md new file mode 100644 index 0000000..390a716 --- /dev/null +++ b/docs/upstream-pkg-config-issue.md @@ -0,0 +1,49 @@ + + +# Title + +feat(config): accept the CLI-only build flags in the config file + +# Body + +## Summary + +Today the pkg config file (`.pkgrc`, `pkg.config.{js,ts,json}`, `package.json#pkg`) accepts `scripts`, `assets`, `ignore`, `targets`, `outputPath`, `patches`, `sea`, `seaConfig`, and `deployAssets`. Most other build-shaping flags are CLI-only. + +It would be very useful to accept these **in the config file as well** so that tooling (CI actions, wrappers, IDE plugins) can drive the build from a single declarative source instead of synthesizing a CLI invocation. + +## Flags that are CLI-only today + +| CLI flag | Suggested config key | +| --------------------------------------- | -------------------------- | +| `--compress ` | `compress` | +| `--fallback-to-source` | `fallbackToSource: true` | +| `--public` | `public: true` | +| `--public-packages ` | `publicPackages: string[]` | +| `--options ` | `options: string[]` | +| `--no-bytecode` | `noBytecode: true` | +| `--no-dict ` | `noDict: string[] \| '*'` | +| `--debug` | `debug: true` | +| `--signature ` | `signature: string` | + +## Why + +Context: we're building the official GitHub Action (yao-pkg/pkg#248, yao-pkg/pkg-action). The action tries to expose pkg's build surface to users via workflow YAML. Two approaches: + +1. **Mirror each CLI flag as an action input.** Every time pkg adds/renames a flag we have to bump the action and preserve back-compat — heavy maintenance burden. +2. **Let users declare everything in their pkg config file.** Action forwards `--config`, pkg owns its schema, zero action-side drift. + +Approach 2 is cleaner — but it's only partial today because of these CLI-only flags. Users who want SEA mode + Zstd + `fallbackToSource` have to either commit a config for `sea` and pass the rest as CLI flags via the action, or we have to re-mirror them as action inputs. + +If pkg accepts the full set in config, the action becomes a thin wrapper that forwards a config path and doesn't have to know anything about pkg's evolving flag set. Users also get one declarative source of truth for "what does this build do" instead of splitting it between `.pkgrc` and a CI YAML. + +## Proposed behavior + +- CLI flag still wins when both are set (CLI overrides config) — matches the existing `targets` / `outputPath` convention. +- Config values validated with clear errors (unknown key, wrong type) rather than silently ignored. + +Happy to help with a PR if there's interest — just wanted to confirm the direction before spending time on it. From 716278615d47f4110b398ddf1ab8943f587a75c1 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Thu, 23 Apr 2026 11:48:06 +0200 Subject: [PATCH 07/10] status: link upstream pkg-config tracking issue (yao-pkg/pkg#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue is open upstream asking @yao-pkg/pkg to accept the currently-CLI-only build flags (compress, fallbackToSource, public, publicPackages, options, noBytecode, noDict, debug, signature) in the config file — the piece that makes this PR's drop-the-CLI-mirror direction fully viable. Replace the local draft with the issue URL in STATUS.yaml#upstream-dependency. --- STATUS.yaml | 5 ++-- docs/upstream-pkg-config-issue.md | 49 ------------------------------- 2 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 docs/upstream-pkg-config-issue.md diff --git a/STATUS.yaml b/STATUS.yaml index 0bbe8c1..8ce9a81 100644 --- a/STATUS.yaml +++ b/STATUS.yaml @@ -74,9 +74,8 @@ input-surface-slim: seaConfig, deployAssets). The rest (--compress, --fallback-to-source, --public, --public-packages, --options, --no-bytecode, --no-dict, --debug, --signature) are CLI-only. Until pkg lands config support for - those, users of this action lose access to them. Draft of the upstream - issue lives at docs/upstream-pkg-config-issue.md — file against - yao-pkg/pkg and link back here once opened. + those, users of this action lose access to them. + Tracking upstream: https://github.com/yao-pkg/pkg/issues/262. known-gaps: - 'Zstd bundled-binary compression — unreachable without the --compress CLI flag. The claude-code-smoke e2e job exercises SEA + tar.gz only until upstream lands pkg-config `compress` support.' diff --git a/docs/upstream-pkg-config-issue.md b/docs/upstream-pkg-config-issue.md deleted file mode 100644 index 390a716..0000000 --- a/docs/upstream-pkg-config-issue.md +++ /dev/null @@ -1,49 +0,0 @@ - - -# Title - -feat(config): accept the CLI-only build flags in the config file - -# Body - -## Summary - -Today the pkg config file (`.pkgrc`, `pkg.config.{js,ts,json}`, `package.json#pkg`) accepts `scripts`, `assets`, `ignore`, `targets`, `outputPath`, `patches`, `sea`, `seaConfig`, and `deployAssets`. Most other build-shaping flags are CLI-only. - -It would be very useful to accept these **in the config file as well** so that tooling (CI actions, wrappers, IDE plugins) can drive the build from a single declarative source instead of synthesizing a CLI invocation. - -## Flags that are CLI-only today - -| CLI flag | Suggested config key | -| --------------------------------------- | -------------------------- | -| `--compress ` | `compress` | -| `--fallback-to-source` | `fallbackToSource: true` | -| `--public` | `public: true` | -| `--public-packages ` | `publicPackages: string[]` | -| `--options ` | `options: string[]` | -| `--no-bytecode` | `noBytecode: true` | -| `--no-dict ` | `noDict: string[] \| '*'` | -| `--debug` | `debug: true` | -| `--signature ` | `signature: string` | - -## Why - -Context: we're building the official GitHub Action (yao-pkg/pkg#248, yao-pkg/pkg-action). The action tries to expose pkg's build surface to users via workflow YAML. Two approaches: - -1. **Mirror each CLI flag as an action input.** Every time pkg adds/renames a flag we have to bump the action and preserve back-compat — heavy maintenance burden. -2. **Let users declare everything in their pkg config file.** Action forwards `--config`, pkg owns its schema, zero action-side drift. - -Approach 2 is cleaner — but it's only partial today because of these CLI-only flags. Users who want SEA mode + Zstd + `fallbackToSource` have to either commit a config for `sea` and pass the rest as CLI flags via the action, or we have to re-mirror them as action inputs. - -If pkg accepts the full set in config, the action becomes a thin wrapper that forwards a config path and doesn't have to know anything about pkg's evolving flag set. Users also get one declarative source of truth for "what does this build do" instead of splitting it between `.pkgrc` and a CI YAML. - -## Proposed behavior - -- CLI flag still wins when both are set (CLI overrides config) — matches the existing `targets` / `outputPath` convention. -- Config values validated with clear errors (unknown key, wrong type) rather than silently ignored. - -Happy to help with a PR if there's interest — just wanted to confirm the direction before spending time on it. From ac90ddbf21e64d94cb49a4a37b14b7f561665ccb Mon Sep 17 00:00:00 2001 From: robertsLando Date: Fri, 24 Apr 2026 15:04:57 +0200 Subject: [PATCH 08/10] =?UTF-8?q?e2e(claude-code):=20enable=20Zstd=20via?= =?UTF-8?q?=20pkg=20config;=20bump=20pkg=20=E2=86=92=20~6.19.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pkg v6.19.0 (yao-pkg/pkg#263, closes #262) landed config support for the CLI-only build flags, and ships the SEA-mode detector fix (yao-pkg/pkg#268) that caused the prior OOM at bytecode-compile time. - claude-code-smoke: add `compress: 'Zstd'` alongside `sea: true` in package.json#pkg; bump pkg-version input ~6.18.0 → ~6.19.0. - Default `pkg-version` input bumped ~6.16.0 → ~6.19.0 — the whole config-as-source-of-truth direction of this branch requires it. - Regenerated action.yml / packages/build/action.yml / docs/inputs.md. - STATUS.yaml: upstream-dependency RESOLVED; Zstd gap dropped; e2e-status lists claude-code-smoke; ci-status bumped to 227 tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/e2e.yml | 40 ++++++++++++-------------- STATUS.yaml | 29 ++++++++++--------- action.yml | 4 +-- docs/inputs.md | 2 +- packages/build/action.yml | 4 +-- packages/core/src/inputs.ts | 6 ++-- packages/core/test/unit/inputs.test.ts | 2 +- 7 files changed, 42 insertions(+), 45 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cc67850..d091181 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -253,22 +253,18 @@ jobs: # ────────────────────────────────────────────────────────────────────── # Real-world smoke: pull @anthropic-ai/claude-code from npm, build a - # native binary per runner OS/arch in SEA mode, tar.gz archive, execute - # the binary with --version, then verify the archive round-trips and - # the sha256 sidecar matches the archive bytes. + # native binary per runner OS/arch in SEA mode with Zstd-compressed + # bundled sources, tar.gz archive, execute the binary with --version, + # then verify the archive round-trips and the sha256 sidecar matches + # the archive bytes. # # claude-code is ESM, so SEA mode is required (standard pkg can't - # bytecode-compile ESM). SEA is expressed via `sea: true` in the pkg - # config (package.json#pkg). Each matrix entry builds for the runner's - # native target so the produced binary can be launched in-place, - # giving real cross-OS + cross-arch coverage (x64 and arm64 on Linux, - # arm64 on macOS, x64 on Windows). - # - # NOTE: Zstd bundled-binary compression (`--compress Zstd`) is a - # CLI-only flag on @yao-pkg/pkg today; there is no pkg-config key for - # it yet. Since this branch doesn't mirror CLI-only flags as action - # inputs, the job can't exercise Zstd until the upstream pkg issue - # lands config support. See docs/upstream-pkg-config-issue.md. + # bytecode-compile ESM). Both `sea: true` and `compress: 'Zstd'` are + # declared in the pkg config (package.json#pkg) — expressible in + # config starting @yao-pkg/pkg v6.19.0 (yao-pkg/pkg#263). Each matrix + # entry builds for the runner's native target so the produced binary + # can be launched in-place, giving real cross-OS + cross-arch + # coverage (x64 and arm64 on Linux, arm64 on macOS, x64 on Windows). claude-code-smoke: name: claude-code-smoke / ${{ matrix.os }} / ${{ matrix.target }} runs-on: ${{ matrix.os }} @@ -338,13 +334,13 @@ jobs: const p = './package.json'; const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); pkg.bin = './' + process.argv[1]; - // SEA mode is expressible in pkg config (sea: true). Other pkg - // build knobs (compress, fallbackToSource, …) are CLI-only - // today — this branch waits on upstream config support rather - // than mirror them as action inputs. + // Full pkg-layer config lives here — no CLI-mirror inputs. + // Requires @yao-pkg/pkg >= 6.19.0 for \`compress\` in config + // (yao-pkg/pkg#263). pkg.pkg = { assets: ['node_modules/@anthropic-ai/claude-code/**/*'], sea: true, + compress: 'Zstd', }; fs.writeFileSync(p, JSON.stringify(pkg, null, 2)); " "$entry" @@ -361,10 +357,10 @@ jobs: with: config: ${{ steps.fix.outputs.fixture }}/package.json targets: ${{ matrix.target }} - # Pin a recent pkg — 6.18 line is what main's e2e was validated - # against. SEA (`sea: true`) is set in the package.json#pkg field, - # not as an action input. - pkg-version: ~6.18.0 + # 6.19.0+ is required for the `compress` key in pkg config. + # SEA (`sea: true`) and Zstd (`compress: 'Zstd'`) live in the + # package.json#pkg field, not as action inputs. + pkg-version: ~6.19.0 compress: tar.gz checksum: sha256 filename: 'claude-{version}-{os}-{arch}' diff --git a/STATUS.yaml b/STATUS.yaml index 8ce9a81..d1bf55b 100644 --- a/STATUS.yaml +++ b/STATUS.yaml @@ -10,13 +10,14 @@ meta: repo: yao-pkg/pkg-action branch: main - last-updated: '2026-04-23' + last-updated: '2026-04-24' action-status: ALPHA — ready for early adopters to try in their own workflows - ci-status: green (222 unit tests) + ci-status: green (227 unit tests) e2e-status: | - 6 jobs after the scope cut — tiny-cjs (ubuntu/macos/windows), codegen-drift, - matrix plan→fanout, multi-target-linux, windows-metadata. Signing e2e still - deferred until live credentials are available. + 7 jobs after the scope cut — tiny-cjs (ubuntu/macos/windows), codegen-drift, + matrix plan→fanout, multi-target-linux, windows-metadata, claude-code-smoke + (SEA + Zstd, 4 OS/arch combos). Signing e2e still deferred until live + credentials are available. # ─── Scope decision (2026-04-23) ───────────────────────────────────────── # @@ -68,16 +69,16 @@ input-surface-slim: - extra-args migration: 'Move each value into .pkgrc / pkg.config.{js,ts,json} or the `pkg` field of package.json. `buildPkgArgs` forwards the config path to pkg.' breaking: 'yes — users setting any dropped input will see the unknown-input warning and the value will be ignored. Release note required.' + minimum-pkg-version: | + @yao-pkg/pkg >= 6.19.0 is required for the full build-flag surface in + pkg config. The default `pkg-version` input is pinned to `~6.19.0`. upstream-dependency: | - @yao-pkg/pkg currently accepts only a subset of build flags in its config - file (scripts, assets, ignore, targets, outputPath, patches, sea, - seaConfig, deployAssets). The rest (--compress, --fallback-to-source, - --public, --public-packages, --options, --no-bytecode, --no-dict, - --debug, --signature) are CLI-only. Until pkg lands config support for - those, users of this action lose access to them. - Tracking upstream: https://github.com/yao-pkg/pkg/issues/262. - known-gaps: - - 'Zstd bundled-binary compression — unreachable without the --compress CLI flag. The claude-code-smoke e2e job exercises SEA + tar.gz only until upstream lands pkg-config `compress` support.' + RESOLVED in @yao-pkg/pkg v6.19.0 (2026-04-24) via yao-pkg/pkg#263 — + closes yao-pkg/pkg#262. All CLI-only build flags now have config + equivalents: compress, fallbackToSource, public, publicPackages, + options, bytecode (inverts --no-bytecode), nativeBuild (inverts + --no-native-build), noDictionary (renamed from --no-dict), debug, + signature. # ─── Removed (2026-04-23) ──────────────────────────────────────────────── # Distribution scope reset. Use dedicated downstream actions instead. diff --git a/action.yml b/action.yml index 452ad04..6c8d981 100644 --- a/action.yml +++ b/action.yml @@ -18,8 +18,8 @@ inputs: targets: description: 'Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target.' pkg-version: - description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.' - default: '~6.16.0' + description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set.' + default: '~6.19.0' pkg-path: description: 'Absolute path to a pre-installed pkg binary. Skips the implicit npm i -g.' strip: diff --git a/docs/inputs.md b/docs/inputs.md index afe18f5..7ad292c 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -12,7 +12,7 @@ Every `pkg-action` input, grouped by category. | `config-inline` | — | no | no | Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked. | | `entry` | — | no | no | Entry script when not specified in the config. | | `targets` | — | no | no | Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target. | -| `pkg-version` | `~6.16.0` | no | no | npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set. | +| `pkg-version` | `~6.19.0` | no | no | npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set. | | `pkg-path` | — | no | no | Absolute path to a pre-installed pkg binary. Skips the implicit npm i -g. | ## Post-build diff --git a/packages/build/action.yml b/packages/build/action.yml index 33cb4fa..c9c92ad 100644 --- a/packages/build/action.yml +++ b/packages/build/action.yml @@ -15,8 +15,8 @@ inputs: targets: description: 'Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target.' pkg-version: - description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.' - default: '~6.16.0' + description: 'npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set.' + default: '~6.19.0' pkg-path: description: 'Absolute path to a pre-installed pkg binary. Skips the implicit npm i -g.' strip: diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts index c2950fd..84d601e 100644 --- a/packages/core/src/inputs.ts +++ b/packages/core/src/inputs.ts @@ -59,8 +59,8 @@ export const INPUT_SPECS: readonly InputSpec[] = [ name: 'pkg-version', category: 'build', description: - 'npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.', - default: '~6.16.0', + 'npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set.', + default: '~6.19.0', }, { name: 'pkg-path', @@ -494,7 +494,7 @@ export function parseInputs(opts: ParseInputsOptions = {}): ActionInputs { configInline, entry: readInput(env, 'entry'), targets, - pkgVersion: readInput(env, 'pkg-version') ?? '~6.16.0', + pkgVersion: readInput(env, 'pkg-version') ?? '~6.19.0', pkgPath: readInput(env, 'pkg-path'), }; diff --git a/packages/core/test/unit/inputs.test.ts b/packages/core/test/unit/inputs.test.ts index 8f4a4c7..9470ab3 100644 --- a/packages/core/test/unit/inputs.test.ts +++ b/packages/core/test/unit/inputs.test.ts @@ -64,7 +64,7 @@ test('parseInputs with no env uses defaults', () => { strictEqual(inputs.build.config, undefined); strictEqual(inputs.build.configInline, undefined); strictEqual(inputs.build.entry, undefined); - strictEqual(inputs.build.pkgVersion, '~6.16.0'); + strictEqual(inputs.build.pkgVersion, '~6.19.0'); strictEqual(inputs.build.pkgPath, undefined); strictEqual(inputs.postBuild.compress, 'none'); From ad7c73bc732df0330814f90d6ae7a44728cdcfa8 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Fri, 24 Apr 2026 15:08:12 +0200 Subject: [PATCH 09/10] chore: rebuild dist/ bundles after pkg-version bump Rebuild packages/{build,windows-metadata}/dist/index.mjs so the committed bundles match the ~6.19.0 default written into packages/core/src/inputs.ts. Drift gate caught it. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/build/dist/index.mjs | 6 +++--- packages/windows-metadata/dist/index.mjs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/build/dist/index.mjs b/packages/build/dist/index.mjs index 81c8095..a1a376c 100644 --- a/packages/build/dist/index.mjs +++ b/packages/build/dist/index.mjs @@ -14784,8 +14784,8 @@ var INPUT_SPECS = [ { name: "pkg-version", category: "build", - description: "npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.", - default: "~6.16.0" + description: "npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set.", + default: "~6.19.0" }, { name: "pkg-path", @@ -15090,7 +15090,7 @@ function parseInputs(opts = {}) { configInline, entry: readInput(env, "entry"), targets, - pkgVersion: readInput(env, "pkg-version") ?? "~6.16.0", + pkgVersion: readInput(env, "pkg-version") ?? "~6.19.0", pkgPath: readInput(env, "pkg-path") }, postBuild = { strip: parseBoolean(readInput(env, "strip"), "strip"), diff --git a/packages/windows-metadata/dist/index.mjs b/packages/windows-metadata/dist/index.mjs index 06792e1..5b9d09a 100644 --- a/packages/windows-metadata/dist/index.mjs +++ b/packages/windows-metadata/dist/index.mjs @@ -13383,8 +13383,8 @@ var INPUT_SPECS = [ { name: "pkg-version", category: "build", - description: "npm version specifier for @yao-pkg/pkg (e.g. ~6.16.0). Bypassed when pkg-path is set.", - default: "~6.16.0" + description: "npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set.", + default: "~6.19.0" }, { name: "pkg-path", From 219740d1795725e46aa56b545b84889eb4445722 Mon Sep 17 00:00:00 2001 From: robertsLando Date: Fri, 24 Apr 2026 15:24:53 +0200 Subject: [PATCH 10/10] review: correct pkg-config keys, mask config-inline, extract materialize helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address four items from the self-review on PR #13: - README examples used `mode: "sea"` / `compressNode: "Brotli"` — neither exists in @yao-pkg/pkg's config schema. The authoritative FLAG_SPECS in lib/config.ts (and the docs-site configuration reference) show `sea: true` and `compress: "..."`. Fixed both examples so a copy-paste works. - `config-inline` carried user-authored JSON but was not masked. Mark the spec with `secret: true` so parseInputs registers the raw value via core.setSecret — exact-match redaction in logs costs nothing given pkg doesn't echo the config blob. README warning softened accordingly (still flags the on-disk temp file). - Extract the config-inline "bytes to disk" step out of the orchestrator into `materializePkgConfigInline` in fs-utils, alongside `PKG_CONFIG_INLINE_FILENAME`. Four new unit tests cover pass-through, both-undefined, inline-writes-and-returns-path, and inline-wins-over-config. Orchestrator is now a two-liner that just logs the resulting path. - BASE_BUILD fixture in pkg-runner.test.ts still pinned pkgVersion to the old `~6.16.0` default; bump to `~6.19.0` to match parseInputs. Regenerated action.yml + docs/inputs.md (secret flag on config-inline doesn't affect the YAML shape but the description changed). Rebuilt packages/{build,windows-metadata}/dist/index.mjs. Tests 231/231 pass (was 227/227), typecheck + lint + prettier green. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 18 +++---- action.yml | 2 +- docs/inputs.md | 2 +- packages/build/action.yml | 2 +- packages/build/dist/index.mjs | 22 ++++++--- packages/build/src/main.ts | 19 ++++---- packages/core/src/fs-utils.ts | 29 +++++++++++ packages/core/src/inputs.ts | 3 +- packages/core/test/unit/fs-utils.test.ts | 56 ++++++++++++++++++++++ packages/core/test/unit/pkg-runner.test.ts | 2 +- packages/windows-metadata/dist/index.mjs | 3 +- 11 files changed, 128 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 00284b4..8f46bf4 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,13 @@ the `pkg` field of `package.json`). See [yao-pkg/pkg's README](https://github.com/yao-pkg/pkg#config) for the full schema. -Example — SEA mode with Brotli-compressed Node + fallback, saved as `.pkgrc.json`: +Example — SEA mode with Brotli-compressed bundle + fallback, saved as `.pkgrc.json`: ```json { "bin": "src/main.js", - "mode": "sea", - "compressNode": "Brotli", + "sea": true, + "compress": "Brotli", "fallbackToSource": true } ``` @@ -72,14 +72,16 @@ it. Mutually exclusive with `config`. config-inline: | { "bin": "src/main.js", - "mode": "sea", - "compressNode": "Brotli" + "sea": true, + "compress": "Brotli" } ``` -> **Do not embed secrets in `config-inline`** — the value is written to disk -> and echoed into the build log. It is not registered with -> `core.setSecret`. +> The value is registered with `core.setSecret`, so exact matches are redacted +> from logs — but it is still written verbatim to a temp file on the runner. +> Prefer `config` (a committed file) for anything beyond a couple of knobs, +> and source long-lived secrets from GitHub Actions secrets / OIDC, not from +> `config-inline`. The action's inputs cover only concerns pkg config cannot express: matrix targets, pkg install version/path, archive format, filename template, diff --git a/action.yml b/action.yml index 6c8d981..e133786 100644 --- a/action.yml +++ b/action.yml @@ -12,7 +12,7 @@ inputs: config: description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline.' config-inline: - description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.' + description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs.' entry: description: 'Entry script when not specified in the config.' targets: diff --git a/docs/inputs.md b/docs/inputs.md index 7ad292c..a02efb2 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -9,7 +9,7 @@ Every `pkg-action` input, grouped by category. | Input | Default | Required | Secret | Description | | --- | --- | --- | --- | --- | | `config` | — | no | no | Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline. | -| `config-inline` | — | no | no | Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked. | +| `config-inline` | — | no | yes | Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs. | | `entry` | — | no | no | Entry script when not specified in the config. | | `targets` | — | no | no | Comma- or newline-separated pkg target triples, e.g. node22-linux-x64,node22-macos-arm64. Defaults to the host target. | | `pkg-version` | `~6.19.0` | no | no | npm version specifier for @yao-pkg/pkg (e.g. ~6.19.0). 6.19.0+ is required for the full build-flag surface in pkg config (compress, fallbackToSource, public, publicPackages, options, bytecode, nativeBuild, noDictionary, debug, signature). Bypassed when pkg-path is set. | diff --git a/packages/build/action.yml b/packages/build/action.yml index c9c92ad..751c1dd 100644 --- a/packages/build/action.yml +++ b/packages/build/action.yml @@ -9,7 +9,7 @@ inputs: config: description: 'Path to a pkg config (.pkgrc, pkg.config.{js,ts,json}, or package.json). Auto-detected when omitted. Mutually exclusive with config-inline.' config-inline: - description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.' + description: 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs.' entry: description: 'Entry script when not specified in the config.' targets: diff --git a/packages/build/dist/index.mjs b/packages/build/dist/index.mjs index a1a376c..3f22ce1 100644 --- a/packages/build/dist/index.mjs +++ b/packages/build/dist/index.mjs @@ -14497,7 +14497,7 @@ function getState(name) { } // packages/build/src/main.ts -import { mkdir as mkdir3, rename as rename3, stat as stat4, writeFile as writeFile5 } from "node:fs/promises"; +import { mkdir as mkdir3, rename as rename3, stat as stat4 } from "node:fs/promises"; import { tmpdir as tmpdir2 } from "node:os"; import { basename as pathBasename, dirname as dirname5, join as join7, resolve as pathResolve } from "node:path"; @@ -14552,6 +14552,12 @@ async function createInvocationTemp(parent) { let id = randomUUID2(), dir = join3(parent, `pkg-action-${id}`); return await mkdir2(dir, { recursive: !0, mode: 448 }), await chmod2(dir, 448), dir; } +var PKG_CONFIG_INLINE_FILENAME = "pkg-config.inline.json"; +async function materializePkgConfigInline(opts) { + if (opts.configInline === void 0) return opts.config; + let path4 = join3(opts.invocationDir, PKG_CONFIG_INLINE_FILENAME); + return await writeFile2(path4, opts.configInline, "utf8"), path4; +} async function atomicWriteFile(path4, data) { await mkdir2(dirname3(path4), { recursive: !0 }); let tmp = `${path4}.tmp-${randomUUID2()}`; @@ -14769,7 +14775,8 @@ var INPUT_SPECS = [ { name: "config-inline", category: "build", - description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets \u2014 this input is not masked." + description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs.", + secret: !0 }, { name: "entry", @@ -19259,11 +19266,12 @@ async function main() { saveState("invocationDir", invocationDir); let pkgOutputDir = join7(invocationDir, "pkg-out"); await mkdir3(pkgOutputDir, { recursive: !0 }); - let effectiveConfig = inputs.build.config; - if (inputs.build.configInline !== void 0) { - let inlinePath = join7(invocationDir, "pkg-config.inline.json"); - await writeFile5(inlinePath, inputs.build.configInline, "utf8"), effectiveConfig = inlinePath, logger.info(`[pkg-action] materialized config-inline \u2192 ${inlinePath}`); - } + let effectiveConfig = await materializePkgConfigInline({ + config: inputs.build.config, + configInline: inputs.build.configInline, + invocationDir + }); + inputs.build.configInline !== void 0 && effectiveConfig !== void 0 && logger.info(`[pkg-action] materialized config-inline \u2192 ${effectiveConfig}`); let pkgCommand = inputs.build.pkgPath ?? "pkg", cfgIsPackageJson = effectiveConfig !== void 0 && pathBasename(effectiveConfig).toLowerCase() === "package.json", pkgBuildInputs = { ...inputs.build, config: cfgIsPackageJson ? void 0 : effectiveConfig diff --git a/packages/build/src/main.ts b/packages/build/src/main.ts index c37e479..1c75898 100644 --- a/packages/build/src/main.ts +++ b/packages/build/src/main.ts @@ -25,7 +25,7 @@ // `checksums` outputs. import * as core from '@actions/core'; -import { mkdir, rename, stat, writeFile } from 'node:fs/promises'; +import { mkdir, rename, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename as pathBasename, dirname, join, resolve as pathResolve } from 'node:path'; import { @@ -38,6 +38,7 @@ import { formatTarget, hostTarget, mapPkgOutputs, + materializePkgConfigInline, parseInputs, parseSigningInputs, parseWindowsMetadataInputs, @@ -153,14 +154,14 @@ async function main(): Promise { // 4.5. Materialize `config-inline` to disk, if set. parseInputs already // validated it as a JSON object and enforced mutual exclusion with - // `config`, so this step just writes the bytes and threads the resulting - // path through as the effective config. - let effectiveConfig = inputs.build.config; - if (inputs.build.configInline !== undefined) { - const inlinePath = join(invocationDir, 'pkg-config.inline.json'); - await writeFile(inlinePath, inputs.build.configInline, 'utf8'); - effectiveConfig = inlinePath; - logger.info(`[pkg-action] materialized config-inline → ${inlinePath}`); + // `config`, so the helper just writes the bytes and returns the path. + const effectiveConfig = await materializePkgConfigInline({ + config: inputs.build.config, + configInline: inputs.build.configInline, + invocationDir, + }); + if (inputs.build.configInline !== undefined && effectiveConfig !== undefined) { + logger.info(`[pkg-action] materialized config-inline → ${effectiveConfig}`); } // 5. Run pkg from the project directory. diff --git a/packages/core/src/fs-utils.ts b/packages/core/src/fs-utils.ts index 9f85b6e..4aefda4 100644 --- a/packages/core/src/fs-utils.ts +++ b/packages/core/src/fs-utils.ts @@ -28,6 +28,35 @@ export async function createInvocationTemp(parent: string): Promise { return dir; } +/** + * Filename used by {@link materializePkgConfigInline}. Exposed as a constant so + * tests can assert the exact placement without re-stringifying it. + */ +export const PKG_CONFIG_INLINE_FILENAME = 'pkg-config.inline.json'; + +/** + * Resolve the effective `--config` path pkg will be invoked with. + * + * - If `configInline` is defined, write it to + * `/pkg-config.inline.json` and return that path. + * - Otherwise return `config` unchanged (may be undefined). + * + * `parseInputs` guarantees mutual exclusion + JSON-object shape, so this + * helper does no validation of its own — it is strictly the "bytes to disk" + * step. Kept here rather than in the orchestrator so it is unit-testable in + * isolation. + */ +export async function materializePkgConfigInline(opts: { + readonly config: string | undefined; + readonly configInline: string | undefined; + readonly invocationDir: string; +}): Promise { + if (opts.configInline === undefined) return opts.config; + const path = join(opts.invocationDir, PKG_CONFIG_INLINE_FILENAME); + await writeFile(path, opts.configInline, 'utf8'); + return path; +} + /** * Atomic write: write to .tmp- then rename over . Avoids * the window where a reader sees a half-written file. diff --git a/packages/core/src/inputs.ts b/packages/core/src/inputs.ts index 84d601e..d546ebb 100644 --- a/packages/core/src/inputs.ts +++ b/packages/core/src/inputs.ts @@ -42,7 +42,8 @@ export const INPUT_SPECS: readonly InputSpec[] = [ name: 'config-inline', category: 'build', description: - 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets — this input is not masked.', + 'Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs.', + secret: true, }, { name: 'entry', diff --git a/packages/core/test/unit/fs-utils.test.ts b/packages/core/test/unit/fs-utils.test.ts index c946401..c8333ba 100644 --- a/packages/core/test/unit/fs-utils.test.ts +++ b/packages/core/test/unit/fs-utils.test.ts @@ -7,6 +7,8 @@ import { atomicWriteFile, createInvocationTemp, exists, + materializePkgConfigInline, + PKG_CONFIG_INLINE_FILENAME, zeroFillAndRemove, } from '../../src/fs-utils.ts'; @@ -87,3 +89,57 @@ test('exists returns false for missing and true for present', async () => { strictEqual(await exists(join(parent, 'yep')), true); }); }); + +test('materializePkgConfigInline passes through config when inline is unset', async () => { + await withTempParent(async (parent) => { + const out = await materializePkgConfigInline({ + config: '.pkgrc.json', + configInline: undefined, + invocationDir: parent, + }); + strictEqual(out, '.pkgrc.json'); + // Nothing should have been written. + strictEqual(await exists(join(parent, PKG_CONFIG_INLINE_FILENAME)), false); + }); +}); + +test('materializePkgConfigInline returns undefined when neither is set', async () => { + await withTempParent(async (parent) => { + const out = await materializePkgConfigInline({ + config: undefined, + configInline: undefined, + invocationDir: parent, + }); + strictEqual(out, undefined); + strictEqual(await exists(join(parent, PKG_CONFIG_INLINE_FILENAME)), false); + }); +}); + +test('materializePkgConfigInline writes inline JSON and returns its path', async () => { + await withTempParent(async (parent) => { + const payload = '{"bin":"src/main.js","sea":true,"compress":"Brotli"}'; + const out = await materializePkgConfigInline({ + config: undefined, + configInline: payload, + invocationDir: parent, + }); + const expected = join(parent, PKG_CONFIG_INLINE_FILENAME); + strictEqual(out, expected); + strictEqual(await readFile(expected, 'utf8'), payload); + }); +}); + +test('materializePkgConfigInline prefers inline over config when inline is set', async () => { + // parseInputs enforces mutual exclusion, but the helper's contract is + // "inline wins" — guard against a future caller that accidentally passes + // both. + await withTempParent(async (parent) => { + const out = await materializePkgConfigInline({ + config: '.pkgrc.json', + configInline: '{"bin":"x.js"}', + invocationDir: parent, + }); + strictEqual(out, join(parent, PKG_CONFIG_INLINE_FILENAME)); + strictEqual(await readFile(out as string, 'utf8'), '{"bin":"x.js"}'); + }); +}); diff --git a/packages/core/test/unit/pkg-runner.test.ts b/packages/core/test/unit/pkg-runner.test.ts index b73da1e..51e2352 100644 --- a/packages/core/test/unit/pkg-runner.test.ts +++ b/packages/core/test/unit/pkg-runner.test.ts @@ -10,7 +10,7 @@ const BASE_BUILD: BuildInputs = { configInline: undefined, entry: undefined, targets: 'host', - pkgVersion: '~6.16.0', + pkgVersion: '~6.19.0', pkgPath: undefined, }; diff --git a/packages/windows-metadata/dist/index.mjs b/packages/windows-metadata/dist/index.mjs index 5b9d09a..05e12cb 100644 --- a/packages/windows-metadata/dist/index.mjs +++ b/packages/windows-metadata/dist/index.mjs @@ -13368,7 +13368,8 @@ var INPUT_SPECS = [ { name: "config-inline", category: "build", - description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Do not embed secrets \u2014 this input is not masked." + description: "Pkg config as a JSON string. Written to a temp file and passed to pkg via --config. Mutually exclusive with config. Registered with core.setSecret so exact matches are redacted from logs; still written to a temp file on the runner, so prefer config for anything beyond trivial knobs.", + secret: !0 }, { name: "entry",