diff --git a/.github/verify-windows-signature.py b/.github/verify-windows-signature.py new file mode 100644 index 0000000000..bf8158f7d3 --- /dev/null +++ b/.github/verify-windows-signature.py @@ -0,0 +1,50 @@ +"""Report whether every executable in the Windows release zip is signed. + +The signing chain is silent when it breaks, and it broke two different ways in +a row under a green run: v0.69.0's signing request failed outright, and +v0.69.1's completed while the signed file never reached the release. This reads +the shipped zip itself, so nothing but the published bytes can satisfy it. + +Reports rather than blocks — the caller runs it with continue-on-error, since +SignPath is on a self-signed test certificate while the project's OSS +application is under review and publishing must not depend on it. For the same +reason this checks that a certificate is present, not that it chains to a +trusted root. + +Usage: python .github/verify-windows-signature.py +""" + +import struct +import sys +import zipfile + + +def certificate_table_size(pe: bytes) -> int: + """Size of the PE's certificate table — data directory entry 4, zero when unsigned.""" + pe_header = struct.unpack_from(" never matched (confirmed via a real + # paths never matched (confirmed via a real # signing-request failure: "Expected path to match exactly 1 # item, but found 0", processing unsigned-windows-zip.zip). # archive: false uploads the file as-is; the artifact name then @@ -171,6 +171,7 @@ jobs: archive: false path: target/distrib/worktrunk-x86_64-pc-windows-msvc.zip - name: Submit SignPath signing request + id: signpath if: contains(matrix.targets, 'x86_64-pc-windows-msvc') && steps.upload-unsigned-windows-artifact.outcome == 'success' continue-on-error: true uses: signpath/github-action-submit-signing-request@v2 @@ -181,11 +182,33 @@ jobs: signing-policy-slug: 'test-signing' github-artifact-id: '${{ steps.upload-unsigned-windows-artifact.outputs.artifact-id }}' wait-for-completion: true - output-artifact-directory: 'target/distrib' - # Without this, the action extracts the zip's contents as loose - # files instead of saving back the signed zip under its original - # name, silently breaking the checksum step and the release upload. + # A scratch directory, not target/distrib: SignPath names the + # download after the GitHub artifact, which is already + # worktrunk-x86_64-pc-windows-msvc.zip, so writing it alongside the + # unsigned build would land it at …zip.zip and leave the unsigned + # original in place (how v0.69.1 shipped unsigned). The next step + # takes whatever lands here, so SignPath's naming doesn't matter. + output-artifact-directory: 'target/signpath' + # Without this, the action extracts the returned zip's contents as + # loose files instead of saving back the signed zip. skip-decompress: true + - name: Replace the Windows artifact with the signed one + if: contains(matrix.targets, 'x86_64-pc-windows-msvc') && steps.signpath.outcome == 'success' + continue-on-error: true + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + signed=(target/signpath/*) + if [ ${#signed[@]} -ne 1 ]; then + echo "::error::expected one signed artifact, found ${#signed[@]}: ${signed[*]-}" + exit 1 + fi + # Check the replacement before it overwrites the build: shipping an + # unsigned zip is tolerable while the certificate is a test one, + # shipping a corrupt one never is. + python .github/verify-windows-signature.py "${signed[0]}" + mv "${signed[0]}" target/distrib/worktrunk-x86_64-pc-windows-msvc.zip - name: Recompute checksum for signed Windows artifact if: contains(matrix.targets, 'x86_64-pc-windows-msvc') shell: bash @@ -194,6 +217,15 @@ jobs: # -b matches cargo-dist's own checksum format (` *`); # sha256sum's default text mode omits the asterisk. sha256sum -b worktrunk-x86_64-pc-windows-msvc.zip > worktrunk-x86_64-pc-windows-msvc.zip.sha256 + # Records what the release actually ships. The step above gates the + # overwrite; this one answers the question the logs never could — a + # signing request can complete while the signed file never reaches the + # release, which is how v0.69.1 shipped unsigned. + - name: Report whether the shipped Windows artifact is signed + if: contains(matrix.targets, 'x86_64-pc-windows-msvc') + continue-on-error: true + shell: bash + run: python .github/verify-windows-signature.py target/distrib/worktrunk-x86_64-pc-windows-msvc.zip - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f2ce032d3..630ee2b244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.69.2 + +### Improved + +- **`wt remove` resolves every fsmonitor daemon in one `lsof` call**: The end-of-command sweep forked one `lsof` per `git fsmonitor--daemon` on the machine — with `core.fsmonitor` enabled globally that is one daemon per repo ever touched, routinely over a hundred — and the spawn cost compounds under load rather than staying fixed per call. All daemons now resolve in a single call: 108 spawns to 1 on a live machine. ([#3581](https://github.com/max-sixty/worktrunk/pull/3581)) + +### Documentation + +- **Troubleshooting no longer suggests disabling `core.fsmonitor` globally**: the guidance for a wedged daemon keeps to the targeted fixes — kill the daemon serving that worktree, or let the next `wt list` respawn the live ones. ([#3581](https://github.com/max-sixty/worktrunk/pull/3581)) + +### Internal + +- **The Windows zip ships the signed binary**: SignPath names its download after the GitHub artifact, so the signed zip landed beside the unsigned build as `worktrunk-x86_64-pc-windows-msvc.zip.zip` while the checksum step and the release upload both kept reading the original — so v0.69.1 shipped an unsigned binary under a green run and a completed signing request. (v0.69.0's unsigned binary was the separate upload bug fixed in [#3566](https://github.com/max-sixty/worktrunk/pull/3566), where the request failed outright.) The signed file now replaces the built one only after it verifies, and a final step reads the shipped zip and reports any executable without a certificate. Signing remains non-blocking on a self-signed test certificate pending SignPath's OSS review, so Windows will not show a trusted publisher yet. ([#3590](https://github.com/max-sixty/worktrunk/pull/3590)) + ## 0.69.1 ### Improved diff --git a/Cargo.lock b/Cargo.lock index d49c6dde16..55fb6d9ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4497,7 +4497,7 @@ dependencies = [ [[package]] name = "worktrunk" -version = "0.69.1" +version = "0.69.2" dependencies = [ "ansi-str", "ansi-to-html", diff --git a/Cargo.toml b/Cargo.toml index 5b1d813708..6f38dc8d9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ filterset = "test(/readme_sync/)" [package] name = "worktrunk" -version = "0.69.1" +version = "0.69.2" edition = "2024" rust-version = "1.96" build = "build.rs"