-
Notifications
You must be signed in to change notification settings - Fork 0
250 lines (228 loc) · 8.99 KB
/
Copy pathpython-react-publish.yml
File metadata and controls
250 lines (228 loc) · 8.99 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
name: Reusable Python+React Publish
# Reusable tag-triggered publish workflow.
# Sequencing: tests → docker → release
# - The shared test suite (backend/frontend/e2e/api-freshness) must pass
# before docker push
# - docker push must succeed before release creation
# This guarantees: a tag never publishes a GitHub release without a matching
# image in GHCR. If the docker push fails, no release is created.
#
# The test matrix is the shared _python-react-tests.yml building block, so
# CI and publish run the identical gates. A skipped optional job (e.g.
# api-freshness when disabled) does not fail the reusable `tests` job, so
# the downstream `needs:` here is a plain dependency — no always() guards.
on:
workflow_call:
inputs:
python-version:
type: string
default: "3.14"
bun-version-file:
type: string
default: ".bun-version"
bun-version:
type: string
default: ""
enable-e2e:
type: boolean
default: true
enable-translations:
type: boolean
default: false
enable-bootstrap-token:
type: boolean
default: false
enable-api-freshness-check:
type: boolean
default: true
runner-os:
type: string
default: ubuntu-latest
security-tripwire-script:
type: string
default: ""
image-name:
type: string
required: true
release-name-prefix:
# E.g. "TideWatch v" → produces "TideWatch v3.9.2"
type: string
required: true
secrets:
github-token:
required: true
permissions:
contents: read
jobs:
tests:
# backend + frontend + e2e + api-freshness, shared with CI.
# Full owner/repo path pinned to this release's own tag — a `./` local path
# resolves against the CONSUMER repo when called cross-repo (0s
# startup_failure). Bump the @ref on every release.
uses: homelabforge/shared-workflows/.github/workflows/_python-react-tests.yml@v1.4.3
with:
python-version: ${{ inputs.python-version }}
bun-version-file: ${{ inputs.bun-version-file }}
bun-version: ${{ inputs.bun-version }}
enable-e2e: ${{ inputs.enable-e2e }}
enable-translations: ${{ inputs.enable-translations }}
enable-bootstrap-token: ${{ inputs.enable-bootstrap-token }}
enable-api-freshness-check: ${{ inputs.enable-api-freshness-check }}
runner-os: ${{ inputs.runner-os }}
security-tripwire-script: ${{ inputs.security-tripwire-script }}
docker:
name: Build and Push Docker Image
runs-on: ${{ inputs.runner-os }}
timeout-minutes: 30
# Plain dependency: the reusable `tests` job succeeds even when an
# optional inner job is skipped, so docker runs iff every gate that
# actually ran passed. No always()/result juggling needed.
needs: [tests]
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.github-token }}
- name: Extract version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f1-2)
# Standard semver pre-release: anything with `-` after patch
# (e.g. v2.27.0-rc1, v2.27.0-beta.1, v3.0.0-alpha).
# Pre-release tags publish only the exact :VERSION image — they
# must NOT move :latest, :MAJOR, or :MINOR away from stable.
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
IS_PRERELEASE=false
else
IS_PRERELEASE=true
fi
{
echo "version=$VERSION"
echo "major=$MAJOR"
echo "minor=$MINOR"
echo "is_prerelease=$IS_PRERELEASE"
} >> "$GITHUB_OUTPUT"
- name: Read bun version
id: bun
env:
BUN_VERSION_FILE: ${{ inputs.bun-version-file }}
run: echo "version=$(cat "$BUN_VERSION_FILE")" >> "$GITHUB_OUTPUT"
- name: Compute Docker tags
id: tags
run: |
# Always publish the exact :VERSION tag.
TAGS="ghcr.io/${{ inputs.image-name }}:${{ steps.version.outputs.version }}"
# Stable tags only update on non-pre-release builds. Pre-release
# users (rc/beta/alpha) pin to the exact version; production
# users on :latest stay on the last stable.
if [[ "${{ steps.version.outputs.is_prerelease }}" == "false" ]]; then
TAGS="$TAGS"$'\n'"ghcr.io/${{ inputs.image-name }}:latest"
TAGS="$TAGS"$'\n'"ghcr.io/${{ inputs.image-name }}:${{ steps.version.outputs.minor }}"
TAGS="$TAGS"$'\n'"ghcr.io/${{ inputs.image-name }}:${{ steps.version.outputs.major }}"
fi
{
echo "tags<<EOF"
echo "$TAGS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push Docker image
id: push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
tags: ${{ steps.tags.outputs.tags }}
build-args: |
BUN_VERSION=${{ steps.bun.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: Generate artifact attestation
id: attest
if: github.event.repository.visibility == 'public'
continue-on-error: true
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-name: ghcr.io/${{ inputs.image-name }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
# The attestation is best-effort (continue-on-error) so a provenance
# hiccup never blocks a release — but a silent skip means an image
# shipped WITHOUT provenance and nobody noticed. Surface it loudly.
- name: Warn if attestation failed
if: ${{ github.event.repository.visibility == 'public' && steps.attest.outcome == 'failure' }}
run: echo "::warning title=Attestation failed::Build provenance attestation did not succeed; the image was pushed WITHOUT a provenance attestation."
release:
name: Create GitHub Release
runs-on: ${{ inputs.runner-os }}
timeout-minutes: 10
needs: [docker]
permissions:
# contents: write to create the GitHub Release. No packages scope:
# this job only reads CHANGELOG.md and calls the releases API.
contents: write
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Extract version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Extract changelog for this version
id: changelog
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
CHANGELOG=$(awk -v ver="$VERSION" '
/^## \[.*\]/ {
if (found) exit
if (index($0, "[" ver "]")) found=1
next
}
found { print }
' CHANGELOG.md)
# Refuse to publish a release whose notes are empty: a missing
# CHANGELOG section is almost always a forgotten changelog edit,
# not an intentional silent release.
if [ -z "$(printf '%s' "$CHANGELOG" | tr -d '[:space:]')" ]; then
echo "::error title=Missing changelog::No '## [$VERSION]' section found in CHANGELOG.md; refusing to create a release with empty notes."
exit 1
fi
printf '%s\n' "$CHANGELOG" > release_notes.md
- name: Check if pre-release
id: prerelease
run: |
VERSION=${{ steps.version.outputs.version }}
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
env:
GITHUB_TOKEN: ${{ secrets.github-token }}
with:
name: ${{ inputs.release-name-prefix }}${{ steps.version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: ${{ steps.prerelease.outputs.prerelease }}
generate_release_notes: true