Repository navigation
138 lines (121 loc) · 5.79 KB
/
Copy pathsync-fork.yml
File metadata and controls
138 lines (121 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
name: Sync Fork
# Fast-forwards this repo's `main` to TryGhost/Ghost's `main`.
#
# This is the manual equivalent of GitHub's "Sync fork" button, for copies of
# Ghost that GitHub doesn't treat as forks — GitHub won't let you fork a public
# repo privately, so those repos are hard copies with no upstream relationship
# and no sync button.
#
# It only ever fast-forwards. If `main` has diverged from upstream — any commit
# on `main` that upstream doesn't have — the run fails and tells you what to do,
# rather than merging or rewriting anything. Fork-local work belongs on branches
# other than `main`.
#
# Auth: requires the `SYNC_FORK_TOKEN` secret — a PAT or GitHub App token with
# contents write + workflows write. GITHUB_TOKEN can't stand in: its pushes
# don't trigger workflows, and GitHub rejects any push from it that touches
# `.github/workflows/` (there's no `workflows` scope in `permissions:` to grant
# — PATs and Apps only), which upstream changes regularly.
#
# If `main` is a protected branch, the token's identity needs permission to push
# to it (bypass list / "Allow specified actors").
on:
schedule:
# Daily at 05:37 UTC. Off the top of the hour to dodge GH Actions scheduler
# contention (see the same note in renovate.yml).
- cron: '37 5 * * *'
workflow_dispatch:
concurrency:
group: sync-fork
cancel-in-progress: false
permissions:
contents: read
env:
UPSTREAM_REPO: https://github.com/TryGhost/Ghost.git
jobs:
sync:
# Runs only in TryGhost-owned copies: never in upstream itself (nothing to
# sync from), never in real GitHub forks (which have the built-in sync), and
# never in someone else's org. `fork` is absent from some event payloads, so
# compare against `true` rather than negating.
if: >-
github.repository_owner == 'TryGhost'
&& github.repository != 'TryGhost/Ghost'
&& github.event.repository.fork != true
name: Fast-forward main to upstream
runs-on: ubuntu-slim
steps:
- name: Require SYNC_FORK_TOKEN
env:
# `secrets` isn't available in `if:`, so test for it in the script.
HAS_SYNC_FORK_TOKEN: ${{ secrets.SYNC_FORK_TOKEN != '' }}
run: |
set -euo pipefail
if [ "$HAS_SYNC_FORK_TOKEN" != "true" ]; then
{
echo "## ⚠️ \`SYNC_FORK_TOKEN\` is not set"
echo
echo "This workflow needs a PAT or GitHub App token with contents write + workflows write, stored as the \`SYNC_FORK_TOKEN\` secret."
echo
echo "\`GITHUB_TOKEN\` can't be used instead: pushes made with it don't trigger CI, and GitHub rejects them outright whenever upstream has changed a file under \`.github/workflows/\`."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::SYNC_FORK_TOKEN is not set — see job summary"
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: main
# Full history — a shallow clone can't compute a merge base against
# upstream. Blobless keeps that cheap: this job only walks commits, it
# never reads file contents. Submodules are deliberately left
# uninitialised; a fast-forward only moves the gitlinks.
fetch-depth: 0
filter: blob:none
token: ${{ secrets.SYNC_FORK_TOKEN }}
- name: Fast-forward main
run: |
set -euo pipefail
git remote add upstream "$UPSTREAM_REPO"
git fetch --quiet upstream 'refs/heads/main:refs/remotes/upstream/main'
# Resolve explicitly against the remote-tracking ref: a bare `main`
# resolves to our *local* main, which would compare the fork to itself.
SHA=$(git rev-parse --verify 'refs/remotes/upstream/main^{commit}')
if [ "$(git rev-parse HEAD)" = "$SHA" ]; then
echo "main is already at upstream main (${SHA}); nothing to do."
exit 0
fi
# Strictly ahead: nothing to fast-forward, but those fork-local commits
# turn into a hard divergence failure below as soon as upstream moves.
if git merge-base --is-ancestor "$SHA" HEAD; then
AHEAD=$(git rev-list --count "$SHA"..HEAD)
echo "::warning::main is ${AHEAD} commit(s) ahead of upstream main (${SHA}) — fork-local commits belong on a branch other than main"
exit 0
fi
if ! git merge-base --is-ancestor HEAD "$SHA"; then
# Unrelated histories have no merge base, and `set -e` would abort
# here before the summary is written — fall back to listing all of
# `main` in that case.
if BASE=$(git merge-base HEAD "$SHA" 2>/dev/null); then
LOCAL_COMMITS="${BASE}..HEAD"
else
LOCAL_COMMITS="HEAD"
fi
{
echo "## ⚠️ Cannot fast-forward \`main\`"
echo
echo "\`main\` has diverged from upstream — it carries commits upstream doesn't have:"
echo
echo '```'
git log --oneline --no-decorate "$LOCAL_COMMITS"
echo '```'
echo
echo "This workflow only fast-forwards. Move that work onto a branch other than \`main\`, or reconcile it by hand."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::main has diverged from upstream — see job summary"
exit 1
fi
COUNT=$(git rev-list --count HEAD.."$SHA")
# No `--force`: git rejects a non-fast-forward push on its own, so this
# can't rewrite `main` even if the checks above are wrong.
git push origin "${SHA}:refs/heads/main"
echo "Fast-forwarded \`main\` to upstream \`${SHA}\` (${COUNT} commits)." >> "$GITHUB_STEP_SUMMARY"