diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index f45ea01c7..d5bea6b4a 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -55,6 +55,47 @@ jobs: setup-java-Linux-x64-maven- - name: Download all dependencies run: ./mvnw --batch-mode -Pci dependency:go-offline -DincludeScope=test -U + # First of two plugin-resolves-for-itself misses, same class, different plugin. go-offline does + # not fetch the platform protoc / protoc-gen-grpc-java EXECUTABLES that protobuf-maven-plugin + # resolves at generate time, so left cold every downstream job fetches them from Central and + # inherits the Azure West US CDN route lottery in + # docs/solutions/build-errors/maven-central-timeout-azure-west-regions-2026-04-21.md - a 240s + # hang, then a hard NullPointerException out of Aether rather than a retry. Measured on + # astubbs/parallel-consumer#293: in one run "Unit Tests" fetched protoc in 272ms while + # "Integration Tests" hung the full 240s on the identical URL, and it failed 2 of 6 consecutive + # runs. The classifier is linux-x86_64 because this job declares ubuntu-latest. (astubbs#242) + - name: Warm plugin-resolved protoc binaries + run: | + set -euo pipefail + evaluate() { + ./mvnw -q --batch-mode -pl parallel-consumer-proxy-protocol \ + -Dexpression="$1" -DforceStdout \ + org.apache.maven.plugins:maven-help-plugin:3.5.2:evaluate 2>/dev/null | tail -1 + } + # A renamed property makes help:evaluate print "null object or invalid expression", which + # dependency:get would happily turn into a warm of the wrong coordinate - warming nothing is + # indistinguishable from a cache hit until it fails four jobs downstream, so require a + # version-shaped answer rather than merely a non-empty one. + require_version() { + case "$2" in + [0-9]*) : ;; + *) echo "::error::$1 did not resolve to a version from parallel-consumer-proxy-protocol/pom.xml (got '$2') - the protoc cache warm would be a silent no-op"; exit 1 ;; + esac + } + protoc_version=$(evaluate proxy.protoc) + grpc_version=$(evaluate grpc.version) + require_version proxy.protoc "$protoc_version" + require_version grpc.version "$grpc_version" + get() { + ./mvnw --batch-mode -ntp \ + org.apache.maven.plugins:maven-dependency-plugin:3.11.0:get -Dartifact="$1" + } + get "com.google.protobuf:protoc:${protoc_version}:exe:linux-x86_64" + get "io.grpc:protoc-gen-grpc-java:${grpc_version}:exe:linux-x86_64" + # Assert the binaries are on disk: the exit code says the goal ran, only the file says the + # cache this job exists to populate actually got populated. + test -s ~/.m2/repository/com/google/protobuf/protoc/"${protoc_version}"/protoc-"${protoc_version}"-linux-x86_64.exe + test -s ~/.m2/repository/io/grpc/protoc-gen-grpc-java/"${grpc_version}"/protoc-gen-grpc-java-"${grpc_version}"-linux-x86_64.exe # go-offline resolves declared dependencies and plugin JARs, but NOT what a plugin resolves # FOR ITSELF at execution time - and scala-maven-plugin fetches the Scala compiler and the # zinc compiler-bridge sources jar that way, so neither is declared anywhere and both stay @@ -270,6 +311,44 @@ jobs: - name: Validate release-documentation data structure run: bin/check-docs-data.sh + # Protocol freeze gate: the proxy wire schema may only GAIN, never change (the language-proxy + # plan's freeze, astubbs#242). `buf breaking` in the FILE category - configured in + # parallel-consumer-proxy-protocol/buf.yaml, FILE because WIRE would tolerate source-breaking + # renames the compatibility promise forbids - compares the PR's schema against the frozen one on + # origin/master. bin/check-proto-breaking.sh passes with a notice while master has no frozen + # schema yet, so the job arms itself the moment the freeze lands. + # + # It also carries the schema's STATIC ANALYSIS - `buf lint`, via bin/check-proto-lint.sh - because + # the two share a toolchain install and a checkout, and splitting them would buy a second job for + # one binary. The lint runs FIRST: a schema that does not lint is a schema whose breaking-change + # comparison is answering a question nobody asked yet. The protocol earns its own analysis because + # it is the artifact all eleven clients will be generated from - one defect there arrives in every + # language at once. + proto-breaking: + if: github.event_name == 'pull_request' + name: "proto: breaking" + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # the baseline is origin/master, which a shallow clone lacks + - name: Install buf + uses: bufbuild/buf-action@v1 + with: + setup_only: true + - name: Lint the schema (buf lint) + run: bin/check-proto-lint.sh + # Self-test first, the same order as the docs-data and copyright scanners: this gate's + # ability to say no was demonstrated once, by hand, and never since - so the cases that pin + # it (a deletion, a renumber, a rename, and a renamed module that must not re-enter the + # "nothing frozen yet" branch) run here, where a regression is visible. It mutates the schema + # one way at a time and restores it, so the gate below still sees an untouched tree. + - name: Self-test the gate (prove it can still say no) + run: bash bin/test-check-proto-breaking.sh + - name: Enforce the frozen schema (additions only) + run: bin/check-proto-breaking.sh + # Experimental Kafka 4.x compatibility check. Non-blocking. # DISABLED pending the Kafka 4.x migration work: the experimental 4.x build currently fails and just adds # a red X of noise to every PR (it is continue-on-error, so it never gated merges). Re-enable by restoring diff --git a/.github/workflows/repo-hygiene.yml b/.github/workflows/repo-hygiene.yml index f18fa300a..f205c65c3 100644 --- a/.github/workflows/repo-hygiene.yml +++ b/.github/workflows/repo-hygiene.yml @@ -148,6 +148,21 @@ jobs: - name: Confirm shellcheck runs, so a shell-lint exit 2 means a real defect run: shellcheck --version + # Same environment gap, third instance, and the one this lane most needs to close: + # bin/test-check-proto-breaking.sh exits 2 "cannot run" without buf, and the gate it protects + # is the only one in bin/ that reads its inputs into ARRAYS - which is where the bash-3.2 + # divergence this whole job exists for actually lives. It shipped with a `mapfile` (bash 4; + # exit 127 on 3.2, a gate reporting neither verdict) and every ubuntu runner passed it. The + # setup action rather than `brew install buf`: it is already pinned in maven.yml's + # `proto: breaking` job, so the two lanes cannot end up on different buf versions. + - name: Install the self-tests' buf dependency + uses: bufbuild/buf-action@v1 + with: + setup_only: true + + - name: Confirm buf runs, so a proto-breaking exit 2 means a real defect + run: buf --version + - name: Pin bash to the system 3.2 run: | mkdir -p "$RUNNER_TEMP/bash32" diff --git a/AGENTS.md b/AGENTS.md index a55796206..a4cfb7776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -315,6 +315,7 @@ bin/performance-test.sh # performance tests (substantial hardware) | `parallel-consumer-vertx` | Vert.x integration for async HTTP | | `parallel-consumer-reactor` | Project Reactor integration | | `parallel-consumer-mutiny` | SmallRye Mutiny integration (Quarkus) | +| `parallel-consumer-proxy-protocol` | The **frozen** v1 wire contract for the language proxy: `proxy.proto`, its specification and client-authoring guide, and the tests and `buf` gates that keep it frozen. The wire may only gain - `bin/check-proto-breaking.sh` enforces it, and that module's `README.md` owns the detail | | `parallel-consumer-proxy-clients` | Eleven language client modules. **Build scaffolding only today** - toolchain, compile, run, one fixture line; no Kafka and no PC semantics. The eight non-JVM ones are outside the reactor unless `-Dpc.foreignClients` is passed, and an absent toolchain is reported and skipped rather than failing. That directory's `README.md` owns the detail | | `parallel-consumer-examples` | Example implementations for each module | diff --git a/bin/check-proto-breaking.sh b/bin/check-proto-breaking.sh new file mode 100755 index 000000000..0071b8bce --- /dev/null +++ b/bin/check-proto-breaking.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Antony Stubbs and contributors +# + +# Breaking-change gate for the frozen proxy protocol schema (the language-proxy plan's freeze unit, +# astubbs#242). The plan travels with the work it plans, on astubbs#293 - +# `git show origin/feats/proxy-requirements:docs/plans/2026-08-14-001-feat-language-proxy-plan.md`. +# What the freeze MEANS is owned by the banner at the top of the schema and by +# parallel-consumer-proxy-protocol/docs/protocol-specification.md, both of which are on master with +# this gate. +# +# After the freeze the wire may only GAIN, never change: `buf breaking` in the FILE category +# forbids deleting, renumbering, retyping or renaming anything in proxy.proto. FILE rather than +# WIRE, deliberately: WIRE tolerates source-breaking changes (a renamed field is wire-identical), +# and the compatibility promise here covers the generated APIs ten client languages sit on, not +# only the bytes. The category is configured in parallel-consumer-proxy-protocol/buf.yaml; this +# script only points buf at the right before/after pair. +# +# Compares the working tree's schema against the frozen one on origin/master. Before the freeze +# commit reaches master there is nothing frozen to compare against; the check says so and passes, +# arming itself the moment the frozen schema lands on master. +# +# BOTH SIDES ARE FOUND BY SEARCHING, NOT BY A HARDCODED PATH. The first version looked for the +# proto at one path on master and took its absence as "not frozen yet" - so a post-freeze PR that +# renamed the module, updated the constant and deleted a frozen field re-entered the grace branch +# and passed green. Searching the baseline for the file wherever it lives means a rename cannot +# disarm the gate: only the file being genuinely absent from the baseline can, which is the one +# case the grace branch is for. Anything else - two copies, or a layout the module root cannot be +# derived from - exits 2 rather than passing, because a gate that cannot locate what it guards has +# not run. (The repo's green-without-having-run class: +# docs/solutions/workflow-issues/a-check-that-reports-success-without-having-run.md.) +# +# Overrides, both for the self-test (bin/test-check-proto-breaking.sh): +# PROTO_BREAKING_AGAINST a complete buf input to compare against, replacing the discovery +# below. NOT a bare git ref: buf reads a bare "master" as a PATH and +# fails with 'had no .proto files'. The working forms are a directory +# holding a copy of the module, or the git form +# '.git#ref=,subdir=parallel-consumer-proxy-protocol'. +# PROTO_BREAKING_BASELINE_REF the ref to search for the frozen schema (default origin/master). +# +# Requires buf (https://buf.build). CI installs it via bufbuild/buf-action; locally it is a +# managed toolchain - this script never downloads anything. +# +# Usage: bin/check-proto-breaking.sh +# Exit codes: 0 = compatible (or nothing frozen to compare against), 1 = breaking change, +# 2 = cannot run (buf missing, no usable baseline ref, or the schema cannot be located). + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# The schema's path INSIDE its module - the layout, not the module's own name, so the search below +# survives the module being renamed but reports loudly if the layout itself moves. +PROTO_IN_MODULE=src/main/proto/parallelconsumer/proxy/v1/proxy.proto +BASELINE_REF=${PROTO_BREAKING_BASELINE_REF:-origin/master} + +# Module roots holding the schema, one per line. `sed -n s///p` rather than `grep -q`: the guard in +# bin/check-shell-sigpipe.sh, and a reader that exits early would take EPIPE under `pipefail`. +module_roots_in_worktree() { + git ls-files | sed -n "s|^\(.*\)/${PROTO_IN_MODULE}\$|\1|p" +} +module_roots_in_ref() { + git ls-tree -r --name-only "$1" | sed -n "s|^\(.*\)/${PROTO_IN_MODULE}\$|\1|p" +} + +if ! command -v buf >/dev/null 2>&1; then + echo "check-proto-breaking: buf is not installed, so the frozen schema cannot be verified." >&2 + echo "CI installs it via bufbuild/buf-action; locally it is an Ansible-managed toolchain." >&2 + exit 2 +fi + +# NO `mapfile`, and both array reads here used to be one. mapfile is a bash 4 builtin; macOS ships +# bash 3.2, where the line dies with "mapfile: command not found" and this gate exits 127 - a gate +# that has NOT run, reported as neither verdict, which is the exact class its own header argues +# about. ShellCheck passes it clean, so the class now has a bin/check-shell-hazards.sh row rather +# than only this comment. `while read` over a process substitution is portable to 3.2 and reads the +# same. +here=() +while IFS= read -r module_root; do here+=("$module_root"); done < <(module_roots_in_worktree) +if [[ ${#here[@]} -ne 1 ]]; then + echo "check-proto-breaking: expected exactly one tracked ${PROTO_IN_MODULE}, found ${#here[@]}" \ + "(${here[*]:-none}); cannot tell which module to check, so this gate has NOT run." >&2 + exit 2 +fi +MODULE=${here[0]} + +AGAINST=${PROTO_BREAKING_AGAINST:-} +if [[ -z "$AGAINST" ]]; then + if ! git rev-parse --verify --quiet "$BASELINE_REF" >/dev/null; then + echo "check-proto-breaking: $BASELINE_REF is not available (shallow clone?); cannot pick a baseline." >&2 + exit 2 + fi + frozen=() + while IFS= read -r module_root; do frozen+=("$module_root"); done < <(module_roots_in_ref "$BASELINE_REF") + if [[ ${#frozen[@]} -eq 0 ]]; then + echo "check-proto-breaking: $BASELINE_REF carries no ${PROTO_IN_MODULE} at any path -" \ + "nothing frozen to compare against; passing. The gate arms when the freeze lands there." + exit 0 + fi + if [[ ${#frozen[@]} -gt 1 ]]; then + echo "check-proto-breaking: $BASELINE_REF holds ${#frozen[@]} copies of the schema" \ + "(${frozen[*]}); the baseline is ambiguous, so this gate has NOT run." >&2 + exit 2 + fi + if [[ ${frozen[0]} != "$MODULE" ]]; then + echo "check-proto-breaking: the module moved since $BASELINE_REF (${frozen[0]} -> $MODULE);" \ + "comparing against it where it lives - a rename does not disarm the freeze." + fi + AGAINST=".git#ref=${BASELINE_REF},subdir=${frozen[0]}" +fi + +echo "check-proto-breaking: buf breaking $MODULE --against $AGAINST" +# The status is captured from buf ITSELF, not read out of `$?` after an `if`: a compound `if` whose +# condition fails and which has no `else` returns 0, so the obvious spelling reports every breaking +# schema as "buf exited 0" and lands in the cannot-run branch below. +status=0 +buf breaking "$MODULE" --against "$AGAINST" || status=$? + +if [[ $status -eq 0 ]]; then + echo "check-proto-breaking: OK - the schema only gained; nothing frozen was changed." + exit 0 +fi + +# buf's own codes, mapped onto this gate's contract: 100 is "breaking changes found", and anything +# else is buf refusing to answer the question at all (a baseline it cannot read is the live case - +# a bare ref in PROTO_BREAKING_AGAINST exits 1 with "had no .proto files"). Those are not a verdict +# of compatible OR breaking, and reporting one as the other is how a gate lies in both directions. +if [[ $status -eq 100 ]]; then + exit 1 +fi +echo "check-proto-breaking: buf could not complete the comparison (exit $status), so this gate has" \ + "NOT run - the schema is neither confirmed compatible nor found breaking." >&2 +exit 2 diff --git a/bin/check-proto-lint.sh b/bin/check-proto-lint.sh new file mode 100755 index 000000000..c8334dcb6 --- /dev/null +++ b/bin/check-proto-lint.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Antony Stubbs and contributors +# + +# Static analysis for the proxy protocol schema itself (astubbs#242). The per-language static-analysis +# policy for the generated clients is a separate document that arrives with the clients it governs, on +# astubbs#293 - `git show origin/feats/proxy-requirements:docs/client-static-analysis.md`. +# +# WHY THE .PROTO IS A STATIC-ANALYSIS TARGET IN ITS OWN RIGHT: it is the one artifact every client +# is generated FROM. A defect here is not one language's bug, it is eleven - and it arrives in each +# of them as generated code nobody reads, wearing whatever shape protoc chose. `buf lint` catches +# the classes no downstream analyser can: an enum whose zero value is not the UNSPECIFIED default +# (so "unset" and "the first real value" become indistinguishable on the wire), a package that +# disagrees with its directory (so generated import paths differ per language), RPC request and +# response types shared between methods, field-presence conventions. +# +# THE RULE SET AND ITS TWO EXCEPTIONS LIVE IN parallel-consumer-proxy-protocol/buf.yaml, with the +# justification written beside them - STANDARD, less RPC_REQUEST_STANDARD_NAME and +# RPC_RESPONSE_STANDARD_NAME, because the single bidirectional stream reuses the session envelopes +# rather than wrapping them. This script does not restate that list; it points buf at the module +# and lets the config be the single authority. Do not add exceptions here. +# +# The schema lints clean today, and that is exactly why this gate exists: the freeze wave achieved +# it and nothing held it. A clean state nobody checks is a clean state with a date on it. +# +# Severity policy: any lint finding fails. There is no warn tier, deliberately - a warn tier on the one +# artifact eleven languages are generated from is a finding nobody reads until it is in eleven languages. +# +# Requires buf (https://buf.build). CI installs it via bufbuild/buf-action; locally it is a managed +# toolchain (mise). This script never downloads anything: the module has no BSR dependencies, so +# the lint is entirely local. +# +# Usage: bin/check-proto-lint.sh +# Exit codes: 0 = clean, 1 = lint findings, 2 = cannot run (buf missing). + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +MODULE=parallel-consumer-proxy-protocol + +if ! command -v buf >/dev/null 2>&1; then + echo "check-proto-lint: buf is not installed, so the schema cannot be linted." >&2 + echo "CI installs it via bufbuild/buf-action; locally it is a managed toolchain (mise use -g buf)." >&2 + exit 2 +fi + +echo "check-proto-lint: buf lint $MODULE (rules and exceptions from $MODULE/buf.yaml)" +# Normalised to 1 rather than propagated: buf exits 100 on findings, and this script's caller reads +# 2 as "could not run". Letting 100 through would be harmless today and wrong the day someone tests +# for a specific code. Findings go to stderr/stdout uncaptured, so nothing is swallowed. +if ! buf lint "$MODULE"; then + echo "check-proto-lint: FAILED - fix the schema, or justify an exception in $MODULE/buf.yaml" \ + "beside the two that are already there. Do not add exceptions to this script." >&2 + exit 1 +fi +echo "check-proto-lint: OK - the schema every client is generated from lints clean." diff --git a/bin/check-shell-hazards.sh b/bin/check-shell-hazards.sh index 8c3c79327..97c6037da 100755 --- a/bin/check-shell-hazards.sh +++ b/bin/check-shell-hazards.sh @@ -105,6 +105,7 @@ gnu-bsd (^|[^[:alnum:]_-])([[:alnum:]_]*[_-])?sort[[:space:]]+(-[[:alnum:]]+[[:s gnu-bsd (^|[^[:alnum:]_-])([[:alnum:]_]*[_-])?base64[[:space:]]+(-[[:alnum:]]+[[:space:]]+)*-w[0-9]*([[:space:]]|$) `base64 -w` is GNU; BSD base64 does not wrap and rejects -w. gnu-bsd (^|[^[:alnum:]_-])([[:alnum:]_]*[_-])?sed[[:space:]]+(-[[:alnum:]]+[[:space:]]+)*-r([[:space:]]|$) `sed -r` is GNU; -E works on both and means the same thing. builtin-option-lookalike (^|[^[:alnum:]_-])printf[[:space:]]+[^-[:space:][:alnum:]]- A quoted `printf` format that STARTS with a hyphen is read as an OPTION by bash's builtin printf: it fails with "invalid option", returns 2, and under `set -e` that becomes the script's exit code. Write `printf -- '-...'`. Observed live: a skip banner appended to $GITHUB_STEP_SUMMARY turned every CI skip into a failed row while every developer box stayed green, because the variable is only set on a runner. +bash-4-builtin (^|[^[:alnum:]_-])([[:alnum:]_]*[_-])?(mapfile|readarray)([[:space:]]|$) `mapfile` and `readarray` are bash 4 BUILTINS; macOS ships bash 3.2, where the line dies with "mapfile: command not found" and the script exits 127 - a gate that never ran, reported as neither verdict. ShellCheck models the language, not which builtins the running bash has, and passes it. Use `arr=(); while IFS= read -r x; do arr+=("$x"); done < <(...)`. shared-git-state (^|[^[:alnum:]_-])([[:alnum:]_]*[_-])?git[[:space:]]+((-[^[:space:]]+)([[:space:]]+[^-[:space:]][^[:space:]]*)?[[:space:]]+)*(fetch|pull)([[:space:]]+[^[:space:]]+)*[[:space:]]+--(depth|shallow-since|shallow-exclude) `git fetch --depth` (and `git pull --depth`) writes the `shallow` file, which lives in the SHARED --git-common-dir - so it truncates history for EVERY worktree of the clone at once, and merge-base and ahead/behind then answer confidently wrong instead of erroring. Fetch into a throwaway git dir and read FETCH_HEAD there; mark it hazard-ok when the fetch target really is disposable. HZ ) diff --git a/bin/test-check-proto-breaking.sh b/bin/test-check-proto-breaking.sh new file mode 100755 index 000000000..1806893cb --- /dev/null +++ b/bin/test-check-proto-breaking.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Antony Stubbs and contributors +# + +# Self-test for bin/check-proto-breaking.sh - the freeze gate for the proxy wire schema. +# +# The freeze unit's own text required this ("verify the failure case so the gate is proven able to +# say no") and the demonstration was a one-off by hand, which proves the gate could say no ONCE, on +# one machine, to one person. Nothing has re-run it since, and a gate nobody can see fail is a gate +# nobody knows has stopped working - the repo's green-without-having-run class, +# docs/solutions/workflow-issues/a-check-that-reports-success-without-having-run.md. +# +# Mutates the real schema one way at a time, asserts the gate's verdict, and restores it: +# 1. baseline, schema untouched -> pass (0) +# 2. a field DELETED -> FAIL (1) +# 3. a field RENUMBERED -> FAIL (1) +# 4. a field RENAMED -> FAIL (1) +# 5. a field ADDED at a fresh number -> pass (0) +# 6. the module RENAMED in the baseline, and a field deleted -> FAIL (1) +# 7. a baseline that genuinely carries no schema -> pass (0), with the notice +# +# Case 4 pins the CATEGORY choice: a rename is wire-compatible, so `buf breaking` in WIRE would +# pass it. FILE is what the compatibility promise needs, because ten client languages sit on the +# generated APIs, not on the bytes - and nothing else would notice the config being weakened. +# +# Cases 6 and 7 are the two halves of the silent-disarm hole the gate shipped with. It armed only +# when the schema was at ONE hardcoded path on the baseline ref, so a post-freeze PR that renamed +# the module, updated the constant and deleted a frozen field re-entered the "nothing frozen yet" +# branch and passed green. Case 6 is that exact PR, and it must go red; case 7 is the branch it was +# abusing, which must still pass while the freeze is genuinely not on master yet - as it is not, +# today, on this branch. +# +# Baselines are built without touching the shared git index: the fixture refs are commit objects +# written with `commit-tree` against a temp GIT_INDEX_FILE, so a parallel agent's staged work is +# never disturbed and no branch is moved. They carry an identity supplied with `git -c`, because +# `commit-tree` refuses to write an object when git cannot resolve a committer and a GitHub-hosted +# runner has none configured - which is how cases 6 and 7 failed on every CI run while passing on +# every developer box. Neither fixture's verdict may be read unless its ref was actually built: an +# empty ref reaches the gate as PROTO_BREAKING_BASELINE_REF= and its `:-` default quietly +# substitutes origin/master, which is the grace branch case 7 asserts - so case 7 reported ok while +# testing nothing at all. +# +# Run: bin/test-check-proto-breaking.sh (CI runs it before the gate it protects) +# +# A developer box has a configured git user and a hosted runner does not, so the green that matters +# is the one WITHOUT one - and the runner's condition cannot be reproduced locally by unsetting +# config, which every worktree of this clone shares. Run this arm instead, which is stricter than +# the runner (an empty environment identity outranks every config level) and must also pass: +# GIT_AUTHOR_NAME='' GIT_COMMITTER_NAME='' bin/test-check-proto-breaking.sh + +set -uo pipefail + +cd "$(git rev-parse --show-toplevel)" + +GATE=bin/check-proto-breaking.sh +MODULE=parallel-consumer-proxy-protocol +PROTO=$MODULE/src/main/proto/parallelconsumer/proxy/v1/proxy.proto +failures=0 + +if ! command -v buf >/dev/null 2>&1; then + echo "test-check-proto-breaking: buf is not installed; the gate cannot run, so neither can its self-test." >&2 + echo "CI installs it via bufbuild/buf-action; locally it is a managed toolchain." >&2 + exit 2 +fi + +WORK=$(mktemp -d) +PRISTINE=$WORK/pristine + +# Restore by copying bytes back, for the reason bin/test-check-docs-data.sh sets out: a variable +# round trip through command substitution strips trailing newlines and silently rewrites the file. +cleanup() { + if [ -f "$WORK/proto.orig" ]; then cp "$WORK/proto.orig" "$PROTO"; fi + rm -rf "$WORK" +} +trap cleanup EXIT + +cp "$PROTO" "$WORK/proto.orig" +mkdir -p "$PRISTINE" +cp -r "$MODULE" "$PRISTINE/$MODULE" + +restore() { cp "$WORK/proto.orig" "$PROTO"; } + +# expect