mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
- ci.yml: on push removido → schedule */30 + workflow_dispatch (fundido com a estrutura de job único sequencial do OPS62 X1) - job gate (host, main-only, sem pnpm): revision do container rodando no homeserver (label org.opencontainers.image.revision — o compose pode mentir após rollback falho) + diff + classificador .dockerignore - fail-open: revision desconhecida → suite roda (verde); incapaz de classificar → run vermelho (nunca blackout silencioso); deploy duplo-gated (needs: [checks, gate] + if explícito) - deploy-homeserver.sh: guard 'already deployed' idempotente (mesma revision) - agent-pool.yml removido; pool dormente (144 pnpm install/dia no tick */10) - specs: dockerignoreMatch, ciClassifyProduction (incl. git-diff com repo scratch e exit codes), pins ciSkipInvariants/deployScript; HIGH_RISK_EXACT - docs: runbook (janela/gate/verificação ao vivo/falhas), AGENTS.md, AGENT-OPS.md (tabela X1 fundida), skills pool dormente, changelog OPS65 - impl plan aprovado; intenção marcada entregue
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
compileDockerignoreRule,
|
|
isDockerignored,
|
|
parseDockerignore,
|
|
} from '../../scripts/lib/dockerignore.mjs'
|
|
|
|
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..')
|
|
const dockerignore = readFileSync(join(repoRoot, '.dockerignore'), 'utf8')
|
|
|
|
describe('dockerignoreMatch (OPS65 production-change gate)', () => {
|
|
it('compiles the moby semantics: basename at any depth, subtree exclusion', () => {
|
|
const rules = parseDockerignore('docs\n*.log\n')
|
|
expect(isDockerignored('docs', rules)).toBe(true)
|
|
expect(isDockerignored('docs/plans/x.md', rules)).toBe(true)
|
|
expect(isDockerignored('a/docs/plans/x.md', rules)).toBe(true)
|
|
expect(isDockerignored('x.log', rules)).toBe(true)
|
|
expect(isDockerignored('a/b/x.log', rules)).toBe(true)
|
|
expect(isDockerignored('src/app/page.tsx', rules)).toBe(false)
|
|
})
|
|
|
|
it('anchors leading-slash patterns to the context root', () => {
|
|
const rules = parseDockerignore('/*.png\n')
|
|
expect(isDockerignored('a.png', rules)).toBe(true)
|
|
expect(isDockerignored('sub/b.png', rules)).toBe(false)
|
|
})
|
|
|
|
it('honours ! negations with last-match-wins', () => {
|
|
const rules = parseDockerignore('.env.*\n!.env.example\n')
|
|
expect(isDockerignored('.env.local', rules)).toBe(true)
|
|
expect(isDockerignored('.env.example', rules)).toBe(false)
|
|
expect(isDockerignored('a/.env.example', rules)).toBe(false)
|
|
})
|
|
|
|
it('supports globstar and directory-only patterns', () => {
|
|
const rules = parseDockerignore('**/fixtures\na/**/b\n')
|
|
expect(isDockerignored('x/fixtures/y.json', rules)).toBe(true)
|
|
expect(isDockerignored('a/b', rules)).toBe(true)
|
|
expect(isDockerignored('a/x/b', rules)).toBe(true)
|
|
expect(isDockerignored('a/x/c/b', rules)).toBe(true)
|
|
})
|
|
|
|
it('ignores blank lines and comments', () => {
|
|
const rules = parseDockerignore('# header\n\ndocs\n')
|
|
expect(rules).toHaveLength(1)
|
|
expect(isDockerignored('docs/x', rules)).toBe(true)
|
|
})
|
|
|
|
it('escapes regex metacharacters in plain patterns', () => {
|
|
const rules = parseDockerignore('foo.bar\n')
|
|
expect(isDockerignored('foo.bar', rules)).toBe(true)
|
|
expect(isDockerignored('fooXbar', rules)).toBe(false)
|
|
})
|
|
|
|
it('treats the repo .dockerignore as the source of truth', () => {
|
|
const rules = parseDockerignore(dockerignore)
|
|
// Anything the image needs is production: changes force the full suite.
|
|
for (const path of [
|
|
'src/app/page.tsx',
|
|
'src/migrations/20260818_x.ts',
|
|
'public/favicon.ico',
|
|
'Dockerfile',
|
|
'.dockerignore',
|
|
'AGENTS.md',
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
'scripts/deploy-homeserver.sh',
|
|
'scripts/ci-classify-production.mjs',
|
|
'.forgejo/workflows/ci.yml',
|
|
'.forgejo/workflows/ci-pr.yml',
|
|
'tests/unit/dockerignoreMatch.unit.spec.ts',
|
|
'.env.example',
|
|
]) {
|
|
expect(isDockerignored(path, rules), path).toBe(false)
|
|
}
|
|
// Non-artifact paths skip the suite (quiet days cost seconds).
|
|
for (const path of [
|
|
'docs/plans/x.md',
|
|
'docs/ops/teqo-1313-deploy.md',
|
|
'.agents/skills/foo/SKILL.md',
|
|
'.env.local',
|
|
'.env.test.local',
|
|
'data/tse/x.zip',
|
|
'media/x.jpg',
|
|
'foo.png',
|
|
'x.log',
|
|
'x.tsbuildinfo',
|
|
'node_modules/x',
|
|
'.next/x',
|
|
'.next-e2e/x',
|
|
]) {
|
|
expect(isDockerignored(path, rules), path).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('keeps the .dockerignore itself explicit (self-referential gate)', () => {
|
|
const rule = compileDockerignoreRule('!.dockerignore')
|
|
expect(rule.negate).toBe(true)
|
|
})
|
|
})
|