Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@ jobs:
keep_files: true

# ── macOS Desktop (push, or PRs with macOS-relevant changes) ─────────────
# Skip platform builds on version-bump PRs (only version numbers change).
build-macos:
if: needs.detect-changes.outputs.macos == 'true'
if: needs.detect-changes.outputs.macos == 'true' && !startsWith(github.head_ref, 'chore-bump-v')
needs: [test, detect-changes]
runs-on: macos-latest
steps:
Expand Down Expand Up @@ -214,7 +215,7 @@ jobs:

# ── Linux Desktop (push, or PRs with Linux-relevant changes) ────────────
build-linux:
if: needs.detect-changes.outputs.linux == 'true'
if: needs.detect-changes.outputs.linux == 'true' && !startsWith(github.head_ref, 'chore-bump-v')
needs: [test, detect-changes]
runs-on: ubuntu-latest
steps:
Expand Down
214 changes: 214 additions & 0 deletions .github/workflows/release-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# One-click release pipeline: dev -> main -> version bump -> tag -> release build.
#
# Replaces the manual 13-step process:
# 1. Merges dev into main (fast-forward or merge commit)
# 2. Bumps version on main
# 3. Pushes annotated tag vX.Y.Z (triggers release.yml)
#
# After release.yml completes, its sync-dev job auto-merges main back to dev.
#
# Prerequisites:
# - Secret: RELEASE_AUTOMATION_PAT (fine-grained PAT: Contents + Pull requests RW)
# - Secret: TAURI_SIGNING_PRIVATE_KEY (for release.yml)
# - Optional var: RELEASE_ALLOWED_ACTORS (comma-separated usernames)
#
# The existing release-bump.yml and release-tag.yml still work as manual fallbacks.

name: Release — one-click pipeline

on:
workflow_dispatch:
inputs:
release_type:
description: "Semver bump type (see https://semver.org)"
type: choice
required: true
default: patch
options:
- patch
- minor
- major
dry_run:
description: "Dry run — show what would happen without pushing"
type: boolean
default: false

permissions:
contents: write
pull-requests: write

concurrency:
group: release-pipeline
cancel-in-progress: false

jobs:
release-pipeline:
runs-on: ubuntu-latest
steps:
- name: Verify release permission
env:
ACTOR: ${{ github.actor }}
REPO_OWNER: ${{ github.repository_owner }}
ALLOWED_ACTORS: ${{ vars.RELEASE_ALLOWED_ACTORS }}
run: |
set -euo pipefail
python3 << 'PY'
import os
actor = os.environ["ACTOR"]
owner = os.environ["REPO_OWNER"]
raw = os.environ.get("ALLOWED_ACTORS", "").strip()
if raw:
allowed = [x.strip() for x in raw.split(",") if x.strip()]
else:
allowed = [owner]
if actor not in allowed:
print(f"::error::User '{actor}' is not allowed to run releases. "
f"Allowed: {allowed}. Set repository variable RELEASE_ALLOWED_ACTORS.")
raise SystemExit(1)
print(f"OK: {actor} is authorized to release.")
PY

- name: Require automation PAT
env:
RELEASE_AUTOMATION_PAT: ${{ secrets.RELEASE_AUTOMATION_PAT }}
run: |
if [ -z "${RELEASE_AUTOMATION_PAT:-}" ]; then
echo "::error::RELEASE_AUTOMATION_PAT secret is required."
exit 1
fi

- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_AUTOMATION_PAT }}

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Merge dev into main
run: |
set -euo pipefail
git fetch origin main dev

# Check that dev is ahead of main
BEHIND=$(git rev-list --count origin/main..origin/dev)
if [ "$BEHIND" = "0" ]; then
echo "::warning::dev has no new commits over main. Continuing with version bump only."
fi

git checkout main
git reset --hard origin/main

# Merge dev into main (non-interactive)
echo "Merging origin/dev into main..."
git merge origin/dev --no-edit -m "Merge branch 'dev' into main for release"

Comment on lines +105 to +108
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This merge step doesn’t handle conflicts beyond failing the job. Since this workflow is meant to be “one-click”, consider detecting merge conflicts and switching to a PR-based flow (similar to sync-dev in release.yml) so the pipeline can still complete with a clear next action instead of leaving a failed run.

Copilot uses AI. Check for mistakes.
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "::notice::DRY RUN — would merge dev into main"
echo "Commits from dev:"
git log origin/main..HEAD --oneline
else
git push origin main
echo "Pushed merged main."
fi

Comment on lines +111 to +117
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow pushes to main immediately after merging dev (before the version bump and tag). If a later step fails (bump/commit/tag), main is left partially updated without a corresponding release/tag. Consider deferring the push until after the version bump commit is created (single push), or doing all work on a temporary branch and only updating main once the entire pipeline is ready.

Suggested change
echo "Commits from dev:"
git log origin/main..HEAD --oneline
else
git push origin main
echo "Pushed merged main."
fi
else
echo "Merged dev into local main. Deferring push until after version bump/tag steps succeed."
fi
echo "Commits from origin/main to current HEAD:"
git log origin/main..HEAD --oneline

Copilot uses AI. Check for mistakes.
- name: Install Rust (for Cargo.lock refresh)
uses: dtolnay/rust-toolchain@stable

- name: Bump version
id: ver
env:
RELEASE_TYPE: ${{ inputs.release_type }}
run: |
set -euo pipefail
CURRENT=$(python3 -c "import json; print(json.load(open('apps/desktop/src-tauri/tauri.conf.json'))['version'])")
export CURRENT
NEW=$(python3 - <<'PY'
import os
cur = os.environ["CURRENT"]
rt = os.environ["RELEASE_TYPE"]
major, minor, patch = map(int, cur.split("."))
if rt == "patch":
print(f"{major}.{minor}.{patch + 1}")
elif rt == "minor":
print(f"{major}.{minor + 1}.0")
elif rt == "major":
print(f"{major + 1}.0.0")
else:
raise SystemExit(1)
PY
)
echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "new=$NEW" >> "$GITHUB_OUTPUT"
echo "Bumping $CURRENT -> $NEW ($RELEASE_TYPE)"
python3 scripts/bump-version.py "$NEW"

- name: Refresh Cargo.lock
run: cargo generate-lockfile

- name: Commit version bump
env:
NEW: ${{ steps.ver.outputs.new }}
run: |
set -euo pipefail
git add apps/desktop/src-tauri/tauri.conf.json \
apps/desktop/src-tauri/Cargo.toml \
apps/desktop/src/components/settings/Settings.tsx \
Cargo.lock
git commit -m "chore: bump version to $NEW"

if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "::notice::DRY RUN — would commit and push version bump"
else
git push origin main
echo "Pushed version bump to main."
fi

- name: Create and push tag
id: tag
env:
NEW: ${{ steps.ver.outputs.new }}
run: |
set -euo pipefail
TAG="v${NEW}"

# Check tag doesn't already exist
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
echo "::error::Tag $TAG already exists on remote. Delete it first or use a different version."
exit 1
fi

git tag -a "$TAG" -m "Release $TAG"

if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "::notice::DRY RUN — would push tag $TAG (triggers release.yml)"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
else
git push origin "$TAG"
echo "Pushed tag $TAG — release.yml will now build and publish."
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
fi

- name: Summary
env:
NEW: ${{ steps.ver.outputs.new }}
CURRENT: ${{ steps.ver.outputs.current }}
TAG: ${{ steps.tag.outputs.tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
if [ "$DRY_RUN" = "true" ]; then
echo "### Dry run complete" >> "$GITHUB_STEP_SUMMARY"
echo "Would have:" >> "$GITHUB_STEP_SUMMARY"
else
echo "### Release pipeline complete" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- Merged \`dev\` into \`main\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Bumped version: \`$CURRENT\` -> \`$NEW\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Pushed tag: \`$TAG\`" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" != "true" ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Next:** [release.yml](${{ github.server_url }}/${{ github.repository }}/actions/workflows/release.yml) builds and publishes the release, then auto-syncs \`main\` back to \`dev\`." >> "$GITHUB_STEP_SUMMARY"
fi
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,71 @@ jobs:
gh release upload "v${VERSION}" latest.json \
--repo "$GITHUB_REPOSITORY" \
--clobber

# ── Sync main back to dev after release ─────────────────────────────────
sync-dev:
runs-on: ubuntu-latest
needs: [publish-latest-json]
if: always() && needs.publish-latest-json.result == 'success'
permissions:
contents: write
pull-requests: write

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_AUTOMATION_PAT || github.token }}

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Merge main into dev
env:
GH_TOKEN: ${{ secrets.RELEASE_AUTOMATION_PAT || github.token }}
run: |
Comment on lines +421 to +434
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync-dev falls back to github.token for both actions/checkout and GH_TOKEN. In this repo, release-bump.yml explicitly requires secrets.RELEASE_AUTOMATION_PAT because the default token often cannot create PRs (createPullRequest not permitted). Consider requiring the PAT here as well (or gracefully skipping PR creation when it’s absent) so the conflict fallback doesn’t fail mid-sync.

Copilot uses AI. Check for mistakes.
set -euo pipefail

# Check if dev branch exists before fetching (fetch fails if branch is missing)
if ! git ls-remote --heads origin dev | grep -q .; then
echo "::notice::No dev branch found — skipping sync."
exit 0
fi

git fetch origin main dev

git checkout dev
git reset --hard origin/dev

# Check if main has anything new for dev
AHEAD=$(git rev-list --count origin/dev..origin/main)
if [ "$AHEAD" = "0" ]; then
echo "dev is already up to date with main."
exit 0
fi

# Try fast-forward merge first, fall back to merge commit
if git merge origin/main --ff-only 2>/dev/null; then
echo "Fast-forwarded dev to main."
git push origin dev
elif git merge origin/main --no-edit -m "chore: sync main back to dev after release"; then
echo "Merged main into dev (merge commit)."
git push origin dev
else
echo "::warning::Auto-merge of main into dev failed (conflicts). Creating PR instead."
# Create a PR for manual resolution
BRANCH="chore/sync-main-to-dev-$(date -u +%Y%m%d-%H%M%S)-${GITHUB_RUN_ID}"
git merge --abort || true
git checkout -b "$BRANCH" origin/main
git push -u origin "$BRANCH"
gh pr create \
--base dev \
--head "$BRANCH" \
--title "chore: sync main back to dev after release" \
--body "Automated post-release sync. Main has changes (version bump + release tag) that need to be merged back into dev. Resolve any conflicts and merge."
fi

- name: Summary
run: echo "### main synced back to dev" >> "$GITHUB_STEP_SUMMARY"
Loading