Repository navigation
306 lines (287 loc) · 14.4 KB
/
Copy pathreusable-publish-release.yml
File metadata and controls
306 lines (287 loc) · 14.4 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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# Reusable workflow: publish a release. Any DependencyControl package repo can call this. It copies
# the dev feed's channel onto the chosen published channel(s) of the publish branch (preserving the
# channels it isn't releasing), tags the versions those channels reference, records the release date
# back on the dev branch when a stable channel ships (so `bump-version` can see what has shipped), and
# optionally cuts a GitHub Release per channel. The depctrl CLI is fetched from the DependencyControl
# repo, so callers don't vendor it.
#
# Third-party caller:
# jobs:
# publish:
# permissions: { contents: write }
# uses: TypesettingTools/DependencyControl/.github/workflows/reusable-publish-release.yml@v1
# with: { ref: main, channels: stable, default-channel: stable, release-channels: stable }
# secrets: { app-private-key: ${{ secrets.DEPCTRL_CI_APP_PRIVATE_KEY }} }
# See this repo's release.yml for a working caller.
#
# If the publish branch is protected, GITHUB_TOKEN can't push to it — pass a GitHub App on its bypass
# list via the app-client-id input and app-private-key secret. See the README's "Release Automation"
# section for the one-time App setup.
name: Reusable — publish a release
on:
workflow_call:
inputs:
ref:
description: "Commit or branch to release (its source is tagged; its feed is published)"
type: string
required: true
channels:
description: "Space-separated channels to publish, e.g. 'stable' or 'stable alpha'"
type: string
required: true
default-channel:
description: "Which published channel is marked default: true"
type: string
required: true
source-channel:
description: "The dev feed's channel to copy from"
type: string
default: main
dev-branch:
description: "Development/default branch; the release date is recorded here"
type: string
default: main
publish-branch:
description: "Branch that serves the feed URL"
type: string
default: publish
tag-prefix:
description: "Prefix for the version tags this creates; must match the feed's file-URL tag scheme"
type: string
default: v
stable-channel:
description: "Publishing this channel also records the release date on the dev branch"
type: string
default: stable
release-channels:
description: "Space-separated published channels to also cut a GitHub Release for (empty: none)"
type: string
default: ""
depctrl-ref:
description: "DependencyControl ref providing the depctrl CLI; 'latest' resolves to its newest release"
type: string
default: latest
app-client-id:
description: "Client ID of a GitHub App on the publish branch's bypass list (optional)"
type: string
default: ""
secrets:
app-private-key:
description: "Private key for app-client-id (optional)"
required: false
concurrency:
group: publish-release-${{ github.repository }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
env:
TAG_PREFIX: ${{ inputs.tag-prefix }}
steps:
- name: Generate a token from the release App
id: app-token
if: ${{ inputs.app-client-id != '' }}
uses: actions/create-github-app-token@v3
with:
client-id: ${{ inputs.app-client-id }}
private-key: ${{ secrets.app-private-key }}
- name: Check out the package repo
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0 # full history + all tags: needed to create/verify version tags
token: ${{ steps.app-token.outputs.token || github.token }}
- name: Resolve the DependencyControl CLI ref
id: cli-ref
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
ref="${{ inputs.depctrl-ref }}"
if [ "$ref" = "latest" ]; then
ref="$(gh api repos/TypesettingTools/DependencyControl/releases/latest --jq .tag_name 2>/dev/null || true)"
[ -n "$ref" ] || { echo "::error::Could not resolve the latest DependencyControl release; pin 'depctrl-ref' to a tag or branch."; exit 1; }
echo "Using latest DependencyControl release: $ref"
fi
echo "ref=$ref" >> "$GITHUB_OUTPUT"
- name: Check out the DependencyControl CLI
uses: actions/checkout@v6
with:
repository: TypesettingTools/DependencyControl
ref: ${{ steps.cli-ref.outputs.ref }}
path: _depctrl
- uses: leafo/gh-actions-lua@v13
with:
luaVersion: "luajit-2.1"
luaCompileFlags: "XCFLAGS=-DLUAJIT_ENABLE_LUA52COMPAT"
- uses: luarocks/gh-actions-luarocks@v7
- run: luarocks install moonscript
- run: luarocks install luafilesystem
- run: luarocks install argparse
- run: luarocks install lua-schema
- run: luarocks install lpeg
- name: Pin one release date for the whole run
run: echo "RELEASE_DATE=$(date -u +%Y-%m-%d)" >> "$GITHUB_ENV"
# Sync the dev feed to its source and — only when a stable channel is shipping — stamp the
# release date on the dev branch, so version tooling sees the dev state as released.
- name: Record the release on the dev branch
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
lua _depctrl/depctrl.lua update-feed
case " ${{ inputs.channels }} " in
*" ${{ inputs.stable-channel }} "*)
lua _depctrl/depctrl.lua update-feed --mark-released --release-date "$RELEASE_DATE" ;;
esac
if ! git diff --quiet -- DependencyControl.json; then
git add DependencyControl.json
git commit -m "release: record ${{ inputs.channels }} for ${{ inputs.ref }} [skip ci]"
git push origin "HEAD:${{ inputs.dev-branch }}"
fi
# Define the tag-naming scheme <prefix><version><tagSuffix[channel]> once for all
# the three steps that build tag names (create tags, publish guard, GitHub release).
- name: Define shared release helpers
run: |
set -euo pipefail
cat > "$RUNNER_TEMP/release-lib.sh" <<'SH'
# tag_name <prefix> <version> <feed> <channel>: the git tag a package version resolves to on a
# channel, as <prefix><version><tagSuffix[channel]>.
tag_name() {
local suffix
suffix=$(jq -r --arg c "$4" '(.vars.tagSuffix // {})[$c] // ""' "$3")
printf '%s%s%s\n' "$1" "$2" "$suffix"
}
# emit_tags <prefix> <feed> <version-channel> <suffix-channel>: one tag per distinct version the
# feed advertises on version-channel, each suffixed for suffix-channel. The two channels differ
# when tagging a source channel's versions for a destination channel that will carry them.
emit_tags() {
local v
while IFS= read -r v; do
[ -n "$v" ] || continue
tag_name "$1" "$v" "$2" "$4"
done < <(lua _depctrl/depctrl.lua get-current-versions -f "$2" -c "$3")
}
SH
# The versions come from the source channel; the suffix from each destination channel.
# Create the missing ones at the released commit.
- name: Create the version tags
run: |
set -euo pipefail
source "$RUNNER_TEMP/release-lib.sh"
for chan in ${{ inputs.channels }}; do
emit_tags "$TAG_PREFIX" DependencyControl.json "${{ inputs.source-channel }}" "$chan"
done | sort -u > "$RUNNER_TEMP/tags.txt"
echo "Tags:"; sed 's/^/ /' "$RUNNER_TEMP/tags.txt"
released_sha=$(git rev-parse HEAD)
while IFS= read -r tag; do
[ -n "$tag" ] || continue
if git rev-parse -q --verify "refs/tags/$tag^{commit}" >/dev/null; then
echo "· $tag exists — leaving it"
else
git tag "$tag" "$released_sha"
git push origin "refs/tags/$tag"
fi
done < "$RUNNER_TEMP/tags.txt"
# Copy the source channel onto the published channel(s), preserving the others, then guard that
# every tag the published feed references exists before pushing. On the first release the publish
# branch doesn't exist yet, so seed the worktree from the released commit with an empty feed and
# let the final push create it.
- name: Publish to the publish branch
run: |
set -euo pipefail
if git ls-remote --exit-code --heads origin "${{ inputs.publish-branch }}" >/dev/null 2>&1; then
git fetch origin "${{ inputs.publish-branch }}:refs/remotes/origin/${{ inputs.publish-branch }}"
git worktree add --detach _publish "origin/${{ inputs.publish-branch }}"
else
echo "Publish branch '${{ inputs.publish-branch }}' doesn't exist yet — bootstrapping it from the released commit."
git worktree add --detach _publish HEAD
# That tree holds the dev feed, whose channels releases are cut from, so hand the merge an
# empty feed to fill instead: a dev channel copied in here would keep the `default: true`
# it carries there and go on claiming it alongside every channel published afterwards.
jq '{dependencyControlFeedFormatVersion, macros: {}, modules: {}}' DependencyControl.json > _publish/DependencyControl.json
fi
rsync -a --delete \
--exclude '.git' --exclude '_publish' --exclude '_depctrl' \
--exclude 'ctrf' --exclude 'types' --exclude 'build' \
--exclude '.lua' --exclude '.luarocks' \
--exclude 'DependencyControl.json' \
./ _publish/
lua _depctrl/depctrl.lua merge-feed -f DependencyControl.json --into _publish/DependencyControl.json \
--from "${{ inputs.source-channel }}" --to "${{ inputs.channels }}" \
--default-channel "${{ inputs.default-channel }}" --released "$RELEASE_DATE"
git fetch --tags --force --quiet
source "$RUNNER_TEMP/release-lib.sh"
channels=$(jq -r '[ (.macros // {}), (.modules // {}) | .[] | select(type == "object") | .channels // {} | keys[] ] | unique[]' _publish/DependencyControl.json)
for chan in $channels; do
emit_tags "$TAG_PREFIX" _publish/DependencyControl.json "$chan" "$chan"
done | sort -u > "$RUNNER_TEMP/publish-tags.txt"
while IFS= read -r tag; do
[ -n "$tag" ] || continue
git rev-parse -q --verify "refs/tags/$tag^{commit}" >/dev/null \
|| { echo "Refusing to publish: referenced tag $tag doesn't exist."; exit 1; }
done < "$RUNNER_TEMP/publish-tags.txt"
cd _publish
git add -A
if git diff --cached --quiet; then
echo "publish branch already matches this release; nothing to publish."
else
git commit -m "release: publish ${{ inputs.channels }} for ${{ inputs.ref }} [skip ci]"
git push origin "HEAD:refs/heads/${{ inputs.publish-branch }}"
fi
# The release version is the highest version any package reaches this run. Lockstep versioning
# bumps only the packages that changed, so the namesake module can sit below it (a release that
# only touches a bundled helper). The bundle asset and the GitHub release(s) both key off this one
# value so they can't disagree, and every released channel carries it (the publish step copied the
# source channel onto them).
- name: Resolve the release version
if: ${{ inputs.release-channels != '' }}
run: |
set -euo pipefail
ver=$(lua _depctrl/depctrl.lua get-version -f DependencyControl.json -c "${{ inputs.source-channel }}")
echo "Release version: v$ver"
echo "RELEASE_VERSION=$ver" >> "$GITHUB_ENV"
# The bundler names the archive after the same release version (highest on the default channel),
# so the asset matches the release with no post-rename.
- name: Build the release bundle
id: bundle
if: ${{ inputs.release-channels != '' }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/bundle"
lua _depctrl/depctrl.lua bundle --out-dir "$RUNNER_TEMP/bundle"
zip=$(find "$RUNNER_TEMP/bundle" -maxdepth 1 -name '*.zip' -print -quit)
if [ -n "$zip" ]; then
echo "Bundled $(basename "$zip")"
else
echo "::warning::The bundle produced no archive; the release(s) will carry no asset."
fi
echo "zip=$zip" >> "$GITHUB_OUTPUT"
# Optionally cut a GitHub Release for each requested channel that shipped this run. Notes are
# rendered from the feed's changelog for the released version; a channel other than the stable
# one is marked prerelease. An existing release for a tag is left untouched.
- name: Create GitHub release(s)
if: ${{ inputs.release-channels != '' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
BUNDLE_ZIP: ${{ steps.bundle.outputs.zip }}
run: |
set -euo pipefail
source "$RUNNER_TEMP/release-lib.sh"
notes="$RUNNER_TEMP/release-notes.md"
lua _depctrl/depctrl.lua release-notes --channel "${{ inputs.source-channel }}" --output "$notes" \
|| echo "(no changelog entries for this release)" > "$notes"
[ -s "$notes" ] || echo "(no changelog entries for this release)" > "$notes"
for chan in ${{ inputs.channels }}; do
case " ${{ inputs.release-channels }} " in *" $chan "*) ;; *) continue ;; esac
tag=$(tag_name "$TAG_PREFIX" "$RELEASE_VERSION" _publish/DependencyControl.json "$chan")
prerelease=""; [ "$chan" = "${{ inputs.stable-channel }}" ] || prerelease="--prerelease"
if gh release view "$tag" >/dev/null 2>&1; then
echo "· release $tag already exists — leaving it"
else
gh release create "$tag" --title "$tag" --notes-file "$notes" $prerelease ${BUNDLE_ZIP:+"$BUNDLE_ZIP"}
echo "· created release $tag $prerelease"
fi
done