Skip to content
Merged
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
70 changes: 32 additions & 38 deletions .github/workflows/sync-fork.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
name: Sync Fork with Upstream

# Pull-based fork sync with GitHub App installation token for approval.
# Pull-based fork sync with Dependabot-style handoff.
#
# Uses `actions/create-github-app-token@v2` to mint a short-lived installation
# token per workflow run. The App is scoped to ONLY the repos where it's
# installed — no org-wide or account-wide access. Tokens expire within the
# hour and cannot be reused if exfiltrated.
# installed. Tokens expire within the hour and cannot be reused if exfiltrated.
#
# Prerequisites (already configured on this repo):
# - GitHub App named 'fork-sync-approver' (or similar) installed on this repo
# - Repo secret 'APPID4510581' containing the .pem private key
# - Repo secret 'APP_ID' containing just the number 4510581
# Flow (Dependabot-style):
# 1. App token opens a sync PR (as the App's identity)
# 2. Workflow posts a comment on the PR asking for human review
# 3. You (the repo owner) click Approve + Merge
#
# Why this design (vs. PAT / OAuth):
# - GITHUB_TOKEN (github-actions[bot]) cannot self-approve its own PRs
# (GitHub blocks this even for admins via API, HTTP 422)
# - PATs are long-lived, single-tenant, and broad-scope — risky on public
# repos (anyone can read the secret via Actions API or fork PR attacks)
# - OAuth tokens have org-wide scopes — dangerous if leaked
# - GitHub App installation tokens are short-lived, repo-scoped, and
# least-privilege — the correct choice for fork sync automation
# Why no self-approval:
# GitHub blocks any identity from approving its own PR (HTTP 422 "Review
# Can not approve your own pull request"). bypass_actors doesn't bypass this
# rule — it only bypasses the ruleset's "require N approvals" requirement
# for actors that ARE allowed to approve.
#
# Prerequisites:
# - GitHub App 'fork-sync-approver' installed on this repo (App ID 4510581)
# - Repo secret APPID4510581 = .pem private key of the App
# - Repo secret APP_ID = 4510581 (just the number)
#
# Pattern sources (verified Aug 2026):
# - agent-of-empires #1420 (pull-based)
# - ConductionNL #61 (PAT-approver pattern; we use App instead of PAT)
# - ConductionNL #61 (PAT-approver pattern; we use Dependabot-style handoff instead)
# - bluesky-social/social-app PR #11010 (App-based workflow approval)

on:
Expand Down Expand Up @@ -87,6 +88,7 @@ jobs:
run: |
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
SYNC_BRANCH="sync/upstream-$(date -u +%Y%m%d-%H%M%S)"
# Create the sync branch on the fork, pointing at upstream's HEAD
gh api -X POST "repos/${{ github.repository }}/git/refs" \
-f ref="refs/heads/$SYNC_BRANCH" \
-f sha="${{ steps.upstream.outputs.upstream_head }}" \
Expand All @@ -100,37 +102,29 @@ jobs:
--title "Sync fork with $PARENT:$UPSTREAM_DEFAULT" \
--body "Automated pull-based sync from [$PARENT](https://github.com/$PARENT).

Uses GitHub App installation token for short-lived, repo-scoped approval (actions/create-github-app-token@v2).")
**Handoff to human:** GitHub blocks any identity from approving its own PR, so the workflow cannot auto-approve. Please click **Approve** then **Merge** when you're ready.

*(This is the Dependabot-style pattern: the workflow opens the PR, you approve and merge. No credentials required.)*)
Automated sync uses GitHub App installation token for PR creation only.")
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "url=$PR_URL" >> $GITHUB_OUTPUT
echo "branch=$SYNC_BRANCH" >> $GITHUB_OUTPUT
echo "::notice::Opened sync PR #$PR_NUMBER: $PR_URL"

- name: Approve PR via GitHub App installation token
- name: Post handoff comment requesting review
if: steps.upstream.outputs.sync_needed == 'true'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
PR_NUMBER="${{ steps.pr.outputs.number }}"
if gh api -X POST "repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews" \
-f event="APPROVE" \
-f body="Auto-approved by Sync Fork workflow (GitHub App installation token)."; then
echo "::notice::Approved PR #$PR_NUMBER via GitHub App token."
else
echo "::error::Failed to approve PR #$PR_NUMBER."
exit 1
fi
# Post a comment tagging the repo owner so they get a notification
gh pr comment "$PR_NUMBER" --repo "${{ github.repository }}" --body "👋 **Sync ready for review** @niStee — this PR syncs upstream changes into this fork.

- name: Merge PR (all ruleset requirements satisfied)
if: steps.upstream.outputs.sync_needed == 'true'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
PR_NUMBER="${{ steps.pr.outputs.number }}"
if gh pr merge "$PR_NUMBER" --repo "${{ github.repository }}" --squash --delete-branch; then
echo "::notice::Merged sync PR #$PR_NUMBER."
else
echo "::error::Failed to merge PR #$PR_NUMBER — check for required status checks or other ruleset rules."
exit 1
fi
**To merge:**
1. Wait for CI checks to pass (if any)
2. Click **Approve**
3. Click **Merge**

_Why manual approval? GitHub blocks any identity from approving its own PR (HTTP 422). The workflow can't auto-approve itself, so this is the standard Dependabot-style handoff pattern: workflow opens, you merge._"
echo "::notice::Posted review request comment on PR #$PR_NUMBER"
Loading