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>
183 lines
8.7 KiB
YAML
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
|