Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ jobs:
shellcheck --shell=bash --severity=error scripts/tests/pin-version-verify.sh
bash -n scripts/check-pin-version.sh
bash -n scripts/tests/pin-version-verify.sh
shellcheck --shell=bash --severity=warning scripts/publish-guard.sh
shellcheck --shell=bash --severity=warning scripts/publish-mirror.sh
shellcheck --shell=bash --severity=error scripts/tests/publish-guard-verify.sh
shellcheck --shell=bash --severity=error scripts/tests/publish-mirror-verify.sh
shellcheck --shell=bash --severity=error scripts/tests/mirror-publish-workflow-verify.sh
bash -n scripts/tests/mirror-publish-workflow-verify.sh
bash -n scripts/publish-guard.sh
bash -n scripts/publish-mirror.sh
# format.sh's own fail-closed properties. Formatters are stubbed, so this is
# hermetic and needs no Go toolchain — which is why it lives in this job
# rather than Lint. It exists because the first cut of format.sh reported
Expand Down Expand Up @@ -127,6 +135,23 @@ jobs:
# pin-version-drift.yml, which must never gate a PR (backend#2704 / #1009).
- name: Pin-version watcher harness (drift reddens / fail-closed)
run: bash scripts/tests/pin-version-verify.sh
# The mirror-publish guard (scripts/publish-guard.sh) and publisher
# (scripts/publish-mirror.sh): each guard reddens on the thing it claims
# to catch (a forbidden path, a forbidden string, a missing scanner), an
# empty or unreadable list is "could not tell", and the publisher refuses
# an unset or self-pointing mirror. gitleaks is a PATH shim here, so this
# is hermetic; the workflow installs the real, pinned binary.
- name: Mirror-publish guard harness (refusals named / fail-closed)
run: bash scripts/tests/publish-guard-verify.sh
- name: Mirror-publish publisher harness (target / tree / release)
run: bash scripts/tests/publish-mirror-verify.sh
# The decisions mirror-publish.yml takes in its own step bodies — a
# prerelease keeps the mirror's default branch, the release tag is fetched
# as data only at the expected commit, no checkout takes an untrusted
# ref, a publisher refusal reaches the step log. The step bodies are read
# out of the YAML and executed with `gh` shimmed, so this is hermetic.
- name: Mirror-publish workflow harness (step bodies / shape / mutations)
run: bash scripts/tests/mirror-publish-workflow-verify.sh

test:
timeout-minutes: 15
Expand Down
452 changes: 452 additions & 0 deletions .github/workflows/mirror-publish.yml

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions .publish-forbidden
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# .publish-forbidden — what must never reach the public mirror, even if the
# allowlist (.publish-include) let it through by mistake.
#
# Read by scripts/publish-guard.sh. Four sections; `#` starts a comment. A
# header the guard does not know, a needle listed in both string tiers, or an
# empty [strings-refuse] is refused as "could not tell" (exit 2).
#
# [paths] gitignore-style names. A pattern containing `/` is
# anchored to the staged root; one without matches ANY path
# component; a trailing `/` means "as a directory".
# [strings-refuse] extended regexes, matched case-insensitively against
# every staged TEXT file. A hit REFUSES the publish and
# names the file and line (never the matched text).
# [strings-report] same syntax. Hits are COUNTED and printed (per-needle
# totals, ten most-hit files) but do not refuse — unless
# the guard runs with --strict, which promotes this tier
# to refusal. A needle moves up to [strings-refuse] the
# day it is decided the mirror must never carry it.
# [allow] exact tokens removed from a line before a needle is
# re-tested, so a line is spared only when the allowed
# token was the whole reason it hit.
#
# This file is ONE list read by both the guard and its tests; the tests write
# their own inputs and never iterate this file to check itself.

[paths]
tests/
scripts/tests/
ci/
.github/
docs/rfcs/
docs/migration-tools/
CLAUDE.md
STYLE.md
Makefile
.cursor/
*.go
go.mod
go.sum
__pycache__
.DS_Store
.env*
*.pem
*.key
kubeconfig*

[strings-refuse]
# Mailboxes (the public support address is spared under [allow]).
[A-Za-z0-9._%+-]+@tracebloc\.io
# AWS account identifiers and ARNs.
arn:aws:
[0-9]{12}\.dkr\.ecr\.
#
# CUSTOMER AND TENANT IDENTIFIERS ARE DELIBERATELY NOT LISTED HERE. This file
# is public, and a list of customer names would itself be the disclosure the
# scan exists to prevent. Those needles are supplied privately at publish time:
# the workflow writes the PUBLISH_FORBIDDEN_TENANTS secret (one needle per line,
# same regex syntax) to a file and passes it as --extra-forbidden; they join
# this tier. The guard refuses to run the scan when that list is missing or
# empty.

[strings-report]
# Internal tracker and RFC identifiers — a reader of the mirror cannot open
# them. Counted until the decision to strip them from the deliverable (or to
# accept them) is taken; --strict refuses them.
backend#
rfcs#
RFC-0
RFC-BACKEND
e2e-test-agent#
tracebloc/backend
# Non-production tracebloc hosts; same decision pending.
dev-api\.tracebloc\.io
stg-api\.tracebloc\.io
dev\.tracebloc\.io
stg\.tracebloc\.io

[allow]
support@tracebloc\.io
16 changes: 16 additions & 0 deletions .publish-include
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# .publish-include — what the public mirror of this repo MAY carry.
#
# Read by scripts/publish-guard.sh. One glob per line; `#` starts a comment.
# `*` and `?` do not cross `/`, `**` does; a leading `!` takes matching files
# back out. Only tracked files are considered. Anything not matched here is
# excluded by construction — .publish-forbidden is the second lock.
#
# The mirror is README + releases. The binaries, SHA256SUMS, signatures and
# the two installers travel as RELEASE ASSETS (copied from this repo's release
# by the publish workflow and scanned by the same guard), never as tree files.
# No Go source, no Makefile, no workflows: the forbidden list refuses each of
# those by name should a line here ever widen.
README.md
LICENSE
# The user docs README links to. One level only: docs/rfcs/ stays home.
docs/*.md
20 changes: 20 additions & 0 deletions scripts/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ have to reverse-engineer the surface area on release day.
and all artifacts attached. `prerelease=true` if the tag
contains a `-` (e.g. `v0.1.0-rc1`).

7. `.github/workflows/mirror-publish.yml` fires when the Release
workflow completes. It stages the public deliverable (README,
LICENSE, `docs/*.md` per `.publish-include`; the release assets)
through `scripts/publish-guard.sh` — allowlist, forbidden paths,
forbidden strings, gitleaks, all fail-closed — and pushes it, plus
a copy of the release, to the public mirror named by the
`MIRROR_REPO` variable. Until that variable is set the job refuses
to publish; `Actions → Mirror publish → Run workflow` with
`dry-run: true` shows what would ship. The string scan has two
tiers: `[strings-refuse]` hits refuse; `[strings-report]` hits
(internal ticket references, non-production hostnames) are counted
and printed with the most-hit files, and refuse only under the
`strict` input or the `PUBLISH_STRICT=true` repository variable.
The guard and publisher run from the workflow's own commit; the
release tag is fetched separately as data and refused unless it
resolves to the commit the Release run ran on. A prerelease
(`-rc.N`) mirrors only its GitHub release, marked prerelease and
pinned to the mirror's current default-branch head — the mirror's
default branch keeps the last stable release.

GitHub Releases plus the cosign-verified `install.sh` are the
install path — a Homebrew tap and the `install.tracebloc.io`
vanity URL were considered and dropped
Expand Down
Loading
Loading