Skip to content

ci: port release shell scripts to Node ESM#46

Draft
kjanat wants to merge 1 commit into
masterfrom
ci/javascript
Draft

ci: port release shell scripts to Node ESM#46
kjanat wants to merge 1 commit into
masterfrom
ci/javascript

Conversation

@kjanat

@kjanat kjanat commented Jun 11, 2026

Copy link
Copy Markdown
Owner

What

Ports the .github/scripts release tooling from shell to Node ESM. Seven
.sh scripts become nine actions/github-script modules (.mjs), invoked
from the workflows via dynamic import():

area modules
build download-release-archives, man-archive, npm, package-release-asset, verify-checksum
publish aur, npm
release notes, publish

Each module is @ts-check'd with JSDoc types against @actions/github-script
and receives the injected core / github / context / exec, so the
scripts carry no runtime dependencies. Also adds .node-version, the
@actions/github-script / @types/node / typescript devDeps, and the
supporting package.json / tsconfig.json / bun.lock / .gitignore
updates.

Verified

  • All 9 .mjs pass node --check.
  • Every workflow import(...) resolves to a real export (12/12 checked).
  • No dangling .sh references remain in any workflow.

⚠️ Not mergeable as-is — integration needed before merge

This branch was authored against the workflow_dispatch release design
and is 5 commits behind master, which has since moved release chaining to
workflow_call (58ace23, 84a987c). The two designs collide, so this
PR will show conflicts on the 4 workflow files and the 2 .sh files master
modified.

Reconciling onto master's workflow_call architecture means:

  • Drop the dispatch layer: release/publish.mjs's dispatchPackagePublishes
    is obsolete (master invokes publishers via uses:, not API dispatch).
  • Trim publish/npm.mjs: resolveReleaseRun + waitForReleaseRun
    (~80 lines of poll-and-wait) are dead under workflow_call, where the
    caller passes run-id and the build is already complete.
  • Reusable as-is: the build-side modules (download-release-archives,
    man-archive, npm (build), package-release-asset, verify-checksum,
    notes, aur, and npm.mjs's deriveNpmPublishSettings /
    publishNpmPackages core) are orchestration-agnostic.

Open decisions for the integration

  1. Keep master's workflow_call orchestration (assumed yes).
  2. Crates trigger model: master chains crates via workflow_call (after the
    full build); this migration self-triggers crates on push: tags. Different
    ordering/failure semantics — pick one.
  3. Whether to retain any run-id fallback for the secondary release:[published]
    trigger path, or delete the wait loop outright.

Kept as a draft until the above is resolved.

Replace the seven `.github/scripts` shell scripts with nine
`actions/github-script` ESM modules (`.mjs`), invoked from the workflows
via dynamic `import()`. Each module is `@ts-check`'d with JSDoc types
against `@actions/github-script` and receives the injected
`core`/`github`/`context`/`exec`, so the scripts carry no runtime
dependencies. Adds `.node-version`, the `@actions/github-script` /
`@types/node` / `typescript` devDeps, and the supporting
`package.json` / `tsconfig.json` / `bun.lock` / `.gitignore` updates.

Built on the `workflow_dispatch` release design (release.yml dispatches
npm/aur publishes and the publishers poll for the build run). This
predates master's `workflow_call` chaining; reconciling the two designs
is tracked as integration work.
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bb557ee1-b45f-4af5-8b14-a8a93ad558a0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch ci/javascript

Comment @coderabbitai help to get the list of available commands and usage tips.

@kjanat kjanat self-assigned this Jun 11, 2026
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/npm-release.yml (1)

45-50: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Arrr! Ye be missin' the .node-version file from yer sparse-checkout, matey!

The setup-node step references node-version-file: .node-version, but the sparse-checkout patterns don't include this file. This'll cause the step to either fail or fall back to some unexpected Node version, sinkin' yer entire publish voyage!

🏴‍☠️ Proposed fix to add the missing file
       with:
         ref: ${{ github.event.repository.default_branch }}
         persist-credentials: false
         sparse-checkout: |
           .github/scripts
           npm/targets.json
+          .node-version
         sparse-checkout-cone-mode: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/npm-release.yml around lines 45 - 50, The sparse-checkout
block is missing the .node-version file referenced by the setup-node step
(node-version-file: .node-version), so update the sparse-checkout patterns under
sparse-checkout to include ".node-version" (alongside .github/scripts and
npm/targets.json) so the setup-node step can read the file and use the intended
Node version; locate the sparse-checkout section in the workflow and add the
.node-version entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/build/man-archive.mjs:
- Around line 20-24: Validate and sanitize environment inputs before using them
to build filenames: check env.RELEASE_TAG (releaseTag) with a strict semver
validator (e.g. semver.valid(...) or an equivalent regex) and call fail(core,
"...") if invalid, and for env.TARGET ensure it contains no path separators (no
'/' or '\\') before using it; apply the same validation pattern to
package-release-asset.mjs as well so both scripts reject traversal characters
and only accept safe, validated values.
- Line 29: The tar invocation using exec.exec("tar", ["-C", "man", "-czf",
archive, "."]) runs without ensuring the man/ directory exists; add a defensive
check (e.g., fs.existsSync or fs.promises.stat) for the "man" directory before
calling exec.exec and call fail(...) with a clear error message if it is missing
so tar doesn't produce a cryptic error. Locate the call to exec.exec in
.github/scripts/build/man-archive.mjs and gate it behind the existence check
referencing the same archive variable and preserve existing error handling flow.

In @.github/scripts/build/package-release-asset.mjs:
- Around line 68-73: The catch is swallowing all copyFile errors and always
reporting "not found"; update the try/catch around the await copyFile(src,
join(staging, bin)) to capture the thrown error (e.g., `err`), check err.code
and if err.code === 'ENOENT' call fail(core, `${src} not found — build step did
not produce ${bin}`), otherwise call fail(core, `${src} copy failed:
${err.message}`) (or include err) so permission, disk, and other I/O errors are
reported accurately; keep the existing return behavior after failing.

In @.github/scripts/build/verify-checksum.mjs:
- Line 68: The current code builds the SHA256 digest by reading the entire file
into memory via readFile; change this to stream-based hashing: create a read
stream from join(dir, expected) (using createReadStream), pipe it into
createHash('sha256') and await the stream completion (using stream.pipeline or
stream/promises.pipeline), then call hash.digest('hex') to produce digest;
update imports to include createReadStream and pipeline if needed and replace
the existing await readFile(...) usage that assigns to digest.

In @.github/scripts/release/notes.mjs:
- Around line 26-31: The current assembly of body using release.data.body and
generated.data.body can duplicate generated notes when the job reruns; update
the logic that builds body (the body constant) to first detect and remove any
previously appended generated.data.body from release.data.body (or check if
release.data.body already includes generated.data.body) before joining, so that
joining release.data.body and generated.data.body is idempotent and does not
produce duplicated content.

In @.github/workflows/crates-release.yml:
- Around line 11-12: The workflow-level permissions block currently grants
id-token: write to every job (permissions: { contents: read, id-token: write });
move the id-token: write permission out of the top-level permissions and add it
only to the job that runs the crates-io-auth-action / publishes to crates.io
(the job that performs the release/publish step), keeping contents: read at
workflow level if needed or scoping it to jobs that require it; update the job's
permissions block to include id-token: write and remove id-token from the global
permissions to enforce least privilege.

In @.github/workflows/release.yml:
- Around line 228-243: The workflow step that dynamically imports
publishGithubRelease and dispatchPackagePublishes from "${{ github.workspace
}}/.github/scripts/release/publish.mjs" can fail because the repository files
are not checked out; add a checkout step (e.g., actions/checkout) before the
actions/github-script steps so that the workspace contains the
.github/scripts/release/publish.mjs file and the imports succeed; ensure the
checkout step appears prior to the steps invoking publishGithubRelease and
dispatchPackagePublishes.

In @.gitignore:
- Around line 17-19: The current unignore line "!.cargo" exposes the entire
.cargo tree (risking token/credential commits); remove or stop unignoring the
whole directory and instead explicitly unignore only safe artifacts while adding
explicit ignore patterns for credentials. Update the .gitignore by removing or
replacing the "!.cargo" entry and add explicit ignores such as
".cargo/credentials", ".cargo/credentials.toml", ".cargo/*.token",
".cargo/*.key", and any other credential filename patterns your project uses; if
you need to unignore specific safe files, unignore those exact filenames or
patterns rather than the entire .cargo directory.

In `@package.json`:
- Line 13: The dependency pin for "`@actions/github-script`" in package.json is
referencing a non-existent commit SHA; update the dependency spec for
"`@actions/github-script`" to a valid reference (either a published npm version
like a release tag e.g. "github-script@^6.1.0" or a real commit/annotated tag
from the actions/github-script repo) so installations and audits succeed; locate
the "`@actions/github-script`" entry in package.json and replace the fake
"github:actions/github-script#373c709c69115d41ff229c7e5df9f8788daa9553" value
with a valid tag/commit or semver release.

---

Outside diff comments:
In @.github/workflows/npm-release.yml:
- Around line 45-50: The sparse-checkout block is missing the .node-version file
referenced by the setup-node step (node-version-file: .node-version), so update
the sparse-checkout patterns under sparse-checkout to include ".node-version"
(alongside .github/scripts and npm/targets.json) so the setup-node step can read
the file and use the intended Node version; locate the sparse-checkout section
in the workflow and add the .node-version entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68be6ee7-911f-4b57-bb34-9671114ada59

📥 Commits

Reviewing files that changed from the base of the PR and between 28e499f and 91a4d64.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .github/scripts/build/build-npm-packages.sh
  • .github/scripts/build/derive-dist-dry.sh
  • .github/scripts/build/download-release-archives.mjs
  • .github/scripts/build/download-release-archives.sh
  • .github/scripts/build/man-archive.mjs
  • .github/scripts/build/npm.mjs
  • .github/scripts/build/package-release-asset.mjs
  • .github/scripts/build/package-release-asset.sh
  • .github/scripts/build/verify-checksum.mjs
  • .github/scripts/build/verify-checksum.sh
  • .github/scripts/publish/aur-prepare.sh
  • .github/scripts/publish/aur.mjs
  • .github/scripts/publish/npm.mjs
  • .github/scripts/publish/npm.sh
  • .github/scripts/release/notes.mjs
  • .github/scripts/release/publish.mjs
  • .github/workflows/aur-release.yml
  • .github/workflows/crates-release.yml
  • .github/workflows/npm-release.yml
  • .github/workflows/release.yml
  • .gitignore
  • .node-version
  • package.json
  • tsconfig.json
💤 Files with no reviewable changes (7)
  • .github/scripts/publish/aur-prepare.sh
  • .github/scripts/build/download-release-archives.sh
  • .github/scripts/build/derive-dist-dry.sh
  • .github/scripts/build/package-release-asset.sh
  • .github/scripts/build/build-npm-packages.sh
  • .github/scripts/build/verify-checksum.sh
  • .github/scripts/publish/npm.sh
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
@(package.json|pyproject.toml|setup.py|Cargo.toml|go.mod|pom.xml|build.gradle|VERSION)

📄 CodeRabbit inference engine (Custom checks)

@(package.json|pyproject.toml|setup.py|Cargo.toml|go.mod|pom.xml|build.gradle|VERSION): If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, then a version field MUST be updated in one of the following files if present in the repo: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or a VERSION file.
The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment.

Files:

  • package.json
.github/**/*.{yml,yaml}

⚙️ CodeRabbit configuration file

Do not ever warn about stylistic yamllint shit. Do warn about security related shit. Insecure shell handling of user supplied / defined strings: think of branch names, inputs, pr content, anything needs to be string interpolation and permission safe.

Files:

  • .github/workflows/crates-release.yml
  • .github/workflows/aur-release.yml
  • .github/workflows/npm-release.yml
  • .github/workflows/release.yml
🧠 Learnings (5)
📚 Learning: 2026-03-26T20:08:23.153Z
Learnt from: kjanat
Repo: kjanat/runner PR: 1
File: src/detect.rs:22-22
Timestamp: 2026-03-26T20:08:23.153Z
Learning: In `src/detect.rs` within the `kjanat/runner` codebase, `detect()` is intentionally infallible and should return `ProjectContext` directly. Parsing/I/O failures from task extractors (which return `anyhow::Result<Vec<String>>`) must be recorded by `push_extracted_tasks` into `ProjectContext.warnings` as `DetectionWarning` entries and then emitted to stderr via `cmd::print_warnings`. When reviewing, do not flag `detect()` for “swallowing errors” as long as failures are routed into the warnings-list design (i.e., they’re preserved in `ProjectContext.warnings`, not dropped silently).

Applied to files:

  • .node-version
  • .gitignore
📚 Learning: 2026-05-04T22:42:28.879Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: CHANGELOG.md:8-27
Timestamp: 2026-05-04T22:42:28.879Z
Learning: In `CHANGELOG.md`, do not treat a `## [Unreleased]` section as an error/mismatch during development PRs even if `Cargo.toml` includes an in-progress version bump. The versioned header format `## [X.Y.Z] - YYYY-MM-DD` should only be added at release/tag time; this repo’s `CHANGELOG.md` includes a “Post-release checklist” confirming that behavior. Only flag changelog/version mismatches if they contradict the intended workflow described in `CHANGELOG.md`.

Applied to files:

  • .node-version
  • .gitignore
📚 Learning: 2026-05-14T15:04:39.496Z
Learnt from: kjanat
Repo: kjanat/runner PR: 27
File: src/cli.rs:780-788
Timestamp: 2026-05-14T15:04:39.496Z
Learning: In `src/cli.rs`, do not recommend removing `trailing_var_arg = true` or `allow_hyphen_values = true` from `Command::Run.args` and `RunAliasCli.args`. They are intentionally required for the documented pass-through contract: hyphen-prefixed args (e.g., `--watch`) that appear after a task name must be forwarded to the underlying task. As a result, in chain mode, chain-failure flags like `-k`/`--kill-on-fail` that are placed after task names will be silently consumed as task args rather than parsed as flags; when proposing fixes or usage changes, use the documented workaround of placing all chain flags before task names (e.g., `runner run -s -k build test`). This behavior is verified in `src/lib.rs` (`run_alias_forwards_trailing_args`) and described in the README, so keep this design trade-off intact.

Applied to files:

  • .node-version
  • .gitignore
📚 Learning: 2026-05-02T17:50:01.271Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: .gitignore:4-4
Timestamp: 2026-05-02T17:50:01.271Z
Learning: In this repository, `package-lock.json` is intentionally added to `.gitignore` and should not be committed. In future reviews, do not flag the absence/presence of `package-lock.json` as an issue as long as `.gitignore` continues to exclude it intentionally.

Applied to files:

  • .gitignore
📚 Learning: 2026-05-04T15:23:38.296Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: .github/workflows/release.yml:73-73
Timestamp: 2026-05-04T15:23:38.296Z
Learning: For GitHub Actions workflows in this repository (kjanat/runner), maintain the maintainer’s preference for “floating” action tags (e.g., `v6`, `v7`) instead of pinned full commit SHAs. During code review, do not flag action usages in `.github/workflows/**/*.yml` for not being pinned to full commit SHAs or suggest switching to SHA pinning.

Applied to files:

  • .github/workflows/crates-release.yml
  • .github/workflows/aur-release.yml
  • .github/workflows/npm-release.yml
  • .github/workflows/release.yml
🪛 zizmor (1.25.2)
.github/workflows/crates-release.yml

[error] 11-11: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)


[warning] 11-11: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

.github/workflows/aur-release.yml

[warning] 33-33: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

.github/workflows/npm-release.yml

[warning] 36-36: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)


[warning] 60-60: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 72-72: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 83-83: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

.github/workflows/release.yml

[warning] 42-42: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 62-62: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 160-160: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 183-183: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 194-194: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 201-201: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 234-234: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 242-242: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 227-227: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

🔇 Additional comments (29)
.github/scripts/publish/npm.mjs (7)

1-21: LGTM!


22-47: LGTM!


49-88: LGTM!


90-190: LGTM!


192-301: LGTM!


303-387: LGTM!


389-443: LGTM!

.github/workflows/npm-release.yml (2)

1-28: LGTM!


52-84: LGTM!

.github/workflows/crates-release.yml (1)

22-22: LGTM!

.github/workflows/aur-release.yml (4)

28-34: Ahoy, the static analysis tool be raisin' false alarms about template injection, but this be safe waters!

Zizmor flagged ${{ github.workspace }} as potentially attacker-controllable, but it be a runner-provided context value pointing to the checkout directory — not user input. The checkout step (line 26) pulls from default_branch, so the imported script comes from trusted code. This pattern matches the other workflows in yer fleet (e.g., release.yml line 38).

No action needed, Captain. The warning can be safely ignored.

Source: Linters/SAST tools


1-12: LGTM!


14-26: LGTM!


36-48: LGTM!

.github/scripts/publish/aur.mjs (8)

1-13: LGTM!


15-43: LGTM!


45-51: LGTM!


53-96: LGTM!


98-103: LGTM!


105-128: LGTM!


130-141: LGTM!


143-166: LGTM!

.github/scripts/release/publish.mjs (1)

35-53: Dispatch orchestration isn’t the workflow_call thing you fear: both .github/workflows/npm-release.yml and .github/workflows/aur-release.yml still declare on: workflow_dispatch. Arrr—so this won’t hard-fail just because it’s createWorkflowDispatch.

But ye’re passing "run-id": String(context.runId) for npm-release.yml; double-check that npm-release.yml actually defines a run-id input under its workflow_dispatch—otherwise the dispatch can still be rejected.

.node-version (1)

1-1: LGTM!

.github/scripts/build/package-release-asset.mjs (1)

59-89: Shiver me timbers! Ye forgot to set permissions on the copied files before tarballing! ⚓

Wait... no, I be seein' it now at line 74. Ye DO call chmod(..., 0o755) on each binary after copyin'. That be shipshape! Carry on, sailor! 🏴‍☠️✨

package.json (1)

18-19: Avast! Node 26.3.0 and npm 11.16.0 actually exist—now check security + “Current” vs LTS

		"node": "26.3.0",
		"npm": "11.16.0"

Ye’re not bumpin’ into imaginary versions: Node.js 26.3.0 and npm 11.16.0 are real releases. Also, Node 26.3.0 is “Current”, not LTS—if ye need LTS stability, don’t sail with “Current”.
Still run/confirm vulnerability impact for these exact engine versions (e.g., via npm audit / advisory checks) before merging.

tsconfig.json (1)

26-26: LGTM!

.github/scripts/build/download-release-archives.mjs (1)

24-54: LGTM!

Also applies to: 60-63

.github/scripts/build/npm.mjs (1)

24-24: 🏴‍☠️ Direct .ts execution is the intended way to run build-packages.ts under the pinned Node
npm/scripts/build-packages.ts explicitly documents being run as node npm/scripts/build-packages.ts ..., and .github/scripts/build/npm.mjs does exactly that (exec.exec("node", ["npm/scripts/build-packages.ts", ...])). With .node-version pinned to v26.3.0, the real failure risk is only if the pinned Node/runtime behaviour changes (e.g., native TS type-stripping disabled).

Comment on lines +20 to +24
const releaseTag = env.RELEASE_TAG;
if (!releaseTag) {
fail(core, "RELEASE_TAG env var is required");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Arrr! Both scripts be vulnerable to the same path traversal scurvy! 🏴‍☠️

Both man-archive.mjs and package-release-asset.mjs accept RELEASE_TAG (and package-release-asset.mjs also accepts TARGET) from environment variables and immediately use them in filename construction without validation. A malicious tag like ../../etc/passwd or v1.0/../evil could write files outside the intended directory. Apply consistent semver validation to RELEASE_TAG in both scripts, and validate that TARGET contains no path separators.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/build/man-archive.mjs around lines 20 - 24, Validate and
sanitize environment inputs before using them to build filenames: check
env.RELEASE_TAG (releaseTag) with a strict semver validator (e.g.
semver.valid(...) or an equivalent regex) and call fail(core, "...") if invalid,
and for env.TARGET ensure it contains no path separators (no '/' or '\\') before
using it; apply the same validation pattern to package-release-asset.mjs as well
so both scripts reject traversal characters and only accept safe, validated
values.

const archive = `runner-${releaseTag}-man.tar.gz`;
const checksum = `runner-${releaseTag}-man.sha256`;

await exec.exec("tar", ["-C", "man", "-czf", archive, "."]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Arrr, this tar command be lookin' mighty suspicious! 🏴‍☠️

Ye be usin' tar -C man -czf with a hardcoded "." as the source, but there be no check that the man/ directory actually exists before running tar! If the directory be missing, tar will fail with a cryptic error instead of yer nice fail() message.

🗺️ Defensive check for man/ directory
+	import { access } from "node:fs/promises";
+	import { constants } from "node:fs";
+
 	const archive = `runner-${releaseTag}-man.tar.gz`;
 	const checksum = `runner-${releaseTag}-man.sha256`;
 
+	try {
+		await access("man", constants.R_OK);
+	} catch {
+		fail(core, "man/ directory not found — man pages must be built first");
+		return;
+	}
+
 	await exec.exec("tar", ["-C", "man", "-czf", archive, "."]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/build/man-archive.mjs at line 29, The tar invocation using
exec.exec("tar", ["-C", "man", "-czf", archive, "."]) runs without ensuring the
man/ directory exists; add a defensive check (e.g., fs.existsSync or
fs.promises.stat) for the "man" directory before calling exec.exec and call
fail(...) with a clear error message if it is missing so tar doesn't produce a
cryptic error. Locate the call to exec.exec in
.github/scripts/build/man-archive.mjs and gate it behind the existence check
referencing the same archive variable and preserve existing error handling flow.

Comment on lines +68 to +73
try {
await copyFile(src, join(staging, bin));
} catch {
fail(core, `${src} not found — build step did not produce ${bin}`);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Nya~ The error handling here is too vague, onii-chan! 💢

The empty catch block swallows ALL errors from copyFile() — not just ENOENT (file not found), but also EACCES (permission denied), ENOSPC (disk full), and other I/O errors. The generic "not found" message will be confusing if the real problem is permissions or disk space.

🎀 More specific error handling
 		for (const bin of ["runner", "run"]) {
 			const src = join(binDir, bin);
 			try {
 				await copyFile(src, join(staging, bin));
-			} catch {
-				fail(core, `${src} not found — build step did not produce ${bin}`);
+			} catch (err) {
+				const reason = err.code === 'ENOENT' 
+					? 'not found — build step did not produce'
+					: `failed to copy: ${err.message}`;
+				fail(core, `${src} ${reason} ${bin}`);
 				return;
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await copyFile(src, join(staging, bin));
} catch {
fail(core, `${src} not found — build step did not produce ${bin}`);
return;
}
try {
await copyFile(src, join(staging, bin));
} catch (err) {
const reason = err.code === 'ENOENT'
? 'not found — build step did not produce'
: `failed to copy: ${err.message}`;
fail(core, `${src} ${reason} ${bin}`);
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/build/package-release-asset.mjs around lines 68 - 73, The
catch is swallowing all copyFile errors and always reporting "not found"; update
the try/catch around the await copyFile(src, join(staging, bin)) to capture the
thrown error (e.g., `err`), check err.code and if err.code === 'ENOENT' call
fail(core, `${src} not found — build step did not produce ${bin}`), otherwise
call fail(core, `${src} copy failed: ${err.message}`) (or include err) so
permission, disk, and other I/O errors are reported accurately; keep the
existing return behavior after failing.

return;
}

const digest = createHash("sha256").update(await readFile(join(dir, expected))).digest("hex");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Hash tarballs via stream, not full-buffer reads.

Line 68 loads each archive fully into memory before hashing. Streaming gives the same result with far steadier memory usage on larger assets.

Suggested refactor
 import { createHash } from "node:crypto";
+import { createReadStream } from "node:fs";
 import { readdir, readFile } from "node:fs/promises";
 import { join } from "node:path";
 import { cwd, env } from "node:process";
@@
-		const digest = createHash("sha256").update(await readFile(join(dir, expected))).digest("hex");
+		const digest = await sha256File(join(dir, expected));
 		if (digest !== parsed.hex) {
 			fail(core, `checksum mismatch for ${expected}: expected ${parsed.hex}, got ${digest}`);
 			return;
 		}
 	}
@@
 }
+
+/**
+ * `@param` {string} filePath
+ * `@returns` {Promise<string>}
+ */
+async function sha256File(filePath) {
+	const hash = createHash("sha256");
+	await new Promise((resolve, reject) => {
+		const stream = createReadStream(filePath);
+		stream.on("error", reject);
+		stream.on("data", (chunk) => hash.update(chunk));
+		stream.on("end", resolve);
+	});
+	return hash.digest("hex");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/build/verify-checksum.mjs at line 68, The current code
builds the SHA256 digest by reading the entire file into memory via readFile;
change this to stream-based hashing: create a read stream from join(dir,
expected) (using createReadStream), pipe it into createHash('sha256') and await
the stream completion (using stream.pipeline or stream/promises.pipeline), then
call hash.digest('hex') to produce digest; update imports to include
createReadStream and pipeline if needed and replace the existing await
readFile(...) usage that assigns to digest.

Comment on lines +26 to +31
const body = `${
[release.data.body, generated.data.body]
.map((part) => (part ?? "").trim())
.filter(Boolean)
.join("\n\n")
}\n`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Release-notes merge is not idempotent.

If this job is re-run for the same tag, release.data.body already contains prior generated notes, so this logic appends them again and duplicates content.

Suggested fix
-	const body = `${
-		[release.data.body, generated.data.body]
-			.map((part) => (part ?? "").trim())
-			.filter(Boolean)
-			.join("\n\n")
-	}\n`;
+	const existing = (release.data.body ?? "").trim();
+	const generatedBody = (generated.data.body ?? "").trim();
+	const merged =
+		existing && generatedBody && existing.includes(generatedBody)
+			? existing
+			: [existing, generatedBody].filter(Boolean).join("\n\n");
+	const body = `${merged}\n`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = `${
[release.data.body, generated.data.body]
.map((part) => (part ?? "").trim())
.filter(Boolean)
.join("\n\n")
}\n`;
const existing = (release.data.body ?? "").trim();
const generatedBody = (generated.data.body ?? "").trim();
const merged =
existing && generatedBody && existing.includes(generatedBody)
? existing
: [existing, generatedBody].filter(Boolean).join("\n\n");
const body = `${merged}\n`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/release/notes.mjs around lines 26 - 31, The current assembly
of body using release.data.body and generated.data.body can duplicate generated
notes when the job reruns; update the logic that builds body (the body constant)
to first detect and remove any previously appended generated.data.body from
release.data.body (or check if release.data.body already includes
generated.data.body) before joining, so that joining release.data.body and
generated.data.body is idempotent and does not produce duplicated content.

Comment on lines +11 to +12
permissions: { contents: read, id-token: write }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Arrr, ye scallywag! These permissions be sittin' at the wrong deck level! 🏴‍☠️

The id-token: write permission be legitimate treasure for the trusted publishin' with crates-io-auth-action, aye. But declarin' it at workflow level means every job in this vessel gets it, even if ye add more crew (jobs) later. The static analysis tool be right to hoist this flag.

Yer other release workflow (npm-release.yml) scopes permissions to the job level — be consistent with yer fleet, matey! With only one job currently, it's functionally the same, but job-level scopin' follows the principle of least privilege and prevents future mishaps.

🔒 Proposed fix to scope permissions to job level
-permissions: { contents: read, id-token: write }
-
 env:
   RELEASE_TAG: ${{ case(github.event_name == 'workflow_dispatch', inputs.tag, github.ref_name) }}
   INPUT_DRY_RUN: ${{ inputs.dry-run || 'false' }}
@@ jobs:
   publish:
     name: publish to crates.io
     runs-on: ubuntu-latest
+    # id-token: write required for rust-lang/crates-io-auth-action trusted publishing
+    permissions: { contents: read, id-token: write }
     environment: { name: crates-io, url: "https://crates.io/crates/runner-run/${{ steps.verify.outputs.version }}" }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions: { contents: read, id-token: write }
env:
RELEASE_TAG: ${{ case(github.event_name == 'workflow_dispatch', inputs.tag, github.ref_name) }}
INPUT_DRY_RUN: ${{ inputs.dry-run || 'false' }}
jobs:
publish:
name: publish to crates.io
runs-on: ubuntu-latest
# id-token: write required for rust-lang/crates-io-auth-action trusted publishing
permissions: { contents: read, id-token: write }
environment: { name: crates-io, url: "https://crates.io/crates/runner-run/${{ steps.verify.outputs.version }}" }
🧰 Tools
🪛 zizmor (1.25.2)

[error] 11-11: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)


[warning] 11-11: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/crates-release.yml around lines 11 - 12, The
workflow-level permissions block currently grants id-token: write to every job
(permissions: { contents: read, id-token: write }); move the id-token: write
permission out of the top-level permissions and add it only to the job that runs
the crates-io-auth-action / publishes to crates.io (the job that performs the
release/publish step), keeping contents: read at workflow level if needed or
scoping it to jobs that require it; update the job's permissions block to
include id-token: write and remove id-token from the global permissions to
enforce least privilege.

Source: Linters/SAST tools

Comment on lines +228 to +243
steps:
- name: publish github release
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
retries: 3
script: |
const { publishGithubRelease } = await import("${{ github.workspace }}/.github/scripts/release/publish.mjs");
await publishGithubRelease({ core, github, context });

- name: dispatch package publishes
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
retries: 3
script: |
const { dispatchPackagePublishes } = await import("${{ github.workspace }}/.github/scripts/release/publish.mjs");
await dispatchPackagePublishes({ core, github, context });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

publish-release imports local scripts without checking out the repository.

This job dynamically imports files from ${{ github.workspace }}, but there is no checkout step in this job. The import will fail when the workspace does not contain repo files.

Suggested fix
   publish-release:
     name: publish github release
     needs: [build-npm-dist]
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
     runs-on: ubuntu-latest
     permissions: { actions: write, contents: write }
     steps:
+      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+        with: { persist-credentials: false }
+
       - name: publish github release
         uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
         with:
           retries: 3
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 234-234: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 242-242: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 228 - 243, The workflow step that
dynamically imports publishGithubRelease and dispatchPackagePublishes from "${{
github.workspace }}/.github/scripts/release/publish.mjs" can fail because the
repository files are not checked out; add a checkout step (e.g.,
actions/checkout) before the actions/github-script steps so that the workspace
contains the .github/scripts/release/publish.mjs file and the imports succeed;
ensure the checkout step appears prior to the steps invoking
publishGithubRelease and dispatchPackagePublishes.

Comment thread .gitignore
Comment on lines +17 to +19
# build scripts
!.github/scripts/build
!.cargo

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block accidental Cargo credential commits.

Line 19 (!.cargo) re-exposes the whole .cargo tree; that can include token-bearing credentials files. Keep the directory unignored, but explicitly re-ignore credential files, matey.

Suggested fix
 # build scripts
 !.github/scripts/build
 !.cargo
+.cargo/credentials
+.cargo/credentials.toml
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# build scripts
!.github/scripts/build
!.cargo
# build scripts
!.github/scripts/build
!.cargo
.cargo/credentials
.cargo/credentials.toml
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 17 - 19, The current unignore line "!.cargo" exposes
the entire .cargo tree (risking token/credential commits); remove or stop
unignoring the whole directory and instead explicitly unignore only safe
artifacts while adding explicit ignore patterns for credentials. Update the
.gitignore by removing or replacing the "!.cargo" entry and add explicit ignores
such as ".cargo/credentials", ".cargo/credentials.toml", ".cargo/*.token",
".cargo/*.key", and any other credential filename patterns your project uses; if
you need to unignore specific safe files, unignore those exact filenames or
patterns rather than the entire .cargo directory.

Comment thread package.json
"fmt": "bunx dprint fmt"
},
"devDependencies": {
"@actions/github-script": "github:actions/github-script#373c709c69115d41ff229c7e5df9f8788daa9553",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the `@actions/github-script` commit exists and identify its version

# Check if commit exists and get its details
gh api repos/actions/github-script/commits/373c709c69115d41ff229c7e5df9f8788daa9553 --jq '{sha: .sha, message: .commit.message, author: .commit.author.name, date: .commit.author.date}' || echo "Commit not found"

# Find which tags contain this commit
gh api repos/actions/github-script/tags --paginate --jq '.[] | select(.commit.sha == "373c709c69115d41ff229c7e5df9f8788daa9553") | {name: .name, sha: .commit.sha}'

Repository: kjanat/runner

Length of output: 347


🏴‍☠️ Rip out the fake pin: the @actions/github-script SHA doesn’t exist.

package.json pins @actions/github-script to github:actions/github-script#373c709c69115d41ff229c7e5df9f8788daa9553, but GitHub’s API responds with HTTP 422 “No commit found” for that SHA in actions/github-script, so this reference can’t be audited. Point it at a commit that exists (or a proper release tag) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 13, The dependency pin for "`@actions/github-script`" in
package.json is referencing a non-existent commit SHA; update the dependency
spec for "`@actions/github-script`" to a valid reference (either a published npm
version like a release tag e.g. "github-script@^6.1.0" or a real
commit/annotated tag from the actions/github-script repo) so installations and
audits succeed; locate the "`@actions/github-script`" entry in package.json and
replace the fake
"github:actions/github-script#373c709c69115d41ff229c7e5df9f8788daa9553" value
with a valid tag/commit or semver release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant