Skip to content

chore(pkg,ci): security/packaging tail (#129)#132

Merged
Gerrrt merged 2 commits into
mainfrom
chore/security-tail-129
Jul 18, 2026
Merged

chore(pkg,ci): security/packaging tail (#129)#132
Gerrrt merged 2 commits into
mainfrom
chore/security-tail-129

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Picks up the security / packaging tail from #129. Verified on the Windows host with Invoke-Pester (590 passing).

Changes

auto-tag — SHA-pin the reusable workflow. auto-tag.yml delegated to dotfiles-core/.github/workflows/auto-tag-call.yml@v3 (a mutable tag); pinned it to the full commit SHA e9cf907 (= v3), matching ci.yml's SHA-pinned Actions discipline so a moved tag can't silently change what runs. Also fixed the stale @v2 comment.

install .bak accumulation. Added Test-CopyCurrent and a copy-mode idempotency skip in Link-Item: on a host with no symlink capability, if the destination already matches the target's content, skip instead of backing up + recopying. Files compare by SHA-256; directories (nvim\, psmux\scripts) by their full {relative path → SHA-256} set. Previously every unelevated re-install dropped a fresh timestamped .bak and recopied identical bytes — backups piled up unbounded. Covered by 7 new Test-CopyCurrent tests.

scoop-installer hash — close the CI gap. install.ps1 verifies get.scoop.sh when DOTFILES_SCOOP_SHA256 is set, but the CI path (Check-PackageFreshness.ps1, runs elevated) had no gate. Applied the same opt-in gate there. The SHA is computed over the UTF-8 string bytes exactly as install.ps1's Get-DotStringSha256 (verified they produce identical digests), so one expected-hash value covers both paths. No hardcoded pin by designget.scoop.sh is a moving target, so a baked-in SHA would break CI on every upstream installer edit (decision confirmed with the maintainer).

winget-import drift gate. Added a Pester test asserting the committed packages/winget-import.json is byte-identical to a fresh Export-WingetImport regeneration (it went stale silently before). Added an -OutPath param to the generator so the test regenerates to a temp path without touching the repo copy. The generator is deterministic (no CreationDate/WinGetVersion stamp), so a clean tree round-trips exactly.

Verification

  • Full suite: 590 passed, 0 failed (+8 new tests: 7 Test-CopyCurrent, 1 winget drift).
  • Confirmed the inline SHA in the CI path matches Get-DotStringSha256 for the same input, and the committed winget-import.json matches a fresh regen byte-for-byte.

Completes the deferred #129 items (alongside #131 for the dedup/dead-code hygiene). The only remaining checkbox is the psmux CRIT-1 runtime-verify, which needs an interactive psmux session and can't be automated in CI.

🤖 Generated with Claude Code

Deferred security/packaging items from the 2026-07-17 triage. Verified with
Invoke-Pester (590 passing).

auto-tag — pin the reusable dotfiles-core auto-tag-call workflow to a full
commit SHA (e9cf907, = v3) instead of the mutable @V3 tag, matching ci.yml's
SHA-pinned Actions discipline; fix the stale "@v2" comment.

install .bak accumulation — add Test-CopyCurrent and skip the back-up+recopy
in COPY mode (no symlink capability) when the destination already matches the
target's content. Files compare by SHA-256, directories by their full
{relative path -> SHA-256} set. Previously every unelevated re-install dropped
a fresh timestamped .bak and recopied identical bytes, piling up backups.

scoop-installer hash — apply the same opt-in DOTFILES_SCOOP_SHA256 gate the
installer uses to the CI freshness path (Check-PackageFreshness.ps1), which
had none. The SHA is computed over the UTF-8 string bytes exactly as
install.ps1's Get-DotStringSha256, so one expected-hash value covers both.
No hardcoded pin by design — get.scoop.sh is a moving target.

winget-import drift gate — add a Pester test asserting the committed
packages/winget-import.json is byte-identical to a fresh Export-WingetImport
regeneration (it went stale silently before). Adds an -OutPath param to the
generator so the test regenerates to a temp path without touching the repo copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSeBsL1YesMCpEqxHXSKkv
Copilot AI review requested due to automatic review settings July 18, 2026 00:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR picks up the remaining “security / packaging tail” items by tightening CI supply-chain checks, adding drift gates for generated artifacts, and making copy-mode installs idempotent to prevent .bak accumulation on hosts without symlink capability.

Changes:

  • Pin the auto-tag reusable workflow call to an immutable commit SHA.
  • Add copy-mode “already current” detection (file + directory tree hashing) and tests to avoid repeated backup/recopy churn.
  • Add CI integrity gating for the Scoop installer and a drift test ensuring packages/winget-import.json matches a fresh regeneration (with a new -OutPath option).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Packages.Tests.ps1 Adds a Pester drift gate to ensure packages/winget-import.json matches a freshly generated output byte-for-byte.
tests/Install.Tests.ps1 Adds unit tests for the new copy-mode idempotency helper (Test-CopyCurrent).
packages/Export-WingetImport.ps1 Adds -OutPath to support deterministic regen to a temp path for drift testing.
packages/Check-PackageFreshness.ps1 Adds an opt-in Scoop installer SHA-256 verification path in CI when Scoop isn’t present.
install.ps1 Adds Test-CopyCurrent and uses it to skip redundant copy-mode work to prevent .bak accumulation.
.github/workflows/auto-tag.yml Pins the reusable workflow reference to a full commit SHA (supply-chain hardening).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread install.ps1 Outdated
Comment on lines +111 to +129
# Directory: hash every file, keyed by its path relative to the tree root.
$hashTree = {
param([string]$Root)
$root = (Resolve-Path -LiteralPath $Root).Path
$map = @{} # PowerShell hashtables are case-insensitive, matching NTFS
Get-ChildItem -LiteralPath $root -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object {
$rel = $_.FullName.Substring($root.Length).TrimStart('\', '/')
$map[$rel] = (Get-FileHash -LiteralPath $_.FullName).Hash
}
$map
}
$a = & $hashTree $Link
$b = & $hashTree $Target
if ($a.Count -ne $b.Count) { return $false }
foreach ($k in $a.Keys) {
if (-not $b.ContainsKey($k) -or $b[$k] -ne $a[$k]) { return $false }
}
return $true
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Test-CopyCurrent now (1) wraps the directory walk in try/catch with -ErrorAction Stop, so any traversal/hash error returns $false (not-current -> safe recopy) instead of silently dropping files, and (2) maps directory entries too (files -> SHA-256, subdirs -> a <dir> sentinel) so an added/removed empty directory is detected. Added a covering test.

Comment on lines +61 to +65
if ($actual -ne $env:DOTFILES_SCOOP_SHA256.ToLowerInvariant()) {
throw "scoop installer hash mismatch — expected $($env:DOTFILES_SCOOP_SHA256), got $actual"
}
}
# Must run as a FILE (scoop's installer takes -RunAsAdmin, which iex can't

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed both points. The hash mismatch now re-throws from the outer catch (transient setup errors still warn+continue), so a configured mismatch fails the run. And the installer is written with [IO.File]::WriteAllBytes using the exact UTF-8 byte array that was hashed (was Set-Content, which re-encodes/appends a newline) — verified the on-disk file hashes to the gated value.

Test-CopyCurrent (install.ps1):
- Treat any traversal/hash error as "not current" (return $false) so an
  incomplete comparison recopies rather than falsely skipping — swapped the
  swallowed -ErrorAction SilentlyContinue for -ErrorAction Stop inside a
  try/catch guard.
- Include directory entries in the tree map (files -> SHA-256, subdirs ->
  '<dir>' sentinel) so an added/removed EMPTY directory is detected too. Added
  a covering test.

Check-PackageFreshness.ps1:
- The DOTFILES_SCOOP_SHA256 gate was ineffective: the mismatch throw was
  swallowed by the surrounding tolerant catch. Re-throw a hash mismatch so the
  run fails; transient setup errors still warn and continue.
- Write the EXACT hashed bytes with [IO.File]::WriteAllBytes (was Set-Content,
  which re-encodes / appends a newline) so the installer that runs is
  byte-for-byte what was verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSeBsL1YesMCpEqxHXSKkv

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +74 to 79
# Must run as a FILE (scoop's installer takes -RunAsAdmin, which iex can't
# pass). WriteAllBytes emits exactly the verified bytes — no re-encoding.
$installer = Join-Path ([System.IO.Path]::GetTempPath()) 'install-scoop.ps1'
Invoke-RestMethod -Uri 'https://get.scoop.sh' -OutFile $installer
[System.IO.File]::WriteAllBytes($installer, $scoopBytes)
& $installer -RunAsAdmin 2>&1 | Out-Null # CI runs elevated; -RunAsAdmin lets scoop install anyway
$env:PATH = "$HOME\scoop\shims;$env:PATH" # make `scoop` resolvable in this session
@Gerrrt
Gerrrt merged commit b7430ad into main Jul 18, 2026
7 checks passed
@Gerrrt
Gerrrt deleted the chore/security-tail-129 branch July 18, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants