Skip to content

feat(packages): add duf to the scoop manifest #278

feat(packages): add duf to the scoop manifest

feat(packages): add duf to the scoop manifest #278

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Cancel superseded runs on the same ref so the queue stays current.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Pinned dependency versions live in one place so a bump is a one-line, reviewable
# change (and Dependabot can PR action SHAs). Actions are pinned to a full commit
# SHA β€” a moved tag can't silently change what runs in CI (supply-chain gate).
env:
PESTER_VERSION: "5.6.1"
PSSA_VERSION: "1.22.0"
LUACHECK_VERSION: "1.2.0"
permissions:
contents: read
jobs:
# --- incremental gate -------------------------------------------------------
# Decide whether the heavy Windows jobs need to run at all. A docs/markdown-only
# change still gets the fast Linux gate, but doesn't spin up two windows-latest
# runners for nothing. `code` is true when any changed file is NOT pure docs.
detect-changes:
name: Detect code changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
lua: ${{ steps.filter.outputs.lua }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- id: filter
shell: bash
run: |
set -euo pipefail
ZERO='0000000000000000000000000000000000000000'
if [ "${{ github.event_name }}" = "pull_request" ]; then
base='${{ github.event.pull_request.base.sha }}'
else
base='${{ github.event.before }}'
fi
# An empty / all-zero (new branch) / unreachable base can't anchor a diff.
# Fall back to HEAD~1; if even that is missing (root commit, shallow clone),
# we have no usable base.
if [ -z "$base" ] || [ "$base" = "$ZERO" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
if git rev-parse HEAD~1 >/dev/null 2>&1; then base='HEAD~1'; else base=''; fi
fi
# FAIL SAFE: when the base is unknown, run EVERYTHING rather than skipping
# the heavy jobs on a guess β€” the old HEAD~1 fallback could yield an empty
# diff and silently skip tests on a root commit / new branch / force-push.
if [ -z "$base" ]; then
echo "no usable diff base (root commit / new branch) β€” running all jobs"
echo 'code=true' >> "$GITHUB_OUTPUT"
echo 'lua=true' >> "$GITHUB_OUTPUT"
exit 0
fi
changed="$(git diff --name-only "${base}...HEAD")"
echo "diff base: $base"; echo "changed files:"; echo "$changed"
# code=true if at least one changed path is NOT a docs/markdown file
# (anything under docs/ or any *.md is treated as docs-only).
if printf '%s\n' "$changed" | grep -qvE '^(docs/.*|.*\.md)$'; then
echo 'code=true' >> "$GITHUB_OUTPUT"
else
echo 'code=false' >> "$GITHUB_OUTPUT"
fi
# lua=true when any Neovim Lua config (or its luacheck config) changed,
# so the Lua lint only spins up when it can actually find something.
if printf '%s\n' "$changed" | grep -qE '(^nvim/|\.lua$|\.luacheckrc$)'; then
echo 'lua=true' >> "$GITHUB_OUTPUT"
else
echo 'lua=false' >> "$GITHUB_OUTPUT"
fi
# --- fast, dependency-free gate ---------------------------------------------
# Runs on Linux pwsh in seconds and fails the build on any syntax/JSON/format
# regression before the heavier Windows jobs spin up. Always runs.
lint-fast:
name: Syntax, manifest & format gate (fast)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Validate (parser + JSON + editorconfig, no Gallery needed)
shell: pwsh
run: ./tests/Invoke-Validation.ps1
# --- Lua lint (nvim config) -------------------------------------------------
# The nvim/ tree had a .luacheckrc but nothing ran it, so the config could rot
# silently. Runs only when Lua actually changed (see detect-changes.lua).
lua-lint:
name: Luacheck (nvim)
runs-on: ubuntu-latest
needs: [lint-fast, detect-changes]
if: needs.detect-changes.outputs.lua == 'true'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install lua + luarocks
shell: bash
run: |
set -euo pipefail
# lua5.1 + luarocks provide the runtime luacheck needs to execute, so
# they come from apt every run (the cached rock below can't run without
# them). The slow, non-hermetic part β€” compiling the luacheck rock β€” is
# what we pin and cache.
sudo apt-get update
# liblua5.1-0-dev so luarocks can compile luacheck's C deps (luafilesystem)
# against the SAME Lua 5.1 we standardize on below.
sudo apt-get install -y lua5.1 liblua5.1-0-dev luarocks
- name: Cache luacheck rock
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.luarocks
# Key on the runner image (ImageOS) and the Lua line too: the cached tree
# is built against the runner's lua5.1/luarocks, so a runner-image bump or
# Lua change must invalidate it rather than silently reuse a mismatched rock.
key: luacheck-${{ runner.os }}-${{ env.ImageOS }}-lua5.1-${{ env.LUACHECK_VERSION }}
- name: Install luacheck (pinned)
shell: bash
run: |
set -euo pipefail
# Pin the exact version (matches the SHA-pinned Actions discipline) AND the
# Lua line (--lua-version 5.1) so the build target is deterministic instead
# of luarocks' default; install to the cached user tree, so a cache hit
# skips the recompile.
if ! "$HOME/.luarocks/bin/luacheck" --version 2>/dev/null | grep -q "$LUACHECK_VERSION"; then
luarocks install --local --lua-version 5.1 luacheck "$LUACHECK_VERSION"
fi
echo "$HOME/.luarocks/bin" >> "$GITHUB_PATH"
- name: Luacheck
shell: bash
# Run from nvim/ so luacheck discovers nvim/.luacheckrc by searching upward.
run: luacheck .
working-directory: nvim
# --- nvim <-> Core parity (B1) ----------------------------------------------
# nvim/ is vendored from dotfiles-core; this fails if the vendored tree drifts
# from the Core commit recorded in nvim/.core-ref (clone Core @ that commit and
# diff). Self-skips until a sync stamps the ref. Same nvim-only gate as luacheck.
nvim-parity:
name: nvim ↔ Core parity
runs-on: ubuntu-latest
needs: [lint-fast, detect-changes]
if: needs.detect-changes.outputs.lua == 'true'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Assert nvim/ matches Core at the recorded ref
shell: pwsh
run: ./tests/Assert-NvimParity.ps1
analyze:
name: PSScriptAnalyzer
runs-on: windows-latest
needs: [lint-fast, detect-changes]
if: needs.detect-changes.outputs.code == 'true'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Cache PowerShell modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Documents/PowerShell/Modules
key: pssa-${{ runner.os }}-${{ env.PSSA_VERSION }}
- name: Install PSScriptAnalyzer (pinned)
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable PSScriptAnalyzer |
Where-Object Version -eq ([version]$env:PSSA_VERSION))) {
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module PSScriptAnalyzer -RequiredVersion $env:PSSA_VERSION `
-Scope CurrentUser -Force
}
# Supply-chain gate: -RequiredVersion above pins the version and Install-Module
# enforces the publisher check, but nothing asserted the installed bits are
# actually Microsoft-signed (a poisoned cache or a compromised feed would slip
# through). Verify the module manifest's Authenticode signature is Valid AND
# Microsoft-issued before trusting the analyzer β€” the Windows analogue of the
# SHA-256 verification the Linux gate tools get from setup-core-tools.
- name: Verify PSScriptAnalyzer is Microsoft-signed
shell: pwsh
run: |
$mod = Get-Module -ListAvailable PSScriptAnalyzer |
Where-Object Version -eq ([version]$env:PSSA_VERSION) |
Select-Object -First 1
if (-not $mod) { throw "PSScriptAnalyzer $($env:PSSA_VERSION) not installed" }
$sig = Get-AuthenticodeSignature -FilePath $mod.Path
"status=$($sig.Status)" | Write-Host
# Check Status FIRST: on a non-Valid status (e.g. NotSigned) SignerCertificate
# is $null, so reading .Subject before this would mask the real failure.
if ($sig.Status -ne 'Valid') {
throw "PSScriptAnalyzer signature is '$($sig.Status)', expected 'Valid'"
}
$subject = $sig.SignerCertificate.Subject
"signer=$subject" | Write-Host
if ($subject -notmatch 'Microsoft Corporation') {
throw "PSScriptAnalyzer signer '$subject' is not Microsoft Corporation"
}
"PSScriptAnalyzer $($env:PSSA_VERSION) verified: Valid + Microsoft-signed" | Write-Host
- name: Analyze
shell: pwsh
run: |
Import-Module PSScriptAnalyzer -RequiredVersion $env:PSSA_VERSION
$results = Invoke-ScriptAnalyzer -Path . -Recurse `
-Settings ./tests/PSScriptAnalyzerSettings.psd1
if ($results) {
$results | Format-Table -AutoSize | Out-String | Write-Host
}
$errors = @($results | Where-Object Severity -eq 'Error')
"Analyzer findings: $($results.Count) total, $($errors.Count) error(s)" | Write-Host
if ($errors.Count) { throw "PSScriptAnalyzer reported $($errors.Count) error(s)" }
test:
name: Pester
runs-on: windows-latest
needs: [lint-fast, detect-changes]
if: needs.detect-changes.outputs.code == 'true'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Cache PowerShell modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Documents/PowerShell/Modules
key: pester-${{ runner.os }}-${{ env.PESTER_VERSION }}
- name: Install Pester (pinned)
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable Pester |
Where-Object Version -eq ([version]$env:PESTER_VERSION))) {
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -RequiredVersion $env:PESTER_VERSION `
-Scope CurrentUser -Force -SkipPublisherCheck
}
- name: Run tests (with coverage gate)
shell: pwsh
run: |
Import-Module Pester -RequiredVersion $env:PESTER_VERSION
# The coverage % bar and the test-case FLOOR come from the versioned,
# generated baseline (tests/coverage-baseline.json) β€” not hand-edited
# literals here. Refresh it with tests/Update-CoverageBaseline.ps1 after
# intentionally removing tests (B5). Read-CoverageBaseline throws if the
# file is missing/corrupt, so the gate can't silently go soft.
. ./tests/CoverageGate.ps1
$baseline = Read-CoverageBaseline (Get-Content ./tests/coverage-baseline.json -Raw)
$cfg = New-PesterConfiguration
$testPath = './tests' # single source for the run path + the test-file glob
$cfg.Run.Path = $testPath
# PassThru (not Run.Exit) so we get the result object back and can gate on
# BOTH failures and coverage ourselves with clear messages.
$cfg.Run.PassThru = $true
$cfg.Output.Verbosity = 'Detailed'
# Regression gate on the whole pure-helper surface: 05-lib.ps1 plus the
# Dotfiles module's extracted helper files (B7), each fully exercised by
# Lib/Wsl/Doctor/Help/Module tests. A drop here means a test was deleted
# or a branch was added without a covering test. Grow this list as more
# pure helpers reach full coverage.
$cfg.CodeCoverage.Enabled = $true
$cfg.CodeCoverage.Path = @(
'./powershell/core/05-lib.ps1'
'./powershell/Dotfiles/Wsl.Helpers.ps1'
'./powershell/Dotfiles/Doctor.Helpers.ps1'
'./powershell/Dotfiles/Help.Helpers.ps1'
'./powershell/Dotfiles/Modules.Helpers.ps1'
)
$cfg.CodeCoverage.CoveragePercentTarget = $baseline.CoveragePercentTarget
$r = Invoke-Pester -Configuration $cfg
$pct = [math]::Round($r.CodeCoverage.CoveragePercent, 1)
"Tests: $($r.PassedCount) passed, $($r.FailedCount) failed, $($r.TotalCount) total across $($r.Containers.Count) file(s). Coverage (pure-helper surface): $pct% (target $($baseline.CoveragePercentTarget)%)" | Write-Host
# The test-FILE count is auto-derived (not stored): count *.Tests.ps1
# under the run path RECURSIVELY (matching Pester's own discovery) and
# assert Pester ran EXACTLY that many β€” a suite that fails to load, or is
# removed/renamed, is caught with no number to maintain (issue #29).
$expectedFiles = @(Get-ChildItem $testPath -Recurse -File -Filter *.Tests.ps1 -ErrorAction Stop).Count
# One pure decision over the whole result: hard failures, the coverage
# bar, the exact file match, and the test-case floor. The floor also
# guards suites NOT in CoveragePaths (install/uninstall/packages), whose
# removed tests wouldn't show up as a coverage drop. TotalCount runs well
# past the floor (two -ForEach blocks expand it); it only trips on a real
# deletion.
$gate = Get-CoverageGateResult -CoveragePercent $r.CodeCoverage.CoveragePercent `
-TotalCount $r.TotalCount -FileCount $r.Containers.Count -ExpectedFileCount $expectedFiles `
-FailedCount $r.FailedCount -Baseline $baseline
if (-not $gate.Passed) {
$gate.Failures | ForEach-Object { Write-Host "::error::$_" }
throw ("coverage gate failed:`n " + ($gate.Failures -join "`n "))
}