-
Notifications
You must be signed in to change notification settings - Fork 9
239 lines (216 loc) · 11.6 KB
/
Copy pathpublish-release.yml
File metadata and controls
239 lines (216 loc) · 11.6 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
name: Publish project release action
# Triggered-Docker publisher, one run = one branch (the trigger ref). The weekly schedule rebuilds `main`
# only (full product matrix + shared base refresh for CVEs + a versioned GitHub release). A path-scoped push
# publishes `main` immediately when codegen commits a new `Make/Matrix.json` pin, so new upstream Nx product
# versions ship at once. A manual dispatch publishes the branch it is started from (main => latest/stable,
# develop => :develop). Building only the trigger branch keeps github.ref aligned with the branch being
# versioned, so NBGV classifies it natively with no cross-branch ref leak.
#
# Ordinary code merges do NOT publish: only a matrix pin change committed by the codegen App (push, branch-
# filtered to main, gated to the App identity so a human pin edit does not publish) or a schedule/dispatch does.
# develop's daily codegen pin update is sync-only (the push trigger is main-only).
# Refresh :develop by dispatching from develop. CI/validation runs separately on push (test-pull-request).
#
# The publish/stable decision is computed once by the plan job (publish-plan-task.yml). Every job gates on its
# outputs instead of re-testing event/actor/branch.
on:
workflow_dispatch:
schedule:
# Weekly rebuild/publish of main (Mon 02:00 UTC) to pick up shared base-image updates.
- cron: '0 2 * * MON'
# Publish main immediately when the codegen matrix changes. The daily codegen commits the new pin here.
push:
branches: [main]
paths: [Make/Matrix.json]
# Single global group (not ref-scoped): this workflow pushes shared tags (base images, product images,
# readme), so a schedule + a dispatch + a pin push (or back-to-back dispatches) must not race on those
# shared tags. cancel-in-progress: false queues the next run instead of cancelling an in-flight publish,
# which could otherwise leave a partially pushed tag set.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
# Single source of the release-gate decision (publish? stable?) - every job below gates on its outputs
# instead of re-testing event/actor/branch. See publish-plan-task.yml for the policy.
plan:
name: Plan release job
uses: ./.github/workflows/publish-plan-task.yml
with:
event_name: ${{ github.event_name }}
actor: ${{ github.actor }}
ref_name: ${{ github.ref_name }}
# Compute the version once from the trigger branch and thread it to every consumer. github.ref already
# equals the built branch (one branch per run), so NBGV classifies natively - main publishes clean, a
# develop dispatch publishes a prerelease.
get-version:
name: Get version information job
# Only the long-lived branches publish. A stray dispatch from a feature branch is a no-op.
needs: [plan]
if: ${{ needs.plan.outputs.publish == 'true' }}
uses: ./.github/workflows/get-version-task.yml
secrets: inherit
with:
ref: ${{ github.ref_name }}
# Base images share a single tag (nx-base:ubuntu-noble) across branches. The main schedule/push/dispatch
# builds and refreshes it. A develop dispatch sets build_base: false (see build-docker below) and reuses
# main's published base so it can't overwrite the shared tag.
build-base:
name: Build base image job
needs: [plan]
if: ${{ needs.plan.outputs.publish == 'true' && needs.plan.outputs.stable == 'true' }}
uses: ./.github/workflows/build-base-images-task.yml
secrets: inherit
with:
push: true
# The shared base is branch-agnostic and its branch-scoped buildcache tag (`buildcache-<ref>`) keys on this
# ref, so it uses the branch ref, not the versioned commit: the base is not part of the per-commit versioned
# product set, and a commit SHA here would split the cache into per-commit tags.
ref: ${{ github.ref_name }}
# Validate the published tree (lint + dotnet test) before building, so a publish can never ship a tree that
# would fail the same gate CI enforces on push. Pinned to the versioned commit on main so it validates the
# exact commit the images and release are built from.
validate:
name: Validate job
needs: [plan, get-version]
if: ${{ needs.plan.outputs.publish == 'true' }}
uses: ./.github/workflows/validate-task.yml
secrets: inherit
with:
ref: ${{ github.ref_name == 'main' && needs.get-version.outputs.GitCommitId || github.ref_name }}
build-docker:
name: Build Docker image job
needs: [plan, get-version, build-base, validate]
# always() so the job still runs when build-base is intentionally skipped on a develop dispatch. Fail only
# if a prerequisite actually failed.
if: >-
${{ always()
&& needs.plan.outputs.publish == 'true'
&& needs.get-version.result == 'success'
&& needs.validate.result == 'success'
&& (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') }}
uses: ./.github/workflows/build-docker-task.yml
secrets: inherit
with:
push: true
branch: ${{ github.ref_name }}
# Pin main to the exact commit get-version versioned, not the moving `main` ref, so the image's embedded
# version matches the GitHub release tag even if main advances mid-run. develop has no versioned release,
# so the moving ref is fine.
ref: ${{ github.ref_name == 'main' && needs.get-version.outputs.GitCommitId || github.ref_name }}
# The publisher's own build-base job already built the shared base on the main run (and a develop
# dispatch reuses main's published base), so the build task never rebuilds it - this avoids a double
# base build and keeps the branch-agnostic nx-base tag owned by the main run.
build_base: false
# Single NBGV classification threaded down (no nested re-run in the build task).
semver2: ${{ needs.get-version.outputs.SemVer2 }}
# main-only: a develop dispatch publishes images + the :develop tag but cuts no versioned GitHub release.
github-release:
name: Publish GitHub release job
needs: [plan, get-version, build-docker]
if: ${{ needs.plan.outputs.publish == 'true' && needs.plan.outputs.stable == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# Check out the exact built commit so the uploaded release files match the tag even if main advances mid-run.
- name: Checkout code step
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}
# Backstop (main only): a public release must not carry a prerelease '-', guarding against NBGV mis-versioning the
# public ref into a malformed "Latest" release. Strip '+buildmetadata' first - a '-' there is legitimate. Only a
# '-' in the core/prerelease segment marks a prerelease.
- name: Verify public release version step
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi
# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
# step when a release for this tag already exists to avoid a no-op republish.
- name: Check for existing release step
id: release-exists
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "Release $TAG already exists; workflow_dispatch will refresh it."
else
echo "Release $TAG already exists; skipping release creation (no-op republish)."
fi
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
# `target_commitish` must be set explicitly: otherwise GitHub's REST API tags the release on the default branch.
# Pin it to `GitCommitId` so the tag is on the exact built commit, consistent with the SemVer2 tag and artifacts.
# Skip when the release already exists, but always let a manual `workflow_dispatch` through to refresh it.
# Every release is a tag on the built commit plus the auto-attached source zip, README, and LICENSE. This is a
# Docker-only repo: it ships no `release-asset-*` binaries/packages, so the release carries no extra files and
# `fail_on_unmatched_files` is omitted (see Template adaptations in AGENTS.md). The image is the published artifact
# on Docker Hub.
- name: Create GitHub release step
if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }}
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
generate_release_notes: true
tag_name: ${{ needs.get-version.outputs.SemVer2 }}
target_commitish: ${{ needs.get-version.outputs.GitCommitId }}
# This run's release represents the main publish.
prerelease: false
files: |
LICENSE
README.md
# main-only: resolve the Docker Hub repository list (Docker Hub does not read the GitHub README). The image
# set is repo-specific, so derive it from main's Matrix.json (lowercased ptr727/<image> plus the shared base
# repos) for the readme matrix below.
docker-readme-repos:
name: Get docker hub readme repositories job
needs: [plan, get-version, build-docker]
if: ${{ needs.plan.outputs.publish == 'true' && needs.plan.outputs.stable == 'true' }}
runs-on: ubuntu-latest
outputs:
repositories: ${{ steps.list.outputs.repositories }}
steps:
- name: Checkout code step
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}
- name: Resolve repository list step
id: list
run: |
set -euo pipefail
# shellcheck disable=SC2016 # the jq program is a literal, not a shell expansion
REPOS=$(jq --compact-output \
'[.Images[].Name | ascii_downcase | "ptr727/\(.)"] + ["ptr727/nx-base","ptr727/nx-base-lsio"] | sort | unique' \
./Make/Matrix.json)
echo "repositories=$REPOS" >> "$GITHUB_OUTPUT"
# main-only: push the repository overview (README.md) to each Docker Hub repo derived above.
docker-readme:
name: Publish docker hub readme job
needs: [plan, get-version, docker-readme-repos]
if: ${{ needs.plan.outputs.publish == 'true' && needs.plan.outputs.stable == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix:
repository: ${{ fromJSON(needs.docker-readme-repos.outputs.repositories) }}
steps:
- name: Checkout code step
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}
- name: Publish Docker Hub readme step
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
repository: ${{ matrix.repository }}
short-description: ${{ github.event.repository.description }}
readme-filepath: ./README.md