name: Release # Tag a commit v1.2.3 and push the tag. This builds, for Windows, the installer and the # two portable exes, and for macOS (Apple silicon and Intel) a disk image of Perch.app and # a perch-cli tarball, then publishes them all, with SHA-256 sidecars, as one GitHub release. # # macOS builds are ad hoc signed unless the signing secrets below exist, in which case # they are signed with a Developer ID and notarised: # MACOS_CERTIFICATE base64 of a Developer ID Application .p12 # MACOS_CERTIFICATE_PASSWORD its password # MACOS_SIGN_IDENTITY "Developer ID Application: Name (TEAMID)" # MACOS_NOTARY_APPLE_ID, MACOS_NOTARY_PASSWORD, MACOS_NOTARY_TEAM_ID on: push: tags: - "v*" permissions: contents: write jobs: version: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} steps: - name: Read version from the tag id: version run: | version="${GITHUB_REF_NAME#v}" if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then echo "::error::Tag '$GITHUB_REF_NAME' is not a version tag like v1.2.3." exit 1 fi echo "version=$version" >> "$GITHUB_OUTPUT" # Windows binaries are code signed through SignPath Foundation once SIGNPATH_API_TOKEN # exists (see "Code signing policy" in the README); until then they ship unsigned. # Signing happens twice: the programs, then the installer built from the signed programs. # Each request waits for an approver to approve it in SignPath. windows: needs: version runs-on: windows-latest env: VERSION: ${{ needs.version.outputs.version }} SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: dotnet-version: "9.0.x" - name: Test run: dotnet run --project tests/Perch.Tests -c Release - name: Publish shell: pwsh run: ./packaging/windows/build.ps1 -Version $env:VERSION -Step publish - name: Upload the programs for signing id: unsigned-programs if: env.SIGNPATH_API_TOKEN != '' uses: actions/upload-artifact@v7 with: name: unsigned-programs retention-days: 1 path: | dist/app/Perch.exe dist/app/perch-cli.exe dist/app/Perch.dll dist/app/perch-cli.dll dist/app/Perch.Core.dll dist/app/Perch.Platform.dll dist/portable/Perch.exe dist/portable/perch-cli.exe - name: Sign the programs if: env.SIGNPATH_API_TOKEN != '' uses: signpath/github-action-submit-signing-request@v3 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'perch' }} signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }} artifact-configuration-slug: binaries github-artifact-id: ${{ steps.unsigned-programs.outputs.artifact-id }} parameters: | version: "${{ env.VERSION }}" wait-for-completion: true wait-for-completion-timeout-in-seconds: 3600 # The artifact's paths are relative to dist, so this puts each file back in place. output-artifact-directory: dist - name: Package shell: pwsh run: ./packaging/windows/build.ps1 -Version $env:VERSION -Step package # Under a fixed name, so the SignPath artifact configuration need not know the version. - name: Stage the installer for signing if: env.SIGNPATH_API_TOKEN != '' shell: pwsh run: | New-Item -ItemType Directory -Force dist/installer | Out-Null Move-Item "dist/Perch-$env:VERSION-setup.exe" dist/installer/Perch-setup.exe - name: Upload the installer for signing id: unsigned-installer if: env.SIGNPATH_API_TOKEN != '' uses: actions/upload-artifact@v7 with: name: unsigned-installer retention-days: 1 path: dist/installer/Perch-setup.exe - name: Sign the installer if: env.SIGNPATH_API_TOKEN != '' uses: signpath/github-action-submit-signing-request@v3 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'perch' }} signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }} artifact-configuration-slug: installer github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }} parameters: | version: "${{ env.VERSION }}" wait-for-completion: true wait-for-completion-timeout-in-seconds: 3600 output-artifact-directory: dist/installer - name: Put the signed installer back if: env.SIGNPATH_API_TOKEN != '' shell: pwsh run: | Move-Item dist/installer/Perch-setup.exe "dist/Perch-$env:VERSION-setup.exe" Remove-Item -Recurse dist/installer - name: Check the signatures if: env.SIGNPATH_API_TOKEN != '' shell: pwsh run: | $files = Get-ChildItem dist/*.exe foreach ($file in $files) { $signature = Get-AuthenticodeSignature $file.FullName "{0,-40} {1}" -f $file.Name, $signature.Status if ($signature.Status -ne 'Valid') { throw "$($file.Name) is not validly signed." } } - uses: actions/upload-artifact@v7 with: name: release-windows path: dist/*.exe macos: needs: version runs-on: macos-latest strategy: matrix: rid: [osx-arm64, osx-x64] env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: dotnet-version: "9.0.x" - name: Test run: dotnet run --project tests/Perch.Tests -c Release - name: Import the signing certificate if: env.MACOS_CERTIFICATE != '' env: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} run: | keychain="$RUNNER_TEMP/signing.keychain-db" password=$(uuidgen) security create-keychain -p "$password" "$keychain" security set-keychain-settings -lut 21600 "$keychain" security unlock-keychain -p "$password" "$keychain" echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" security import "$RUNNER_TEMP/certificate.p12" -P "$MACOS_CERTIFICATE_PASSWORD" \ -A -t cert -f pkcs12 -k "$keychain" security set-key-partition-list -S apple-tool:,apple: -k "$password" "$keychain" security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') rm "$RUNNER_TEMP/certificate.p12" - name: Package env: MACOS_SIGN_IDENTITY: ${{ env.MACOS_CERTIFICATE != '' && secrets.MACOS_SIGN_IDENTITY || '' }} MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }} MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} run: packaging/macos/build.sh "${{ needs.version.outputs.version }}" "${{ matrix.rid }}" - uses: actions/upload-artifact@v7 with: name: release-${{ matrix.rid }} path: | dist/*.dmg dist/*.tar.gz publish: needs: [windows, macos] runs-on: ubuntu-latest permissions: contents: write actions: write # to start the WinGet workflow steps: - uses: actions/download-artifact@v8 with: pattern: release-* path: dist merge-multiple: true - name: Checksum working-directory: dist run: | for file in *; do sha256sum "$file" > "$file.sha256" done ls -l - name: Create the release env: GH_TOKEN: ${{ github.token }} run: | gh release create "$GITHUB_REF_NAME" dist/* \ --repo "$GITHUB_REPOSITORY" \ --title "$GITHUB_REF_NAME" \ --generate-notes # A release created with this workflow's token does not fire the "released" event # for other workflows (GitHub blocks that to prevent loops), so winget.yml never # heard about releases made here. Starting it directly is the one route allowed. - name: Start the WinGet update env: GH_TOKEN: ${{ github.token }} run: gh workflow run winget.yml --repo "$GITHUB_REPOSITORY" --ref main -f tag="$GITHUB_REF_NAME"