From 071e8dbea77e26a73abc66b41882dc481e87ee21 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sat, 3 Oct 2026 06:00:29 +0200 Subject: [PATCH 1/3] Check the pinned toolchain against the compile service's The template-firmware check pins the compile service's toolchain, but nothing noticed if the service moved on: its builds of edited sketches would then differ from the bundled firmware, and the pins would be stale. The compile service now reports its cores and libraries in /health (PinBench/compile-service#7); a new job compares every pinned version with it, on each change and weekly. It passes today, and a wrong pin is reported as drift. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/template-firmware.yml | 41 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/template-firmware.yml b/.github/workflows/template-firmware.yml index e85268d..ff1463f 100644 --- a/.github/workflows/template-firmware.yml +++ b/.github/workflows/template-firmware.yml @@ -16,6 +16,10 @@ on: - assets/templates/** - tools/check_template_firmware.dart - .github/workflows/template-firmware.yml + # Weekly too, for the service-toolchain job: the service can change + # without anything here changing. + schedule: + - cron: "41 6 * * 1" workflow_dispatch: concurrency: @@ -25,10 +29,11 @@ concurrency: permissions: contents: read -# The compile service's toolchain (PinBench/compile-service tools/setup-vm.sh), -# pinned: a build only reproduces with the same versions. When the service -# moves to newer ones, bump these to match and rebuild the templates with -# --write in the same pull request. +# The compile service's toolchain, pinned (its source of truth is +# PinBench/compile-service deploy/toolchain.env): a build only reproduces with +# the same versions. The service-toolchain job checks these against what the +# live service reports. When the service moves to newer ones, bump these to +# match and rebuild the templates with --write in the same pull request. env: ARDUINO_CLI_VERSION: 1.5.1 ARDUINO_CORES: arduino:avr@1.8.8 rp2040:rp2040@6.2.0 @@ -36,8 +41,36 @@ env: RP2040_INDEX: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json jobs: + # The live service's /health lists the versions it builds with; if they are + # not these, the bundled firmware no longer matches what an edited sketch + # compiles to, and this pinned set is out of date. + service-toolchain: + name: Pinned toolchain matches the compile service + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + COMPILE_SERVICE: ${{ vars.COMPILE_API_URL || 'https://138-3-254-103.sslip.io' }} + steps: + - name: Compare with /health + run: | + health=$(curl -fsS --max-time 30 "$COMPILE_SERVICE/health") + problems=0 + check() { # what, pinned, live + if [ "$2" = "$3" ]; then echo "ok $1 $2"; else echo "DRIFT $1: pinned $2, the service has $3"; problems=1; fi + } + check arduino-cli "$ARDUINO_CLI_VERSION" "$(jq -r '.arduinoCliVersion' <<<"$health")" + for core in $ARDUINO_CORES; do + check "${core%@*}" "${core#*@}" "$(jq -r --arg k "${core%@*}" '.toolchain.cores[$k] // "nothing"' <<<"$health")" + done + IFS='|' read -ra libraries <<< "$ARDUINO_LIBRARIES" + for lib in "${libraries[@]}"; do + check "${lib%@*}" "${lib#*@}" "$(jq -r --arg k "${lib%@*}" '.toolchain.libraries[$k] // "nothing"' <<<"$health")" + done + exit "$problems" + check: name: Templates match their firmware + if: github.event_name != 'schedule' runs-on: ubuntu-latest timeout-minutes: 20 steps: From 37b6474a80a4513068a66d559660457bdf3cf396 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sat, 3 Oct 2026 06:03:52 +0200 Subject: [PATCH 2/3] Add the release workflow the updater and installer were built for windows/packaging/installer.iss says it is built by release.yml, and tools/release_metadata.dart writes the appcasts and latest.json the updater polls, but no release workflow existed, so there has never been a release and the download page has nothing to list. On a vX.Y.Z tag it builds the three desktop targets (the same steps as ci.yml's build-desktop), packages a .dmg, the Inno Setup installer and a .tar.gz, writes the updater's files and SHA256SUMS.txt, and drafts a release to publish by hand. Build number X*10000+Y*100+Z, so Sparkle's comparison only goes up. A pull request touching the release machinery runs it as a trial without releasing. Unsigned until there are signing certificates; the release notes say how to open the app meanwhile. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 218 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 15 +++ 2 files changed, 233 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c9dae70 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,218 @@ +name: Release + +# Builds the desktop app for macOS, Windows and Linux and makes a release of +# them: a .dmg, the Inno Setup installer (windows/packaging/installer.iss) and +# a .tar.gz, with SHA256SUMS.txt and the files the app's updater reads +# (appcast-macos.xml, appcast-windows.xml and latest.json, written by +# tools/release_metadata.dart). The download page lists the newest release. +# +# git tag v0.2.0 && git push origin v0.2.0 +# +# A tag makes a *draft* release: look it over, then publish it, which is when +# the updater and the download page see it. A pull request that changes this +# file, the installer or the metadata tool builds everything without +# releasing, which is how a change here is tested. +# +# Not signed yet: there is no Apple Developer ID or Windows certificate. Until +# there is, macOS asks to confirm opening the app (right-click, Open) and +# Windows shows a SmartScreen prompt for the installer. +on: + push: + tags: ["v*"] + pull_request: + paths: + - .github/workflows/release.yml + - windows/packaging/** + - tools/release_metadata.dart + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + version: + name: Version + runs-on: ubuntu-latest + outputs: + name: ${{ steps.v.outputs.name }} + number: ${{ steps.v.outputs.number }} + steps: + - uses: actions/checkout@v7 + # A tag v1.2.3 is version 1.2.3 with build number 10203, which only ever + # grows from one release to the next: Sparkle compares build numbers. A + # pull request's trial build takes pubspec.yaml's version. + - id: v + run: | + if [ "$GITHUB_REF_TYPE" = tag ]; then + name="${GITHUB_REF_NAME#v}" + else + name="$(sed -n 's/^version: *\([0-9.]*\).*/\1/p' pubspec.yaml)" + fi + IFS=. read -r major minor patch <<< "$name" + echo "name=$name" >> "$GITHUB_OUTPUT" + echo "number=$((major * 10000 + minor * 100 + patch))" >> "$GITHUB_OUTPUT" + + build: + name: Build ${{ matrix.target }} + needs: version + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + platform: macos + target: macOS + - os: windows-latest + platform: windows + target: Windows + - os: ubuntu-latest + platform: linux + target: Linux + env: + VERSION: ${{ needs.version.outputs.name }} + BUILD_NUMBER: ${{ needs.version.outputs.number }} + steps: + - uses: actions/checkout@v7 + + # The two format packages are private: submodules here, and git + # dependencies for pub. See ci.yml. + - name: Load the deploy keys for the private format repositories + uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 + with: + ssh-private-key: | + ${{ secrets.PDL_DEPLOY_KEY }} + ${{ secrets.CDL_DEPLOY_KEY }} + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Install Linux build dependencies + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libblkid-dev libasound2-dev + + - name: Set up Flutter + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 + with: + flutter-version-file: .fvmrc + channel: stable + cache: true + + - run: flutter config --enable-${{ matrix.platform }}-desktop + - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs + + - name: Build + shell: bash + env: + CODE_SIGNING_ALLOWED: "NO" + CODE_SIGNING_REQUIRED: "NO" + CODE_SIGN_IDENTITY: "" + run: flutter build ${{ matrix.platform }} --release --build-name "$VERSION" --build-number "$BUILD_NUMBER" + + - name: Package (macOS disk image) + if: matrix.platform == 'macos' + run: | + mkdir -p dist + hdiutil create -volname PinBench -srcfolder build/macos/Build/Products/Release/pinbench.app \ + -ov -format UDZO "dist/pinbench-$VERSION-macos.dmg" + + - name: Package (Windows installer) + if: matrix.platform == 'windows' + shell: pwsh + run: | + & "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" ` + "/DAppVersion=$env:VERSION" ` + "/DSourceDir=$env:GITHUB_WORKSPACE\build\windows\x64\runner\Release" ` + "/DOutputDir=$env:GITHUB_WORKSPACE\dist" ` + windows\packaging\installer.iss + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Package (Linux tarball) + if: matrix.platform == 'linux' + run: | + mkdir -p dist + tar -czf "dist/pinbench-$VERSION-linux.tar.gz" -C build/linux/x64/release/bundle . + + - uses: actions/upload-artifact@v7 + with: + name: pinbench-${{ matrix.platform }} + path: dist/* + if-no-files-found: error + retention-days: 14 + + release: + name: ${{ github.event_name == 'pull_request' && 'Assemble (trial, not released)' || 'Draft the release' }} + needs: [version, build] + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + env: + VERSION: ${{ needs.version.outputs.name }} + BUILD_NUMBER: ${{ needs.version.outputs.number }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v7 + + # tools/release_metadata.dart needs the app's dependencies (crypto), + # which resolve through the private format packages. + - name: Load the deploy keys for the private format repositories + uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 + with: + ssh-private-key: | + ${{ secrets.PDL_DEPLOY_KEY }} + ${{ secrets.CDL_DEPLOY_KEY }} + - run: git submodule update --init --recursive + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 + with: + flutter-version-file: .fvmrc + channel: stable + cache: true + - run: flutter pub get + + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + + - name: Write the updater's files and the checksums + run: | + tag="v$VERSION" + base="https://github.com/$GITHUB_REPOSITORY/releases/download/$tag" + dart run tools/release_metadata.dart \ + --version "$VERSION" \ + --build-number "$BUILD_NUMBER" \ + --base-url "$base" \ + --notes-url "https://github.com/$GITHUB_REPOSITORY/releases/tag/$tag" \ + --published-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --out dist \ + --asset "macos=dist/pinbench-$VERSION-macos.dmg" \ + --asset "windows=dist/pinbench-$VERSION-windows-setup.exe" \ + --asset "linux=dist/pinbench-$VERSION-linux.tar.gz" + (cd dist && sha256sum pinbench-* > SHA256SUMS.txt) + ls -la dist + cat dist/SHA256SUMS.txt + + - name: Keep the trial's files + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: release-trial + path: dist/* + retention-days: 7 + + - name: Draft the release + if: github.event_name == 'push' + run: | + gh release create "$GITHUB_REF_NAME" dist/* \ + --draft \ + --title "PinBench $VERSION" \ + --generate-notes \ + --notes "Desktop builds for macOS (Apple silicon and Intel), Windows and Linux. Not yet code-signed: on macOS, right-click the app and choose Open the first time; on Windows, the installer shows a SmartScreen prompt." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6412335..b497067 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -187,3 +187,18 @@ For anything security-related, **do not open a public issue** — see ## Code of conduct By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). + +## Releasing + +Push a version tag; `.github/workflows/release.yml` does the rest: + +```bash +git tag v0.2.0 && git push origin v0.2.0 +``` + +It builds macOS, Windows and Linux, packages a `.dmg`, the Inno Setup +installer and a `.tar.gz`, writes the updater's appcasts and `latest.json` +(`tools/release_metadata.dart`) and `SHA256SUMS.txt`, and makes a **draft** +release. Publishing the draft is what the updater and the download page see. +A tag `vX.Y.Z` gets build number `X*10000 + Y*100 + Z`, so tags must only go +up. The builds are not code-signed yet. From 9817593b3e56cf8ac0f22dc030c5642d984d5120 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Sat, 3 Oct 2026 06:40:10 +0200 Subject: [PATCH 3/3] Build the macOS release without a provisioning profile; sign it ad hoc The release trial failed on macOS: "No profiles for 'dev.pinbench.app' were found". The Release target signs automatically with the team's identity, and CODE_SIGNING_ALLOWED=NO as a plain environment variable never reaches xcodebuild; Flutter passes FLUTTER_XCODE_* through as build settings, so those are what turn signing off (here and in ci.yml's build-desktop, which had the same problem but runs only on native changes). An unsigned app does not run on Apple silicon, so the release signs it ad hoc, with the release entitlements less keychain-access-groups, which needs a provisioning profile (an app claiming it without one is killed at launch; it serves the pro edition's sign-in, absent from this build), verifies the signature, and launches it for 15 s. Tried on a Mac first: a universal app, a valid ad hoc signature, running after 15 s. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 9 ++++++--- .github/workflows/release.yml | 32 +++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a4f96b..87b1f81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -376,11 +376,14 @@ jobs: - name: Generate code run: dart run build_runner build --delete-conflicting-outputs + # Flutter hands FLUTTER_XCODE_* to xcodebuild as build settings; plain + # CODE_SIGNING_* variables never reach it, and the Release target's + # automatic signing then wants a provisioning profile CI cannot have. - name: Build Desktop App env: - CODE_SIGNING_ALLOWED: "NO" - CODE_SIGNING_REQUIRED: "NO" - CODE_SIGN_IDENTITY: "" + FLUTTER_XCODE_CODE_SIGNING_ALLOWED: "NO" + FLUTTER_XCODE_CODE_SIGNING_REQUIRED: "NO" + FLUTTER_XCODE_CODE_SIGN_IDENTITY: "" run: flutter build ${{ matrix.platform }} --release # One step instead of four `if:`-guarded ones. Every matrix leg used to diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9dae70..7b4ba08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,8 @@ name: Release # releasing, which is how a change here is tested. # # Not signed yet: there is no Apple Developer ID or Windows certificate. Until -# there is, macOS asks to confirm opening the app (right-click, Open) and -# Windows shows a SmartScreen prompt for the installer. +# there is, the macOS app is signed ad hoc, so macOS asks to confirm opening it +# (right-click, Open), and Windows shows a SmartScreen prompt for the installer. on: push: tags: ["v*"] @@ -110,12 +110,34 @@ jobs: - name: Build shell: bash + # Flutter hands FLUTTER_XCODE_* to xcodebuild as build settings; plain + # CODE_SIGNING_* variables never reach it, and the Release target's + # automatic signing then wants a provisioning profile CI cannot have. env: - CODE_SIGNING_ALLOWED: "NO" - CODE_SIGNING_REQUIRED: "NO" - CODE_SIGN_IDENTITY: "" + FLUTTER_XCODE_CODE_SIGNING_ALLOWED: "NO" + FLUTTER_XCODE_CODE_SIGNING_REQUIRED: "NO" + FLUTTER_XCODE_CODE_SIGN_IDENTITY: "" run: flutter build ${{ matrix.platform }} --release --build-name "$VERSION" --build-number "$BUILD_NUMBER" + # Apple silicon will not run an unsigned app, so until there is a + # Developer ID it is signed ad hoc, with the release entitlements less + # keychain-access-groups: that one needs a provisioning profile, and an + # app claiming it without one is killed at launch. (It serves the pro + # edition's sign-in, which this build does not include.) Then it is + # started, to prove macOS lets it run. + - name: Sign ad hoc, and launch (macOS) + if: matrix.platform == 'macos' + run: | + app=build/macos/Build/Products/Release/pinbench.app + cp macos/Runner/Release.entitlements "$RUNNER_TEMP/adhoc.entitlements" + plutil -remove keychain-access-groups "$RUNNER_TEMP/adhoc.entitlements" + codesign --force --deep --sign - --entitlements "$RUNNER_TEMP/adhoc.entitlements" "$app" + codesign --verify --deep --strict --verbose=2 "$app" + "$app/Contents/MacOS/pinbench" & pid=$! + sleep 15 + kill -0 "$pid" || { echo "::error::the app exited within 15 s of launch"; exit 1; } + kill "$pid" + - name: Package (macOS disk image) if: matrix.platform == 'macos' run: |