fix(yield): harden clean-slate developer experience #194
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Boundary identifier scan | |
| # Non-bypassable gate for PRIVATE IDENTIFIERS in the public repo. | |
| # | |
| # A generic secret scanner (see secret-scan.yml) catches credential *material* by shape, but it | |
| # cannot catch bare infrastructure *identifiers* (project names, pool paths, a private repo name): | |
| # those have no generic shape and must not be named in any public file. So the specific terms live | |
| # only in a private denylist, and this workflow reads that denylist AT RUNTIME — the workflow itself | |
| # names nothing private. It scans the added lines of each PR / push for a denied term and fails | |
| # closed, reporting only the offending file and a generic category (never the matched text, which | |
| # would reprint the very identifier into a public log). | |
| # | |
| # Access is via the org's existing GitHub App (same one the publish workflows use). At runtime the | |
| # job mints a token DOWNSCOPED to contents:read on the single source repo — even though the App can | |
| # do more — so the gate has the least privilege it needs and depends on no personal PAT. | |
| # | |
| # Setup (one-time, by a maintainer): configure secrets/vars so the gate can reach the denylist: | |
| # BOUNDARY_DENYLIST_REPO <org>/<private-repo> holding the denylist (secret; keeps the name out of source) | |
| # BOUNDARY_DENYLIST_PATH path to the denylist within it, e.g. boundary/deny-content.txt (secret) | |
| # plus the App credentials already present for the publishers (client-id var + private-key secret). | |
| # Until they are set the job fails closed (an unconfigured gate is treated as an unenforced one). | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: boundary-identifier-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| identifier-scan: | |
| name: Private identifier scan | |
| runs-on: ubuntu-latest | |
| env: | |
| DENYLIST_REPO: ${{ secrets.BOUNDARY_DENYLIST_REPO }} | |
| DENYLIST_PATH: ${{ secrets.BOUNDARY_DENYLIST_PATH }} | |
| steps: | |
| - name: Preflight — resolve source coordinates (fail-closed) | |
| id: coords | |
| run: | | |
| set -euo pipefail | |
| [ -n "${DENYLIST_REPO:-}" ] || { | |
| echo "::error::BOUNDARY_DENYLIST_REPO is not set — failing closed (an unenforced gate is not a gate)." | |
| exit 1 | |
| } | |
| # Split <owner>/<repo> so the App token can be scoped to exactly this repo. Derived from the | |
| # secret at runtime, so the private repo NAME never appears in this workflow's source. | |
| echo "owner=${DENYLIST_REPO%%/*}" >> "$GITHUB_OUTPUT" | |
| echo "repo=${DENYLIST_REPO##*/}" >> "$GITHUB_OUTPUT" | |
| echo "coordinates resolved." | |
| - name: Mint a read-only token scoped to the source repo | |
| id: denylist-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.OPERATOR_STACK_PUBLISHER_APP_CLIENT_ID || vars.BOATSTACK_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.OPERATOR_STACK_PUBLISHER_APP_PRIVATE_KEY || secrets.BOATSTACK_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.coords.outputs.owner }} | |
| repositories: ${{ steps.coords.outputs.repo }} | |
| permission-contents: read | |
| - name: Checkout public repo (full history for range diff) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Fetch private denylist at runtime | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ env.DENYLIST_REPO }} | |
| token: ${{ steps.denylist-token.outputs.token }} | |
| path: .boundary-private | |
| persist-credentials: false | |
| - name: Scan introduced lines for denied identifiers (redacted, fail-closed) | |
| run: | | |
| set -euo pipefail | |
| DENY="${DENYLIST_PATH:-boundary/deny-content.txt}" | |
| SRC=".boundary-private/${DENY}" | |
| if [ ! -f "$SRC" ]; then | |
| echo "::error::denylist not found at ${DENY} in the private repo — failing closed." | |
| exit 1 | |
| fi | |
| # Strip comments/blank lines so grep -f never sees an empty pattern (matches everything). | |
| PATS="$(mktemp)" | |
| grep -vE '^[[:space:]]*(#|$)' "$SRC" > "$PATS" || true | |
| if [ ! -s "$PATS" ]; then | |
| echo "::error::denylist is empty after stripping comments — failing closed." | |
| exit 1 | |
| fi | |
| ZERO="0000000000000000000000000000000000000000" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" | |
| elif [ "${{ github.event.before }}" != "$ZERO" ] && [ -n "${{ github.event.before }}" ]; then | |
| RANGE="${{ github.event.before }}..${{ github.sha }}" | |
| else | |
| RANGE="" | |
| fi | |
| echo "scan range: ${RANGE:-<full working tree>}" | |
| # Files changed in the range (or all tracked files when there is no range). | |
| if [ -n "$RANGE" ]; then | |
| mapfile -t FILES < <(git diff --name-only --diff-filter=ACMR "$RANGE" -- . ':!.boundary-private/**') | |
| else | |
| mapfile -t FILES < <(git ls-files -- . ':!.boundary-private/**') | |
| fi | |
| fail=0 | |
| for f in "${FILES[@]:-}"; do | |
| [ -n "$f" ] || continue | |
| [ -f "$f" ] || continue | |
| if [ -n "$RANGE" ]; then | |
| added="$(git diff --unified=0 --diff-filter=ACMR "$RANGE" -- "$f" | grep -E '^\+' | grep -vE '^\+\+\+' || true)" | |
| else | |
| added="$(cat "$f")" | |
| fi | |
| # Redacted: we only report THAT a file matched and a generic category, never the match. | |
| if printf '%s' "$added" | grep -qE -f "$PATS"; then | |
| echo "::error file=$f::private identifier detected (redacted). Move the value into the private repo and reference cloud coordinates via runtime resolution / CI variables." | |
| fail=1 | |
| fi | |
| done | |
| rm -f "$PATS" | |
| if [ "$fail" -ne 0 ]; then | |
| echo "::error::boundary identifier scan failed — one or more public files contain a private identifier (see annotations above)." | |
| exit 1 | |
| fi | |
| echo "no denied identifiers in the introduced changes." |