Skip to content

Commit f3e5aa1

Browse files
committed
ci: add automation
1 parent b707d13 commit f3e5aa1

3 files changed

Lines changed: 389 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
build-and-test:
13+
name: Build and test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v7
17+
with:
18+
submodules: 'true'
19+
20+
- uses: actions/setup-go@v7
21+
with:
22+
go-version-file: go.mod
23+
cache: true
24+
25+
- name: Build
26+
run: go build ./...
27+
28+
- name: Test
29+
run: go test -race -count=1 ./...
30+
31+
# This repo holds several Go modules, each with its own go.mod, and
32+
# `go build ./...` never crosses a module boundary. The two steps above
33+
# therefore cover only the root module; this one covers the rest.
34+
#
35+
# The set of modules is discovered rather than written out, because it
36+
# differs on either side of an upstream sync. Right now this fork has
37+
# codelab, repl, repl/appengine and server, while current upstream has
38+
# codelab, conformance, policy, repl and tools. A hardcoded list breaks
39+
# every time that changes.
40+
#
41+
# Three are skipped because they do not build against unmodified upstream
42+
# cel-go either, so building them would report upstream's breakage as
43+
# ours:
44+
#
45+
# repl - its go.mod replaces cel.dev/expr with ../../cel-spec, a
46+
# sibling checkout that does not exist on a CI runner
47+
# codelab - stale go.mod; `go mod tidy` is needed before it builds
48+
# tools - does not compile against the version of the policy module
49+
# that it pins
50+
- name: Build other Go modules
51+
run: |
52+
skip="repl codelab tools"
53+
for m in $(git ls-files '*/go.mod' | sed 's|/go.mod$||' | sort -u); do
54+
case " $skip " in
55+
*" $m "*) echo "skipping $m"; continue ;;
56+
esac
57+
echo "::group::go build ./... ($m)"
58+
(cd "$m" && go build ./...)
59+
echo "::endgroup::"
60+
done

.github/workflows/sync-upstream.sh

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/env bash
2+
# Syncs this fork with upstream cel-go.
3+
# Run it locally, or from Actions -> "Sync with upstream" -> Run workflow.
4+
#
5+
# sync-upstream.sh # full sync
6+
# sync-upstream.sh --continue # after resolving conflicts by hand
7+
# sync-upstream.sh rename [--reverse] # just the rename
8+
#
9+
# This fork differs from upstream in exactly one way: every
10+
# `github.com/google/cel-go` import is rewritten to `github.com/authzed/cel-go`.
11+
# Merging upstream directly means both sides edited the same import lines, so it
12+
# conflicts on nearly every file upstream touched -- ~77 conflicts, none real.
13+
#
14+
# So the rename is applied last, and never merged:
15+
#
16+
# 1. un-rename, making our tree match the upstream commit we last synced from
17+
# 2. merge upstream, which now sees no changes on our side
18+
# 3. re-rename
19+
# 4. build and test
20+
#
21+
# Commits, never pushes. The workflow hands the commits to
22+
# peter-evans/create-pull-request.
23+
#
24+
# Merge the resulting PR with a merge commit, NOT a squash. Step 2's merge
25+
# commit is what moves `git merge-base master upstream/master` forward, and that
26+
# is what keeps the next sync clean.
27+
#
28+
# Env:
29+
# PREFER_UPSTREAM=0 stop on conflicts instead of taking upstream's version
30+
# NO_TEST=1 skip `go test ./...`
31+
# UPSTREAM_REMOTE default: upstream
32+
# UPSTREAM_URL default: https://github.com/cel-expr/cel-go.git
33+
# UPSTREAM_BRANCH default: master
34+
# BRANCH branch to commit on; defaults to a new
35+
# sync-upstream-<sha> locally, current branch on CI
36+
set -euo pipefail
37+
38+
cd "$(git rev-parse --show-toplevel)"
39+
40+
UPSTREAM_PATH="github.com/google/cel-go"
41+
FORK_PATH="github.com/authzed/cel-go"
42+
43+
UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}"
44+
UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/cel-expr/cel-go.git}"
45+
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}"
46+
PREFER_UPSTREAM="${PREFER_UPSTREAM:-1}"
47+
48+
# Skipped when verifying: these do not build against unmodified upstream
49+
# either, so a failure there says nothing about the sync.
50+
SKIP_MODULES="repl codelab tools"
51+
52+
log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
53+
warn() { printf '\033[33m%s\033[0m\n' "$*"; }
54+
die() { printf '\n\033[31merror: %s\033[0m\n' "$*" >&2; exit 1; }
55+
56+
# The entire fork delta, as one substitution.
57+
rename() {
58+
local from="$UPSTREAM_PATH" to="$FORK_PATH" files count
59+
if [[ "${1:-}" == "--reverse" ]]; then
60+
from="$FORK_PATH"
61+
to="$UPSTREAM_PATH"
62+
fi
63+
64+
# -I skips binary files, and git grep only looks at tracked ones.
65+
files="$(git grep -I --name-only --fixed-strings -e "$from" -- . || true)"
66+
if [[ -z "$files" ]]; then
67+
echo "rename: no occurrences of ${from}; nothing to do"
68+
return 0
69+
fi
70+
71+
count="$(printf '%s\n' "$files" | wc -l | tr -d ' ')"
72+
printf '%s\n' "$files" | tr '\n' '\0' \
73+
| xargs -0 perl -pi -e "s{\\Q${from}\\E}{${to}}g"
74+
echo "rename: ${from} -> ${to} in ${count} file(s)"
75+
}
76+
77+
# Take upstream's version, and honor upstream's deletions. Correct for a fork
78+
# whose only intentional change is the rename, which goes back on afterwards.
79+
auto_resolve() {
80+
local unmerged path
81+
unmerged="$(git diff --name-only --diff-filter=U)"
82+
[[ -z "$unmerged" ]] && return 0
83+
84+
warn "Auto-resolving in upstream's favor (PREFER_UPSTREAM=1):"
85+
while IFS= read -r path; do
86+
[[ -z "$path" ]] && continue
87+
if git rev-parse -q --verify ":3:$path" >/dev/null 2>&1; then
88+
git checkout --theirs -- "$path"
89+
git add -- "$path"
90+
echo " took upstream: $path"
91+
else
92+
git rm -q --force -- "$path" >/dev/null
93+
echo " upstream deleted: $path"
94+
fi
95+
done <<< "$unmerged"
96+
}
97+
98+
# Builds every Go module in the repo. The set is discovered, not listed,
99+
# because it changes as upstream adds and drops modules.
100+
verify() {
101+
local m
102+
for m in . $(git ls-files '*/go.mod' | sed 's|/go.mod$||' | sort -u); do
103+
case " $SKIP_MODULES " in
104+
*" $m "*) echo " skipped $m" ; continue ;;
105+
esac
106+
( cd "$m" && go build ./... ) || die "go build failed in module '$m'"
107+
echo " go build OK $m"
108+
done
109+
if [[ "${NO_TEST:-0}" == "1" ]]; then
110+
echo " go test skipped (NO_TEST=1)"
111+
else
112+
go test ./... >/dev/null || die "go test failed in the root module"
113+
echo " go test OK ."
114+
fi
115+
}
116+
117+
finish() {
118+
log "Step 3/4: re-applying the rename"
119+
rename
120+
if git diff --quiet && git diff --cached --quiet; then
121+
echo "Nothing to re-rename."
122+
else
123+
git add -A
124+
git commit -qm "Re-apply authzed rename"
125+
fi
126+
127+
log "Step 4/4: verifying"
128+
verify
129+
130+
# The fork should be upstream plus the rename and nothing else.
131+
local extra
132+
extra="$(git diff "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" -- . ':(exclude).github' \
133+
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -cv 'cel-go' || true)"
134+
echo " non-rename lines vs upstream (excluding .github/): ${extra}"
135+
136+
log "Sync complete on '$(git rev-parse --abbrev-ref HEAD)'. Nothing was pushed."
137+
}
138+
139+
case "${1:-}" in
140+
rename)
141+
shift
142+
rename "$@"
143+
exit 0
144+
;;
145+
--continue)
146+
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
147+
if git diff --name-only --diff-filter=U | grep -q .; then
148+
die "still unresolved: $(git diff --name-only --diff-filter=U | tr '\n' ' ')"
149+
fi
150+
git commit -qm "Merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into fork"
151+
fi
152+
finish
153+
exit 0
154+
;;
155+
"") ;;
156+
*) die "unknown argument '${1}'" ;;
157+
esac
158+
159+
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
160+
die "a merge is already in progress; run 'git merge --abort' or use --continue"
161+
fi
162+
if ! git diff --quiet || ! git diff --cached --quiet; then
163+
die "working tree is dirty; commit or stash first"
164+
fi
165+
166+
log "Step 1/4: fetching ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
167+
git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1 \
168+
|| git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
169+
git fetch --quiet "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH"
170+
UPSTREAM_REF="$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
171+
UPSTREAM_SHA="$(git rev-parse --short "$UPSTREAM_REF")"
172+
echo "upstream is at ${UPSTREAM_SHA}"
173+
174+
if git merge-base --is-ancestor "$UPSTREAM_REF" HEAD; then
175+
log "Already up to date with ${UPSTREAM_REF}. Nothing to do."
176+
exit 0
177+
fi
178+
179+
# On CI, commit onto the branch that is already checked out.
180+
# create-pull-request only pushes commits as-is when they sit on the base
181+
# branch; otherwise it cherry-picks them, and cherry-pick cannot replay the
182+
# merge commit that step 2 creates.
183+
if [[ -n "${BRANCH:-}" ]]; then
184+
git checkout -q -B "$BRANCH"
185+
elif [[ -z "${GITHUB_ACTIONS:-}" ]]; then
186+
git checkout -q -B "sync-upstream-${UPSTREAM_SHA}"
187+
fi
188+
echo "working on '$(git rev-parse --abbrev-ref HEAD)'"
189+
190+
BASE="$(git merge-base HEAD "$UPSTREAM_REF")"
191+
192+
log "Step 2/4: reverting the rename, then merging"
193+
rename --reverse
194+
git add -A
195+
if git diff --cached --quiet; then
196+
echo "Nothing to un-rename."
197+
else
198+
git commit -qm "Revert authzed rename for upstream sync"
199+
fi
200+
201+
# If un-renaming did not make our tree identical to the merge base, the fork
202+
# carries changes of its own, and those files are the only ones that can
203+
# conflict below.
204+
DRIFT="$(git diff --name-only "$BASE" HEAD)"
205+
if [[ -n "$DRIFT" ]]; then
206+
warn "Fork differs from upstream beyond the rename in $(printf '%s\n' "$DRIFT" | wc -l | tr -d ' ') file(s):"
207+
printf '%s\n' "$DRIFT" | sed 's/^/ /'
208+
echo " -> only these files can conflict."
209+
else
210+
echo "drift check: clean. Fork is pure upstream + rename; the merge cannot conflict."
211+
fi
212+
213+
if ! git merge --no-edit "$UPSTREAM_REF"; then
214+
if [[ "$PREFER_UPSTREAM" == "1" ]]; then
215+
auto_resolve
216+
git commit -qm "Merge ${UPSTREAM_REF} into fork"
217+
else
218+
warn "Conflicts (real fork divergence, not rename noise):"
219+
git diff --name-only --diff-filter=U | sed 's/^/ /'
220+
cat <<'EOF'
221+
222+
Resolve them, `git add` the files, then run:
223+
.github/workflows/sync-upstream.sh --continue
224+
EOF
225+
exit 1
226+
fi
227+
fi
228+
229+
finish
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Sync with upstream
2+
3+
# Run it whenever you want: Actions -> "Sync with upstream" -> Run workflow.
4+
#
5+
# All the git logic lives in sync-upstream.sh, next to this file, so the
6+
# identical run works locally. See that script for why the rename has to be
7+
# applied last.
8+
on:
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
15+
jobs:
16+
sync:
17+
runs-on: ubuntu-latest
18+
steps:
19+
# Every step here authenticates with AUTHZEDBOT_REPO_SCOPED_TOKEN, a
20+
# personal access token, rather than the GITHUB_TOKEN that Actions
21+
# provides for free.
22+
#
23+
# GITHUB_TOKEN is a throwaway credential scoped to this single workflow
24+
# run, and GitHub will not start a new workflow run in response to
25+
# anything done with it. That rule exists so a workflow cannot trigger
26+
# itself forever. The consequence here: a pull request opened with
27+
# GITHUB_TOKEN arrives with no checks on it at all, ci.yaml never runs,
28+
# and the auto-merge step at the end of this job merges code that
29+
# nothing has built or tested.
30+
#
31+
# AUTHZEDBOT_REPO_SCOPED_TOKEN belongs to the authzedbot account, so the
32+
# pull request is attributed to that account rather than to
33+
# GITHUB_TOKEN, and the rule above does not apply to it. ci.yaml runs on
34+
# the PR, and auto-merge then has a check worth waiting for.
35+
#
36+
# fetch-depth: 0 because the sync merges upstream history, which a
37+
# shallow clone does not contain.
38+
- name: Check out fork
39+
uses: actions/checkout@v7
40+
with:
41+
fetch-depth: 0
42+
token: ${{ secrets.AUTHZEDBOT_REPO_SCOPED_TOKEN }}
43+
44+
- uses: actions/setup-go@v7
45+
with:
46+
go-version-file: go.mod
47+
cache: true
48+
49+
# Commits onto the checked-out master, which is what lets
50+
# create-pull-request push them as-is. It never pushes.
51+
- name: Sync with upstream and verify
52+
run: .github/workflows/sync-upstream.sh
53+
54+
- name: Record upstream revision
55+
run: echo "UPSTREAM_SHA=$(git rev-parse --short upstream/master)" >> "$GITHUB_ENV"
56+
57+
# merge-commit, never squash: the merge commit from the sync is what
58+
# advances `git merge-base master upstream/master`, and that is what keeps
59+
# the next sync conflict-free.
60+
- name: Create Pull Request
61+
id: cpr-sync
62+
uses: peter-evans/create-pull-request@v8
63+
with:
64+
delete-branch: "true"
65+
token: ${{ secrets.AUTHZEDBOT_REPO_SCOPED_TOKEN }}
66+
branch: sync-upstream
67+
title: "Sync with upstream ${{ env.UPSTREAM_SHA }}"
68+
body: |-
69+
Automatically Generated Pull Request
70+
71+
Synced with upstream `cel-expr/cel-go@${{ env.UPSTREAM_SHA }}` via `.github/workflows/sync-upstream.sh`:
72+
73+
1. Reverted the `authzed` import rename
74+
2. Merged `upstream/master`
75+
3. Re-applied the rename
76+
4. Verified `go build` (root, `policy`, `conformance`) and `go test ./...`
77+
78+
The result is upstream plus the import-path rename and nothing else.
79+
80+
> [!IMPORTANT]
81+
> Merge this with a **merge commit**, not a squash. The merge commit is
82+
> what advances `git merge-base master upstream/master`, which is what
83+
> keeps the next sync conflict-free.
84+
85+
- name: Approve the Pull Request
86+
if: steps.cpr-sync.outputs.pull-request-operation == 'created' || steps.cpr-sync.outputs.pull-request-operation == 'updated'
87+
uses: juliangruber/approve-pull-request-action@v2.0.4
88+
with:
89+
github-token: ${{ secrets.AUTHZEDAPPROVER_REPO_SCOPED_TOKEN }}
90+
repo: authzed/cel-go
91+
number: ${{ steps.cpr-sync.outputs.pull-request-number }}
92+
93+
- name: Enable auto-merge
94+
if: steps.cpr-sync.outputs.pull-request-operation == 'created' || steps.cpr-sync.outputs.pull-request-operation == 'updated'
95+
uses: peter-evans/enable-pull-request-automerge@v3
96+
with:
97+
token: ${{ secrets.AUTHZEDBOT_REPO_SCOPED_TOKEN }}
98+
repository: authzed/cel-go
99+
pull-request-number: ${{ steps.cpr-sync.outputs.pull-request-number }}
100+
merge-method: merge

0 commit comments

Comments
 (0)