Skip to content

fix(napi): prevent prepublish uploads to immutable releases - #83

Merged
robinbraemer merged 2 commits into
mainfrom
fm/akua-napi-prepublish-hardening
Jul 21, 2026
Merged

fix(napi): prevent prepublish uploads to immutable releases#83
robinbraemer merged 2 commits into
mainfrom
fm/akua-napi-prepublish-hardening

Conversation

@robinbraemer

Copy link
Copy Markdown
Member

Intent

Harden the Akua release-publish workflow so the @napi-rs/cli prePublish hook can never attempt to write to an immutable GitHub Release again (captain-approved follow-up to the 0.8.25 recovery, akua#71). During 0.8.25 the meta-package @akua-dev/native publish invoked napi prepublish, which tried to upload per-platform .node files as assets to the already-cut immutable Release via POST releases//assets; GitHub rejected each with HTTP 400 so the Release survived by luck, not by design. The intended fix was to add --ignore-scripts to the meta publish (mirroring sdk-publish), BUT I verified that would break the package: the published @akua-dev/native@0.8.25 carries 7 optionalDependencies that are absent from the committed package.json and are injected by the napi prepublish hook at publish time, so --ignore-scripts would ship a meta with no native binary and break installation. Instead, per the task's pre-authorized tightly-scoped alternative (a napi config option that disables ONLY the Release-write), I changed the prepublishOnly script in crates/akua-napi/package.json from 'napi prepublish -t npm' to 'napi prepublish -t npm --no-gh-release'. --gh-release defaults on in CI; --no-gh-release disables only the immutable-Release asset upload while preserving the essential optionalDependencies injection + addon copy. Added a guard comment in .github/workflows/release-publish.yml explaining why the meta publish must NOT use --ignore-scripts like sdk-publish, and a matching AGENTS.md release-section note so the hook is never reverted to --ignore-scripts. Scope is workflow/packaging hardening only: no version changes, no publishing, no Release/GHCR actions, and no publish workflow was dispatched (0.8.25 is already published; validation is by review/CI only).

What Changed

  • Run @akua-dev/native prepublish with --no-gh-release, preventing immutable GitHub Release asset uploads while preserving platform optionalDependencies generation.
  • Harden release workflow validation to require the safe hook and reject --ignore-scripts for native publishing.
  • Document why the native meta-package must keep lifecycle scripts enabled during releases.

Risk Assessment

✅ Low: The supported flag correctly prevents GitHub Release creation/uploads while preserving dependency injection; only a minor documentation inaccuracy remains.

Testing

After baseline change inspection, the focused red/green workflow guard, intercepted real prepublish comparison, packed-manifest verification, and clean consumer install/load all succeeded: the hardened hook made no GitHub API request, preserved seven optional native dependencies, and produced a loadable package; no workflow, publish, Release, or GHCR action was dispatched.

Evidence: Hardened-hook network evidence

Target: NO OUTBOUND FETCH ATTEMPT RECORDED

NO OUTBOUND FETCH ATTEMPT RECORDED
Evidence: Legacy-hook negative control

Legacy control: FETCH POST https://api.github.com/repos/akua-dev/akua/releases

FETCH POST https://api.github.com/repos/akua-dev/akua/releases
Evidence: Published meta-package manifest

Generated publish manifest containing all seven injected platform optionalDependencies.

{
  "name": "@akua-dev/native",
  "version": "0.8.8",
  "description": "Native Node.js + Bun + Deno addon for akua-core. Loaded by @akua-dev/sdk; not a public surface.",
  "main": "loader.js",
  "types": "index.d.ts",
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/akua-dev/akua.git"
  },
  "engines": {
    "node": ">= 22"
  },
  "files": [
    "index.d.ts",
    "index.js",
    "loader.js"
  ],
  "keywords": [
    "akua",
    "napi-rs",
    "NAPI",
    "node-addon",
    "helm",
    "kustomize",
    "kcl"
  ],
  "napi": {
    "binaryName": "akua",
    "packageName": "@akua-dev/native",
    "targets": [
      "x86_64-apple-darwin",
      "aarch64-apple-darwin",
      "x86_64-unknown-linux-gnu",
      "aarch64-unknown-linux-gnu",
      "x86_64-unknown-linux-musl",
      "aarch64-unknown-linux-musl",
      "x86_64-pc-windows-msvc"
    ]
  },
  "scripts": {
    "build": "napi build --platform --release",
    "build:debug": "napi build --platform",
    "prepublishOnly": "napi prepublish -t npm --no-gh-release",
    "artifacts": "napi artifacts",
    "version": "napi version"
  },
  "dependencies": {
    "@akua-dev/native-engines": "0.8.8"
  },
  "devDependencies": {
    "@napi-rs/cli": "^3.2.0"
  },
  "optionalDependencies": {
    "@akua-dev/native-darwin-x64": "0.8.8",
    "@akua-dev/native-darwin-arm64": "0.8.8",
    "@akua-dev/native-linux-x64-gnu": "0.8.8",
    "@akua-dev/native-linux-arm64-gnu": "0.8.8",
    "@akua-dev/native-linux-x64-musl": "0.8.8",
    "@akua-dev/native-linux-arm64-musl": "0.8.8",
    "@akua-dev/native-win32-x64-msvc": "0.8.8"
  }
}
Evidence: Packed native meta-package

Source: Packed native meta-package (local file: /var/folders/1y/cjgf53nj31n_dxsspqnjfjvc0000gn/T/no-mistakes-evidence/01KY1ZANZQ9WWWTCRE2H7E31BB/akua-dev-native-0.8.8.tgz)

Installable @akua-dev/native package generated by the hardened lifecycle.
Evidence: Consumer install/load smoke test

Clean installation selected @akua-dev/native-darwin-arm64 and successfully loaded the native addon exports.

{
  "loaded": true,
  "name": "@akua-dev/native",
  "version": "0.8.8",
  "optionalDependencyCount": 7,
  "installedPlatformPackages": [
    "@akua-dev/native-darwin-arm64"
  ],
  "exportSample": [
    "fmt",
    "lint",
    "check",
    "diff",
    "tree",
    "render",
    "export",
    "verify"
  ]
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ .github/workflows/release-publish.yml:280 - Correct the guard comment: napi prepublish does not copy addons; the earlier napi artifacts step does. The hook is load-bearing here for platform-version and optionalDependencies synchronization. The current wording misstates the release invariant. See the NAPI-RS prepublish documentation.
✅ **Test** - passed

✅ No issues found.

  • bash scripts/check-release-workflows.sh before and after adding the focused regression guard
  • bash scripts/check-release-workflows.sh after temporarily removing --no-gh-release (expected failure), then after restoration
  • bash scripts/check-release-workflows.sh after temporarily adding --ignore-scripts to the native meta publish (expected failure), then after restoration
  • bun install --frozen-lockfile in crates/akua-napi
  • CI=true GITHUB_ACTIONS=true GITHUB_REPOSITORY=akua-dev/akua ... npm run prepublishOnly with an outbound-fetch blocker/logger
  • CI=true GITHUB_ACTIONS=true GITHUB_REPOSITORY=akua-dev/akua ... bunx napi prepublish -t npm as the legacy negative control
  • npm pack --ignore-scripts --pack-destination <evidence-dir> --json and packed-manifest inspection
  • npm install --prefix <evidence-dir>/install-smoke <packed-tarball> --ignore-scripts --package-lock=false
  • node -e &#34;require(&#39;@akua-dev/native&#39;) ...&#34; in the clean consumer fixture
  • Fresh post-cleanup workflow-guard, network-log, packed-manifest, tarball-content, consumer-load, and worktree-residue checks
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The `native-publish` job's `@akua-dev/native` publish runs its
`prepublishOnly` hook (`napi prepublish`), which — beyond injecting the
per-platform `optionalDependencies` and copying addons — attempted to
upload the per-platform `.node` files as assets to the already-cut,
immutable GitHub Release (`POST releases/<id>/assets`). During the
0.8.25 recovery GitHub rejected each upload with HTTP 400, so the
Release survived intact — but it failed safe by luck, not by design.

Disable only the Release-write at its source with `--no-gh-release` in
the `prepublishOnly` script. This keeps the essential
optionalDependencies injection (the published meta's 7 optionalDeps do
not exist in the committed package.json, so `--ignore-scripts` — the
sdk-publish pattern — would ship a meta with no native binary and break
installation). Add a guard comment in the workflow and an AGENTS.md note
so the hook is never "fixed" back to `--ignore-scripts`.
@robinbraemer
robinbraemer merged commit 1b87537 into main Jul 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant