Git Credential Manager (GCM) installed by Git for Windows tries to open
an interactive credential prompt or wincredman store — both fail in a
WinRM session (no TTY, no interactive desktop).
Fix:
- Set GIT_TERMINAL_PROMPT=0 to suppress all interactive git prompts
- Prepend '-c credential.helper=' (empty string) to reset the helper chain
before injecting our store-based helper (clearing GCM from the chain)
- Custom credential helper (store --file <tmp>) then appended only when PAT present
Without PAT (public repo): GCM still cleared, no prompt attempted, clean fail
if auth is actually required.
With PAT: store-based helper used exclusively, GCM never invoked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. Run-Test used '& $orchestrator @params' without Out-Host — orchestrator output
collected in function pipeline, mixed with returned hashtable → caller received
array instead of hashtable → .Success property lookup threw under StrictMode.
Fix: pipe to Out-Host.
2. RepoUrl was ssh://gitea-ci/... (SSH config alias) — alias exists only on
host SSH config, not in guest VM. Guest-side clone (§3.3) needs HTTP URL
directly reachable from inside the VM.
Fix: separate -HostRepoUrl / -GuestRepoUrl params, inject per mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: Assert-Step 'Final' 'dotnet/python/msbuild' block contains a
registry PATH refresh that overwrites $env:PATH, discarding the temporary
session injection added in §6.6. Next assertion (7z command available) then fails.
Fix: use SetEnvironmentVariable('PATH', ..., 'Machine') to permanently register
C:\Program Files\7-Zip in the system PATH — same pattern used for dotnet and MSBuild.
Follow-up registry refresh ensures $env:PATH is also current.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes:
1. Git skip check used C:\BuildTools\Git but Git installs to
C:\Program Files\Git (default). Changed $gitDir to correct path.
2. 7-Zip MSI does not register in system PATH. Registry refresh
(GetEnvironmentVariable Machine+User) was picking up empty result.
Fixed: inject sevenZipDir directly into $env:PATH if not present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PATH refresh was inside the 'else' block (only during fresh install).
If tools already exist from previous run, PATH not refreshed → validation fails.
Move PATH refresh outside if/else for both Git and 7-Zip:
- Already installed: skip download+install, but refresh PATH anyway
- Fresh install: download+install, then refresh PATH
Ensures validations can always resolve git/7z commands, even on retry runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7-Zip MSI installer adds to system PATH but PowerShell session doesn't
see it until PATH is refreshed. Refresh via:
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('PATH', 'User')
Same pattern already used for Git installer. Fixes validation error:
'7z command resolvable' now passes after installation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub release tag: v2.54.0.windows.1
Filename in release: Git-2.54.0-64-bit.exe (no .windows.1)
Extract base version for filename via regex:
$gitVersionBase = $GitVersion -replace '\.windows\.\d+$', ''
URL now correctly formed as:
.../download/v2.54.0.windows.1/Git-2.54.0-64-bit.exe
Reverted GitVersion back to '2.54.0.windows.1' (needed for tag).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Version 2.47.0.windows.1 no longer available on GitHub (network errors).
Updated to latest stable 2.54.0.windows.1 with SHA256 hash pinning (§1.3).
Hash: 2b96e7854f0520f0f6b709c21041d9801b1be44d5e1a0d9fa621b2fbc40f1983
Source: https://github.com/git-for-windows/git/releases
This fixes download failures in Setup-WinBuild2025 Step 11 (Tier-1 Toolchain).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§6.6 Tier-1 Toolchain (Git for Windows, 7-Zip) now included in final
state validation. Checks:
- 'Git for Windows (git.exe in PATH)'
- '7-Zip (7z.exe in PATH)'
Added to both initial validation pass and re-validation pass (post-remediation).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create Test-E2E-Section3.3.ps1 for side-by-side measurement:
- Runs nsis-plugin-nsinnounp with default host-side clone (baseline)
- Runs same build with -UseGitClone flag (target: guest-side git)
- Displays elapsed time, artifact size, performance delta
Usage:
.\scripts\Test-E2E-Section3.3.ps1 # Full comparison
.\scripts\Test-E2E-Section3.3.ps1 -SkipBaseline # Guest-clone only
.\scripts\Test-E2E-Section3.3.ps1 -SkipGuestClone # Baseline only
Prerequisite: Template VM has Git installed (§6.6 Tier-1 Toolchain).
Also: Defer §3.5 (vCPU/RAM tuning) and §3.4 (pre-warm pool) to home lab
section — not required for initial CI operability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update TODO.md to reflect §3.3 implementation:
- §3 summary: 4 done (3.1/3.2/3.3/3.6), 1 open (3.5), 1 deferred (3.4)
- §3.3 marked [x] completato 2026-05-10
- Document implementation: Mode 1 (host) vs Mode 2 (guest) clone
- Update recommended next steps: prioritize E2E test §3.3 + full test plan
Next actionable: E2E validation with -UseGitClone flag to measure
host-zip-transfer overhead elimination.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add guest-side clone mode to Invoke-RemoteBuild.ps1:
Changes:
1. Update DESCRIPTION: document Mode 1 (host) vs Mode 2 (guest) clone
2. Add params: CloneUrl, CloneBranch, CloneCommit, CloneSubmodules
3. Mode validation: enforce mutual exclusion (HostSourceDir XOR CloneUrl)
4. Conditional source prep:
- Mode 1 (default): compress host source, zip-transfer, expand in guest
- Mode 2 (§3.3): clone repo directly in guest via git
Guest clone implementation (§3.3):
- Clone with --depth 1, --branch specified, optionally --recurse-submodules
- Checkout specific commit if provided (handles shallow history edge case)
- PAT handling per §1.5: read from Credential Manager (placeholder),
passed as env var in WinRM session, cleaned up immediately after clone
- No PAT in argv, logs, or transcript — only in session env scope
Prerequisite: Git for Windows in template (§6.6, complete)
Testing: E2E with nsis-plugin-nsinnounp (-UseGitClone flag)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add -UseGitClone switch to Invoke-CIJob.ps1 + conditional logic:
Changes:
1. Add -UseGitClone [switch] param + docstring (§3.3 mode)
2. Modify Phase 1 to skip host clone when -UseGitClone active
3. Phase 5: Pass CloneUrl/CloneBranch/CloneCommit instead of HostSourceDir
when -UseGitClone enabled
4. Finally: Skip host cleanup when -UseGitClone (dir never created)
When -UseGitClone is active:
- Phase 1 (host clone) skipped — clone happens in VM instead
- Phase 5 passes git repo parameters to Invoke-RemoteBuild.ps1
- PAT (if needed) read in Invoke-RemoteBuild session (§1.5)
Prerequisite: Git for Windows + 7-Zip in template (§6.6, now complete)
Work in progress: Invoke-RemoteBuild.ps1 modifications pending (source transfer
conditional + guest-side git clone logic).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update TODO.md to reflect progress on §6.6:
- Mark §6 summary: 1 done (6.6 Git+7-Zip), 5 open (6.1 Linux, 6.2-6.5 extensions)
- §6.6 now shows [~] (partially complete) with note on implementation status
- Separate Tier-1 (done: Git, 7-Zip) from Tier-2 backlog (8 tools)
- Update recommended next steps: §3.3 In-VM clone now prioritized (§6.6 prerequisite met)
Next: §3.3 In-VM Git Clone implementation can now proceed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement §6.6 minimal viable product: install Git for Windows and 7-Zip
to the template VM. Critical prerequisites for §3.3 (In-VM Git clone).
Step 11 additions:
- Git for Windows (latest LTS configurable) — silent installer, adds to PATH
- 7-Zip (latest stable configurable) — MSI installer to Program Files
- SHA256 hash pinning for both tools (currently unpinned, warn at runtime)
- Assert-Step validation for git/7z command availability
- Final pre-snapshot gate checks for both tools + cleanup of installers
Parameters added:
- -GitVersion (default: 2.47.0.windows.1)
- -SevenZipVersion (default: 24.07)
Rationale:
- Enables §3.3 in-VM Git clone (git clone directly in VM instead of host-zip-transfer)
- Supports cross-platform builds and multi-tool workflows
- Fallback from §3.2 7-Zip compression finally available in template
- Positions for future expansion of Tier-1 tools (NSIS, CMake, Node.js, WiX, etc.)
Updated documentation:
- Setup-WinBuild2025.ps1 docstring: Step 11 added to list + step ordering rationale
- Removed NOTE about Git not being installed
- Final validation now checks: 9 assertions (was 7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move pre-warm pool (maintaining 2 avviati clones) from main §3 backlog to Deferred section.
Rationale: optimization only valuable if profiling shows New-BuildVM+Wait-VMReady is
bottleneck; with 2-min builds ~40% overhead, but ~0% for 10+ min builds.
Update summary table: §3 now shows 3 done, 2 open, 1 deferred (instead of 3 open).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace "all" with actual count for consistency across table.
Makes progress tracking clearer and quantifiable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove original §1.2, §1.3, §1.5 entries from main security section since
they are now consolidated in "Deferred (Home Lab)" section with better
organization and context about when to revisit.
Also removed original §1.7, §1.8 entries (was done in previous commit).
Final §1 structure: 1.1, 1.4, 1.6 (done) — 3 items.
All deferred items in separate section with clear criteria for re-enablement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark §3.2 (7-Zip compression) and comprehensive test plan as complete.
Defer security tasks to home-lab section (excessive for isolated environment).
Update next steps to prioritize test execution and §6.6 toolchain Tier-1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§3.2 — Performance: Implement 7-Zip compression for artifacts with fallback
to Compress-Archive for compatibility. Three call sites updated:
1. Host-side source compression: new Compress-BuildArtifact helper function
2. Guest-side custom build artifacts: inline 7-Zip check in scriptblock
3. Guest-side dotnet build output: inline 7-Zip check in scriptblock
Uses 7z.exe with -mmt=on (multi-threaded) and -mx1 (fast, low ratio impact).
Falls back to Compress-Archive if 7-Zip not found (until §6.6 template update).
Expected 10-20s speedup per build once 7-Zip installed in template.
Also defer all security items (§1.2/1.3/1.5/1.7/1.8) to home-lab section
since environment is isolated and excessive for current use case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§1.6 — Threat Model & Hardening Trade-offs: New section in BEST-PRACTICES.md §2.1
explains why Defender/Firewall/UAC are disabled in the template VM:
- Current state: tradeoff table (AV overhead vs attack surface, firewall fragility, UAC blocking elevated WinRM)
- Acceptable conditions: isolated lab, trusted code, no host sharing, VMnet8 not exposed
- Breaking conditions: untrusted code builds, shared host, LAN-exposed network
- Mitigations: how to re-enable each feature with specific cost/benefit trade-offs
Addresses security documentation gap — future modifications to security posture
can now reference this single authoritative source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four test files under tests/ using fake vmrun.cmd (exit code via
%FAKE_VMRUN_EXIT% env var) — no VMware or network dependency:
_Common.Tests.ps1 — New-CISessionOption TLS flags, Resolve-VmrunPath
throw/return, Invoke-Vmrun ExitCode/Output/ThrowOnError
New-BuildVM.Tests.ps1 — throw on missing template, throw+cleanup on clone
failure, Clone_{JobId}_{timestamp} name format
Remove-BuildVM.Tests.ps1 — no-op on missing VMX, partial dir cleanup,
-WhatIf preservation, full destroy removes dir
Wait-VMReady.Tests.ps1 — throw on missing vmrun, timeout throw,
IP ValidatePattern accept/reject
Run: Invoke-Pester tests\ -Output Detailed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§4.1 — Structured JSONL log (Invoke-CIJob.ps1)
Each job now emits a parallel invoke-ci.jsonl alongside the transcript.
Write-JobEvent appends one JSON line per phase transition (ts, jobId,
phase, status, data). Events emitted at: job.start, phase1.clone-repo
start/success, phase2-3b.vm-start start/success, phase4.wait-ready
start/success, phase5.build start/success, phase6.artifacts start/success,
job.success/failure (with elapsedSec and error string).
Silent no-op if $jsonLog is null (log dir setup failed). Errors in
Write-JobEvent are swallowed — logging never breaks the build.
Enables post-hoc per-phase duration analysis with jq or ConvertFrom-Json.
§4.3 — Disk space alert (scripts/Watch-DiskSpace.ps1)
Checks drive free space every 15 min (scheduled by Register-CIScheduledTasks).
Below MinFreeGB (default 50 GB): writes Warning to Windows Application Event Log
(source CI-DiskAlert, EventId 1001) and exits 1 so Task Scheduler flags the run.
Optional -WebhookUrl for Discord/Gitea webhook notification.
Register-CIScheduledTasks.ps1 updated with Task 3: CI-DiskSpaceAlert (every 15 min).
§4.4 — Incident runbook (docs/RUNBOOK.md)
Four scenarios with symptom / triage commands / fix / escalation:
1. Runner offline in Gitea UI
2. All builds fail in Phase 2 (clone/start/IP)
3. Builds are slow (per-phase JSONL analysis, disk/CPU/NAT checks)
4. Template VMX corrupt after host crash (lock removal + backup restore)
Quick-reference table at the end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§2.1 — IP race with capacity>1 (Invoke-CIJob.ps1)
Phases 2-3b (clone+start+IP) now run inside a file-based exclusive mutex
(F:\CI\State\vm-start.lock via FileShare.None). Only one job at a time is
in the VM-start phase, preventing concurrent vmrun starts from racing for
the same DHCP lease. After IP is confirmed unique it is written to an IP
lease file (F:\CI\State\ip-leases\<IP>.lease). Lock is released immediately
so build phases run fully in parallel. Lease released in finally block.
Collision detection: if a lease file already exists for the detected IP,
the job fails fast with a clear error rather than silently sharing a WinRM target.
§2.5 — Versioned snapshot (Invoke-CIJob.ps1 + runner/config.yaml)
$SnapshotName now defaults to GITEA_CI_SNAPSHOT_NAME env var, falling back
to 'BaseClean'. config.yaml documents the commented-out variable for snapshot
refresh workflow (BaseClean_<yyyyMMdd> naming convention).
§2.3 — Artifact/log retention (scripts/Invoke-RetentionPolicy.ps1)
Purges per-job subdirs under F:\CI\Artifacts and F:\CI\Logs older than
RetentionDays (default 30). Switches to AggressiveRetentionDays (default 7)
when F: free space drops below MinFreeGB (default 50 GB). Also removes stale
IP lease files older than 12 hours (defensive cleanup for crashed jobs).
SupportsShouldProcess (-WhatIf) for dry-run.
§2.2 — Scheduled task registration (scripts/Register-CIScheduledTasks.ps1)
Registers two tasks under \CI\ task folder as SYSTEM/HighestPrivilege:
CI-CleanupOrphans — every 6h + AtStartup, -MaxAgeHours 4
CI-RetentionPolicy — daily 03:00 (+30min random) + AtStartup (+15min random)
Idempotent (-Force), SupportsShouldProcess (-WhatIf), validates script paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Activates Windows against kms8.msguides.com before Windows Update runs
so WU sees correct license state. Uses cscript //NoLogo to route slmgr
output to stdout (dialogs hang in WinRM sessions). Best-effort: activation
failure emits a warning but does not abort Setup — CI builds function
within the grace period.
Placed between Step 3 (WinRM/network confirmed) and Step 4 (user creation).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$sdFiles is $null when SoftwareDistribution\Download is empty. StrictMode
throws on $null.Count. @($sdFiles).Count safely returns 0 for null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PS 5.1 treats GenericMeasureInfo.Sum as absent (not null) when the input pipe
is empty. StrictMode throws 'property Sum cannot be found'. Replace Measure-Object
with an explicit foreach loop for SoftwareDistribution size reporting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two persistent Validate-SetupState failures despite §7.4 fixes:
1. wuauserv — Setup's Assert-Steps pass at exit but WaaSMedicSvc (tamper-protected,
cannot be disabled) polls SCM ~30-60s and resets StartType to Manual.
Fix: write Start=4 directly to registry key (SCM API alone is overrideable);
deny WaaSMedicSvc write access to wuauserv registry key via ACL (best-effort,
non-fatal if WinRM session lacks permission); re-enforce Start=4 after DISM
in Cleanup; add Pre-Final re-affirmation block before Final gate.
2. AutoAdminLogon — Step 5c re-affirms it mid-script, but Steps 7-10 take hours
and no Final gate check catches a late reset. Fix: add Pre-Final re-affirmation
block (same as wuauserv) + add Assert-Step 'Final' 'AutoAdminLogon=1' to Final gate.
Also update WinISO default path to April 2026 refresh ISO.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test-WSMan in Windows PowerShell 5.1 does not support -SessionOption (WSManSessionOption),
causing "Cannot find a parameter matching the name 'SessionOption'" at runtime.
Prepare-WinBuild2025.ps1:
- Remove Test-WSMan connectivity check step entirely
- Open New-PSSession directly (handles -SkipCACheck via PSSessionOption)
- Wrap New-PSSession in try/catch with the same actionable error message
- Remove $wsmOpt (WSManSessionOption) — no longer needed
Wait-GuestWinRMReady: replace Test-WSMan probe with New-PSSession probe + Remove-PSSession
Wait-VMReady.ps1:
- Remove $wsmOpt entirely
- Replace Test-WSMan -UseSSL -SessionOption with Test-NetConnection -Port 5986
(TCP open on 5986 = HTTPS listener up = VM ready; credentials not available here)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix emerged during §7.4 e2e test run (2026-05-10):
- Deploy: set UsoSvc to Manual explicitly (Server 2025 default = Automatic)
- Setup cleanup: re-apply Set-Service Disabled on wuauserv/UsoSvc after DISM
StartComponentCleanup (WaaSMedicSvc resets StartType during component store cleanup)
- Deploy + Setup: add Administrator autologin (AutoAdminLogon, DefaultUserName,
DefaultPassword, DefaultDomainName) in post-install.ps1; Assert-Step 5c validates it
- Add Validate-DeployState.ps1: standalone host-side check of all Deploy-set state
- Add Validate-SetupState.ps1: standalone host-side check of full post-Setup state
- Mark §7.4, §7.1 and §7.2 e2e validation items as complete in TODO.md
- Update docs: WINDOWS-TEMPLATE-SETUP.md (arch table, validation scripts section,
autologin in confine Deploy/Setup); TEST-7.4-e2e.md checklist marked done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step-by-step guide to validate §7 refactor on a real VM:
Deploy stand-alone, state verification, Prepare with/without -SkipWindowsUpdate,
snapshot promotion. Includes expected values table and checklist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert §7.1/7.2/7.3 from emoji ✅ + prose into standard [x] checkbox lists.
§7.4 items converted to [ ] open checkboxes. Consistent with rest of TODO.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
template/Deploy-WinBuild2025.ps1:
New host-side script that drives the unattended Windows install phase:
creates the VM, injects autounattend.xml (disabling Defender/firewall/UAC
before Setup-WinBuild2025.ps1 runs), boots from ISO, waits for first-boot.
template/autounattend.template.xml:
WiM-based answer file template for Windows Server 2025 unattended install.
Sets DisableAntiSpyware GPO + RTP off so Defender is fully off at first
logon (prerequisite for Setup-WinBuild2025.ps1 Step-2 validation-only path).
TODO.md:
- Date + wording updated (2026-05-10)
- §1.1: file refs updated to post-refactor line numbers (Step 3 WinRM,
Deploy-WinBuild2025 Enable-PSRemoting, Invoke-RemoteBuild, Get-BuildArtifacts)
- §1.2: TrustedHosts audit status corrected (Setup-Host.ps1 never sets '*';
Prepare appends IP and restores in finally)
- §1.3: Python/dotnet/VS Build Tools line refs updated
- §1.5: PAT security constraints expanded with grep-on-log safety net rule
- §1.6: Defender/Firewall/UAC state updated to reflect Deploy vs Setup split
- §3.3 deploy reference: VMX path corrected to CI-WinBuild.vmx
- §3.3 doc ref: WINDOWS-TEMPLATE-SETUP updated to list Deploy step
Setup-WinBuild2025.ps1 (Step 2):
- Defender is now disabled at deploy time by Deploy-WinBuild2025.ps1 via
DisableAntiSpyware=1 GPO + RTP off during unattended install.
- Step 2 reduced to a single Assert-Step sanity check (GPO key present = 1).
Exclusion paths removed: scanner is not running, they would be moot.
Prepare-WinBuild2025.ps1 (Setup invocation):
- Replace one-shot Invoke-Command + manual 're-run with -SkipWindowsUpdate'
message with an automated loop (max 10 iterations):
* Invoke-GuestSetup helper runs Setup inside the VM and returns exit code.
* Exit 3010 (ERROR_SUCCESS_REBOOT_REQUIRED) triggers Restart-Computer on
the guest, waits for WinRM via Wait-GuestWinRMReady (20 min timeout),
reopens PSSession and loops.
* Exit 0 breaks the loop; any other code throws immediately.
* Hard cap prevents infinite loops if WU never converges.