-
-
Notifications
You must be signed in to change notification settings - Fork 23
205 lines (184 loc) · 9.78 KB
/
Copy pathrelease.yml
File metadata and controls
205 lines (184 loc) · 9.78 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
name: Release
# Cuts a release: builds the self-contained installer, signs it with Azure Trusted
# Signing (Azure Artifact Signing) via GitHub OIDC, and publishes a GitHub Release.
#
# Trigger by pushing a version tag: git push origin v2.5.0
# Or run manually (workflow_dispatch) with a version, e.g. for a dry run.
#
# Signing is GATED on the six AZURE_* secrets (set by scripts/SetupAzure.ps1). Without
# them the job still builds and publishes an UNSIGNED installer (with a ::warning::), so
# the release path works on a fork or before signing is configured. See docs/code-signing.md.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Version to build (e.g. 2.5.0, no v prefix)'
required: true
# Least-privilege floor for the whole workflow; each job escalates only what it needs (below).
permissions:
contents: read
env:
SIGN: ${{ secrets.AZURE_CLIENT_ID != '' && secrets.AZURE_TENANT_ID != '' && secrets.AZURE_SUBSCRIPTION_ID != '' && secrets.AZURE_SIGNING_ACCOUNT != '' && secrets.AZURE_SIGNING_PROFILE != '' && secrets.AZURE_SIGNING_ENDPOINT != '' }}
HAS_WINGET: ${{ secrets.WINGET_TOKEN != '' }}
jobs:
release:
name: Build, sign, and publish installer
runs-on: windows-latest
permissions:
contents: write # create the GitHub Release
id-token: write # Azure OIDC federation (no client secret)
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
# POLICY: releases are cut from the CURRENT TIP of `main`, never from `develop`, a feature branch,
# or an older `main` commit. GitHub Pages also publishes the docs site from `main/docs`, so gating
# the release on "commit == main tip" ALSO guarantees the published docs match the released build;
# a release can never ship ahead of (or behind) the docs site. An "is-ancestor of main" test would
# be too weak: an older main commit (or a develop commit already merged into main) is an ancestor
# yet can carry older docs than the tip Pages publishes. The fix when this fails is always the
# same: merge develop into main, push main, then tag that main commit. Fails fast, before any
# build/sign work.
- name: Enforce release comes from main
shell: pwsh
run: |
git fetch --no-tags origin main
$mainSha = (git rev-parse FETCH_HEAD).Trim()
$sha = (git rev-parse HEAD).Trim()
if ($sha -ne $mainSha) {
throw "Release commit $sha is not the current tip of origin/main ($mainSha). Releases must come from main: merge develop into main, push main, then tag that main commit (see dev/RELEASING.md)."
}
Write-Host "::notice::Verified the release commit is the current tip of main."
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
global-json-file: global.json
- name: Determine version
id: ver
shell: pwsh
# Untrusted values (dispatch input, ref name) are passed via env: so pwsh never
# parses them as code (#164). A literal '${{ ... }}' interpolation would let a
# value containing a quote inject PowerShell into this privileged job.
env:
RAW_VERSION: ${{ github.event.inputs.version }}
REF_NAME: ${{ github.ref_name }}
run: |
$v = if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { $env:RAW_VERSION } else { $env:REF_NAME }
$v = $v.TrimStart('v')
if ($v -notmatch '^\d+\.\d+\.\d+$') { throw "Version '$v' must be X.Y.Z (tag like v2.5.0)" }
"version=$v" >> $env:GITHUB_OUTPUT
"tag=v$v" >> $env:GITHUB_OUTPUT
Write-Host "Building version $v"
- name: Install NSIS
run: choco install nsis -y --no-progress
- name: Build installer
shell: pwsh
run: ./Release.ps1 -Version ${{ steps.ver.outputs.version }}
- name: Stage installer for signing
shell: pwsh
run: |
New-Item -ItemType Directory -Force artifacts | Out-Null
Copy-Item src/bin/mcec.Setup.exe artifacts/mcec.Setup.exe -Force
- name: Azure login (OIDC)
if: ${{ env.SIGN == 'true' }}
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Ensure PSGallery repository is registered
if: ${{ env.SIGN == 'true' }}
shell: pwsh
run: |
if (-not (Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue)) {
Write-Host "Registering default PSGallery..."
Register-PSRepository -Default
}
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
- name: Sign installer (Azure Trusted Signing)
if: ${{ env.SIGN == 'true' }}
# SECURITY (#160): do NOT swallow signing failures. When signing is expected (SIGN == 'true')
# a failed/rotated secret or service outage must FAIL the job, not silently publish an unsigned
# installer that every client's auto-updater (#146) would fetch and run.
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2
with:
endpoint: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.AZURE_SIGNING_PROFILE }}
files-folder: ${{ github.workspace }}\artifacts
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Verify installer is Authenticode-signed
if: ${{ env.SIGN == 'true' }}
shell: pwsh
# SECURITY (#160): defense in depth; even if the signing action reports success, prove the
# artifact is actually signed by the expected publisher before publishing. Fails the job otherwise.
run: |
$sig = Get-AuthenticodeSignature artifacts/mcec.Setup.exe
Write-Host "Signature status: $($sig.Status); signer: $($sig.SignerCertificate.Subject)"
# HARD gate (the #160 fix): a non-Valid signature must never publish. This catches signing
# failures and no-ops alike.
if ($sig.Status -ne 'Valid') {
throw "mcec.Setup.exe is not validly signed (status: $($sig.Status)). Refusing to publish."
}
# SOFT check: warn (don't block) if the signer subject doesn't look like the Kindel publisher.
# The Trusted Signing certificate CN is set by Microsoft from the validated legal entity, so a
# formatting difference must not false-negative-block a legitimately signed release.
if ($sig.SignerCertificate.Subject -notmatch 'Kindel') {
Write-Host "::warning::mcec.Setup.exe signer '$($sig.SignerCertificate.Subject)' does not contain 'Kindel'; verify the signing certificate identity."
}
Write-Host "::notice::Installer signature verified (status: Valid)."
- name: Warn that installer is unsigned
if: ${{ env.SIGN != 'true' }}
run: echo "::warning::AZURE_* signing secrets not set; installer is UNSIGNED. Run 'pwsh scripts/SetupAzure.ps1 -SetGitHubSecrets' to enable Azure Trusted Signing (see docs/code-signing.md)."
# Package the portable PDBs (excluded from the installer) so crash reports and post-mortem
# debugging can be symbolicated. Staged AFTER signing so the signer only ever sees the .exe.
- name: Stage symbols
shell: pwsh
run: |
$pdbs = Get-ChildItem src/bin/publish -Filter *.pdb -Recurse
if (-not $pdbs) { throw "No .pdb symbols in publish output; expected portable PDBs (src/MCEControl.csproj DebugType=portable)." }
Compress-Archive -Path $pdbs.FullName -DestinationPath "artifacts/symbols-${{ steps.ver.outputs.tag }}.zip" -Force
Write-Host "::notice::Packaged $($pdbs.Count) symbol file(s) into symbols-${{ steps.ver.outputs.tag }}.zip"
- name: Publish GitHub Release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create ${{ steps.ver.outputs.tag }} `
--title "MCEC ${{ steps.ver.outputs.tag }}" `
--generate-notes `
--latest `
artifacts/mcec.Setup.exe `
"artifacts/symbols-${{ steps.ver.outputs.tag }}.zip"
# Auto-submit the new version to winget-pkgs on every STABLE release (skips pre-releases
# and is gated on the WINGET_TOKEN secret). Requires a one-time manual bootstrap of the
# package into microsoft/winget-pkgs; see packaging/winget/README.md.
winget:
name: Submit to winget-pkgs
needs: release
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && !contains(github.ref_name, '-') }}
permissions:
contents: read # only needs its own WINGET_TOKEN; no repo write or OIDC
steps:
- name: Derive version (strip leading v)
id: ver
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Submit to winget-pkgs
if: ${{ env.HAS_WINGET == 'true' }}
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
with:
identifier: Kindel.mcec
version: ${{ steps.ver.outputs.version }}
release-tag: ${{ github.ref_name }}
installers-regex: 'mcec\.Setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
- name: Skipped (no WINGET_TOKEN)
if: ${{ env.HAS_WINGET != 'true' }}
run: echo "::notice::WINGET_TOKEN not set; skipping winget-pkgs submission. See packaging/winget/README.md."