Skip to content

Release Content Gate

A BoB release is a tag of this repository, and every client installs from it. Whatever is tracked at the release commit ships to every tenant. The release content gate refuses a release whose tree carries operator data, a client identifier or a credential (#2290, part of #2215).

Entry point What it does
bob release (stable, nightly, promote) Runs the gate after every other gate and before any bump, tag or push. A hit refuses with exit 3: one line with the hit count and the command that lists the hits. No tag is created. --dry-run runs the gate too.
make check-release-content [REF=<sha>] The same check without releasing. It is not part of make check, because it needs an operator profile, and CI has none.
node scripts/checks/release-content.js [--ref <sha>] [--root DIR] [--allow FILE] [--json] The engine. --ref defaults to HEAD. --allow grades against an uncommitted allow-list.

BOB_RELEASE_CONTENT=<script> replaces the engine inside release.sh, the same seam as BOB_RELEASE_DOCS.

Rule What it refuses Input
path A tracked path that must not exist in a release tree scripts/checks/release-content.json paths[], plus the session_state and machine_local paths of scripts/lib/derived-paths.json
tenant A path or a line carrying a client identifier: repo names, org names, domains and product names The operator profile’s tenants[] only (operator.json, schemas/operator.schema.json)
credential A line matching a credential pattern that is not a placeholder registry/skills/secrets-management/references/secret-patterns.json, the catalogue the commit secret scan reads too (#3662)

Path patterns are anchored at the repo root. * matches within one path segment, a leading **/ matches at any depth, and a trailing / matches the directory and everything under it. except lists exact paths a pattern does not cover. So apps/intake/config/ matches the operator’s intake config but not apps/intake/config.example/. config/cloudflare-accounts.json does not match the operator fixtures under tests/fixtures/operator/sample/. Operator data (#2270), projects.json, and any secrets/ or memory/ directory are covered. The rules file holds paths only, never a client name.

Tenant literals are matched case-insensitively as substrings of every tracked path (a file named after the client ships its name; the hit has no line) and of every tracked text line, including docs/, *.md, journal/, requirements/ and CHANGELOG.md. Binary files’ contents are skipped. The product repo never names a client in order to keep that client out: the names live only in the operator’s profile.

"tenants": [
{ "id": "client-a", "label": "Globex Corporation", "literals": ["globex", "globex-corp.example"] }
]

A tenant id is opaque. The allow-list in the product repo names tenants by id, so an id that overlaps one of its own literals ("id": "globex") is a config error.

Credential hits never reproduce the secret. A hit names the file, the line and the pattern. The match is masked to its first four characters (ghp_****), and the line itself is never kept. A line matching the catalogue’s placeholder regex (REPLACE_ME, EXAMPLE, ${VAR}, …) is an example, not a hit.

release-content — /home/me/projects/bigbrain at HEAD (44203325)
operator: example-co (via default) tenants: client-a
graded: 2145 tracked file(s) · 39 path rule(s) · 10 credential pattern(s)
allow-listed: path 1 · tenant 289 · credential 24 (scripts/checks/release-content-allow.json)
PASS no operator data, client identifiers or credentials in the release tree

--json emits release-content/1: root, ref, sha, verdict (clean / hits), operator, tenants (ids), rules, hits[] (one per file, line, rule and tenant id; pattern and the masked match for a credential), allowed (count per rule) and unused_allow[]. On exit 2 or 3 the document carries verdict config-error / no-operator and error.

Exit Meaning
0 Clean: every hit, if any, is allow-listed
1 Hits: a release of this tree would ship them
2 Invalid config: the rules, the catalogue, the profile’s tenants[] or the allow-list
3 No operator resolved: the gate cannot know which clients to keep out, so it fails closed

scripts/checks/release-content-allow.json is read from the graded tree at <ref>, so a release carries its own baseline. Entries are {path, rule, tenant, why, retires}:

  • path is a file or a directory prefix (trailing /).
  • rule is path, tenant or credential.
  • tenant is required for rule tenant. It is an id the operator profile declares, never a literal. An undeclared id is a config error.
  • why and retires are required. retires names the requirement that removes the entry.
  • The allow-list is graded like any other file, so it must not name a client either. A file whose name carries the client is allow-listed by its directory, not by its own path.

The allow-list is loaded by abs-path-lint.js’s loader, not by a second parser. Every run prints the allow-listed count per rule and lists the entries that match nothing, so the burn-down stays visible.

The baseline at #2290 allow-lists four groups, each against a follow-on requirement:

Group Retired by
The eloqua-dev skill’s as-built client material #3430 — retired: the client notes live in the operator repo’s skills/eloqua-dev/
Tenant literals in docs, briefings, tests, scripts, journal and CHANGELOG #3431
projects.json, the derived registry (path and tenant) #3432 — retired: the registry is operator data in the operator repo
Credential-shaped samples in tests and doc examples #3433
  • Credential Inventory covers credentials by name. This gate covers the tree.
  • Versioning explains the release channels and the other gates.
  • Operator describes the profile that declares tenants[].
  • check-abs-paths (#2268) keeps the operator’s own identity literals out of synced files during development. This gate grades the whole tree at the release commit, docs included, for every declared tenant.