Repository navigation
183 lines (169 loc) · 8.51 KB
/
Copy pathupstream-sync.yml
File metadata and controls
183 lines (169 loc) · 8.51 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
name: upstream-sync
# Absorb an upstream release into `develop` as a reviewable pull request.
#
# We follow upstream TAGS rather than its `dev` branch: a tag is a point upstream decided
# was coherent, `dev` is whatever landed an hour ago.
#
# This never pushes to `develop` and never resolves a conflict. A fork's conflicts are a
# judgement about which side is right -- ours is usually a rebrand, theirs is usually a
# fix -- and a machine guessing at that produces a tree nobody can review. On conflict it
# reports which paths disagreed and stops.
#
# Needs no secrets: the default GITHUB_TOKEN can push a branch and open a pull request.
on:
workflow_dispatch:
inputs:
tag:
description: "Upstream tag to merge, e.g. v1.18.32. Leave empty for upstream's latest release."
required: false
type: string
permissions:
# Pushing the sync branch is all this needs. It deliberately does not open the pull
# request, so `pull-requests: write` would be a permission granted for nothing.
contents: write
concurrency:
group: upstream-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 30
# Pinned to this repository's immutable id, not its name: a fork of a fork should
# never open pull requests here, and a rename must not silently switch the check off.
if: github.repository_id == '1371675259'
steps:
- name: Checkout develop
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: develop
# Full history: a shallow clone has no merge base with upstream, which turns a
# three-way merge into the 650-conflict mess this fork exists to avoid.
fetch-depth: 0
- name: Resolve the upstream tag
id: target
env:
GH_TOKEN: ${{ github.token }}
REQUESTED: ${{ inputs.tag }}
run: |
set -euo pipefail
git remote add upstream https://github.com/anomalyco/opencode.git
# Upstream tags land in `refs/upstream-tags/*`, NOT `refs/tags/*`.
#
# Upstream publishes over a thousand tags and this fork inherited every one of them,
# which is what made our own ten releases impossible to find and made a `v*` glob
# deletion unusable. They were deleted, and this refspec is what keeps them deleted:
# a plain `--tags` fetch pulls the whole set back into the tag namespace, where the
# next `git push --tags` would republish them.
#
# `--no-tags` is also passed because the refspec alone does not stop git's automatic
# tag following from filling `refs/tags/*` with anything reachable.
git fetch --quiet --no-tags upstream "+refs/tags/*:refs/upstream-tags/*"
if [ -n "${REQUESTED:-}" ]; then
tag="$REQUESTED"
else
tag="$(gh release view --repo anomalyco/opencode --json tagName --jq .tagName)"
echo "upstream's latest release is $tag"
fi
if ! git rev-parse --verify --quiet "refs/upstream-tags/$tag" >/dev/null; then
echo "::error::upstream has no tag $tag"
exit 1
fi
# Nothing to do when the tag is already an ancestor. Reported rather than
# failed: a scheduled run finding no new release is the normal case.
if git merge-base --is-ancestor "refs/upstream-tags/$tag" HEAD; then
echo "already-merged=true" >> "$GITHUB_OUTPUT"
echo "::notice::$tag is already an ancestor of develop; nothing to sync"
else
echo "already-merged=false" >> "$GITHUB_OUTPUT"
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "branch=sync/upstream-$tag" >> "$GITHUB_OUTPUT"
- name: Merge the tag
id: merge
if: steps.target.outputs.already-merged == 'false'
env:
TAG: ${{ steps.target.outputs.tag }}
BRANCH: ${{ steps.target.outputs.branch }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$BRANCH"
base="$(git merge-base HEAD "refs/upstream-tags/$TAG")"
echo "merge base: $(git rev-parse --short "$base")"
echo "upstream commits to absorb: $(git rev-list --count "$base..refs/upstream-tags/$TAG")"
if git merge --no-edit "refs/upstream-tags/$TAG"; then
echo "conflicted=false" >> "$GITHUB_OUTPUT"
else
# Record what disagreed BEFORE aborting, then abort so the pushed branch is a
# clean starting point for a human rather than a tree full of markers.
git diff --name-only --diff-filter=U > "$RUNNER_TEMP/conflicts.txt" || true
count="$(wc -l < "$RUNNER_TEMP/conflicts.txt" | tr -d ' ')"
echo "conflicted=true" >> "$GITHUB_OUTPUT"
echo "conflict-count=$count" >> "$GITHUB_OUTPUT"
git merge --abort
echo "::warning::$count path(s) conflicted; a human has to resolve this one"
fi
- name: Push the branch and point at the pull request
if: steps.target.outputs.already-merged == 'false' && steps.merge.outputs.conflicted == 'false'
env:
TAG: ${{ steps.target.outputs.tag }}
BRANCH: ${{ steps.target.outputs.branch }}
run: |
set -euo pipefail
git push --set-upstream origin "$BRANCH"
# Deliberately does not open the pull request. `gh pr create` from a workflow
# needs "Allow GitHub Actions to create and approve pull requests", and that one
# setting grants BOTH -- in a public repository a workflow that can approve pull
# requests can approve its own, which costs more than clicking a link.
url="${{ github.server_url }}/${{ github.repository }}/compare/develop...${BRANCH}?expand=1"
{
echo "### Upstream $TAG merged cleanly onto \`$BRANCH\`"
echo
echo "[Open the pull request into \`develop\`]($url)"
echo
echo "A clean three-way merge does **not** mean a correct one. Check that"
echo "upstream's changes did not undo anything deliberate -- the legacy"
echo "\`.opencode\` config directory, the three \`opencode-*\` npm dependencies,"
echo "and our provider endpoint are the usual casualties. See"
echo "\`docs/BRANCHING.md\`."
} >> "$GITHUB_STEP_SUMMARY"
echo "::notice::upstream $TAG merged onto $BRANCH; open the pull request at $url"
- name: Report a conflicting sync
if: steps.merge.outputs.conflicted == 'true'
env:
TAG: ${{ steps.target.outputs.tag }}
COUNT: ${{ steps.merge.outputs.conflict-count }}
run: |
set -euo pipefail
# Written to the run summary rather than filed as an issue: `gh issue create`
# needs `issues: write`, and widening this workflow's token to file its own
# tickets is not worth it when the summary is where someone reading a failed run
# already is.
{
echo "### Upstream $TAG needs manual resolution"
echo
echo "Merging upstream \`$TAG\` into \`develop\` conflicts in **$COUNT path(s)**."
echo "The merge was aborted rather than pushing a tree full of conflict markers."
echo
echo '```'
cat "$RUNNER_TEMP/conflicts.txt"
echo '```'
echo
echo "To resolve it locally:"
echo
echo '```sh'
echo "git remote add upstream https://github.com/anomalyco/opencode.git # once"
echo "# Upstream's tags stay out of refs/tags: it publishes over a thousand of them"
echo "# and this repository's own releases have to remain findable."
echo "git fetch --no-tags upstream '+refs/tags/*:refs/upstream-tags/*'"
echo "git switch develop && git pull"
echo "git switch -c sync/upstream-$TAG"
echo "git merge refs/upstream-tags/$TAG"
echo '```'
echo
echo "Conflicts here are usually rebranding: upstream edited a line we renamed."
echo "Keep ours, take their surrounding change. Do not finish the rebrand while"
echo "resolving -- some \`opencode\` strings are load-bearing, see \`NOTICE.md\`."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::upstream $TAG conflicts in $COUNT path(s); see the run summary"