diff --git a/.github/scripts/New-ReleaseNotes.ps1 b/.github/scripts/New-ReleaseNotes.ps1 deleted file mode 100644 index f80f4ee..0000000 --- a/.github/scripts/New-ReleaseNotes.ps1 +++ /dev/null @@ -1,289 +0,0 @@ -#Requires -Version 7.4 -<# -.SYNOPSIS - Writes release notes for a tag from the Conventional Commit subjects merged since - the previous release (K22-REL-02). - -.DESCRIPTION - Pull requests squash-merge with their Conventional Commit title as the subject, - so the commits between the previous published release and this tag are the - changelog. Subjects are grouped by type with breaking changes and security - fixes first; subjects that are not Conventional Commits land under "Other - changes" so nothing is silently dropped. Each entry links its pull request - ("(#123)" suffix) or, failing that, its commit. When an issue titled - "Release " exists (the release checklist, K22-REL-12) it is linked too. - - Needs the GitHub CLI authenticated for the repository (GH_TOKEN in Actions). - -.PARAMETER Tag - The release tag, e.g. v1.2.3 or v1.2.3-beta.1. - -.PARAMETER Repository - owner/name on GitHub. - -.PARAMETER OutputPath - Where the Markdown notes are written. - -.PARAMETER PreviousTag - The tag to compare from. Default: the most recent published release before - this tag, or the repository's first commit when there is none. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [ValidatePattern('^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$')] - [string] $Tag, - - [Parameter(Mandatory)] - [ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')] - [string] $Repository, - - [Parameter(Mandatory)] - [string] $OutputPath, - - [string] $PreviousTag -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -<# -.SYNOPSIS - Calls the GitHub REST API through gh and returns the parsed JSON, or $null when - the call fails (the caller decides whether that matters). -#> -function Invoke-GitHubApi -{ - param( - [Parameter(Mandatory)] [string] $Path, - [switch] $Paginate - ) - - $arguments = @('api', '-H', 'Accept: application/vnd.github+json') - if($Paginate) - { - $arguments += @('--paginate', '--slurp') - } - $arguments += $Path - - $output = & gh @arguments 2>$null - if($LASTEXITCODE -ne 0) - { - return $null - } - return (($output | ForEach-Object { "$_" }) -join "`n") | ConvertFrom-Json -Depth 50 -} - -<# -.SYNOPSIS - Finds the tag of the most recent published release that precedes the one being - released, so the notes cover exactly what changed since users last got a build. -#> -function Find-PreviousReleaseTag -{ - param([Parameter(Mandatory)] [string] $CurrentTag) - - $pages = Invoke-GitHubApi -Path "repos/$Repository/releases?per_page=100" -Paginate - if($null -eq $pages) - { - return $null - } - - $releases = @($pages | ForEach-Object { $_ }) | - Where-Object { -not $_.draft -and $_.tag_name -ne $CurrentTag } | - Sort-Object { [DateTime] $_.created_at } -Descending - if(@($releases).Count -eq 0) - { - return $null - } - return @($releases)[0].tag_name -} - -<# -.SYNOPSIS - Lists the commits between two refs through the compare API: subject, body, sha, - and the pull request number when the subject carries a "(#123)" suffix. -#> -function Get-CommitsBetween -{ - param( - [string] $From, - [Parameter(Mandatory)] [string] $To - ) - - if([string]::IsNullOrEmpty($From)) - { - $pages = Invoke-GitHubApi -Path "repos/$Repository/commits?sha=$To&per_page=100" -Paginate - $commits = @($pages | ForEach-Object { $_ }) - } - else - { - $compare = Invoke-GitHubApi -Path "repos/$Repository/compare/$From...$To" - if($null -eq $compare) - { - throw "Cannot compare $From...$To in $Repository." - } - $commits = @($compare.commits) - } - - foreach($commit in $commits) - { - $lines = $commit.commit.message -split "`r?`n" - $subject = $lines[0].Trim() - if($subject -match '^Merge (pull request|branch) ') - { - continue - } - $pullNumber = $null - if($subject -match '\s\(#(\d+)\)$') - { - $pullNumber = [int] $Matches[1] - $subject = $subject -replace '\s\(#\d+\)$', '' - } - $body = ($lines | Select-Object -Skip 1) -join "`n" - [pscustomobject]@{ Sha = $commit.sha; Subject = $subject; Body = $body; PullNumber = $pullNumber } - } -} - -<# -.SYNOPSIS - Classifies one commit into a notes section from its Conventional Commit header. - Breaking changes ("!" or a BREAKING CHANGE footer) and security fixes come - first; anything that is not a Conventional Commit goes to "Other changes". -#> -function Get-NotesSection -{ - param([Parameter(Mandatory)] [pscustomobject] $Commit) - - $header = '^(?[a-z]+)(\((?[^)]+)\))?(?!)?: (?.+)$' - if($Commit.Subject -notmatch $header) - { - return [pscustomobject]@{ Section = 'Other changes'; Text = $Commit.Subject } - } - - $type = $Matches['type'] - $scope = $Matches['scope'] - $description = $Matches['description'] - $text = if($scope) { "**$scope**: $description" } else { $description } - - if($Matches['bang'] -eq '!' -or $Commit.Body -match '(?m)^BREAKING[ -]CHANGE:') - { - return [pscustomobject]@{ Section = 'Breaking changes'; Text = $text } - } - if($type -eq 'security' -or $scope -eq 'security') - { - return [pscustomobject]@{ Section = 'Security'; Text = $text } - } - - $section = switch($type) - { - 'feat' { 'Features' } - 'fix' { 'Fixes' } - 'perf' { 'Performance' } - 'refactor' { 'Refactoring' } - 'docs' { 'Documentation' } - 'build' { 'Build and dependencies' } - 'ci' { 'CI' } - 'test' { 'Tests' } - 'chore' { 'Chores' } - 'style' { 'Style' } - 'revert' { 'Reverts' } - default { 'Other changes' } - } - return [pscustomobject]@{ Section = $section; Text = $text } -} - -<# -.SYNOPSIS - Finds the release-checklist issue ("Release ") so the notes can link it. -#> -function Find-ChecklistIssue -{ - param([Parameter(Mandatory)] [string] $CurrentTag) - - $query = [uri]::EscapeDataString("repo:$Repository is:issue in:title `"Release $CurrentTag`"") - $result = Invoke-GitHubApi -Path "search/issues?q=$query&per_page=5" - if($null -eq $result) - { - return $null - } - $match = @($result.items) | Where-Object { $_.title -eq "Release $CurrentTag" } | Select-Object -First 1 - return $match -} - -################################################################################ -## Resolve the range, classify every commit, and write the notes in section ## -## order with breaking changes and security fixes first (K22-REL-02). ## -################################################################################ -if([string]::IsNullOrEmpty($PreviousTag)) -{ - $PreviousTag = Find-PreviousReleaseTag -CurrentTag $Tag -} - -$commits = @(Get-CommitsBetween -From $PreviousTag -To $Tag) -$order = @('Breaking changes', 'Security', 'Features', 'Fixes', 'Performance', 'Refactoring', - 'Documentation', 'Build and dependencies', 'CI', 'Tests', 'Style', 'Chores', 'Reverts', 'Other changes') -$grouped = @{} -foreach($commit in $commits) -{ - $entry = Get-NotesSection -Commit $commit - $link = if($null -ne $commit.PullNumber) - { - "[#$($commit.PullNumber)](https://github.com/$Repository/pull/$($commit.PullNumber))" - } - else - { - "[$($commit.Sha.Substring(0, 7))](https://github.com/$Repository/commit/$($commit.Sha))" - } - if(-not $grouped.ContainsKey($entry.Section)) - { - $grouped[$entry.Section] = [System.Collections.Generic.List[string]]::new() - } - $grouped[$entry.Section].Add("- $($entry.Text) ($link)") -} - -$notes = [System.Collections.Generic.List[string]]::new() -foreach($section in $order) -{ - if(-not $grouped.ContainsKey($section)) - { - continue - } - $notes.Add("## $section") - $notes.Add('') - foreach($line in $grouped[$section]) - { - $notes.Add($line) - } - $notes.Add('') -} -if($commits.Count -eq 0) -{ - $notes.Add('No changes since the previous release.') - $notes.Add('') -} - -$checklist = Find-ChecklistIssue -CurrentTag $Tag -if($null -ne $checklist) -{ - $notes.Add("Release checklist: [#$($checklist.number)]($($checklist.html_url))") - $notes.Add('') -} -if(-not [string]::IsNullOrEmpty($PreviousTag)) -{ - $notes.Add("**Full changelog**: https://github.com/$Repository/compare/$PreviousTag...$Tag") -} -else -{ - $notes.Add("**Full changelog**: https://github.com/$Repository/commits/$Tag") -} -$notes.Add('') -$notes.Add('Every asset is listed in `SHA256SUMS` and carries a build-provenance attestation; a CycloneDX SBOM per artifact is attached. See "Verifying a release" in the README.') - -$directory = Split-Path -Parent ([System.IO.Path]::GetFullPath($OutputPath)) -if(-not (Test-Path -LiteralPath $directory)) -{ - New-Item -ItemType Directory -Path $directory | Out-Null -} -[System.IO.File]::WriteAllText($OutputPath, (($notes -join "`n") + "`n"), [System.Text.UTF8Encoding]::new($false)) -Write-Verbose "Wrote $($commits.Count) commit(s) of notes to $OutputPath" diff --git a/.github/scripts/Update-PackageManifests.ps1 b/.github/scripts/Update-PackageManifests.ps1 index c6016c6..7fa8362 100644 --- a/.github/scripts/Update-PackageManifests.ps1 +++ b/.github/scripts/Update-PackageManifests.ps1 @@ -5,7 +5,7 @@ (packaging/chocolatey) at a stable release's CLI zip. .DESCRIPTION - Both package managers install the same asset, gclo-cli-win-x64.zip, by URL and + Both package managers install the same asset, gclo-cli-win-x64-.zip, by URL and SHA-256. This script rewrites the version, URL, checksum, and release-notes link in all three files from one source of truth: the version and the zip's hash (computed from -CliZipPath, or taken from -Sha256). The release workflow runs it @@ -16,7 +16,7 @@ The stable semantic version, e.g. 1.2.3 (no leading 'v'). .PARAMETER CliZipPath - Path to the built gclo-cli-win-x64.zip to hash. Either this or -Sha256. + Path to the built gclo-cli-win-x64-.zip to hash. Either this or -Sha256. .PARAMETER Sha256 The zip's SHA-256 (lowercase hex) when it is already known. @@ -76,7 +76,7 @@ if($Sha256 -notmatch '^[0-9a-f]{64}$') } $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') -$url = "https://github.com/KofTwentyTwo/gclo/releases/download/v$Version/gclo-cli-win-x64.zip" +$url = "https://github.com/KofTwentyTwo/gclo/releases/download/v$Version/gclo-cli-win-x64-$Version.zip" ################################################################################ ## Scoop: a real JSON edit, so the file stays valid whatever its layout. ## diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e2a6e4..035eaf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,8 @@ on: branches: [main] pull_request: # The Release workflow calls this one so a tag is gated by exactly the checks - # a pull request is: build, coverage, and formatting, with the UI smoke suite - # advisory (see ui-tests-advisory) just as it is not a required check on main. + # a pull request is: build, coverage, formatting, and the UI end-to-end suite. workflow_call: - inputs: - ui-tests-advisory: - description: >- - When true, a failing UI end-to-end job does not fail this workflow. The - hosted-runner UIA suite is not yet a required check on main, so a release - must not be held to a stricter bar than a merge; a failure is still - visible in the job list for follow-up. - type: boolean - default: false permissions: contents: read @@ -110,16 +100,11 @@ jobs: if-no-files-found: warn ui-tests: - # End-to-end FlaUI/UIA tests against the real gclo.exe. Not in the branch - # ruleset's required checks yet: hosted-runner UIA is new here, so this job - # earns required status by proving itself stable first. Until then a caller - # (the Release workflow) can make it advisory so a flaky UI run cannot block - # an immutable tag that main itself would have accepted. On push/PR events the - # input is unset, so the job fails the workflow exactly as before. + # End-to-end FlaUI/UIA tests against the real gclo.exe. A required check on + # main since 2026-10-09 (#54), and therefore a blocking gate for releases too. name: ui-tests runs-on: windows-latest timeout-minutes: 30 - continue-on-error: ${{ inputs.ui-tests-advisory == true }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0621e06..9f44579 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,37 @@ -# Release pipeline, triggered by pushing a tag that starts with 'v'. +# Release pipeline, triggered by pushing a tag that starts with 'v' +# (docs/RELEASING.md is the runbook). # # v1.2.3 -> stable release -> Velopack channel 'stable' # v1.2.3-beta.1 -> prerelease -> Velopack channel 'dev' # -# See docs/RELEASING.md for the full release runbook, including the secrets that -# enable the optional NuGet and winget steps. +# Stages, each gated on the previous one: +# verify - the tag is strict semver and its commit is on main +# gates - the full ci workflow, exactly as a pull request runs it (K22-CI-31) +# release - the KofTwentyTwo standards' shared Velopack workflow: locked restore, +# Release build, tests and coverage gate, publish, vpk pack, SBOMs, +# SHA256SUMS, provenance, draft-then-publish GitHub release. The build +# runs inside the shared workflow, so the provenance names it as the +# builder (SLSA Build L3, K22-CI-30). +# packages - stable only: Scoop and Chocolatey manifests pointed at the release, +# Chocolatey package pushed, manifests proposed as a PR against main +# winget - stable only, once WINGET_PACKAGE_ID is set: manifest update +# submitted to microsoft/winget-pkgs +# nuget - only when the repository variable NUGET_PUBLISH is 'true' +# +# A publishing job that cannot publish FAILS; nothing is skipped quietly. winget +# is the one explicit opt-in: `wingetcreate update` needs a package that already +# exists in microsoft/winget-pkgs, and the first submission is manual and needs a +# published stable installer, so the job runs once the WINGET_PACKAGE_ID variable +# is set (docs/RELEASING.md) and fails if the token is missing from then on. The +# tokens live in the `release` environment, which only v* tags can use: +# DEPS_PAT fine-grained token: contents + pull requests (write) on this +# repository, so the packaging PR runs the required checks +# CHOCO_API_KEY chocolatey.org API key +# WINGET_TOKEN fine-grained token that can fork and open pull requests on +# public repositories (microsoft/winget-pkgs) +# NUGET_API_KEY nuget.org key with push rights for gclo.Engine (NUGET_PUBLISH) +# plus the repository variable WINGET_PACKAGE_ID (KofTwentyTwo.gclo, set after the +# one-time manual `wingetcreate new`). name: Release on: @@ -12,15 +39,7 @@ on: tags: - 'v*' -# Three stages, each gated on the previous: -# verify - the tag must be strict semver AND its commit must be on main -# gates - the full CI workflow (build, 100% coverage, format; UI smoke advisory) -# release - package and publish; only runs when both stages succeeded -# -# Read-only by default; only the release job gets contents: write, which vpk -# and gh need to create the GitHub Release and upload assets to it. -permissions: - contents: read +permissions: {} concurrency: group: release-${{ github.ref }} @@ -29,12 +48,10 @@ concurrency: jobs: verify: name: Verify tag - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 - outputs: - version: ${{ steps.version.outputs.version }} - prerelease: ${{ steps.version.outputs.prerelease }} - channel: ${{ steps.version.outputs.channel }} + permissions: + contents: read steps: - name: Checkout (full history, so main's ancestry can be checked) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -42,32 +59,10 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Derive version from tag - id: version - shell: pwsh - run: | - $tag = $env:GITHUB_REF_NAME - # Strict semver 2.0 (major.minor.patch with optional prerelease - # identifiers). Build metadata ('+...') is deliberately rejected: it - # is not meaningful to NuGet or Velopack asset naming. - $semverPattern = '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$' - if ($tag -notmatch $semverPattern) { - Write-Error "Tag '$tag' is not a valid release tag. Expected vMAJOR.MINOR.PATCH with an optional prerelease suffix, e.g. v1.2.3 or v1.2.3-beta.1." - exit 1 - } - $version = $tag.Substring(1) - $prerelease = $version.Contains('-') - $channel = if ($prerelease) { 'dev' } else { 'stable' } - Write-Host "Tag: $tag | Version: $version | Prerelease: $prerelease | Velopack channel: $channel" - Add-Content -Path $env:GITHUB_OUTPUT -Value "version=$version" - Add-Content -Path $env:GITHUB_OUTPUT -Value "prerelease=$($prerelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_OUTPUT -Value "channel=$channel" - - # Releases are cut from main (docs/RELEASING.md). A tag anywhere else - - # a feature branch, an unmerged dev commit, a detached experiment - would - # otherwise publish an immutable release that never passed main's gates. - # To exercise the failure path, push a 'v*' tag on a commit that is not - # on main: this step fails and nothing downstream runs. + # Releases are cut from main. A tag anywhere else (a feature branch, a + # detached experiment) would otherwise publish an immutable release that + # never passed main's gates. The shared workflow validates the tag's + # format itself. - name: Require the tagged commit to be on main shell: bash run: | @@ -81,274 +76,91 @@ jobs: fi # The same gates a pull request must pass (K22-CI-31): zero-warning x64 build, - # 100% line coverage on Engine/ViewModels/Cli, and formatting; the security - # scans and CodeQL run on the tagged commit when it reached main. If any of those fails, this - # job fails and release is skipped. The FlaUI UI smoke suite runs too but is - # advisory here, exactly as it is not a required check on main: hosted-runner UI - # automation is not yet proven stable enough to hold an immutable tag hostage. - # Documented exception per #44; flip ui-tests-advisory to false (and add the job - # to main's required checks) once the suite has earned it. + # 100% line coverage, formatting, and the FlaUI end-to-end suite. gates: name: Gates needs: verify uses: ./.github/workflows/ci.yml - with: - ui-tests-advisory: true + permissions: + contents: read release: - name: Build, package, and publish + name: Release needs: [verify, gates] - runs-on: windows-latest - timeout-minutes: 90 + uses: KofTwentyTwo/standards/.github/workflows/release-velopack.yml@e8ab30f8c60df437440ffd786c87678207246f5c # main 2026-10-09 (pre-1.0 draft, standards#5) permissions: - contents: write - # Build provenance attestations for the published assets. - id-token: write - attestations: write + contents: write # create and publish the GitHub release + id-token: write # sign attestations; Azure OIDC when signing + attestations: write # store attestations + with: + solution: gclo.slnx + app-project: gclo/gclo.csproj + app-id: gclo + main-exe: gclo.exe + icon: gclo/Assets/Brand/gclo.ico + platform: x64 + runtime: win-x64 + coverage-packages: gclo.Engine,gclo.ViewModels,gclo + coverage-minimum: 100 + test-projects: | + gclo.Engine.Tests + gclo.Cli.Tests + cli-project: gclo.Cli/gclo.Cli.csproj + sign: false # standards exception EX-0002 (#57): true once the Azure Trusted Signing identity is validated + + # Scoop and Chocolatey (#9), stable releases only. The repository is its own + # Scoop bucket (bucket/gclo.json) and ships the Chocolatey package source + # (packaging/chocolatey). Both are pointed at this release's CLI zip; the + # Chocolatey package is pushed, and the updated manifests are proposed as a + # pull request against main, where Scoop reads bucket/gclo.json from. + packages: + name: Scoop and Chocolatey + needs: release + if: needs.release.outputs.prerelease == 'false' + runs-on: windows-latest + timeout-minutes: 20 + environment: release + permissions: + contents: read env: - REPO_URL: https://github.com/KofTwentyTwo/gclo - # The verify job's outputs, as plain environment variables: run: scripts - # read these instead of interpolating ${{ }} expressions (K22-CI-12). - VERSION: ${{ needs.verify.outputs.version }} - PRERELEASE: ${{ needs.verify.outputs.prerelease }} - CHANNEL: ${{ needs.verify.outputs.channel }} - # SBOM generator (CycloneDX for .NET), pinned like the other release tooling. - CYCLONEDX_VERSION: 6.2.0 - # Pinned release tooling. vpk builds the installer and packages users - # receive and wingetcreate is a binary fetched at run time; neither may - # float. Bump deliberately, together with the Velopack package reference. - VPK_VERSION: 1.2.161 - WINGETCREATE_VERSION: v1.12.13.0 - WINGETCREATE_SHA256: 24042bd37915805615e6cf969ac57c6439124c3fe85823327f5f3fb24bd9ffea - # The publishing secrets (NUGET_API_KEY, WINGET_TOKEN) are NOT job-level env: - # only the two steps that publish receive them, so nothing executed by the - # build, the tests, vpk, or wingetcreate's download can read them. + K22_VERSION: ${{ needs.release.outputs.version }} + TAG: ${{ github.ref_name }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # No restore cache here (K22-CI-15): a release never builds from a cache a - # pull request run could have written. - - name: Set up .NET SDK - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json - - # ---------------------------------------------------------------------- - # (a) CLI: self-contained single-file publish, zipped as a release asset. - # ---------------------------------------------------------------------- - - name: Publish CLI (win-x64, single file) - run: dotnet publish gclo.Cli -c Release -r win-x64 --self-contained -p:Version=$env:VERSION -p:PublishSingleFile=true -p:RestoreLockedMode=true -o publish/cli - - # Released artifacts carry the license (K22-REPO-01). - - name: Zip CLI - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path artifacts | Out-Null - Copy-Item LICENSE publish/cli/LICENSE - Compress-Archive -Path publish/cli/* -DestinationPath artifacts/gclo-cli-win-x64.zip - - # ---------------------------------------------------------------------- - # (b) WinUI app: unpackaged (no MSIX) self-contained publish, then - # packaged with Velopack. vpk upload creates the GitHub Release itself - # and attaches the Setup, portable, and delta assets to it. - # ---------------------------------------------------------------------- - - name: Publish app for Velopack (unpackaged win-x64) - run: dotnet publish gclo/gclo.csproj -c Release -p:Platform=x64 -r win-x64 --self-contained -p:Version=$env:VERSION -p:WindowsPackageType=None -p:WindowsAppSDKSelfContained=true -p:PublishTrimmed=false -p:RestoreLockedMode=true -o publish/app - - - name: Add the license to the app payload - shell: pwsh - run: Copy-Item LICENSE publish/app/LICENSE - - - name: Install Velopack CLI - run: dotnet tool install -g vpk --version $env:VPK_VERSION - - - name: Download previous Velopack release (delta baseline) - # The very first release on a channel has nothing to download; that is - # fine - vpk pack simply produces no delta package - so a failure here - # must not fail the release. - continue-on-error: true + # The secrets context cannot be read in `if`; this step is the only one + # besides the publishers that sees the values, and it never prints them. + - name: Require the publishing secrets shell: pwsh env: - GITHUB_TOKEN: ${{ github.token }} - run: | - $preFlag = @() - if ($env:PRERELEASE -eq 'true') { $preFlag = @('--pre') } - vpk download github --repoUrl $env:REPO_URL --token $env:GITHUB_TOKEN --channel $env:CHANNEL @preFlag - - - name: Velopack pack - run: vpk pack --packId gclo --packVersion $env:VERSION --packDir publish/app --mainExe gclo.exe --channel $env:CHANNEL --icon gclo/Assets/Brand/gclo.ico - - # ---------------------------------------------------------------------- - # SBOM (K22-REL-06): one CycloneDX document per shipped artifact, from the - # resolved dependency graph of the project that produced it. File names - # carry the version (K22-REL-03). - # ---------------------------------------------------------------------- - - name: Install CycloneDX - run: dotnet tool install -g CycloneDX --version $env:CYCLONEDX_VERSION - - - name: Generate SBOMs - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path artifacts/sbom | Out-Null - dotnet CycloneDX gclo.Cli/gclo.Cli.csproj --output artifacts/sbom --filename "gclo-cli-win-x64-$env:VERSION.cdx.json" --output-format Json --spec-version 1.6 --set-name gclo-cli --set-version $env:VERSION --set-type Application --exclude-test-projects --disable-package-restore - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - dotnet CycloneDX gclo/gclo.csproj --output artifacts/sbom --filename "gclo-app-$env:VERSION.cdx.json" --output-format Json --spec-version 1.6 --set-name gclo --set-version $env:VERSION --set-type Application --exclude-test-projects --disable-package-restore - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Get-ChildItem artifacts/sbom - - # SHA256SUMS (K22-REL-04): one line per asset (' *'), so users - # and the Scoop autoupdate can verify downloads without computing hashes by - # hand. Fixed-name channel assets (the update feed) are listed too (K22-REL-03). - - name: Write SHA256SUMS - shell: pwsh - run: | - $files = @(Get-Item artifacts/gclo-cli-win-x64.zip) + @(Get-ChildItem artifacts/sbom -File) + @(Get-ChildItem Releases -File | Where-Object { $_.Extension -in '.exe', '.zip', '.nupkg', '.json' }) - $lines = $files | ForEach-Object { "{0} *{1}" -f (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant(), $_.Name } - [System.IO.File]::WriteAllText("artifacts/SHA256SUMS", ($lines -join "`n") + "`n", (New-Object System.Text.UTF8Encoding($false))) - Get-Content artifacts/SHA256SUMS - - # Provenance: a signed statement that THIS workflow run built these exact - # bytes, verifiable with `gh attestation verify -R KofTwentyTwo/gclo`. - # Not a code signature (K22 exception EX-0002, #57); it makes a release - # attributable to the workflow rather than only to whoever holds the account. - - name: Attest build provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 - with: - subject-path: | - artifacts/gclo-cli-win-x64.zip - artifacts/SHA256SUMS - artifacts/sbom/*.cdx.json - Releases/*.exe - Releases/*.zip - Releases/*.nupkg - - - name: Attest the CLI SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: artifacts/gclo-cli-win-x64.zip - sbom-path: artifacts/sbom/gclo-cli-win-x64-${{ needs.verify.outputs.version }}.cdx.json - - - name: Attest the app SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: | - Releases/*.exe - Releases/*.zip - Releases/*.nupkg - sbom-path: artifacts/sbom/gclo-app-${{ needs.verify.outputs.version }}.cdx.json - - # This repo has immutable releases: assets freeze the moment a release is - # PUBLISHED. So vpk creates a DRAFT (no --publish), every asset and the - # changelog land while it is still mutable, and publishing happens last. - - name: Velopack upload (creates a draft GitHub Release) - shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - $preFlag = @() - if ($env:PRERELEASE -eq 'true') { $preFlag = @('--pre') } - vpk upload github --repoUrl $env:REPO_URL --token $env:GITHUB_TOKEN --channel $env:CHANNEL --releaseName "gclo $env:VERSION" --tag $env:GITHUB_REF_NAME @preFlag - - - name: Attach the CLI zip, checksums, and SBOMs to the draft release - env: - GH_TOKEN: ${{ github.token }} - run: gh release upload $env:GITHUB_REF_NAME artifacts/gclo-cli-win-x64.zip artifacts/SHA256SUMS (Get-ChildItem artifacts/sbom -File).FullName --clobber - - # Release notes from the Conventional Commit titles merged since the previous - # release, breaking changes and security fixes first (K22-REL-02). - - name: Generate release notes - env: - GH_TOKEN: ${{ github.token }} - REPOSITORY: ${{ github.repository }} - run: | - ./.github/scripts/New-ReleaseNotes.ps1 -Tag $env:GITHUB_REF_NAME -Repository $env:REPOSITORY -OutputPath notes.md - Get-Content notes.md - gh release edit $env:GITHUB_REF_NAME --notes-file notes.md - - - name: Publish the completed release - env: - GH_TOKEN: ${{ github.token }} - run: gh release edit $env:GITHUB_REF_NAME --draft=false - - # ---------------------------------------------------------------------- - # (c) NuGet: pack gclo.Engine and push to nuget.org - only when the - # NUGET_API_KEY secret is configured on the repository. - # ---------------------------------------------------------------------- - # ContinuousIntegrationBuild normalizes the embedded source paths so the - # package is deterministic and SourceLink step-through works; the .snupkg - # beside it carries the symbols. - - name: Pack gclo.Engine - run: dotnet pack gclo.Engine -c Release -p:Version=$env:VERSION -p:ContinuousIntegrationBuild=true -p:RestoreLockedMode=true -o artifacts/nuget - - # The secrets context is not available in a step's `if`, so one step turns - # "is it configured?" into outputs; it is the only step besides the two - # publishers that sees the values, and it never prints them. - - name: Check which optional publishing secrets are configured - id: secrets - shell: pwsh - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} + DEPS_PAT: ${{ secrets.DEPS_PAT }} CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} run: | - Add-Content -Path $env:GITHUB_OUTPUT -Value ("nuget=" + (-not [string]::IsNullOrEmpty($env:NUGET_API_KEY)).ToString().ToLower()) - Add-Content -Path $env:GITHUB_OUTPUT -Value ("winget=" + (-not [string]::IsNullOrEmpty($env:WINGET_TOKEN)).ToString().ToLower()) - Add-Content -Path $env:GITHUB_OUTPUT -Value ("choco=" + (-not [string]::IsNullOrEmpty($env:CHOCO_API_KEY)).ToString().ToLower()) + $missing = @('DEPS_PAT', 'CHOCO_API_KEY') | Where-Object { [string]::IsNullOrEmpty((Get-Item -Path "env:$_" -ErrorAction SilentlyContinue).Value) } + if ($missing) + { + Write-Output "::error::Missing in the release environment: $($missing -join ', '). Add them (docs/RELEASING.md) and re-run this job." + exit 1 + } - - name: Push gclo.Engine to nuget.org - if: steps.secrets.outputs.nuget == 'true' - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: dotnet nuget push "artifacts/nuget/*.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate - - # ---------------------------------------------------------------------- - # (d) winget: update the KofTwentyTwo.gclo manifest for STABLE releases - # only, and only when the WINGET_TOKEN secret is configured. - # - # NOTE: the FIRST winget submission cannot be automated with `update` - - # the package must already exist in microsoft/winget-pkgs. Bootstrap it - # once manually with `wingetcreate new ` (see - # docs/RELEASING.md), then this step keeps it current. - # ---------------------------------------------------------------------- - - name: Submit winget manifest update - if: needs.verify.outputs.prerelease == 'false' && steps.secrets.outputs.winget == 'true' + - name: Download the CLI zip from the published release shell: pwsh env: - WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} + GH_TOKEN: ${{ github.token }} run: | - # A pinned release asset, verified against its committed SHA-256, instead - # of whatever https://aka.ms/wingetcreate/latest redirects to today. - $url = "https://github.com/microsoft/winget-create/releases/download/$env:WINGETCREATE_VERSION/wingetcreate.exe" - Invoke-WebRequest $url -OutFile wingetcreate.exe - $actual = (Get-FileHash wingetcreate.exe -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $env:WINGETCREATE_SHA256) { - Write-Error "wingetcreate.exe hash mismatch: expected $env:WINGETCREATE_SHA256, got $actual" - exit 1 - } - # Velopack names the installer '--Setup.exe'. - $setupUrl = "$env:REPO_URL/releases/download/$env:GITHUB_REF_NAME/gclo-stable-Setup.exe" - .\wingetcreate.exe update KofTwentyTwo.gclo --urls $setupUrl --version $env:VERSION --submit --token $env:WINGET_TOKEN + New-Item -ItemType Directory -Force -Path artifacts | Out-Null + gh release download $env:TAG --pattern "gclo-cli-win-x64-$env:K22_VERSION.zip" --dir artifacts + Get-ChildItem artifacts - # ---------------------------------------------------------------------- - # (e) Scoop + Chocolatey (#9), STABLE releases only. The repository is its - # own Scoop bucket (bucket/gclo.json) and ships the Chocolatey package - # source (packaging/chocolatey). Both are pointed at this release's CLI - # zip; the Chocolatey package is pushed when CHOCO_API_KEY is configured, - # and the updated manifests are proposed as a pull request against main, - # where Scoop reads bucket/gclo.json from. (With only GITHUB_TOKEN the PR - # triggers no checks; DEPS_PAT fixes that, see #7.) - # ---------------------------------------------------------------------- - name: Point the Scoop and Chocolatey manifests at this release - if: needs.verify.outputs.prerelease == 'false' id: manifests shell: pwsh - run: ./.github/scripts/Update-PackageManifests.ps1 -Version $env:VERSION -CliZipPath artifacts/gclo-cli-win-x64.zip + run: ./.github/scripts/Update-PackageManifests.ps1 -Version $env:K22_VERSION -CliZipPath "artifacts/gclo-cli-win-x64-$env:K22_VERSION.zip" - name: Pack the Chocolatey package - if: needs.verify.outputs.prerelease == 'false' shell: pwsh run: | New-Item -ItemType Directory -Force artifacts/chocolatey | Out-Null @@ -356,31 +168,29 @@ jobs: Get-ChildItem artifacts/chocolatey - name: Push the Chocolatey package - if: needs.verify.outputs.prerelease == 'false' && steps.secrets.outputs.choco == 'true' shell: pwsh env: CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} run: choco push (Get-ChildItem artifacts/chocolatey/*.nupkg | Select-Object -First 1).FullName --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY - name: Open a PR with the updated package manifests - if: needs.verify.outputs.prerelease == 'false' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - token: ${{ secrets.DEPS_PAT || github.token }} + token: ${{ secrets.DEPS_PAT }} base: main - branch: packaging/v${{ needs.verify.outputs.version }} + branch: packaging/v${{ needs.release.outputs.version }} delete-branch: true - commit-message: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.verify.outputs.version }}' - title: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.verify.outputs.version }}' + commit-message: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.release.outputs.version }}' + title: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.release.outputs.version }}' body: | Generated by the Release workflow for tag `${{ github.ref_name }}`. - `bucket/gclo.json` (Scoop) and `packaging/chocolatey` now point at - `gclo-cli-win-x64.zip` of v${{ needs.verify.outputs.version }} + `gclo-cli-win-x64-${{ needs.release.outputs.version }}.zip` (SHA-256 `${{ steps.manifests.outputs.sha256 }}`). - `scoop update gclo` picks this up as soon as this PR is merged (Scoop - reads the manifest from `main`); the Chocolatey package - was ${{ steps.secrets.outputs.choco == 'true' && 'pushed to chocolatey.org' || 'packed but not pushed (CHOCO_API_KEY not configured)' }}. + reads the manifest from `main`); the Chocolatey package was pushed to + chocolatey.org. Relates to #9. labels: | @@ -389,3 +199,87 @@ jobs: bucket/gclo.json packaging/chocolatey/gclo.nuspec packaging/chocolatey/tools/chocolateyinstall.ps1 + + # winget, stable releases only, once the package exists. `wingetcreate update` + # can only update a package that already exists in microsoft/winget-pkgs; the + # first submission is manual (docs/RELEASING.md), after which WINGET_PACKAGE_ID + # is set and this job keeps the manifest current (re-run it on the release that + # was bootstrapped). The job lives here, not in the shared workflow, because its + # token belongs to this repository's `release` environment. + 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_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 }} + WINGETCREATE_VERSION: v1.12.13.0 + WINGETCREATE_SHA256: 24042bd37915805615e6cf969ac57c6439124c3fe85823327f5f3fb24bd9ffea + run: | + if ([string]::IsNullOrEmpty($env:WINGET_TOKEN)) + { + Write-Output '::error::WINGET_PACKAGE_ID is set but WINGET_TOKEN is missing from the release environment. Add it and re-run this job.' + exit 1 + } + # A pinned release asset, verified against its committed SHA-256, instead + # of whatever https://aka.ms/wingetcreate/latest redirects to today. + $wingetcreate = Join-Path $env:RUNNER_TEMP 'wingetcreate.exe' + Invoke-WebRequest -Uri "https://github.com/microsoft/winget-create/releases/download/$env:WINGETCREATE_VERSION/wingetcreate.exe" -OutFile $wingetcreate + $actual = (Get-FileHash -LiteralPath $wingetcreate -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $env:WINGETCREATE_SHA256) + { + Write-Output "::error::wingetcreate.exe hash mismatch: expected $env:WINGETCREATE_SHA256, got $actual" + exit 1 + } + & $wingetcreate update $env:WINGET_PACKAGE_ID --urls $env:SETUP_URL --version $env:K22_VERSION --submit --token $env:WINGET_TOKEN + + # gclo.Engine on nuget.org: opt-in through the repository variable NUGET_PUBLISH + # (the library has no published consumers yet). Once opted in, a missing key fails. + nuget: + name: NuGet (gclo.Engine) + needs: release + if: vars.NUGET_PUBLISH == 'true' + runs-on: windows-latest + timeout-minutes: 20 + environment: release + permissions: + contents: read + env: + K22_VERSION: ${{ needs.release.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + + # ContinuousIntegrationBuild normalizes the embedded source paths so the + # package is deterministic and SourceLink step-through works. + - name: Pack gclo.Engine + shell: pwsh + run: dotnet pack gclo.Engine -c Release "-p:Version=$env:K22_VERSION" -p:ContinuousIntegrationBuild=true -p:RestoreLockedMode=true -o artifacts/nuget + + - name: Push gclo.Engine to nuget.org + shell: pwsh + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + if ([string]::IsNullOrEmpty($env:NUGET_API_KEY)) + { + Write-Output '::error::NUGET_PUBLISH is true but NUGET_API_KEY is not in the release environment.' + exit 1 + } + dotnet nuget push "artifacts/nuget/*.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b80e53..73a7987 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,7 @@ pre-commit install --hook-type pre-commit --hook-type commit-msg | `codeql / analyze (csharp)`, `codeql / analyze (actions)` | No high-severity static analysis findings | | `build-test` | Zero-warning x64 build, both unit suites, 100% coverage gate | | `format` | `dotnet format --verify-no-changes --severity warn` and PSScriptAnalyzer | -| `ui-tests` | FlaUI end-to-end tests against the real `gclo.exe` (advisory until it is made required, #54) | +| `ui-tests` | FlaUI end-to-end tests against the real `gclo.exe`, required since 2026-10-09 (#54) | ## Sign-off (Developer Certificate of Origin) diff --git a/README.md b/README.md index 5362f98..17ccefb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ All builds are published on the [**GitHub Releases** page](https://github.com/Ko **Desktop app (GUI)** — download and run `gclo-stable-Setup.exe`. It installs per-user (no admin rights needed) and updates itself in-app via **Help > Check for updates**. A portable build (`gclo-stable-Portable.zip`) is also attached to each release; portable builds do not self-update. -**CLI** — download `gclo-cli-win-x64.zip` and unzip it anywhere. It is a self-contained single `gclo.exe` — no .NET runtime required. +**CLI** — download `gclo-cli-win-x64-.zip` and unzip it anywhere. It is a self-contained single `gclo.exe` — no .NET runtime required. Plain version tags (`v1.2.3`) are **stable** releases; pre-release tags (`v1.2.3-beta.1`) publish to the **dev** channel, and an installed app only ever updates within its own channel. @@ -50,18 +50,18 @@ choco install gclo ## Verifying a release -Every release asset is listed in `SHA256SUMS`, carries a build-provenance attestation, and ships with a CycloneDX SBOM (`gclo-cli-win-x64-.cdx.json`, `gclo-app-.cdx.json`): +Every release asset is listed in `SHA256SUMS`, carries a build-provenance attestation, and ships with a CycloneDX SBOM (`gclo-.cdx.json` for the app, `gclo-cli-.cdx.json` for the CLI). Releases are built by the [KofTwentyTwo standards' shared release workflow](https://github.com/KofTwentyTwo/standards/blob/main/.github/workflows/release-velopack.yml), so the provenance names that workflow as the builder: ```powershell # 1. Integrity: the file matches the published checksum (Get-FileHash .\gclo-stable-Setup.exe -Algorithm SHA256).Hash # compare with the line in SHA256SUMS sha256sum --check --ignore-missing SHA256SUMS # Linux / macOS -# 2. Origin: built by this repository's release workflow from the tagged commit -gh attestation verify .\gclo-stable-Setup.exe --repo KofTwentyTwo/gclo +# 2. Origin: built from this repository's tagged commit by the shared release workflow +gh attestation verify .\gclo-stable-Setup.exe --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards # 3. Dependencies: the SBOM is attested too -gh attestation verify .\gclo-cli-win-x64.zip --repo KofTwentyTwo/gclo --predicate-type https://cyclonedx.org/bom +gh attestation verify .\gclo-cli-win-x64-.zip --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards --predicate-type https://cyclonedx.org/bom ``` The binaries are not yet Authenticode-signed (standards exception [EX-0002](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0002), [#57](https://github.com/KofTwentyTwo/gclo/issues/57)), so step 2 is the origin check until `Get-AuthenticodeSignature` can be. diff --git a/SECURITY.md b/SECURITY.md index 6271e49..14bfe7e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -52,7 +52,7 @@ MAJOR's last MINOR receives security fixes for 6 months. There are no maintenanc branches beyond that. Installed desktop builds update in place via **Help → Check for updates**; the CLI is -updated by downloading the latest `gclo-cli-win-x64.zip` from the +updated by downloading the latest `gclo-cli-win-x64-.zip` from the [releases page](https://github.com/KofTwentyTwo/gclo/releases) or through Scoop. ## Published vulnerabilities diff --git a/bucket/gclo.json b/bucket/gclo.json index 2aa16f2..ddfe031 100644 --- a/bucket/gclo.json +++ b/bucket/gclo.json @@ -18,7 +18,7 @@ "autoupdate": { "architecture": { "64bit": { - "url": "https://github.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64.zip", + "url": "https://github.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64-$version.zip", "hash": { "url": "$baseurl/SHA256SUMS" } diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 566bcc8..d81d175 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,9 +1,12 @@ # Releasing gclo -Releases are fully automated by [`.github/workflows/release.yml`](../.github/workflows/release.yml). -Pushing a tag that starts with `v` builds, tests, packages, and publishes -everything. This document explains how versioning works, what gets published -where, and how to cut a release start to finish. +Releases are automated by [`.github/workflows/release.yml`](../.github/workflows/release.yml), +which calls the KofTwentyTwo standards' shared +[`release-velopack.yml`](https://github.com/KofTwentyTwo/standards/blob/main/.github/workflows/release-velopack.yml) +for the build (ADR [0007](adr/0007-shared-velopack-release-workflow.md)). Pushing a +tag that starts with `v` builds, tests, packages, and publishes everything. This +document explains how versioning works, what gets published where, what must be +configured for a release to succeed, and how to cut one start to finish. ## Versioning and channels @@ -32,41 +35,49 @@ its own channel: - Users who installed from a **dev** release (`gclo-dev-Setup.exe`) get every prerelease you tag. -Velopack asset names include the channel. A stable release `v1.2.3` carries: +A stable release `v1.2.3` carries: - `gclo-stable-Setup.exe` — the installer - `gclo-stable-Portable.zip` — portable build - `gclo-1.2.3-stable-full.nupkg` — full update package - `gclo-1.2.3-stable-delta.nupkg` — delta from the previous stable release (absent on the first release of a channel) -- `gclo-cli-win-x64.zip` — self-contained single-file CLI +- `releases.stable.json` and `assets.stable.json` — the Velopack feed, limited to + this build +- `gclo-cli-win-x64-1.2.3.zip` — self-contained single-file CLI, with the LICENSE +- `gclo-1.2.3.cdx.json`, `gclo-cli-1.2.3.cdx.json` — CycloneDX SBOMs +- `SHA256SUMS` — one line per asset -The workflow runs `vpk download github` before packing so deltas can be -generated against the previous release on the same channel; on the very first -release of a channel that step finds nothing and is allowed to fail — you just -get a release without a delta package, which is normal. +The shared workflow downloads the previous release on the same channel before +packing so deltas can be generated; on the very first release of a channel that +finds nothing, and the release simply has no delta package. -## What gets published, and which secrets enable it +## What gets published, and what it needs -| Target | Condition | Secret | -| ------ | --------- | ------ | -| GitHub Release (Setup, portable, full/delta packages, CLI zip) | always | none — the built-in `GITHUB_TOKEN` is enough | -| nuget.org (`gclo.Engine` package) | `NUGET_API_KEY` secret is set | `NUGET_API_KEY` — an API key from nuget.org with push rights for `gclo.Engine` | -| winget (`KofTwentyTwo.gclo`) | stable releases only, and `WINGET_TOKEN` secret is set | `WINGET_TOKEN` — a GitHub personal access token (classic, `public_repo` scope) used by `wingetcreate` to fork/PR [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) | -| `SHA256SUMS` and CycloneDX SBOMs on the GitHub Release | always | none | -| Scoop (`bucket/gclo.json` in this repository) | stable releases only | none — the workflow opens a PR against `main` with the updated manifest; merge it and `scoop update gclo` sees the release | -| Chocolatey (`gclo` on chocolatey.org) | stable releases only, and `CHOCO_API_KEY` secret is set | `CHOCO_API_KEY` — an API key from your chocolatey.org account; the first push creates the package (it then goes through Chocolatey moderation) | +Everything a publishing job needs lives in the **`release` environment** +(Settings → Environments → `release`, deployment rule: tags matching `v*`) or in +repository variables. **A job whose token is missing fails** — a green run means +every enabled channel was published. A failed publisher can be re-run on its own +after the token is added ("Re-run failed jobs"); the release itself is not rebuilt. +winget is the one channel switched on explicitly (`WINGET_PACKAGE_ID`), because +its first submission cannot be automated; see below. -If a secret is not configured, the corresponding step is skipped cleanly — the -release still succeeds. Configure secrets under -**Settings → Secrets and variables → Actions**. +| Job | Runs for | Needs | Publishes | +| --- | -------- | ----- | --------- | +| `release` (shared workflow) | every tag | nothing beyond `GITHUB_TOKEN` | the GitHub Release: Setup, portable, full/delta packages, feed, CLI zip, SBOMs, `SHA256SUMS`, provenance and SBOM attestations | +| `packages` | stable only | `DEPS_PAT` (fine-grained token: contents and pull requests, write, this repository, ≤ 90 days), `CHOCO_API_KEY` (chocolatey.org) | the Chocolatey package pushed to chocolatey.org, and a `build(packaging): …` pull request against `main` with `bucket/gclo.json` and `packaging/chocolatey` pointed at the release | +| `winget` | stable only, once the repository variable `WINGET_PACKAGE_ID` is set (`KofTwentyTwo.gclo`, after the one-time manual submission below) | `WINGET_TOKEN` (fine-grained token that can fork and open pull requests on public repositories, ≤ 90 days); missing → the job fails | a manifest update PR on [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) | +| `nuget` | only when the repository variable `NUGET_PUBLISH` is `true` | `NUGET_API_KEY` (nuget.org, push rights for `gclo.Engine`) | `gclo.Engine` on nuget.org | + +Why `DEPS_PAT`: a pull request opened with the default `GITHUB_TOKEN` triggers no +status checks, so the packaging PR could never satisfy `main`'s required checks. ## First winget submission (one-time, manual) `wingetcreate update` can only update a package that already exists in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). The first -submission must be done by hand, once, after your first stable GitHub release -exists: +submission is done by hand, once, after the first stable GitHub release exists +(until `WINGET_PACKAGE_ID` is set the `winget` job does not run): ```powershell # From any Windows machine: @@ -74,31 +85,29 @@ Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe # Interactive: prompts for package id (use KofTwentyTwo.gclo), publisher, # license, description, etc., then opens a PR against microsoft/winget-pkgs. -.\wingetcreate.exe new https://github.com/KofTwentyTwo/gclo/releases/download/v1.0.0/gclo-stable-Setup.exe --token +.\wingetcreate.exe new https://github.com/KofTwentyTwo/gclo/releases/download/v1.2.3/gclo-stable-Setup.exe --token ``` -Once that first PR is merged and the package is live, set the `WINGET_TOKEN` -secret and every subsequent stable release updates the manifest automatically -via `wingetcreate update KofTwentyTwo.gclo ... --submit`. +Once that first PR is merged and the package is live, set the repository variable +`WINGET_PACKAGE_ID=KofTwentyTwo.gclo` and the `WINGET_TOKEN` secret. Re-running the +`winget` job of the bootstrapped release is not needed (it is already in winget); +every later stable release updates the manifest automatically. ## Scoop and Chocolatey -Both install the same asset, `gclo-cli-win-x64.zip`, by URL and SHA-256. +Both install the same asset, `gclo-cli-win-x64-.zip`, by URL and SHA-256. - **Scoop** needs no account: this repository is its own bucket. Users run `scoop bucket add gclo https://github.com/KofTwentyTwo/gclo` once, then `scoop install gclo`. The manifest (`bucket/gclo.json`) carries `checkver` and `autoupdate` entries keyed on `SHA256SUMS`, so `scoop update` works as - soon as the manifest on `main` names the new version. The release workflow - therefore opens its "Packaging" pull request against `main` (three generated - files, nothing else). Note that a PR opened with the default `GITHUB_TOKEN` - triggers no status checks, so `DEPS_PAT` should be configured (#7) for the - required checks to run on it. + soon as the manifest on `main` names the new version — which is why the + `packages` job opens its pull request against `main` (three generated files, + nothing else). Merge it. - **Chocolatey** needs a one-time account on chocolatey.org and its API key as - the `CHOCO_API_KEY` secret. The workflow packs `packaging/chocolatey` on every - stable release and pushes it when the key exists; the first version goes - through Chocolatey's moderation queue (expect a few days), later versions are - usually auto-approved. Users then run `choco install gclo`. + `CHOCO_API_KEY`. The first version goes through Chocolatey's moderation queue + (expect a few days); later versions are usually auto-approved. Users then run + `choco install gclo`. - To re-point the manifests by hand (for example after a missed release): `./.github/scripts/Update-PackageManifests.ps1 -Version 1.2.3 -CliZipPath `. @@ -109,94 +118,75 @@ Both install the same asset, `gclo-cli-win-x64.zip`, by URL and SHA-256. titled `Release vX.Y.Z`, and work through it: the conformance checker (`tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/gclo`, attach the output), open scanning alerts, and for a MINOR or MAJOR the threat-model - review (`docs/security/threat-model.md`). The release notes link the issue. + review (`docs/security/threat-model.md`). -2. **Make sure `main` is green.** CI, the security scans, and CodeQL must all - pass. The release workflow re-runs the CI gates against the tag, so a red - `main` will fail the release anyway, just later. +2. **Make sure `main` is green** and the `release` environment holds every + token the table above lists for the kind of release you are cutting. -3. **Pick the version.** Follow semver: breaking change → major, new feature → - minor, fix → patch. Add a prerelease suffix (`-beta.1`) if this should go to - the `dev` channel only. +3. **Rehearse on a prerelease.** Tag `vX.Y.Z-beta.N` first: it runs the whole + pipeline except the stable-only publishers, on the `dev` channel. Install + `gclo-dev-Setup.exe` on a clean machine, run a Quick Sync, and let an + installed earlier prerelease self-update to it. Only then tag the stable + version. -4. **Tag and push** (the `protect-release-tags` ruleset lets only the +4. **Pick the version.** Follow semver: breaking change → major, new feature → + minor, fix → patch. + +5. **Tag and push** (the `protect-release-tags` ruleset lets only the repository admin create a `v*` tag): ```powershell git checkout main git pull - git tag v1.2.3 # or v1.2.3-beta.1 for a dev-channel prerelease + git tag -s v1.2.3 # or v1.2.3-beta.1 for a dev-channel prerelease git push origin v1.2.3 ``` -5. **Watch the workflow.** The `Release` workflow appears under the Actions - tab. It runs three jobs, each gated on the previous one: - - **Verify tag** — validate the tag, derive version/channel, and check - that the tagged commit is contained in `main`. A tag on any other commit - fails here and nothing is built or published. - - **Gates** — the full `ci` workflow, exactly as a pull request runs it: - x64 build with warnings as errors, 100% line coverage on - `gclo.Engine` / `gclo.ViewModels` / `gclo`, and the formatting gate. - The FlaUI UI smoke suite runs as well but is **advisory** for a release, - matching its status on `main` (not a required check yet, because UI - automation on hosted runners is still proving itself). A red UI job - shows in the run but does not stop publishing; look at it afterwards. - If a *blocking* gate fails, fix the cause on `main` and cut a new patch - version — never reuse the tag (see "If a release goes wrong"). - - **Build, package, and publish**, which will, in order: - - publish and zip the CLI (with the LICENSE inside), - - publish the WinUI app unpackaged and pack it with Velopack, - - generate a CycloneDX SBOM per artifact, write `SHA256SUMS`, and attest - provenance and SBOMs, - - create a **draft** GitHub Release (marked prerelease for `dev`), upload - all assets and the notes generated from the Conventional Commit titles - since the previous release (`.github/scripts/New-ReleaseNotes.ps1`), then - **publish** it — this repository uses immutable releases, so everything - must land while the release is still a draft, - - push `gclo.Engine` to nuget.org (if `NUGET_API_KEY` is set), - - submit the winget manifest update (stable only, if `WINGET_TOKEN` is set). +6. **Watch the workflow** under the Actions tab: + - **Verify tag** — the tagged commit must be contained in `main`. A tag on + any other commit fails here and nothing is built or published. + - **Gates** — the full `ci` workflow, exactly as a pull request runs it, + including the FlaUI end-to-end suite. If a gate fails, fix the cause on + `main` and cut a new patch version; never reuse the tag. + - **Release** (shared workflow): locked restore, Release build with warnings + as errors, tests with the 100% coverage gate, publish, Velopack pack, SBOMs, + `SHA256SUMS`, provenance, a **draft** GitHub Release with every asset and + generated notes, then **publish** — releases are immutable, so everything + lands while the release is still a draft. + - **Scoop and Chocolatey**, **winget**, **NuGet** — the publishers described + above; each fails loudly when it cannot publish. -6. **Verify.** Check the new release on the - [releases page](https://github.com/KofTwentyTwo/gclo/releases): the Setup - exe, portable zip, full package, CLI zip (and a delta package after the - first release), `SHA256SUMS`, and the two SBOMs should all be attached; - `gh attestation verify` passes for each asset. Tick the "Ship" section of - the checklist issue. If the winget step ran, check your PR on - microsoft/winget-pkgs. +7. **Verify.** On the [releases page](https://github.com/KofTwentyTwo/gclo/releases) + every asset listed above is attached and + `gh attestation verify --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards` + passes. Merge the packaging PR. Check the winget PR on microsoft/winget-pkgs. + Tick the "Ship" section of the checklist issue. -7. **If something failed**, where it failed decides the recovery. The workflow - is a single job, so "re-run failed jobs" always re-runs everything from the - start — and because releases in this repository are **immutable**, a - published release's tag and assets are frozen and cannot be re-uploaded. +8. **If something failed**, where it failed decides the recovery: - - **Failed before "Publish the completed release"** (build, tests, - packaging, asset upload, changelog): nothing is public yet. Delete the - draft release if one was created — drafts can always be deleted: + - **In `verify`, `gates`, or inside `release` before "Publish release"**: + nothing is public. Delete the draft release if one was created + (`gh release delete v1.2.3 --yes`), fix the cause through a pull request, + and re-run the workflow from the Actions UI; the tag is unchanged and + everything is rebuilt from it. A fix that changes the shipped code needs a + new patch version instead. + - **In a publisher after the release was published**: the release is fine and + frozen. Add the missing token or variable and **re-run the failed job + only**; do not re-run the whole workflow, which would try to recreate the + release. The manual equivalents, should a job keep failing: ```powershell - gh release delete v1.2.3 --yes - ``` - - Then fix the cause and re-run the job from the Actions UI; the tag is - unchanged and everything is rebuilt from it. - - - **Failed after the release was published** (the NuGet push or the winget - submission): the release itself is fine but frozen — do **not** re-run - the job, which would try to recreate it. Run the missing step manually - instead: - - ```powershell - # NuGet push (needs an API key with push rights for gclo.Engine) - dotnet pack gclo.Engine -c Release -p:Version=1.2.3 -o artifacts/nuget - dotnet nuget push "artifacts/nuget/*.nupkg" --api-key --source https://api.nuget.org/v3/index.json --skip-duplicate + # Scoop + Chocolatey manifests, then push the package and open the PR by hand + ./.github/scripts/Update-PackageManifests.ps1 -Version 1.2.3 -CliZipPath .\gclo-cli-win-x64-1.2.3.zip + choco pack packaging/chocolatey/gclo.nuspec --outputdirectory artifacts + choco push artifacts\gclo.1.2.3.nupkg --source https://push.chocolatey.org/ --api-key # winget manifest update (stable releases only) - Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe .\wingetcreate.exe update KofTwentyTwo.gclo --urls https://github.com/KofTwentyTwo/gclo/releases/download/v1.2.3/gclo-stable-Setup.exe --version 1.2.3 --submit --token ``` - If the published release itself is broken, do not try to fix it in - place and do not delete it: see "A broken release" below. + - **The published release itself is broken**: do not fix it in place and do + not delete it; see "A broken release". ### A broken release diff --git a/docs/adr/0003-repository-as-scoop-bucket.md b/docs/adr/0003-repository-as-scoop-bucket.md index 024c8cc..d6e5f50 100644 --- a/docs/adr/0003-repository-as-scoop-bucket.md +++ b/docs/adr/0003-repository-as-scoop-bucket.md @@ -9,7 +9,7 @@ Users asked for package-manager installs of the CLI (#9). Scoop needs a bucket (a git repository with `bucket/.json`) read from the default branch; Chocolatey needs a chocolatey.org account, an API key, and moderation of the first push. Both -install the same release asset, `gclo-cli-win-x64.zip`, by URL and SHA-256, so their +install the same release asset, `gclo-cli-win-x64-.zip`, by URL and SHA-256, so their manifests must be updated on every stable release without hand edits. ## Options considered diff --git a/docs/adr/0007-shared-velopack-release-workflow.md b/docs/adr/0007-shared-velopack-release-workflow.md new file mode 100644 index 0000000..18b2fa2 --- /dev/null +++ b/docs/adr/0007-shared-velopack-release-workflow.md @@ -0,0 +1,46 @@ +# ADR 0007: Release through the standards' shared Velopack workflow + +- **Status:** Accepted +- **Date:** 2026-10-09 +- **Issue:** [#76](https://github.com/KofTwentyTwo/gclo/issues/76) + +## Context + +Until 1.0.1 the release pipeline built, packed, attested and published inside this +repository's `release.yml`. The KofTwentyTwo standards require the build to run in a +shared reusable workflow (K22-CI-30) so the provenance attestation names a workflow +the calling repository cannot edit (SLSA Build L3); gclo carried exception EX-0004 +until that workflow existed. It now does: `release-velopack.yml` in +KofTwentyTwo/standards builds, tests, gates coverage, publishes, packs with Velopack, +generates SBOMs with Syft, writes `SHA256SUMS`, attests, and publishes the release. + +Publishing to Scoop, Chocolatey, winget and (optionally) NuGet is specific to gclo, +and the previous workflow skipped any of them whose secret was missing, so a green +run did not mean every channel had been published. + +## Decision + +- `release.yml` keeps only what is gclo's: the "tag is on main" check, the `ci` + gates (K22-CI-31), the call to the shared workflow pinned to a commit, and the + caller-side publishing jobs. +- Publishing jobs run in the `release` environment, which only `v*` tags can use, + and **fail** when a token or variable they need is absent. NuGet publishing is an + explicit opt-in (`NUGET_PUBLISH` variable) because gclo.Engine has no consumers + yet; once opted in, a missing key fails too. +- Asset names follow the shared workflow: the CLI zip is + `gclo-cli-win-x64-.zip`, SBOMs are `gclo-.cdx.json` and + `gclo-cli-.cdx.json`, and release notes are GitHub's generated notes. + The Scoop and Chocolatey manifests, their updater script, and the tests follow. +- The LICENSE ships inside the published app and CLI through the project files, + since the shared workflow does not add it. + +## Consequences + +- The provenance's signer is `KofTwentyTwo/standards`; verification commands pass + `--signer-repo KofTwentyTwo/standards` (README, "Verifying a release"). +- Exception EX-0004 closes after the first successful release on the shared + workflow; the first winget submission is still manual (`wingetcreate new`). +- Jobs are separate, so a failed publisher can be re-run alone after its token is + added, without rebuilding or touching the immutable release. +- Changes to the build itself happen in the standards repository and reach gclo by + moving the pin. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index f434fb2..b7689e7 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -93,7 +93,7 @@ flowchart LR | T8 | GitHub API / remotes | Spoofing | A MITM impersonates GitHub | Low | Critical | HTTPS with system trust store (Octokit, libgit2); no HTTP fallback | Mitigated | | T9 | WSL recovery | Elevation of privilege | Hostile org/repo names injected into the shell script | Low | High | Script fed on stdin with positional parameters; names validated as repository names first; no string interpolation into the script | Mitigated | | T10 | Logs and crash output | Information disclosure | Exception text carries a token or private path | Low | Medium | Crash net logs messages only; log review in incident handling; tokens never in exception messages (tests) | Mitigated | -| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release | Low | Critical | SHA-pinned actions, no caches in the release job, read-only tokens, OIDC attestations, `protect-release-tags`, required gates re-run on the tag | Mitigated | +| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release | Low | Critical | Build runs in the standards' shared release workflow (SLSA Build L3 provenance, `--signer-repo KofTwentyTwo/standards`), SHA-pinned actions, no caches, read-only tokens, publishing tokens only in the tag-restricted `release` environment, `protect-release-tags`, required gates re-run on the tag | Mitigated | | T12 | Repository | Repudiation | Unattributed change reaches `main` | Low | High | Signed, signed-off commits; squash-only through gated pull requests; no bypass actors | Mitigated | | T13 | CLI / data directory | Tampering | A malicious `accounts.json` or `settings.json` redirects targets | Low | Medium | Files live under the user profile; sanitized on load; target paths resolved and contained | Accepted (same-user boundary) | @@ -110,3 +110,4 @@ flowchart LR | Date | Version | Reviewer | Changes | | --- | --- | --- | --- | | 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | Initial model, covering the path-recovery, WSL clone, vault scoping, package-manager, and SBOM changes of 1.0.1 | +| 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | T11 updated: releases build in the shared standards workflow; publishing tokens moved to the `release` environment (#76) | diff --git a/gclo.Cli.Tests/PackagingManifestTests.cs b/gclo.Cli.Tests/PackagingManifestTests.cs index 64d2cc9..688286a 100644 --- a/gclo.Cli.Tests/PackagingManifestTests.cs +++ b/gclo.Cli.Tests/PackagingManifestTests.cs @@ -38,6 +38,18 @@ public sealed class PackagingManifestTests + /// + /// The release asset the manifests install: releases up to 1.0.0 shipped it as + /// gclo-cli-win-x64.zip, the shared release workflow names it with the version. + /// + private static string CliZipUrlPattern(string version) + { + string v = Regex.Escape(version); + return $@"^https://github\.com/KofTwentyTwo/gclo/releases/download/v{v}/gclo-cli-win-x64(-{v})?\.zip$"; + } + + + private static (string Version, string Url, string Hash) ReadScoop() { using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(RepoRoot, "bucket", "gclo.json"))); @@ -47,7 +59,7 @@ public sealed class PackagingManifestTests Assert.Equal("MIT", root.GetProperty("license").GetString()); Assert.Equal("github", root.GetProperty("checkver").GetString()); string autoupdateUrl = root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit").GetProperty("url").GetString()!; - Assert.Contains("v$version/gclo-cli-win-x64.zip", autoupdateUrl); + Assert.Contains("v$version/gclo-cli-win-x64-$version.zip", autoupdateUrl); Assert.EndsWith("/SHA256SUMS", root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit").GetProperty("hash").GetProperty("url").GetString()); return (root.GetProperty("version").GetString()!, x64.GetProperty("url").GetString()!, x64.GetProperty("hash").GetString()!); } @@ -78,7 +90,7 @@ public sealed class PackagingManifestTests (string? version, string? url, string? hash) = ReadScoop(); Assert.Matches(@"^\d+\.\d+\.\d+$", version); // package managers get stable releases only - Assert.Equal($"https://github.com/KofTwentyTwo/gclo/releases/download/v{version}/gclo-cli-win-x64.zip", url); + Assert.Matches(CliZipUrlPattern(version), url); Assert.Matches(s_sha256, hash); } @@ -90,7 +102,7 @@ public sealed class PackagingManifestTests (string? version, string? url, string? hash) = ReadChocolatey(); Assert.Matches(@"^\d+\.\d+\.\d+$", version); - Assert.Equal($"https://github.com/KofTwentyTwo/gclo/releases/download/v{version}/gclo-cli-win-x64.zip", url); + Assert.Matches(CliZipUrlPattern(version), url); Assert.Matches(s_sha256, hash); } diff --git a/gclo.Cli/gclo.Cli.csproj b/gclo.Cli/gclo.Cli.csproj index 4c029e4..81e1fd3 100644 --- a/gclo.Cli/gclo.Cli.csproj +++ b/gclo.Cli/gclo.Cli.csproj @@ -6,6 +6,8 @@ win-x64 + + true enable enable gclo @@ -24,4 +26,9 @@ + + + + + diff --git a/gclo.Cli/packages.lock.json b/gclo.Cli/packages.lock.json index 7465a57..aa8fff0 100644 --- a/gclo.Cli/packages.lock.json +++ b/gclo.Cli/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "3.0.296", "contentHash": "1Yp/6OK/Ev66hE2YLe9UvIgtJrKOX/xvoGDe0UbdtuG2ZxUoDoyONdEn+HWCDuy02Mu8SmglHEd/9SGPrM1qYg==" }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, "LibGit2Sharp.NativeBinaries": { "type": "Transitive", "resolved": "2.0.324", diff --git a/gclo.Engine/gclo.Engine.csproj b/gclo.Engine/gclo.Engine.csproj index 779685b..7bb4d37 100644 --- a/gclo.Engine/gclo.Engine.csproj +++ b/gclo.Engine/gclo.Engine.csproj @@ -1,6 +1,14 @@ + + win-x64 + true net10.0 enable enable diff --git a/gclo.Engine/packages.lock.json b/gclo.Engine/packages.lock.json index c4fe9b0..a01a3d6 100644 --- a/gclo.Engine/packages.lock.json +++ b/gclo.Engine/packages.lock.json @@ -17,6 +17,12 @@ "resolved": "3.0.296", "contentHash": "1Yp/6OK/Ev66hE2YLe9UvIgtJrKOX/xvoGDe0UbdtuG2ZxUoDoyONdEn+HWCDuy02Mu8SmglHEd/9SGPrM1qYg==" }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, "Octokit": { "type": "Direct", "requested": "[14.0.0, )", @@ -28,6 +34,13 @@ "resolved": "2.0.324", "contentHash": "5YZVGNuO9nYFQfzRQGPGY9fdpSsABwlF2jSaZ7ydxCfjvt5Krbj/eHbXkvvgAQzp4lkjvgDrlAuuI4KTmqhDlA==" } + }, + "net10.0/win-x64": { + "LibGit2Sharp.NativeBinaries": { + "type": "Transitive", + "resolved": "2.0.324", + "contentHash": "5YZVGNuO9nYFQfzRQGPGY9fdpSsABwlF2jSaZ7ydxCfjvt5Krbj/eHbXkvvgAQzp4lkjvgDrlAuuI4KTmqhDlA==" + } } } } \ No newline at end of file diff --git a/gclo.ViewModels/gclo.ViewModels.csproj b/gclo.ViewModels/gclo.ViewModels.csproj index 1392ecb..5c154ee 100644 --- a/gclo.ViewModels/gclo.ViewModels.csproj +++ b/gclo.ViewModels/gclo.ViewModels.csproj @@ -9,6 +9,14 @@ + + win-x64 + true net10.0 enable enable diff --git a/gclo.ViewModels/packages.lock.json b/gclo.ViewModels/packages.lock.json index 05d9365..b4298fe 100644 --- a/gclo.ViewModels/packages.lock.json +++ b/gclo.ViewModels/packages.lock.json @@ -14,6 +14,12 @@ "resolved": "3.0.296", "contentHash": "1Yp/6OK/Ev66hE2YLe9UvIgtJrKOX/xvoGDe0UbdtuG2ZxUoDoyONdEn+HWCDuy02Mu8SmglHEd/9SGPrM1qYg==" }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, "LibGit2Sharp.NativeBinaries": { "type": "Transitive", "resolved": "2.0.324", @@ -41,6 +47,13 @@ "resolved": "14.0.0", "contentHash": "jGOuTH1l+TCpJH+fwYOp7USzHDuGfN1jKbLz3J2COwyn+wL08eynvpnM6rY2qkzIEXum3PN2p2QkP3BW/p9Qcw==" } + }, + "net10.0/win-x64": { + "LibGit2Sharp.NativeBinaries": { + "type": "Transitive", + "resolved": "2.0.324", + "contentHash": "5YZVGNuO9nYFQfzRQGPGY9fdpSsABwlF2jSaZ7ydxCfjvt5Krbj/eHbXkvvgAQzp4lkjvgDrlAuuI4KTmqhDlA==" + } } } } \ No newline at end of file diff --git a/gclo/gclo.csproj b/gclo/gclo.csproj index 7fbc212..926ad71 100644 --- a/gclo/gclo.csproj +++ b/gclo/gclo.csproj @@ -29,6 +29,8 @@ build has no MSIX packaging step to carry them otherwise. --> + +