forked from drakkan/sftpgo
-
Notifications
You must be signed in to change notification settings - Fork 0
202 lines (185 loc) · 8.91 KB
/
Copy pathsync-upstream.yaml
File metadata and controls
202 lines (185 loc) · 8.91 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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
---
# Requires:
# * Settings -> Actions -> General ->
# "Allow GitHub Actions to create and approve pull requests".
# * A repo secret SYNC_TOKEN holding a PAT with the `workflow` scope
name: Sync upstream release
on: # yamllint disable-line rule:truthy
schedule:
# Mondays 06:00 UTC
- cron: '0 6 * * 1'
workflow_dispatch:
env:
UPSTREAM_REPO: drakkan/sftpgo
TARGET_BRANCH: main
# The only workflows this fork runs. Everything else upstream ships (their
# CI, Windows/arm64 builds, their docker and release pipelines) is deleted
# on every sync, so an upstream workflow can never start running here
# unnoticed. Extended regex, anchored to .github/workflows/<name>.
KEEP_WORKFLOWS: 'build-image\.yaml|sync-upstream\.yaml'
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.TARGET_BRANCH }}
fetch-depth: 0
# PAT with `workflow` scope so the push may include upstream
# changes to .github/workflows/*. Persisted for the git push step.
token: ${{ secrets.SYNC_TOKEN }}
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Determine latest upstream release
id: upstream
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG=$(gh release view --repo "${UPSTREAM_REPO}" --json tagName --jq .tagName)
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "branch=upstream-sync/${TAG}" >> "$GITHUB_OUTPUT"
echo "Latest upstream release: ${TAG}"
- name: Merge upstream release onto sync branch
id: merge
env:
UPSTREAM_TAG: ${{ steps.upstream.outputs.tag }}
SYNC_BRANCH: ${{ steps.upstream.outputs.branch }}
run: |
set -euo pipefail
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
git fetch upstream "refs/tags/${UPSTREAM_TAG}:refs/tags/${UPSTREAM_TAG}"
# Nothing to do if the default branch already contains this release.
# `--is-ancestor` exits 1 for "no" but 128 for a bad revision, so the
# two cases are told apart instead of both meaning "not up to date".
ANCESTOR_RC=0
git merge-base --is-ancestor "refs/tags/${UPSTREAM_TAG}" "origin/${TARGET_BRANCH}" || ANCESTOR_RC=$?
if [ "${ANCESTOR_RC}" -eq 0 ]; then
echo "up_to_date=true" >> "$GITHUB_OUTPUT"
echo "Default branch already contains ${UPSTREAM_TAG}."
exit 0
elif [ "${ANCESTOR_RC}" -ne 1 ]; then
echo "::error::git merge-base failed (rc=${ANCESTOR_RC})"
exit "${ANCESTOR_RC}"
fi
git checkout -B "${SYNC_BRANCH}" "origin/${TARGET_BRANCH}"
MERGE_RC=0
git merge --no-edit "refs/tags/${UPSTREAM_TAG}" || MERGE_RC=$?
if [ "${MERGE_RC}" -ne 0 ] && [ -z "$(git ls-files --unmerged)" ]; then
# Merge failed for a reason other than conflicts (bad ref, dirty
# tree, ...). Fail loudly rather than push a bogus branch.
echo "::error::git merge failed without conflicts (rc=${MERGE_RC})"
exit "${MERGE_RC}"
fi
# Delete every workflow upstream ships; this fork runs only its own.
# Done before the conflict report so conflicts in workflows we drop
# anyway are not raised to the reviewer. `sort -u` because during a
# conflicted merge ls-files lists a path once per stage.
DROPPED=$(git ls-files -- '.github/workflows/*.yml' '.github/workflows/*.yaml' \
| sort -u | grep -Ev "^\.github/workflows/(${KEEP_WORKFLOWS})$" || true)
if [ -n "${DROPPED}" ]; then
echo "Dropping upstream workflows:"
echo "${DROPPED}"
# -f is required to remove paths that are unmerged or modified.
while IFS= read -r wf; do
git rm -q -f -- "${wf}"
done <<< "${DROPPED}"
fi
CONFLICTED=$(git ls-files --unmerged | awk '{print $4}' | sort -u | sed 's/^/- /')
if [ -n "${CONFLICTED}" ]; then
# Conflicted merge: commit the markers so the reviewer has a branch to fix.
echo "conflicts=true" >> "$GITHUB_OUTPUT"
{
echo "conflicted_files<<EOF"
echo "${CONFLICTED}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
echo "conflicts=false" >> "$GITHUB_OUTPUT"
fi
if [ "${MERGE_RC}" -ne 0 ]; then
# `add -u`, not `add -A`: restricts staging to tracked and unmerged
# paths, so untracked files in the runner's working tree can never
# end up in the merge commit.
git add -u
git commit --no-edit
elif [ -n "${DROPPED}" ]; then
git commit -q -m "Drop upstream workflows not used by this fork"
fi
echo "up_to_date=false" >> "$GITHUB_OUTPUT"
- name: Push sync branch
if: steps.merge.outputs.up_to_date == 'false'
env:
# Via env, not a `${{ }}` inline expansion: the branch name is derived
# from an upstream tag and must not be pasted into the shell.
SYNC_BRANCH: ${{ steps.upstream.outputs.branch }}
run: git push --force-with-lease origin "${SYNC_BRANCH}"
- name: Open or update pull request
if: steps.merge.outputs.up_to_date == 'false'
env:
# PAT (not GITHUB_TOKEN) so the PR triggers build-image's
# pull_request check to confirm the merged upstream still builds.
GH_TOKEN: ${{ secrets.SYNC_TOKEN }}
UPSTREAM_TAG: ${{ steps.upstream.outputs.tag }}
SYNC_BRANCH: ${{ steps.upstream.outputs.branch }}
CONFLICTS: ${{ steps.merge.outputs.conflicts }}
CONFLICTED_FILES: ${{ steps.merge.outputs.conflicted_files }}
run: |
set -euo pipefail
{
printf '%s\n' "Merges upstream release ${UPSTREAM_TAG} (${UPSTREAM_REPO}) into ${TARGET_BRANCH}."
printf '%s\n' ""
printf '%s\n' "Workflows upstream ships are deleted by the sync job; this fork runs only \`build-image.yaml\` and \`sync-upstream.yaml\`."
printf '%s\n' ""
if [ "${CONFLICTS}" = "true" ]; then
printf '%s\n' "**This branch contains conflict markers and does NOT build.** Resolve it locally:"
printf '%s\n' ""
printf '%s\n' '```sh'
printf '%s\n' "git fetch origin"
printf '%s\n' "git checkout ${SYNC_BRANCH}"
printf '%s\n' "git status # find conflicts, fix the <<<<<<< markers"
printf '%s\n' "git add -u # -u, not -A: do not sweep in untracked files"
printf '%s\n' "git commit"
printf '%s\n' "git push"
printf '%s\n' '```'
printf '%s\n' ""
printf '%s\n' "Conflicted files:"
printf '%s\n' "${CONFLICTED_FILES}"
printf '%s\n' ""
printf '%s\n' "Keep this fork's custom changes intact while taking the upstream changes."
else
printf '%s\n' "No conflicts. Review the upstream changes, then merge."
printf '%s\n' "After merge, publish a new Release to build the image."
fi
} > pr-body.md
# Conflicted branches are opened as drafts: they carry conflict
# markers and do not build, so they must not look mergeable.
EXTRA_ARGS=()
if [ "${CONFLICTS}" = "true" ]; then
TITLE="Merge upstream release ${UPSTREAM_TAG} — conflicts, manual resolution needed"
EXTRA_ARGS+=(--draft)
else
TITLE="Merge upstream release ${UPSTREAM_TAG}"
fi
# --repo pins gh to this repo (detached mode) so --head is treated as a
# branch in swisscom/sftpgo, not a cross-fork ref. Without it, gh infers
# the head from the PAT user and the fork relationship, producing
# "Head ref must be a branch / Head sha can't be blank".
OPEN=$(gh pr list --repo "${GITHUB_REPOSITORY}" --head "${SYNC_BRANCH}" --state open --json number --jq 'length')
if [ "${OPEN}" != "0" ]; then
# PR already open — the force-push above updated it; just refresh title/body.
gh pr edit "${SYNC_BRANCH}" --repo "${GITHUB_REPOSITORY}" --title "${TITLE}" --body-file pr-body.md
else
gh pr create \
--repo "${GITHUB_REPOSITORY}" \
--base "${TARGET_BRANCH}" \
--head "${SYNC_BRANCH}" \
--title "${TITLE}" \
--body-file pr-body.md \
"${EXTRA_ARGS[@]}"
fi