- 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
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
|
|
}
|