Release CE #378
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: Release CE | |
| # Release workflow for WhoDB Community Edition | |
| # | |
| # Triggers: | |
| # - Push to release branch (e.g., merging main → release) | |
| # - Manual workflow_dispatch | |
| # | |
| # Modes: | |
| # - stage-only: Deploys to edge/draft channels (for testing) | |
| # - production: Deploys to stable/production channels (for users) | |
| # | |
| # Store Selection: | |
| # - Works in BOTH modes - select which stores to deploy to | |
| # - Can deploy to all stores or just specific ones | |
| # - Stage deploys to: Docker (tag only), Snap (edge), MS Store (draft), Apple (TestFlight) | |
| # - Production deploys to: Docker (latest), Snap (stable), MS Store (prod), Apple (App Store) | |
| on: | |
| push: | |
| branches: [ release ] | |
| workflow_dispatch: | |
| inputs: | |
| deployment-mode: | |
| description: 'Deployment Mode' | |
| required: true | |
| type: choice | |
| default: 'stage-only' | |
| options: | |
| - 'stage-only' # Deploy selected stores to edge/draft channels | |
| - 'production' # Deploy selected stores to stable/production channels | |
| version-bump: | |
| description: 'Version Bump Type (ignored if explicit version is set)' | |
| required: true | |
| type: choice | |
| default: 'minor' | |
| options: | |
| - 'major' # 1.0.0 -> 2.0.0 | |
| - 'minor' # 0.61.0 -> 0.62.0 | |
| - 'patch' # 0.61.0 -> 0.61.1 | |
| - 'current' # Use current version (rebuild without increment) | |
| explicit-version: | |
| description: 'Explicit Version (e.g., 0.65.0) - overrides version bump' | |
| required: false | |
| type: string | |
| default: '' | |
| # Store selection (applies to both stage and production modes) | |
| deploy-docker: | |
| description: 'Deploy to Docker Hub' | |
| required: false | |
| type: boolean | |
| default: false | |
| deploy-snap: | |
| description: 'Deploy to Snap Store' | |
| required: false | |
| type: boolean | |
| default: false | |
| deploy-microsoft: | |
| description: 'Deploy to Microsoft Store' | |
| required: false | |
| type: boolean | |
| default: false | |
| deploy-apple: | |
| description: 'Deploy to Apple App Store' | |
| required: false | |
| type: boolean | |
| default: false | |
| # DISABLED: AppImage requires system WebKit2GTK - not truly portable | |
| # deploy-appimage: | |
| # description: 'Build AppImage (for GitHub Release)' | |
| # required: false | |
| # type: boolean | |
| # default: false | |
| deploy-linux-terminal: | |
| description: 'Build Linux terminal binaries (amd64, arm64, riscv64, armv6, armv7)' | |
| required: false | |
| type: boolean | |
| default: false | |
| deploy-cli: | |
| description: 'Build CLI binaries (all platforms) - also generates Homebrew formula' | |
| required: false | |
| type: boolean | |
| default: false | |
| deploy-npm: | |
| description: 'Publish to npm registry (@clidey/whodb-cli)' | |
| required: false | |
| type: boolean | |
| default: false | |
| publish-github-release: | |
| description: 'Publish GitHub release to production (leave unchecked to keep as draft)' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| actions: write | |
| id-token: write | |
| jobs: | |
| # Step 1: Calculate version and deployment parameters | |
| calculate-version: | |
| name: Calculate Version | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' | |
| outputs: | |
| version: ${{ steps.calc.outputs.version }} | |
| previous-version: ${{ steps.calc.outputs.previous-version }} | |
| deployment-mode: ${{ steps.calc.outputs.deployment-mode }} | |
| stage-only: ${{ steps.calc.outputs.stage-only }} | |
| deploy-docker: ${{ steps.calc.outputs.deploy-docker }} | |
| deploy-snap: ${{ steps.calc.outputs.deploy-snap }} | |
| deploy-microsoft: ${{ steps.calc.outputs.deploy-microsoft }} | |
| deploy-apple: ${{ steps.calc.outputs.deploy-apple }} | |
| # deploy-appimage: ${{ steps.calc.outputs.deploy-appimage }} # DISABLED | |
| deploy-linux-terminal: ${{ steps.calc.outputs.deploy-linux-terminal }} | |
| deploy-cli: ${{ steps.calc.outputs.deploy-cli }} | |
| deploy-npm: ${{ steps.calc.outputs.deploy-npm }} | |
| publish-github-release: ${{ steps.calc.outputs.publish-github-release }} | |
| release-notes: ${{ steps.release_notes.outputs.notes }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| submodules: false | |
| - name: Determine deployment mode and version bump | |
| id: deployment_mode | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| MODE="${{ inputs.deployment-mode }}" | |
| VERSION_BUMP="${{ inputs.version-bump || 'minor' }}" | |
| else | |
| # Push to release branch triggers production with minor bump | |
| MODE="production" | |
| VERSION_BUMP="minor" | |
| fi | |
| echo "mode=$MODE" >> $GITHUB_OUTPUT | |
| echo "version_bump=$VERSION_BUMP" >> $GITHUB_OUTPUT | |
| - name: Calculate version and parameters | |
| id: calc | |
| uses: ./.github/actions/calculate-version | |
| with: | |
| deployment-mode: ${{ steps.deployment_mode.outputs.mode }} | |
| version-bump: ${{ steps.deployment_mode.outputs.version_bump }} | |
| explicit-version: ${{ inputs.explicit-version }} | |
| deploy-docker: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-docker) && 'true' || 'false') }} | |
| deploy-snap: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-snap) && 'true' || 'false') }} | |
| deploy-microsoft: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-microsoft) && 'true' || 'false') }} | |
| deploy-apple: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-apple) && 'true' || 'false') }} | |
| deploy-appimage: 'false' # DISABLED: AppImage requires system WebKit2GTK | |
| deploy-linux-terminal: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-linux-terminal) && 'true' || 'false') }} | |
| deploy-cli: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-cli) && 'true' || 'false') }} | |
| deploy-npm: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.deploy-npm) && 'true' || 'false') }} | |
| publish-github-release: ${{ github.event_name == 'push' && 'true' || (fromJSON(inputs.publish-github-release) && 'true' || 'false') }} | |
| - name: Extract release notes from PR | |
| id: release_notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| NOTES="" | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| # Find the PR that was merged from this push by looking at the commit | |
| PR_NUMBER=$(gh pr list --state merged --base release --limit 1 --json number --jq '.[0].number // empty') | |
| if [ -n "$PR_NUMBER" ]; then | |
| echo "Extracting release notes from PR #$PR_NUMBER..." | |
| NOTES=$(gh pr view "$PR_NUMBER" --json body --jq '.body // empty') | |
| if [ -n "$NOTES" ]; then | |
| echo "Found release notes from PR description" | |
| fi | |
| fi | |
| fi | |
| # Default if no notes found | |
| if [ -z "$NOTES" ]; then | |
| NOTES="Bug fixes and performance improvements." | |
| echo "Using default release notes" | |
| fi | |
| # Output using heredoc for multiline support | |
| { | |
| echo "notes<<EOF" | |
| echo "$NOTES" | |
| echo "EOF" | |
| } >> $GITHUB_OUTPUT | |
| - name: Display configuration | |
| run: | | |
| echo "# 🎯 Release Configuration" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Version** | ${{ steps.calc.outputs.version }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Mode** | ${{ steps.calc.outputs.deployment-mode }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Stage Only** | ${{ steps.calc.outputs.stage-only }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Publish GitHub Release** | ${{ steps.calc.outputs.publish-github-release }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## Deployment Targets" >> $GITHUB_STEP_SUMMARY | |
| MODE="${{ steps.calc.outputs.deployment-mode }}" | |
| if [ "$MODE" = "production" ]; then | |
| echo "| Store | Deploy | Channel |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|---------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Hub | ${{ steps.calc.outputs.deploy-docker }} | latest |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snap Store | ${{ steps.calc.outputs.deploy-snap }} | stable |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Microsoft Store | ${{ steps.calc.outputs.deploy-microsoft }} | production |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Apple App Store | ${{ steps.calc.outputs.deploy-apple }} | App Store |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Store | Deploy | Channel |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|---------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Hub | ${{ steps.calc.outputs.deploy-docker }} | version tag |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snap Store | ${{ steps.calc.outputs.deploy-snap }} | edge |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Microsoft Store | ${{ steps.calc.outputs.deploy-microsoft }} | draft |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Apple App Store | ${{ steps.calc.outputs.deploy-apple }} | TestFlight |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Step 2a: Build frontend once (shared by all platform builds) | |
| build-frontend: | |
| name: Build Frontend | |
| needs: [ calculate-version ] | |
| uses: ./.github/workflows/_build-frontend.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| # Step 2b: Build platform artifacts in parallel (only what's selected) | |
| build-docker: | |
| name: Build Docker | |
| needs: [ calculate-version, build-frontend ] | |
| if: needs.calculate-version.outputs.deploy-docker == 'true' | |
| uses: ./.github/workflows/_build-docker.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| build-windows: | |
| name: Build Windows | |
| needs: [ calculate-version, build-frontend ] | |
| if: needs.calculate-version.outputs.deploy-microsoft == 'true' | |
| uses: ./.github/workflows/_build-windows.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| platforms: 'windows-amd64,windows-arm64' | |
| secrets: inherit | |
| build-apple: | |
| name: Build Apple | |
| needs: [ calculate-version, build-frontend ] | |
| if: needs.calculate-version.outputs.deploy-apple == 'true' | |
| uses: ./.github/workflows/_build-apple.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| build-snap: | |
| name: Build Snap | |
| needs: [ calculate-version ] | |
| if: needs.calculate-version.outputs.deploy-snap == 'true' | |
| uses: ./.github/workflows/_build-snap.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| # DISABLED: AppImage requires system WebKit2GTK - not truly portable | |
| # build-appimage: | |
| # name: Build AppImage | |
| # needs: [ calculate-version ] | |
| # if: needs.calculate-version.outputs.deploy-appimage == 'true' | |
| # uses: ./.github/workflows/_build-appimage.yml | |
| # with: | |
| # version: ${{ needs.calculate-version.outputs.version }} | |
| # secrets: inherit | |
| build-linux-terminal: | |
| name: Build Linux Terminal | |
| needs: [ calculate-version, build-frontend ] | |
| if: needs.calculate-version.outputs.deploy-linux-terminal == 'true' | |
| uses: ./.github/workflows/_build-linux-terminal.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| build-cli: | |
| name: Build CLI | |
| needs: [ calculate-version ] | |
| if: needs.calculate-version.outputs.deploy-cli == 'true' || needs.calculate-version.outputs.deploy-npm == 'true' | |
| uses: ./.github/workflows/_build-cli.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| build-docker-cli: | |
| name: Build CLI Docker | |
| needs: [ calculate-version ] | |
| if: needs.calculate-version.outputs.deploy-docker == 'true' | |
| uses: ./.github/workflows/_build-docker-cli.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| secrets: inherit | |
| # Step 3: Sign and validate all builds (best effort - continues even if any build fails) | |
| sign-and-validate: | |
| name: Sign and Validate | |
| needs: [ calculate-version, build-docker, build-docker-cli, build-windows, build-apple, build-snap, build-linux-terminal, build-cli ] | |
| if: | | |
| always() && | |
| needs.calculate-version.result == 'success' && | |
| (needs.build-docker.result == 'success' || needs.build-docker.result == 'skipped' || needs.build-docker.result == 'failure') && | |
| (needs.build-docker-cli.result == 'success' || needs.build-docker-cli.result == 'skipped' || needs.build-docker-cli.result == 'failure') && | |
| (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped' || needs.build-windows.result == 'failure') && | |
| (needs.build-apple.result == 'success' || needs.build-apple.result == 'skipped' || needs.build-apple.result == 'failure') && | |
| (needs.build-snap.result == 'success' || needs.build-snap.result == 'skipped' || needs.build-snap.result == 'failure') && | |
| (needs.build-linux-terminal.result == 'success' || needs.build-linux-terminal.result == 'skipped' || needs.build-linux-terminal.result == 'failure') && | |
| (needs.build-cli.result == 'success' || needs.build-cli.result == 'skipped' || needs.build-cli.result == 'failure') | |
| uses: ./.github/workflows/_sign-validate.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| validate-snap: ${{ needs.build-snap.result == 'success' }} | |
| validate-desktop: ${{ needs.build-windows.result == 'success' || needs.build-apple.result == 'success' }} | |
| validate-appimage: false # DISABLED: AppImage builds disabled | |
| # Step 4: Create GitHub release (only if ALL selected platforms built successfully) | |
| create-github-release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [ calculate-version, build-docker, build-docker-cli, build-windows, build-apple, build-snap, build-linux-terminal, build-cli, sign-and-validate ] | |
| if: | | |
| always() && | |
| needs.calculate-version.result == 'success' && | |
| needs.sign-and-validate.result == 'success' && | |
| (needs.calculate-version.outputs.deploy-docker != 'true' || needs.build-docker.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-docker != 'true' || needs.build-docker-cli.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-snap != 'true' || needs.build-snap.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-microsoft != 'true' || needs.build-windows.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-apple != 'true' || needs.build-apple.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-linux-terminal != 'true' || needs.build-linux-terminal.result == 'success') && | |
| (needs.calculate-version.outputs.deploy-cli != 'true' || needs.build-cli.result == 'success') | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| api.github.com:443 | |
| deb.debian.org:443 | |
| deb.debian.org:80 | |
| github.com:443 | |
| uploads.github.com:443 | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| submodules: false | |
| - name: Download Snap packages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| digest-mismatch: warn | |
| pattern: snap-package-* | |
| path: artifacts/ | |
| - name: Download Desktop builds | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| digest-mismatch: warn | |
| pattern: desktop-* | |
| path: artifacts/ | |
| # DISABLED: AppImage builds disabled | |
| # - name: Download AppImages | |
| # uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| # continue-on-error: true | |
| # with: | |
| # pattern: appimage-* | |
| # path: artifacts/ | |
| - name: Download Linux server binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| digest-mismatch: warn | |
| pattern: server-binaries-* | |
| path: artifacts/ | |
| - name: Download CLI binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| digest-mismatch: warn | |
| name: cli-binaries | |
| path: artifacts/cli-binaries/ | |
| - name: Download signatures | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| digest-mismatch: warn | |
| name: signatures | |
| path: artifacts/signatures/ | |
| - name: Prepare release assets | |
| run: | | |
| mkdir -p final-assets | |
| # Copy desktop builds (exe, dmg, msixbundle) | |
| find artifacts -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.msixbundle" \) -exec cp {} final-assets/ \; 2>/dev/null || true | |
| # Copy Linux server binaries | |
| find artifacts -type f -name "whodb-*-linux-*" -exec cp {} final-assets/ \; 2>/dev/null || true | |
| # Copy CLI binaries | |
| find artifacts/cli-binaries -type f -name "whodb-cli-*" -exec cp {} final-assets/ \; 2>/dev/null || true | |
| # Copy signatures (extract and copy only files, not directories) | |
| if [ -f artifacts/signatures/signatures.tar.gz ]; then | |
| mkdir -p /tmp/sig-extract | |
| tar -xzf artifacts/signatures/signatures.tar.gz -C /tmp/sig-extract/ | |
| find /tmp/sig-extract -type f -exec cp {} final-assets/ \; 2>/dev/null || true | |
| rm -rf /tmp/sig-extract | |
| fi | |
| echo "📦 Release assets prepared:" | |
| ls -la final-assets/ || echo "No assets found" | |
| - name: Prepare release body | |
| id: release_body | |
| env: | |
| VERSION: ${{ needs.calculate-version.outputs.version }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| echo "📝 Preparing release body from template..." | |
| # Read the template | |
| TEMPLATE=$(cat .github/templates/RELEASE_TEMPLATE.md) | |
| # Get PR description if this was triggered by a push (merged PR) | |
| PR_DESCRIPTION="" | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| # Find the most recently merged PR to release branch | |
| PR_NUMBER=$(gh pr list --state merged --base release --limit 1 --json number --jq '.[0].number // empty') | |
| if [ -n "$PR_NUMBER" ]; then | |
| PR_DESCRIPTION=$(gh pr view $PR_NUMBER --json body --jq '.body') | |
| echo "Using PR #$PR_NUMBER description" | |
| else | |
| echo "No merged PR found" | |
| fi | |
| else | |
| echo "Not triggered by push - no PR description" | |
| fi | |
| # Replace PR description or remove the placeholder | |
| if [ -n "$PR_DESCRIPTION" ]; then | |
| # Save PR description to temp file to preserve newlines | |
| echo "$PR_DESCRIPTION" > /tmp/pr_description.txt | |
| # Replace version in template | |
| TEMPLATE="${TEMPLATE//\{\{VERSION\}\}/$VERSION}" | |
| # Replace PR_DESCRIPTION with actual file content using awk | |
| awk ' | |
| /{{PR_DESCRIPTION}}/ { | |
| while ((getline line < "/tmp/pr_description.txt") > 0) { | |
| print line | |
| } | |
| close("/tmp/pr_description.txt") | |
| next | |
| } | |
| { print } | |
| ' <<< "$TEMPLATE" > /tmp/release-body.md | |
| else | |
| # Replace version and remove PR_DESCRIPTION placeholder | |
| TEMPLATE="${TEMPLATE//\{\{VERSION\}\}/$VERSION}" | |
| echo "$TEMPLATE" | sed '/{{PR_DESCRIPTION}}/d' > /tmp/release-body.md | |
| fi | |
| echo "✅ Release body prepared" | |
| echo "Preview:" | |
| cat /tmp/release-body.md | |
| - name: Create GitHub release (as draft) | |
| uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 | |
| with: | |
| tag: ${{ needs.calculate-version.outputs.version }} | |
| name: Release ${{ needs.calculate-version.outputs.version }} | |
| # IMPORTANT: Always create as draft first to support immutable releases | |
| # Assets must be attached BEFORE publishing, as immutable releases | |
| # cannot have assets added after publication | |
| draft: true | |
| prerelease: false | |
| generateReleaseNotes: true | |
| artifacts: final-assets/* | |
| bodyFile: /tmp/release-body.md | |
| # Verify and publish the draft release (production runs only) | |
| # This step runs AFTER assets are uploaded by the release-action above | |
| - name: Verify and publish release (if not stage-only) | |
| if: needs.calculate-version.outputs.stage-only != 'true' && needs.calculate-version.outputs.publish-github-release == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.calculate-version.outputs.version }} | |
| run: | | |
| echo "🔍 Verifying draft release ${VERSION} before publishing..." | |
| # Step 1: Verify release exists and is a draft | |
| RELEASE_INFO=$(gh release view "${VERSION}" --json isDraft,assets,body) | |
| IS_DRAFT=$(echo "$RELEASE_INFO" | jq -r '.isDraft') | |
| if [ "$IS_DRAFT" != "true" ]; then | |
| echo "⚠️ Release is not a draft (isDraft=${IS_DRAFT}). It may have been published already." | |
| echo "Skipping publish step." | |
| exit 0 | |
| fi | |
| echo "✅ Release is in draft state" | |
| # Step 2: Verify assets were uploaded | |
| ASSET_COUNT=$(echo "$RELEASE_INFO" | jq '.assets | length') | |
| echo "📦 Found ${ASSET_COUNT} assets attached to release" | |
| if [ "$ASSET_COUNT" -eq 0 ]; then | |
| echo "❌ ERROR: No assets found on draft release!" | |
| echo "This may indicate an upload failure. Check previous steps." | |
| exit 1 | |
| fi | |
| echo "✅ Assets verified (${ASSET_COUNT} files)" | |
| # Step 3: Verify release body is present | |
| BODY_LENGTH=$(echo "$RELEASE_INFO" | jq '.body | length') | |
| if [ "$BODY_LENGTH" -lt 50 ]; then | |
| echo "⚠️ Warning: Release body seems short (${BODY_LENGTH} chars)" | |
| else | |
| echo "✅ Release body present (${BODY_LENGTH} chars)" | |
| fi | |
| # Step 4: Publish the release | |
| echo "" | |
| echo "🚀 Publishing release ${VERSION}..." | |
| gh release edit "${VERSION}" --draft=false | |
| echo "✅ Release ${VERSION} published successfully!" | |
| # Step 5: Final verification | |
| FINAL_STATE=$(gh release view "${VERSION}" --json isDraft --jq '.isDraft') | |
| if [ "$FINAL_STATE" = "false" ]; then | |
| echo "✅ Verified: Release is now public and immutable" | |
| else | |
| echo "❌ ERROR: Release still in draft state after publish attempt" | |
| exit 1 | |
| fi | |
| # Step 5: Deploy to various platforms (after GitHub release created) | |
| deploy-snap: | |
| name: Deploy Snap | |
| needs: [ calculate-version, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-snap == 'true' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-snap.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| channel: ${{ needs.calculate-version.outputs.stage-only == 'true' && 'edge' || 'stable' }} | |
| secrets: inherit | |
| deploy-microsoft: | |
| name: Deploy Microsoft | |
| needs: [ calculate-version, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-microsoft == 'true' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-microsoft.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| stage-only: ${{ needs.calculate-version.outputs.stage-only == 'true' }} | |
| secrets: inherit | |
| deploy-apple: | |
| name: Deploy Apple | |
| needs: [ calculate-version, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-apple == 'true' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-apple.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| stage-only: ${{ needs.calculate-version.outputs.stage-only == 'true' }} | |
| release-notes: ${{ needs.calculate-version.outputs.release-notes }} | |
| secrets: inherit | |
| # Step 6: Deploy to Docker Hub (after GitHub release) | |
| # Rebuilds from GHA buildx cache populated by build-docker and pushes with attestations. | |
| deploy-docker: | |
| name: Deploy Docker | |
| needs: [ calculate-version, build-docker, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-docker == 'true' && | |
| needs.build-docker.result == 'success' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-docker.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| stage-only: ${{ needs.calculate-version.outputs.stage-only == 'true' }} | |
| secrets: inherit | |
| # Step 6b: Deploy CLI to Docker Hub (after GitHub release) | |
| deploy-docker-cli: | |
| name: Deploy CLI Docker | |
| needs: [ calculate-version, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-docker == 'true' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-docker-cli.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| stage-only: ${{ needs.calculate-version.outputs.stage-only == 'true' }} | |
| secrets: inherit | |
| # Step 7: Deploy to npm (after CLI build and GitHub release) | |
| deploy-npm: | |
| name: Deploy npm | |
| needs: [ calculate-version, build-cli, create-github-release ] | |
| if: | | |
| always() && | |
| needs.calculate-version.outputs.deploy-npm == 'true' && | |
| needs.build-cli.result == 'success' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-npm.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| # Step 8: Generate Homebrew formula for CLI (disabled - homebrew-core auto-updates via livecheck) | |
| # To generate manually: .github/scripts/generate-homebrew-formula.sh <version> | |
| deploy-homebrew-formula: | |
| name: Deploy Homebrew Formula | |
| needs: [ calculate-version, create-github-release ] | |
| if: false | |
| uses: ./.github/workflows/_deploy-homebrew-formula.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| # Step 8b: Generate Homebrew cask for desktop app (disabled - run manually when needed) | |
| generate-homebrew-cask: | |
| name: Generate Homebrew Cask | |
| needs: [ calculate-version, create-github-release ] | |
| if: | | |
| false && | |
| needs.calculate-version.outputs.stage-only != 'true' && | |
| needs.calculate-version.outputs.deployment-mode == 'production' && | |
| needs.create-github-release.result == 'success' | |
| uses: ./.github/workflows/_deploy-homebrew.yml | |
| with: | |
| version: ${{ needs.calculate-version.outputs.version }} | |
| # Step 10: Stamp plugin.json version on main after successful release | |
| stamp-plugin-version: | |
| name: Stamp Plugin Version on Main | |
| runs-on: ubuntu-latest | |
| needs: [calculate-version, create-github-release] | |
| if: | | |
| needs.create-github-release.result == 'success' && | |
| needs.calculate-version.outputs.stage-only != 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout main | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: main | |
| submodules: false | |
| - name: Stamp and push plugin.json | |
| env: | |
| VERSION: ${{ needs.calculate-version.outputs.version }} | |
| run: | | |
| PLUGIN_JSON="cli/external-plugin/whodb/.claude-plugin/plugin.json" | |
| jq --arg v "$VERSION" '.version = $v' "$PLUGIN_JSON" > "$PLUGIN_JSON.tmp" | |
| mv "$PLUGIN_JSON.tmp" "$PLUGIN_JSON" | |
| if git diff --quiet "$PLUGIN_JSON"; then | |
| echo "plugin.json already at version $VERSION" | |
| else | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add "$PLUGIN_JSON" | |
| git commit -m "chore: stamp plugin.json version to $VERSION" | |
| git push origin main | |
| echo "Stamped plugin.json to $VERSION on main" | |
| fi | |
| # Step 11: Final summary | |
| deployment-summary: | |
| name: Deployment Summary | |
| runs-on: ubuntu-latest | |
| if: always() | |
| needs: [ | |
| calculate-version, | |
| build-docker, | |
| build-docker-cli, | |
| build-windows, | |
| build-apple, | |
| build-snap, | |
| build-linux-terminal, | |
| build-cli, | |
| sign-and-validate, | |
| deploy-homebrew-formula, | |
| deploy-docker, | |
| deploy-docker-cli, | |
| deploy-snap, | |
| deploy-microsoft, | |
| deploy-apple, | |
| deploy-npm, | |
| create-github-release, | |
| generate-homebrew-cask, | |
| stamp-plugin-version | |
| ] | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| deb.debian.org:443 | |
| deb.debian.org:80 | |
| - name: Generate comprehensive summary | |
| run: | | |
| echo "# 📊 Release Summary for ${{ needs.calculate-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Deployment Mode:** ${{ needs.calculate-version.outputs.deployment-mode }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Timestamp:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Build Status | |
| echo "## 🔨 Build Status" >> $GITHUB_STEP_SUMMARY | |
| echo "| Component | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.build-docker.result }}" = "success" ]; then | |
| echo "| Docker | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-docker.result }}" = "skipped" ]; then | |
| echo "| Docker | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Docker | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-docker-cli.result }}" = "success" ]; then | |
| echo "| CLI Docker | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-docker-cli.result }}" = "skipped" ]; then | |
| echo "| CLI Docker | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| CLI Docker | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-windows.result }}" = "success" ]; then | |
| echo "| Windows | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-windows.result }}" = "skipped" ]; then | |
| echo "| Windows | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Windows | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-apple.result }}" = "success" ]; then | |
| echo "| Apple | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-apple.result }}" = "skipped" ]; then | |
| echo "| Apple | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Apple | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-snap.result }}" = "success" ]; then | |
| echo "| Snap | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-snap.result }}" = "skipped" ]; then | |
| echo "| Snap | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Snap | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-linux-terminal.result }}" = "success" ]; then | |
| echo "| Linux Terminal | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-linux-terminal.result }}" = "skipped" ]; then | |
| echo "| Linux Terminal | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Linux Terminal | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-cli.result }}" = "success" ]; then | |
| echo "| CLI (MCP Server) | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-cli.result }}" = "skipped" ]; then | |
| echo "| CLI (MCP Server) | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| CLI (MCP Server) | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Deployment Status | |
| echo "## 🚀 Deployment Status" >> $GITHUB_STEP_SUMMARY | |
| echo "| Platform | Status | Notes |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|--------|-------|" >> $GITHUB_STEP_SUMMARY | |
| # Docker Hub | |
| if [ "${{ needs.deploy-docker.result }}" = "success" ]; then | |
| echo "| Docker Hub | ✅ Deployed | Latest tag updated |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.deploy-docker.result }}" = "skipped" ]; then | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ]; then | |
| echo "| Docker Hub | ⏭️ Skipped | Stage-only mode |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Docker Hub | ⏭️ Skipped | Not selected |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "| Docker Hub | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Docker Hub CLI | |
| if [ "${{ needs.deploy-docker-cli.result }}" = "success" ]; then | |
| echo "| Docker Hub (CLI) | ✅ Deployed | Latest tag updated |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.deploy-docker-cli.result }}" = "skipped" ]; then | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ]; then | |
| echo "| Docker Hub (CLI) | ⏭️ Skipped | Stage-only mode |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Docker Hub (CLI) | ⏭️ Skipped | Not selected |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "| Docker Hub (CLI) | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Snap Store | |
| if [ "${{ needs.deploy-snap.result }}" = "success" ]; then | |
| CHANNEL="${{ needs.calculate-version.outputs.stage-only == 'true' && 'edge' || 'stable' }}" | |
| echo "| Snap Store | ✅ Deployed | ${CHANNEL} channel |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.deploy-snap.result }}" = "skipped" ]; then | |
| echo "| Snap Store | ⏭️ Skipped | Not selected |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Snap Store | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Microsoft Store | |
| if [ "${{ needs.deploy-microsoft.result }}" = "success" ]; then | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ]; then | |
| echo "| Microsoft Store | ✅ Deployed | Draft submission |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Microsoft Store | ✅ Deployed | Submitted for certification |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| elif [ "${{ needs.deploy-microsoft.result }}" = "skipped" ]; then | |
| echo "| Microsoft Store | ⏭️ Skipped | Not selected |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Microsoft Store | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Apple App Store | |
| if [ "${{ needs.deploy-apple.result }}" = "success" ]; then | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ]; then | |
| echo "| Apple App Store | ✅ Deployed | TestFlight only |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Apple App Store | ✅ Deployed | Submitted for review |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| elif [ "${{ needs.deploy-apple.result }}" = "skipped" ]; then | |
| echo "| Apple App Store | ⏭️ Skipped | Not selected |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Apple App Store | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # GitHub Release | |
| if [ "${{ needs.create-github-release.result }}" = "success" ]; then | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ] || [ "${{ needs.calculate-version.outputs.publish-github-release }}" != "true" ]; then | |
| RELEASE_NOTE="Draft release" | |
| else | |
| RELEASE_NOTE="Published" | |
| fi | |
| if [ "${{ needs.calculate-version.outputs.deploy-linux-terminal }}" = "true" ]; then | |
| RELEASE_NOTE="$RELEASE_NOTE (includes Linux Terminal)" | |
| fi | |
| echo "| GitHub Release | ✅ Created | $RELEASE_NOTE |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.create-github-release.result }}" = "skipped" ]; then | |
| echo "| GitHub Release | ⏭️ Skipped | Stage-only mode |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| GitHub Release | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Homebrew Formula (CLI) | |
| if [ "${{ needs.deploy-homebrew-formula.result }}" = "success" ]; then | |
| echo "| Homebrew Formula | ✅ Generated | brew install clidey/tap/whodb-cli |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.deploy-homebrew-formula.result }}" = "skipped" ]; then | |
| echo "| Homebrew Formula | ⏭️ Skipped | CLI not selected |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Homebrew Formula | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Homebrew Cask (Desktop App - disabled) | |
| if [ "${{ needs.generate-homebrew-cask.result }}" = "success" ]; then | |
| echo "| Homebrew Cask | ✅ Generated | Desktop app cask |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.generate-homebrew-cask.result }}" = "skipped" ]; then | |
| echo "| Homebrew Cask | ⏭️ Skipped | Disabled |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| Homebrew Cask | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # npm | |
| if [ "${{ needs.deploy-npm.result }}" = "success" ]; then | |
| echo "| npm (@clidey/whodb-cli) | ✅ Published | All platform packages |" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.deploy-npm.result }}" = "skipped" ]; then | |
| echo "| npm | ⏭️ Skipped | CLI not selected |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| npm | ❌ Failed | Check logs |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Next Steps | |
| echo "## 📝 Next Steps" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.calculate-version.outputs.stage-only }}" = "true" ]; then | |
| echo "Stage deployments completed. To promote to production:" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Test the edge/draft releases" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Promote Snap from edge to stable channel" >> $GITHUB_STEP_SUMMARY | |
| echo "3. Submit Microsoft Store draft for certification" >> $GITHUB_STEP_SUMMARY | |
| echo "4. Submit Apple TestFlight build for App Store review" >> $GITHUB_STEP_SUMMARY | |
| echo "5. Publish GitHub draft release" >> $GITHUB_STEP_SUMMARY | |
| echo "6. Deploy to Docker Hub" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "Production deployment completed!" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Monitor the following:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- Microsoft Store certification status" >> $GITHUB_STEP_SUMMARY | |
| echo "- Apple App Store review status" >> $GITHUB_STEP_SUMMARY | |
| echo "- User feedback on new release" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**If any platform failed:**" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Check the logs for the failed job" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Fix the issue" >> $GITHUB_STEP_SUMMARY | |
| echo "3. Re-run the failed job from Actions → Re-run jobs → Re-run failed jobs" >> $GITHUB_STEP_SUMMARY | |
| fi |