fix(dashmate)!: give Debian packages versions apt can order - #4282
fix(dashmate)!: give Debian packages versions apt can order#4282shumkov wants to merge 2 commits into
Conversation
oclif builds the Debian version as <upstream>.<git sha>-1, which discards the semver prerelease tag and makes the git sha an ordering component. Under dpkg's comparison digits sort below letters, so ordering between builds is effectively random: of the four real transitions in the 4.1.0 series, apt reads two as downgrades. A same-version security rebuild is worse still, since it compares lower and apt reports the package as already newest while the operator believes they are patched. Versions are now Debian-idiomatic and monotonic: 4.1.0~rc.3-1 sorts below 4.1.0-1, rebuilds bump the Debian revision, and the sha moves into the package description where it cannot affect ordering. Verified against real dpkg, which also confirms this ordering agrees with semver precedence for every prerelease form the mapping can produce. Filenames deliberately diverge from the control version: GitHub rewrites the tilde and colon in release asset names, so both are stripped from the filename while the control field keeps them. dpkg-name strips epochs from filenames for the same reason. BREAKING CHANGE: Debian package filenames and versions change shape. A rebuild of an already published version needs DASHMATE_DEB_REVISION, and re-releasing a version published under the old scheme needs DASHMATE_DEB_EPOCH. Test would have caught this in CI: 4 of the new specs fail before the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing published today can be verified: there are no checksums, no signatures beyond the macOS notarisation, and the apt metadata oclif generates is uploaded unsigned and unserved. This adds the pieces that do not depend on where the repository will eventually be hosted. Every published asset is now hashed into a deterministic SHA256SUMS, in a job that checks out nothing and installs nothing so no dependency lifecycle script can run beside the signing key that job will later hold. Each packaging leg records the hashes it produced and the checksum job refuses any asset it did not build, so the file attests what was built rather than whatever is attached. npm packages publish with provenance where the registry will accept it, which required correcting dashmate's own repository field; the rest publish as before with a warning naming the manifest to fix, so a metadata gap cannot fail a release mid-loop. The Debian version gate refuses a release apt would read as a downgrade. It reads every version from the published package rather than deriving it from a tag, and the packaging job asserts the built package carries the version that was gated, so the check binds to the bytes that ship. Third-party actions are pinned to commit SHAs, Binaryen is checksummed, and the jobs holding credentials name environments so tag and reviewer policies can be attached to them. Note the npm publish job cannot avoid running install scripts, because packing runs prepack and prepublishOnly hooks. That exposure is documented in the workflow rather than claimed to be solved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesDashmate release packaging
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ReleaseWorkflow
participant VersionGate
participant PackageBuilder
participant ReleaseAssets
participant ChecksumJob
VersionGate->>ReleaseWorkflow: validate candidate Debian version
ReleaseWorkflow->>PackageBuilder: build packages with validated version
PackageBuilder->>ReleaseWorkflow: upload packages and built checksums
ReleaseWorkflow->>ReleaseAssets: publish package assets
ChecksumJob->>ReleaseAssets: download published assets
ChecksumJob->>PackageBuilder: download built checksum artifacts
ChecksumJob->>ReleaseAssets: upload verified SHA256SUMS
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
⛔ Blockers found — Sonnet deferred (commit 773bc64) |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
scripts/deb_version.js (1)
41-41: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueReject epochs with leading zeros.
EPOCH_REGEXaccepts01, soDASHMATE_DEB_EPOCH=01yields01:4.1.0-1. dpkg parses the epoch numerically, so01:and1:compare equal while the control field text differs from the value the release gate echoes and the packaging check compares as a string. TheCheck the built deb carries the validated versionstep in.github/workflows/release.ymlcompares version strings exactly, so any normalization difference becomes a hard failure. Restrict the epoch the same way the numeric identifier is restricted.♻️ Proposed change
-const EPOCH_REGEX = /^\d+$/; +const EPOCH_REGEX = /^(?:0|[1-9]\d*)$/;Also applies to: 66-68
🤖 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 `@scripts/deb_version.js` at line 41, Update EPOCH_REGEX in scripts/deb_version.js to reject leading zeros while still accepting the valid zero epoch and nonzero numeric epochs, matching the existing numeric-identifier validation behavior. Ensure epoch values such as 01 are rejected before version construction and validation output.scripts/check_deb_version.sh (1)
44-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winHandle a malformed version explicitly.
dpkg --compare-versionsexits 2 when either argument is not a parsable Debian version. Bothiftests then evaluate false, and the script reports"$NEW_VERSION sorts below $PREVIOUS_VERSION"and exits 1. That message is wrong for a parse error, and the operator advice aboutDASHMATE_DEB_EPOCHdoes not apply. Validate both arguments first.♻️ Proposed change
+for version in "$NEW_VERSION" "$PREVIOUS_VERSION" +do + if ! dpkg --validate-version "$version" > /dev/null 2>&1 + then + echo "check_deb_version.sh: \"$version\" is not a valid Debian version." >&2 + exit 2 + fi +done + if dpkg --compare-versions "$NEW_VERSION" gt "$PREVIOUS_VERSION"🤖 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 `@scripts/check_deb_version.sh` around lines 44 - 59, Validate both NEW_VERSION and PREVIOUS_VERSION with dpkg --compare-versions before the greater-than/equality checks in the version comparison flow. Detect its parse-error status explicitly, report that the version input is malformed, and exit nonzero without using the downgrade message or DASHMATE_DEB_EPOCH guidance; preserve the existing ordering behavior for valid versions..github/workflows/release.yml (1)
699-720: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winThe published-asset check is one-directional.
The loop proves that every published hash came from a packaging job. It does not prove that every built package reached the release. If an asset is deleted or an upload silently fails,
SHA256SUMSsimply omits it and the step reports success. The comment at Line 678 states that apt publication refuses any package whose hash is not in the signed file, so a missing entry becomes a silent omission of a package rather than a detected fault.Add the reverse check: every hash recorded by a packaging job must appear in
SHA256SUMS.♻️ Proposed addition
if [ "${unmatched}" -ne 0 ]; then echo "::error::Published assets do not match the built packages; refusing to publish checksums" exit 1 fi + + cut -d' ' -f1 assets/SHA256SUMS | LC_ALL=C sort -u > "${RUNNER_TEMP}/published-hashes" + missing="$(comm -23 "${RUNNER_TEMP}/built-hashes" "${RUNNER_TEMP}/published-hashes")" + if [ -n "${missing}" ]; then + echo "::error::These built package hashes are not published: ${missing}" + exit 1 + fi echo "All published assets match packages built in this run"🤖 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 699 - 720, Extend the verification step after the existing published-asset loop to also validate completeness: iterate over the unique hashes in "${RUNNER_TEMP}/built-hashes" and require each to appear in the first field of assets/SHA256SUMS. Report missing published assets, set unmatched, and preserve the existing failure path that refuses publication when unmatched is nonzero.
🤖 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/workflows/release.yml:
- Around line 45-46: Update all three release workflow checkout steps using
actions/checkout@v4 to set persist-credentials to false, unless the
corresponding job has a later step requiring authenticated Git access; preserve
authentication only for those jobs that need it.
In `@packages/dashmate/docs/installation.md`:
- Around line 37-38: The shell glob pattern ./dashmate_*.deb expands before apt
runs and can match multiple files (older packages or wrong architectures),
causing installation failures. Replace the glob pattern in the sudo apt install
command with an explicit variable or filename that captures the exact basename
from the preceding curl -o download operation, ensuring only the intended
downloaded package is passed to apt.
In `@scripts/pack_dashmate.sh`:
- Around line 160-164: Update the DASHMATE_DEB_KEY check in the signing block to
use a default-safe parameter expansion, so the unset variable is treated as
empty under set -u and unsigned local builds continue without signing.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 699-720: Extend the verification step after the existing
published-asset loop to also validate completeness: iterate over the unique
hashes in "${RUNNER_TEMP}/built-hashes" and require each to appear in the first
field of assets/SHA256SUMS. Report missing published assets, set unmatched, and
preserve the existing failure path that refuses publication when unmatched is
nonzero.
In `@scripts/check_deb_version.sh`:
- Around line 44-59: Validate both NEW_VERSION and PREVIOUS_VERSION with dpkg
--compare-versions before the greater-than/equality checks in the version
comparison flow. Detect its parse-error status explicitly, report that the
version input is malformed, and exit nonzero without using the downgrade message
or DASHMATE_DEB_EPOCH guidance; preserve the existing ordering behavior for
valid versions.
In `@scripts/deb_version.js`:
- Line 41: Update EPOCH_REGEX in scripts/deb_version.js to reject leading zeros
while still accepting the valid zero epoch and nonzero numeric epochs, matching
the existing numeric-identifier validation behavior. Ensure epoch values such as
01 are rejected before version construction and validation output.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5a1c5837-d2d3-4e64-8cba-f135f4e15942
📒 Files selected for processing (7)
.github/workflows/release.ymlpackages/dashmate/docs/installation.mdpackages/dashmate/package.jsonpackages/dashmate/test/unit/packaging/debVersion.spec.jsscripts/check_deb_version.shscripts/deb_version.jsscripts/pack_dashmate.sh
| - name: Check out repo | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== release workflow relevant checkout/token/action references =="
rg -n "actions/checkout|persist-credentials|contents|permissions:|GITHUB_TOKEN|yarn install|cargo|wasm|uses:" .github/workflows/release.yml
echo
echo "== checkout occurrences with surrounding lines =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/release.yml')
lines=p.read_text().splitlines()
for target in [45, 177, 346]:
print(f"\n--- around line {target} ---")
for i in range(max(1,target-8), min(len(lines), target+12)+1):
print(f"{i:4}: {lines[i-1]}")
PYRepository: dashpay/platform
Length of output: 146
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== release workflow size =="
wc -l .github/workflows/release.yml
echo
echo "== quick targeted matches =="
grep -nE "actions/checkout|persist-credentials|contents|permissions:|GITHUB_TOKEN|yarn install|cargo|wasm|uses:" .github/workflows/release.yml || true
echo
echo "== checkout occurrences with surrounding lines =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/release.yml')
lines=p.read_text().splitlines()
for target in [45, 177, 346]:
print(f"\n--- around line {target} ---")
start=max(1,target-6)
end=min(len(lines), target+10)
for i in range(start,end+1):
print(f"{i:4}: {lines[i-1]}")
PYRepository: dashpay/platform
Length of output: 146
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== release workflow size =="
wc -l .github/workflows/release.yml
echo
echo "== checkout occurrences with surrounding lines =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/release.yml')
lines=p.read_text().splitlines()
for target in [45, 177, 346]:
print(f"\n--- around line {target} ---")
start=max(1,target-6)
end=min(len(lines), target+10)
for i in range(start,end+1):
print(f"{i:4}: {lines[i-1]}")
PYRepository: dashpay/platform
Length of output: 2394
🌐 Web query:
actions/checkout persist-credentials default true GitHub token local git config
💡 Result:
In the GitHub actions/checkout action, the persist-credentials input defaults to true [1][2][3]. When enabled, this setting configures the authentication token (or SSH key) into the local git configuration [1][4]. This allows subsequent git commands executed within your workflow steps to automatically use these credentials without requiring manual authentication [1][5]. Important technical details regarding this mechanism include: 1. Security and Lifecycle: The token is automatically removed during the post-job cleanup process [1][5]. 2. Storage Location: While traditionally stored directly in the local git config, version 6.0.0 and later updated this behavior to store the credentials under $RUNNER_TEMP to improve security, such as preventing leaks through artifact publishing [6]. 3. Opt-out: If you do not want your credentials persisted to the git configuration, you can explicitly set persist-credentials to false in your workflow file [1][5]. Example configuration to disable persistence: - uses: actions/checkout@v4 with: persist-credentials: false
Citations:
- 1: https://github.com/actions/checkout
- 2: https://raw.githubusercontent.com/actions/checkout/v6/action.yml
- 3: https://github.com/actions/checkout/blob/v4/action.yml
- 4: https://github.com/marketplace/actions/checkout
- 5: https://github.com/actions/checkout/tree/v6.0.2?tab=readme-ov-file
- 6: https://github.com/actions/checkout/tree/v6.0.0
Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials
Disable credential persistence on checkout steps that do not need git authentication.
actions/checkout@v4 persists the GitHub token by default, so later workflow steps can use it until checkout cleanup. Add persist-credentials: false to the three release workflow checkouts unless a later step in that job needs authenticated Git access.
🧰 Tools
🪛 zizmor (1.28.0)
[warning] 45-46: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 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 45 - 46, Update all three release
workflow checkout steps using actions/checkout@v4 to set persist-credentials to
false, unless the corresponding job has a later step requiring authenticated Git
access; preserve authentication only for those jobs that need it.
Source: Linters/SAST tools
There was a problem hiding this comment.
The three cited checkouts persist only a contents: read token in this public repository, no .git data is uploaded, and no later step needs authenticated Git. The separate write-scoped packaging checkout predates this PR and is not covered by the comment.
There was a problem hiding this comment.
@thepastaclaw, the contents: read scope and the absence of .git uploads reduce the impact. The three jobs also do not need authenticated Git after checkout.
However, actions/checkout@v4 still persists an unnecessary credential for the job lifetime. Setting persist-credentials: false remains a valid least-privilege hardening change. I would keep this as a minor finding.
✏️ Learnings added
Learnt from: thepastaclaw
Repo: dashpay/platform PR: 4282
File: .github/workflows/release.yml:45-46
Timestamp: 2026-08-04T17:05:22.168Z
Learning: In `.github/workflows/release.yml`, the `release-npm`, `publish-npm`, and `check-dashmate-deb-version` jobs have `contents: read` permissions, do not upload `.git` data, and do not require authenticated Git operations after their `actions/checkoutv4` steps.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
You are interacting with an AI system.
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The workflow improves privilege separation and release verification, but three blocking gaps remain. The checksum gate does not bind hashes to filenames or require all built artifacts to be present, the Debian baseline can be selected using release creation order instead of publication order, and mutable actions inside local composites undermine the PR's action-pinning guarantee.
Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 3 blocking
1 additional finding(s) omitted (not in diff).
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `.github/workflows/release.yml`:
- [BLOCKING] .github/workflows/release.yml:699-720: Bind published checksums to filenames and require completeness
The verification reduces the build records to a set of hashes and only checks that each published hash appears in that set. Removing a built asset therefore still passes, and bytes built for one architecture can be published under another architecture's filename because that hash was produced by some matrix leg. This can produce and later sign an incomplete or mislabeled SHA256SUMS. Normalize the recorded paths to release basenames and compare the complete `(hash, filename)` sets in both directions.
- [BLOCKING] .github/workflows/release.yml:394-403: Order Debian baselines by publication time
The candidate list is intended to represent releases in the order operators could install them, but it sorts by `created_at`. A release can be created as a draft and published after another release created later. On a rerun, an older-created current release can then lose to its predecessor even though the current release already has a Debian asset, allowing a same-version rebuild to pass without incrementing the Debian revision. Sort by `published_at` so the gate compares against the most recently exposed package.
- [BLOCKING] .github/workflows/release.yml:58-72: Pin third-party actions used by local composites
The PR explicitly claims that third-party actions are pinned, but the local Rust and sccache composites invoked after DockerHub login still resolve `dtolnay/rust-toolchain@master` and `mozilla-actions/sccache-action@v0.0.6`. Those mutable external actions execute in the credential-bearing build job and can modify the workspace before `yarn build`; the resulting artifact is then packaged, hashed, and published by the new release path. Pin the external `uses` references in `.github/actions/rust/action.yaml:42` and `.github/actions/sccache/action.yaml:80` to full commit SHAs.
| - name: Verify published assets against the built packages | ||
| run: | | ||
| set -euo pipefail | ||
| cat built-checksums/*/built.sha256 > "${RUNNER_TEMP}/built-all.sha256" | ||
| if [ ! -s "${RUNNER_TEMP}/built-all.sha256" ]; then | ||
| echo "::error::No built package checksums were recorded" | ||
| exit 1 | ||
| fi | ||
| cut -d' ' -f1 "${RUNNER_TEMP}/built-all.sha256" | LC_ALL=C sort -u > "${RUNNER_TEMP}/built-hashes" | ||
|
|
||
| unmatched=0 | ||
| while read -r hash name; do | ||
| if ! grep -qxF "${hash}" "${RUNNER_TEMP}/built-hashes"; then | ||
| echo "::error::${name} (${hash}) was not produced by any packaging job" | ||
| unmatched=1 | ||
| fi | ||
| done < assets/SHA256SUMS | ||
| if [ "${unmatched}" -ne 0 ]; then | ||
| echo "::error::Published assets do not match the built packages; refusing to publish checksums" | ||
| exit 1 | ||
| fi | ||
| echo "All published assets match packages built in this run" |
There was a problem hiding this comment.
🔴 Blocking: Bind published checksums to filenames and require completeness
The verification reduces the build records to a set of hashes and only checks that each published hash appears in that set. Removing a built asset therefore still passes, and bytes built for one architecture can be published under another architecture's filename because that hash was produced by some matrix leg. This can produce and later sign an incomplete or mislabeled SHA256SUMS. Normalize the recorded paths to release basenames and compare the complete (hash, filename) sets in both directions.
| - name: Verify published assets against the built packages | |
| run: | | |
| set -euo pipefail | |
| cat built-checksums/*/built.sha256 > "${RUNNER_TEMP}/built-all.sha256" | |
| if [ ! -s "${RUNNER_TEMP}/built-all.sha256" ]; then | |
| echo "::error::No built package checksums were recorded" | |
| exit 1 | |
| fi | |
| cut -d' ' -f1 "${RUNNER_TEMP}/built-all.sha256" | LC_ALL=C sort -u > "${RUNNER_TEMP}/built-hashes" | |
| unmatched=0 | |
| while read -r hash name; do | |
| if ! grep -qxF "${hash}" "${RUNNER_TEMP}/built-hashes"; then | |
| echo "::error::${name} (${hash}) was not produced by any packaging job" | |
| unmatched=1 | |
| fi | |
| done < assets/SHA256SUMS | |
| if [ "${unmatched}" -ne 0 ]; then | |
| echo "::error::Published assets do not match the built packages; refusing to publish checksums" | |
| exit 1 | |
| fi | |
| echo "All published assets match packages built in this run" | |
| cat built-checksums/*/built.sha256 > "${RUNNER_TEMP}/built-all.sha256" | |
| if [ ! -s "${RUNNER_TEMP}/built-all.sha256" ]; then | |
| echo "::error::No built package checksums were recorded" | |
| exit 1 | |
| fi | |
| while read -r hash name; do | |
| printf '%s %s\n' "${hash}" "${name##*/}" | |
| done < "${RUNNER_TEMP}/built-all.sha256" \ | |
| | LC_ALL=C sort > "${RUNNER_TEMP}/built-assets.sha256" | |
| LC_ALL=C sort assets/SHA256SUMS > "${RUNNER_TEMP}/published-assets.sha256" | |
| if ! diff -u "${RUNNER_TEMP}/built-assets.sha256" "${RUNNER_TEMP}/published-assets.sha256"; then | |
| echo "::error::Published assets do not exactly match the packages built in this run" | |
| exit 1 | |
| fi | |
| echo "All published assets match packages built in this run" |
source: ['codex']
| gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ | ||
| | jq -r ' | ||
| add | ||
| | sort_by(.created_at) | ||
| | reverse | ||
| | .[] | ||
| | select(.draft == false) | ||
| | { tag: .tag_name, asset: ([.assets[].name | select(endswith("_amd64.deb"))] | first) } | ||
| | select(.asset != null) | ||
| | "\(.tag)\t\(.asset)"' > "${candidates}" |
There was a problem hiding this comment.
🔴 Blocking: Order Debian baselines by publication time
The candidate list is intended to represent releases in the order operators could install them, but it sorts by created_at. A release can be created as a draft and published after another release created later. On a rerun, an older-created current release can then lose to its predecessor even though the current release already has a Debian asset, allowing a same-version rebuild to pass without incrementing the Debian revision. Sort by published_at so the gate compares against the most recently exposed package.
| gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ | |
| | jq -r ' | |
| add | |
| | sort_by(.created_at) | |
| | reverse | |
| | .[] | |
| | select(.draft == false) | |
| | { tag: .tag_name, asset: ([.assets[].name | select(endswith("_amd64.deb"))] | first) } | |
| | select(.asset != null) | |
| | "\(.tag)\t\(.asset)"' > "${candidates}" | |
| | sort_by(.published_at) |
source: ['codex']
Issue being fixed or feature implemented
aptcannot order dashmate's Debian packages, and nothing we publish can be verified.oclif builds the deb version as
<upstream>.<git sha>-1. That discards the semver prerelease tag and makes the git sha an ordering component — and under dpkg's comparison digits sort below letters, so ordering between builds is effectively random. Verified against the real published 4.1.0 series:4.1.0.08152ea51e-1vs4.1.0.ae554fdd83-14.1.0.3de436123d-1vs4.1.0.08152ea51e-14.1.0.61be67f7bf-1vs4.1.0.3de436123d-14.1.0.bfc80249b9-1vs4.1.0.61be67f7bf-1Two of four real transitions are read as downgrades. Worse, a same-version security rebuild never ships: with
4.1.0.bfc80249b9-1installed, a hotfix built from a later commit compares lower, so apt reports the package as already newest and the operator believes they are patched.Separately, nothing in the release is verifiable — no checksums, no signatures beyond the macOS notarisation, and the apt metadata oclif generates is uploaded unsigned and unserved.
What was done?
Monotonic Debian versions.
4.1.0~rc.3-1sorts below4.1.0-1; rebuilds bump the Debian revision; the git sha moves into the package description where it cannot affect ordering. Filenames deliberately diverge from the control version, because GitHub rewrites~and:in release asset names — the control field keeps them, the filename drops them, anddpkg-namestrips epochs from filenames for the same reason.A release-blocking ordering gate. It reads every version from the published package via
dpkg-deb -frather than deriving it from a tag, and the packaging job then asserts the built package carries the version that was gated — so the check binds to the bytes that ship.Verifiable releases. Every published asset is hashed into a deterministic
SHA256SUMS, in a job that checks out nothing and installs nothing, so no dependency lifecycle script can run beside the signing key that job will later hold. Each packaging leg records the hashes it produced and the checksum job refuses any asset it did not build. npm packages publish with provenance where the registry accepts it. Third-party actions are pinned to commit SHAs, Binaryen is checksummed, and the jobs holding credentials name environments so tag and reviewer policies attach to them.How Has This Been Tested?
Verified against real dpkg (installed locally) rather than a port of its algorithm: every transition in the 4.1.0 series now sorts strictly upward, and dpkg ordering agrees with semver precedence across all 15 prerelease forms the mapping can produce. The version mapping is injection-proof (shell metacharacters, command substitution, embedded newlines all rejected) and the round-trip preserves symlinks, hardlinks, conffiles, maintainer scripts and exec bits.
Not executed:
apt-ftparchiveindex regeneration needs a Linux host, and no real release run has exercised the workflow changes. Both are recorded as gates rather than assumed.Breaking Changes
Debian package filenames and versions change shape. A rebuild of an already-published version needs
DASHMATE_DEB_REVISION; re-releasing a version published under the old scheme needsDASHMATE_DEB_EPOCH, because4.1.0-1sorts below the old4.1.0.bfc80249b9-1.Checklist:
For repository code-owners and collaborators only
Summary by CodeRabbit
New Features
Bug Fixes