feat: automate staging deploy from Windows Gitea runner
Build immutable API and web images in Aliyun ACR after staging validation, then deploy them safely to the staging host.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
name: Staging Backend Quality Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/workflows/**'
|
||||
- 'deploy/**'
|
||||
- 'frontend/**'
|
||||
- 'jyotish_vedic/**'
|
||||
- 'scripts/**'
|
||||
- 'tests/**'
|
||||
- 'mcp_server.py'
|
||||
- 'pyproject.toml'
|
||||
- 'requirements*.txt'
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: staging-quality-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: runner-win
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
python -m pip install --upgrade pip
|
||||
if ($LASTEXITCODE -ne 0) { throw 'pip upgrade failed' }
|
||||
python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Python dependency installation failed' }
|
||||
npm ci --prefix frontend
|
||||
if ($LASTEXITCODE -ne 0) { throw 'frontend dependency installation failed' }
|
||||
- name: Validate backend, package, frontend, and database contracts
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
ruff check scripts/run_quality_gate.py tests/test_varga_bphs.py tests/test_ashtakavarga_invariants.py tests/test_cli_smoke.py tests/test_yoga_rules_integrity.py
|
||||
if ($LASTEXITCODE -ne 0) { throw 'ruff validation failed' }
|
||||
$pythonFiles = @(
|
||||
Get-ChildItem scripts -Filter '*.py' -File | ForEach-Object FullName
|
||||
Get-ChildItem jyotish_vedic -Filter '*.py' -File | ForEach-Object FullName
|
||||
(Resolve-Path mcp_server.py).Path
|
||||
)
|
||||
python -m py_compile @pythonFiles
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Python compilation failed' }
|
||||
python scripts/run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime
|
||||
if ($LASTEXITCODE -ne 0) { throw 'quick quality gate failed' }
|
||||
python scripts/commercial_privacy_artifact_scan.py --json
|
||||
if ($LASTEXITCODE -ne 0) { throw 'privacy scan failed' }
|
||||
python -m build
|
||||
if ($LASTEXITCODE -ne 0) { throw 'package build failed' }
|
||||
npm test --prefix frontend
|
||||
if ($LASTEXITCODE -ne 0) { throw 'frontend tests failed' }
|
||||
npm run lint --prefix frontend
|
||||
if ($LASTEXITCODE -ne 0) { throw 'frontend lint failed' }
|
||||
npm run build --prefix frontend
|
||||
if ($LASTEXITCODE -ne 0) { throw 'frontend build failed' }
|
||||
|
||||
publish-and-deploy:
|
||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/staging'
|
||||
needs: validate
|
||||
runs-on: runner-win
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
env:
|
||||
REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com
|
||||
IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha
|
||||
DEPLOY_HOST: ${{ vars.STAGING_HOST }}
|
||||
DEPLOY_PORT: ${{ vars.STAGING_PORT }}
|
||||
DEPLOY_USER: ${{ vars.STAGING_USER }}
|
||||
DEPLOY_PATH: ${{ vars.STAGING_PATH }}
|
||||
STAGING_URL: ${{ vars.STAGING_URL }}
|
||||
STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }}
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
with: { fetch-depth: 0, persist-credentials: false }
|
||||
- name: Build, publish, and deploy immutable staging images
|
||||
env:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
function Assert-Exit([string]$Message) {
|
||||
if ($LASTEXITCODE -ne 0) { throw $Message }
|
||||
}
|
||||
function Invoke-Ssh([string]$Command) {
|
||||
& ssh @script:SshOptions "$env:DEPLOY_USER@$env:DEPLOY_HOST" $Command
|
||||
Assert-Exit 'remote SSH command failed'
|
||||
}
|
||||
|
||||
if ($env:GITEA_EVENT_NAME -ne 'push' -or $env:GITEA_REF -ne 'refs/heads/staging') { throw 'not an exact staging push' }
|
||||
if ($env:GITEA_SHA -notmatch '^[0-9a-f]{40}$') { throw 'invalid staging commit SHA' }
|
||||
if ($env:DEPLOY_HOST -notmatch '^[A-Za-z0-9.-]+$' -or $env:DEPLOY_PORT -notmatch '^[1-9][0-9]{0,4}$' -or $env:DEPLOY_USER -notmatch '^[a-z_][a-z0-9_-]*$' -or $env:DEPLOY_PATH -notmatch '^/[A-Za-z0-9._/-]+$') { throw 'invalid staging SSH target' }
|
||||
if ($env:STAGING_URL -notmatch '^https://[A-Za-z0-9.-]+(?::[1-9][0-9]{0,4})?$' -or [string]::IsNullOrWhiteSpace($env:STAGING_KNOWN_HOSTS)) { throw 'invalid staging endpoint configuration' }
|
||||
$remoteLine = (& git ls-remote origin refs/heads/staging)
|
||||
Assert-Exit 'unable to resolve remote staging head'
|
||||
$remoteSha = (($remoteLine -split '\s+')[0])
|
||||
if ($remoteSha -ne $env:GITEA_SHA) { throw 'staging head changed before publication' }
|
||||
& git fetch origin main
|
||||
Assert-Exit 'unable to fetch reviewed main history'
|
||||
& git merge-base --is-ancestor $env:GITEA_SHA origin/main
|
||||
Assert-Exit 'staging revision is not in reviewed main history'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:REGISTRY_USERNAME) -or [string]::IsNullOrWhiteSpace($env:REGISTRY_PASSWORD)) { throw 'registry credentials are missing' }
|
||||
$env:REGISTRY_PASSWORD | & docker login $env:REGISTRY_HOST --username $env:REGISTRY_USERNAME --password-stdin
|
||||
Assert-Exit 'registry login failed'
|
||||
|
||||
$sshRoot = Join-Path $env:RUNNER_TEMP 'jyotisha-staging-ssh'
|
||||
$keyPath = Join-Path $sshRoot 'id_ed25519'
|
||||
$knownHostsPath = Join-Path $sshRoot 'known_hosts'
|
||||
$archivePath = Join-Path $env:RUNNER_TEMP "deploy-$($env:GITEA_RUN_NUMBER)-$($env:GITEA_RUN_ATTEMPT).tar"
|
||||
$incoming = "$env:DEPLOY_PATH/.incoming/$env:GITEA_RUN_NUMBER-$env:GITEA_RUN_ATTEMPT"
|
||||
$script:SshOptions = @('-i', $keyPath, '-p', $env:DEPLOY_PORT, '-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=yes', '-o', "UserKnownHostsFile=$knownHostsPath")
|
||||
$scpOptions = @('-i', $keyPath, '-P', $env:DEPLOY_PORT, '-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=yes', '-o', "UserKnownHostsFile=$knownHostsPath")
|
||||
$remotePrepared = $false
|
||||
try {
|
||||
$apiTag = "$env:IMAGE_REPOSITORY`:api-$env:GITEA_SHA"
|
||||
$webTag = "$env:IMAGE_REPOSITORY`:web-$env:GITEA_SHA"
|
||||
& docker build --file deploy/railway-api.Dockerfile --tag $apiTag .
|
||||
Assert-Exit 'API image build failed'
|
||||
& docker push $apiTag
|
||||
Assert-Exit 'API image push failed'
|
||||
& docker build --file deploy/railway-web.Dockerfile --tag $webTag .
|
||||
Assert-Exit 'web image build failed'
|
||||
& docker push $webTag
|
||||
Assert-Exit 'web image push failed'
|
||||
|
||||
$apiDigests = (& docker image inspect --format '{{json .RepoDigests}}' $apiTag | ConvertFrom-Json)
|
||||
Assert-Exit 'unable to inspect API image digests'
|
||||
$webDigests = (& docker image inspect --format '{{json .RepoDigests}}' $webTag | ConvertFrom-Json)
|
||||
Assert-Exit 'unable to inspect web image digests'
|
||||
$apiRef = @($apiDigests | Where-Object { $_ -match "^$([regex]::Escape($env:IMAGE_REPOSITORY))@sha256:[0-9a-f]{64}$" })[0]
|
||||
$webRef = @($webDigests | Where-Object { $_ -match "^$([regex]::Escape($env:IMAGE_REPOSITORY))@sha256:[0-9a-f]{64}$" })[0]
|
||||
if (-not $apiRef -or -not $webRef) { throw 'immutable image digest was not published' }
|
||||
$apiDigest = ($apiRef -split '@', 2)[1]
|
||||
$webDigest = ($webRef -split '@', 2)[1]
|
||||
$manifestPath = Join-Path $env:RUNNER_TEMP 'staging-image-manifest.env'
|
||||
[IO.File]::WriteAllText($manifestPath, "git_sha=$env:GITEA_SHA`napi_digest=$apiDigest`nweb_digest=$webDigest`n", [Text.UTF8Encoding]::new($false))
|
||||
$manifestOutput = & node frontend/scripts/staging-image-manifest.mjs $manifestPath $env:GITEA_SHA $env:IMAGE_REPOSITORY
|
||||
Assert-Exit 'immutable image manifest validation failed'
|
||||
$images = @{}
|
||||
foreach ($line in $manifestOutput) {
|
||||
$parts = $line -split '=', 2
|
||||
if ($parts.Count -eq 2) { $images[$parts[0]] = $parts[1] }
|
||||
}
|
||||
if (-not $images.api_image -or -not $images.web_image) { throw 'image manifest output is incomplete' }
|
||||
|
||||
New-Item -ItemType Directory -Path $sshRoot -Force | Out-Null
|
||||
$normalizedKey = ($env:SSH_PRIVATE_KEY -replace "`r`n", "`n" -replace "`r", "`n").TrimEnd("`n") + "`n"
|
||||
[IO.File]::WriteAllText($keyPath, $normalizedKey, [Text.UTF8Encoding]::new($false))
|
||||
$normalizedHosts = ($env:STAGING_KNOWN_HOSTS -replace "`r`n", "`n" -replace "`r", "`n").TrimEnd("`n") + "`n"
|
||||
[IO.File]::WriteAllText($knownHostsPath, $normalizedHosts, [Text.UTF8Encoding]::new($false))
|
||||
|
||||
& tar -cf $archivePath deploy
|
||||
Assert-Exit 'deploy archive creation failed'
|
||||
Invoke-Ssh "install -d -m 700 '$incoming/.docker'"
|
||||
$remotePrepared = $true
|
||||
& scp @scpOptions $archivePath "$env:DEPLOY_USER@$env:DEPLOY_HOST`:$incoming/deploy.tar"
|
||||
Assert-Exit 'deploy archive upload failed'
|
||||
Invoke-Ssh "tar -xf '$incoming/deploy.tar' -C '$incoming' && rm -f -- '$incoming/deploy.tar'"
|
||||
|
||||
$previousSha = (& ssh @script:SshOptions "$env:DEPLOY_USER@$env:DEPLOY_HOST" "state='$env:DEPLOY_PATH/.state/deployed-revision'; if [ -f \"`$state\" ]; then cat \"`$state\"; else id=`$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1); if [ -n \"`$id\" ]; then value=`$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \"`$id\" | sed -n 's/^GITHUB_SHA=//p' | head -n 1); printf '%s' \"`${value:-not-deployed}\"; else printf not-deployed; fi; fi").Trim()
|
||||
Assert-Exit 'unable to read deployed staging revision'
|
||||
if ($previousSha -ne 'not-deployed' -and $previousSha -notmatch '^[0-9a-f]{40}$') { throw 'invalid deployed staging revision state' }
|
||||
$forwardVerified = 'false'
|
||||
if ($previousSha -ne 'not-deployed' -and $previousSha -ne $env:GITEA_SHA) {
|
||||
& git cat-file -e "$previousSha^{commit}"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
& git fetch origin $previousSha
|
||||
Assert-Exit 'unable to fetch deployed staging revision'
|
||||
}
|
||||
& git merge-base --is-ancestor $previousSha $env:GITEA_SHA
|
||||
Assert-Exit 'automatic rollback or divergent staging deployment refused'
|
||||
$forwardVerified = 'true'
|
||||
}
|
||||
|
||||
$env:REGISTRY_PASSWORD | & ssh @script:SshOptions "$env:DEPLOY_USER@$env:DEPLOY_HOST" "DOCKER_CONFIG='$incoming/.docker' docker login '$env:REGISTRY_HOST' --username '$env:REGISTRY_USERNAME' --password-stdin"
|
||||
Assert-Exit 'remote registry login failed'
|
||||
Invoke-Ssh "INCOMING_PATH='$incoming' DEPLOY_PATH='$env:DEPLOY_PATH' API_IMAGE='$($images.api_image)' WEB_IMAGE='$($images.web_image)' DEPLOY_SHA='$env:GITEA_SHA' EXPECTED_PREVIOUS_SHA='$previousSha' ALLOW_ROLLBACK='false' FORWARD_REVISION_VERIFIED='$forwardVerified' DOCKER_CONFIG='$incoming/.docker' STAGING_URL='$env:STAGING_URL' bash '$incoming/deploy/run-staging-deploy.sh'"
|
||||
} finally {
|
||||
if ($remotePrepared) {
|
||||
& ssh @script:SshOptions "$env:DEPLOY_USER@$env:DEPLOY_HOST" "DOCKER_CONFIG='$incoming/.docker' docker logout '$env:REGISTRY_HOST' >/dev/null 2>&1 || true; rm -rf -- '$incoming'" 2>$null
|
||||
}
|
||||
& docker logout $env:REGISTRY_HOST 2>$null | Out-Null
|
||||
Remove-Item $archivePath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $sshRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -14,7 +14,7 @@ for key in "${required[@]}"; do
|
||||
done
|
||||
|
||||
sha_pattern='^[0-9a-f]{40}$'
|
||||
digest_pattern='^ghcr\.io/jesse-ux/jyotisha-(api|web)@sha256:[0-9a-f]{64}$'
|
||||
digest_pattern='^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[1-9][0-9]{0,4})?(/[a-z0-9]+([._-][a-z0-9]+)*)+@sha256:[0-9a-f]{64}$'
|
||||
image_id_pattern='^sha256:[0-9a-f]{64}$'
|
||||
if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] ||
|
||||
[[ ! "$API_IMAGE" =~ $digest_pattern ]] ||
|
||||
@@ -22,6 +22,8 @@ if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] ||
|
||||
echo "unsafe staging image identity" >&2
|
||||
exit 1
|
||||
fi
|
||||
api_repository="${API_IMAGE%@sha256:*}"
|
||||
web_repository="${WEB_IMAGE%@sha256:*}"
|
||||
if [ "$ALLOW_ROLLBACK" != "true" ] && [ "$ALLOW_ROLLBACK" != "false" ]; then
|
||||
echo "invalid rollback authorization" >&2
|
||||
exit 1
|
||||
@@ -85,8 +87,8 @@ repo_digest_for_container() {
|
||||
awk -v prefix="$repository@sha256:" 'index($0, prefix) == 1 { print; exit }'
|
||||
}
|
||||
|
||||
previous_api_image="$(repo_digest_for_container api ghcr.io/jesse-ux/jyotisha-api)"
|
||||
previous_web_image="$(repo_digest_for_container web ghcr.io/jesse-ux/jyotisha-web)"
|
||||
previous_api_image="$(repo_digest_for_container api "$api_repository")"
|
||||
previous_web_image="$(repo_digest_for_container web "$web_repository")"
|
||||
previous_api_id=""
|
||||
previous_web_id=""
|
||||
if [ -n "$(container_id api)" ]; then
|
||||
|
||||
@@ -5,8 +5,14 @@ import { pathToFileURL } from "node:url";
|
||||
const shaPattern = /^[0-9a-f]{40}$/;
|
||||
const digestPattern = /^sha256:[0-9a-f]{64}$/;
|
||||
const expectedKeys = ["git_sha", "api_digest", "web_digest"];
|
||||
const defaultRegistry = "ghcr.io/jesse-ux";
|
||||
const acrRepository = "crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha";
|
||||
const registryPattern = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(?::[1-9][0-9]{0,4})?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
||||
|
||||
export function parseStagingImageManifest(text, expectedSha) {
|
||||
export function parseStagingImageManifest(text, expectedSha, registry = defaultRegistry) {
|
||||
if (!registryPattern.test(registry)) {
|
||||
throw new Error("invalid staging image registry");
|
||||
}
|
||||
if (!shaPattern.test(expectedSha)) {
|
||||
throw new Error("invalid expected staging revision");
|
||||
}
|
||||
@@ -37,12 +43,13 @@ export function parseStagingImageManifest(text, expectedSha) {
|
||||
}
|
||||
}
|
||||
|
||||
const sharedRepository = registry === acrRepository;
|
||||
return {
|
||||
gitSha: expectedSha,
|
||||
apiDigest: values.get("api_digest"),
|
||||
webDigest: values.get("web_digest"),
|
||||
apiImage: `ghcr.io/jesse-ux/jyotisha-api@${values.get("api_digest")}`,
|
||||
webImage: `ghcr.io/jesse-ux/jyotisha-web@${values.get("web_digest")}`,
|
||||
apiImage: `${sharedRepository ? registry : `${registry}/jyotisha-api`}@${values.get("api_digest")}`,
|
||||
webImage: `${sharedRepository ? registry : `${registry}/jyotisha-web`}@${values.get("web_digest")}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,13 +59,14 @@ const invokedPath = process.argv[1]
|
||||
|
||||
if (invokedPath === import.meta.url) {
|
||||
try {
|
||||
const [manifestPath, expectedSha] = process.argv.slice(2);
|
||||
const [manifestPath, expectedSha, registry] = process.argv.slice(2);
|
||||
if (!manifestPath || !expectedSha) {
|
||||
throw new Error("manifest path and expected revision are required");
|
||||
}
|
||||
const manifest = parseStagingImageManifest(
|
||||
await readFile(manifestPath, "utf8"),
|
||||
expectedSha,
|
||||
registry,
|
||||
);
|
||||
process.stdout.write(
|
||||
[
|
||||
|
||||
@@ -30,6 +30,10 @@ const syncScript = new URL(
|
||||
"../../deploy/sync-staging-tree.sh",
|
||||
import.meta.url,
|
||||
);
|
||||
const giteaQualityWorkflow = new URL(
|
||||
"../../.gitea/workflows/backend-quality-gate.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
function read(url: URL): string {
|
||||
return readFileSync(url, "utf8");
|
||||
@@ -376,6 +380,21 @@ test("production remains manual-only and separate from staging database automati
|
||||
assert.doesNotMatch(production, /docker-compose\.postgres\.yml|db:migrate/);
|
||||
});
|
||||
|
||||
test("Gitea staging push uses the Windows runner and immutable ACR images", () => {
|
||||
const workflow = read(giteaQualityWorkflow);
|
||||
assert.match(workflow, /runs-on: runner-win/);
|
||||
assert.match(workflow, /shell: powershell/);
|
||||
assert.match(workflow, /crpi-d1feco6itet73spp\.cn-hongkong\.personal\.cr\.aliyuncs\.com\/copse\/jyotisha/);
|
||||
assert.match(workflow, /secrets\.REGISTRY_USERNAME/);
|
||||
assert.match(workflow, /secrets\.REGISTRY_PASSWORD/);
|
||||
assert.match(workflow, /:api-\$env:GITEA_SHA/);
|
||||
assert.match(workflow, /:web-\$env:GITEA_SHA/);
|
||||
assert.match(workflow, /EXPECTED_PREVIOUS_SHA='\$previousSha'/);
|
||||
assert.match(workflow, /git merge-base --is-ancestor \$previousSha \$env:GITEA_SHA/);
|
||||
assert.match(workflow, /\$scpOptions = @\([^\n]*'-P'/);
|
||||
assert.doesNotMatch(workflow, /17631000304|copse\.ai\.2026/);
|
||||
});
|
||||
|
||||
test("staging scripts pass shell syntax validation", () => {
|
||||
for (const script of [deployScript, migrationScript, syncScript]) {
|
||||
const path = fileURLToPath(script);
|
||||
|
||||
@@ -25,6 +25,18 @@ test("manifest produces immutable GHCR digest references", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("manifest produces immutable shared ACR repository references", () => {
|
||||
const repository =
|
||||
"crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha";
|
||||
assert.deepEqual(parseStagingImageManifest(validManifest(), gitSha, repository), {
|
||||
gitSha,
|
||||
apiDigest,
|
||||
webDigest,
|
||||
apiImage: `${repository}@${apiDigest}`,
|
||||
webImage: `${repository}@${webDigest}`,
|
||||
});
|
||||
});
|
||||
|
||||
test("manifest rejects revision drift, mutable tags, duplicates, extras, and malformed digests", () => {
|
||||
const invalid = [
|
||||
validManifest().replace(gitSha, "f".repeat(40)),
|
||||
@@ -41,4 +53,14 @@ test("manifest rejects revision drift, mutable tags, duplicates, extras, and mal
|
||||
for (const contents of invalid) {
|
||||
assert.throws(() => parseStagingImageManifest(contents, gitSha));
|
||||
}
|
||||
for (const registry of [
|
||||
"https://git.copse.top/root",
|
||||
"git.copse.top/root;touch /tmp/pwned",
|
||||
"git.copse.top/Root",
|
||||
"git.copse.top/root@evil.example",
|
||||
"git.copse.top/root/../evil",
|
||||
"",
|
||||
]) {
|
||||
assert.throws(() => parseStagingImageManifest(validManifest(), gitSha, registry));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user