diff --git a/eng/Containerfile b/eng/Containerfile new file mode 100644 index 0000000..3aa7913 --- /dev/null +++ b/eng/Containerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 +WORKDIR /work +COPY ./src /work diff --git a/src/Geekeey.Build.Pwsh.psd1 b/src/Geekeey.Build.Pwsh.psd1 new file mode 100644 index 0000000..c692709 --- /dev/null +++ b/src/Geekeey.Build.Pwsh.psd1 @@ -0,0 +1,37 @@ +@{ + RootModule = 'Geekeey.Build.Pwsh.psm1' + ModuleVersion = '1.0.0' + GUID = '234baa68-d26f-4bf9-996d-45ec3520cb95' + Author = 'The Geekeey Authors' + CompanyName = 'Geekeey' + Copyright = '(c) The Geekeey Authors. Licensed under EUPL-1.2.' + Description = 'Forgejo Actions helper functions for PowerShell.' + FunctionsToExport = @( + 'Set-ActionOutput', + 'Set-ActionEnv', + 'Add-ActionPath', + 'Add-ActionMask', + 'Add-ActionMatcher', + 'Remove-ActionMatcher', + 'Save-ActionState', + 'Add-ActionStepSummary', + 'Get-ActionInput', + 'Write-ActionDebug', + 'Write-ActionNotice', + 'Write-ActionWarning', + 'Write-ActionError', + 'Stop-Action', + 'Invoke-ForgejoApi', + 'Get-ForgejoContext' + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + PowerShellVersion = '7.0' + PrivateData = @{ + PSData = @{ + LicenseUri = 'https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12' + Tags = @('Forgejo', 'Actions', 'CI') + } + } +} diff --git a/src/Geekeey.Build.Pwsh.psm1 b/src/Geekeey.Build.Pwsh.psm1 new file mode 100644 index 0000000..e132748 --- /dev/null +++ b/src/Geekeey.Build.Pwsh.psm1 @@ -0,0 +1,488 @@ +# Copyright (c) The Geekeey Authors +# SPDX-License-Identifier: EUPL-1.2 + +Set-StrictMode -Version Latest + +$Script:MULTILINE_DELIMITER = "234baa68-d26f-4bf9-996d-45ec3520cb95" + +function Write-FileInstruction +{ + param( + [Parameter(Mandatory)] + [string]$EnvVar, + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Value + ) + $filePath = [System.Environment]::GetEnvironmentVariable($EnvVar) + if ([string]::IsNullOrEmpty($filePath)) + { + throw "$EnvVar is not set" + } + $content = "${Name}<<${MULTILINE_DELIMITER}`n${Value}`n${MULTILINE_DELIMITER}" + Add-Content -Path $filePath -Value $content +} + +function Set-ActionOutput +{ + <# + .SYNOPSIS + Sets an output variable for the workflow. + .PARAMETER Name + The name of the output variable. + .PARAMETER Value + The value to set. + #> + param( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Value + ) + Write-FileInstruction -EnvVar "FORGEJO_OUTPUT" -Name $Name -Value $Value +} + +function Set-ActionEnv +{ + <# + .SYNOPSIS + Sets an environment variable for subsequent steps. + .PARAMETER Name + The name of the environment variable. + .PARAMETER Value + The value to set. + #> + param( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Value + ) + Write-FileInstruction -EnvVar "FORGEJO_ENV" -Name $Name -Value $Value +} + +function Add-ActionPath +{ + <# + .SYNOPSIS + Adds a directory to the PATH for subsequent steps. + .PARAMETER Path + The directory path to add. + #> + param( + [Parameter(Mandatory)] + [string]$Path + ) + $filePath = [System.Environment]::GetEnvironmentVariable("FORGEJO_PATH") + if ([string]::IsNullOrEmpty($filePath)) + { + throw "FORGEJO_PATH is not set" + } + Add-Content -Path $filePath -Value $Path +} + +function Save-ActionState +{ + <# + .SYNOPSIS + Saves a state value for use in subsequent steps of the same job. + .PARAMETER Name + The name of the state variable. + .PARAMETER Value + The value to save. + #> + param( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Value + ) + Write-FileInstruction -EnvVar "FORGEJO_STATE" -Name $Name -Value $Value +} + +function Add-ActionStepSummary +{ + <# + .SYNOPSIS + Writes a summary for the workflow run step. + .PARAMETER Summary + The markdown summary text. + #> + param( + [Parameter(Mandatory)] + [string]$Summary + ) + $envPath = $env:FORGEJO_STEP_SUMMARY + if ([string]::IsNullOrEmpty($envPath)) + { + throw "FORGEJO_STEP_SUMMARY is not set" + } + Add-Content -Path $envPath -Value $Summary +} + +function Add-ActionMask +{ + <# + .SYNOPSIS + Masks a value in log output. Future attempts to log this value will show "***". + .PARAMETER Value + The value to mask. + #> + param( + [Parameter(Mandatory)] + [string]$Value + ) + Write-Host "::add-mask::$Value" +} + +function Add-ActionMatcher +{ + <# + .SYNOPSIS + Registers a problem matcher from a JSON file. + .PARAMETER Path + The path to the problem matcher JSON file. + #> + param( + [Parameter(Mandatory)] + [string]$Path + ) + Write-Host "::add-matcher::$Path" +} + +function Remove-ActionMatcher +{ + <# + .SYNOPSIS + Removes a previously registered problem matcher by owner. + .PARAMETER Owner + The owner of the problem matcher to remove. + #> + param( + [Parameter(Mandatory)] + [string]$Owner + ) + Write-Host "::remove-matcher owner=$Owner" +} + +function Start-ActionGroup +{ + <# + .SYNOPSIS + Starts an expandable group in the log. + .PARAMETER Title + The group title. + #> + param( + [Parameter(Mandatory)] + [string]$Title + ) + Write-Host "::group::$Title" +} + +function Stop-ActionGroup +{ + <# + .SYNOPSIS + Ends the most recently started log group. + #> + Write-Host "::endgroup::" +} + +function Set-ActionCommandEcho +{ + <# + .SYNOPSIS + Enables or disables echoing of workflow commands to the log. + .PARAMETER Enabled + If $true, workflow commands are echoed; otherwise they are not. + #> + param( + [Parameter(Mandatory)] + [bool]$Enabled + ) + $value = if ($Enabled) { "on" } else { "off" } + Write-Host "::echo::$value" +} + +function Suspend-ActionCommands +{ + <# + .SYNOPSIS + Stops processing of workflow commands until resumed. + .PARAMETER Token + A unique token used to later resume command processing. + #> + param( + [Parameter(Mandatory)] + [string]$Token + ) + Write-Host "::stop-commands::$Token" +} + +function Resume-ActionCommands +{ + <# + .SYNOPSIS + Resumes processing of workflow commands. + .PARAMETER Token + The same token previously passed to Suspend-ActionCommands. + #> + param( + [Parameter(Mandatory)] + [string]$Token + ) + Write-Host "::$Token::" +} + +function Get-ActionInput +{ + <# + .SYNOPSIS + Gets an input value by name. + .PARAMETER Name + The input name (spaces are replaced with underscores, uppercased). + .PARAMETER Required + If $true, throws when the input is not set. + #> + param( + [Parameter(Mandatory)] + [string]$Name, + [switch]$Required + ) + + $value = [System.Environment]::GetEnvironmentVariable("INPUT_$($Name.Replace(' ', '_').ToUpper())")?.Trim() + + if ($Required -and [string]::IsNullOrEmpty($value)) + { + Stop-Action "Input '$Name' is required but not provided." + } + + return $value +} + +function Get-ActionState +{ + <# + .SYNOPSIS + Gets a state value saved by Save-ActionState. + .PARAMETER Name + The name of the state variable. + #> + param( + [Parameter(Mandatory)] + [string]$Name + ) + + $value = [System.Environment]::GetEnvironmentVariable("STATE_$($Name.Replace(' ', '_').ToUpper())")?.Trim() + + return $value +} + +function Test-ActionDebug +{ + <# + .SYNOPSIS + Returns $true when debug logging is enabled (RUNNER_DEBUG=1). + #> + $value = [System.Environment]::GetEnvironmentVariable("RUNNER_DEBUG") + return $value -eq "1" +} + +function Write-ActionDebug +{ + <# + .SYNOPSIS + Writes a debug-level message. + .PARAMETER Message + The message to write. + #> + param( + [Parameter(Mandatory)] + [string]$Message + ) + Write-Host "::debug::$Message" +} + +function Write-ActionNotice +{ + <# + .SYNOPSIS + Writes a notice-level message. + .PARAMETER Message + The message to write. + #> + param( + [Parameter(Mandatory)] + [string]$Message + ) + Write-Host "::notice::$Message" +} + +function Write-ActionWarning +{ + <# + .SYNOPSIS + Writes a warning-level message. + .PARAMETER Message + The message to write. + #> + param( + [Parameter(Mandatory)] + [string]$Message + ) + Write-Host "::warning::$Message" +} + +function Write-ActionError +{ + <# + .SYNOPSIS + Writes an error-level message. + .PARAMETER Message + The message to write. + #> + param( + [Parameter(Mandatory)] + [string]$Message + ) + Write-Host "::error::$Message" +} + +function Stop-Action +{ + <# + .SYNOPSIS + Fails the action with an error message. + .PARAMETER Message + The error message. + .PARAMETER ExitCode + The exit code (default: 1). + #> + param( + [Parameter(Mandatory)] + [string]$Message, + [int]$ExitCode = 1 + ) + Write-ActionError $Message + exit $ExitCode +} + +function Invoke-ForgejoApi +{ + <# + .SYNOPSIS + Makes an authenticated Forgejo API call. + .PARAMETER Method + The HTTP method (GET, POST, PUT, PATCH, DELETE). + .PARAMETER Path + The API path (e.g., "/repos/{owner}/{repo}/issues"). + .PARAMETER Body + Optional request body (will be converted to JSON). + .PARAMETER ContentType + The content type (default: application/json). + .OUTPUTS + The deserialized JSON response. + #> + param( + [Parameter(Mandatory)] + [ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")] + [string]$Method, + [Parameter(Mandatory)] + [string]$Path, + [object]$Body, + [string]$ContentType = "application/json" + ) + + $token = $env:FORGEJO_TOKEN + if ([string]::IsNullOrEmpty($token)) + { + throw "FORGEJO_TOKEN is not set." + } + + $serverUrl = $env:FORGEJO_SERVER_URL + if ([string]::IsNullOrEmpty($serverUrl)) + { + throw "FORGEJO_SERVER_URL is not set." + } + + $headers = @{ + "Authorization" = "token $token" + "Accept" = "application/json" + } + + $params = @{ + Method = $Method + Uri = "$serverUrl/api/v1$Path" + Headers = $headers + ContentType = $ContentType + } + + if ($Body) + { + $params.Body = ($Body | ConvertTo-Json -Depth 100 -Compress) + } + + try + { + return (Invoke-RestMethod @params) + } + catch + { + $statusCode = $_.Exception.Response.StatusCode.value__ + $message = $_.ErrorDetails.Message + if ([string]::IsNullOrEmpty($message)) + { + $message = $_.Exception.Message + } + throw "Forgejo API call failed: $Method $Path -> $statusCode $message" + } +} + +function Get-ForgejoContext +{ + <# + .SYNOPSIS + Returns a hashtable with the Forgejo Actions context. + .DESCRIPTION + Provides access to ev payload, repository info, and a convenience + method for making authenticated API calls. + .OUTPUTS + Hashtable with Context, Repository, ServerUrl, Token, Ref, Sha, RunId, + and an InvokeApi scriptblock. + #> + + $ev = @{} + if ($env:FORGEJO_EVENT_PATH -and (Test-Path $env:FORGEJO_EVENT_PATH)) + { + try + { + $ev = (Get-Content $env:FORGEJO_EVENT_PATH -Raw | ConvertFrom-Json -AsHashtable) + } + catch + { + Write-ActionWarning "Failed to parse FORGEJO_EVENT_PATH: $_" + } + } + + return @{ + Event = $ev + Repository = $env:FORGEJO_REPOSITORY + ServerUrl = $env:FORGEJO_SERVER_URL + Token = $env:FORGEJO_TOKEN + Ref = $env:FORGEJO_REF + Sha = $env:FORGEJO_SHA + RunId = $env:FORGEJO_RUN_ID + RunNumber = $env:FORGEJO_RUN_NUMBER + Actor = $env:FORGEJO_ACTOR + Workflow = $env:FORGEJO_WORKFLOW + Job = $env:FORGEJO_JOB + Action = $env:FORGEJO_ACTION + # Convenience: $Forgejo.InvokeApi("GET", "/repos/{owner}/{repo}/issues") + InvokeApi = [ScriptBlock] { + param($Method, $Path, $Body) + Invoke-ForgejoApi -Method $Method -Path $Path -Body $Body + } + } +} diff --git a/src/collection/checkout.ps1 b/src/collection/checkout.ps1 new file mode 100644 index 0000000..d52cbbe --- /dev/null +++ b/src/collection/checkout.ps1 @@ -0,0 +1 @@ +using module ../Geekeey.Build.Pwsh.psd1 diff --git a/src/collection/pwsh.ps1 b/src/collection/pwsh.ps1 new file mode 100644 index 0000000..19b681d --- /dev/null +++ b/src/collection/pwsh.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) The Geekeey Authors +# SPDX-License-Identifier: EUPL-1.2 +using module ../Geekeey.Build.Pwsh.psd1 + +Set-StrictMode -Version Latest + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'Ignore' + +$PSNativeCommandUseErrorActionPreference = $true + +$script = Get-ActionInput -Name "script" -Required + +try +{ + Invoke-Expression $script +} +catch +{ + Write-ActionError "Script failed: $_" + exit 1 +} diff --git a/src/collection/ssh-add-key.ps1 b/src/collection/ssh-add-key.ps1 new file mode 100644 index 0000000..d492350 --- /dev/null +++ b/src/collection/ssh-add-key.ps1 @@ -0,0 +1 @@ +using module ../Geekeey.Build.Pwsh.psm1 diff --git a/src/core/context.ps1 b/src/core/context.ps1 new file mode 100644 index 0000000..e69de29