chore(release): 0.9.13 #28
Workflow file for this run
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 | |
| # Triggered when `np` (or manual `git tag`) pushes a semver tag. | |
| # Orchestrates: GitHub Release creation -> Desktop builds -> Mobile builds -> Checksums. | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g. 0.3.2)' | |
| required: true | |
| type: string | |
| # Minimal permissions — only contents:write for release management | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ── 1. Create the GitHub Release ──────────────────────────────────────── | |
| create-release: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_id: ${{ steps.create.outputs.id }} | |
| upload_url: ${{ steps.create.outputs.upload_url }} | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| - name: Extract and validate version | |
| id: version | |
| run: | | |
| VERSION="${{ github.event.inputs.version || '' }}" | |
| if [ -z "$VERSION" ]; then | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| fi | |
| # Validate semver format (major.minor.patch with optional pre-release) | |
| if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then | |
| echo "Error: tag does not match semver format: $VERSION" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| # Clean slate: delete any pre-existing release for this tag so we don't | |
| # end up with duplicate releases (e.g., from a previous failed run that | |
| # left a stale draft, or from a re-trigger). The git tag is preserved. | |
| - name: Delete any pre-existing releases for this tag | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| TAG="v${{ steps.version.outputs.version }}" | |
| # Use gh api directly — `gh release view/delete TAG` uses | |
| # /releases/tags/{tag} which does NOT find draft releases, | |
| # so stale drafts from failed runs would linger. | |
| ids=$(gh api "repos/${GH_REPO}/releases" --paginate \ | |
| --jq ".[] | select(.tag_name==\"$TAG\") | .id") | |
| if [ -z "$ids" ]; then | |
| echo "No existing releases for $TAG" | |
| else | |
| for id in $ids; do | |
| echo "Deleting release id=$id (tag=$TAG)" | |
| gh api -X DELETE "repos/${GH_REPO}/releases/${id}" || true | |
| done | |
| fi | |
| - name: Create GitHub Release | |
| id: create | |
| uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 | |
| with: | |
| draft: true | |
| prerelease: false | |
| generate_release_notes: true | |
| tag_name: v${{ steps.version.outputs.version }} | |
| name: v${{ steps.version.outputs.version }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ── 2. Build Desktop (Tauri) ──────────────────────────────────────────── | |
| build-desktop: | |
| needs: create-release | |
| uses: ./.github/workflows/release-desktop.yml | |
| with: | |
| version: ${{ needs.create-release.outputs.version }} | |
| release_id: ${{ needs.create-release.outputs.release_id }} | |
| secrets: inherit | |
| # ── 2b. Build Android APK ────────────────────────────────────────────── | |
| build-android: | |
| needs: create-release | |
| uses: ./.github/workflows/release-mobile.yml | |
| with: | |
| version: ${{ needs.create-release.outputs.version }} | |
| release_id: ${{ needs.create-release.outputs.release_id }} | |
| secrets: inherit | |
| # ── 3. Generate checksums (best-effort) ──────────────────────────────── | |
| checksums: | |
| needs: [create-release, build-desktop, build-android] | |
| if: always() && needs.build-desktop.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: List existing release assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_ID: ${{ needs.create-release.outputs.release_id }} | |
| run: | | |
| echo "=== Release id=${RELEASE_ID} assets ===" | |
| gh api "repos/${GH_REPO}/releases/${RELEASE_ID}/assets" \ | |
| --jq '.[].name' || echo "Failed to list assets" | |
| - name: Download release assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_ID: ${{ needs.create-release.outputs.release_id }} | |
| run: | | |
| # Use release_id via gh api — `gh release download TAG` uses | |
| # /releases/tags/{tag} which doesn't find draft releases. | |
| mkdir -p artifacts | |
| cd artifacts | |
| patterns='\.(dmg|app\.tar\.gz|app\.tar\.gz\.sig|msi|msi\.zip|msi\.zip\.sig|nsis\.zip|nsis\.zip\.sig|exe|deb|rpm|AppImage|AppImage\.tar\.gz|AppImage\.tar\.gz\.sig|apk|aab|ipa)$' | |
| gh api "repos/${GH_REPO}/releases/${RELEASE_ID}/assets" \ | |
| --jq ".[] | select(.name | test(\"${patterns}\")) | [.id, .name] | @tsv" | \ | |
| while IFS=$'\t' read -r asset_id asset_name; do | |
| [ -z "$asset_id" ] && continue | |
| echo "→ $asset_name" | |
| gh api -H "Accept: application/octet-stream" \ | |
| "repos/${GH_REPO}/releases/assets/${asset_id}" > "$asset_name" || true | |
| done | |
| echo "=== Downloaded files ===" | |
| ls -la . || true | |
| - name: Generate SHA-256 checksums | |
| id: sums | |
| run: | | |
| cd artifacts | |
| shopt -s nullglob | |
| files=( * ) | |
| if [ ${#files[@]} -gt 0 ]; then | |
| sha256sum -- "${files[@]}" | LC_ALL=C sort > SHA256SUMS.txt | |
| echo "=== SHA256SUMS.txt ===" | |
| cat SHA256SUMS.txt | |
| echo "=== File count: $(wc -l < SHA256SUMS.txt) ===" | |
| echo "generated=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No artifacts found to checksum — skipping (release will publish without SHA256SUMS.txt)" | |
| echo "generated=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload checksums to release | |
| if: steps.sums.outputs.generated == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_ID: ${{ needs.create-release.outputs.release_id }} | |
| run: | | |
| # Use release_id via gh api instead of softprops/action-gh-release, | |
| # which uses the /releases/tags/{tag} endpoint that does NOT find | |
| # draft releases and would create a duplicate non-draft release. | |
| cd artifacts | |
| name="SHA256SUMS.txt" | |
| existing_id=$(gh api "repos/${GH_REPO}/releases/${RELEASE_ID}/assets" \ | |
| --jq ".[] | select(.name==\"$name\") | .id" | head -n 1) | |
| if [ -n "$existing_id" ]; then | |
| gh api -X DELETE "repos/${GH_REPO}/releases/assets/${existing_id}" || true | |
| fi | |
| gh api \ | |
| --method POST \ | |
| -H "Content-Type: text/plain" \ | |
| "https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${name}" \ | |
| --input "$name" | |
| # ── 4. Publish the release ───────────────────────────────────────────── | |
| # Publish as long as the desktop build succeeded — checksums are best-effort | |
| # and shouldn't block publishing the actual binaries. | |
| publish: | |
| needs: [create-release, build-desktop, build-android, checksums] | |
| if: always() && needs.build-desktop.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Publish release (remove draft status) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_ID: ${{ needs.create-release.outputs.release_id }} | |
| run: | | |
| # Use release_id via gh api — `gh release edit TAG` uses | |
| # /releases/tags/{tag} which does NOT find draft releases. | |
| gh api --method PATCH "repos/${GH_REPO}/releases/${RELEASE_ID}" \ | |
| -f draft=false \ | |
| -F make_latest=true | |
| # ── 5. Deploy web app ───────────────────────────────────────────────── | |
| # Inline deploy because GITHUB_TOKEN events don't trigger other workflows, | |
| # so deploy.yml's `on: release: [published]` never fires from the publish step. | |
| # deploy.yml is kept for manual re-deploys via workflow_dispatch. | |
| deploy: | |
| needs: [create-release, publish] | |
| if: needs.publish.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| environment: production | |
| env: | |
| R2_BUCKET: ${{ vars.R2_BUCKET }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 | |
| with: | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '20' | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build | |
| run: pnpm build | |
| - name: Strip source maps | |
| run: find dist -name '*.map' -delete | |
| - name: Deploy to Cloudflare R2 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} | |
| run: | | |
| ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" | |
| aws --endpoint-url "$ENDPOINT" s3 sync dist/ "s3://${R2_BUCKET}/" --acl private | |
| - name: Deploy Worker | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} | |
| run: | | |
| sed -i "s/bucket_name = \"myselfhosted-webmail-test-1\"/bucket_name = \"${R2_BUCKET}\"/" worker/wrangler.toml | |
| cd worker | |
| pnpm install --no-frozen-lockfile | |
| npx wrangler deploy | |
| - name: Purge Cloudflare Cache | |
| env: | |
| CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \ | |
| -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| --data '{"purge_everything":true}' |