Repository navigation
326 lines (318 loc) · 15.1 KB
/
Copy pathnative-release.yml
File metadata and controls
326 lines (318 loc) · 15.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
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
name: native 0.x release
on:
workflow_dispatch:
inputs:
channel:
description: "Publish a native beta, or promote a tested beta commit to stable"
required: true
type: choice
options: [beta, stable]
tag:
description: "Exact v0.X.Y-beta.N or v0.X.Y tag"
required: true
type: string
source_beta:
description: "Published and site-tested beta tag (stable only)"
required: false
type: string
dry_run:
description: "Verify and build the packages without creating a tag or release"
required: true
default: false
type: boolean
permissions:
contents: write
concurrency:
group: native-release-${{ inputs.tag }}
cancel-in-progress: false
jobs:
publish:
name: verify, package and publish native ${{ inputs.channel }}
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
newly_published: ${{ steps.publication.outputs.newly_published }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: go/go.mod
cache: false
- name: Bind the candidate to its source
env:
CHANNEL: ${{ inputs.channel }}
TAG: ${{ inputs.tag }}
SOURCE_BETA: ${{ inputs.source_beta }}
GH_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
test "${GITHUB_REF}" = refs/heads/master
test -n "${GH_TOKEN}"
[[ "${CHANNEL}" = beta || "${CHANNEL}" = stable ]]
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
> "${RUNNER_TEMP}/native-release-pages.json"
python3 scripts/check-native-release-order.py "${TAG}" \
"${RUNNER_TEMP}/native-release-pages.json"
# Stable checks out an older beta's source below, which may lack
# this checker; keep the current one.
cp scripts/check-native-schema.py "${RUNNER_TEMP}/check-native-schema.py"
cp scripts/write-native-release-notes.py "${RUNNER_TEMP}/write-native-release-notes.py"
if [ "${CHANNEL}" = stable ]; then
if [[ ! "${TAG}" =~ ^v0\.[0-9]+\.[0-9]+$ ]] ||
[[ ! "${SOURCE_BETA}" =~ ^v0\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]] ||
[[ "${SOURCE_BETA}" != "${TAG}-beta."* ]]; then
echo "stable needs a v0.X.Y tag and matching beta" >&2
exit 1
fi
git fetch origin "refs/tags/${SOURCE_BETA}:refs/tags/${SOURCE_BETA}"
beta_commit=$(git rev-list -n 1 "${SOURCE_BETA}")
mkdir -p "${RUNNER_TEMP}/beta-receipt"
beta_json=$(gh release view "${SOURCE_BETA}" --repo "${GITHUB_REPOSITORY}" \
--json tagName,isDraft,isPrerelease,assets)
jq -e --arg tag "${SOURCE_BETA}" '
.tagName == $tag and .isDraft == false and .isPrerelease == true and
([.assets[].name] | index("ftw-native-release.json") != null) and
([.assets[].name] | index("ftw-linux-amd64.tar.gz") != null) and
([.assets[].name] | index("ftw-linux-arm64.tar.gz") != null)
' <<<"${beta_json}" >/dev/null
gh release download "${SOURCE_BETA}" --repo "${GITHUB_REPOSITORY}" \
--pattern ftw-native-release.json --dir "${RUNNER_TEMP}/beta-receipt"
jq -e --arg tag "${SOURCE_BETA}" --arg commit "${beta_commit}" '
.schema == 1 and .channel == "beta" and .tag == $tag and .commit == $commit and
(.sha256.amd64 | test("^[0-9a-f]{64}$")) and
(.sha256.arm64 | test("^[0-9a-f]{64}$"))
' "${RUNNER_TEMP}/beta-receipt/ftw-native-release.json" >/dev/null
for arch in amd64 arm64; do
name="ftw-linux-${arch}.tar.gz"
gh release download "${SOURCE_BETA}" --repo "${GITHUB_REPOSITORY}" \
--pattern "${name}" --pattern "${name}.sha256" \
--dir "${RUNNER_TEMP}/beta-receipt"
(cd "${RUNNER_TEMP}/beta-receipt" && sha256sum -c "${name}.sha256")
actual=$(sha256sum "${RUNNER_TEMP}/beta-receipt/${name}" | cut -d ' ' -f 1)
recorded=$(jq -r --arg arch "${arch}" '.sha256[$arch]' \
"${RUNNER_TEMP}/beta-receipt/ftw-native-release.json")
test "${actual}" = "${recorded}"
done
git checkout --detach "${beta_commit}"
else
if [[ ! "${TAG}" =~ ^v0\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]] ||
[ -n "${SOURCE_BETA}" ]; then
echo "beta needs a v0.X.Y-beta.N tag and no source_beta" >&2
exit 1
fi
fi
source_commit=$(git rev-parse HEAD)
package_version=$(node -p "require('./package.json').version")
base_version=${TAG#v}
base_version=${base_version%-beta.*}
if [ "${base_version}" != "${package_version}" ]; then
echo "${TAG} does not match package.json ${package_version}" >&2
exit 1
fi
schema=$(node -p "require('./state-schema.json').version")
[[ "${schema}" =~ ^[1-9][0-9]*$ ]]
python3 "${RUNNER_TEMP}/check-native-schema.py" "${TAG}" "${schema}" \
"${RUNNER_TEMP}/native-release-pages.json"
printf 'tag=%s\ncommit=%s\nschema=%s\n' "${TAG}" "${source_commit}" "${schema}" >> "${GITHUB_OUTPUT}"
id: source
- name: Build complete ARM64 and AMD64 packages
env:
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
make drivers
git diff --exit-code
test -z "$(git status --porcelain)"
release_dir="${RUNNER_TEMP}/native-release"
for arch in amd64 arm64; do
VERSION="${TAG}" CANDIDATE_TAG="${TAG}" \
bash scripts/build-core.sh linux "${arch}" "${RUNNER_TEMP}/native-bin-${arch}"
build_info=$(go version -m "${RUNNER_TEMP}/native-bin-${arch}/ftw")
git_rev=$(awk '$1 == "build" && $2 ~ /^vcs.revision=/ {sub(/^vcs.revision=/, "", $2); print $2}' <<<"${build_info}")
if [ "${git_rev}" != "${{ steps.source.outputs.commit }}" ]; then
echo "linux/${arch} build has source ${git_rev}, not ${{ steps.source.outputs.commit }}" >&2
exit 1
fi
grep -F 'vcs.modified=false' <<<"${build_info}" >/dev/null
done
for arch in amd64 arm64; do
VERSION="${TAG}" python3 scripts/package-linux.py "${arch}" \
--binaries "${RUNNER_TEMP}/native-bin-${arch}" --output "${release_dir}"
(cd "${release_dir}" && sha256sum -c "ftw-linux-${arch}.tar.gz.sha256")
tar -xOf "${release_dir}/ftw-linux-${arch}.tar.gz" release-version.json | \
jq -e --arg tag "${TAG}" --arg arch "${arch}" \
'.version == $tag and .arch == $arch and .state_schema > 0' >/dev/null
done
- name: Verify the exact source
run: |
make verify
git diff --exit-code
- name: Create release receipt and notes
env:
CHANNEL: ${{ inputs.channel }}
TAG: ${{ inputs.tag }}
SOURCE_BETA: ${{ inputs.source_beta }}
SOURCE_COMMIT: ${{ steps.source.outputs.commit }}
STATE_SCHEMA: ${{ steps.source.outputs.schema }}
run: |
set -euo pipefail
python3 - <<'PY'
import hashlib
import json
import os
from pathlib import Path
release = Path(os.environ["RUNNER_TEMP"]) / "native-release"
assets = {}
for arch in ("amd64", "arm64"):
path = release / f"ftw-linux-{arch}.tar.gz"
assets[arch] = hashlib.sha256(path.read_bytes()).hexdigest()
receipt = {
"schema": 1,
"channel": os.environ["CHANNEL"],
"tag": os.environ["TAG"],
"commit": os.environ["SOURCE_COMMIT"],
"state_schema": int(os.environ["STATE_SCHEMA"]),
"source_beta": os.environ["SOURCE_BETA"],
"sha256": assets,
}
(release / "ftw-native-release.json").write_text(json.dumps(receipt, indent=2) + "\n")
PY
python3 "${RUNNER_TEMP}/write-native-release-notes.py" \
"${RUNNER_TEMP}/native-release/ftw-native-release.json" CHANGELOG.md \
"${RUNNER_TEMP}/native-release/notes.md" --repo "${GITHUB_REPOSITORY}"
- name: Anchor immutable tag and publish the selected channel
id: publication
if: ${{ !inputs.dry_run }}
env:
CHANNEL: ${{ inputs.channel }}
TAG: ${{ inputs.tag }}
SOURCE_COMMIT: ${{ steps.source.outputs.commit }}
GH_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
release_dir="${RUNNER_TEMP}/native-release"
if git ls-remote --exit-code --tags --refs origin "refs/tags/${TAG}" >/dev/null 2>&1; then
git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}"
if [ "$(git rev-list -n 1 "${TAG}")" != "${SOURCE_COMMIT}" ]; then
echo "${TAG} already points at another commit" >&2
exit 1
fi
else
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git tag -a "${TAG}" -m "Native ${CHANNEL} ${TAG}" "${SOURCE_COMMIT}"
git push origin "refs/tags/${TAG}"
fi
release_json="${RUNNER_TEMP}/native-release.json"
status=$(curl -sS -L -o "${release_json}" -w '%{http_code}' \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H 'Accept: application/vnd.github+json' \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}")
case "${status}" in
200) ;;
404)
# Tag lookup may omit a draft. Find it before trying to create
# another release for the same immutable candidate.
drafts=$(gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/releases?per_page=100")
matches=$(jq --arg tag "${TAG}" '[.[][] | select(.tag_name == $tag)] | length' <<<"${drafts}")
if [ "${matches}" -gt 1 ]; then
echo "more than one release uses ${TAG}" >&2
exit 1
fi
if [ "${matches}" -eq 0 ]; then
args=(--repo "${GITHUB_REPOSITORY}" --title "${TAG}" --notes-file "${release_dir}/notes.md" \
--draft --latest=false --verify-tag)
if [ "${CHANNEL}" = beta ]; then args+=(--prerelease); fi
gh release create "${TAG}" "${args[@]}"
fi
;;
*) echo "release lookup failed (HTTP ${status})" >&2; exit 1 ;;
esac
release_info=$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--json tagName,isDraft,isPrerelease,body)
expected_prerelease=false
if [ "${CHANNEL}" = beta ]; then expected_prerelease=true; fi
jq -e --arg tag "${TAG}" --argjson prerelease "${expected_prerelease}" \
--arg marker "<!-- ftw-state-schema-v2:${{ steps.source.outputs.schema }} -->" '
.tagName == $tag and .isPrerelease == $prerelease and
(.body | contains($marker))
' <<<"${release_info}" >/dev/null
if jq -e '.isDraft == false' <<<"${release_info}" >/dev/null; then
gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json assets | \
jq -e '([.assets[].name] | . as $names |
["ftw-linux-amd64.tar.gz", "ftw-linux-amd64.tar.gz.sha256",
"ftw-linux-arm64.tar.gz", "ftw-linux-arm64.tar.gz.sha256",
"ftw-native-release.json"] | all(. as $name | $names | index($name) != null))' >/dev/null
fi
GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" \
bash scripts/upload-release-assets.sh "${TAG}" \
"${release_dir}/ftw-linux-amd64.tar.gz" "${release_dir}/ftw-linux-amd64.tar.gz.sha256" \
"${release_dir}/ftw-linux-arm64.tar.gz" "${release_dir}/ftw-linux-arm64.tar.gz.sha256" \
"${release_dir}/ftw-native-release.json"
if jq -e '.isDraft == true' <<<"${release_info}" >/dev/null; then
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--draft=false --latest=false --verify-tag --notes-file "${release_dir}/notes.md"
echo "newly_published=true" >> "${GITHUB_OUTPUT}"
else
echo "newly_published=false" >> "${GITHUB_OUTPUT}"
fi
- name: Report verified dry run
if: ${{ inputs.dry_run }}
run: echo "Verified native packages without creating a tag or release."
ha-addon:
name: Offer the published beta to the Home Assistant app
needs: publish
if: ${{ !inputs.dry_run && inputs.channel == 'beta' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Verify publication and request a pin PR
env:
TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
HA_ADDON_DISPATCH_TOKEN: ${{ secrets.HA_ADDON_DISPATCH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${HA_ADDON_DISPATCH_TOKEN}" ]; then
echo "::notice::HA_ADDON_DISPATCH_TOKEN is absent; pin the Home Assistant app manually."
exit 0
fi
[[ "${TAG}" =~ ^v0\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]
gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--json tagName,isDraft,isPrerelease,assets | \
jq -e --arg tag "${TAG}" '
.tagName == $tag and .isDraft == false and .isPrerelease == true and
([.assets[].name] | . as $names |
["ftw-linux-amd64.tar.gz", "ftw-linux-amd64.tar.gz.sha256",
"ftw-linux-arm64.tar.gz", "ftw-linux-arm64.tar.gz.sha256",
"ftw-native-release.json"] | all(. as $name | $names | index($name) != null))
' >/dev/null
receipt_dir=$(mktemp -d)
trap 'rm -rf "${receipt_dir}"' EXIT
gh release download "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--pattern ftw-native-release.json --dir "${receipt_dir}"
jq -e --arg tag "${TAG}" '
.schema == 1 and .channel == "beta" and .tag == $tag and
(.sha256.amd64 | test("^[0-9a-f]{64}$")) and
(.sha256.arm64 | test("^[0-9a-f]{64}$"))
' "${receipt_dir}/ftw-native-release.json" >/dev/null
jq -n --arg tag "${TAG}" \
'{event_type:"ftw-native-release", client_payload:{tag:$tag, channel:"beta"}}' \
> "${receipt_dir}/dispatch.json"
GH_TOKEN="${HA_ADDON_DISPATCH_TOKEN}" gh api --method POST \
repos/srcfl/home-assistant-addons/dispatches --input "${receipt_dir}/dispatch.json"
discord:
needs: publish
if: ${{ !inputs.dry_run && needs.publish.outputs.newly_published == 'true' }}
permissions:
contents: read
uses: ./.github/workflows/release-discord.yml
with:
tag: ${{ inputs.tag }}
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}