-
Notifications
You must be signed in to change notification settings - Fork 200
289 lines (257 loc) · 11.1 KB
/
Copy pathsemver-release.yml
File metadata and controls
289 lines (257 loc) · 11.1 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
name: SemVer Release
# Biweekly stable releases + manual urgent releases from master
# Normal commits to master only create dev releases (via publish-dev.yml)
on:
schedule:
# Every other Wednesday at 10:00 UTC (biweekly-gate skips odd weeks)
- cron: '0 10 * * 3'
workflow_dispatch:
inputs:
force:
description: 'Force a patch when no fix, feat, or perf commit exists'
required: false
default: 'false'
type: boolean
permissions:
contents: read
# Serialize with every other workflow that pushes to master (sync-tool-docs,
# addon-publish-dev, locale-sync). semantic-release's internal push aborts
# hard when origin/master moves mid-run ("Upstream branch ... has changed") and
# that abort is not retryable from inside the action, so the only safe fix is
# to keep master writers from overlapping. cancel-in-progress stays false: a
# queued release must run, not be cancelled.
concurrency:
group: master-write
cancel-in-progress: false
# queue: max keeps every queued run waiting FIFO. The default single-slot
# queue evicts the OLDER pending run when a new one queues, which could
# silently drop a queued release behind a bot push.
queue: max
env:
PYTHON_VERSION: "3.13"
jobs:
# Skip on odd ISO weeks (biweekly cadence) unless manually triggered
biweekly-gate:
name: Biweekly schedule gate
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.gate.outputs.should_run }}
steps:
- name: Check if this is a release week
id: gate
run: |
if [ "${{ github.event_name }}" != "schedule" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Manual or non-scheduled trigger, proceeding"
exit 0
fi
WEEK=$((10#$(date +%V)))
if [ $((WEEK % 2)) -eq 0 ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Even week ($WEEK), proceeding with release"
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "Odd week ($WEEK), skipping release"
fi
# Check for changes since last release (skip if no changes)
check-changes:
name: Check for releasable changes
needs: biweekly-gate
if: needs.biweekly-gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
fetch-depth: 0
- name: Check for releasable commits since last tag
id: check
run: |
# Get latest non-dev tag
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*dev*' 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
echo "No previous release found, proceeding"
echo "has_changes=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "Latest stable tag: $LATEST_TAG"
# Match every commit form that Python Semantic Release can bump.
CHANGES=$(git log $LATEST_TAG..HEAD --oneline -E \
--grep="^(feat|fix|perf|refactor)(\(.+\))?!?:" \
--grep="^[a-z]+(\(.+\))?!:" \
--grep="^BREAKING[ -]CHANGE:" | head -5)
if [ -n "$CHANGES" ] && [ "${{ inputs.force }}" = "true" ]; then
echo "::error::Force pins the next version to patch, but releasable commits exist:"
echo "$CHANGES"
echo "Dispatch again without force so semantic-release selects the correct level."
exit 1
fi
if [ -n "$CHANGES" ] || [ "${{ inputs.force }}" = "true" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changes found:"
echo "$CHANGES"
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No releasable changes since $LATEST_TAG"
fi
# Validate Docker build before committing to a release
validate-docker:
name: Validate Docker Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
submodules: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Build addon image (no push)
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: ./homeassistant-addon/Dockerfile
push: false
platforms: linux/amd64
# Semantic versioning and release creation
semantic-release:
needs: [check-changes, validate-docker]
if: needs.check-changes.outputs.has_changes == 'true'
# This display name is load-bearing: sync-integration-mirror.yml's gate
# matches the triggering run's jobs by this exact name to decide whether a
# release happened. Rename it together with that gate's matcher.
name: Semantic Release
runs-on: ubuntu-latest
outputs:
released: ${{ steps.semantic.outputs.released }}
version: ${{ steps.semantic.outputs.version }}
permissions:
contents: write # Required to push commits and tags
id-token: write # Required for trusted publishing
pull-requests: write # Required to create pull requests
issues: write # Required for GitHub releases
steps:
# Generate GitHub App token with bypass permissions for tag creation
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-contents: write
# Fallback to RELEASE_TOKEN (PAT) or GITHUB_TOKEN if app credentials not configured
continue-on-error: true
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
# Priority: GitHub App token > RELEASE_TOKEN (PAT) > GITHUB_TOKEN
token: ${{ steps.app-token.outputs.token || secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
- name: Python Semantic Release
id: semantic
uses: python-semantic-release/python-semantic-release@39dd2052f2ce8282a5d932c31d58a2ca06d2550e # v10.6.1
with:
# Use GitHub App token with bypass permissions
github_token: ${{ steps.app-token.outputs.token || secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
verbosity: "2"
# Force pins the bump to patch and is rejected above when releasable
# commits exist, so it cannot override a required minor/major bump.
force: ${{ inputs.force && 'patch' || '' }}
# Don't create GitHub release here - we'll create a draft below
vcs_release: "false"
- name: Create draft GitHub release
if: steps.semantic.outputs.released == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.semantic.outputs.version }}"
TAG="v${VERSION}"
# Extract changelog for this version, capped at GitHub's release-body limit
# (an over-long body is a 422 that strands the already-pushed tag).
python3 scripts/extract_release_notes.py --version "$VERSION" --out release_notes.md
if [ ! -s release_notes.md ]; then
# An empty extract means either no "## v${VERSION}" heading or an
# empty section, and the file cannot tell those apart — so warn
# instead of letting a one-line release body pass for a normal run.
echo "::warning::no changelog section extracted for v${VERSION} — releasing with fallback notes"
echo "Release v${VERSION}" > release_notes.md
fi
# Create draft release (build-binary.yml will add binaries and publish)
gh release create "$TAG" \
--title "$TAG" \
--notes-file release_notes.md \
--draft
echo "✓ Created draft release $TAG (waiting for binaries)"
- name: Copy changelog to addon directory
if: steps.semantic.outputs.released == 'true'
run: |
set -e
cp CHANGELOG.md homeassistant-addon/CHANGELOG.md
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add homeassistant-addon/CHANGELOG.md
git diff --staged --quiet || git commit -m "chore(addon): sync changelog for Home Assistant add-on [skip ci]"
# Master can advance between checkout and this push (bot commits from
# other workflows); retry with a rebase instead of failing the release.
# Mirrors the loop in sync-tool-docs.yml.
for attempt in 1 2 3 4 5; do
if git push; then
echo "Synced addon changelog (attempt ${attempt})."
exit 0
fi
echo "Push rejected on attempt ${attempt}; rebasing onto origin/master and retrying."
# Abort a conflicted rebase so later attempts fail for the real reason.
git pull --rebase origin master || {
git rebase --abort 2>/dev/null || true
echo "::error::Rebase onto origin/master failed; aborting retries."
exit 1
}
done
echo "::error::Could not push addon changelog sync after 5 attempts."
exit 1
- name: Update stable git tag
if: steps.semantic.outputs.released == 'true'
run: |
# Move the 'stable' tag to point to the new release
VERSION="${{ steps.semantic.outputs.version }}"
echo "Updating 'stable' tag to point to v$VERSION"
# Delete existing stable tag (local and remote)
git tag -d stable 2>/dev/null || true
git push origin :refs/tags/stable 2>/dev/null || true
# Create new stable tag pointing to the release commit
git tag stable "v$VERSION"
git push origin stable
echo "✓ 'stable' tag now points to v$VERSION"
# Build binaries and attach to release
build-and-release:
needs: semantic-release
if: needs.semantic-release.outputs.released == 'true'
uses: ./.github/workflows/_build-and-release.yml
with:
release_tag: v${{ needs.semantic-release.outputs.version }}
is_prerelease: false
permissions:
contents: write
# Build and push addon Docker images
build-addon:
needs: semantic-release
if: needs.semantic-release.outputs.released == 'true'
uses: ./.github/workflows/addon-publish.yml
with:
version: ${{ needs.semantic-release.outputs.version }}
permissions:
contents: read
packages: write
# Update addon config.yaml after images are published
update-addon-config:
needs: [semantic-release, build-addon]
if: needs.semantic-release.outputs.released == 'true'
uses: ./.github/workflows/_update-addon-config.yml
with:
version: ${{ needs.semantic-release.outputs.version }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
permissions:
contents: write