ci: adopt the standards' pull-request, security, CodeQL and Scorecard gates #68
No files matched your search
+18
-14
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -62,6 +62,7 @@ BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
.trivycache/
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
|
||||
@@ -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)$''',
|
||||
]
|
||||
@@ -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)$
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.401",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
+22
@@ -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
@@ -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: {}
|
||||
Reference in new issue
Block a user