Skip to content

Commit 6b4f15d

Browse files
committed
chore(lint): add format and workflow checks (2026.6.4.0-AE13)
1 parent ae139cd commit 6b4f15d

18 files changed

Lines changed: 451 additions & 67 deletions

File tree

.editorconfig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
insert_final_newline = true
6+
trim_trailing_whitespace = true
7+
8+
[*.go]
9+
indent_style = tab
10+
11+
[*.{ps1,psm1,psd1}]
12+
indent_style = space
13+
indent_size = 4
14+
15+
[*.{json,yml,yaml,md}]
16+
indent_style = space
17+
indent_size = 2
18+
trim_trailing_whitespace = false

.githooks/commit-msg

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
# Reject commit subjects that contain more than one build-version stamp.
3+
4+
set -euo pipefail
5+
6+
msg_file="$1"
7+
[ -f "$msg_file" ] || exit 0
8+
9+
subject="$(grep -m1 -vE '^(#|$)' "$msg_file" || true)"
10+
[ -n "$subject" ] || exit 0
11+
12+
pattern='\([0-9]{4}\.[0-9]+\.[0-9]+\.[0-9]+(-([A-Fa-f0-9]{4}|beta))?\)'
13+
count=$( ( printf '%s\n' "$subject" | grep -oE "$pattern" || true ) | grep -c . || true )
14+
count=${count//[^0-9]/}
15+
[ -n "$count" ] || count=0
16+
17+
if [ "${count:-0}" -gt 1 ]; then
18+
echo "" >&2
19+
echo "commit-msg: subject contains $count build-version stamps; expected at most one." >&2
20+
echo "" >&2
21+
echo " subject: $subject" >&2
22+
echo "" >&2
23+
echo "Remove stale version trailers so only one (YYYY.M.D.N...) stamp remains." >&2
24+
echo "" >&2
25+
exit 1
26+
fi
27+
28+
exit 0

.githooks/prepare-commit-msg

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# Append the current build version to the commit subject.
3+
4+
set -eu
5+
6+
msg_file="$1"
7+
source="${2:-}"
8+
pattern='\([0-9]{4}\.[0-9]+\.[0-9]+\.[0-9]+(-([A-Fa-f0-9]{4}|beta))?\)'
9+
10+
case "$source" in
11+
merge|squash) exit 0 ;;
12+
esac
13+
14+
if [ "$source" != "commit" ]; then
15+
subject="$(grep -m1 -vE '^(#|$)' "$msg_file" || true)"
16+
if printf '%s\n' "$subject" | grep -qE "$pattern"; then
17+
echo "" >&2
18+
echo "prepare-commit-msg: subject already contains a build-version stamp." >&2
19+
echo "" >&2
20+
echo " subject: $subject" >&2
21+
echo "" >&2
22+
echo "Drop the existing stamp and let this hook append the current one." >&2
23+
echo "" >&2
24+
exit 1
25+
fi
26+
fi
27+
28+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
29+
[ -n "$repo_root" ] || exit 0
30+
31+
version_file="$repo_root/version.txt"
32+
version=""
33+
if [ -f "$version_file" ]; then
34+
version="$(tr -d '\r' < "$version_file" | sed 's/[^[:print:]]//g' | xargs)"
35+
fi
36+
37+
today="$(date +%Y.%-m.%-d)"
38+
if ! printf '%s\n' "$version" | grep -qE "^${today}\."; then
39+
short_head="$(git rev-parse --short=4 HEAD 2>/dev/null | tr 'a-f' 'A-F')"
40+
[ -n "$short_head" ] || short_head="0000"
41+
version="${today}.0-${short_head}"
42+
printf '%s' "$version" > "$version_file"
43+
fi
44+
45+
[ -n "$version" ] || exit 0
46+
47+
if ! grep -qE "$pattern" "$msg_file"; then
48+
sed -i "1s/\$/ ($version)/" "$msg_file"
49+
fi
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env pwsh
2+
[CmdletBinding()]
3+
param(
4+
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
5+
)
6+
7+
$ErrorActionPreference = 'Stop'
8+
9+
$errors = New-Object System.Collections.Generic.List[string]
10+
11+
function Get-RepoRelativePath {
12+
param([string]$Path)
13+
14+
$rootFull = [System.IO.Path]::GetFullPath($Root).TrimEnd([char[]]@('\', '/'))
15+
$pathFull = [System.IO.Path]::GetFullPath($Path)
16+
$prefix = $rootFull + [System.IO.Path]::DirectorySeparatorChar
17+
if ($pathFull.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
18+
return $pathFull.Substring($prefix.Length)
19+
}
20+
return $pathFull
21+
}
22+
23+
function Add-ParseErrors {
24+
param([string]$Source, $ParseErrors)
25+
if (-not $ParseErrors) { return }
26+
foreach ($err in $ParseErrors) {
27+
$errors.Add("$Source (line $($err.Extent.StartLineNumber):$($err.Extent.StartColumnNumber)): $($err.Message)") | Out-Null
28+
}
29+
}
30+
31+
$trackedScripts = & git -C $Root ls-files '*.ps1' '*.psm1' '*.psd1'
32+
foreach ($relative in $trackedScripts) {
33+
$path = Join-Path $Root $relative
34+
$parseErrors = $null
35+
[void][System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$parseErrors)
36+
Add-ParseErrors -Source $relative -ParseErrors $parseErrors
37+
}
38+
39+
$workflowDir = Join-Path $Root '.github\workflows'
40+
if (Test-Path -LiteralPath $workflowDir) {
41+
$ghaPattern = [regex]'\$\{\{[^}]*\}\}'
42+
Get-ChildItem -LiteralPath $workflowDir -Filter '*.yml' | ForEach-Object {
43+
$relativeWorkflow = Get-RepoRelativePath -Path $_.FullName
44+
$lines = Get-Content -LiteralPath $_.FullName
45+
$stepName = '<unnamed>'
46+
$isPwsh = $false
47+
$inRun = $false
48+
$runIndent = -1
49+
$runStart = 0
50+
$blockLines = New-Object System.Collections.Generic.List[string]
51+
52+
$flush = {
53+
if ($blockLines.Count -eq 0) { return }
54+
$baseline = -1
55+
foreach ($line in $blockLines) {
56+
if ($line.Trim().Length -eq 0) { continue }
57+
$baseline = $line.Length - $line.TrimStart(' ').Length
58+
break
59+
}
60+
if ($baseline -lt 0) { return }
61+
$body = ($blockLines | ForEach-Object {
62+
if ($_.Length -gt $baseline) { $_.Substring($baseline) } else { '' }
63+
}) -join "`n"
64+
$stubbed = $ghaPattern.Replace($body, '__GHA_EXPR__')
65+
$parseErrors = $null
66+
[void][System.Management.Automation.Language.Parser]::ParseInput($stubbed, [ref]$null, [ref]$parseErrors)
67+
Add-ParseErrors -Source "$relativeWorkflow step '$stepName' run block starting line $runStart" -ParseErrors $parseErrors
68+
}
69+
70+
for ($i = 0; $i -lt $lines.Count; $i++) {
71+
$line = $lines[$i]
72+
$indent = $line.Length - $line.TrimStart(' ').Length
73+
$trimmed = $line.Trim()
74+
75+
if ($inRun) {
76+
if ($trimmed.Length -gt 0 -and $indent -le $runIndent) {
77+
& $flush
78+
$blockLines.Clear()
79+
$inRun = $false
80+
} else {
81+
$blockLines.Add($line) | Out-Null
82+
continue
83+
}
84+
}
85+
86+
if ($trimmed -match '^- name:\s*(.+?)\s*$') {
87+
$stepName = $Matches[1]
88+
$isPwsh = $false
89+
continue
90+
}
91+
if ($trimmed -match '^shell:\s*(.+?)\s*$') {
92+
$isPwsh = ($Matches[1] -eq 'pwsh')
93+
continue
94+
}
95+
if ($isPwsh -and $trimmed -match '^run:\s*\|\s*$') {
96+
$inRun = $true
97+
$runIndent = $indent
98+
$runStart = $i + 1
99+
$blockLines.Clear()
100+
}
101+
}
102+
if ($inRun) { & $flush }
103+
}
104+
}
105+
106+
if ($errors.Count -gt 0) {
107+
Write-Host 'PowerShell syntax errors:'
108+
foreach ($err in $errors) { Write-Host " $err" }
109+
throw "Found $($errors.Count) PowerShell syntax error(s)."
110+
}
111+
112+
Write-Host 'PowerShell scripts and inline workflow blocks parsed cleanly.'

.github/workflows/ci.yml

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
1-
name: ci
1+
name: CI
22

33
on:
44
push:
55
branches: [main]
66
pull_request:
77
branches: [main]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: ci-${{ github.ref }}
15+
cancel-in-progress: true
816

917
jobs:
1018
build:
19+
name: lint + build
1120
runs-on: windows-latest
21+
timeout-minutes: 15
1222
steps:
13-
- uses: actions/checkout@v4
14-
- uses: actions/setup-go@v5
23+
- name: Checkout
24+
uses: actions/checkout@v6
25+
26+
- name: Validate PowerShell and release scripts
27+
shell: pwsh
28+
run: |
29+
./.github/scripts/Test-WorkflowSyntax.ps1
30+
./.github/scripts/Test-ResolveReleaseBaseTag.ps1
31+
32+
- name: Set up Go
33+
uses: actions/setup-go@v6
1534
with:
16-
go-version: '1.22'
35+
go-version-file: go.mod
1736
cache: true
37+
cache-dependency-path: go.sum
1838

19-
- name: go vet
20-
run: go vet ./...
39+
- name: Install PowerShell analyzer
40+
shell: pwsh
41+
run: Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
2142

22-
- name: go test
23-
run: go test ./...
43+
- name: Lint and test
44+
shell: pwsh
45+
run: ./scripts/lint.ps1
2446

2547
- name: go build (default)
2648
run: go build -v ./...
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Commit message check
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
check:
14+
name: Reject double-stamped commit subjects
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 2
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v6
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Validate commit messages in the pushed range
24+
env:
25+
BEFORE_SHA: ${{ github.event.before }}
26+
AFTER_SHA: ${{ github.event.after }}
27+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
28+
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
29+
run: |
30+
set -euo pipefail
31+
32+
if [ -n "${PR_HEAD_SHA:-}" ] && [ -n "${PR_BASE_SHA:-}" ]; then
33+
range="${PR_BASE_SHA}..${PR_HEAD_SHA}"
34+
elif [ "${BEFORE_SHA:-}" = "0000000000000000000000000000000000000000" ] || [ -z "${BEFORE_SHA:-}" ]; then
35+
range="${AFTER_SHA}~1..${AFTER_SHA}"
36+
else
37+
range="${BEFORE_SHA}..${AFTER_SHA}"
38+
fi
39+
echo "Validating commits in range: $range"
40+
41+
pattern='\([0-9]{4}\.[0-9]+\.[0-9]+\.[0-9]+(-([A-Fa-f0-9]{4}|beta))?\)'
42+
43+
failures=0
44+
while IFS= read -r line; do
45+
sha="${line%% *}"
46+
subject="${line#* }"
47+
count=$( ( printf '%s\n' "$subject" | grep -oE "$pattern" || true ) | grep -c . || true )
48+
count=${count//[^0-9]/}
49+
count=${count:-0}
50+
if [ "$count" -gt 1 ]; then
51+
echo "::error::Commit $sha has $count build-version stamps in its subject:"
52+
echo "::error:: $subject"
53+
failures=$((failures + 1))
54+
fi
55+
done < <(git log --format='%H %s' "$range")
56+
57+
if [ "$failures" -gt 0 ]; then
58+
echo ""
59+
echo "::error::$failures commit(s) carry more than one build-version stamp."
60+
echo "Reword the offending commits to drop stale version trailers."
61+
exit 1
62+
fi
63+
64+
echo "All commit subjects in $range carry at most one build-version stamp."

.github/workflows/release.yml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,25 @@ jobs:
2222
runs-on: windows-latest
2323
timeout-minutes: 30
2424
steps:
25-
- uses: actions/checkout@v4
25+
- name: Checkout
26+
uses: actions/checkout@v6
2627
with:
2728
# Full history + all tags so the release-body step can diff
2829
# commits against the previous version tag for the changelog.
2930
fetch-depth: 0
3031

31-
- name: Test release base selection
32+
- name: Validate PowerShell and release scripts
3233
shell: pwsh
33-
run: ./.github/scripts/Test-ResolveReleaseBaseTag.ps1
34+
run: |
35+
./.github/scripts/Test-WorkflowSyntax.ps1
36+
./.github/scripts/Test-ResolveReleaseBaseTag.ps1
3437
35-
- uses: actions/setup-go@v5
38+
- name: Set up Go
39+
uses: actions/setup-go@v6
3640
with:
37-
go-version: '1.22'
41+
go-version-file: go.mod
3842
cache: true
43+
cache-dependency-path: go.sum
3944

4045
- name: Resolve version from tag
4146
id: ver
@@ -51,8 +56,8 @@ jobs:
5156
$version = $tag.Substring(1)
5257
# Sanity-check the YYYY.M.D.N(-XXXX) shape so a fat-fingered
5358
# tag fails the release rather than shipping a broken version.
54-
if ($version -notmatch '^\d{4}\.\d+\.\d+\.\d+(-[A-Fa-f0-9]{4})?$') {
55-
throw "version '$version' does not match the YYYY.M.D.N(-XXXX) shape"
59+
if ($version -notmatch '^\d{4}\.\d+\.\d+\.\d+(-([A-Fa-f0-9]{4}|beta))?$') {
60+
throw "version '$version' does not match the YYYY.M.D.N, YYYY.M.D.N-XXXX, or YYYY.M.D.N-beta shape"
5661
}
5762
$isPre = $version -like '*-*'
5863
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ vendor/
2222
# Bundled picotool binary downloaded by scripts/fetch-picotool.ps1.
2323
# Embedded only into -tags embed_picotool builds; default builds use PATH.
2424
internal/picotool/binaries/picotool.exe
25+
version.txt
2526

2627
# Log files
2728
*.log

0 commit comments

Comments
 (0)