teqo/scripts/lib/dockerignore.mjs
Francisco Solla 9d7e4a1414 ops(OPS65): CI do main em janela de 30 min (gate .dockerignore fail-open) + matar agent-pool
- 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
2026-08-18 20:06:22 -03:00

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
}