name: offline-release on: workflow_dispatch: inputs: base_ref: description: "Optional base ref for impact diff, defaults to HEAD^" required: false include_infra: description: "Whether to pack infra bundle too" required: false default: "false" jobs: build-upload: runs-on: local-build steps: - name: Prepare local worktree env: SMARTFLOW_REPO_URL: https://git.lecspace.com/${{ gitea.repository }}.git SMARTFLOW_GIT_REPO_URL: ${{ secrets.SMARTFLOW_GIT_REPO_URL }} SMARTFLOW_REPO_SHA: ${{ gitea.sha }} SMARTFLOW_GITEA_USER: ${{ secrets.SMARTFLOW_GITEA_USER }} SMARTFLOW_GITEA_TOKEN: ${{ secrets.SMARTFLOW_GITEA_TOKEN }} shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest function Add-GitHubEnv { param([string]$Line) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::AppendAllText($env:GITHUB_ENV, $Line + [Environment]::NewLine, $utf8NoBom) } $worktreeRoot = Join-Path ([System.IO.Path]::GetTempPath()) "smartflow-actions" $worktree = Join-Path $worktreeRoot $env:SMARTFLOW_REPO_SHA $repoUrl = if ([string]::IsNullOrWhiteSpace($env:SMARTFLOW_GIT_REPO_URL)) { $env:SMARTFLOW_REPO_URL } else { $env:SMARTFLOW_GIT_REPO_URL } if (Test-Path $worktree) { Remove-Item -LiteralPath $worktree -Recurse -Force } New-Item -ItemType Directory -Force -Path $worktreeRoot | Out-Null $gitArgs = @() if (-not [string]::IsNullOrWhiteSpace($env:SMARTFLOW_GITEA_TOKEN)) { $giteaUser = $env:SMARTFLOW_GITEA_USER if ([string]::IsNullOrWhiteSpace($giteaUser)) { $giteaUser = "Losita" } $basicToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $giteaUser, $env:SMARTFLOW_GITEA_TOKEN))) $gitArgs += @("-c", ("http.extraHeader=Authorization: Basic {0}" -f $basicToken)) } & git @gitArgs clone --no-checkout $repoUrl $worktree if ($LASTEXITCODE -ne 0) { throw "source clone failed." } & git -C $worktree checkout --force $env:SMARTFLOW_REPO_SHA if ($LASTEXITCODE -ne 0) { throw "source checkout failed." } & git -C $worktree clean -dffx if ($LASTEXITCODE -ne 0) { throw "source cleanup failed." } $appTag = (& git -C $worktree rev-parse --short=12 HEAD).Trim() Add-GitHubEnv "APP_TAG=$appTag" Add-GitHubEnv "SMARTFLOW_WORKTREE=$worktree" - name: Resolve release base env: INPUT_BASE_REF: ${{ inputs.base_ref }} shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest function Add-GitHubEnv { param([string]$Line) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::AppendAllText($env:GITHUB_ENV, $Line + [Environment]::NewLine, $utf8NoBom) } Set-Location $env:SMARTFLOW_WORKTREE $baseRef = $env:INPUT_BASE_REF if ([string]::IsNullOrWhiteSpace($baseRef)) { & git rev-parse --verify --quiet "HEAD^" | Out-Null if ($LASTEXITCODE -eq 0) { $baseRef = (& git rev-parse "HEAD^").Trim() } } Add-GitHubEnv "BASE_REF=$baseRef" - name: Build release plan shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest Set-Location $env:SMARTFLOW_WORKTREE .\deploy\impact-rules.ps1 -BaseRef $env:BASE_REF -HeadRef "HEAD" -OutputFile "deploy\release-plan.env" Get-Content -LiteralPath "deploy\release-plan.env" - name: Pack docker images env: INPUT_INCLUDE_INFRA: ${{ inputs.include_infra }} shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest Set-Location $env:SMARTFLOW_WORKTREE $packArgs = @{ AppTag = $env:APP_TAG PlanFile = "deploy\release-plan.env" } if ($env:INPUT_INCLUDE_INFRA -eq "true") { $packArgs["IncludeInfra"] = $true } & .\deploy\docker-pack.ps1 @packArgs - name: Stage release directory shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest Set-Location $env:SMARTFLOW_WORKTREE .\deploy\stage-release.ps1 ` -ReleaseDir ".release\$env:APP_TAG" ` -PlanFile "deploy\release-plan.env" ` -BundleDir ".docker-bundles" - name: Upload release to server env: SMARTFLOW_RELEASE_HOST: ${{ secrets.SMARTFLOW_RELEASE_HOST }} SMARTFLOW_RELEASE_USER: ${{ secrets.SMARTFLOW_RELEASE_USER }} SMARTFLOW_RELEASE_PORT: ${{ secrets.SMARTFLOW_RELEASE_PORT }} SMARTFLOW_RELEASE_ROOT: ${{ secrets.SMARTFLOW_RELEASE_ROOT }} SMARTFLOW_SSH_KEY: ${{ secrets.SMARTFLOW_SSH_KEY }} shell: powershell run: | $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest Set-Location $env:SMARTFLOW_WORKTREE $hostName = $env:SMARTFLOW_RELEASE_HOST if ([string]::IsNullOrWhiteSpace($hostName)) { $hostName = "192.140.166.210" } $userName = $env:SMARTFLOW_RELEASE_USER if ([string]::IsNullOrWhiteSpace($userName)) { $userName = "root" } $port = $env:SMARTFLOW_RELEASE_PORT if ([string]::IsNullOrWhiteSpace($port)) { $port = "22" } $releaseRoot = $env:SMARTFLOW_RELEASE_ROOT if ([string]::IsNullOrWhiteSpace($releaseRoot)) { $releaseRoot = "/srv/smartflow/releases" } if ($releaseRoot -notmatch '^/srv/smartflow/releases(/.*)?$') { throw "release root must stay under /srv/smartflow/releases." } $remote = "{0}@{1}" -f $userName, $hostName $archivePath = Join-Path ([System.IO.Path]::GetTempPath()) ("smartflow-release-{0}.tgz" -f $env:APP_TAG) $remoteArchive = ("{0}/{1}.tgz" -f $releaseRoot.TrimEnd('/'), $env:APP_TAG) if (Test-Path $archivePath) { Remove-Item -LiteralPath $archivePath -Force } & tar -C ".release\$env:APP_TAG" -czf $archivePath . if ($LASTEXITCODE -ne 0) { throw "release archive failed." } $sshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=30", "-p", $port) $scpArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=30", "-P", $port) if (-not [string]::IsNullOrWhiteSpace($env:SMARTFLOW_SSH_KEY)) { $keyPath = Join-Path ([System.IO.Path]::GetTempPath()) ("smartflow-release-{0}.key" -f $env:APP_TAG) $env:SMARTFLOW_SSH_KEY.Replace("`r`n", "`n") | Out-File -FilePath $keyPath -Encoding ascii -NoNewline if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { & icacls $keyPath /inheritance:r /grant:r "$($env:USERNAME):(R)" | Out-Null } else { & chmod 600 $keyPath } $sshArgs += @("-i", $keyPath) $scpArgs += @("-i", $keyPath) } & ssh @sshArgs $remote "mkdir -p '$releaseRoot'" if ($LASTEXITCODE -ne 0) { throw "remote release root prepare failed." } & scp @scpArgs $archivePath ("{0}:{1}" -f $remote, $remoteArchive) if ($LASTEXITCODE -ne 0) { throw "release upload failed." } $remoteScript = @( "set -euo pipefail", "release_root='$releaseRoot'", "app_tag='$env:APP_TAG'", "archive='$remoteArchive'", "[[ -n `"`$release_root`" && `"`$app_tag`" =~ ^[0-9a-f]{12}$ ]]", "target=`"`$release_root/`$app_tag`"", "rm -rf `"`$target`"", "mkdir -p `"`$target`"", "tar -xzf `"`$archive`" -C `"`$target`"", "find `"`$target/deploy`" -maxdepth 1 -type f -name '*.sh' -exec chmod 755 {} +", "rm -f `"`$archive`"" ) -join "`n" $remoteScript | ssh @sshArgs $remote "bash -s" if ($LASTEXITCODE -ne 0) { throw "remote release unpack failed." } - name: Cleanup worktree if: ${{ always() }} shell: powershell run: | $ErrorActionPreference = "Stop" $worktreeRoot = Join-Path ([System.IO.Path]::GetTempPath()) "smartflow-actions" $expectedPrefix = $worktreeRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar if (-not [string]::IsNullOrWhiteSpace($env:SMARTFLOW_WORKTREE) -and $env:SMARTFLOW_WORKTREE.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $env:SMARTFLOW_WORKTREE -Recurse -Force -ErrorAction SilentlyContinue } deploy: runs-on: build-host needs: build-upload steps: - name: Trigger deploy env: SMARTFLOW_REPO_SHA: ${{ gitea.sha }} shell: bash run: | set -euo pipefail app_tag="${SMARTFLOW_REPO_SHA:0:12}" smartflow-release deploy "${app_tag}"