Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.0.16-beta.0 — 2026-07-31

### Fixes
- Flag `printf "a\nb\n" > file` in the `prefer-write-over-heredoc` detector. The `printf` branch only matched literal embedded newlines, so the usual `\n`-escape form (which `printf` interprets into real newlines) slipped through. A `\n` only at the end (`printf "%s\n" ...`) stays unflagged, since that is a single line. (#638)
- Harden the release workflow against shell injection from ref names and generated outputs, align every Bun cache key with the tracked `bun.lock`, and discard the temporary publish-version edit before switching to `main` for the development-version bump. (#634)
- Ship the binaries the release already builds, and stop a branch dispatch from rewriting main's version. The daemon split added every packaging input — platform manifests, pinned optional dependencies, a 4-way cross-compile matrix — but never touched `publish.yml`, so each release built four binaries as Actions artifacts and discarded them with the runner; CI stayed green because nothing checks that what gets built also gets shipped. `publish.yml` is now four jobs — preflight (version/dist-tag resolution, an npm credential check that fails in seconds rather than after a 20-minute matrix, and daemon detection), a call into `build-daemon.yml` as a reusable workflow, an asset job that assembles `SHA256SUMS` and attaches it plus the four binaries to the GitHub Release, and the npm publish — in that order, because the installed CLI downloads its daemon from that release tag and publishing the package first ships a version whose binary does not exist yet. A failed cross-compile now blocks the publish explicitly: a failed dependency leaves its dependents `skipped`, which the old-style guard would have read as "nothing to do". The version bump checks main out and pushes to it, so it runs only for a release or a dispatch from main, and `latest` is refused from a non-main dispatch (`auto` resolves to `next` there) so a branch build cannot move a dist-tag that a later release from main would move backwards. Adds a `dry_run` input that builds, checksums and validates the publish while writing nothing, and fixes the bun cache key, which hashed a `bun.lockb` this repo does not track. All of it is gated on the ref carrying a Rust workspace, so on main this changes nothing until the daemon lands. (#634)

Expand Down
6 changes: 6 additions & 0 deletions __tests__/audit/detectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ describe("prefer-write-over-heredoc", () => {
it("matches `echo \"multi\\nline\" > file`", () => {
expect(preferWriteOverHeredoc.detect(bash('echo "a\nb" > out'), {})).not.toBeNull();
});
it("matches `printf \"a\\nb\\n\" > file` (escaped newlines)", () => {
expect(preferWriteOverHeredoc.detect(bash('printf "line1\\nline2\\n" > out.txt'), {})).not.toBeNull();
});
it("does not match `printf \"%s\\n\" ... > file` (single line)", () => {
expect(preferWriteOverHeredoc.detect(bash('printf "%s\\n" "$var" > out.txt'), {})).toBeNull();
});
});

describe("sleep-polling-loop", () => {
Expand Down
8 changes: 8 additions & 0 deletions src/audit/detectors/prefer-write-over-heredoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export const preferWriteOverHeredoc: Detector = {
const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160);
return { example: summary };
}
// `printf "a\nb\n" > file`. printf always interprets \n escapes into real
// newlines, so a format string whose \n has more content after it is
// multi-line content headed for a file. A \n only at the very end
// (`printf "%s\n" ...`) is a single line, so it is left alone.
if (/(?:^|\s|;|&&|\|\|)printf\s+["'][^"']*\\n[^"'][^"']*["']\s*>\s*\S/.test(cmd)) {
const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160);
return { example: summary };
}
return null;
},
};