Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0954f89
feat: Add CMake FetchContent support to updater
vaind Sep 18, 2025
891ea75
fix: Complete CMake FetchContent implementation
vaind Sep 18, 2025
9566917
docs: Fix CMake examples to be more logical
vaind Sep 18, 2025
ca1444d
docs: Refactor path input description into cleaner sublist
vaind Sep 18, 2025
c676be0
fix: cleanup CMake file handling in update-dependency script
vaind Sep 18, 2025
e11ac81
fix: ensure newline at end of file in Update-CMakeFile function
vaind Sep 18, 2025
0cea893
refactor: Use cross-platform temp directory approach
vaind Sep 18, 2025
e0b5a39
security: Fix ancestry validation to fail safely
vaind Sep 18, 2025
b9c8c4f
refactor: Simplify GIT_TAG line replacement logic
vaind Sep 18, 2025
8c0107a
fix: Add proper error handling for git ls-remote commands
vaind Sep 18, 2025
f058514
test: Add missing hash-to-hash update test case
vaind Sep 18, 2025
2f572cc
refactor: Inline test data and group related test cases
vaind Sep 19, 2025
b419603
refactor: Improve test structure with shared data and individual cases
vaind Sep 19, 2025
6888e3f
refactor: Reorganize test hierarchy for better clarity
vaind Sep 19, 2025
814a5c5
test: Use exact hash instead of regex pattern in assertion
vaind Sep 19, 2025
6b48177
test: Use exact hash in integration test assertion
vaind Sep 19, 2025
d7e850f
test: Use exact version in remaining integration test assertions
vaind Sep 19, 2025
132845a
revert: Use generic patterns in integration tests without version con…
vaind Sep 19, 2025
cd95e70
docs: Add changelog entry for CMake FetchContent support
vaind Sep 19, 2025
df907ad
Add parameter validation to CMake helper functions
vaind Sep 19, 2025
58cef75
Add dependency name validation in update-dependency.ps1
vaind Sep 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/updater.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
workflow_call:
inputs:
path:
description: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script.
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.
type: string
required: true
name:
Expand Down Expand Up @@ -87,9 +87,9 @@ jobs:
- name: Validate dependency path
shell: pwsh
run: |
# Validate that inputs.path contains only safe characters
if ('${{ inputs.path }}' -notmatch '^[a-zA-Z0-9_\./-]+$') {
Write-Output "::error::Invalid dependency path: '${{ inputs.path }}'. Only alphanumeric characters and _-./ are allowed."
# Validate that inputs.path contains only safe characters (including # for CMake dependencies)
if ('${{ inputs.path }}' -notmatch '^[a-zA-Z0-9_\./#-]+$') {
Write-Output "::error::Invalid dependency path: '${{ inputs.path }}'. Only alphanumeric characters and _-./# are allowed."
exit 1
}
Write-Output "✓ Dependency path '${{ inputs.path }}' is valid"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add Proguard artifact endpoint for Android builds in sentry-server ([#100](https://github.com/getsentry/github-workflows/pull/100))
- Updater - Add CMake FetchContent support for automated dependency updates ([#104](https://github.com/getsentry/github-workflows/pull/104))

### Security

Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,35 @@ jobs:
name: Gradle Plugin
secrets:
api-token: ${{ secrets.CI_DEPLOY_KEY }}

# Update a CMake FetchContent dependency with auto-detection (single dependency only)
sentry-native:
uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
with:
path: vendor/sentry-native.cmake
name: Sentry Native SDK
secrets:
api-token: ${{ secrets.CI_DEPLOY_KEY }}

# Update a CMake FetchContent dependency with explicit dependency name
deps:
uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
with:
path: vendor/dependencies.cmake#googletest
name: GoogleTest
secrets:
api-token: ${{ secrets.CI_DEPLOY_KEY }}
```

### Inputs

* `path`: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script.
* `path`: Dependency path in the source repository. Supported formats:
* Submodule path
* Properties file (`.properties`)
* Shell script (`.ps1`, `.sh`)
* CMake file with FetchContent:
* `path/to/file.cmake#DepName` - specify dependency name
* `path/to/file.cmake` - auto-detection (single dependency only)
* type: string
* required: true
* `name`: Name used in the PR title and the changelog entry.
Expand Down
187 changes: 187 additions & 0 deletions updater/scripts/cmake-functions.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# CMake FetchContent helper functions for update-dependency.ps1

function Parse-CMakeFetchContent {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Leaf})]
[string]$filePath,

[Parameter(Mandatory=$false)]
[ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})]
[string]$depName
)
$content = Get-Content $filePath -Raw

if ($depName) {
$pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)"
} else {
# Find all FetchContent_Declare blocks
$allMatches = [regex]::Matches($content, "FetchContent_Declare\s*\(\s*([a-zA-Z0-9_-]+)", 'Singleline')
if ($allMatches.Count -eq 1) {
$depName = $allMatches[0].Groups[1].Value
$pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)"
} else {
throw "Multiple FetchContent declarations found. Use #DepName syntax."
}
}

$match = [regex]::Match($content, $pattern, 'Singleline,IgnoreCase')
if (-not $match.Success) {
throw "FetchContent_Declare for '$depName' not found in $filePath"
}
$block = $match.Groups[1].Value

# Look for GIT_REPOSITORY and GIT_TAG patterns specifically
# Exclude matches that are in comments (lines starting with #)
$repoMatch = [regex]::Match($block, '(?m)^\s*GIT_REPOSITORY\s+(\S+)')
$tagMatch = [regex]::Match($block, '(?m)^\s*GIT_TAG\s+(\S+)')

$repo = if ($repoMatch.Success) { $repoMatch.Groups[1].Value } else { "" }
$tag = if ($tagMatch.Success) { $tagMatch.Groups[1].Value } else { "" }

if ([string]::IsNullOrEmpty($repo) -or [string]::IsNullOrEmpty($tag)) {
throw "Could not parse GIT_REPOSITORY or GIT_TAG from FetchContent_Declare block"
}

return @{ GitRepository = $repo; GitTag = $tag; DepName = $depName }
}

function Find-TagForHash {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$repo,

[Parameter(Mandatory=$true)]
[ValidatePattern('^[a-f0-9]{40}$')]
[string]$hash
)
try {
$refs = git ls-remote --tags $repo
if ($LASTEXITCODE -ne 0) {
throw "Failed to fetch tags from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)"
}
foreach ($ref in $refs) {
$commit, $tagRef = $ref -split '\s+', 2
if ($commit -eq $hash) {
return $tagRef -replace '^refs/tags/', ''
}
}
return $null
}
catch {
Write-Host "Warning: Could not resolve hash $hash to tag name: $_"
return $null
}
}

function Test-HashAncestry {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$repo,

[Parameter(Mandatory=$true)]
[ValidatePattern('^[a-f0-9]{40}$')]
[string]$oldHash,

[Parameter(Mandatory=$true)]
[ValidatePattern('^[a-f0-9]{40}$')]
[string]$newHash
)
try {
# Create a temporary directory for git operations
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid())
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null

try {
Push-Location $tempDir

# Initialize a bare repository and add the remote
git init --bare 2>$null | Out-Null
git remote add origin $repo 2>$null | Out-Null

# Fetch both commits
git fetch origin $oldHash 2>$null | Out-Null
git fetch origin $newHash 2>$null | Out-Null

# Check if old hash is ancestor of new hash
git merge-base --is-ancestor $oldHash $newHash 2>$null
$isAncestor = $LastExitCode -eq 0

return $isAncestor
}
finally {
Pop-Location
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
catch {
Write-Host "Error: Could not validate ancestry for $oldHash -> $newHash : $_"
# When in doubt, fail safely to prevent incorrect updates
return $false
}
}

function Update-CMakeFile {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Leaf})]
[string]$filePath,

[Parameter(Mandatory=$false)]
[ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})]
[string]$depName,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$newValue
)
$content = Get-Content $filePath -Raw
$fetchContent = Parse-CMakeFetchContent $filePath $depName
$originalValue = $fetchContent.GitTag
$repo = $fetchContent.GitRepository
$wasHash = $originalValue -match '^[a-f0-9]{40}$'

if ($wasHash) {
# Convert tag to hash and add comment
$newHashRefs = git ls-remote $repo "refs/tags/$newValue"
if ($LASTEXITCODE -ne 0) {
throw "Failed to fetch tag $newValue from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)"
}
if (-not $newHashRefs) {
throw "Tag $newValue not found in repository $repo"
}
$newHash = ($newHashRefs -split '\s+')[0]
$replacement = "$newHash # $newValue"

# Validate ancestry: ensure old hash is reachable from new tag
if (-not (Test-HashAncestry $repo $originalValue $newHash)) {
throw "Cannot update: hash $originalValue is not in history of tag $newValue"
}
} else {
$replacement = $newValue
}

# Update GIT_TAG value, replacing entire line content after GIT_TAG
# This removes potentially outdated version-specific comments
$pattern = "(FetchContent_Declare\s*\(\s*$depName\s+[^)]*GIT_TAG\s+)[^\r\n]+(\r?\n[^)]*\))"
$newContent = [regex]::Replace($content, $pattern, "`${1}$replacement`${2}", 'Singleline')

if ($newContent -eq $content) {
throw "Failed to update GIT_TAG in $filePath - pattern may not have matched"
}

$newContent | Out-File $filePath -NoNewline

# Verify the update worked
$verifyContent = Parse-CMakeFetchContent $filePath $depName
$expectedValue = $wasHash ? $newHash : $newValue
if ($verifyContent.GitTag -notmatch [regex]::Escape($expectedValue)) {
throw "Update verification failed - read-after-write did not match expected value"
}
}
51 changes: 50 additions & 1 deletion updater/scripts/update-dependency.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ param(
# * `get-version` - return the currently specified dependency version
# * `get-repo` - return the repository url (e.g. https://github.com/getsentry/dependency)
# * `set-version` - update the dependency version (passed as another string argument after this one)
# - a CMake file (.cmake) with FetchContent_Declare statements:
# * Use `path/to/file.cmake#DepName` to specify dependency name
# * Or just `path/to/file.cmake` if file contains single FetchContent_Declare
[Parameter(Mandatory = $true)][string] $Path,
# RegEx pattern that will be matched against available versions when picking the latest one
[string] $Pattern = '',
Expand All @@ -16,6 +19,24 @@ param(
Set-StrictMode -Version latest
. "$PSScriptRoot/common.ps1"

# Parse CMake file with dependency name
if ($Path -match '^(.+\.cmake)(#(.+))?$') {
$Path = $Matches[1] # Set Path to file for existing logic
if ($Matches[3]) {
$cmakeDep = $Matches[3]
# Validate dependency name follows CMake naming conventions
if ($cmakeDep -notmatch '^[a-zA-Z][a-zA-Z0-9_.-]*$') {
throw "Invalid CMake dependency name: '$cmakeDep'. Must start with letter and contain only alphanumeric, underscore, dot, or hyphen."
}
} else {
$cmakeDep = $null # Will auto-detect
}
$isCMakeFile = $true
} else {
$cmakeDep = $null
$isCMakeFile = $false
}

if (-not (Test-Path $Path ))
{
throw "Dependency $Path doesn't exit";
Expand All @@ -41,7 +62,32 @@ if (-not $isSubmodule)
$isScript = $Path -match '\.(ps1|sh)$'
function DependencyConfig ([Parameter(Mandatory = $true)][string] $action, [string] $value = $null)
{
if ($isScript)
if ($isCMakeFile) {
# CMake file handling
switch ($action) {
'get-version' {
$fetchContent = Parse-CMakeFetchContent $Path $cmakeDep
$currentValue = $fetchContent.GitTag
if ($currentValue -match '^[a-f0-9]{40}$') {
# Try to resolve hash to tag for version comparison
$repo = $fetchContent.GitRepository
$tagForHash = Find-TagForHash $repo $currentValue
return $tagForHash ?? $currentValue
}
return $currentValue
}
'get-repo' {
return (Parse-CMakeFetchContent $Path $cmakeDep).GitRepository
}
'set-version' {
Update-CMakeFile $Path $cmakeDep $value
}
Default {
throw "Unknown action $action"
}
}
}
elseif ($isScript)
{
if (Get-Command 'chmod' -ErrorAction SilentlyContinue)
{
Expand Down Expand Up @@ -99,6 +145,9 @@ if (-not $isSubmodule)
}
}
}

# Load CMake helper functions
. "$PSScriptRoot/cmake-functions.ps1"
}

if ("$Tag" -eq '')
Expand Down
Loading
Loading