From 3483c9b0ec0e2bd18c95ff69e2e078e9cc8e953a Mon Sep 17 00:00:00 2001 From: Joe Petrucelli Date: Tue, 4 Aug 2026 09:11:07 -0400 Subject: [PATCH] ci(release): the published installer can be verified, and its build is pinned Pin PyInstaller and Inno Setup to specific versions so the build is reproducible instead of drifting to whatever chocolatey and pip happen to resolve on a given day. Pass the workflow_dispatch version through the environment rather than interpolating it into the PowerShell script body, so a crafted dispatch input is read as data instead of executed. SHA-pin the three third-party actions instead of trusting a mutable version tag. Hash the installer after it is built, ship the SHA256SUMS file as a release asset, and repeat the checksum in the release notes so a download can be verified without fetching anything else. Release notes are now generated fresh on every run instead of accreted with --generate-notes, so re-running a release cannot append a second checksum block to notes that already have one. --- .github/workflows/release.yml | 51 +++++++++++++++++++++++++------ installer/claude-batch-runner.iss | 3 ++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3494469..2f9108c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,16 +25,18 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - name: Take the version from the tag, or from the dispatch input + env: + DISPATCH_VERSION: ${{ inputs.version }} run: | if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { - $v = "${{ inputs.version }}" + $v = $env:DISPATCH_VERSION } else { $v = $env:GITHUB_REF_NAME -replace '^v', '' } @@ -53,7 +55,7 @@ jobs: - name: Install claude-batch-runner and PyInstaller run: | python -m pip install --upgrade pip - python -m pip install . pyinstaller + python -m pip install . pyinstaller==6.21.0 - name: Build the one-file executable # The entry point is a module function, so PyInstaller is aimed at @@ -69,7 +71,7 @@ jobs: - name: Install Inno Setup # Installed, not assumed: a runner image that quietly drops a # preinstalled tool would break releases with no warning. - run: choco install innosetup -y --no-progress + run: choco install innosetup -y --no-progress --version=6.7.1 - name: Build the installer run: | @@ -78,13 +80,27 @@ jobs: if (-not (Test-Path $iscc)) { throw "ISCC.exe not found at $iscc - Inno Setup's install layout changed" } & $iscc /DAppVersion=$env:VERSION installer\claude-batch-runner.iss + - name: Checksum the installer + # A binary nobody can verify is a binary nobody should trust. The hash is + # written beside the exe, shipped as a release asset, and repeated in the + # release notes so a download can be checked without fetching the asset. + run: | + $exe = "dist\claude-batch-runner-setup-$env:VERSION.exe" + if (-not (Test-Path $exe)) { throw "no installer at $exe" } + $leaf = Split-Path $exe -Leaf + $hash = (Get-FileHash $exe -Algorithm SHA256).Hash.ToLower() + "$hash $leaf" | Out-File -FilePath dist\SHA256SUMS -Encoding ascii + "INSTALLER_SHA256=$hash" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + - name: Keep the installer as an artifact (dry run only) # A dry run proves the build; it deliberately publishes nothing. if: github.event_name == 'workflow_dispatch' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: claude-batch-runner-setup-${{ env.VERSION }} - path: dist/claude-batch-runner-setup-${{ env.VERSION }}.exe + path: | + dist/claude-batch-runner-setup-${{ env.VERSION }}.exe + dist/SHA256SUMS if-no-files-found: error - name: Attach the installer to the release @@ -97,10 +113,27 @@ jobs: run: | $ErrorActionPreference = 'Continue' $exe = "dist\claude-batch-runner-setup-$env:VERSION.exe" + $leaf = Split-Path $exe -Leaf + # Notes are rebuilt from scratch every run, so re-running a release cannot + # append a second checksum block to notes that already carry one. + $generated = gh api "repos/$env:GITHUB_REPOSITORY/releases/generate-notes" -f tag_name=$env:GITHUB_REF_NAME -q .body + if ($LASTEXITCODE -ne 0) { throw "could not generate release notes for $env:GITHUB_REF_NAME" } + $notes = "dist\release-notes.md" + @( + $generated + "" + "## Verify the download" + "" + '```' + "$env:INSTALLER_SHA256 $leaf" + '```' + ) | Set-Content -Path $notes -Encoding utf8 gh release view $env:GITHUB_REF_NAME --json tagName | Out-Null if ($LASTEXITCODE -eq 0) { - gh release upload $env:GITHUB_REF_NAME $exe --clobber + gh release upload $env:GITHUB_REF_NAME $exe dist\SHA256SUMS --clobber + if ($LASTEXITCODE -ne 0) { throw "could not upload $exe to $env:GITHUB_REF_NAME" } + gh release edit $env:GITHUB_REF_NAME --notes-file $notes } else { - gh release create $env:GITHUB_REF_NAME --verify-tag --generate-notes $exe + gh release create $env:GITHUB_REF_NAME --verify-tag --notes-file $notes $exe dist\SHA256SUMS } if ($LASTEXITCODE -ne 0) { throw "could not publish $exe to $env:GITHUB_REF_NAME" } diff --git a/installer/claude-batch-runner.iss b/installer/claude-batch-runner.iss index 2e5c7a8..a5c704a 100644 --- a/installer/claude-batch-runner.iss +++ b/installer/claude-batch-runner.iss @@ -7,6 +7,9 @@ ; ISCC.exe /DAppVersion=0.1.0 installer\claude-batch-runner.iss ; ; See .github/workflows/release.yml for the whole sequence. +; +; Keep this file pure ASCII. Inno reads a BOM-less script as ANSI, so an em +; dash or a curly quote pasted in from prose becomes mojibake in the wizard. #define AppName "claude-batch-runner" #define AppURL "https://github.com/Back-Road-Creative/claude-batch-runner"