Skip to content

fix: promote tagged release commit to main and stabilize release checks#584

Draft
lourou wants to merge 5 commits intodevfrom
03-12-fix_remove_promote-to-main_skip_swiftlint_on_non-swift_prs
Draft

fix: promote tagged release commit to main and stabilize release checks#584
lourou wants to merge 5 commits intodevfrom
03-12-fix_remove_promote-to-main_skip_swiftlint_on_non-swift_prs

Conversation

@lourou
Copy link
Copy Markdown
Member

@lourou lourou commented Mar 12, 2026

Summary

This PR revises the release pipeline to keep automation while tightening safety and CI reliability.

What changed

  • Restored promote-to-main.yml and changed promotion to use the exact tagged release commit (not the moving dev tip).
  • Added promotion safeguards:
    • resolve SHA from the release tag
    • ensure main is ancestor of that SHA before fast-forward
    • clear summary for promoted/up-to-date/skipped states
  • Updated swiftlint.yml so required checks remain stable:
    • workflow always runs on PRs to main/dev
    • lint steps run only when .swift files changed
    • no-Swift PRs get an explicit successful skip message
  • Added release-pipeline concurrency to auto-release.yml.
  • Updated release messaging in:
    • create-release.yml
    • release-tag-on-merge.yml
    • auto-release.yml
  • Added/updated docs/release-process.md to match actual behavior end-to-end.

Why

  • Prevent unintentionally promoting extra commits from dev if new commits land after tagging.
  • Avoid release PRs getting blocked by missing SwiftLint required checks.
  • Keep docs and CI summaries aligned with real pipeline behavior.

One-time repo setup still required

For direct CI fast-forward of main, add GitHub Actions to the main ruleset bypass list:
Settings → Rules → Rulesets → main ruleset → Bypass list.

Copy link
Copy Markdown
Member Author

lourou commented Mar 12, 2026


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown

claude Bot commented Mar 12, 2026

Claude finished @lourou's task —— View job


Code Review Complete ✓

Tasks

  • Read all changed workflow files
  • Review promote-to-main.yml logic and safety
  • Review swiftlint.yml conditional execution
  • Review other workflow changes
  • Review documentation
  • Provide comprehensive feedback

Summary

This PR makes significant improvements to the release pipeline with well-thought-out safety guardrails. The changes are solid overall, but I've identified several issues ranging from critical bugs to best practice improvements.

Critical Issues: 1 bug that will cause workflow failures
High Priority: 2 logic issues and 1 security improvement
Medium Priority: 3 robustness improvements
Low Priority: 2 documentation clarifications


Critical Issues

1. Bug: Summary step will fail when git command unavailable (promote-to-main.yml:98)

Severity: Critical — will cause workflow failure

RELEASE_SHA_SHORT=$(git rev-parse --short "$RELEASE_SHA")

Problem: This runs in the Summary step which always executes (no if condition), but git is only available after the Checkout repository step which runs conditionally (only when is_production == 'true'). For prereleases, the Summary step will fail trying to run git rev-parse without a repository.

Fix:

- name: Summary
  run: |
    TAG="${{ steps.version.outputs.tag }}"

    if [[ "$TAG" == *-* ]]; then
      echo "## Promotion Skipped" >> $GITHUB_STEP_SUMMARY
      echo "" >> $GITHUB_STEP_SUMMARY
      echo "Prerelease **$TAG** — no promotion to main." >> $GITHUB_STEP_SUMMARY
    else
      RESULT="${{ steps.promote_main.outputs.result }}"
      RELEASE_SHA="${{ steps.resolve.outputs.release_sha }}"
      RELEASE_SHA_SHORT=$(echo "$RELEASE_SHA" | cut -c1-7)  # Use string manipulation instead of git

      if [ "$RESULT" = "up_to_date" ]; then
        echo "## No Promotion Needed" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "main is already at release commit **$RELEASE_SHA_SHORT** for tag **$TAG**." >> $GITHUB_STEP_SUMMARY
      else
        echo "## main Promoted" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "main fast-forwarded to release commit **$RELEASE_SHA_SHORT** for tag **$TAG**." >> $GITHUB_STEP_SUMMARY
      fi
    fi

High Priority Issues

2. Logic Issue: Tag resolution from workflow_run event (promote-to-main.yml:22)

Severity: High — may not work as expected in all cases

TAG="${{ github.event.workflow_run.head_branch }}"

Problem: workflow_run.head_branch gives the branch name, not the tag. For tag-triggered workflows, this might work coincidentally if the tag name matches, but it's semantically incorrect and fragile. The auto-release.yml workflow is triggered by a tag push, so head_branch may not reliably contain the tag name.

Recommended Fix:

- name: Extract tag from workflow_run
  id: version
  run: |
    # workflow_run doesn't directly expose the tag, but we can extract it from the display title
    # or alternatively query the workflow run for its triggering ref
    RUN_ID="${{ github.event.workflow_run.id }}"
    TAG=$(gh api "/repos/${{ github.repository }}/actions/runs/${RUN_ID}" --jq '.head_branch')
    
    # Verify it looks like a tag (not a branch)
    if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.].*)?$ ]]; then
      echo "::error::Expected a version tag but got: $TAG"
      exit 1
    fi
    
    echo "tag=$TAG" >> $GITHUB_OUTPUT
    # ... rest of logic

Alternative approach: Pass the tag explicitly via workflow outputs from auto-release.yml or use workflow_call instead of workflow_run for more control.

3. Logic Issue: SwiftLint change detection may miss files (swiftlint.yml:28)

Severity: High — may skip linting when it shouldn't

if git diff --name-only "origin/${{ github.base_ref }}...${{ github.sha }}" | grep -E '\.swift$' > /dev/null; then

Problem: The three-dot ... syntax shows the diff from the merge base to HEAD, which is correct for most cases. However, if the base branch has moved forward since the PR branch was created, this might miss new Swift files added to the PR branch. The two-dot .. syntax would be more explicit about "what changed in this PR."

Recommended: Use two-dot syntax for clarity:

git diff --name-only "origin/${{ github.base_ref }}..${{ github.sha }}" | grep -E '\.swift$'

Both work in most cases, but .. is semantically clearer for "changes introduced by this branch."

4. Security: Token permissions overly broad (promote-to-main.yml:17)

Severity: Medium-High — violates least-privilege principle

permissions:
  contents: write

Current: Grants write to all repository content.

Best Practice: GitHub Actions should use minimal permissions. While contents: write is needed to push to main, it's worth noting that this also allows creating/deleting branches, creating/deleting tags, and other operations. This is acceptable for a release workflow, but consider if you need to restrict further in the future.

Recommendation: Document in a comment why broad permissions are needed:

permissions:
  contents: write  # Required to push directly to main branch

Medium Priority Issues

5. Robustness: Race condition in concurrent workflow runs (auto-release.yml:8-10)

Severity: Medium — unlikely but possible

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

Problem: All release workflows share the release-pipeline concurrency group. This is good! But there's a subtle race: if two tags are pushed in rapid succession (e.g., manual tag creation followed by automated retry), both auto-release.yml runs will queue, and both will try to create/update the GitHub release.

Current handling: The auto-release.yml workflow checks if the release exists and updates it if so (line 152), which mitigates this. However, the promote-to-main workflow could still race.

Recommendation: This is already well-handled with the concurrency group. Consider adding a comment to explain the race protection:

concurrency:
  group: release-pipeline
  cancel-in-progress: false  # Queue instead of cancel to ensure every tag gets processed

6. Robustness: Git fetch may be stale (promote-to-main.yml:51)

Severity: Medium — could cause subtle issues

git fetch origin --tags --force

Problem: Fetches tags but not refs. Later, on line 68, you git fetch origin main, but there's no guarantee that the tag is associated with a commit in the fetched history. If the tag was created on a commit not yet in the local clone, git rev-list may not traverse properly.

Recommendation: Fetch both tags and full history:

git fetch origin --tags --force
git fetch origin main dev  # Ensure we have recent history

Or use:

git fetch origin --tags --force --unshallow || true

7. Robustness: Fast-forward push could fail on race (promote-to-main.yml:83)

Severity: Medium — could fail if main is updated during the workflow

git push origin "$RELEASE_SHA":refs/heads/main

Problem: If another workflow (or person) pushes to main between the ancestor check (line 78) and the push (line 83), this will fail. The workflow assumes it has exclusive access to main via the concurrency group, which is correct, but there's no explicit check.

Recommendation: Add --force-with-lease for safer pushes:

# First set remote tracking
git fetch origin main
git branch -f main "$RELEASE_SHA"
git push --force-with-lease=main:$MAIN_SHA origin main

# Or simpler - use git push with explicit refspec and --force-with-lease
git push --force-with-lease origin "$RELEASE_SHA":refs/heads/main

Wait — on second thought, --force-with-lease without setting up tracking might not work as expected. The current approach is actually fine because:

  1. The concurrency group prevents concurrent modifications
  2. The push is a fast-forward (verified by the ancestor check)
  3. The GitHub Actions bot bypass ensures no branch protection issues

Verdict: The current approach is acceptable given the concurrency controls. No change needed, but consider adding a comment:

# Safe to push directly because:
# 1. Concurrency group prevents races
# 2. Verified fast-forward above
# 3. GitHub Actions is in bypass list for main branch ruleset
git push origin "$RELEASE_SHA":refs/heads/main

Low Priority / Best Practices

8. Documentation: Unclear legend placement (docs/release-process.md:22-24)

Issue: The "Legend" appears after the flowchart, but it explains symbols used in the flowchart. Readers have to read ahead to understand the diagram.

Recommendation: Move the legend immediately before or as part of the Overview section:

## Overview

Releases are fully automated via GitHub Actions. A single `make release` command triggers the entire pipeline.

**Version Types:**
- `X.Y.Z` — Production release (promotes to `main`, deploys to App Store)
- `X.Y.Z-dev.N` — Prerelease (no promotion, optional TestFlight distribution)

[flowchart here]

9. Documentation: Missing failure scenario (docs/release-process.md:126-128)

Issue: The troubleshooting section mentions tag resolution failure, but doesn't explain the most common cause: workflow_run event timing.

Recommendation: Add a note:

**`promote-to-main.yml` fails with "Could not resolve commit for tag"**

The tag referenced by the Auto Release run is missing or not yet visible in the clone. This can happen if:
1. The tag was deleted manually after creation
2. The workflow_run event raced with tag propagation (rare)

**Fix:** Re-run the workflow after confirming the tag exists remotely with `git ls-remote --tags origin`.

Positive Observations

  1. Excellent safety guardrails in promote-to-main.yml:

    • Explicit SHA resolution from tags
    • Ancestor check before push
    • Clear separation of prerelease vs production logic
  2. Smart SwiftLint handling: The conditional execution in swiftlint.yml elegantly solves the "required check always runs" vs "skip unnecessary work" problem.

  3. Clear concurrency controls: Using a shared release-pipeline group across all release workflows prevents races and ensures orderly execution.

  4. Good separation of concerns: Each workflow has a single responsibility, making debugging and maintenance easier.

  5. Comprehensive documentation: The release-process.md is thorough and well-structured. The flowchart is particularly helpful.

  6. Idempotent release creation: Checking for existing releases and updating them (auto-release.yml:152) makes retries safe.


Testing Recommendations

Before merging, verify these scenarios:

  1. Production release (X.Y.Z)

    • Creates tag correctly
    • Promotes to main with exact tagged commit
    • GitHub Release created with notes
  2. Prerelease (X.Y.Z-dev.N)

    • Creates tag correctly
    • Does NOT promote to main
    • Summary shows skip message
  3. Non-Swift PR to main/dev

    • SwiftLint workflow runs
    • Lint step skips with success message
    • Required check passes
  4. Swift PR to main/dev

    • SwiftLint workflow runs
    • Lint step executes
    • Failures block PR
  5. Edge case: main already at release commit

    • Promotes workflow detects this
    • Exits successfully with "up_to_date" message

Summary of Recommendations

Priority Issue File Action
Critical Summary step git command promote-to-main.yml:98 Use string manipulation instead of git rev-parse
High Tag extraction from workflow_run promote-to-main.yml:22 Query API or validate tag format
High SwiftLint diff syntax swiftlint.yml:28 Use .. instead of ... for clarity
Medium-High Token permissions promote-to-main.yml:17 Add clarifying comment
Medium Git fetch completeness promote-to-main.yml:51 Fetch refs in addition to tags
Low Documentation legend release-process.md:22 Move legend before flowchart
Low Documentation troubleshooting release-process.md:126 Add workflow_run timing note

@lourou lourou changed the title fix: remove promote-to-main, skip swiftlint on non-Swift PRs fix: promote tagged release commit to main and stabilize release checks Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant