#!/usr/bin/env pwsh
# ======================
# Script Init
# ======================
# Ensure we're running in PowerShell Core
if (-not ($PSVersionTable.PSEdition -eq "Core"))
{
Write-Host "This script requires PowerShell Core. Please install it first."
exit 1
}
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ======================
# Variables and Constants
# ======================
# Set default log level (DEBUG, INFO, WARNING, ERROR)
$Script:LOG_LEVEL = if ($env:LOG_LEVEL)
{ $env:LOG_LEVEL
} else
{ "INFO"
}
# Required environment variables for branch promotion
$Script:RELEASE_ORIGIN_BRANCH = if ($env:RELEASE_ORIGIN_BRANCH)
{ $env:RELEASE_ORIGIN_BRANCH
} else
{ ""
}
$Script:RELEASE_DESTINATION_BRANCH = if ($env:RELEASE_DESTINATION_BRANCH)
{ $env:RELEASE_DESTINATION_BRANCH
} else
{ ""
}
$Script:RELEASE_STAGE = if ($env:RELEASE_STAGE)
{ $env:RELEASE_STAGE
} else
{ ""
}
# Git authentication (from environment variables)
$Script:GIT_ACCESS_TOKEN = if ($env:GIT_ACCESS_TOKEN)
{ $env:GIT_ACCESS_TOKEN
} else
{ ""
}
$Script:SCM_HOSTNAME = if ($env:SCM_HOSTNAME)
{ $env:SCM_HOSTNAME
} else
{ ""
}
$Script:SCM_REPOSITORY_PATH = if ($env:SCM_REPOSITORY_PATH)
{ $env:SCM_REPOSITORY_PATH
} else
{ ""
}
$Script:BUILD_PIPELINE_URL = if ($env:BUILD_PIPELINE_URL)
{ $env:BUILD_PIPELINE_URL
} else
{ ""
}
# Optional environment variables
$Script:RELEASE_PREVIEW_ONLY = if ($env:RELEASE_PREVIEW_ONLY -eq "true")
{ $true
} else
{ $false
}
# Git identity for automated commits
$Script:GIT_AUTHOR_NAME = if ($env:GIT_AUTHOR_NAME)
{ $env:GIT_AUTHOR_NAME
} else
{ "Entropy Release Bot"
}
$Script:GIT_AUTHOR_EMAIL = if ($env:GIT_AUTHOR_EMAIL)
{ $env:GIT_AUTHOR_EMAIL
} else
{ "releases@entropysoftworks.com"
}
# Deploy action: "promote" (branch fast-forward, default) or "publish"
# (cargo publish to crates.io on a vX.Y.Z tag).
$Script:DeployAction = if ($env:DEPLOY_ACTION)
{ $env:DEPLOY_ACTION
} else
{ "promote"
}
# Log file name for this script
$Script:LOG_FILE_NAME = "entropy_auth_deploy.log"
# Sanitize branch references (strip remote prefix for local operations)
$Script:DestinationBranchLocal = $Script:RELEASE_DESTINATION_BRANCH -replace '^origin/', ''
$Script:OriginBranchLabel = $Script:RELEASE_ORIGIN_BRANCH -replace '^origin/', ''
$Script:DestinationBranchLabel = $Script:RELEASE_DESTINATION_BRANCH -replace '^origin/', ''
# ======================
# Shared Functions
# ======================
. "$PSScriptRoot/common.ps1"
# ======================
# Function Definitions
# ======================
# Ensure all mandatory environment variables are present
function Test-EnvironmentVariables
{
$absent = @()
if ([string]::IsNullOrEmpty($Script:RELEASE_ORIGIN_BRANCH))
{ $absent += "RELEASE_ORIGIN_BRANCH"
}
if ([string]::IsNullOrEmpty($Script:RELEASE_DESTINATION_BRANCH))
{ $absent += "RELEASE_DESTINATION_BRANCH"
}
if ([string]::IsNullOrEmpty($Script:RELEASE_STAGE))
{ $absent += "RELEASE_STAGE"
}
if ([string]::IsNullOrEmpty($Script:SCM_HOSTNAME))
{ $absent += "SCM_HOSTNAME"
}
if ([string]::IsNullOrEmpty($Script:SCM_REPOSITORY_PATH))
{ $absent += "SCM_REPOSITORY_PATH"
}
if ([string]::IsNullOrEmpty($Script:GIT_ACCESS_TOKEN))
{ $absent += "GIT_ACCESS_TOKEN"
}
if ($absent.Count -gt 0)
{
Write-Log -LogLevel "ERROR" -LogMessage "Missing required environment variables:"
foreach ($v in $absent)
{
Write-Log -LogLevel "ERROR" -LogMessage " - $v"
}
return $false
}
Write-Log -LogLevel "INFO" -LogMessage "All required environment variables are present"
return $true
}
# Configure git identity and remote authentication for CI
function Initialize-GitConfiguration
{
Write-Log -LogLevel "INFO" -LogMessage "Configuring git for automated release..."
try
{
# Allow the CI checkout directory (runner UID may differ from repo owner)
Write-Log -LogLevel "DEBUG" -LogMessage "Marking working directory as safe for git..."
& git config --global --add safe.directory (Get-Location).Path
if ($LASTEXITCODE -ne 0)
{ throw "Failed to configure safe.directory"
}
# Set committer identity
Write-Log -LogLevel "DEBUG" -LogMessage "Setting git user.name to: $($Script:GIT_AUTHOR_NAME)"
& git config --global user.name $Script:GIT_AUTHOR_NAME
if ($LASTEXITCODE -ne 0)
{ throw "Failed to set git user.name"
}
Write-Log -LogLevel "DEBUG" -LogMessage "Setting git user.email to: $($Script:GIT_AUTHOR_EMAIL)"
& git config --global user.email $Script:GIT_AUTHOR_EMAIL
if ($LASTEXITCODE -ne 0)
{ throw "Failed to set git user.email"
}
# Authenticate via HTTPS token on the remote
$authenticatedRemote = "https://oauth2:$($Script:GIT_ACCESS_TOKEN)@$($Script:SCM_HOSTNAME)/$($Script:SCM_REPOSITORY_PATH).git"
Write-Log -LogLevel "DEBUG" -LogMessage "Updating remote origin URL (credentials redacted)"
& git remote set-url origin $authenticatedRemote
if ($LASTEXITCODE -ne 0)
{ throw "Failed to update remote URL"
}
Write-Log -LogLevel "INFO" -LogMessage "Git configuration complete"
return $true
} catch
{
Write-Log -LogLevel "ERROR" -LogMessage "Git configuration failed: $_"
return $false
}
}
# Publish the crate to crates.io. Runs on every push to main (i.e. on merge);
# the version is a CalVer derived from the UTC build date — YYYY.M.D with
# leading zeros dropped (e.g. 2026.6.5). No tags are involved: the version is
# stamped into Cargo.toml at build time and published as-is. Republishing the
# same day (same version) is treated as a no-op success so re-runs/second
# merges don't fail the pipeline.
function Invoke-CratePublish
{
Start-OperationTiming -Operation "crate_publish"
if ([string]::IsNullOrEmpty($env:CARGO_REGISTRY_TOKEN))
{
Write-Log -LogLevel "ERROR" -LogMessage "CARGO_REGISTRY_TOKEN is required to publish to crates.io"
exit 1
}
# CalVer from the UTC build date. Integer parts drop leading zeros.
$now = [DateTime]::UtcNow
$version = "$($now.Year).$($now.Month).$($now.Day)"
Write-Log -LogLevel "INFO" -LogMessage "Build-date version (UTC): $version"
# Stamp the version into [package] of Cargo.toml. The anchored `^version`
# matches only the package version, never the indented `{ version = ... }`
# dependency entries.
$manifest = Join-Path $PSScriptRoot "../Cargo.toml"
$content = Get-Content $manifest -Raw
$content = $content -replace '(?m)^version = "[^"]*"', "version = `"$version`""
Set-Content -Path $manifest -Value $content -NoNewline
Write-Log -LogLevel "INFO" -LogMessage "Publishing entropy-auth $version to crates.io"
Push-Location (Join-Path $PSScriptRoot "..")
try
{
# --allow-dirty: the version stamp above leaves the working tree
# modified (the change is intentional and never committed back).
$pubOut = & cargo publish --allow-dirty --token $env:CARGO_REGISTRY_TOKEN 2>&1
$pubCode = $LASTEXITCODE
$pubOut | ForEach-Object {
$l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[cargo publish] $l"
}
}
if ($pubCode -ne 0)
{
# Already published this day → idempotent success.
if ("$pubOut" -match 'already (uploaded|exists)')
{
Write-Log -LogLevel "WARNING" -LogMessage "entropy-auth $version already on crates.io — skipping (no-op)."
} else
{
throw "cargo publish failed"
}
}
}
finally
{ Pop-Location
}
Stop-OperationTiming -Operation "crate_publish"
Write-Log -LogLevel "INFO" -LogMessage "Published entropy-auth $version to crates.io" -OperationName "crate_publish"
Show-TimingSummary
exit 0
}
# ======================
# Main Script Execution
# ======================
# Display Banner
Write-Host '
##############################################################################################
# #
# Entropy Softworks — entropy-auth Branch Promotion #
# #
##############################################################################################
=== Overview ===
Fast-forwards the destination branch to match the origin branch for
environment promotion of the entropy-auth crate:
- Authenticates git using credentials from environment variables
- Fetches the latest remote state and checks out the destination branch
- Fast-forwards the destination to the origin branch tip (git merge --ff-only)
- Pushes the result to the remote
=== Environment Variables ===
Required: RELEASE_ORIGIN_BRANCH, RELEASE_DESTINATION_BRANCH, RELEASE_STAGE,
SCM_HOSTNAME, SCM_REPOSITORY_PATH, GIT_ACCESS_TOKEN
Optional: RELEASE_PREVIEW_ONLY, GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, LOG_LEVEL
'
# Publish path: ship to crates.io and exit. Promote is the default action.
if ($Script:DeployAction -eq "publish")
{
Invoke-CratePublish
}
Start-OperationTiming -Operation "total_promotion"
# Validate environment variables
if (-not (Test-EnvironmentVariables))
{
Write-Log -LogLevel "ERROR" -LogMessage "Environment validation failed — aborting"
exit 1
}
# Log the resolved configuration
Write-Log -LogLevel "INFO" -LogMessage "Release configuration:"
Write-Log -LogLevel "INFO" -LogMessage " Origin Branch : $($Script:RELEASE_ORIGIN_BRANCH)"
Write-Log -LogLevel "INFO" -LogMessage " Destination Branch : $($Script:RELEASE_DESTINATION_BRANCH)"
Write-Log -LogLevel "INFO" -LogMessage " Stage : $($Script:RELEASE_STAGE)"
Write-Log -LogLevel "INFO" -LogMessage " Preview Only : $($Script:RELEASE_PREVIEW_ONLY)"
# Configure git identity and authentication
if (-not (Initialize-GitConfiguration))
{
Write-Log -LogLevel "ERROR" -LogMessage "Git configuration failed — aborting"
exit 1
}
# Main promotion logic
try
{
Write-Log -LogLevel "INFO" -LogMessage "Beginning branch promotion to $($Script:RELEASE_STAGE)..."
Write-Log -LogLevel "INFO" -LogMessage " Origin: $Script:OriginBranchLabel"
Write-Log -LogLevel "INFO" -LogMessage " Destination: $Script:DestinationBranchLabel"
# Fetch latest state from the remote
Start-OperationTiming -Operation "fetch_remote"
Write-Log -LogLevel "INFO" -LogMessage "Fetching latest remote refs..."
& git fetch origin 2>&1 | ForEach-Object { $l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[git fetch] $l"
} }
if ($LASTEXITCODE -ne 0)
{ throw "Failed to fetch from remote"
}
Stop-OperationTiming -Operation "fetch_remote"
# Check out the destination branch
Start-OperationTiming -Operation "checkout_destination"
Write-Log -LogLevel "INFO" -LogMessage "Checking out $Script:DestinationBranchLabel..."
& git checkout $Script:DestinationBranchLocal 2>&1 | ForEach-Object { $l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[git checkout] $l"
} }
if ($LASTEXITCODE -ne 0)
{ throw "Failed to check out $Script:DestinationBranchLocal"
}
Stop-OperationTiming -Operation "checkout_destination"
# Hard-reset to the remote tracking branch to guarantee a clean state
Write-Log -LogLevel "DEBUG" -LogMessage "Resetting to origin/$Script:DestinationBranchLocal..."
& git reset --hard "origin/$Script:DestinationBranchLocal" 2>&1 | ForEach-Object { $l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[git reset] $l"
} }
if ($LASTEXITCODE -ne 0)
{ throw "Failed to reset $Script:DestinationBranchLocal to remote state"
}
# Determine whether there are new commits to promote
$originTip = & git rev-parse "origin/$($Script:RELEASE_ORIGIN_BRANCH -replace '^origin/', '')"
$destinationTip = & git rev-parse HEAD
Write-Log -LogLevel "DEBUG" -LogMessage "Origin tip: $originTip"
Write-Log -LogLevel "DEBUG" -LogMessage "Destination tip: $destinationTip"
if ($originTip -eq $destinationTip)
{
Write-Log -LogLevel "INFO" -LogMessage "No new commits to promote. $($Script:RELEASE_STAGE) is already current."
Stop-OperationTiming -Operation "total_promotion"
Show-TimingSummary
exit 0
}
# Enumerate the commits that will be fast-forwarded
$pendingCommits = & git log --oneline HEAD.."origin/$($Script:RELEASE_ORIGIN_BRANCH -replace '^origin/', '')"
$commitTotal = ($pendingCommits | Measure-Object).Count
Write-Log -LogLevel "INFO" -LogMessage "Found $commitTotal commit(s) pending promotion:"
$pendingCommits | ForEach-Object { Write-Log -LogLevel "DEBUG" -LogMessage " - $_" }
# Preview-only check
if ($Script:RELEASE_PREVIEW_ONLY)
{
Write-Log -LogLevel "INFO" -LogMessage "PREVIEW — Would fast-forward $Script:DestinationBranchLabel to $Script:OriginBranchLabel ($originTip)"
$pendingCommits | ForEach-Object { Write-Log -LogLevel "INFO" -LogMessage " - $_" }
Stop-OperationTiming -Operation "total_promotion"
Write-Log -LogLevel "INFO" -LogMessage "Preview completed successfully" -OperationName "total_promotion"
Show-TimingSummary
exit 0
}
# Perform the fast-forward merge
Start-OperationTiming -Operation "ff_merge"
$originRef = "origin/$($Script:RELEASE_ORIGIN_BRANCH -replace '^origin/', '')"
Write-Log -LogLevel "INFO" -LogMessage "Fast-forwarding $Script:DestinationBranchLabel to $Script:OriginBranchLabel..."
& git merge --ff-only $originRef 2>&1 | ForEach-Object { $l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[git merge --ff-only] $l"
} }
if ($LASTEXITCODE -ne 0)
{
throw "Fast-forward failed. $Script:DestinationBranchLabel has diverged from $Script:OriginBranchLabel. Manual reconciliation required."
}
Stop-OperationTiming -Operation "ff_merge"
# Push the promoted branch
Start-OperationTiming -Operation "push_promoted"
Write-Log -LogLevel "INFO" -LogMessage "Pushing $Script:DestinationBranchLabel to remote..."
& git push origin $Script:DestinationBranchLocal 2>&1 | ForEach-Object { $l = "$_"; if (-not [string]::IsNullOrWhiteSpace($l))
{ Write-Log -LogLevel "DEBUG" -LogMessage "[git push] $l"
} }
if ($LASTEXITCODE -ne 0)
{
throw "Failed to push $Script:DestinationBranchLocal to remote"
}
Stop-OperationTiming -Operation "push_promoted"
Stop-OperationTiming -Operation "total_promotion"
Write-Log -LogLevel "INFO" -LogMessage "Successfully promoted $commitTotal commit(s) to $($Script:RELEASE_STAGE) via fast-forward" -OperationName "total_promotion"
Show-TimingSummary
exit 0
} catch
{
Write-Log -LogLevel "ERROR" -LogMessage "Promotion failed: $_"
Stop-OperationTiming -Operation "total_promotion"
Show-TimingSummary
exit 1
}