Repository navigation
200 lines (193 loc) · 11 KB
/
Copy pathgate.yml
File metadata and controls
200 lines (193 loc) · 11 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
# SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
name: gate
on:
push:
branches: ["**"]
pull_request:
# `edited` so the pull-request body is re-read when it changes after the last push -- the
# case the pr-body job exists for; the matrix legs skip that event (the tree did not move).
types: [opened, synchronize, reopened, edited]
# THE MATRIX: the floor pair, the pinned pair, the head pair. The floor leg is what turns
# "beam_mcp supports OTP 27" (the compile-time floor in mix.exs) from a sentence into a
# measurement; until it ran, the floor was a claim about a release nobody here had run. The
# pinned leg is `.tool-versions`' line, and it is the only leg on which the gate's format step
# measures anything -- `mix format` differs between Elixir minors, so the formatter is one
# program and the other legs' format line reads NOT MEASURED by design (tools/gate.sh). The
# head leg is the newest pair the compatibility table lists. Each pair is one the table lists
# as supported (Elixir 1.17 -> OTP 25-27; 1.18 -> 25-27, with 1.18.5 running on 28 as
# CONVENTIONS records; 1.20 -> 27-29). What setup-beam installs for a pair on a given runner
# image is observed, not assumed: the first run's log is the evidence, and the slice record
# quotes it. `fail-fast: false` so one leg's red does not hide another's measurement.
#
# THE BOX LEG: the pair a fielded edge box runs (OTP 28 at a patched release, Elixir 1.20), on
# the architecture it runs on (aarch64, GitHub's arm64 runner, free for public repositories),
# over the OpenSSL line the box's image pins (3.5 or later): Ubuntu 26.04's runner ships 3.5.5,
# and builds.hex.pm's OTP for it links that, where 24.04's links 3.0.13. Every runner is named
# by its image, never `-latest`, so an image moves only in a commit (governance_test.exs).
# Its versions are exact (`version-type: strict`), not "the newest 28": the box pins its
# runtime, so the leg does too. Every leg's gate runs the OTP floor step (tools/otp_floor.sh),
# so a resolution below a line's patched floor is a red leg, not a green one on a vulnerable
# runtime.
#
# THE REQUIRED CHECK IS THE SUMMARY JOB. The repository's ruleset on main requires the status
# context "Gate - quality gate" by name; a matrix leg's context carries its values, so no leg
# could satisfy it and a pull request would wait on a check that never reports. `summary`
# carries that exact name, runs after every leg (`if: always()`), and fails unless the legs
# all succeeded -- so the ruleset stays as it is and the required check means "every leg".
# The token does nothing here but read: the legs read the tree, the summary reads check runs.
# (What the Scorecard's Token-Permissions check asks for, and what the jobs need anyway.)
permissions:
contents: read
jobs:
gate:
name: Gate - leg (OTP ${{ matrix.otp }}, Elixir ${{ matrix.elixir }}, ${{ matrix.leg }})
runs-on: ${{ matrix.os }}
if: github.event_name != 'pull_request' || github.event.action != 'edited'
strategy:
fail-fast: false
matrix:
include:
- { leg: floor, otp: "27", elixir: "1.17", os: ubuntu-24.04, version-type: loose }
- { leg: pinned, otp: "28", elixir: "1.18", os: ubuntu-24.04, version-type: loose }
- { leg: head, otp: "29", elixir: "1.20", os: ubuntu-24.04, version-type: loose }
- { leg: box, otp: "28.5.0.8", elixir: "1.20.4", os: ubuntu-26.04-arm, version-type: strict }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
id: beam
with:
otp-version: ${{ matrix.otp }}
elixir-version: ${{ matrix.elixir }}
version-type: ${{ matrix.version-type }}
# The OpenSSL `:crypto` linked is part of the pair: builds.hex.pm's OTP links the runner
# image's own, so the image decides it (3.0.13 on 24.04, 3.5.5 on 26.04), and the log says it.
- name: the pair setup-beam installed (observed, quoted in the slice record)
run: |
erl -noshell -eval 'io:format("OTP ~s~n", [erlang:system_info(otp_release)]), halt().'
erl -noshell -eval 'ok = application:load(crypto), [{_, _, V}] = crypto:info_lib(), io:format("crypto linked to ~s~n", [V]), halt().'
elixir --version
cat .tool-versions
- run: mix deps.get
# Dialyzer's PLT, keyed by the EXACT versions setup-beam installed (not the pair asked
# for: a runner image's OTP patch bump carries a dialyzer bump, and a PLT from another
# dialyzer is refused -- with a cache that never overwrites a key, the major alone made
# a permanent red) and the lock: built cold once per key (87-116 s measured), restored after.
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: _build/dialyzer
key: dialyzer-${{ runner.arch }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
# The leg says what it claims to be, so the gate can hold it to it: on the pinned leg a
# format step that cannot measure (the running Elixir is not `.tool-versions`' line) is a
# FAIL, not NOT MEASURED -- otherwise a pin moved in `.tool-versions` and not here would
# leave every leg unmeasured and the summary green (a review lane's finding).
# GATE_BENCH=record: on a shared runner the bench's figures are recorded beside the
# local thresholds, never enforced -- the draw overlaps the ceiling there (tools/gate.sh).
# GATE_AUDIT=require: the dependency audit must reach the registry here; an answer from
# hex's cache is a failure in CI, where a stale pass would be taken for a fresh one.
- run: GATE_LEG=${{ matrix.leg }} GATE_BENCH=record GATE_AUDIT=require ./tools/gate.sh
# The pull-request body, read from the event and held to the same terms as the commit
# messages (tools/text_terms.sh) -- the one public surface a person writes last, which no
# offline step can read. The body goes through an environment variable, never inline in the
# script, so its contents cannot be run as a command.
pr-body:
name: Gate - pull-request body
runs-on: ubuntu-24.04
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: the body names no trailer, session, board id or consumer
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -uo pipefail
printf '%s\n' "$PR_BODY" > body.txt
echo "body: $(wc -l < body.txt) line(s)"
if hits=$(bash tools/text_terms.sh < body.txt); then
echo "pass: the body names no trailer, session, board id or consumer"
else
echo "FAIL: the body carries a term the gate refuses in commit messages:"
printf '%s\n' "$hits" | sed 's/^/ /'
exit 1
fi
summary:
name: Gate - quality gate
runs-on: ubuntu-24.04
needs: [gate, pr-body]
if: always()
# The edited path may wait up to 20 minutes for running legs (tools/ci_legs_verdict.sh's
# bound); the job's own ceiling sits just above it so a hang is a FAIL, not a stall.
timeout-minutes: 25
# The edited path reads the legs' check runs for the head SHA: a read, granted here and
# nowhere wider.
permissions:
contents: read
checks: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: every leg of the matrix passed, and the pull-request body where there is one
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -uo pipefail
echo "event: ${{ github.event_name }}/${{ github.event.action }}; legs: ${{ needs.gate.result }}; pr-body: ${{ needs.pr-body.result }}"
if [ "${{ github.event.action }}" = "edited" ]; then
# On a body edit the legs do not run (the tree did not move), but this job carries
# the REQUIRED context, and a fresh green under that name on a commit whose legs
# this run never read would let a title edit stand a green beside a red (a review
# lane's finding). So the legs' verdicts for this head SHA are read from the checks
# API -- the latest run of each leg name -- and all four must be success. The
# read WAITS for legs still running (G-079: an edit during the synchronize run read
# null conclusions, failed, and the ruleset blocked on that red beside the later
# green); tools/ci_legs_verdict.sh polls to a bound, and its probe runs offline.
[ "${{ needs.pr-body.result }}" = "success" ] || exit 1
bash tools/ci_legs_verdict.sh "${GITHUB_REPOSITORY}" "${HEAD_SHA}"
else
[ "${{ needs.gate.result }}" = "success" ]
case "${{ needs.pr-body.result }}" in success|skipped) ;; *) exit 1 ;; esac
fi
dco:
name: Gate - DCO sign-off
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: every commit in the range carries a Signed-off-by
run: |
set -uo pipefail
before="${{ github.event.before }}"
if [ "${{ github.event_name }}" = "pull_request" ]; then
range="origin/${{ github.base_ref }}..HEAD"
git rev-list --no-merges "$range" > /tmp/commits \
|| { echo "cannot resolve $range"; exit 1; }
elif [ -z "$before" ] \
|| [ "$before" = "0000000000000000000000000000000000000000" ] \
|| ! git rev-parse --verify --quiet "$before^{commit}" >/dev/null; then
# Two cases, one fallback. A branch's first push has no previous state, so
# github.event.before is the all-zeros SHA. A force-push names a real-looking SHA
# that is no longer reachable -- dropping a commit leaves `before` pointing at
# something the fetched history does not contain. Testing the SHA's SHAPE catches
# only the first; asking git whether it RESOLVES catches both.
echo "before is unusable ('$before'): checking every commit reachable from HEAD"
git rev-list --no-merges HEAD > /tmp/commits \
|| { echo "cannot list commits from HEAD"; exit 1; }
else
range="$before..HEAD"
git rev-list --no-merges "$range" > /tmp/commits \
|| { echo "cannot resolve $range"; exit 1; }
fi
echo "commits in scope: $(wc -l < /tmp/commits)"
fail=0
while read -r sha; do
[ -z "$sha" ] && continue
if git log -1 --format='%B' "$sha" | grep -q '^Signed-off-by: '; then
echo " ok $sha"
else
echo " FAIL $sha carries no Signed-off-by"; fail=1
fi
done < /tmp/commits
exit $fail