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
106 lines
3.5 KiB
JavaScript
106 lines
3.5 KiB
JavaScript
/**
|
|
* dockerignore match — pure `.dockerignore` semantics (moby patternmatcher
|
|
* subset) for the CI production-change gate (OPS65). No git, no fs, no deps.
|
|
*
|
|
* The gate classifies "production change" by asking: would this path enter the
|
|
* docker build context? The `.dockerignore` is the source of truth for the
|
|
* artifact (the Dockerfile does `COPY . .`), so a path is production when it
|
|
* is NOT excluded by the ignore patterns.
|
|
*
|
|
* Implemented rules (moby semantics):
|
|
* - blank lines and `#` comments are ignored
|
|
* - `!` prefixes a negation (re-include); last match wins
|
|
* - trailing `/` is a directory pattern (the dir itself and its tree)
|
|
* - leading `/` anchors the pattern to the context root
|
|
* - a pattern with no other `/` matches the basename at ANY depth
|
|
* - any matched path also excludes everything under it (the builder prunes
|
|
* at the matched directory, so `docs` excludes `docs/x.md`)
|
|
* - `*` matches any run of non-separator chars, `?` a single one
|
|
* - `**` matches any number of directories (globstar)
|
|
*/
|
|
|
|
/** A compiled ignore rule: regex against the cleaned repo-relative path. */
|
|
export class DockerignoreRule {
|
|
/**
|
|
* @param {RegExp} regex - compiled matcher
|
|
* @param {boolean} negate - `!` prefix: re-include when matched
|
|
* @param {string} source - original pattern text (diagnostics)
|
|
*/
|
|
constructor(regex, negate, source) {
|
|
this.regex = regex
|
|
this.negate = negate
|
|
this.source = source
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} pattern - one `.dockerignore` line (no trailing newline)
|
|
* @returns {DockerignoreRule}
|
|
*/
|
|
export function compileDockerignoreRule(pattern) {
|
|
const source = pattern
|
|
let negate = false
|
|
if (pattern.startsWith('!')) {
|
|
negate = true
|
|
pattern = pattern.slice(1)
|
|
}
|
|
if (pattern.endsWith('/')) pattern = pattern.slice(0, -1)
|
|
const anchored = pattern.startsWith('/')
|
|
if (anchored) pattern = pattern.slice(1)
|
|
|
|
let body = ''
|
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
const char = pattern[index]
|
|
if (char === '*') {
|
|
if (pattern[index + 1] === '*') {
|
|
index += 1
|
|
if (pattern[index + 1] === '/') {
|
|
body += '(?:.*/)?'
|
|
index += 1
|
|
} else {
|
|
body += '.*'
|
|
}
|
|
} else {
|
|
body += '[^/]*'
|
|
}
|
|
} else if (char === '?') {
|
|
body += '[^/]'
|
|
} else if ('\\^$.|+()[]{}'.includes(char)) {
|
|
body += `\\${char}`
|
|
} else {
|
|
body += char
|
|
}
|
|
}
|
|
|
|
const anyDepth = !anchored && !pattern.includes('/') ? '(?:.*/)?' : ''
|
|
const subtree = '(?:/.*)?'
|
|
const regexSource = body.length > 0 ? `^${anyDepth}${body}${subtree}$` : '$^'
|
|
return new DockerignoreRule(new RegExp(regexSource), negate, source)
|
|
}
|
|
|
|
/**
|
|
* @param {string} text - full `.dockerignore` content
|
|
* @returns {DockerignoreRule[]} rules in order (last match wins)
|
|
*/
|
|
export function parseDockerignore(text) {
|
|
return text
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
.map(compileDockerignoreRule)
|
|
}
|
|
|
|
/**
|
|
* Last-match-wins exclusion test for one repo-relative path.
|
|
*
|
|
* @param {string} path - repo-relative, forward slashes (e.g. "src/app/x.ts")
|
|
* @param {DockerignoreRule[]} rules - from parseDockerignore
|
|
* @returns {boolean} true when the path is excluded from the build context
|
|
*/
|
|
export function isDockerignored(path, rules) {
|
|
let ignored = false
|
|
for (const rule of rules) {
|
|
if (rule.regex.test(path)) ignored = !rule.negate
|
|
}
|
|
return ignored
|
|
}
|