441 lines
21 KiB
PowerShell
441 lines
21 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$EvidenceDirectory = 'output\aug1-rc\0.1.13-113-af8af2f6-frozen',
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BackendJar,
|
|
|
|
[string]$ContentCandidate = 'docs\content-candidates\aug1-life-advisor-content-candidates-v0.1.json',
|
|
|
|
[string]$RuntimeCommit = 'af8af2f6dd169e4616a4ca0ca724e8b809502ab3',
|
|
|
|
[string]$OperationsCommit = 'HEAD',
|
|
|
|
[string]$OutputDirectory = 'output\aug1-release',
|
|
|
|
[switch]$PlanOnly
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
|
$allowedOutputRoot = [IO.Path]::GetFullPath((Join-Path $projectRoot 'output'))
|
|
$utf8NoBom = New-Object Text.UTF8Encoding($false)
|
|
|
|
function Resolve-ProjectPath {
|
|
param([string]$Path)
|
|
if ([IO.Path]::IsPathRooted($Path)) {
|
|
return [IO.Path]::GetFullPath($Path)
|
|
}
|
|
return [IO.Path]::GetFullPath((Join-Path $projectRoot $Path))
|
|
}
|
|
|
|
function Read-Git {
|
|
param([string[]]$Arguments)
|
|
$output = & git -C $projectRoot @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "git command failed: git $($Arguments -join ' ')"
|
|
}
|
|
return ($output | Out-String).Trim()
|
|
}
|
|
|
|
function Get-Sha256 {
|
|
param([string]$Path)
|
|
return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Require-File {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
throw "Required release input is missing: $Path"
|
|
}
|
|
}
|
|
|
|
function Write-Utf8 {
|
|
param(
|
|
[string]$Path,
|
|
[string]$Content
|
|
)
|
|
[IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
|
|
}
|
|
|
|
function Ensure-UnderOutput {
|
|
param([string]$Path)
|
|
$resolved = [IO.Path]::GetFullPath($Path)
|
|
$prefix = $allowedOutputRoot.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
|
|
if (-not $resolved.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Release packaging path must stay under $allowedOutputRoot"
|
|
}
|
|
return $resolved
|
|
}
|
|
|
|
$evidenceDirectoryPath = Resolve-ProjectPath $EvidenceDirectory
|
|
$backendJarPath = Resolve-ProjectPath $BackendJar
|
|
$contentCandidatePath = Resolve-ProjectPath $ContentCandidate
|
|
$outputDirectoryPath = Ensure-UnderOutput (Resolve-ProjectPath $OutputDirectory)
|
|
$evidencePath = Join-Path $evidenceDirectoryPath 'release-evidence.json'
|
|
|
|
Require-File $evidencePath
|
|
Require-File $backendJarPath
|
|
Require-File $contentCandidatePath
|
|
|
|
$runtimeCommitSha = Read-Git @('rev-parse', '--verify', "$RuntimeCommit^{commit}")
|
|
$operationsCommitSha = Read-Git @('rev-parse', '--verify', "$OperationsCommit^{commit}")
|
|
& git -C $projectRoot merge-base --is-ancestor $runtimeCommitSha $operationsCommitSha
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Runtime commit must be an ancestor of the operations commit: $runtimeCommitSha"
|
|
}
|
|
|
|
$worktreeStatus = Read-Git @('status', '--porcelain')
|
|
if (-not $PlanOnly -and -not [string]::IsNullOrWhiteSpace($worktreeStatus)) {
|
|
throw 'Release packaging requires a clean Git worktree.'
|
|
}
|
|
|
|
$evidence = Get-Content -Raw -Encoding UTF8 -LiteralPath $evidencePath | ConvertFrom-Json
|
|
if (-not [bool]$evidence.releaseEligible -or -not [bool]$evidence.git.clean) {
|
|
throw 'Frozen evidence is not release eligible or was produced from a dirty worktree.'
|
|
}
|
|
if ([string]$evidence.git.commit -ne $runtimeCommitSha) {
|
|
throw "Frozen evidence commit does not match runtime commit: $($evidence.git.commit)"
|
|
}
|
|
|
|
$apkPath = [IO.Path]::GetFullPath([string]$evidence.artifacts.apk.path)
|
|
$evidencePrefix = $evidenceDirectoryPath.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
|
|
if (-not $apkPath.StartsWith($evidencePrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw 'Frozen APK path resolves outside the evidence directory.'
|
|
}
|
|
Require-File $apkPath
|
|
|
|
$apkSha256 = Get-Sha256 $apkPath
|
|
$backendSha256 = Get-Sha256 $backendJarPath
|
|
$contentSha256 = Get-Sha256 $contentCandidatePath
|
|
if ($apkSha256 -ne ([string]$evidence.artifacts.apk.sha256).ToLowerInvariant()) {
|
|
throw "Frozen APK hash mismatch: $apkSha256"
|
|
}
|
|
if ($backendSha256 -ne ([string]$evidence.artifacts.backendJarSha256).ToLowerInvariant()) {
|
|
throw "Frozen backend JAR hash mismatch: $backendSha256"
|
|
}
|
|
if ($contentSha256 -ne ([string]$evidence.contentCandidates.sha256).ToLowerInvariant()) {
|
|
throw "Content candidate hash mismatch: $contentSha256"
|
|
}
|
|
|
|
$goNoGoCandidates = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs') -File -Filter '*Android*Go-No-Go-20260728.md')
|
|
if ($goNoGoCandidates.Count -ne 1) {
|
|
throw "Expected one Android Go/No-Go document, found $($goNoGoCandidates.Count)."
|
|
}
|
|
$businessSignoffCandidates = @(
|
|
Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs') -File -Filter '*20260729.md' |
|
|
Where-Object {
|
|
$candidateText = Get-Content -Raw -Encoding UTF8 -LiteralPath $_.FullName
|
|
$candidateText.Contains('direct_president') -and
|
|
$candidateText.Contains('direct_finance') -and
|
|
$candidateText.Contains('direct_hr') -and
|
|
$candidateText.Contains('direct_audit') -and
|
|
$candidateText.Contains('direct_operations')
|
|
}
|
|
)
|
|
if ($businessSignoffCandidates.Count -ne 1) {
|
|
throw "Expected one August 1 business-content and five-channel sign-off document, found $($businessSignoffCandidates.Count)."
|
|
}
|
|
$businessSignoffPath = $businessSignoffCandidates[0].FullName
|
|
$operationSources = [ordered]@{
|
|
'operations/release-backend.sh' = Join-Path $projectRoot 'scripts\release-backend.sh'
|
|
'operations/release-preflight.sh' = Join-Path $projectRoot 'scripts\release-preflight.sh'
|
|
'operations/verify-aug1-production-api-readonly.sh' = Join-Path $projectRoot 'scripts\verify-aug1-production-api-readonly.sh'
|
|
'operations/verify-aug1-authenticated-production.sh' = Join-Path $projectRoot 'scripts\verify-aug1-authenticated-production.sh'
|
|
'operations/verify-aug1-content-candidates.mjs' = Join-Path $projectRoot 'scripts\verify-aug1-content-candidates.mjs'
|
|
'operations/capture-android-acceptance.ps1' = Join-Path $projectRoot 'scripts\capture-android-acceptance.ps1'
|
|
'operations/go-no-go.md' = $goNoGoCandidates[0].FullName
|
|
'operations/business-content-and-five-channel-signoff.md' = $businessSignoffPath
|
|
}
|
|
foreach ($source in $operationSources.Values) {
|
|
Require-File $source
|
|
}
|
|
|
|
$versionName = [string]$evidence.app.versionName
|
|
$versionCode = [string]$evidence.app.versionCode
|
|
$runtimeShort = $runtimeCommitSha.Substring(0, 8)
|
|
$operationsShort = $operationsCommitSha.Substring(0, 8)
|
|
$packageName = "bangdao-aug1-$versionName-$versionCode-$runtimeShort-ops$operationsShort.zip"
|
|
$packagePath = Ensure-UnderOutput (Join-Path $outputDirectoryPath $packageName)
|
|
|
|
Write-Output "package_mode=$(if ($PlanOnly) { 'read-only-plan' } else { 'build' })"
|
|
Write-Output "runtime_commit=$runtimeCommitSha"
|
|
Write-Output "operations_commit=$operationsCommitSha"
|
|
Write-Output "apk_sha256=$apkSha256"
|
|
Write-Output "backend_sha256=$backendSha256"
|
|
Write-Output "content_sha256=$contentSha256"
|
|
Write-Output "package_path=$packagePath"
|
|
Write-Output 'package_entries=17'
|
|
if ($PlanOnly) {
|
|
exit 0
|
|
}
|
|
if (Test-Path -LiteralPath $packagePath) {
|
|
throw "Release package already exists and will not be overwritten: $packagePath"
|
|
}
|
|
|
|
New-Item -ItemType Directory -Path $outputDirectoryPath -Force | Out-Null
|
|
$stagingDirectory = Ensure-UnderOutput (Join-Path $outputDirectoryPath ('.staging-' + [Guid]::NewGuid().ToString('N')))
|
|
$partialPackagePath = Ensure-UnderOutput ($packagePath + '.partial-' + $PID)
|
|
New-Item -ItemType Directory -Path $stagingDirectory | Out-Null
|
|
|
|
$apkName = "bangdao-$versionName-$versionCode-dcloud-test.apk"
|
|
$backendName = "ruoyi-admin-$runtimeShort.jar"
|
|
$generatedAt = Read-Git @('show', '-s', '--format=%cI', $operationsCommitSha)
|
|
$operationsEpoch = [int64](Read-Git @('show', '-s', '--format=%ct', $operationsCommitSha))
|
|
$entryTimestamp = [DateTimeOffset]::FromUnixTimeSeconds($operationsEpoch)
|
|
if ($entryTimestamp.Year -lt 1980) {
|
|
$entryTimestamp = [DateTimeOffset]::new(1980, 1, 1, 0, 0, 0, [TimeSpan]::Zero)
|
|
}
|
|
|
|
try {
|
|
Copy-Item -LiteralPath $apkPath -Destination (Join-Path $stagingDirectory $apkName)
|
|
Copy-Item -LiteralPath $backendJarPath -Destination (Join-Path $stagingDirectory $backendName)
|
|
Copy-Item -LiteralPath $contentCandidatePath -Destination (Join-Path $stagingDirectory 'aug1-life-advisor-content-candidates-v0.1.json')
|
|
Copy-Item -LiteralPath $evidencePath -Destination (Join-Path $stagingDirectory 'release-evidence.json')
|
|
|
|
foreach ($entry in $operationSources.GetEnumerator()) {
|
|
$destination = Join-Path $stagingDirectory ($entry.Key -replace '/', '\')
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
|
|
Copy-Item -LiteralPath $entry.Value -Destination $destination
|
|
}
|
|
|
|
$readme = @(
|
|
'# Bangdao August 1 Android controlled-test release package',
|
|
'',
|
|
'## Dual-commit freeze',
|
|
'',
|
|
"- Runtime commit: $runtimeCommitSha (unchanged APK/JAR)",
|
|
"- Operations commit: $operationsCommitSha (deploy, rollback, API and Android evidence tools)",
|
|
'- Automated candidate: `releaseEligible=true`',
|
|
'- Current decision: **No-Go**; Android device permission/write/media, formal content/handler, production authorization and enterprise sign-off gates remain.',
|
|
'',
|
|
'## Core artifacts',
|
|
'',
|
|
"| File | SHA-256 |",
|
|
'|---|---|',
|
|
"| $apkName | $apkSha256 |",
|
|
"| $backendName | $backendSha256 |",
|
|
"| aug1-life-advisor-content-candidates-v0.1.json | $contentSha256 |",
|
|
'',
|
|
'The APK uses a DCloud test signer and is limited to a small controlled internal test. Admin/H5 are deferred, schema is unchanged, and content candidates must not be imported or enabled.',
|
|
'',
|
|
'## Release procedure',
|
|
'',
|
|
'Run the versioned release script from a clean checkout of the operations commit. The ZIP operations folder is an audit snapshot and must not be executed outside repository context. Run read-only plan first; deploy only after separate explicit authorization. Do not manually overwrite the JAR.',
|
|
'',
|
|
'Run the GET-only API probe before and after release, and capture read-only Android evidence after each manual step. Neither replaces Android device acceptance or formal business sign-off.',
|
|
'',
|
|
'Recheck the bundled disabled content candidates with `node operations/verify-aug1-content-candidates.mjs aug1-life-advisor-content-candidates-v0.1.json`. Complete `operations/business-content-and-five-channel-signoff.md` before enabling content or binding handlers.',
|
|
'',
|
|
'The fixed-code authenticated smoke is bundled as `operations/verify-aug1-authenticated-production.sh`. It requires the explicit `--execute` flag, selects an existing eligible APP identity without printing it, never enables or sends real SMS, and does not submit business records.',
|
|
'',
|
|
'This package does not authorize production deployment, service restart, database change, Git push or public distribution.'
|
|
) -join [Environment]::NewLine
|
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'README.md') -Content ($readme + [Environment]::NewLine)
|
|
|
|
$preflightSummary = @(
|
|
'# August 1 production preflight summary',
|
|
'',
|
|
"- Generated from operations commit: $operationsCommitSha",
|
|
'- Check type: read-only deploy plan; no deployment, restart, database mutation, SMS request, login, upload or static sync.',
|
|
'- Backend deploy plan: passed for the frozen JAR.',
|
|
"- Candidate JAR SHA-256: $backendSha256",
|
|
'- Current production JAR SHA-256: `46c99cff75d21f8536a71c3c058b6437e99dbf9ff4bae3542b44471f3f34ad84`',
|
|
'- Production service: `wygj-aihr.service` active and starts the fixed JAR path.',
|
|
'- Production schema: 64/64 and runtime schema bootstrap disabled/default.',
|
|
'- Production GET-only route probe: 20/20 (`1x200`, `6x401`, `13x405`).',
|
|
'- Separate fixed-code authenticated smoke passed request/login, read-only business routes, logout, old-token rejection and re-login; application logs confirmed that no real SMS was sent.',
|
|
'- Backend normalized AIHR module remains mismatched until an authorized deploy.',
|
|
'- Overall result: **No-Go** pending the manual and authorization gates in `operations/go-no-go.md`.'
|
|
) -join [Environment]::NewLine
|
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'production-preflight-summary.md') -Content ($preflightSummary + [Environment]::NewLine)
|
|
|
|
$contentReview = @(
|
|
'# August 1 content-candidate review notice',
|
|
'',
|
|
"- Content version: $($evidence.contentCandidates.contentVersion)",
|
|
"- Status: $($evidence.contentCandidates.candidateStatus)",
|
|
"- Scenario candidates: $($evidence.contentCandidates.scenarioCandidates)",
|
|
"- Policy-question candidates: $($evidence.contentCandidates.policyQuestionCandidates)",
|
|
'- Formally publishable scenarios: 0',
|
|
'- Formally sourced standard answers: 0',
|
|
'',
|
|
'Candidates are for business review only. Do not import, enable or count them toward August 1 acceptance until formal sources and item-level sign-off are complete.',
|
|
'',
|
|
'Run `node operations/verify-aug1-content-candidates.mjs aug1-life-advisor-content-candidates-v0.1.json` after extraction, then use `operations/business-content-and-five-channel-signoff.md` for item-level review and handler assignment.'
|
|
) -join [Environment]::NewLine
|
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'content-review-readme.md') -Content ($contentReview + [Environment]::NewLine)
|
|
|
|
$manifest = [ordered]@{
|
|
packageFormatVersion = 2
|
|
releaseTarget = [string]$evidence.releaseTarget
|
|
profile = 'android-controlled-test'
|
|
generatedAt = $generatedAt
|
|
runtimeCommit = $runtimeCommitSha
|
|
operationsCommit = $operationsCommitSha
|
|
goNoGo = 'NO_GO'
|
|
automatedReleaseEligible = $true
|
|
android = [ordered]@{
|
|
packageName = [string]$evidence.app.packageName
|
|
versionName = $versionName
|
|
versionCode = [int]$versionCode
|
|
targetSdkVersion = [int]$evidence.app.targetSdkVersion
|
|
artifact = $apkName
|
|
sha256 = $apkSha256
|
|
signerSha256 = ([string]$evidence.app.signerSha256).ToLowerInvariant()
|
|
internalTestOnly = $true
|
|
androidDeviceAcceptancePending = $true
|
|
loginLegalInteractionOnAndroidPending = $true
|
|
}
|
|
backend = [ordered]@{
|
|
artifact = $backendName
|
|
sha256 = $backendSha256
|
|
productionSha256 = '46c99cff75d21f8536a71c3c058b6437e99dbf9ff4bae3542b44471f3f34ad84'
|
|
readOnlyDeployPlanPassed = $true
|
|
deploymentAuthorized = $false
|
|
target = 'YCWY:/opt/wygj/app/ruoyi-admin.jar'
|
|
service = 'wygj-aihr.service'
|
|
schemaChangeRequired = $false
|
|
}
|
|
productionReadOnly = [ordered]@{
|
|
schema = '64/64'
|
|
runtimeSchemaBootstrap = 'false/default'
|
|
routeProbe = '20/20: 1 public 200, 6 auth 401, 13 method 405'
|
|
deployPlanSmsOrLoginTriggered = $false
|
|
authenticatedFixedCodeSmokePassed = $true
|
|
fixedCodeNoRealSmsConfirmed = $true
|
|
}
|
|
contentCandidates = [ordered]@{
|
|
artifact = 'aug1-life-advisor-content-candidates-v0.1.json'
|
|
sha256 = $contentSha256
|
|
status = [string]$evidence.contentCandidates.candidateStatus
|
|
publishable = $false
|
|
scenarioCandidates = [int]$evidence.contentCandidates.scenarioCandidates
|
|
policyQuestionCandidates = [int]$evidence.contentCandidates.policyQuestionCandidates
|
|
formalPublishableScenarios = 0
|
|
formallySourcedStandardAnswers = 0
|
|
}
|
|
operations = @($operationSources.Keys)
|
|
manualGatesRemaining = @($evidence.manualGatesRemaining)
|
|
}
|
|
$manifestJson = $manifest | ConvertTo-Json -Depth 8
|
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'release-manifest.json') -Content ($manifestJson + [Environment]::NewLine)
|
|
|
|
$payloadFiles = Get-ChildItem -LiteralPath $stagingDirectory -Recurse -File |
|
|
Sort-Object { $_.FullName.Substring($stagingDirectory.Length + 1) }
|
|
$checksumLines = foreach ($file in $payloadFiles) {
|
|
$relative = $file.FullName.Substring($stagingDirectory.Length + 1).Replace('\', '/')
|
|
"$(Get-Sha256 $file.FullName) $relative"
|
|
}
|
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'SHA256SUMS.txt') -Content (($checksumLines -join [Environment]::NewLine) + [Environment]::NewLine)
|
|
|
|
$allFiles = Get-ChildItem -LiteralPath $stagingDirectory -Recurse -File |
|
|
Sort-Object { $_.FullName.Substring($stagingDirectory.Length + 1) }
|
|
if ($allFiles.Count -ne 17) {
|
|
throw "Release package expected 17 files, found $($allFiles.Count)."
|
|
}
|
|
foreach ($file in $allFiles) {
|
|
$file.LastWriteTimeUtc = $entryTimestamp.UtcDateTime
|
|
}
|
|
|
|
Add-Type -AssemblyName System.IO.Compression
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$archiveStream = [IO.File]::Open($partialPackagePath, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
|
|
try {
|
|
$archive = New-Object IO.Compression.ZipArchive($archiveStream, [IO.Compression.ZipArchiveMode]::Create, $true)
|
|
try {
|
|
foreach ($file in $allFiles) {
|
|
$relative = $file.FullName.Substring($stagingDirectory.Length + 1).Replace('\', '/')
|
|
$entry = $archive.CreateEntry($relative, [IO.Compression.CompressionLevel]::Optimal)
|
|
$entry.LastWriteTime = $entryTimestamp
|
|
$entryStream = $entry.Open()
|
|
$sourceStream = [IO.File]::OpenRead($file.FullName)
|
|
try {
|
|
$sourceStream.CopyTo($entryStream)
|
|
}
|
|
finally {
|
|
$sourceStream.Dispose()
|
|
$entryStream.Dispose()
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
$archive.Dispose()
|
|
}
|
|
}
|
|
finally {
|
|
$archiveStream.Dispose()
|
|
}
|
|
|
|
$verificationArchive = [IO.Compression.ZipFile]::OpenRead($partialPackagePath)
|
|
try {
|
|
if ($verificationArchive.Entries.Count -ne 17) {
|
|
throw "ZIP verification expected 17 entries, found $($verificationArchive.Entries.Count)."
|
|
}
|
|
$entryNames = @($verificationArchive.Entries | ForEach-Object { $_.FullName })
|
|
foreach ($requiredEntry in @(
|
|
$apkName,
|
|
$backendName,
|
|
'release-manifest.json',
|
|
'SHA256SUMS.txt',
|
|
'operations/release-backend.sh',
|
|
'operations/verify-aug1-authenticated-production.sh',
|
|
'operations/verify-aug1-content-candidates.mjs',
|
|
'operations/business-content-and-five-channel-signoff.md'
|
|
)) {
|
|
if ($entryNames -notcontains $requiredEntry) {
|
|
throw "ZIP verification is missing $requiredEntry"
|
|
}
|
|
}
|
|
$expectedEntryHashes = @{}
|
|
foreach ($checksumLine in $checksumLines) {
|
|
if ($checksumLine -notmatch '^([0-9a-f]{64}) (.+)$') {
|
|
throw "Invalid generated checksum line: $checksumLine"
|
|
}
|
|
$expectedEntryHashes[$Matches[2]] = $Matches[1]
|
|
}
|
|
foreach ($entry in $verificationArchive.Entries) {
|
|
if ($entry.FullName -eq 'SHA256SUMS.txt') {
|
|
continue
|
|
}
|
|
if (-not $expectedEntryHashes.ContainsKey($entry.FullName)) {
|
|
throw "ZIP verification has no expected hash for $($entry.FullName)"
|
|
}
|
|
$entryStream = $entry.Open()
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$hashBytes = $sha256.ComputeHash($entryStream)
|
|
$actualEntryHash = ([BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
|
|
}
|
|
finally {
|
|
$sha256.Dispose()
|
|
$entryStream.Dispose()
|
|
}
|
|
if ($actualEntryHash -ne $expectedEntryHashes[$entry.FullName]) {
|
|
throw "ZIP entry hash mismatch: $($entry.FullName)"
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
$verificationArchive.Dispose()
|
|
}
|
|
|
|
[IO.File]::Move($partialPackagePath, $packagePath)
|
|
Write-Output "package_bytes=$((Get-Item -LiteralPath $packagePath).Length)"
|
|
Write-Output "package_sha256=$(Get-Sha256 $packagePath)"
|
|
Write-Output 'package_verified=true'
|
|
}
|
|
finally {
|
|
if (Test-Path -LiteralPath $partialPackagePath -PathType Leaf) {
|
|
Remove-Item -LiteralPath $partialPackagePath -Force
|
|
}
|
|
if (Test-Path -LiteralPath $stagingDirectory -PathType Container) {
|
|
$resolvedStaging = [IO.Path]::GetFullPath($stagingDirectory)
|
|
$outputPrefix = $allowedOutputRoot.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
|
|
if (-not $resolvedStaging.StartsWith($outputPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Refusing to clean unsafe staging directory: $resolvedStaging"
|
|
}
|
|
Remove-Item -LiteralPath $resolvedStaging -Recurse -Force
|
|
}
|
|
}
|