Repository navigation
172 lines (148 loc) · 6.14 KB
/
Copy pathrelease.yml
File metadata and controls
172 lines (148 loc) · 6.14 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
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build-windows:
runs-on: windows-latest
timeout-minutes: 90
permissions:
attestations: write
contents: write
id-token: write
env:
RELEASE_TAG: ${{ github.ref_name }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # zizmor: ignore[cache-poisoning] v4 caching is off when the cache input is omitted
with:
node-version: "22"
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
- name: Setup uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
with:
enable-cache: false
- name: Validate tag and configure optional signing
id: metadata
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
shell: pwsh
run: |
$version = (Get-Content package.json -Raw | ConvertFrom-Json).version
if ($env:RELEASE_TAG -ne "v$version") {
throw "Tag $env:RELEASE_TAG does not match package version v$version"
}
$hasCertificate = ![string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_BASE64)
$hasPassword = ![string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)
if ($hasCertificate -ne $hasPassword) {
throw "WINDOWS_CERTIFICATE_BASE64 and WINDOWS_CERTIFICATE_PASSWORD must be configured together"
}
if ($hasCertificate) {
$certificateFile = Join-Path $env:RUNNER_TEMP "trainkit-signing.pfx"
[IO.File]::WriteAllBytes(
$certificateFile,
[Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64)
)
"WINDOWS_CERTIFICATE_FILE=$certificateFile" >> $env:GITHUB_ENV
"signed=true" >> $env:GITHUB_OUTPUT
} else {
"signed=false" >> $env:GITHUB_OUTPUT
}
"version=$version" >> $env:GITHUB_ENV
"version=$version" >> $env:GITHUB_OUTPUT
- name: Install frontend dependencies
run: npm ci
- name: Install locked backend dependencies
run: uv sync --project backend --locked
- name: Verify source
run: npm run verify
- name: Build Windows ZIP
run: npm run make
- name: Verify packaged layout
run: npm run verify:package
- name: Verify Authenticode signatures
if: steps.metadata.outputs.signed == 'true'
shell: pwsh
run: |
$target = "out/TrainKit-win32-x64/TrainKit.exe"
$signature = Get-AuthenticodeSignature $target
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for ${target}: $($signature.Status)"
}
- name: Prepare release artifacts and checksums
shell: pwsh
run: |
$archive = "out/make/zip/win32/x64/TrainKit-win32-x64-$env:version.zip"
if (!(Test-Path $archive)) {
throw "Windows archive was not produced: $archive"
}
@($archive) | ForEach-Object {
$hash = (Get-FileHash $_ -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $(Split-Path $_ -Leaf)"
} | Set-Content "out/SHA256SUMS.txt" -Encoding utf8
- name: Prepare changelog release notes
shell: pwsh
run: |
$changelog = Get-Content "CHANGELOG.md" -Raw
$escapedVersion = [regex]::Escape($env:version)
$match = [regex]::Match(
$changelog,
"(?ms)^## \[$escapedVersion\].*?(?=^## \[|\z)"
)
if (!$match.Success) {
throw "CHANGELOG.md does not contain a section for $env:version"
}
if ('${{ steps.metadata.outputs.signed }}' -eq 'true') {
$releaseNotice = @"
---
The Windows executable is Authenticode-signed. The ZIP is accompanied by a SHA-256 checksum and GitHub build-provenance attestation.
"@
} else {
$releaseNotice = @'
---
> [!WARNING]
> This release is currently unsigned. Windows SmartScreen or antivirus software may display a warning or false-positive detection. Download only from this GitHub release and verify the ZIP against `SHA256SUMS.txt` before running it.
Download the Windows ZIP, extract the entire archive to a writable folder on the drive where you want TrainKit stored, then run `TrainKit.exe`. The ZIP is the standard Windows distribution; there is no installer.
The ZIP is accompanied by a SHA-256 checksum and GitHub build-provenance attestation.
'@
}
"$($match.Value.Trim())`n`n$($releaseNotice.Trim())" |
Set-Content "out/RELEASE_NOTES.md" -Encoding utf8
- name: Attest release provenance
uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
with:
subject-path: |
out/make/zip/win32/x64/TrainKit-win32-x64-${{ steps.metadata.outputs.version }}.zip
out/SHA256SUMS.txt
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
shell: pwsh
run: |
gh release create $env:RELEASE_TAG `
"out/make/zip/win32/x64/TrainKit-win32-x64-$env:version.zip" `
"out/SHA256SUMS.txt" `
--verify-tag `
--title "TrainKit $env:RELEASE_TAG" `
--notes-file "out/RELEASE_NOTES.md"
- name: Remove signing certificate
if: always()
shell: pwsh
run: |
if (![string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_FILE)) {
Remove-Item -LiteralPath $env:WINDOWS_CERTIFICATE_FILE -Force -ErrorAction SilentlyContinue
}