diff --git a/.gitea/workflows/publish-psgallery.yml b/.gitea/workflows/publish-psgallery.yml index 3db6610..4b21bff 100644 --- a/.gitea/workflows/publish-psgallery.yml +++ b/.gitea/workflows/publish-psgallery.yml @@ -6,7 +6,7 @@ on: branches: [main] jobs: - publish: + build: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: @@ -41,16 +41,6 @@ jobs: sudo apt-get install -y powershell pwsh --version - - name: Bootstrap PowerShellGet / NuGet provider - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null - } - Get-PackageProvider -Name NuGet | Format-Table Name,Version - - name: Build and test module shell: pwsh run: ./build.ps1 -RunTests @@ -63,6 +53,218 @@ jobs: $manifest = Test-ModuleManifest -Path $manifestPath Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)" + - name: Upload module artifact + uses: actions/upload-artifact@v4 + with: + name: PSInfisicalAPI-module + path: Module/PSInfisicalAPI + if-no-files-found: error + retention-days: 7 + + release: + needs: build + if: ${{ success() && github.event.pull_request.merged == true }} + runs-on: ubuntu-latest + outputs: + version: ${{ steps.meta.outputs.version }} + tag: ${{ steps.meta.outputs.tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install PowerShell 7 (if not present) + shell: bash + run: | + set -euo pipefail + if command -v pwsh >/dev/null 2>&1; then + echo "pwsh already installed: $(pwsh --version)" + exit 0 + fi + + sudo apt-get update + sudo apt-get install -y --no-install-recommends wget ca-certificates apt-transport-https gnupg + + source /etc/os-release + wget -q "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms-prod.deb + sudo dpkg -i /tmp/ms-prod.deb + rm -f /tmp/ms-prod.deb + + sudo apt-get update + sudo apt-get install -y powershell + pwsh --version + + - name: Download module artifact + uses: actions/download-artifact@v4 + with: + name: PSInfisicalAPI-module + path: Module/PSInfisicalAPI + + - name: Resolve module version and tag + id: meta + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $manifestPath = Join-Path $PWD 'Module/PSInfisicalAPI/PSInfisicalAPI.psd1' + $manifest = Test-ModuleManifest -Path $manifestPath + $version = $manifest.Version.ToString() + $tag = $version + Write-Host "Module version: $version" + Write-Host "Release tag: $tag" + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Package module as release asset + shell: pwsh + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + $ErrorActionPreference = 'Stop' + $zipPath = Join-Path $PWD "PSInfisicalAPI-$($env:VERSION).zip" + if (Test-Path $zipPath) { Remove-Item $zipPath -Force } + Compress-Archive -Path 'Module/PSInfisicalAPI/*' -DestinationPath $zipPath -Force + Write-Host "Created: $zipPath ($([math]::Round((Get-Item $zipPath).Length / 1KB, 1)) KB)" + + - name: Create Gitea release + shell: pwsh + env: + GITEA_TOKEN: ${{ github.token }} + API_URL: ${{ github.api_url }} + REPO: ${{ github.repository }} + TAG: ${{ steps.meta.outputs.tag }} + VERSION: ${{ steps.meta.outputs.version }} + COMMIT_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + SERVER_URL: ${{ github.server_url }} + RUN_ID: ${{ github.run_id }} + run: | + $ErrorActionPreference = 'Stop' + if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) { + throw "github.token is not available; cannot call the Gitea release API." + } + + $shortSha = $env:COMMIT_SHA.Substring(0, [Math]::Min(12, $env:COMMIT_SHA.Length)) + $buildUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $runUrl = "$($env:SERVER_URL)/$($env:REPO)/actions/runs/$($env:RUN_ID)" + $prUrl = "$($env:SERVER_URL)/$($env:REPO)/pulls/$($env:PR_NUMBER)" + + $changelogSection = '' + if (Test-Path 'CHANGELOG.md') { + $lines = [System.IO.File]::ReadAllLines('CHANGELOG.md') + $start = -1; $end = $lines.Length + for ($i = 0; $i -lt $lines.Length; $i++) { + if ($lines[$i] -match "^##\s+$([regex]::Escape($env:VERSION))\s*$") { $start = $i + 1; continue } + if ($start -ge 0 -and $lines[$i] -match '^##\s+') { $end = $i; break } + } + if ($start -ge 0) { + $changelogSection = ($lines[$start..($end - 1)] -join "`n").Trim() + } + } + + $body = @" + **PSInfisicalAPI $($env:VERSION)** + + | Field | Value | + | --- | --- | + | Version | ``$($env:VERSION)`` | + | Tag | ``$($env:TAG)`` | + | Commit | [``$shortSha``]($($env:SERVER_URL)/$($env:REPO)/commit/$($env:COMMIT_SHA)) | + | Built (UTC) | $buildUtc | + | Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) | + | Workflow run | [$($env:RUN_ID)]($runUrl) | + + ## Changes + $(if ($changelogSection) { $changelogSection } else { '_No CHANGELOG section found for this version._' }) + + ## Install + ``````powershell + Install-Module -Name PSInfisicalAPI -RequiredVersion $($env:VERSION) -Scope CurrentUser + `````` + "@ + + $headers = @{ + Authorization = "token $($env:GITEA_TOKEN)" + Accept = 'application/json' + } + $createUri = "$($env:API_URL)/repos/$($env:REPO)/releases" + + $existing = $null + try { + $existing = Invoke-RestMethod -Method Get -Headers $headers ` + -Uri "$createUri/tags/$($env:TAG)" -ErrorAction Stop + } catch { + if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw } + } + if ($existing) { + Write-Host "Release tag '$($env:TAG)' already exists (id=$($existing.id)); skipping release creation." + return + } + + $payload = @{ + tag_name = $env:TAG + target_commitish = $env:COMMIT_SHA + name = "PSInfisicalAPI $($env:VERSION)" + body = $body + draft = $false + prerelease = $false + } | ConvertTo-Json -Depth 4 + + $release = Invoke-RestMethod -Method Post -Uri $createUri -Headers $headers ` + -ContentType 'application/json' -Body $payload + Write-Host "Created release id=$($release.id) at $($release.html_url)" + + $assetPath = Join-Path $PWD "PSInfisicalAPI-$($env:VERSION).zip" + $uploadUri = "$createUri/$($release.id)/assets?name=PSInfisicalAPI-$($env:VERSION).zip" + $fileBytes = [System.IO.File]::ReadAllBytes($assetPath) + Invoke-RestMethod -Method Post -Uri $uploadUri -Headers $headers ` + -ContentType 'application/zip' -Body $fileBytes | Out-Null + Write-Host "Uploaded asset: PSInfisicalAPI-$($env:VERSION).zip" + + publish: + needs: release + if: ${{ success() && github.event.pull_request.merged == true }} + runs-on: ubuntu-latest + steps: + - name: Install PowerShell 7 (if not present) + shell: bash + run: | + set -euo pipefail + if command -v pwsh >/dev/null 2>&1; then + echo "pwsh already installed: $(pwsh --version)" + exit 0 + fi + + sudo apt-get update + sudo apt-get install -y --no-install-recommends wget ca-certificates apt-transport-https gnupg + + source /etc/os-release + wget -q "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms-prod.deb + sudo dpkg -i /tmp/ms-prod.deb + rm -f /tmp/ms-prod.deb + + sudo apt-get update + sudo apt-get install -y powershell + pwsh --version + + - name: Download module artifact + uses: actions/download-artifact@v4 + with: + name: PSInfisicalAPI-module + path: Module/PSInfisicalAPI + + - name: Bootstrap PowerShellGet / NuGet provider + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null + } + Get-PackageProvider -Name NuGet | Format-Table Name,Version + - name: Verify PowerShell Gallery API key is configured shell: pwsh env: @@ -72,6 +274,14 @@ jobs: throw "Repository secret 'PSGALLERY_API_KEY' is not configured." } + - name: Re-validate downloaded module manifest + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $manifestPath = Join-Path $PWD 'Module/PSInfisicalAPI/PSInfisicalAPI.psd1' + $manifest = Test-ModuleManifest -Path $manifestPath + Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)" + - name: Publish to PowerShell Gallery shell: pwsh env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 25eb16d..faaa5d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,47 +6,383 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1648 - Build produced from commit 430e3a00c921. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) (carried forward) +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1638 - Build produced from commit 3c47d6ff30ec. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1648 - Build produced from commit 430e3a00c921. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. -## Unreleased (carried forward) (carried forward) (carried forward) +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) ## 2026.06.02.1611 @@ -54,47 +390,383 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1648 - Build produced from commit 430e3a00c921. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) (carried forward) +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1638 - Build produced from commit 3c47d6ff30ec. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + ## Unreleased (carried forward) +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1648 - Build produced from commit 430e3a00c921. ## Unreleased +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) + ## 2026.06.02.1724 - Build produced from commit 5801b4774af5. -## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) + +## 2026.06.02.1737 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) + +## 2026.06.02.1902 + +- Build produced from commit fa65c18bc171. + +## Unreleased + +## 2026.06.02.1907 + +- Build produced from commit fa65c18bc171. + +## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) ### Added diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index f69c71f..2b6215b 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.02.1724' + ModuleVersion = '2026.06.02.1907' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -27,7 +27,7 @@ LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.' - CommitHash = '5801b4774af5' + CommitHash = 'fa65c18bc171' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index 82cda94..04b0b56 100644 Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ diff --git a/scripts/Install-GiteaRunner.ps1 b/scripts/Install-GiteaRunner.ps1 new file mode 100644 index 0000000..0f234ee --- /dev/null +++ b/scripts/Install-GiteaRunner.ps1 @@ -0,0 +1,426 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS + Downloads the Gitea act_runner binary, installs it as a system daemon, and registers it. + +.DESCRIPTION + Cross-platform installer for the Gitea Actions runner. Idempotent: re-running upgrades + in place, skips work when the requested version already matches, and reuses an existing + registration unless -Force is supplied. + + Resolution order for InstanceUrl and RegistrationToken when not passed explicitly: + Process scope env vars -> User scope env vars -> Machine scope env vars + Explicit candidate variable names tried in order: + InstanceUrl: GITEA_INSTANCE_URL, GITEA_URL, + CLOUDINIT_GITEA_INSTANCE_URL, CLOUDINIT_GITEA_URL + RegistrationToken: GITEA_RUNNER_REGISTRATION_TOKEN, GITEA_RUNNER_TOKEN, + GITEA_REGISTRATION_TOKEN, + CLOUDINIT_GITEA_RUNNER_REGISTRATION_TOKEN, + CLOUDINIT_GITEA_RUNNER_TOKEN, + CLOUDINIT_GITEA_REGISTRATION_TOKEN + +.PARAMETER InstanceUrl + Base URL of the Gitea instance (e.g. https://prod.git.gracesolution.info). + +.PARAMETER RegistrationToken + Runner registration token. Treated as secret; never logged. + +.PARAMETER RunnerName + Name presented to Gitea. Defaults to the machine hostname. + +.PARAMETER Labels + Optional comma-separated runner labels (e.g. linux-amd64,docker). When omitted, + no --labels argument is passed to act_runner register and the runner is + registered with whatever labels are configured server-side or in config.yaml. + +.PARAMETER Version + Specific release tag (e.g. v1.0.7) or 'latest'. Defaults to latest. + +.PARAMETER InstallRoot + Override directory. Defaults are /opt/gitea/runner on Linux, + /usr/local/gitea/runner on macOS, and %ProgramData%\Gitea\Runner on Windows. + +.PARAMETER BinaryName + Renamed binary on disk. Defaults to act_runner (act_runner.exe on Windows). + +.PARAMETER ServiceName + System service identifier (used as the systemd unit name, launchd label suffix, + and Windows service name). Must not contain spaces or characters illegal in + a unit/service identifier. Defaults to 'gitea-runner'. + +.PARAMETER ServiceDisplayName + Friendly human-readable name shown in systemctl status, Windows Services MMC, + and similar UIs. Defaults to 'Gitea Runner'. + +.PARAMETER Force + Re-download binary and re-register the runner even when already present. + +.EXAMPLE + pwsh -File Install-GiteaRunner.ps1 -InstanceUrl https://prod.git.gracesolution.info + +.EXAMPLE + Remote one-liner using env vars for InstanceUrl / RegistrationToken: + $env:GITEA_INSTANCE_URL = 'https://prod.git.gracesolution.info' + $env:GITEA_RUNNER_REGISTRATION_TOKEN = '' + irm 'https://prod.git.gracesolution.info/gsadmin/Gitea-Bootstrap/raw/branch/main/scripts/Install-GiteaRunner.ps1' | iex +#> +[CmdletBinding()] +param( + [string] $InstanceUrl, + [string] $RegistrationToken, + [string] $RunnerName = [System.Net.Dns]::GetHostName(), + [string] $Labels, + [string] $Version = 'latest', + [string] $InstallRoot, + [string] $BinaryName, + [string] $ServiceName = 'gitea-runner', + [string] $ServiceDisplayName = 'Gitea Runner', + [switch] $Force +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +if ($PSVersionTable.PSVersion.Major -lt 7) { + throw "Install-GiteaRunner requires PowerShell 7 or later. Detected: $($PSVersionTable.PSVersion)." +} + +$script:Utf8NoBom = New-Object System.Text.UTF8Encoding($false) + +function Write-Stage { + param([string] $Message) + $stamp = [DateTimeOffset]::UtcNow.UtcDateTime.ToString('yyyy-MM-ddTHH:mm:ss.fffffffZ') + Write-Host "[$stamp] - [Install-GiteaRunner] - $Message" +} + +function Resolve-FromEnvVarNames { + param( + [Parameter(Mandatory)][string[]] $Names, + [string] $DisplayName + ) + + $scopes = @('Process', 'User', 'Machine') + foreach ($name in $Names) { + foreach ($scope in $scopes) { + try { $value = [System.Environment]::GetEnvironmentVariable($name, [System.EnvironmentVariableTarget]::$scope) } + catch { continue } + + if (-not [string]::IsNullOrWhiteSpace($value)) { + Write-Stage "Resolved $DisplayName from $scope env var '$name'." + return [string] $value + } + } + } + return $null +} + +function Get-PlatformDescriptor { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant() + switch ($arch) { + 'x64' { $assetArch = 'amd64' } + 'arm64' { $assetArch = 'arm64' } + 'arm' { $assetArch = 'arm-7' } + default { throw "Unsupported architecture: $arch" } + } + + if ($IsWindows) { return @{ Os = 'windows'; Arch = $assetArch; Suffix = '.exe' } } + if ($IsMacOS) { return @{ Os = 'darwin'; Arch = $assetArch; Suffix = '' } } + if ($IsLinux) { return @{ Os = 'linux'; Arch = $assetArch; Suffix = '' } } + throw 'Unable to determine host operating system.' +} + +function Get-DefaultInstallRoot { + if ($IsWindows) { return (Join-Path $env:ProgramData 'Gitea' 'Runner') } + if ($IsMacOS) { return '/usr/local/gitea/runner' } + return '/opt/gitea/runner' +} + +function Assert-Elevated { + if ($IsWindows) { + $principal = New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent()) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'This script must be run from an elevated PowerShell session (Administrator).' + } + } else { + $uid = & id -u + if ($uid -ne '0') { throw 'This script must be run as root (use sudo).' } + } +} + +function Resolve-ReleaseTag { + param([string] $Requested) + if ($Requested -and $Requested -ne 'latest') { return ($Requested.TrimStart('v')) } + + Write-Stage 'Querying Gitea for latest runner release.' + $proxyUri = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy([Uri] 'https://gitea.com') + $invokeArgs = @{ + Uri = 'https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest' + UseBasicParsing = $true + UseDefaultCredentials = $true + } + if ($proxyUri -and $proxyUri.AbsoluteUri -ne 'https://gitea.com/') { + $invokeArgs.Proxy = $proxyUri.AbsoluteUri + $invokeArgs.ProxyUseDefaultCredentials = $true + } + $release = Invoke-RestMethod @invokeArgs + if (-not $release.tag_name) { throw 'Failed to resolve latest act_runner release tag.' } + return ($release.tag_name.TrimStart('v')) +} + + +function Get-InstalledBinaryVersion { + param([string] $BinaryPath) + if (-not (Test-Path -LiteralPath $BinaryPath)) { return $null } + try { + $raw = & $BinaryPath --version 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { return $null } + $match = [regex]::Match($raw, '(\d+\.\d+\.\d+(?:[\-\.][0-9A-Za-z\.\-]+)?)') + if ($match.Success) { return $match.Groups[1].Value } + } catch { } + return $null +} + +function Save-BinaryViaWebClient { + param( + [Parameter(Mandatory)][string] $Uri, + [Parameter(Mandatory)][string] $Destination + ) + Write-Stage "Downloading $Uri" + $client = New-Object System.Net.WebClient + try { + $client.Headers['User-Agent'] = 'Install-GiteaRunner' + $client.UseDefaultCredentials = $true + $proxy = [System.Net.WebRequest]::GetSystemWebProxy() + $proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + $client.Proxy = $proxy + $client.DownloadFile($Uri, $Destination) + } finally { + $client.Dispose() + } +} + +function Add-ToMachinePath { + param([Parameter(Mandatory)][string] $Directory) + if ($IsWindows) { + $current = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + $parts = @() + if ($current) { $parts = $current.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) } + if ($parts -notcontains $Directory) { + $updated = ($parts + $Directory) -join ';' + [System.Environment]::SetEnvironmentVariable('Path', $updated, 'Machine') + Write-Stage "Added $Directory to Machine PATH." + } + if (-not ($env:Path -split ';' -contains $Directory)) { $env:Path = "$env:Path;$Directory" } + return + } + + $profilePath = '/etc/profile.d/gitea-runner.sh' + $line = "export PATH=`"$Directory`":`$PATH" + $existing = if ([System.IO.File]::Exists($profilePath)) { [System.IO.File]::ReadAllText($profilePath) } else { '' } + if ($existing -notmatch [regex]::Escape($Directory)) { + [System.IO.File]::WriteAllText($profilePath, $line, $script:Utf8NoBom) + & chmod 0644 $profilePath | Out-Null + Write-Stage "Wrote $profilePath for system-wide PATH." + } + if (($env:PATH -split ':') -notcontains $Directory) { $env:PATH = "$Directory`:$env:PATH" } +} + +function Register-Runner { + param( + [Parameter(Mandatory)][string] $BinaryPath, + [Parameter(Mandatory)][string] $WorkingDirectory, + [Parameter(Mandatory)][string] $InstanceUrl, + [Parameter(Mandatory)][string] $Token, + [Parameter(Mandatory)][string] $RunnerName, + [string] $Labels, + [switch] $Force + ) + + $configPath = Join-Path $WorkingDirectory 'config.yaml' + if ((-not [System.IO.File]::Exists($configPath)) -or $Force) { + Write-Stage 'Generating config.yaml.' + Push-Location $WorkingDirectory + try { + $configContent = (& $BinaryPath generate-config) | Out-String + [System.IO.File]::WriteAllText($configPath, $configContent, $script:Utf8NoBom) + } finally { Pop-Location } + } + + $runnerStateFile = Join-Path $WorkingDirectory '.runner' + if ((Test-Path -LiteralPath $runnerStateFile) -and -not $Force) { + Write-Stage 'Runner already registered (.runner present); skipping registration.' + return + } + + $labelDescription = if ([string]::IsNullOrWhiteSpace($Labels)) { '(no labels specified; using server/config defaults)' } else { "with labels '$Labels'" } + Write-Stage "Registering runner '$RunnerName' $labelDescription." + + Push-Location $WorkingDirectory + try { + $registerArgs = @('register', '--no-interactive', '--instance', $InstanceUrl, + '--token', $Token, '--name', $RunnerName, '--config', $configPath) + if (-not [string]::IsNullOrWhiteSpace($Labels)) { $registerArgs += @('--labels', $Labels) } + & $BinaryPath @registerArgs + if ($LASTEXITCODE -ne 0) { throw "act_runner register exited with code $LASTEXITCODE." } + } finally { Pop-Location } +} + +function Install-SystemdUnit { + param( + [Parameter(Mandatory)][string] $ServiceName, + [Parameter(Mandatory)][string] $ServiceDisplayName, + [Parameter(Mandatory)][string] $BinaryPath, + [Parameter(Mandatory)][string] $WorkingDirectory + ) + $unitPath = "/etc/systemd/system/$ServiceName.service" + $configPath = Join-Path $WorkingDirectory 'config.yaml' + $unit = @" +[Unit] +Description=$ServiceDisplayName +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=$WorkingDirectory +ExecStart=$BinaryPath daemon --config $configPath +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target +"@ + [System.IO.File]::WriteAllText($unitPath, $unit, $script:Utf8NoBom) + & systemctl daemon-reload | Out-Null + & systemctl enable $ServiceName | Out-Null + & systemctl restart $ServiceName | Out-Null + Write-Stage "Systemd unit $unitPath installed and started." +} + +function Install-LaunchdPlist { + param( + [Parameter(Mandatory)][string] $ServiceName, + [Parameter(Mandatory)][string] $ServiceDisplayName, + [Parameter(Mandatory)][string] $BinaryPath, + [Parameter(Mandatory)][string] $WorkingDirectory + ) + $label = "com.gitea.$ServiceName" + $plistPath = "/Library/LaunchDaemons/$label.plist" + $configPath = Join-Path $WorkingDirectory 'config.yaml' + $plist = @" + + + + + Label$label + ProgramArguments + + $BinaryPath + daemon + --config + $configPath + + WorkingDirectory$WorkingDirectory + RunAtLoad + KeepAlive + + +"@ + [System.IO.File]::WriteAllText($plistPath, $plist, $script:Utf8NoBom) + & chmod 0644 $plistPath | Out-Null + & launchctl unload $plistPath 2>$null | Out-Null + & launchctl load -w $plistPath | Out-Null + Write-Stage "Launchd plist $plistPath installed and loaded." +} + +function Install-WindowsService { + param( + [Parameter(Mandatory)][string] $ServiceName, + [Parameter(Mandatory)][string] $ServiceDisplayName, + [Parameter(Mandatory)][string] $BinaryPath, + [Parameter(Mandatory)][string] $WorkingDirectory + ) + $configPath = Join-Path $WorkingDirectory 'config.yaml' + $binPath = "`"$BinaryPath`" daemon --config `"$configPath`"" + $existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($existing) { + Write-Stage "Service $ServiceName exists; updating binary path and restarting." + & sc.exe config $ServiceName binPath= $binPath DisplayName= $ServiceDisplayName | Out-Null + if ($existing.Status -eq 'Running') { Stop-Service -Name $ServiceName -Force } + } else { + Write-Stage "Creating Windows service $ServiceName." + & sc.exe create $ServiceName binPath= $binPath start= auto DisplayName= $ServiceDisplayName | Out-Null + if ($LASTEXITCODE -ne 0) { throw "sc.exe create failed with exit code $LASTEXITCODE." } + } + Start-Service -Name $ServiceName + Write-Stage "Service $ServiceName started." +} + + +# --- Main ---------------------------------------------------------------- + +Assert-Elevated + +if ([string]::IsNullOrWhiteSpace($ServiceName) -or ($ServiceName -match '\s')) { + throw "ServiceName must be a non-empty identifier with no whitespace (got: '$ServiceName'). Use -ServiceDisplayName for the friendly label." +} +if ([string]::IsNullOrWhiteSpace($ServiceDisplayName)) { $ServiceDisplayName = $ServiceName } + +if ([string]::IsNullOrWhiteSpace($InstanceUrl)) { + $InstanceUrl = Resolve-FromEnvVarNames -DisplayName 'InstanceUrl' -Names @( + 'GITEA_INSTANCE_URL', + 'GITEA_URL', + 'CLOUDINIT_GITEA_INSTANCE_URL', + 'CLOUDINIT_GITEA_URL' + ) +} +if ([string]::IsNullOrWhiteSpace($RegistrationToken)) { + $RegistrationToken = Resolve-FromEnvVarNames -DisplayName 'RegistrationToken' -Names @( + 'GITEA_RUNNER_REGISTRATION_TOKEN', + 'GITEA_RUNNER_TOKEN', + 'GITEA_REGISTRATION_TOKEN', + 'CLOUDINIT_GITEA_RUNNER_REGISTRATION_TOKEN', + 'CLOUDINIT_GITEA_RUNNER_TOKEN', + 'CLOUDINIT_GITEA_REGISTRATION_TOKEN' + ) +} + +if ([string]::IsNullOrWhiteSpace($InstanceUrl)) { throw 'InstanceUrl not provided and no matching env var found.' } +if ([string]::IsNullOrWhiteSpace($RegistrationToken)) { throw 'RegistrationToken not provided and no matching env var found.' } + +$platform = Get-PlatformDescriptor +if (-not $InstallRoot) { $InstallRoot = Get-DefaultInstallRoot } +if (-not $BinaryName) { $BinaryName = if ($IsWindows) { 'act_runner.exe' } else { 'act_runner' } } + +$resolvedVersion = Resolve-ReleaseTag -Requested $Version +$assetName = "gitea-runner-$resolvedVersion-$($platform.Os)-$($platform.Arch)$($platform.Suffix)" +$downloadUri = "https://gitea.com/gitea/runner/releases/download/v$resolvedVersion/$assetName" + +if (-not (Test-Path -LiteralPath $InstallRoot)) { + Write-Stage "Creating install root $InstallRoot." + New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null +} + +$binaryPath = Join-Path $InstallRoot $BinaryName +$installedVersion = Get-InstalledBinaryVersion -BinaryPath $binaryPath + +if ($installedVersion -eq $resolvedVersion -and -not $Force) { + Write-Stage "act_runner $installedVersion already installed; skipping download." +} else { + if ($installedVersion) { Write-Stage "Upgrading act_runner $installedVersion -> $resolvedVersion." } + Save-BinaryViaWebClient -Uri $downloadUri -Destination $binaryPath + if (-not $IsWindows) { & chmod 0755 $binaryPath | Out-Null } +} + +Add-ToMachinePath -Directory $InstallRoot + +Register-Runner -BinaryPath $binaryPath -WorkingDirectory $InstallRoot ` + -InstanceUrl $InstanceUrl -Token $RegistrationToken ` + -RunnerName $RunnerName -Labels $Labels -Force:$Force + +if ($IsLinux) { Install-SystemdUnit -ServiceName $ServiceName -ServiceDisplayName $ServiceDisplayName -BinaryPath $binaryPath -WorkingDirectory $InstallRoot } +elseif ($IsMacOS) { Install-LaunchdPlist -ServiceName $ServiceName -ServiceDisplayName $ServiceDisplayName -BinaryPath $binaryPath -WorkingDirectory $InstallRoot } +elseif ($IsWindows) { Install-WindowsService -ServiceName $ServiceName -ServiceDisplayName $ServiceDisplayName -BinaryPath $binaryPath -WorkingDirectory $InstallRoot } + +Write-Stage "Gitea runner installation complete (version $resolvedVersion, service $ServiceName / '$ServiceDisplayName')." \ No newline at end of file diff --git a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs index 9caae56..6a3f19f 100644 --- a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs @@ -89,6 +89,7 @@ namespace PSInfisicalAPI.Cmdlets { switch (encoding) { + case InfisicalExportEncoding.UTF8: return new UTF8Encoding(false); case InfisicalExportEncoding.UTF8Bom: return new UTF8Encoding(true); case InfisicalExportEncoding.Unicode: return new UnicodeEncoding(); default: return new UTF8Encoding(false); diff --git a/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs b/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs index 1c417d0..55fe767 100644 --- a/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs +++ b/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs @@ -42,6 +42,14 @@ namespace PSInfisicalAPI.Http webRequest.UserAgent = "PSInfisicalAPI"; webRequest.Timeout = _timeoutSeconds * 1000; webRequest.ReadWriteTimeout = _timeoutSeconds * 1000; + webRequest.UseDefaultCredentials = true; + + IWebProxy systemProxy = WebRequest.GetSystemWebProxy(); + if (systemProxy != null) + { + systemProxy.Credentials = CredentialCache.DefaultNetworkCredentials; + webRequest.Proxy = systemProxy; + } ApplyHeaders(webRequest, request.Headers);