Skip to content

Commit 307b5b4

Browse files
AndreaV-Lsiclaude
andcommitted
Merge feature/cross-platform-ssh: v8.0.0 release
Cross-platform SSH support, module architecture, automated testing, structured JSON output, and SemVer bug fixes. Closes #15. Fixes #16. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 1920bd7 + 44e0ca2 commit 307b5b4

32 files changed

Lines changed: 1930 additions & 190 deletions

File tree

.gitignore

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ LsiGitCheckout_Errors.txt
55
# Repository directories
66
repos/
77

8-
# User configuration files
9-
dependencies.json
8+
# Cloned test repositories (created by integration tests inside subdirectories)
9+
tests/*/test-root-*/
10+
tests/*/libs/
11+
tests/*/correlator-driver/
12+
13+
# User configuration files (root only — test subdirectories have their own dependencies.json)
14+
/dependencies.json
15+
git_credentials.json
1016

1117
# SSH keys
1218
*.ppk

CHANGELOG.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,48 @@ All notable changes to LsiGitCheckout will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [8.0.0] - 2026-03-20
8+
## [8.0.0] - 2026-03-21
99

1010
### Changed
11+
1112
- **BREAKING**: Requires PowerShell 7.6 LTS or later (previously 5.1). PowerShell 7.6 installs side-by-side with Windows PowerShell 5.1.
1213
- Refactored monolithic script into module architecture: `LsiGitCheckout.psm1` (functions) + `LsiGitCheckout.ps1` (entry point)
1314
- Post-checkout scripts now execute via `pwsh` instead of `powershell.exe`
1415
- Replaced verbose null-check patterns with `??` null-coalescing operator
16+
- Entry point refactored to `try/catch/finally` pattern for guaranteed output file writing
17+
- Integration tests now perform actual git clones (no `-DryRun`) with full recursive checkout
18+
- Integration tests validate structured JSON output schema, not just exit codes
19+
- Integration tests clean up cloned repos between runs for test isolation
20+
- API incompatibility test configs now use `"Dependency File Name"` override for recursive lookup
1521

1622
### Added
23+
24+
- **Cross-platform SSH support**: OpenSSH on macOS/Linux via `GIT_SSH_COMMAND`, PuTTY/plink on Windows
25+
- `Test-SshTransportAvailable` replaces `Test-PlinkInstalled` (platform-aware)
26+
- `Set-GitSshKeyOpenSsh` for Unix: validates key format, checks permissions, sets `GIT_SSH_COMMAND`
27+
- `Set-GitSshKeyPlink` for Windows: existing PuTTY/Pageant logic (unchanged behavior)
28+
- `Read-CredentialsFile` now warns about wrong key format for the current platform
1729
- `LsiGitCheckout.psm1` module file containing all function definitions
1830
- `LsiGitCheckout.psd1` module manifest
1931
- `Initialize-LsiGitCheckout` function for module state initialization
2032
- Automated unit tests using Pester 5.x (`tests/LsiGitCheckout.Unit.Tests.ps1`)
2133
- Automated integration tests for all 16 test configs (`tests/LsiGitCheckout.Integration.Tests.ps1`)
34+
- **Structured JSON output** via `-OutputFile` parameter for CI/CD pipeline integration
35+
- Schema version 1.0.0 with execution metadata, per-repository results, summary counters, and error messages
36+
- `Export-CheckoutResults` function generates the JSON output
37+
- Output is guaranteed even on failure (written in `finally` block)
38+
- **Post-checkout script tracking** in JSON output per repository (`postCheckoutScript` field with configured, found, executed, status, reason) and at root level (`rootPostCheckoutScripts` array)
39+
- **`requestedBy` field** on each repository entry showing which parent repo or dependency file requested it
40+
- `Test-InteractiveSession` helper to detect non-interactive environments
41+
- Error message collector in `Write-Log` for structured output
42+
- `docs/test_repositories_reference.md` — catalog of all test repo tags and their dependencies
43+
44+
### Fixed
45+
46+
- **Show-ErrorDialog pipeline leakage**: `MessageBox::Show()` return value leaked to pipeline, causing failures to be counted as successes
47+
- **Non-interactive GUI dialogs**: `Show-ErrorDialog` and `Show-ConfirmDialog` now skip GUI in `pwsh -NonInteractive` and CI environments
48+
- **SemVer version parsing in DryRun**: added guard to skip parsing when repo is not cloned
49+
- **SemVer version resolution with `-DisableRecursion`** (#16): version resolution was gated behind `RecursiveMode`, causing empty tags when using `-DisableRecursion` with SemVer mode
2250

2351
## [7.1.0] - 2025-09-02
2452

CLAUDE.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ PowerShell-based dependency management tool that checks out multiple Git reposit
1111
.\LsiGitCheckout.ps1 -InputFile "path/to/deps.json" # custom config
1212
.\LsiGitCheckout.ps1 -DryRun # preview without executing
1313
.\LsiGitCheckout.ps1 -EnableDebug -EnableErrorContext # full debug output
14+
.\LsiGitCheckout.ps1 -OutputFile result.json # structured JSON output
1415
```
1516

16-
Key parameters: `-InputFile`, `-CredentialsFile`, `-DryRun`, `-EnableDebug`, `-DisableRecursion`, `-MaxDepth` (default 5), `-ApiCompatibility` (Strict|Permissive), `-DisablePostCheckoutScripts`, `-EnableErrorContext`
17+
Key parameters: `-InputFile`, `-CredentialsFile`, `-DryRun`, `-EnableDebug`, `-DisableRecursion`, `-MaxDepth` (default 5), `-ApiCompatibility` (Strict|Permissive), `-DisablePostCheckoutScripts`, `-EnableErrorContext`, `-OutputFile` (structured JSON results)
1718

1819
## Testing
1920

@@ -32,13 +33,17 @@ Invoke-Pester ./tests/LsiGitCheckout.Integration.Tests.ps1 -Output Detailed
3233

3334
### Manual Testing
3435

35-
Test configs in `tests/` can also be run manually:
36+
Test configs in `tests/` are organized in subdirectories, each containing a `dependencies.json`:
3637

3738
```powershell
38-
.\LsiGitCheckout.ps1 -InputFile tests/dependencies_semver.json -DryRun
39+
.\LsiGitCheckout.ps1 -InputFile tests/semver-basic/dependencies.json
40+
.\LsiGitCheckout.ps1 -InputFile tests/agnostic-recursive/dependencies.json
41+
.\LsiGitCheckout.ps1 -InputFile tests/api-incompatibility-agnostic/dependencies.json -ApiCompatibility Strict
3942
```
4043

41-
There are 16 test JSON configs covering SemVer, Agnostic, API incompatibility, custom paths, post-checkout scripts, and recursive dependencies.
44+
There are 17 test cases across 16 test configs covering SemVer, Agnostic, API incompatibility (Permissive + Strict), custom paths, post-checkout scripts, and recursive dependencies. See `docs/testing_infrastructure.md` for the full test architecture.
45+
46+
**Important**: Integration tests depend on 5 external GitHub test repos. Modifying those repos will break the tests. See `docs/testing_infrastructure.md` for details.
4247

4348
## Architecture
4449

@@ -48,8 +53,15 @@ There are 16 test JSON configs covering SemVer, Agnostic, API incompatibility, c
4853
- **Two dependency resolution modes**: SemVer (recommended, automatic version resolution) and Agnostic (explicit tag-based)
4954
- **Configuration**: JSON files — `dependencies.json` for repos, `git_credentials.json` for SSH keys
5055
- **Recursive processing**: walks dependency trees with conflict detection, max depth configurable
51-
- **SSH**: PuTTY/Pageant integration for authentication (`.ppk` key format)
56+
- **SSH**: Cross-platform — PuTTY/Pageant on Windows (`.ppk`), OpenSSH on macOS/Linux
5257
- **Post-checkout scripts**: optional PowerShell scripts run after successful checkouts
58+
- **Structured output**: `-OutputFile` writes JSON (schema 1.0.0) with per-repo results, post-checkout script tracking, and `requestedBy` parent chain
59+
60+
## Design Decisions
61+
62+
- **API compatibility in Agnostic mode**: In **Permissive** mode (default), version/tag conflicts during recursive checkout are resolved silently by picking the best available tag. In **Strict** mode, any tag mismatch is an error. This is controlled by `-ApiCompatibility` (CLI) or `"API Compatibility"` (per-repo JSON field).
63+
- **SemVer major version conflicts**: SemVer mode always rejects cross-major version incompatibilities regardless of the API compatibility setting, since different major versions imply breaking API changes by SemVer convention.
64+
- **SSH transport is platform-specific**: On Windows, PuTTY/plink with `.ppk` keys and Pageant is used. On macOS/Linux, OpenSSH is used via `GIT_SSH_COMMAND="ssh -i <key> -o IdentitiesOnly=yes"`, which specifies keys per-host without requiring `~/.ssh/config` changes or a running `ssh-agent`. **Why PuTTY on Windows**: We attempted OpenSSH on Windows but hit a specific failure: when a parent repository is cloned via HTTPS and has submodules accessed via SSH, the `git submodule update` process on Windows did not reliably inherit `GIT_SSH_COMMAND`/`GIT_SSH` environment variables, causing SSH submodule fetches to fail silently or hang. PuTTY/plink with Pageant (which manages keys via a system-tray agent process rather than environment variables) was the only reliable workaround. This issue was not reproduced on macOS/Linux, where environment variable inheritance across git subprocess forks works correctly.
5365

5466
## Coding Conventions
5567

@@ -71,13 +83,18 @@ LsiGitCheckout.psd1 # Module manifest
7183
CHANGELOG.md # Version history
7284
README.md # Comprehensive user documentation
7385
docs/
74-
developer_guide.md # Developer setup, testing, debugging
75-
comparison_guide.md # vs Google Repo Tool
76-
migration_guide.md # Migration strategies
86+
developer_guide.md # Developer setup, testing, debugging
87+
comparison_guide.md # vs Google Repo Tool
88+
migration_guide.md # Migration strategies
89+
test_repositories_reference.md # Test repo tags and dependency data
90+
testing_infrastructure.md # Test architecture, constraints, and categories
7791
examples/ # 7 example dependency JSON configs
78-
tests/ # 16 test JSON configs + Pester test files
79-
LsiGitCheckout.Unit.Tests.ps1 # Unit tests (no network)
80-
LsiGitCheckout.Integration.Tests.ps1 # Integration tests (needs network)
92+
tests/ # Pester test files + 16 test config subdirectories
93+
LsiGitCheckout.Unit.Tests.ps1 # 65 unit tests (no network)
94+
LsiGitCheckout.Integration.Tests.ps1 # 17 integration tests (needs network)
95+
semver-basic/dependencies.json # Test configs in subdirectories
96+
agnostic-recursive/dependencies.json # (16 subdirectories total)
97+
api-incompatibility-*/dependencies.json
8198
```
8299

83100
## Key Domain Concepts

LsiGitCheckout.ps1

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@ param(
8484
[switch]$DisablePostCheckoutScripts,
8585

8686
[Parameter()]
87-
[switch]$EnableErrorContext
87+
[switch]$EnableErrorContext,
88+
89+
[Parameter()]
90+
[string]$OutputFile
8891
)
8992

9093
# Import module from same directory as this script
9194
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
92-
Import-Module (Join-Path $scriptDir 'LsiGitCheckout.psm1') -Force
95+
Import-Module (Join-Path $scriptDir 'LsiGitCheckout.psm1') -Force -DisableNameChecking
9396

9497
# Initialize module state from script parameters
9598
Initialize-LsiGitCheckout `
@@ -100,9 +103,11 @@ Initialize-LsiGitCheckout `
100103
-MaxDepth $MaxDepth `
101104
-ApiCompatibility $ApiCompatibility `
102105
-DisablePostCheckoutScripts:$DisablePostCheckoutScripts `
103-
-EnableErrorContext:$EnableErrorContext
106+
-EnableErrorContext:$EnableErrorContext `
107+
-OutputFile $OutputFile
104108

105109
# Main execution
110+
$exitCode = 0
106111
try {
107112
Write-Log "LsiGitCheckout started - Version 8.0.0" -Level Info
108113
Write-Log "Script path: $scriptDir" -Level Debug
@@ -128,6 +133,10 @@ try {
128133
Write-Log "Error context: DISABLED - Use -EnableErrorContext for detailed error information" -Level Debug
129134
}
130135

136+
if ($OutputFile) {
137+
Write-Log "Structured output will be written to: $OutputFile" -Level Info
138+
}
139+
131140
# Calculate and log script hash in debug mode
132141
if ($EnableDebug) {
133142
$scriptContent = Get-Content -Path $MyInvocation.MyCommand.Path -Raw
@@ -149,7 +158,7 @@ try {
149158

150159
# Check Git installation
151160
if (-not (Test-GitInstalled)) {
152-
exit 1
161+
throw "Git is not installed or not accessible in PATH"
153162
}
154163

155164
# Determine input file path
@@ -175,10 +184,7 @@ try {
175184

176185
# Check if input file exists
177186
if (-not (Test-Path $InputFile)) {
178-
$errorMessage = "Input file not found: $InputFile"
179-
Write-Log $errorMessage -Level Error
180-
Show-ErrorDialog -Message $errorMessage
181-
exit 1
187+
throw "Input file not found: $InputFile"
182188
}
183189

184190
# Process the initial dependency file with enhanced error handling
@@ -236,19 +242,27 @@ try {
236242
# Show summary
237243
Show-Summary
238244

239-
# Get failure count from module
245+
# Determine exit code from failure count
240246
$failureCount = & (Get-Module LsiGitCheckout) { $script:FailureCount }
241-
242-
# Exit with appropriate code
243247
if ($failureCount -gt 0) {
244-
exit 1
245-
}
246-
else {
247-
exit 0
248+
$exitCode = 1
248249
}
249250
}
250251
catch {
251252
Write-ErrorWithContext -ErrorRecord $_ -AdditionalMessage "Unexpected error in main execution"
252253
Show-ErrorDialog -Message $_.Exception.Message
253-
exit 1
254+
$exitCode = 1
254255
}
256+
finally {
257+
# Write structured output if requested — guaranteed even on failure
258+
if (-not [string]::IsNullOrEmpty($OutputFile)) {
259+
try {
260+
Export-CheckoutResults -OutputFile $OutputFile
261+
}
262+
catch {
263+
Write-Host "Failed to write output file: $_" -ForegroundColor Red
264+
}
265+
}
266+
}
267+
268+
exit $exitCode

LsiGitCheckout.psd1

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
'Show-ConfirmDialog',
2727
'Test-GitInstalled',
2828
'Test-GitLfsInstalled',
29-
'Test-PlinkInstalled',
29+
'Test-SshTransportAvailable',
3030
'Get-RepositoryUrl',
3131
'Get-HostnameFromUrl',
3232
'Get-SshKeyForUrl',
@@ -45,6 +45,8 @@
4545
'Process-DependencyFile',
4646
'Process-RecursiveDependencies',
4747
'Read-CredentialsFile',
48+
'Set-PostCheckoutScriptResult',
49+
'Export-CheckoutResults',
4850
'Show-Summary'
4951
)
5052

0 commit comments

Comments
 (0)