Skip to content

Commit a4ff0c2

Browse files
vaindclaude
andauthored
feat: Add CMake FetchContent support to updater (#104)
* feat: Add CMake FetchContent support to updater Implements support for updating CMake FetchContent_Declare() statements in addition to existing submodules, properties files, and scripts. Key features: - Support for path.cmake#DepName and auto-detection syntax - Hash vs tag detection with hash format preservation - Hash-to-tag resolution for version comparison - GitHub Actions output integration - Comprehensive test coverage (23 tests) Resolves: #91 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Complete CMake FetchContent implementation Critical fixes and improvements: - Fix GitHub Actions workflow validation to allow # character in paths - Update documentation with CMake examples and usage - Improve comment handling in hash updates - Implement proper ancestry validation for hash updates - Test with real console SDK CMake files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: Fix CMake examples to be more logical - sentry-native.cmake now uses auto-detection (single dependency) - dependencies.cmake now shows explicit dependency name syntax - Better reflects real-world usage patterns * docs: Refactor path input description into cleaner sublist - Split long bullet point into structured sublist - Clear separation of different path format types - Better readability for CMake file options * fix: cleanup CMake file handling in update-dependency script * fix: ensure newline at end of file in Update-CMakeFile function * refactor: Use cross-platform temp directory approach - Replace $env:TEMP with [System.IO.Path]::GetTempPath() - Use [System.Guid]::NewGuid() for unique directory names - More robust cross-platform compatibility * security: Fix ancestry validation to fail safely - Return false instead of true when ancestry validation fails - Change warning to error message for clarity - Prevents potentially incorrect updates when validation is uncertain - Follows fail-safe principle for security-critical operations * refactor: Simplify GIT_TAG line replacement logic - Always replace entire line content after GIT_TAG - Removes potentially outdated version-specific comments - Simplifies regex pattern (no separate hash/tag logic needed) - Cleaner and more predictable behavior * fix: Add proper error handling for git ls-remote commands - Check $LASTEXITCODE after git ls-remote calls - Prevent parsing error messages as commit hashes - Fixes potential corruption of CMake files with 'fatal:' etc. - Applies to both Update-CMakeFile and Find-TagForHash functions Fixes critical bug where network failures could corrupt dependency files. * test: Add missing hash-to-hash update test case - Tests updating from one git hash to a newer tag's hash - Covers important scenario of hash-to-hash updates - Verifies hash format preservation and comment replacement - Ensures old hash and comments are properly removed * refactor: Inline test data and group related test cases - Move CMake test data from external files to inline here-strings - Group related test scenarios into single test cases for better readability - Reduce test count from 16 to 6 while maintaining same coverage - Remove external testdata/cmake/ directory (no longer needed) - Improve test maintainability - all test input/output visible in one place Test groupings: - Parse scenarios: basic, auto-detect, hash, complex formatting - Multiple deps: auto-detection errors, explicit selection - Error scenarios: missing deps, missing repo/tag - Hash resolution: null results, network failures - Update scenarios: tag-to-tag, hash-to-hash, complex formatting - Update errors: missing dependency updates * refactor: Improve test structure with shared data and individual cases - Move test data creation to Context BeforeAll level - Restore individual test cases (16 total) for focused testing - Eliminate data duplication while keeping inline visibility - Best of both worlds: shared setup + granular test cases Structure: - Context BeforeAll: Creates shared test files with inline data - Individual It blocks: Reference shared files for specific scenarios - Clear test names and focused assertions per test case * refactor: Reorganize test hierarchy for better clarity - Promote function names to Describe level (Parse-CMakeFetchContent, Find-TagForHash, Update-CMakeFile) - Group tests by CMake file type at Context level - Each Context has its own test data (no duplication) - Clear logical organization: function -> file type -> specific tests Structure: ├── Describe 'Parse-CMakeFetchContent' │ ├── Context 'Basic single dependency file' (3 tests) │ ├── Context 'Hash-based dependency file' (1 test) │ ├── Context 'Complex formatting file' (1 test) │ ├── Context 'Multiple dependencies file' (2 tests) │ └── Context 'Malformed files' (2 tests) ├── Describe 'Find-TagForHash' │ └── Context 'Hash resolution scenarios' (2 tests) └── Describe 'Update-CMakeFile' ├── Context 'Basic tag updates' (3 tests) ├── Context 'Hash updates' (1 test) └── Context 'Complex formatting' (1 test) * test: Use exact hash instead of regex pattern in assertion - Replace generic pattern [a-f0-9]{40} with actual 0.11.0 hash - More precise assertion: 3bd091313ae97be90be62696a2babe591a988eb8 - Consistent with integration test data expectations - Eliminates ambiguity in test validation * test: Use exact hash in integration test assertion - Replace generic pattern [a-f0-9]{40} # \d+\.\d+\.\d+ with exact values - More precise assertion: 3bd091313ae97be90be62696a2babe591a988eb8 # 0\.11\.0 - Matches unit test precision and validates exact expected output - Eliminates ambiguity in hash-to-tag update validation * test: Use exact version in remaining integration test assertions - Replace generic \d+\.\d+\.\d+ patterns with exact 0\.11\.0 - More precise assertions for explicit dependency and auto-detection tests - Completes migration from generic patterns to exact expected values - Ensures deterministic test validation across all CMake tests * revert: Use generic patterns in integration tests without version constraints - Revert exact version assertions where UpdateDependency gets latest version - Keep generic patterns \d+\.\d+\.\d+ and [a-f0-9]{40} for future-proof tests - Integration tests call UpdateDependency without pattern constraints - Latest version will change over time (0.11.0 → 0.12.0, etc.) - Unit tests can keep exact values since they specify exact versions * docs: Add changelog entry for CMake FetchContent support - Document new CMake FetchContent functionality in CHANGELOG.md - References PR #104 for automated dependency updates - Follows existing changelog format and conventions * Add parameter validation to CMake helper functions Added robust parameter validation with type constraints to all CMake helper functions: - Parse-CMakeFetchContent: Validates file path exists and dependency name format - Find-TagForHash: Validates repository URL and 40-char hash format - Test-HashAncestry: Validates repository URL and hash formats - Update-CMakeFile: Validates file path, dependency name, and new value This prevents misuse, improves error handling, and addresses security concerns around parameter injection attacks. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add dependency name validation in update-dependency.ps1 Added validation to ensure CMake dependency names follow proper naming conventions and prevent potential regex injection attacks. Dependency names must start with a letter and contain only alphanumeric characters, underscores, dots, or hyphens. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 672dd4d commit a4ff0c2

File tree

7 files changed

+790
-6
lines changed

7 files changed

+790
-6
lines changed

.github/workflows/updater.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
workflow_call:
44
inputs:
55
path:
6-
description: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script.
6+
description: Dependency path in the source repository, this can be either a submodule, a .properties file, a shell script, or a CMake file with FetchContent.
77
type: string
88
required: true
99
name:
@@ -87,9 +87,9 @@ jobs:
8787
- name: Validate dependency path
8888
shell: pwsh
8989
run: |
90-
# Validate that inputs.path contains only safe characters
91-
if ('${{ inputs.path }}' -notmatch '^[a-zA-Z0-9_\./-]+$') {
92-
Write-Output "::error::Invalid dependency path: '${{ inputs.path }}'. Only alphanumeric characters and _-./ are allowed."
90+
# Validate that inputs.path contains only safe characters (including # for CMake dependencies)
91+
if ('${{ inputs.path }}' -notmatch '^[a-zA-Z0-9_\./#-]+$') {
92+
Write-Output "::error::Invalid dependency path: '${{ inputs.path }}'. Only alphanumeric characters and _-./# are allowed."
9393
exit 1
9494
}
9595
Write-Output "✓ Dependency path '${{ inputs.path }}' is valid"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Danger - Improve conventional commit scope handling, and non-conventional PR title support ([#105](https://github.com/getsentry/github-workflows/pull/105))
88
- Add Proguard artifact endpoint for Android builds in sentry-server ([#100](https://github.com/getsentry/github-workflows/pull/100))
9+
- Updater - Add CMake FetchContent support for automated dependency updates ([#104](https://github.com/getsentry/github-workflows/pull/104))
910

1011
### Security
1112

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,35 @@ jobs:
4646
name: Gradle Plugin
4747
secrets:
4848
api-token: ${{ secrets.CI_DEPLOY_KEY }}
49+
50+
# Update a CMake FetchContent dependency with auto-detection (single dependency only)
51+
sentry-native:
52+
uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
53+
with:
54+
path: vendor/sentry-native.cmake
55+
name: Sentry Native SDK
56+
secrets:
57+
api-token: ${{ secrets.CI_DEPLOY_KEY }}
58+
59+
# Update a CMake FetchContent dependency with explicit dependency name
60+
deps:
61+
uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
62+
with:
63+
path: vendor/dependencies.cmake#googletest
64+
name: GoogleTest
65+
secrets:
66+
api-token: ${{ secrets.CI_DEPLOY_KEY }}
4967
```
5068
5169
### Inputs
5270
53-
* `path`: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script.
71+
* `path`: Dependency path in the source repository. Supported formats:
72+
* Submodule path
73+
* Properties file (`.properties`)
74+
* Shell script (`.ps1`, `.sh`)
75+
* CMake file with FetchContent:
76+
* `path/to/file.cmake#DepName` - specify dependency name
77+
* `path/to/file.cmake` - auto-detection (single dependency only)
5478
* type: string
5579
* required: true
5680
* `name`: Name used in the PR title and the changelog entry.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# CMake FetchContent helper functions for update-dependency.ps1
2+
3+
function Parse-CMakeFetchContent {
4+
[CmdletBinding()]
5+
param(
6+
[Parameter(Mandatory=$true)]
7+
[ValidateScript({Test-Path $_ -PathType Leaf})]
8+
[string]$filePath,
9+
10+
[Parameter(Mandatory=$false)]
11+
[ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})]
12+
[string]$depName
13+
)
14+
$content = Get-Content $filePath -Raw
15+
16+
if ($depName) {
17+
$pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)"
18+
} else {
19+
# Find all FetchContent_Declare blocks
20+
$allMatches = [regex]::Matches($content, "FetchContent_Declare\s*\(\s*([a-zA-Z0-9_-]+)", 'Singleline')
21+
if ($allMatches.Count -eq 1) {
22+
$depName = $allMatches[0].Groups[1].Value
23+
$pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)"
24+
} else {
25+
throw "Multiple FetchContent declarations found. Use #DepName syntax."
26+
}
27+
}
28+
29+
$match = [regex]::Match($content, $pattern, 'Singleline,IgnoreCase')
30+
if (-not $match.Success) {
31+
throw "FetchContent_Declare for '$depName' not found in $filePath"
32+
}
33+
$block = $match.Groups[1].Value
34+
35+
# Look for GIT_REPOSITORY and GIT_TAG patterns specifically
36+
# Exclude matches that are in comments (lines starting with #)
37+
$repoMatch = [regex]::Match($block, '(?m)^\s*GIT_REPOSITORY\s+(\S+)')
38+
$tagMatch = [regex]::Match($block, '(?m)^\s*GIT_TAG\s+(\S+)')
39+
40+
$repo = if ($repoMatch.Success) { $repoMatch.Groups[1].Value } else { "" }
41+
$tag = if ($tagMatch.Success) { $tagMatch.Groups[1].Value } else { "" }
42+
43+
if ([string]::IsNullOrEmpty($repo) -or [string]::IsNullOrEmpty($tag)) {
44+
throw "Could not parse GIT_REPOSITORY or GIT_TAG from FetchContent_Declare block"
45+
}
46+
47+
return @{ GitRepository = $repo; GitTag = $tag; DepName = $depName }
48+
}
49+
50+
function Find-TagForHash {
51+
[CmdletBinding()]
52+
param(
53+
[Parameter(Mandatory=$true)]
54+
[ValidateNotNullOrEmpty()]
55+
[string]$repo,
56+
57+
[Parameter(Mandatory=$true)]
58+
[ValidatePattern('^[a-f0-9]{40}$')]
59+
[string]$hash
60+
)
61+
try {
62+
$refs = git ls-remote --tags $repo
63+
if ($LASTEXITCODE -ne 0) {
64+
throw "Failed to fetch tags from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)"
65+
}
66+
foreach ($ref in $refs) {
67+
$commit, $tagRef = $ref -split '\s+', 2
68+
if ($commit -eq $hash) {
69+
return $tagRef -replace '^refs/tags/', ''
70+
}
71+
}
72+
return $null
73+
}
74+
catch {
75+
Write-Host "Warning: Could not resolve hash $hash to tag name: $_"
76+
return $null
77+
}
78+
}
79+
80+
function Test-HashAncestry {
81+
[CmdletBinding()]
82+
param(
83+
[Parameter(Mandatory=$true)]
84+
[ValidateNotNullOrEmpty()]
85+
[string]$repo,
86+
87+
[Parameter(Mandatory=$true)]
88+
[ValidatePattern('^[a-f0-9]{40}$')]
89+
[string]$oldHash,
90+
91+
[Parameter(Mandatory=$true)]
92+
[ValidatePattern('^[a-f0-9]{40}$')]
93+
[string]$newHash
94+
)
95+
try {
96+
# Create a temporary directory for git operations
97+
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid())
98+
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
99+
100+
try {
101+
Push-Location $tempDir
102+
103+
# Initialize a bare repository and add the remote
104+
git init --bare 2>$null | Out-Null
105+
git remote add origin $repo 2>$null | Out-Null
106+
107+
# Fetch both commits
108+
git fetch origin $oldHash 2>$null | Out-Null
109+
git fetch origin $newHash 2>$null | Out-Null
110+
111+
# Check if old hash is ancestor of new hash
112+
git merge-base --is-ancestor $oldHash $newHash 2>$null
113+
$isAncestor = $LastExitCode -eq 0
114+
115+
return $isAncestor
116+
}
117+
finally {
118+
Pop-Location
119+
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
120+
}
121+
}
122+
catch {
123+
Write-Host "Error: Could not validate ancestry for $oldHash -> $newHash : $_"
124+
# When in doubt, fail safely to prevent incorrect updates
125+
return $false
126+
}
127+
}
128+
129+
function Update-CMakeFile {
130+
[CmdletBinding()]
131+
param(
132+
[Parameter(Mandatory=$true)]
133+
[ValidateScript({Test-Path $_ -PathType Leaf})]
134+
[string]$filePath,
135+
136+
[Parameter(Mandatory=$false)]
137+
[ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})]
138+
[string]$depName,
139+
140+
[Parameter(Mandatory=$true)]
141+
[ValidateNotNullOrEmpty()]
142+
[string]$newValue
143+
)
144+
$content = Get-Content $filePath -Raw
145+
$fetchContent = Parse-CMakeFetchContent $filePath $depName
146+
$originalValue = $fetchContent.GitTag
147+
$repo = $fetchContent.GitRepository
148+
$wasHash = $originalValue -match '^[a-f0-9]{40}$'
149+
150+
if ($wasHash) {
151+
# Convert tag to hash and add comment
152+
$newHashRefs = git ls-remote $repo "refs/tags/$newValue"
153+
if ($LASTEXITCODE -ne 0) {
154+
throw "Failed to fetch tag $newValue from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)"
155+
}
156+
if (-not $newHashRefs) {
157+
throw "Tag $newValue not found in repository $repo"
158+
}
159+
$newHash = ($newHashRefs -split '\s+')[0]
160+
$replacement = "$newHash # $newValue"
161+
162+
# Validate ancestry: ensure old hash is reachable from new tag
163+
if (-not (Test-HashAncestry $repo $originalValue $newHash)) {
164+
throw "Cannot update: hash $originalValue is not in history of tag $newValue"
165+
}
166+
} else {
167+
$replacement = $newValue
168+
}
169+
170+
# Update GIT_TAG value, replacing entire line content after GIT_TAG
171+
# This removes potentially outdated version-specific comments
172+
$pattern = "(FetchContent_Declare\s*\(\s*$depName\s+[^)]*GIT_TAG\s+)[^\r\n]+(\r?\n[^)]*\))"
173+
$newContent = [regex]::Replace($content, $pattern, "`${1}$replacement`${2}", 'Singleline')
174+
175+
if ($newContent -eq $content) {
176+
throw "Failed to update GIT_TAG in $filePath - pattern may not have matched"
177+
}
178+
179+
$newContent | Out-File $filePath -NoNewline
180+
181+
# Verify the update worked
182+
$verifyContent = Parse-CMakeFetchContent $filePath $depName
183+
$expectedValue = $wasHash ? $newHash : $newValue
184+
if ($verifyContent.GitTag -notmatch [regex]::Escape($expectedValue)) {
185+
throw "Update verification failed - read-after-write did not match expected value"
186+
}
187+
}

updater/scripts/update-dependency.ps1

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ param(
66
# * `get-version` - return the currently specified dependency version
77
# * `get-repo` - return the repository url (e.g. https://github.com/getsentry/dependency)
88
# * `set-version` - update the dependency version (passed as another string argument after this one)
9+
# - a CMake file (.cmake) with FetchContent_Declare statements:
10+
# * Use `path/to/file.cmake#DepName` to specify dependency name
11+
# * Or just `path/to/file.cmake` if file contains single FetchContent_Declare
912
[Parameter(Mandatory = $true)][string] $Path,
1013
# RegEx pattern that will be matched against available versions when picking the latest one
1114
[string] $Pattern = '',
@@ -16,6 +19,24 @@ param(
1619
Set-StrictMode -Version latest
1720
. "$PSScriptRoot/common.ps1"
1821

22+
# Parse CMake file with dependency name
23+
if ($Path -match '^(.+\.cmake)(#(.+))?$') {
24+
$Path = $Matches[1] # Set Path to file for existing logic
25+
if ($Matches[3]) {
26+
$cmakeDep = $Matches[3]
27+
# Validate dependency name follows CMake naming conventions
28+
if ($cmakeDep -notmatch '^[a-zA-Z][a-zA-Z0-9_.-]*$') {
29+
throw "Invalid CMake dependency name: '$cmakeDep'. Must start with letter and contain only alphanumeric, underscore, dot, or hyphen."
30+
}
31+
} else {
32+
$cmakeDep = $null # Will auto-detect
33+
}
34+
$isCMakeFile = $true
35+
} else {
36+
$cmakeDep = $null
37+
$isCMakeFile = $false
38+
}
39+
1940
if (-not (Test-Path $Path ))
2041
{
2142
throw "Dependency $Path doesn't exit";
@@ -41,7 +62,32 @@ if (-not $isSubmodule)
4162
$isScript = $Path -match '\.(ps1|sh)$'
4263
function DependencyConfig ([Parameter(Mandatory = $true)][string] $action, [string] $value = $null)
4364
{
44-
if ($isScript)
65+
if ($isCMakeFile) {
66+
# CMake file handling
67+
switch ($action) {
68+
'get-version' {
69+
$fetchContent = Parse-CMakeFetchContent $Path $cmakeDep
70+
$currentValue = $fetchContent.GitTag
71+
if ($currentValue -match '^[a-f0-9]{40}$') {
72+
# Try to resolve hash to tag for version comparison
73+
$repo = $fetchContent.GitRepository
74+
$tagForHash = Find-TagForHash $repo $currentValue
75+
return $tagForHash ?? $currentValue
76+
}
77+
return $currentValue
78+
}
79+
'get-repo' {
80+
return (Parse-CMakeFetchContent $Path $cmakeDep).GitRepository
81+
}
82+
'set-version' {
83+
Update-CMakeFile $Path $cmakeDep $value
84+
}
85+
Default {
86+
throw "Unknown action $action"
87+
}
88+
}
89+
}
90+
elseif ($isScript)
4591
{
4692
if (Get-Command 'chmod' -ErrorAction SilentlyContinue)
4793
{
@@ -99,6 +145,9 @@ if (-not $isSubmodule)
99145
}
100146
}
101147
}
148+
149+
# Load CMake helper functions
150+
. "$PSScriptRoot/cmake-functions.ps1"
102151
}
103152

104153
if ("$Tag" -eq '')

0 commit comments

Comments
 (0)