Files
james.maesandCodex GPT-6 a44449b991 feat(releases): ship MSI and reuse exact-source CI proof (#40)
Stable1.0.2 and dev1.0.2-beta.1 EXEs/ZIPs are public and their actual
downloaded signatures, timestamps, payloads, hashes, SBOM/provenance and
updater feeds verify. Complete the owner's renewed MSI request by
pinning standards1.2.0 at0ef3d319, enabling signed per-user MSI
alongside EXE/ZIP, preparing1.0.3 channel links and next development
prefix1.0.4.

Stop duplicate release validation: a tag must be on main, have a
verified source signature, match the entire genuinely merged PR Git
tree, and reuse that head's latest successful required technical checks
under their configured provider identities. Missing, failed, stale,
pending or different-tree proof fails publication. All branch technical
gates remain required; fresh release build/core coverage, signing, exact
MSI payload/native lifecycle and distribution/SBOM/provenance checks
remain required. Application behavior/dependencies/Azure trust are
unchanged.

Validation: all8 script tests pass (including10 release-proof boundary
cases), live source proof verifies all17 genuine checks on the actual
published18d8ae9 tree, five actual documents pass canonical lint,
actionlint/zizmor and signed commit hooks pass. Exact-head full CI
starts with this PR; production signed MSI lifecycle is still required
before delivery. No claim of published MSI yet.

Owner explicitly authorized release without further human-review
requests. Record a truthful release-specific waiver; never fabricate
personal-read acknowledgement; restore original rules immediately after
the bounded merge. Substantial AI assistance: Codex GPT-6. Refs
#33,#36,#38 and KofTwentyTwo/standards#47.

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-13 09:04:32 -05:00

183 lines
8.7 KiB
YAML

# Windows desktop application release for a KofTwentyTwo repository (K22-CI-30,
# K22-REL-*, architecture/windows-desktop-app.md).
# Pushing a protected v* tag builds, tests, publishes, packs with Velopack, generates
# SBOMs, attests, and publishes the GitHub release through the shared reusable workflow.
#
# Tags: v1.2.3 -> Velopack channel `stable`; v1.2.3-beta.1 -> channel `dev` (prerelease).
#
# One-time owner setup:
# - Environment `release` (Settings -> Environments) with a deployment rule that allows
# only tags matching v*.
# - Configure verified Azure signing variables and a repository/environment-bound
# OIDC identity; pin the reviewed builder, enable sign and declare
# the owned-file catalog. See docs/release-signing.md. Unsigned publication is blocked.
# - winget (optional, stable releases only): the package must already exist in
# microsoft/winget-pkgs. Submit it once by hand with
# wingetcreate new <Setup.exe URL of the first stable release>
# then add a WINGET_TOKEN secret (a fine-grained token that can fork and open pull
# requests on public repositories, expiring within 90 days, K22-SEC-03) to the
# `release` environment, and set the repository variable WINGET_PACKAGE_ID.
name: release
on:
push:
tags:
- "v*"
permissions: {}
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
verify:
name: verify
runs-on: ubuntu-24.04
timeout-minutes: 10
environment: release
permissions:
contents: read
checks: read
pull-requests: read
steps:
- name: Checkout main and tag history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Require the tagged commit to be on main
shell: bash
run: |
set -euo pipefail
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
- name: Require verified release signing configuration
shell: pwsh
env:
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
ARTIFACT_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
ARTIFACT_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
ARTIFACT_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
run: ./build/Assert-ReleaseSigning.ps1
- name: Require passing CI for the exact merged release tree
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { run } = require('./.github/scripts/assert-release-source.cjs');
await run({ github, context, core });
release:
needs: verify
uses: KofTwentyTwo/standards/.github/workflows/release-velopack.yml@70b555bd5658273090769991bfc4c9b6681dde18 # v1.2.1
permissions:
contents: write # create and publish the GitHub release
id-token: write # sign attestations; Azure OIDC when signing
attestations: write # store attestations
with:
solution: MarkdownViewr.Windows.slnx
app-project: src/MarkdownViewr.Windows/MarkdownViewr.Windows.csproj
# Separate package IDs install side by side; the installed channel selects its title/data root.
app-id: ${{ contains(github.ref_name, '-') && 'markdownviewr-windows-dev' || 'markdownviewr-windows' }}
main-exe: MarkdownViewr.Windows.exe
icon: src/MarkdownViewr.Windows/Assets/Brand/app.ico
platform: x64
runtime: win-x64
coverage-packages: MarkdownViewr.Windows.Core
test-projects: |
tests/MarkdownViewr.Windows.Core.Tests
sign: true
msi: true
signing-files: |
app/MarkdownViewr.Windows.exe
app/MarkdownViewr.Windows.dll
app/MarkdownViewr.Windows.Core.dll
app/KofTwentyTwo.AppKit.dll
app/KofTwentyTwo.AppKit.Updates.dll
app/KofTwentyTwo.AppKit.WinUI.dll
# winget lives here, not in the shared workflow, because its token belongs to this
# repository's `release` environment. It runs only for stable releases, and only when
# the WINGET_PACKAGE_ID variable is set (after the manual first submission).
winget:
name: winget
needs: release
if: needs.release.outputs.prerelease == 'false' && vars.WINGET_PACKAGE_ID != ''
runs-on: windows-latest
timeout-minutes: 15
environment: release
permissions: {}
steps:
- name: Submit winget manifest update
shell: pwsh
env:
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }}
WINGET_PACKAGE_ID: ${{ vars.WINGET_PACKAGE_ID }}
K22_VERSION: ${{ needs.release.outputs.version }}
SETUP_URL: ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ needs.release.outputs.setup }}
run: |
if (-not $env:WINGET_CREATE_GITHUB_TOKEN)
{
Write-Output '::error::WINGET_PACKAGE_ID is configured but WINGET_TOKEN is missing from the release environment.'
exit 1
}
# wingetcreate v1.12.13.0, pinned by URL and verified by SHA-256 before use.
$wingetcreate = Join-Path $env:RUNNER_TEMP 'wingetcreate.exe'
Invoke-WebRequest -Uri 'https://github.com/microsoft/winget-create/releases/download/v1.12.13.0/wingetcreate.exe' -OutFile $wingetcreate
if ((Get-FileHash -LiteralPath $wingetcreate -Algorithm SHA256).Hash -ne '24042BD37915805615E6CF969AC57C6439124C3FE85823327F5F3FB24BD9FFEA')
{
Write-Output '::error::wingetcreate.exe does not match the pinned SHA-256.'
exit 1
}
& $wingetcreate update $env:WINGET_PACKAGE_ID --urls $env:SETUP_URL --version $env:K22_VERSION --submit
distribution:
name: distribution candidates
needs: release
if: needs.release.outputs.prerelease == 'false' && (vars.DISTRIBUTION_ARTIFACT == 'portable' || vars.DISTRIBUTION_ARTIFACT == 'cli')
runs-on: windows-latest
timeout-minutes: 15
environment: release
permissions:
contents: read
steps:
- name: Checkout reviewed release
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download and verify final stable artifact
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
K22_REPOSITORY: ${{ github.repository }}
K22_TAG: ${{ github.ref_name }}
K22_VERSION: ${{ needs.release.outputs.version }}
K22_KIND: ${{ vars.DISTRIBUTION_ARTIFACT }}
run: |
$ErrorActionPreference = 'Stop'
if ($env:K22_KIND -eq 'cli') { throw 'CLI distribution requires a generated CLI head.' }
$asset = if ($env:K22_KIND -eq 'cli') { "markdownviewr-windows-cli-win-x64-$env:K22_VERSION.zip" } else { 'markdownviewr-windows-stable-Portable.zip' }
New-Item -ItemType Directory -Force -Path artifacts/distribution-input | Out-Null
gh release download $env:K22_TAG --repo $env:K22_REPOSITORY --pattern $asset --pattern SHA256SUMS --dir artifacts/distribution-input
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$path = Join-Path 'artifacts/distribution-input' $asset
$hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
$line = @(Get-Content artifacts/distribution-input/SHA256SUMS | Where-Object { $_ -match "^[0-9a-fA-F]{64}\s+\*?$([regex]::Escape($asset))$" })
if ($line.Count -ne 1 -or $line[0].Substring(0, 64).ToLowerInvariant() -ne $hash) { throw 'Release archive does not match SHA256SUMS.' }
gh attestation verify $path --repo $env:K22_REPOSITORY --signer-repo KofTwentyTwo/standards
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
./build/New-DistributionManifests.ps1 -Version $env:K22_VERSION -ArtifactPath $path -ArtifactKind $env:K22_KIND
$spec = @(Get-ChildItem artifacts/distribution/chocolatey -Recurse -Filter '*.nuspec')
if ($spec.Count -ne 1) { throw 'Expected one selected Chocolatey package.' }
choco pack $spec[0].FullName --outputdirectory artifacts/distribution
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Upload candidates for review
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: distribution-candidates
path: artifacts/distribution
if-no-files-found: error