-
Notifications
You must be signed in to change notification settings - Fork 1
475 lines (458 loc) · 18.5 KB
/
Copy pathrelease.yml
File metadata and controls
475 lines (458 loc) · 18.5 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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
name: Workspace release
on:
workflow_dispatch:
inputs:
release_id:
description: Immutable workspace release ID (workspace-YYYY.MM.DD.N)
required: true
type: string
target_ref:
description: Full commit SHA expected to equal current main
required: true
type: string
include_router_artifact:
description: Build and attach the reviewed Linux router artifact
required: true
default: false
type: boolean
router_distribution_approval:
description: Required approval/evidence reference when attaching the router binary
required: false
type: string
prerelease:
description: Mark the GitHub release as a prerelease
required: true
default: false
type: boolean
permissions:
contents: write
id-token: write
attestations: write
concurrency:
group: workspace-release
cancel-in-progress: false
# Release validation is decomposed into independent jobs that mirror the CI
# workflow, and the AI provider lanes fan out one runner per provider feature,
# so no single `target/` ever holds every package, backend, and provider
# permutation. Debug info and incremental artifacts still dominate whatever a
# lane leaves behind and contribute nothing to a test, Clippy, rustdoc, or
# check lane, so they stay disabled rather than dropping or weakening any lane.
# Failure backtraces lose line detail; the CI workflow keeps `line-tables-only`
# for diagnosis.
env:
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
CARGO_INCREMENTAL: "0"
jobs:
guard:
name: Prove the selected source is current main
runs-on: ubuntu-latest
timeout-minutes: 10
# The protected `release` environment stays on the single entry job. Every
# validation lane and the publication job depend on it, so one human
# approval still gates the whole run before any release lane compiles
# anything, exactly as the single `validate` job required.
environment: release
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 0
- name: Prove the selected source is current main
env:
TARGET_REF: ${{ inputs.target_ref }}
RELEASE_ID: ${{ inputs.release_id }}
INCLUDE_ROUTER: ${{ inputs.include_router_artifact }}
ROUTER_APPROVAL: ${{ inputs.router_distribution_approval }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# An environment name alone does not enforce human approval.
protection=$(gh api "repos/$GITHUB_REPOSITORY/environments/release" \
--jq '.protection_rules | any(.type == "required_reviewers" and any(.reviewers[]; .type == "User" and .reviewer.login == "Dastari"))')
test "$protection" = true
git fetch origin main --tags --force
test "$(git rev-parse HEAD)" = "${TARGET_REF}"
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"
if git show-ref --verify --quiet "refs/tags/${RELEASE_ID}"; then
echo "release tag already exists: ${RELEASE_ID}" >&2
exit 1
fi
if [[ "${INCLUDE_ROUTER}" == "true" && -z "${ROUTER_APPROVAL}" ]]; then
echo "router_distribution_approval is required for a router binary" >&2
exit 1
fi
workspace-integrity:
name: Workspace integrity and release metadata
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
- name: Validate documentation and release metadata
run: |
python3 scripts/check-documentation.py
python3 scripts/generate-workspace-inventory.py --check
python3 scripts/check-release-state.py
scripts/check-release-manifest.sh
python3 scripts/test-router-notices.py
scripts/check-workspace-dependencies.sh
cargo fmt --all -- --check
neutral-packages:
name: Neutral package lanes
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Verify neutral packages
run: |
cargo test -p graphql-orm-operation-catalog -p graphql-orm-ai-tool-profiles --locked
cargo test -p graphql-orm-storage --locked
cargo test -p graphql-orm-backup --features orm-sqlite --locked
cargo clippy -p graphql-orm-operation-catalog -p graphql-orm-ai-tool-profiles -p graphql-orm-storage --all-targets --locked -- -D warnings
cargo clippy -p graphql-orm-backup --all-targets --features orm-sqlite --locked -- -D warnings
RUSTDOCFLAGS="-D warnings -D missing_docs" cargo doc -p graphql-orm-operation-catalog -p graphql-orm-ai-tool-profiles -p graphql-orm-storage --no-deps --locked
RUSTDOCFLAGS="-D warnings" cargo doc -p graphql-orm-backup --features orm-sqlite --no-deps --locked
orm:
name: ORM backend lanes
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Verify ORM backend lanes
run: |
cargo test -p graphql-orm --locked
cargo check -p graphql-orm --no-default-features --features postgres --locked
cargo check -p graphql-orm --no-default-features --features mssql --locked
cargo clippy -p graphql-orm --all-targets --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc -p graphql-orm -p graphql-orm-macros --no-deps --locked
ai-provider-matrix:
name: Enumerate AI provider lanes
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
providers: ${{ steps.providers.outputs.providers }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- id: providers
name: Read the provider lane list from the lane runner
# The provider list stays owned by scripts/check-ai-provider-lanes.sh.
# Reading it here means a provider added to the script cannot be
# silently dropped from the release matrix.
run: |
set -euo pipefail
providers=$(scripts/check-ai-provider-lanes.sh --list)
echo "${providers}"
printf 'providers=%s\n' "${providers}" >> "${GITHUB_OUTPUT}"
ai-provider-lanes:
name: AI provider lane (${{ matrix.provider }})
needs:
- guard
- ai-provider-matrix
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
# Release runs are rare and gate an immutable publication, so report
# every failing provider lane instead of cancelling the siblings.
fail-fast: false
matrix:
provider: ${{ fromJSON(needs.ai-provider-matrix.outputs.providers) }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- name: Reclaim runner disk
# graphql-orm-ai relinks every test binary for the test, Clippy, and
# rustdoc lane of this provider. One provider per runner is what fixes
# the exhaustion; reclaiming the unused preinstalled toolchains keeps
# headroom for the crate that originally filled the disk.
uses: ./.github/actions/reclaim-runner-disk
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.provider }}
- name: Verify the provider lane
env:
PROVIDER: ${{ matrix.provider }}
run: |
scripts/check-ai-provider-lanes.sh test "${PROVIDER}"
scripts/check-ai-provider-lanes.sh clippy "${PROVIDER}"
scripts/check-ai-provider-lanes.sh doc "${PROVIDER}"
ai-graphql-naming:
name: AI GraphQL naming lane
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test -p graphql-orm-ai --features graphql-case-pascal --test graphql_naming --locked
ai-postgres-parity:
name: AI PostgreSQL parity lane
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run ORM parity against a test-owned disposable PostgreSQL container
run: cargo test -p graphql-orm-ai --no-default-features --features postgres,provider-openai --test postgres_parity --locked -- --test-threads=1
alternate-features:
name: Alternate feature lanes
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Verify alternate AI, storage, and backup lanes
run: |
cargo check -p graphql-orm-ai --no-default-features --features postgres --locked
cargo check -p graphql-orm-ai --no-default-features --features mssql --locked
cargo check -p graphql-orm-ai -p graphql-orm --no-default-features --features graphql-orm-ai/sqlite,graphql-orm/mssql --locked
cargo check -p graphql-orm-backup --no-default-features --features local,orm-postgres --locked
cargo check -p graphql-orm-storage --no-default-features --features s3,azure --locked
cargo check -p graphql-orm-storage --no-default-features --features smb --locked
router:
name: Router lanes
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Verify router lanes
run: |
cargo test -p graphql-orm-router-protocol --locked
cargo test -p graphql-orm-router --locked
cargo test -p graphql-orm-router --features auth-agql --locked
cargo clippy -p graphql-orm-router-protocol --all-targets --locked -- -D warnings
cargo clippy -p graphql-orm-router --all-targets --features auth-agql --locked -- -D warnings
RUSTDOCFLAGS="-D warnings -D missing_docs" cargo doc -p graphql-orm-router-protocol --no-deps --locked
RUSTDOCFLAGS="-D warnings" cargo doc -p graphql-orm-router --no-deps --features auth-agql --locked
router-msrv:
name: Router minimum supported Rust version
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.90.0
- uses: Swatinem/rust-cache@v2
- name: Verify router minimum supported Rust version
run: |
cargo test -p graphql-orm-router-protocol --locked
cargo test -p graphql-orm-router --locked
validate:
name: Release validation complete
needs:
- guard
- workspace-integrity
- neutral-packages
- orm
- ai-provider-matrix
- ai-provider-lanes
- ai-graphql-naming
- ai-postgres-parity
- alternate-features
- router
- router-msrv
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require every release validation lane to have succeeded
# A failed or cancelled dependency already skips this job. The explicit
# assertion also fails the release when a lane is skipped, so a future
# condition on a validation job cannot quietly narrow what an immutable
# release was verified against.
env:
LANE_RESULTS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
unsuccessful=$(
jq -r 'to_entries[] | select(.value.result != "success") | "\(.key): \(.value.result)"' \
<<<"${LANE_RESULTS}"
)
if [[ -n "${unsuccessful}" ]]; then
echo "release: validation lanes did not succeed:" >&2
echo "${unsuccessful}" >&2
exit 1
fi
jq -r 'to_entries[] | "release: \(.key) succeeded"' <<<"${LANE_RESULTS}"
release:
name: Publish immutable workspace release
needs:
- guard
- validate
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 0
- name: Restore release tag visibility
env:
TARGET_REF: ${{ inputs.target_ref }}
run: |
set -euo pipefail
# The release identity proof - current main, unused workspace tag,
# router approval - runs once in the guard job before any validation
# lane, exactly as it did before validation was split. This job only
# re-establishes the tag data that the manifest generator and the tag
# step read, and confirms this runner checked out the validated
# commit.
git fetch origin main --tags --force
test "$(git rev-parse HEAD)" = "${TARGET_REF}"
- name: Reclaim runner disk
# Only the optional router artifact compiles anything in this job.
if: inputs.include_router_artifact
uses: ./.github/actions/reclaim-runner-disk
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Generate deterministic release bundle
env:
RELEASE_ID: ${{ inputs.release_id }}
run: |
mkdir -p release-dist
python3 scripts/generate-release-manifest.py \
--release-id "${RELEASE_ID}" \
--ref HEAD \
--check-clean \
--verify-tags \
--output "release-dist/${RELEASE_ID}.json" \
--notes-output "release-dist/${RELEASE_ID}.md"
- name: Build approved router artifact
if: inputs.include_router_artifact
env:
RELEASE_ID: ${{ inputs.release_id }}
ROUTER_APPROVAL: ${{ inputs.router_distribution_approval }}
run: |
set -euo pipefail
test "$(rustc -vV | sed -n 's/^host: //p')" = "x86_64-unknown-linux-gnu"
SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)
export SOURCE_DATE_EPOCH
cargo install cargo-cyclonedx --version 0.5.9 --locked
cargo build -p graphql-orm-router --release --features auth-agql --locked
cargo cyclonedx \
--manifest-path crates/graphql-orm-router/Cargo.toml \
--format json \
--spec-version 1.5 \
--features auth-agql \
--target x86_64-unknown-linux-gnu
install -m 0755 target/release/graphql-orm-router release-dist/graphql-orm-router
cp crates/graphql-orm-router/graphql-orm-router.cdx.json \
release-dist/graphql-orm-router.cdx.json
cargo metadata --format-version 1 --locked \
--filter-platform x86_64-unknown-linux-gnu \
--features graphql-orm-router/auth-agql > release-dist/router-metadata.json
python3 scripts/generate-router-notices.py \
--metadata release-dist/router-metadata.json \
--sbom release-dist/graphql-orm-router.cdx.json \
--output release-dist/router-notices \
--rust-sysroot "$(rustc --print sysroot)"
rm release-dist/router-metadata.json
cp LICENSE release-dist/LICENSE
printf '%s\n' "${ROUTER_APPROVAL}" > release-dist/router-distribution-approval.txt
tar -C release-dist --sort=name --mtime="@${SOURCE_DATE_EPOCH}" \
--owner=0 --group=0 --numeric-owner \
-czf "release-dist/${RELEASE_ID}-graphql-orm-router-x86_64-unknown-linux-gnu.tar.gz" \
graphql-orm-router graphql-orm-router.cdx.json LICENSE \
router-distribution-approval.txt router-notices
rm -r -- release-dist/router-notices
rm release-dist/graphql-orm-router release-dist/graphql-orm-router.cdx.json \
release-dist/LICENSE release-dist/router-distribution-approval.txt
- name: Create checksums
run: |
cd release-dist
sha256sum -- * > SHA256SUMS
- name: Attest release assets
uses: actions/attest-build-provenance@v3
with:
subject-path: release-dist/*
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.release_id }}
path: release-dist/
if-no-files-found: error
retention-days: 30
- name: Create annotated release tags
env:
RELEASE_ID: ${{ inputs.release_id }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
tags_to_push=()
while IFS= read -r package_tag; do
if git show-ref --verify --quiet "refs/tags/${package_tag}"; then
continue
fi
git tag -a "${package_tag}" -m "${package_tag}"
tags_to_push+=("refs/tags/${package_tag}")
done < <(jq -r '.packages[].tag' "release-dist/${RELEASE_ID}.json")
git tag -a "${RELEASE_ID}" -m "${RELEASE_ID}"
tags_to_push+=("refs/tags/${RELEASE_ID}")
git push --atomic origin "${tags_to_push[@]}"
- name: Publish immutable workspace release
env:
RELEASE_ID: ${{ inputs.release_id }}
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ inputs.prerelease }}
run: |
set -euo pipefail
prerelease_flag=()
if [[ "${PRERELEASE}" == "true" ]]; then
prerelease_flag=(--prerelease)
fi
gh release create "${RELEASE_ID}" \
--verify-tag \
--title "${RELEASE_ID}" \
--notes-file "release-dist/${RELEASE_ID}.md" \
"${prerelease_flag[@]}" \
release-dist/*