- ci.yml: on push removido; schedule */30 + workflow_dispatch - job check (host, sem pnpm): SHA deployado via ssh + diff + classificador .dockerignore (scripts/ci-classify-production.mjs, lib/dockerignore.mjs) - suite fail-open no gate; deploy duplo-gated (checks + production) - deploy-homeserver.sh: guard 'already deployed' (idempotente) - agent-pool.yml removido; pool dormente - specs: dockerignoreMatch, ciClassifyProduction, pins ciSkipInvariants/deployScript - docs: runbook, AGENTS.md, AGENT-OPS.md, changelog
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..')
|
|
const scriptPath = join(repoRoot, 'scripts', 'deploy-homeserver.sh')
|
|
const script = readFileSync(scriptPath, 'utf8')
|
|
|
|
describe('scripts/deploy-homeserver.sh (OPS53 deploy pipeline)', () => {
|
|
it('is valid bash', () => {
|
|
expect(() => execFileSync('bash', ['-n', scriptPath], { stdio: 'pipe' })).not.toThrow()
|
|
})
|
|
|
|
it('guards against stale runs: only the current main HEAD deploys', () => {
|
|
expect(script).toContain('git ls-remote')
|
|
expect(script).toContain('refs/heads/main')
|
|
expect(script).toContain('stale run')
|
|
})
|
|
|
|
it('skips idempotently when the compose already references the SHA (OPS65)', () => {
|
|
expect(script).toContain('already deployed')
|
|
expect(script).toContain('image: teqo-1313:$SHA')
|
|
expect(script).toContain('docker-compose.yml')
|
|
})
|
|
|
|
it('builds with BuildKit secrets — prod credentials never enter image layers', () => {
|
|
expect(script).toContain('--secret')
|
|
expect(script).toContain('id=database_url,env=DATABASE_URL')
|
|
expect(script).toContain('id=payload_secret,env=PAYLOAD_SECRET')
|
|
})
|
|
|
|
it('reaches the prod DB from the build (loopback proxy on the compose network)', () => {
|
|
// BuildKit rejects `--network <bridge>`; the build uses --network host and
|
|
// a socat proxy (on stack_default, published on the host loopback) with a
|
|
// rewritten DATABASE_URL.
|
|
expect(script).toContain('--network host')
|
|
expect(script).toContain('teqo-1313-build-proxy')
|
|
expect(script).toContain('TCP:postgres:5432')
|
|
expect(script).toContain('127.0.0.1:5433')
|
|
})
|
|
|
|
it('applies migrations through the maintenance service before the rollout', () => {
|
|
// The rollback helper also contains a `compose up -d` (before the migrate
|
|
// step); the REAL rollout is the last occurrence.
|
|
const migrateIndex = script.indexOf('teqo-1313-migrate')
|
|
const upIndex = script.lastIndexOf('docker compose up -d teqo-1313')
|
|
expect(migrateIndex).toBeGreaterThan(-1)
|
|
expect(upIndex).toBeGreaterThan(-1)
|
|
expect(migrateIndex).toBeLessThan(upIndex)
|
|
})
|
|
|
|
it('smokes the deployed surface with the prod revalidate secret', () => {
|
|
for (const fragment of [
|
|
'/campanha/login',
|
|
'/admin',
|
|
'/campanha/webauthn/login-options',
|
|
'/api/revalidate',
|
|
'x-revalidate-secret',
|
|
]) {
|
|
expect(script, fragment).toContain(fragment)
|
|
}
|
|
})
|
|
|
|
it('never echoes secret values (no set -x, passwords only via stdin/secrets)', () => {
|
|
expect(script).not.toMatch(/^set -x\b/m)
|
|
expect(script).not.toContain('echo "$DATABASE_URL"')
|
|
expect(script).not.toContain('echo "$PAYLOAD_SECRET"')
|
|
// Registry password goes to docker via stdin — never stdout, never argv.
|
|
expect(script).toContain('echo "$REGISTRY_PASSWORD" | docker login')
|
|
expect(script).not.toContain('--password "$REGISTRY_PASSWORD"')
|
|
})
|
|
})
|