Repository navigation
174 lines (156 loc) · 8.43 KB
/
Copy pathsubsplit-turbo-ext.yml
File metadata and controls
174 lines (156 loc) · 8.43 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
# Monorepo-style subsplit: replays every commit that touches turbo-ext/ onto
# https://github.com/phpstan/turbo-ext, where the extension is distributed
# standalone (the phpstan/turbo PIE package on Packagist).
#
# Each replayed commit keeps the original message, author, committer and
# dates, and its tree is the commit's turbo-ext/ subtree — plus a
# "Split-From: <monorepo sha>" trailer and a generated VERSION.txt carrying
# the extension version at that commit (the monorepo short SHA the enabler
# pins; builds in the split cannot compute it — replayed commits have
# different SHAs, and PIE tarballs have no git at all). The trailer is what
# makes the replay incremental and idempotent: a run reads the most recent
# trailer from the split repository and replays only the commits made since,
# on top of the split branch's current head. A failed or skipped run is
# simply caught up by the next one (or by a manual workflow_dispatch).
# git subtree split is not used because it walks the entire monorepo history
# on every run, which takes tens of minutes at this repository's size.
#
# Tag pushes (2.2.*) first bring the split branch up to date and then tag
# the split commit replayed from the last turbo-ext-touching commit at or
# before the tagged monorepo commit. Packagist picks the tag up as a
# phpstan/turbo release, and the release workflow in phpstan/phpstan
# attaches the PIE binary assets to a phpstan/turbo-ext release of the same
# name.
#
# When a new maintained branch (2.3.x, ...) starts, update the triggers
# below (like phar.yml) and create the same-named branch in phpstan/turbo-ext
# from the previous branch's head — that head's trailer keeps the replay
# incremental across the branch point.
name: "Subsplit turbo-ext"
on:
workflow_dispatch:
push:
branches:
- "2.2.x"
tags:
- "2.2.*"
# The replay chain builds on the split branch's current head; two runs
# building on the same head would race each other, so queue them instead.
concurrency:
group: subsplit-turbo-ext
cancel-in-progress: false
permissions: {}
jobs:
subsplit:
name: "Replay turbo-ext commits"
runs-on: "ubuntu-latest"
timeout-minutes: 10
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: "Checkout"
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
fetch-depth: 0 # the replay walks the full history of the branch
- name: "Replay commits touching turbo-ext/ and push them to phpstan/turbo-ext"
env:
PUSH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }}
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
run: |
PREFIX="turbo-ext"
# A tag lives on its maintained branch (2.2.8 → 2.2.x). The replay
# always follows the branch — a tag run just also marks the split
# commit corresponding to the tagged monorepo commit afterwards.
if [ "$REF_TYPE" = "tag" ]; then
BRANCH="${REF_NAME%.*}.x"
git fetch --no-tags origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH"
replay_tip="$(git rev-parse "refs/remotes/origin/$BRANCH")"
if ! git merge-base --is-ancestor HEAD "$replay_tip"; then
echo "::error::Tag $REF_NAME does not point into branch $BRANCH."
exit 1
fi
else
BRANCH="$REF_NAME"
replay_tip="$(git rev-parse HEAD)"
fi
# The split branch must already exist (see the header comment); the
# fetch fails loudly when it does not.
git fetch --no-tags https://github.com/phpstan/turbo-ext.git "refs/heads/$BRANCH"
target_head="$(git rev-parse FETCH_HEAD)"
# The most recent Split-From trailer marks where the previous
# replay stopped. No trailer anywhere (the split repository only
# has its initial commit) means the full history gets replayed.
last_synced="$(git log FETCH_HEAD --format='%(trailers:key=Split-From,valueonly)' | grep -m1 . || true)"
if [ -n "$last_synced" ] && ! git merge-base --is-ancestor "$last_synced" "$replay_tip"; then
echo "::error::$BRANCH does not contain $last_synced, the most recent Split-From trailer in the split repository. Was the branch history rewritten?"
exit 1
fi
# Every commit touching turbo-ext/ so far sits on the first-parent
# chain (verified July 2026), and first-parent order is append-only,
# so the replay never reorders. A maintained-branch forward merge
# that touches turbo-ext/ replays as the merge commit itself.
mapfile -t commits < <(git rev-list --reverse --first-parent ${last_synced:+"$last_synced.."}"$replay_tip" -- "$PREFIX")
tip="$target_head"
if [ "${#commits[@]}" -gt 0 ]; then
for commit in "${commits[@]}"; do
# The version at this commit, mirroring the Makefile's git
# computation: the last commit touching turbo-ext/src as of
# this commit ("dev" before that layout existed). Commits
# already replayed keep the VERSION.txt committed with them,
# even where an older computation produced a different value.
version="$(git log -1 --format=%H "$commit" -- "$PREFIX/src" | cut -c1-7)"
blob="$(printf '%s\n' "${version:-dev}" | git hash-object -w --stdin)"
# a commit removing the directory altogether replays as an empty tree
if subtree="$(git rev-parse -q --verify "$commit:$PREFIX")"; then
tree="$({ git ls-tree "$subtree" | awk -F'\t' '$2 != "VERSION.txt"'; printf '100644 blob %s\tVERSION.txt\n' "$blob"; } | git mktree)"
else
tree="$(git hash-object -t tree /dev/null)"
fi
tip="$(
git log -1 --format=%B "$commit" \
| git interpret-trailers --trailer "Split-From: $commit" \
| GIT_AUTHOR_NAME="$(git log -1 --format=%an "$commit")" \
GIT_AUTHOR_EMAIL="$(git log -1 --format=%ae "$commit")" \
GIT_AUTHOR_DATE="$(git log -1 --format=%aD "$commit")" \
GIT_COMMITTER_NAME="$(git log -1 --format=%cn "$commit")" \
GIT_COMMITTER_EMAIL="$(git log -1 --format=%ce "$commit")" \
GIT_COMMITTER_DATE="$(git log -1 --format=%cD "$commit")" \
git commit-tree "$tree" -p "$tip"
)"
echo "$commit -> $tip"
done
echo "Replayed ${#commits[@]} commit(s), pushing to $BRANCH"
git push "https://x-access-token:${PUSH_TOKEN}@github.com/phpstan/turbo-ext.git" "$tip:refs/heads/$BRANCH"
else
echo "The split repository is up to date at $target_head."
fi
if [ "$REF_TYPE" != "tag" ]; then
exit 0
fi
# Tag the split commit replayed from the last turbo-ext-touching
# commit at or before the tagged monorepo commit (HEAD on tag runs).
tag_src="$(git rev-list -1 --first-parent HEAD -- "$PREFIX")"
if [ -z "$tag_src" ]; then
echo "::error::No commit touching $PREFIX/ precedes tag $REF_NAME."
exit 1
fi
split_commit="$(git log "$tip" --format='%H%x09%(trailers:key=Split-From,valueonly,separator=)' | awk -F'\t' -v want="$tag_src" '$2 == want {print $1; exit}')"
if [ -z "$split_commit" ]; then
echo "::error::No split commit carries Split-From: $tag_src — was $tag_src replayed?"
exit 1
fi
existing="$(git ls-remote https://github.com/phpstan/turbo-ext.git "refs/tags/$REF_NAME" | cut -f1)"
if [ "$existing" = "$split_commit" ]; then
echo "Tag $REF_NAME already points at $split_commit."
exit 0
fi
if [ -n "$existing" ]; then
echo "::error::Tag $REF_NAME already exists in phpstan/turbo-ext at $existing, expected $split_commit. Delete it there if it must move."
exit 1
fi
echo "Tagging $split_commit (replayed from $tag_src) as $REF_NAME"
git push "https://x-access-token:${PUSH_TOKEN}@github.com/phpstan/turbo-ext.git" "$split_commit:refs/tags/$REF_NAME"