Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).

## Unreleased

### Fixed

- **Release scripts report observed lock holders separately from stale release labels.** Shared
diagnostics no longer attribute a live lock to a previous run; wait for the holders to finish, then
retry.

## 0.38.0 — 16.10.2026

### Added
Expand Down
3 changes: 2 additions & 1 deletion bin/release
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ git merge -q --ff-only origin/main
# The workflow publishes origin/main, so a local commit not pushed yet would silently not ship.
[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || { echo "main is ahead of origin/main — push first" >&2; exit 1; }
# One release at a time on this machine, across every package: two sessions published at once on 2026-10-08.
lock="${XDG_RUNTIME_DIR:-/tmp}/leemour-release.lock"; exec 9>"$lock"; flock -n 9 || { echo "another release is running: $(cat "$lock.who" 2>/dev/null)" >&2; exit 1; }; echo "$(basename "$PWD") pid $$ since $(date -u +%H:%M)" > "$lock.who"
. ./config/wirecat/release-lock.sh
wirecat_release_lock

version=$(node -p 'require("./package.json").version')
# `npm view` rather than the registry document, which a CDN serves stale for minutes.
Expand Down
8 changes: 7 additions & 1 deletion config/wirecat/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"no-shell-true.grit": "d3943cfa1d7ebbf7be0fa22c2aaf2b7d7aa9ff462f2eb14a7798632764e3875c",
"no-unbounded-spread.grit": "f4178e5b74212fef58fec8916b6783d9f4f192ac618aa4ced041c4167bfbf81b",
"rumdl.toml": "048b929176f13234324f5825d1e811397f1491b9cb6684a1953e268e2c2ed863",
"check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1"
"check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1",
"release-lock.sh": "f3309e569daea423c35f2a7b51cd55aec24c28160428b67ee2ece05e67fab877",
"release-lock-info.mjs": "4abab1f01ceee7d4c709222e923c9a6fc634bcf7b8b64d745a9ac495993310e0"
},
"packageManager": "pnpm@11.20.0",
"node": ">=22",
Expand All @@ -23,5 +25,9 @@
"cspell": "10.3.6",
"lefthook": "2.1.16",
"markdownlint-cli2": "0.23.3"
},
"fileRevisions": {
"release-lock.sh": "f5e184c78cb170158773d2270cecb14431315ed6",
"release-lock-info.mjs": "f5e184c78cb170158773d2270cecb14431315ed6"
}
}
71 changes: 71 additions & 0 deletions config/wirecat/release-lock-info.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { execFileSync } from "node:child_process"
import { closeSync, openSync, readSync, realpathSync } from "node:fs"
import { fileURLToPath } from "node:url"

const safe = (value) => JSON.stringify(String(value))

export function releaseLockInfo(path, { run = execFileSync, realpath = realpathSync, label = readLabel } = {}) {
const lines = [`another release holds ${safe(path)}`]
let rows
for (const columns of ["PID,COMMAND,PATH,TYPE,MODE,HOLDERS", "PID,COMMAND,PATH,TYPE,MODE"]) {
try {
const result = JSON.parse(
run("lslocks", ["--json", "--output", columns], {
encoding: "utf8",
timeout: 2000,
maxBuffer: 1024 * 1024,
stdio: ["ignore", "pipe", "ignore"],
}),
)
rows = result.locks
break
} catch {
// Older util-linux lacks HOLDERS; diagnostics also work without lslocks.
}
}
const holders = new Set()
if (Array.isArray(rows)) {
for (const row of rows) {
try {
if (row.type !== "FLOCK" || row.mode?.endsWith("*")) continue
if (realpath(row.path) !== realpath(path)) continue
const before = holders.size
if (Array.isArray(row.holders)) {
for (const holder of row.holders) {
const match = /^(\d+),([^,]+),\d+$/.exec(holder)
if (match && Number(match[1]) > 0) holders.add(`pid ${match[1]} (${safe(match[2])})`)
}
}
if (holders.size === before && Number.isInteger(row.pid) && row.pid > 0)
holders.add(`pid ${row.pid} (${safe(row.command)})`)
} catch {
// A holder may exit while the snapshot is read, or hide its path.
}
}
}
lines.push(
holders.size
? `Observed lock holders (may change): ${[...holders].join(", ")}`
: "Observed lock holders: unavailable",
)
try {
const recorded = label(`${path}.who`)
if (recorded) lines.push(`Last recorded release (may be stale): ${safe(recorded)}`)
} catch {
// The label is advisory; only flock decides whether a release may proceed.
}
lines.push("Wait for the lock holders to finish, then retry. Do not remove the lock file.")
return `${lines.join("\n")}\n`
}

function readLabel(path) {
const fd = openSync(path, "r")
try {
const bytes = Buffer.alloc(512)
return bytes.subarray(0, readSync(fd, bytes)).toString("utf8").trim()
} finally {
closeSync(fd)
}
}

if (process.argv[1] === fileURLToPath(import.meta.url)) process.stderr.write(releaseLockInfo(process.argv[2]))
25 changes: 25 additions & 0 deletions config/wirecat/release-lock.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Source from the repository root; FD9 must stay in the release shell and its children.
wirecat_release_lock() {
_wirecat_release_wait=${1-0}
case "$_wirecat_release_wait" in
''|*[!0-9]*) echo "release wait must be a nonnegative integer" >&2; return 2 ;;
esac
command -v flock >/dev/null 2>&1 || {
echo "release locking requires flock" >&2
return 1
}
_wirecat_release_lock="${XDG_RUNTIME_DIR:-/tmp}/leemour-release.lock"
exec 9>>"$_wirecat_release_lock"
if flock -E 75 -w "$_wirecat_release_wait" 9; then
printf '%s pid %s since %s\n' "$(basename "$PWD")" "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_wirecat_release_lock.who"
else
_wirecat_release_status=$?
if [ "$_wirecat_release_status" -ne 75 ]; then
printf 'could not acquire release lock %s (flock exit %s)\n' "$_wirecat_release_lock" "$_wirecat_release_status" >&2
return 1
fi
node ./config/wirecat/release-lock-info.mjs "$_wirecat_release_lock" ||
printf 'another release holds %s; holder details unavailable\n' "$_wirecat_release_lock" >&2
return 1
fi
}
Loading