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 new file mode 100644 index 0000000..7b4ba08 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,240 @@ +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, 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*"] + 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 + # 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: + 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: | + 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/.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: 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.