Skip to content

chore(release): 0.9.13 #28

chore(release): 0.9.13

chore(release): 0.9.13 #28

Workflow file for this run

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}'