Merge pull request #169 from realgarit/codex/release-recovery-notes #312
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| inputs: | |
| validate_ref: | |
| description: Release Please branch to validate | |
| required: false | |
| type: string | |
| release_tag: | |
| description: Existing draft release tag to rebuild after a failed release run | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || inputs.release_tag || inputs.validate_ref || 'release' }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' || inputs.validate_ref != '' }} | |
| jobs: | |
| release-please: | |
| name: Manage Release | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.validate_ref == '') | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| outputs: | |
| release_created: ${{ steps.release.outputs.release_created || steps.recover.outputs.release_created }} | |
| tag_name: ${{ steps.release.outputs.tag_name || steps.recover.outputs.tag_name }} | |
| version: ${{ steps.release.outputs.version || steps.recover.outputs.version }} | |
| release_sha: ${{ github.sha }} | |
| steps: | |
| - name: Create or update release pull request | |
| id: release | |
| if: github.event_name != 'workflow_dispatch' || inputs.release_tag == '' | |
| uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| - name: Checkout recovery source | |
| if: github.event_name == 'workflow_dispatch' && inputs.release_tag != '' | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 0 | |
| - name: Recover existing draft release | |
| id: recover | |
| if: github.event_name == 'workflow_dispatch' && inputs.release_tag != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Release tag '$RELEASE_TAG' is not a stable semantic version." | |
| exit 1 | |
| fi | |
| VERSION="${RELEASE_TAG#v}" | |
| bash scripts/validate-version-sync.sh "$VERSION" | |
| MANIFEST_VERSION="$(jq -r '."."' .release-please-manifest.json)" | |
| if [[ "$MANIFEST_VERSION" != "$VERSION" ]]; then | |
| echo "::error::.release-please-manifest.json contains '$MANIFEST_VERSION', expected '$VERSION'." | |
| exit 1 | |
| fi | |
| TAG_SHA="$(git ls-remote --tags origin "refs/tags/$RELEASE_TAG" | awk '{print $1}')" | |
| if [[ -z "$TAG_SHA" ]]; then | |
| echo "::error::Release tag '$RELEASE_TAG' was not found on origin." | |
| exit 1 | |
| fi | |
| gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json databaseId --jq .databaseId > /dev/null | |
| echo "release_created=true" >> "$GITHUB_OUTPUT" | |
| echo "tag_name=$RELEASE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Start release pull request validation | |
| if: steps.release.outputs.prs_created == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_PR: ${{ steps.release.outputs.pr }} | |
| run: | | |
| RELEASE_REF="$(jq -r '.headBranchName' <<< "$RELEASE_PR")" | |
| gh workflow run build.yml --repo "$GITHUB_REPOSITORY" --ref "$RELEASE_REF" -f validate_ref="$RELEASE_REF" | |
| validate-release: | |
| name: Validate Release Version | |
| runs-on: ubuntu-latest | |
| needs: release-please | |
| if: needs.release-please.outputs.release_created == 'true' | |
| outputs: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Checkout release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| # Draft GitHub releases do not create their tag until publication. | |
| ref: ${{ needs.release-please.outputs.release_sha }} | |
| fetch-depth: 0 | |
| - name: Match tag and project versions | |
| id: version | |
| env: | |
| RELEASE_TAG: ${{ needs.release-please.outputs.tag_name }} | |
| RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Release tag '$RELEASE_TAG' is not a stable semantic version." | |
| exit 1 | |
| fi | |
| VERSION="${RELEASE_TAG#v}" | |
| bash scripts/validate-version-sync.sh "$VERSION" | |
| MANIFEST_VERSION="$(jq -r '."."' .release-please-manifest.json)" | |
| if [[ "$MANIFEST_VERSION" != "$VERSION" ]]; then | |
| echo "::error::.release-please-manifest.json contains '$MANIFEST_VERSION', expected '$VERSION'." | |
| exit 1 | |
| fi | |
| if [[ "$(git rev-parse HEAD)" != "$RELEASE_SHA" ]]; then | |
| echo "::error::Checked out commit does not match Release Please SHA '$RELEASE_SHA'." | |
| exit 1 | |
| fi | |
| git fetch origin main | |
| if ! git merge-base --is-ancestor HEAD origin/main; then | |
| echo "::error::Release commit '$RELEASE_SHA' is not reachable from main." | |
| exit 1 | |
| fi | |
| echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| build: | |
| name: Build ${{ matrix.runtime }} on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| needs: [release-please, validate-release] | |
| if: needs.release-please.outputs.release_created == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| runtime: win-x64 | |
| - os: macos-latest | |
| runtime: osx-x64 | |
| - os: macos-latest | |
| runtime: osx-arm64 | |
| - os: ubuntu-latest | |
| runtime: linux-x64 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: ${{ needs.release-please.outputs.release_sha }} | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Download PowerShell modules (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: ./scripts/download-modules.ps1 | |
| - name: Download PowerShell modules (macOS/Linux) | |
| if: matrix.os != 'windows-latest' | |
| shell: bash | |
| run: | | |
| chmod +x scripts/download-modules.sh | |
| ./scripts/download-modules.sh | |
| - name: Restore dependencies | |
| run: dotnet restore PhoneDesk.slnx | |
| - name: Build | |
| run: dotnet build PhoneDesk.slnx --configuration Release --no-restore | |
| - name: Run tests | |
| run: dotnet test PhoneDesk.Tests/PhoneDesk.Tests.csproj --configuration Release --no-build --verbosity normal | |
| - name: Publish | |
| # Self-contained so users need no .NET runtime install. | |
| # OutputType is set in the project (WinExe). It must NOT be passed as a global -p:OutputType, | |
| # because that propagates to the referenced class libraries and makes them fail with CS5001 | |
| # (no Main). | |
| run: dotnet publish phonedesk.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true --output ./publish/${{ matrix.runtime }} | |
| - name: Import signing certificate (macOS) | |
| if: startsWith(matrix.runtime, 'osx-') | |
| env: | |
| P12_BASE64: ${{ secrets.SIGNING_CERT_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.SIGNING_CERT_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| printf '%s' "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/signing.p12" | |
| export KEYCHAIN="$RUNNER_TEMP/tpm-ci.keychain-db" | |
| chmod +x scripts/import-cert.sh | |
| scripts/import-cert.sh "$RUNNER_TEMP/signing.p12" | |
| rm -f "$RUNNER_TEMP/signing.p12" | |
| echo "KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV" | |
| - name: Package (macOS .app bundle, stable self-signed identity) | |
| if: startsWith(matrix.runtime, 'osx-') | |
| env: | |
| VERSION: ${{ needs.validate-release.outputs.version }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| chmod +x scripts/package-macos.sh | |
| SIGN_IDENTITY="Teams Phone Manager Self-Signed" \ | |
| ./scripts/package-macos.sh ./publish/${{ matrix.runtime }} "$VERSION" ./dist | |
| ditto -c -k --keepParent "./dist/PhoneDesk.app" "./dist/phonedesk-${{ matrix.runtime }}.zip" | |
| - name: Package (Windows zip + Inno Setup installer) | |
| if: matrix.runtime == 'win-x64' | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ needs.validate-release.outputs.version }} | |
| run: | | |
| New-Item -ItemType Directory -Force -Path ./dist | Out-Null | |
| Compress-Archive -Path ./publish/win-x64/* -DestinationPath ./dist/phonedesk-win-x64.zip | |
| iscc /DAppVersion=$env:VERSION /DPublishDir=..\..\publish\win-x64 /O.\dist installer\windows\phonedesk.iss | |
| - name: Package (Linux zip) | |
| if: matrix.runtime == 'linux-x64' | |
| run: | | |
| mkdir -p ./dist | |
| chmod +x ./publish/linux-x64/phonedesk | |
| cd ./publish/linux-x64 | |
| zip -r ../../dist/phonedesk-linux-x64.zip . | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: phonedesk-${{ matrix.runtime }} | |
| path: ./dist/* | |
| retention-days: 30 | |
| upload-release-assets: | |
| name: Upload Release Assets | |
| needs: [release-please, validate-release, build] | |
| runs-on: ubuntu-latest | |
| if: needs.release-please.outputs.release_created == 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download all artifacts | |
| # Build jobs upload finished assets (zips / setup exe), so a flat merge is enough. | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: phonedesk-* | |
| path: ./artifacts | |
| merge-multiple: true | |
| - name: List downloaded artifacts | |
| run: ls -lh artifacts/ | |
| - name: Add legacy-named installer for pre-rebrand updaters | |
| # In-app updaters shipped before the PhoneDesk rebrand only accept a release | |
| # asset named exactly teams-phonemanager-win-x64-setup.exe (strict filename | |
| # check in their GitHubUpdateInstallerService). Attach the same installer | |
| # under the old name so existing installs can still self-update. | |
| working-directory: artifacts | |
| run: cp phonedesk-win-x64-setup.exe teams-phonemanager-win-x64-setup.exe | |
| - name: Generate SHA-256 manifest | |
| working-directory: artifacts | |
| run: sha256sum phonedesk-* teams-phonemanager-win-x64-setup.exe > SHA256SUMS | |
| - name: Publish draft release | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.validate-release.outputs.tag_name }} | |
| RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} | |
| run: | | |
| set -euo pipefail | |
| gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \ | |
| artifacts/phonedesk-win-x64.zip \ | |
| artifacts/phonedesk-win-x64-setup.exe \ | |
| artifacts/teams-phonemanager-win-x64-setup.exe \ | |
| artifacts/phonedesk-osx-x64.zip \ | |
| artifacts/phonedesk-osx-arm64.zip \ | |
| artifacts/phonedesk-linux-x64.zip \ | |
| artifacts/SHA256SUMS | |
| RELEASE_ID="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json databaseId --jq .databaseId)" | |
| gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \ | |
| -F draft=false \ | |
| -f target_commitish="$RELEASE_SHA" | |
| bump-homebrew-cask: | |
| name: Update Homebrew cask | |
| needs: [release-please, validate-release, upload-release-assets] | |
| runs-on: ubuntu-latest | |
| if: needs.release-please.outputs.release_created == 'true' | |
| steps: | |
| - name: Checkout tap | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| repository: realgarit/homebrew-tap | |
| token: ${{ secrets.PAT_TOKEN }} | |
| - name: Regenerate cask | |
| env: | |
| TAG: ${{ needs.validate-release.outputs.tag_name }} | |
| run: | | |
| VERSION="${TAG#v}" | |
| BASE="https://github.com/realgarit/phonedesk/releases/download/${TAG}" | |
| curl -fsSL -o arm64.zip "${BASE}/phonedesk-osx-arm64.zip" | |
| curl -fsSL -o x64.zip "${BASE}/phonedesk-osx-x64.zip" | |
| SHA_ARM=$(sha256sum arm64.zip | cut -d' ' -f1) | |
| SHA_X64=$(sha256sum x64.zip | cut -d' ' -f1) | |
| rm arm64.zip x64.zip | |
| mkdir -p Casks | |
| cat > Casks/phonedesk.rb <<EOF | |
| cask "phonedesk" do | |
| version "${VERSION}" | |
| on_arm do | |
| sha256 "${SHA_ARM}" | |
| url "https://github.com/realgarit/phonedesk/releases/download/v#{version}/phonedesk-osx-arm64.zip" | |
| end | |
| on_intel do | |
| sha256 "${SHA_X64}" | |
| url "https://github.com/realgarit/phonedesk/releases/download/v#{version}/phonedesk-osx-x64.zip" | |
| end | |
| name "PhoneDesk" | |
| desc "Microsoft Teams Phone System administration made simple" | |
| homepage "https://github.com/realgarit/phonedesk" | |
| depends_on macos: :monterey | |
| app "PhoneDesk.app" | |
| # The app is ad-hoc signed (not notarized); clear quarantine so | |
| # Gatekeeper does not block the launch. | |
| postflight do | |
| system_command "/usr/bin/xattr", | |
| args: ["-dr", "com.apple.quarantine", "#{appdir}/PhoneDesk.app"], | |
| sudo: false | |
| end | |
| zap trash: [ | |
| "~/Library/Preferences/ch.realgar.teams-phonemanager.plist", | |
| ] | |
| end | |
| EOF | |
| - name: Commit and push | |
| env: | |
| TAG: ${{ needs.validate-release.outputs.tag_name }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add Casks/phonedesk.rb | |
| if git diff --cached --quiet; then | |
| echo "Cask unchanged, nothing to push" | |
| else | |
| git commit -m "phonedesk ${TAG}" | |
| git push | |
| fi | |
| pr-validate: | |
| name: PR Validate ${{ matrix.runtime }} | |
| # Pre-merge gate: build + test + publish on every runtime without releasing. | |
| if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.validate_ref != '') | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| runtime: win-x64 | |
| - os: macos-latest | |
| runtime: osx-x64 | |
| - os: macos-latest | |
| runtime: osx-arm64 | |
| - os: ubuntu-latest | |
| runtime: linux-x64 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| # For PR events, use the head SHA (not the merge ref) to avoid race | |
| # where the merge ref is deleted before the runner picks up the job. | |
| ref: ${{ inputs.validate_ref || github.event.pull_request.head.sha || github.ref }} | |
| - name: Validate version source alignment | |
| shell: bash | |
| run: bash scripts/validate-version-sync.sh | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Restore dependencies | |
| run: dotnet restore PhoneDesk.slnx | |
| - name: Build | |
| run: dotnet build PhoneDesk.slnx --configuration Release --no-restore | |
| - name: Run tests | |
| run: dotnet test PhoneDesk.Tests/PhoneDesk.Tests.csproj --configuration Release --no-build --verbosity normal | |
| - name: Publish (packaging smoke, not released) | |
| run: dotnet publish phonedesk.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true --output ./publish/${{ matrix.runtime }} | |
| - name: Package smoke (macOS .app bundle) | |
| if: startsWith(matrix.runtime, 'osx-') | |
| run: | | |
| chmod +x scripts/package-macos.sh | |
| ./scripts/package-macos.sh ./publish/${{ matrix.runtime }} 0.0.0 ./dist | |
| ditto -c -k --keepParent "./dist/PhoneDesk.app" "./dist/phonedesk-${{ matrix.runtime }}.zip" | |
| - name: Package smoke (Windows installer) | |
| if: matrix.runtime == 'win-x64' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path ./dist | Out-Null | |
| iscc /DAppVersion=0.0.0 /DPublishDir=..\..\publish\win-x64 /O.\dist installer\windows\phonedesk.iss |