ci: adopt the standards' pull-request, security, CodeQL and Scorecard gates #68

Merged
KofTwentyTwo merged 2 commits from ci/standards-gates into main 2026-10-09 19:27:45 +00:00
24 changed files with 819 additions and 137 deletions

No files matched your search

+18 -14
View File
@@ -1,23 +1,27 @@
# Weekly dependency updates for the GitHub Actions pinned in .github/workflows.
# Dependabot (K22-DEP-20): keeps the SHA-pinned actions and the standards'
# reusable workflows current, weekly and grouped. The 7-day cooldown lets the
# community catch a compromised release before it reaches us; security updates
# are not delayed by it.
#
# NuGet packages are deliberately NOT handled here. Dependabot's NuGet updater
# cannot evaluate the WinUI project's Windows-versioned target framework
# NuGet is deliberately NOT handled here. Dependabot's NuGet updater cannot
# evaluate the WinUI project's Windows-versioned target framework
# (net10.0-windows10.0.19041.0; dependabot/dependabot-core#13923) and declares
# every newer package "not compatible", which also blocks the same packages in the
# other projects. The NuGet updates workflow (.github/workflows/nuget-updates.yml)
# does that job through `dotnet list package --outdated` instead. Dependabot
# security alerts for NuGet are unaffected: they come from the dependency graph.
# every newer package "not compatible", which also blocks the same packages in
# the other projects. .github/workflows/nuget-updates.yml does that job through
# `dotnet list package --outdated`, with the same cooldown (docs/adr/0004).
# Dependabot security alerts for NuGet are unaffected: they come from the
# dependency graph.
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
target-branch: dev
directory: /
schedule:
interval: weekly
day: monday
time: "06:00"
timezone: America/New_York
cooldown:
default-days: 7
groups:
actions-all:
patterns:
- "*"
actions:
patterns: ["*"]
commit-message:
prefix: ci
+289
View File
@@ -0,0 +1,289 @@
#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 <tag>" 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 = '^(?<type>[a-z]+)(\((?<scope>[^)]+)\))?(?<bang>!)?: (?<description>.+)$'
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 <tag>") 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"
+31 -2
View File
@@ -16,7 +16,9 @@
- minor and patch bumps are applied;
- major bumps are reported but NOT applied unless -IncludeMajor is given, so
they get a deliberate review;
- prerelease versions are never applied to a package that is on a stable one.
- prerelease versions are never applied to a package that is on a stable one;
- a version published fewer than -CooldownDays days ago is held back
(K22-DEP-20), so the ecosystem has time to catch a bad release first.
Versions are edited in place in each .csproj (the exact Include/Version pair),
preserving the file's encoding and line endings.
@@ -30,6 +32,10 @@
.PARAMETER IncludeMajor
Also apply major-version bumps.
.PARAMETER CooldownDays
Hold back versions published fewer than this many days ago. Default: 7.
When nuget.org cannot be asked for the publish date, the version is held.
.OUTPUTS
Writes `changed=true|false` and `report=<path>` to $env:GITHUB_OUTPUT when set.
Exit code 0 on success (whether or not anything changed), non-zero on error.
@@ -38,7 +44,8 @@
param(
[string]$Solution = 'gclo.slnx',
[string]$ReportPath = 'nuget-update-report.md',
[switch]$IncludeMajor
[switch]$IncludeMajor,
[int]$CooldownDays = 7
)
$ErrorActionPreference = 'Stop'
@@ -58,6 +65,18 @@ function Test-Prerelease([string]$version) {
return $version.Contains('-')
}
function Get-PublishedDate([string]$id, [string]$version) {
# nuget.org's registration leaf carries the publish timestamp of one version.
$url = "https://api.nuget.org/v3/registration5-gz-semver2/$($id.ToLowerInvariant())/$($version.ToLowerInvariant()).json"
try {
$leaf = Invoke-RestMethod -Uri $url -TimeoutSec 30
return [DateTimeOffset]::Parse($leaf.published, [System.Globalization.CultureInfo]::InvariantCulture)
}
catch {
return $null
}
}
Write-Host "Restoring $Solution"
dotnet restore $Solution | Out-Null
if ($LASTEXITCODE -ne 0) { throw "dotnet restore failed with exit code $LASTEXITCODE." }
@@ -107,6 +126,16 @@ foreach ($row in ($rows.Values | Sort-Object Project, Id)) {
elseif ($latest -le $current) {
$reason = 'not newer than the requested version'
}
elseif ($CooldownDays -gt 0) {
$published = Get-PublishedDate $row.Id $row.Latest
if ($null -eq $published) {
$reason = "publish date unavailable from nuget.org; held for the $CooldownDays-day cooldown"
}
elseif ($published -gt [DateTimeOffset]::UtcNow.AddDays(-$CooldownDays)) {
$age = [int][math]::Floor(([DateTimeOffset]::UtcNow - $published).TotalDays)
$reason = "published $age day(s) ago; waits out the $CooldownDays-day cooldown"
}
}
if ($reason) {
$held.Add([pscustomobject]@{ Project = $projectName; Id = $row.Id; From = $row.Requested; To = $row.Latest; Reason = $reason })
+10 -4
View File
@@ -7,7 +7,7 @@ name: CI
on:
push:
branches: [main, dev]
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
@@ -38,11 +38,13 @@ jobs:
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:
dotnet-version: 10.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
@@ -121,11 +123,13 @@ jobs:
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:
dotnet-version: 10.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
@@ -157,11 +161,13 @@ jobs:
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:
dotnet-version: 10.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
+24 -50
View File
@@ -1,59 +1,33 @@
# CodeQL static analysis for C#.
#
# C# is a compiled language, so CodeQL needs a real build. We use build-mode
# "manual" and run the same x64 solution build the CI workflow uses (without
# -warnaserror - the CI workflow already gates warnings; this workflow's job is
# security analysis, and it should still scan code that has a pending warning).
name: CodeQL
# Static analysis (K22-CI-22): CodeQL with the security-extended suite for C# and
# for the workflows themselves, failing on alerts of high severity or above,
# through the KofTwentyTwo standards' reusable workflow. C# is compiled, so the
# analysis builds the solution exactly as CI does (x64, locked restore); the .NET
# SDK comes from global.json.
name: codeql
# Runs on dev as well as main: feature PRs target dev (CONTRIBUTING.md), so
# scanning only main would let code reach the integration branch unanalyzed.
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
push:
branches: [main]
schedule:
# Weekly scan (Mondays 06:30 UTC) so new CodeQL queries run even when the
# code has not changed.
- cron: '30 6 * * 1'
- cron: "29 6 * * 1"
workflow_dispatch:
permissions:
contents: read
permissions: {}
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
name: Analyze (csharp)
runs-on: windows-latest
timeout-minutes: 120
codeql:
uses: KofTwentyTwo/standards/.github/workflows/codeql.yml@ae174f54959ba4297f2ae174356c4481918fdf44 # main 2026-10-09 (pre-1.0 draft, standards#4)
permissions:
# Required for github/codeql-action to upload scan results.
security-events: write
# Required to fetch public CodeQL packs.
packages: read
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
with:
languages: csharp
build-mode: manual
- name: Build solution (x64)
run: dotnet build gclo.slnx -p:Platform=x64 -p:RestoreLockedMode=true
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
with:
category: '/language:csharp'
security-events: write
with:
languages: '["csharp", "actions"]'
build-mode: manual
build-command: dotnet build gclo.slnx -p:Platform=x64 -p:RestoreLockedMode=true
runs-on: windows-latest
setup-dotnet: true
+6
View File
@@ -1,3 +1,7 @@
# Superseded by pr-gates.yml (`pr / dependency-review`, K22-CI-01); kept only
# until the protect-main ruleset lists the new required contexts (#66), then
# removed.
#
# Blocks pull requests that add dependencies with known vulnerabilities or with
# licenses outside the allowlist below. Only dependencies *added or changed* by
# the PR are checked (via the GitHub dependency graph diff), so existing
@@ -23,6 +27,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
+35 -25
View File
@@ -1,4 +1,4 @@
# Weekly NuGet dependency updates, as a pull request against dev.
# Weekly NuGet dependency updates, as a pull request against main.
#
# This replaces Dependabot's NuGet ecosystem for this repository. Dependabot's NuGet
# updater cannot evaluate Windows-versioned target frameworks (the WinUI project's
@@ -9,17 +9,18 @@
# another way". `dotnet list package --outdated` evaluates through real MSBuild and
# NuGet, so the script below drives the bump from that instead.
#
# Flow: bump minor/patch versions in the .csproj files -> run the same gates CI
# runs (zero-warning x64 build, both unit suites, format) -> open or update a single
# PR on branch deps/nuget-weekly with the report and the gate results in its body.
# Major bumps are listed in the report but not applied; run the workflow manually
# with include-major to take them.
# Flow: bump minor/patch versions in the .csproj files (skipping versions newer
# than the 7-day cooldown, K22-DEP-20) -> run the same gates CI runs (zero-warning
# x64 build, both unit suites, format) -> open or update a single PR on branch
# build/nuget-weekly with the report and the gate results in its body. Major bumps
# are listed in the report but not applied; run the workflow manually with
# include-major to take them. See docs/adr/0004-nuget-updates-workflow.md.
#
# Token note: with the default GITHUB_TOKEN, GitHub does not start other workflows
# for the PR this job creates, so the PR shows no CI checks of its own (the gates
# were run here, and dev -> main still goes through the required checks). To get
# checks on the PR itself, add a fine-grained PAT with contents + pull-requests
# write as the DEPS_PAT repository secret; the step below prefers it when present.
# were run here). To get checks on the PR itself, add a fine-grained PAT with
# contents + pull-requests write as the DEPS_PAT repository secret; the step below
# prefers it when present.
name: NuGet updates
on:
@@ -49,24 +50,27 @@ jobs:
contents: write
pull-requests: write
steps:
- name: Checkout dev
- name: Checkout main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: dev
ref: main
persist-credentials: false
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Bump package versions
id: bump
shell: pwsh
env:
INCLUDE_MAJOR: ${{ inputs.include-major }}
run: |
$params = @{ Solution = 'gclo.slnx'; ReportPath = 'nuget-update-report.md' }
if ('${{ inputs.include-major }}' -eq 'true') { $params.IncludeMajor = $true }
$params = @{ Solution = 'gclo.slnx'; ReportPath = 'nuget-update-report.md'; CooldownDays = 7 }
if ($env:INCLUDE_MAJOR -eq 'true') { $params.IncludeMajor = $true }
./.github/scripts/Update-NuGetPackages.ps1 @params
# The committed lock files must follow the bump, or CI's locked restore
@@ -99,11 +103,17 @@ jobs:
if: steps.bump.outputs.changed == 'true'
id: format
continue-on-error: true
run: dotnet format gclo.slnx --verify-no-changes --severity error
run: dotnet format gclo.slnx --verify-no-changes --severity warn --no-restore
- name: Compose PR body
if: steps.bump.outputs.changed == 'true'
shell: pwsh
env:
BUILD_OUTCOME: ${{ steps.build.outcome }}
ENGINE_TESTS_OUTCOME: ${{ steps.engine-tests.outcome }}
CLI_TESTS_OUTCOME: ${{ steps.cli-tests.outcome }}
FORMAT_OUTCOME: ${{ steps.format.outcome }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
function Mark($outcome) {
switch ($outcome) {
@@ -115,13 +125,13 @@ jobs:
$gates = @(
'### Gates (run in this workflow against the bumped tree)',
'',
"- Build (x64, warnings as errors): $(Mark '${{ steps.build.outcome }}')",
"- Engine + ViewModels tests: $(Mark '${{ steps.engine-tests.outcome }}')",
"- CLI tests: $(Mark '${{ steps.cli-tests.outcome }}')",
"- Format: $(Mark '${{ steps.format.outcome }}')",
"- Build (x64, warnings as errors): $(Mark $env:BUILD_OUTCOME)",
"- Engine + ViewModels tests: $(Mark $env:ENGINE_TESTS_OUTCOME)",
"- CLI tests: $(Mark $env:CLI_TESTS_OUTCOME)",
"- Format: $(Mark $env:FORMAT_OUTCOME)",
'',
'The UI smoke suite and the 100% coverage gate run when dev is merged to main.',
"Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
'The UI smoke suite, the 100% coverage gate, and the security scans run on the pull request.',
"Workflow run: $env:RUN_URL",
''
)
$report = Get-Content nuget-update-report.md -Raw
@@ -133,11 +143,11 @@ jobs:
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.DEPS_PAT || github.token }}
base: dev
branch: deps/nuget-weekly
base: main
branch: build/nuget-weekly
delete-branch: true
commit-message: 'Deps: weekly NuGet package updates'
title: 'Deps: weekly NuGet package updates'
commit-message: 'build(deps): weekly NuGet package updates'
title: 'build(deps): weekly NuGet package updates'
body-path: pr-body.md
labels: |
dependencies
+26
View File
@@ -0,0 +1,26 @@
# Pull request gates (K22-CI-01): Conventional Commit title, DCO sign-off on every
# commit, and review of added dependencies, through the KofTwentyTwo standards'
# reusable workflow. Pinned by full commit SHA like any other action (K22-CI-11);
# the comment names the standards revision. Dependabot's github-actions ecosystem
# updates the SHA and the comment together.
name: pr-gates
on:
pull_request:
permissions: {}
concurrency:
group: pr-gates-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr:
uses: KofTwentyTwo/standards/.github/workflows/pr.yml@ae174f54959ba4297f2ae174356c4481918fdf44 # main 2026-10-09 (pre-1.0 draft, standards#4)
permissions:
contents: read
pull-requests: read
with:
# These packages declare a license FILE in their nuspec, so the dependency
# graph reports NOASSERTION; see standards EX-0003.
allow-dependencies-licenses: pkg:nuget/LibGit2Sharp, pkg:nuget/Microsoft.WindowsAppSDK, pkg:nuget/Microsoft.Windows.SDK.BuildTools
+93 -37
View File
@@ -22,6 +22,10 @@ on:
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
verify:
name: Verify tag
@@ -36,6 +40,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Derive version from tag
id: version
@@ -75,8 +80,9 @@ jobs:
exit 1
fi
# The same gates a pull request must pass: zero-warning x64 build, 100% line
# coverage on Engine/ViewModels/Cli, and formatting. If any of those fails, this
# 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.
@@ -101,6 +107,13 @@ jobs:
attestations: write
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.
@@ -113,24 +126,28 @@ jobs:
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:
dotnet-version: 10.0.x
cache: true
cache-dependency-path: '**/packages.lock.json'
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=${{ needs.verify.outputs.version }} -p:PublishSingleFile=true -o publish/cli
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
# ----------------------------------------------------------------------
@@ -139,7 +156,11 @@ jobs:
# 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=${{ needs.verify.outputs.version }} -p:WindowsPackageType=None -p:WindowsAppSDKSelfContained=true -p:PublishTrimmed=false -o publish/app
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
@@ -154,36 +175,71 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: |
$preFlag = @()
if ('${{ needs.verify.outputs.prerelease }}' -eq 'true') { $preFlag = @('--pre') }
vpk download github --repoUrl $env:REPO_URL --token $env:GITHUB_TOKEN --channel ${{ needs.verify.outputs.channel }} @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 ${{ needs.verify.outputs.version }} --packDir publish/app --mainExe gclo.exe --channel ${{ needs.verify.outputs.channel }} --icon gclo/Assets/Brand/gclo.ico
run: vpk pack --packId gclo --packVersion $env:VERSION --packDir publish/app --mainExe gclo.exe --channel $env:CHANNEL --icon gclo/Assets/Brand/gclo.ico
# SHA256SUMS.txt: one line per asset ('<sha256> *<name>'), so users and the
# Scoop autoupdate can verify downloads without computing hashes by hand.
- name: Write SHA256SUMS.txt
# ----------------------------------------------------------------------
# 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: |
$files = @(Get-Item artifacts/gclo-cli-win-x64.zip) + @(Get-ChildItem Releases -File | Where-Object { $_.Extension -in '.exe', '.zip', '.nupkg' })
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 ('<sha256> *<name>'), 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.txt", ($lines -join "`n") + "`n", (New-Object System.Text.UTF8Encoding($false)))
Get-Content artifacts/SHA256SUMS.txt
[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 <file> -R KofTwentyTwo/gclo`.
# Not a code signature (see #57); it makes a release attributable to the
# workflow rather than only to whoever holds the account.
# 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.txt
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.
@@ -193,20 +249,23 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: |
$preFlag = @()
if ('${{ needs.verify.outputs.prerelease }}' -eq 'true') { $preFlag = @('--pre') }
vpk upload github --repoUrl $env:REPO_URL --token $env:GITHUB_TOKEN --channel ${{ needs.verify.outputs.channel }} --releaseName "gclo ${{ needs.verify.outputs.version }}" --tag $env:GITHUB_REF_NAME @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 CLI zip and checksums to the draft release
- 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.txt --clobber
run: gh release upload $env:GITHUB_REF_NAME artifacts/gclo-cli-win-x64.zip artifacts/SHA256SUMS (Get-ChildItem artifacts/sbom -File).FullName --clobber
# Auto-generated changelog (categorized by labels via .github/release.yml).
- name: Generate release changelog
# 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: |
gh api "repos/${{ github.repository }}/releases/generate-notes" -f tag_name=$env:GITHUB_REF_NAME --jq .body | Out-File -FilePath notes.md -Encoding utf8
./.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
@@ -222,7 +281,7 @@ jobs:
# 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=${{ needs.verify.outputs.version }} -p:ContinuousIntegrationBuild=true -o artifacts/nuget
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
@@ -271,24 +330,22 @@ jobs:
}
# Velopack names the installer '<packId>-<channel>-Setup.exe'.
$setupUrl = "$env:REPO_URL/releases/download/$env:GITHUB_REF_NAME/gclo-stable-Setup.exe"
.\wingetcreate.exe update KofTwentyTwo.gclo --urls $setupUrl --version ${{ needs.verify.outputs.version }} --submit --token $env:WINGET_TOKEN
.\wingetcreate.exe update KofTwentyTwo.gclo --urls $setupUrl --version $env:VERSION --submit --token $env:WINGET_TOKEN
# ----------------------------------------------------------------------
# (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:
# Scoop reads bucket/gclo.json from the default branch, so a PR against
# dev would leave `scoop install` on the previous version until the next
# dev->main merge. Merge the PR, then merge main back into dev. (With only
# GITHUB_TOKEN the PR triggers no checks; DEPS_PAT fixes that, see #7.)
# 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 ${{ needs.verify.outputs.version }} -CliZipPath artifacts/gclo-cli-win-x64.zip
run: ./.github/scripts/Update-PackageManifests.ps1 -Version $env:VERSION -CliZipPath artifacts/gclo-cli-win-x64.zip
- name: Pack the Chocolatey package
if: needs.verify.outputs.prerelease == 'false'
@@ -313,8 +370,8 @@ jobs:
base: main
branch: packaging/v${{ needs.verify.outputs.version }}
delete-branch: true
commit-message: 'Packaging: point the Scoop and Chocolatey manifests at v${{ needs.verify.outputs.version }}'
title: 'Packaging: Scoop and Chocolatey manifests for v${{ needs.verify.outputs.version }}'
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 }}'
body: |
Generated by the Release workflow for tag `${{ github.ref_name }}`.
@@ -324,7 +381,6 @@ jobs:
- `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)' }}.
- After merging, merge `main` back into `dev` so the branches agree.
Relates to #9.
labels: |
+54
View File
@@ -0,0 +1,54 @@
# OpenSSF Scorecard (K22-CI-24). Scorecard's publishing API only accepts results
# from a workflow shaped exactly like this one: a single job, read-only top-level
# permissions, no top-level env or defaults, and only the allowed steps.
name: scorecard
on:
branch_protection_rule:
schedule:
- cron: "17 7 * * 1"
push:
branches: [main]
workflow_dispatch:
permissions: {}
concurrency:
group: scorecard-${{ github.ref }}
cancel-in-progress: false
jobs:
analysis:
name: analysis
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read # Scorecard reads workflow run metadata
security-events: write # upload SARIF to code scanning
id-token: write # publish results to the OpenSSF API and badge
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run Scorecard
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: scorecard.sarif
results_format: sarif
publish_results: true
- name: Keep the results
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: scorecard-sarif
path: scorecard.sarif
retention-days: 30
- name: Upload SARIF to code scanning
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: scorecard.sarif
category: scorecard
+27
View File
@@ -0,0 +1,27 @@
# Security scanning (K22-CI-20, -21, -23): gitleaks over the full history, Trivy
# and OSV-Scanner over the lock files, zizmor and actionlint over the workflows,
# through the KofTwentyTwo standards' reusable workflow. Runs on every change and
# weekly, so advisories published against unchanged code still surface.
# Repository-specific configuration: .gitleaks.toml, trivy.yaml, zizmor.yml.
name: security
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "23 6 * * 1"
workflow_dispatch:
permissions: {}
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
security:
uses: KofTwentyTwo/standards/.github/workflows/security.yml@ae174f54959ba4297f2ae174356c4481918fdf44 # main 2026-10-09 (pre-1.0 draft, standards#4)
permissions:
contents: read
security-events: write
+1
View File
@@ -62,6 +62,7 @@ BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
.trivycache/
project.fragment.lock.json
artifacts/
+18
View File
@@ -0,0 +1,18 @@
# KofTwentyTwo gitleaks configuration (K22-CI-20, K22-REPO-04), from
# KofTwentyTwo/standards configs/security/.gitleaks.toml.
#
# Starts from gitleaks' full default rule set and adds nothing to ignore except
# test fixtures. A real secret is never allowlisted: it is rotated (K22-REPO-04).
# A false positive outside test fixtures is suppressed inline with a
# `gitleaks:allow` comment and a reason, which keeps the decision reviewable.
title = "KofTwentyTwo gitleaks config"
[extend]
useDefault = true
[[allowlists]]
description = "Deliberately fake credentials used as test fixtures"
paths = [
'''(^|/)(test|tests|__tests__|testdata|fixtures)/.*\.(json|ya?ml|txt|env\.example)$''',
]
+78
View File
@@ -0,0 +1,78 @@
# KofTwentyTwo pre-commit configuration, from KofTwentyTwo/standards
# configs/pre-commit/.pre-commit-config.yaml with the .NET hook enabled. Works
# with pre-commit and prek. Install both hook types once per clone:
#
# pre-commit install --hook-type pre-commit --hook-type commit-msg
#
# Every external hook is pinned to a full commit SHA with the release in a comment
# (`pre-commit autoupdate --freeze` keeps that format). The hooks are the local half
# of the CI gates in the standards' ci-cd.md; CI runs the same tools again.
default_install_hook_types: [pre-commit, commit-msg]
default_stages: [pre-commit]
repos:
# File hygiene.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: check-added-large-files
args: [--maxkb=1024]
- id: check-case-conflict
- id: check-merge-conflict
- id: check-yaml
- id: check-json
- id: check-toml
- id: detect-private-key
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix=lf]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: no-commit-to-branch
args: [--branch, main]
# Secrets (K22-CI-20): staged changes only, so it stays fast.
- repo: https://github.com/gitleaks/gitleaks
rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # frozen: v8.30.1
hooks:
- id: gitleaks
# Workflow security (K22-CI-23).
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: fa412071e4f5d44d44f9e365f4676f9df92456a2 # frozen: v1.30.1
hooks:
- id: zizmor
args: [--min-severity, medium]
- repo: https://github.com/rhysd/actionlint
rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12
hooks:
- id: actionlint
# Commit message: Conventional Commits header (K22-SDLC-11).
- repo: https://github.com/compilerla/conventional-pre-commit
rev: 91ab4bf57e58b32adf1a122681f6ebe164d081c8 # frozen: v4.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [--strict, build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]
# Commit message: DCO sign-off (`git commit -s`). pygrep with --negate fails when
# no line of the message matches.
- repo: local
hooks:
- id: dco-sign-off
name: DCO sign-off present (git commit -s)
language: pygrep
entry: '^Signed-off-by: .+ <.+@.+>$'
args: [--negate]
stages: [commit-msg]
# .NET (K22-CODE-CS): the format gate, same command as CI.
- repo: local
hooks:
- id: dotnet-format
name: dotnet format --verify-no-changes
language: system
entry: dotnet format gclo.slnx --verify-no-changes --severity warn --no-restore
pass_filenames: false
files: \.(cs|csproj|props|targets|editorconfig)$
+55
View File
@@ -0,0 +1,55 @@
# KofTwentyTwo PowerShell analysis (K22-CODE-SH-05), from KofTwentyTwo/standards
# configs/shell/PSScriptAnalyzerSettings.psd1. CI runs:
# Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit
# and `Invoke-Formatter` with the same settings for layout. PowerShell supports the
# Kingsrook layout fully: next-line (Allman) braces and 3-space indentation.
@{
Severity = @('Error', 'Warning', 'Information')
IncludeDefaultRules = $true
ExcludeRules = @()
Rules = @{
PSPlaceOpenBrace = @{
Enable = $true
OnSameLine = $false
NewLineAfter = $true
IgnoreOneLineBlock = $true
}
PSPlaceCloseBrace = @{
Enable = $true
NewLineAfter = $true
IgnoreOneLineBlock = $true
NoEmptyLineBefore = $true
}
PSUseConsistentIndentation = @{
Enable = $true
IndentationSize = 3
Kind = 'space'
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
}
PSUseConsistentWhitespace = @{
Enable = $true
CheckInnerBrace = $true
# Kingsrook writes if(...) with no space; the rule cannot require that, so it
# is not checked either way.
CheckOpenParen = $false
CheckOpenBrace = $false
CheckOperator = $true
CheckPipe = $true
CheckSeparator = $true
CheckParameter = $true
}
PSAlignAssignmentStatement = @{
Enable = $true
CheckHashtable = $true
}
PSUseCorrectCasing = @{ Enable = $true }
PSAvoidUsingCmdletAliases = @{ Enable = $true }
PSAvoidUsingWriteHost = @{ Enable = $true }
PSProvideCommentHelp = @{
Enable = $true
ExportedOnly = $false
BlockComment = $true
Placement = 'before'
}
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ scoop install gclo
choco install gclo
```
Every release also attaches `SHA256SUMS.txt`, and every asset carries a build-provenance attestation you can check with `gh attestation verify <file> -R KofTwentyTwo/gclo`. `winget` support for the desktop app is tracked in [#7](https://github.com/KofTwentyTwo/gclo/issues/7).
Every release also attaches `SHA256SUMS` and a CycloneDX SBOM per artifact, and every asset carries a build-provenance attestation you can check with `gh attestation verify <file> -R KofTwentyTwo/gclo`. `winget` support for the desktop app is tracked in [#7](https://github.com/KofTwentyTwo/gclo/issues/7).
## Getting a token
+1 -1
View File
@@ -20,7 +20,7 @@
"64bit": {
"url": "https://github.com/KofTwentyTwo/gclo/releases/download/v$version/gclo-cli-win-x64.zip",
"hash": {
"url": "$baseurl/SHA256SUMS.txt"
"url": "$baseurl/SHA256SUMS"
}
}
}
+2 -2
View File
@@ -53,7 +53,7 @@ get a release without a delta package, which is normal.
| 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.txt` on the GitHub Release | always | none |
| `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 (then `main` back into `dev`) 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) |
@@ -88,7 +88,7 @@ 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.txt`, so `scoop update` works as
`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`** (the one
exception to the dev-first branch model: three generated files, nothing else).
+1 -1
View File
@@ -37,7 +37,7 @@ public sealed class PackagingManifestTests
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("SHA256SUMS.txt", root.GetProperty("autoupdate").GetProperty("architecture").GetProperty("64bit").GetProperty("hash").GetProperty("url").GetString());
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()!);
}
+3
View File
@@ -3,6 +3,9 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<!-- The release publishes win-x64; declaring it keeps that runtime graph in
packages.lock.json so the publish can restore in locked mode. -->
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>gclo</AssemblyName>
+7
View File
@@ -39,6 +39,13 @@
"gclo.Engine": "[1.0.1-dev, )"
}
}
},
"net10.0/win-x64": {
"LibGit2Sharp.NativeBinaries": {
"type": "Transitive",
"resolved": "2.0.324",
"contentHash": "5YZVGNuO9nYFQfzRQGPGY9fdpSsABwlF2jSaZ7ydxCfjvt5Krbj/eHbXkvvgAQzp4lkjvgDrlAuuI4KTmqhDlA=="
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.401",
"rollForward": "latestFeature"
}
}
+22
View File
@@ -0,0 +1,22 @@
# KofTwentyTwo Trivy configuration for software composition analysis (K22-CI-21),
# from KofTwentyTwo/standards configs/security/trivy.yaml. `trivy fs` loads it from
# the working directory; the reusable security workflow passes severity and output
# flags itself, so this file only holds what is repository-specific.
scan:
scanners:
- vuln
skip-dirs:
- .trivycache
- node_modules
- "**/bin"
- "**/obj"
vulnerability:
# Report unfixed vulnerabilities too: an unfixed HIGH still needs a decision
# (replace the dependency, or record a VEX statement and an exception).
ignore-unfixed: false
# Accepted findings live in .trivyignore (or .trivyignore.yaml), one entry per
# finding, each with the exception ID, the reason, and an expiry date, e.g.:
# CVE-2026-12345 exp:2026-12-31 # EX-0003: not reachable, see VEX statement
+11
View File
@@ -0,0 +1,11 @@
# KofTwentyTwo zizmor configuration (K22-CI-23), from KofTwentyTwo/standards
# configs/security/zizmor.yml.
#
# Every audit is enabled. Ignore a finding only for a specific workflow and line,
# with a comment citing the reason or exception ID, for example:
#
# rules:
# template-injection:
# ignore:
# - release.yml:42 # EX-0007: input is a validated semver tag
rules: {}