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
# The release automation and the crates.io trusted publisher for GitLab CI.
#
# Both jobs run on every push to the trunk, and to a RK_LINE_PREFIX* line when a
# project keeps older lines. release-plz maintains the one release merge
# request against the pushed branch, and merging it is the release: the bump
# push is what makes the release job tag the commit, create the GitLab
# release, and publish to crates.io over OIDC. Only the bot's own bump
# releases, which release_always = false recognizes by its release-plz-*
# source branch.
#
# Every job authenticates with RELEASE_BOT_TOKEN, the project access token
# the setup stores as a masked CI variable: a push made with the default CI
# job token starts no pipeline, which would silently skip the release half,
# and a merge armed under it would land a bump that releases nothing.
# Trusted publishing covers GitLab.com only; a self-hosted instance cannot
# take the OIDC path and falls back to a token, which the method's
# invariants exist to remove.
include:
- local: .gitlab/ci/mr-title.yml
stages:
- release
- test
# Merge request pipelines run the title gate; branch pipelines on the trunk
# and the release lines run the release jobs, whose own rules never match a
# merge request event.
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PROJECT_ROOT_NAMESPACE == "OWNER" && ($CI_COMMIT_BRANCH == "RK_TRUNK_BRANCH" || $CI_COMMIT_BRANCH =~ /^RK_LINE_PREFIX_RE/)'
# The project's own jobs, in a pipeline of their own. This forge
# reverse-deep-merges an include, so a file the project owns would
# contribute every key the jobs above leave out, whatever the include order;
# a child pipeline is the only shape that isolates. Declare jobs in
# .gitlab/ci/project.yml and they run here, with their own stages and their
# own nested includes, reaching nothing this file declares. strategy: mirror
# carries the child's status into this pipeline, so a failing project job
# blocks the merge; it needs GitLab 18.2 or newer, which is this
# convention's minimum version. The exists: guard keeps this job out of the
# pipeline altogether while the file is absent, so no empty downstream
# pipeline appears. A job in that file needs no merge_request_event rule:
# this rule already scopes it, and inside a child pipeline
# CI_PIPELINE_SOURCE reads parent_pipeline; use CI_MERGE_REQUEST_ID where a
# rule is wanted. The forge document carries the rest.
project-jobs:
stage: test
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
exists:
- .gitlab/ci/project.yml
trigger:
include:
- local: .gitlab/ci/project.yml
strategy: mirror
.release-plz:
stage: release
image: rust:1
variables:
GIT_DEPTH: "0"
before_script:
- curl -fsSL "https://github.com/release-plz/release-plz/releases/download/release-plz-v0.3.160/release-plz-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin
# The release request: the version bump and the changelog, as one merge
# request against the pushed branch. Force-push refresh is unavailable on
# this forge, so release-plz closes an outdated request and opens a fresh one
# when new work lands; the changelog correction window is correspondingly
# narrower.
release-request:
extends: .release-plz
rules:
- if: '$CI_COMMIT_BRANCH == "RK_TRUNK_BRANCH" || $CI_COMMIT_BRANCH =~ /^RK_LINE_PREFIX_RE/'
resource_group: release-request
variables:
RELEASE_STYLE: RK_STYLE
script:
- release-plz release-pr --forge gitlab --git-token "$RELEASE_BOT_TOKEN"
- |
set -eu
# The trunk style's standing arm. It dies with the request it was made
# on — this forge refreshes by closing the outdated request and opening
# a fresh one — so it is re-applied on every run, and it authenticates
# as the bot for the same reason every job here does: a merge under the
# job token would land a bump that releases nothing. The bot's own
# authorship is the filter, so no other open request can be armed by
# mistake, and an arm the forge cannot take yet — the request's first
# pipeline not started — waits for the next push rather than failing
# the job; the lines style merges by hand and arms nothing.
if [ "$RELEASE_STYLE" != "trunk" ]; then
echo "the lines style merges each release by hand; not arming."
exit 0
fi
if [ "$CI_COMMIT_BRANCH" != "RK_TRUNK_BRANCH" ]; then
echo "a line's request is never armed; its candidate is what a human validated."
exit 0
fi
api() { curl -fsS --header "PRIVATE-TOKEN: $RELEASE_BOT_TOKEN" "$@"; }
project="$CI_API_V4_URL/projects/$CI_PROJECT_ID"
bot="$(api "$CI_API_V4_URL/user" | grep -o '"username":"[^"]*"' | head -n 1 | cut -d '"' -f 4)"
iid="$(api "$project/merge_requests?state=opened&target_branch=$CI_COMMIT_BRANCH&author_username=$bot&per_page=1" | grep -o '"iid":[0-9]*' | head -n 1 | cut -d : -f 2)"
if [ -z "$iid" ]; then
echo "no release request to arm."
exit 0
fi
if api -X PUT "$project/merge_requests/$iid/merge?auto_merge=true&merge_when_pipeline_succeeds=true&squash=true&should_remove_source_branch=true" >/dev/null 2>&1; then
echo "armed merge request !$iid."
else
echo "could not arm !$iid yet — its pipeline has not started; the next trunk push re-arms."
fi
# The release itself: tag, GitLab release, and the crates.io publish over
# OIDC. The job's ID token is exchanged for a short-lived registry token;
# nothing long-lived exists on the publish path.
release:
extends: .release-plz
rules:
- if: '$CI_COMMIT_BRANCH == "RK_TRUNK_BRANCH" || $CI_COMMIT_BRANCH =~ /^RK_LINE_PREFIX_RE/'
resource_group: release
id_tokens:
CRATES_IO_ID_TOKEN:
aud: crates.io
script:
- |
set -eu
response="$(curl -fsS -X PUT "https://crates.io/api/v1/trusted_publishing/tokens" \
-H 'Content-Type: application/json' \
-d "{\"jwt\":\"$CRATES_IO_ID_TOKEN\"}")"
CARGO_REGISTRY_TOKEN="$(printf '%s' "$response" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
export CARGO_REGISTRY_TOKEN
release-plz release --forge gitlab --git-token "$RELEASE_BOT_TOKEN"
curl -fsS -X DELETE "https://crates.io/api/v1/trusted_publishing/tokens" \
-H "Authorization: Bearer $CARGO_REGISTRY_TOKEN" || true