From 61efb1f57555cb6afc42e0440209fa83ec94f4d1 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 5 Nov 2026 13:04:51 -0500 Subject: [PATCH 1/7] docs: Document the contributor workflow (repo topology + engine vendoring) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Development section to the README — the three-repo source-of-truth map (automation = engine dev, repoman = distribution and canonical home for RepoMan-specific SKILL.mds, agent-skills = standalone skills that also work with RepoMan), a brief five-step contributor workflow (placement, testing in automation, dry runs against scratch state, vendoring, PR rules), and the vendor-engine.sh invocation, folding in the former "How it is built" paragraph to avoid duplication. Adds a matching repo-topology section to CLAUDE.md phrased for LLM contributors, with the rule that skills/repoman/scripts/ is generated output and must never be edited in place. Fixes #17 Assisted-By: Claude (Anthropic AI) Signed-off-by: Jim Cadden --- CLAUDE.md | 25 ++++++++++++++++++++++++ README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b441f7..dc07413 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,31 @@ This applies to: - Commit message trailers referenced in PR descriptions - Any auto-generated changelogs or release notes +## Repo Topology & Vendored Code (Read Before Editing) + +This repo **distributes** RepoMan; it is not where the engine is developed. + +- Engine scripts are developed and tested in + [rossoctl/automation](https://github.com/rossoctl/automation) `scripts/`. + Engine changes are made there, then vendored here. +- `skills/repoman/scripts/*.sh` are **vendored copies** — generated output, + provenance-stamped in `skills/repoman/scripts/_vendor.json`. **Never edit + them in place.** To update them, run + `scripts/vendor-engine.sh --source --dest skills/repoman/scripts` + from a checkout of automation that has your fix. +- RepoMan-specific skills are canonical **here**: their `SKILL.md`s are + authored in this repo. + [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) hosts + skills designed to work standalone as well as with RepoMan — do not port + RepoMan skill docs there. +- The per-program `skills//scripts/` copies are known-stale relative + to automation's engine (resync tracked in + [#5](https://github.com/rossoctl/repoman/issues/5)). + Do not treat either side as authoritative or copy between them piecemeal. +- The full contributor workflow (placement, testing, dry runs, vendoring, PR + rules) is in the README's `## Development` section — read it before + changing anything here. + ## Scope Discipline Only commit files directly related to the PR's stated purpose. Do not bundle diff --git a/README.md b/README.md index e0940ca..26b9964 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,59 @@ adding the marketplace to browse the authoritative list. Individual programs may have additional requirements documented in their `SKILL.md`. -## How it is built +## Development + +RepoMan is developed across three repositories: + +| Repository | Role | +|------------|------| +| [rossoctl/automation](https://github.com/rossoctl/automation) | Engine source of truth: the flat scripts in `scripts/`, plus tests and specs. All engine changes are made and tested here first. | +| [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) | Skills designed to work standalone as well as with RepoMan. | +| [rossoctl/repoman](https://github.com/rossoctl/repoman) (this repo) | Distribution (`rossoctl-repoman` marketplace) and the canonical home for RepoMan-specific `SKILL.md`s. The engine is not authored here. | + +See the `CHANGELOG.md` for release history. + +### Contributor workflow + +1. **Place the work.** A skill that is useful on its own → author it in + agent-skills. A RepoMan-specific program (keeps state under `~/.repoman`) → + deterministic scripts in automation, `SKILL.md` in this repo. New programs + start from automation's `docs/program-template.md`. +2. **Test in automation**, next to the existing tests in `tests/`. Bash 3.2 + only: no `mapfile`, no associative arrays, BSD fallbacks for GNU-only + flags. If the skill writes to `~/.repoman`, give its `SKILL.md` a + machine-readable `### Requirements` block so `repoman-check.sh` can gate + the run. +3. **Keep real state out of test runs.** Point `REPOMAN_CONFIG_FILE` / + `REPOMAN_REPOS_FILE` at scratch files; never hand-edit `~/.repoman` + (`repoman-setup.sh` is its only writer); drive first runs through + `repoman-run.sh owner/repo --dry-run` and drop `--dry-run` only + once the output looks right. +4. **Vendor the engine** into this repo (below) — a new engine script must be + added to the vendor manifest — then run `tests/test-vendor-engine.sh`. To + try the plugin without reinstalling from the marketplace: + `claude --plugin-dir `. +5. **Before the PR**: `git config core.hooksPath scripts/hooks`, sign off + with `git commit -s` (DCO), use a conventional title, one purpose per PR. + +### Vendoring the engine + +`skills/repoman/scripts/` is **generated output** — a vendored copy of the +automation engine, stamped with its source commit in +[`_vendor.json`](skills/repoman/scripts/_vendor.json). Never edit those files +directly: an in-place fix bypasses automation's tests and is silently +overwritten on the next vendor. To pick up engine changes, from a checkout of +this repo: + +```bash +scripts/vendor-engine.sh --source \ + --dest skills/repoman/scripts +``` -The engine scripts are vendored from [rossoctl/automation](https://github.com/rossoctl/automation) -(the development source of truth) into this distribution repo, with a provenance stamp -(`skills/repoman/scripts/_vendor.json`) recording the exact source commit. See the -`CHANGELOG.md` for release history. +The script copies an explicit manifest (never a glob, so it cannot touch the +standalone skills' own `scripts/`), pre-validates the whole manifest before +copying anything, and rewrites `_vendor.json` with the automation HEAD SHA it +vendored from. `tests/test-vendor-engine.sh` is its regression test. ## License From b559fc92df1ec1030ef01e644aed7135e175dc36 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 5 Nov 2026 13:54:00 -0500 Subject: [PATCH 2/7] docs: Address review feedback on the contributor workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the non-working `claude --plugin-dir` smoke test (this repo is a marketplace layout, not a plugin) with the verified `claude plugin marketplace add` + `claude plugin install` form. - Name the repoman-side wiring a new program needs: marketplace.json entry, README Programs table row, and the program → skill mapping in skills/repoman/SKILL.md. - Say "both the vendor manifest and its test fixture (including the counts)" — the manifest is duplicated in tests/test-vendor-engine.sh. - Require a clean automation checkout for vendoring: the provenance stamp records the source HEAD SHA, so a dirty tree would stamp a SHA that does not contain the vendored bytes (CLAUDE.md bullet included). Assisted-By: Claude (Anthropic AI) Signed-off-by: Jim Cadden --- CLAUDE.md | 2 +- README.md | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dc07413..d6b49f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ This repo **distributes** RepoMan; it is not where the engine is developed. provenance-stamped in `skills/repoman/scripts/_vendor.json`. **Never edit them in place.** To update them, run `scripts/vendor-engine.sh --source --dest skills/repoman/scripts` - from a checkout of automation that has your fix. + from a clean automation checkout that has your fix committed. - RepoMan-specific skills are canonical **here**: their `SKILL.md`s are authored in this repo. [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) hosts diff --git a/README.md b/README.md index 26b9964..a7fd324 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,11 @@ See the `CHANGELOG.md` for release history. 1. **Place the work.** A skill that is useful on its own → author it in agent-skills. A RepoMan-specific program (keeps state under `~/.repoman`) → deterministic scripts in automation, `SKILL.md` in this repo. New programs - start from automation's `docs/program-template.md`. + start from automation's `docs/program-template.md`. A new program also + needs repoman-side wiring that is not engine code: a + `.claude-plugin/marketplace.json` entry, a row in the README + `## Programs` table, and the program → skill mapping in + `skills/repoman/SKILL.md`. 2. **Test in automation**, next to the existing tests in `tests/`. Bash 3.2 only: no `mapfile`, no associative arrays, BSD fallbacks for GNU-only flags. If the skill writes to `~/.repoman`, give its `SKILL.md` a @@ -92,9 +96,11 @@ See the `CHANGELOG.md` for release history. `repoman-run.sh owner/repo --dry-run` and drop `--dry-run` only once the output looks right. 4. **Vendor the engine** into this repo (below) — a new engine script must be - added to the vendor manifest — then run `tests/test-vendor-engine.sh`. To - try the plugin without reinstalling from the marketplace: - `claude --plugin-dir `. + added to both the vendor manifest and its test fixture in + `tests/test-vendor-engine.sh`, including the expected counts — then run + that test. To try the plugin without reinstalling from the marketplace: + `claude plugin marketplace add ` then + `claude plugin install repoman@rossoctl-repoman`. 5. **Before the PR**: `git config core.hooksPath scripts/hooks`, sign off with `git commit -s` (DCO), use a conventional title, one purpose per PR. @@ -104,8 +110,10 @@ See the `CHANGELOG.md` for release history. automation engine, stamped with its source commit in [`_vendor.json`](skills/repoman/scripts/_vendor.json). Never edit those files directly: an in-place fix bypasses automation's tests and is silently -overwritten on the next vendor. To pick up engine changes, from a checkout of -this repo: +overwritten on the next vendor. To pick up engine changes, run it from this +repo against a **clean automation checkout** — the stamp records the source +HEAD SHA, so vendoring uncommitted changes would produce a stamp that does not +match the copied bytes: ```bash scripts/vendor-engine.sh --source \ From 883a07122366d5c7ea15617165442889aac7a9ce Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 5 Nov 2026 17:31:21 -0500 Subject: [PATCH 3/7] fix: Reject a dirty source tree in vendor-engine.sh (provenance guard) The provenance stamp records the source HEAD SHA, but the copy loop reads the working-tree bytes, so vendoring an automation checkout with uncommitted changes stamped a commit that does not contain the vendored content. vendor-engine.sh now fails unless the source is clean, with an explicit --allow-dirty escape hatch for scratch experiments. Regression cases cover the rejection (before any copy, dest left uncreated) and the override. Also phrases the CLAUDE.md vendored-output rule as the whole skills/repoman/scripts/ directory (shell scripts and non-shell extras like doc-drift-check.py), so an agent cannot read the old *.sh glob as leaving the Python helper editable, and notes the guard in the README vendoring section. Fixes #20 Assisted-By: Claude (Anthropic AI) Signed-off-by: Jim Cadden --- CLAUDE.md | 8 +++++--- README.md | 3 ++- scripts/vendor-engine.sh | 12 +++++++++++- tests/test-vendor-engine.sh | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d6b49f6..927ffd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,11 +52,13 @@ This repo **distributes** RepoMan; it is not where the engine is developed. - Engine scripts are developed and tested in [rossoctl/automation](https://github.com/rossoctl/automation) `scripts/`. Engine changes are made there, then vendored here. -- `skills/repoman/scripts/*.sh` are **vendored copies** — generated output, +- Everything under `skills/repoman/scripts/` — the shell scripts and the + non-shell extras (e.g. `doc-drift-check.py`) — is **vendored output**, provenance-stamped in `skills/repoman/scripts/_vendor.json`. **Never edit - them in place.** To update them, run + anything there in place.** To update it, run `scripts/vendor-engine.sh --source --dest skills/repoman/scripts` - from a clean automation checkout that has your fix committed. + from a clean automation checkout that has your fix committed (the script + rejects a dirty source unless `--allow-dirty` is passed). - RepoMan-specific skills are canonical **here**: their `SKILL.md`s are authored in this repo. [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) hosts diff --git a/README.md b/README.md index a7fd324..83d8837 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,8 @@ directly: an in-place fix bypasses automation's tests and is silently overwritten on the next vendor. To pick up engine changes, run it from this repo against a **clean automation checkout** — the stamp records the source HEAD SHA, so vendoring uncommitted changes would produce a stamp that does not -match the copied bytes: +match the copied bytes (the script rejects a dirty source unless +`--allow-dirty` is passed): ```bash scripts/vendor-engine.sh --source \ diff --git a/scripts/vendor-engine.sh b/scripts/vendor-engine.sh index 4cfa244..8009b2c 100644 --- a/scripts/vendor-engine.sh +++ b/scripts/vendor-engine.sh @@ -8,17 +8,27 @@ set -uo pipefail # Part of the RepoMan v0.1.0 packaged release (epic rossoctl/automation#72). # # vendor-engine.sh --source --dest +# [--allow-dirty] -SRC="" DEST="" +SRC="" DEST="" ALLOW_DIRTY=false while [ $# -gt 0 ]; do case "$1" in --source) SRC="${2:-}"; shift 2 ;; --dest) DEST="${2:-}"; shift 2 ;; + --allow-dirty) ALLOW_DIRTY=true; shift ;; *) echo "ERROR: unknown arg: $1" >&2; exit 2 ;; esac done [ -n "$SRC" ] && [ -n "$DEST" ] || { echo "ERROR: --source and --dest are required" >&2; exit 2; } +# Provenance guard: the stamp records the source HEAD SHA, so vendoring a +# dirty tree would stamp a commit that does not contain the copied bytes +# (see repoman#20). Reject a dirty source unless --allow-dirty is passed +# (scratch experiments only; the stamp then knowingly lies about the bytes). +if [ "$ALLOW_DIRTY" = false ] && [ -n "$(git -C "$SRC" status --porcelain 2>/dev/null)" ]; then + echo "ERROR: source $SRC has uncommitted changes; commit or stash them so the provenance stamp matches the vendored bytes (or pass --allow-dirty)" >&2; exit 1 +fi + # The 26 engine shell scripts. Flat set: nearly all source program-lib.sh, # which sources core.sh/github-api.sh/fork.sh/org.sh/repoman-requirements.sh. MANIFEST="automation-health-dashboard core dep-bump-fixer dep-bump-scanner \ diff --git a/tests/test-vendor-engine.sh b/tests/test-vendor-engine.sh index 413c700..762fe9c 100644 --- a/tests/test-vendor-engine.sh +++ b/tests/test-vendor-engine.sh @@ -53,6 +53,20 @@ bash "$VENDOR" --source "$SRC" --dest "$DEST" >/dev/null 2>&1 got2=$(jq -r '.source_sha' "$DEST/_vendor.json" 2>/dev/null) if [ "$got2" = "$want" ]; then echo "PASS: idempotent stamp sha"; else echo "FAIL: idempotency $got2 != $want"; fail=1; fi +# --- Dirty source: rejected before any copy, dest left uncreated --- +DIRTYDEST="$TMP/dirtydest" +echo x >> "$SRC/scripts/org.sh" # uncommitted modification +if bash "$VENDOR" --source "$SRC" --dest "$DIRTYDEST" >/dev/null 2>&1; then + echo "FAIL: dirty source was not rejected"; fail=1 +else echo "PASS: dirty source rejected"; fi +if [ ! -d "$DIRTYDEST" ]; then echo "PASS: dirty rejection left dest uncreated"; else echo "FAIL: dirty rejection still created dest"; fail=1; fi +# --- Dirty source with --allow-dirty: override works, stamp still stamped --- +if bash "$VENDOR" --source "$SRC" --dest "$DIRTYDEST" --allow-dirty >/dev/null 2>&1; then + echo "PASS: --allow-dirty overrides the guard" +else echo "FAIL: --allow-dirty did not override"; fail=1; fi +if [ -f "$DIRTYDEST/org.sh" ]; then echo "PASS: --allow-dirty run copied files"; else echo "FAIL: --allow-dirty run copied nothing"; fail=1; fi +git -C "$SRC" checkout -- scripts/org.sh # restore for the tests below + # --- Missing source fails loud AND leaves the dest empty (no half-vendored dir) --- BADDEST="$TMP/baddest" missing_source_fails() ( From a40fc6eddadd0eaa77f28dcf4bad69d6acb00af5 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Fri, 6 Nov 2026 13:56:46 -0500 Subject: [PATCH 4/7] Update README.md Co-authored-by: Gloire Rubambiza Signed-off-by: Jim Cadden --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83d8837..64564af 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ RepoMan is developed across three repositories: | [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) | Skills designed to work standalone as well as with RepoMan. | | [rossoctl/repoman](https://github.com/rossoctl/repoman) (this repo) | Distribution (`rossoctl-repoman` marketplace) and the canonical home for RepoMan-specific `SKILL.md`s. The engine is not authored here. | -See the `CHANGELOG.md` for release history. +See the `[CHANGELOG.md](https://github.com/rossoctl/repoman/blob/main/CHANGELOG.md)` for release history. ### Contributor workflow From b2e304374a110c974cc89e141ee10ae1ec4c9bb0 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Fri, 6 Nov 2026 13:56:56 -0500 Subject: [PATCH 5/7] Update CLAUDE.md Co-authored-by: Gloire Rubambiza Signed-off-by: Jim Cadden --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 927ffd6..b625517 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ This repo **distributes** RepoMan; it is not where the engine is developed. - Everything under `skills/repoman/scripts/` — the shell scripts and the non-shell extras (e.g. `doc-drift-check.py`) — is **vendored output**, provenance-stamped in `skills/repoman/scripts/_vendor.json`. **Never edit - anything there in place.** To update it, run + anything here in place.** To update it, run `scripts/vendor-engine.sh --source --dest skills/repoman/scripts` from a clean automation checkout that has your fix committed (the script rejects a dirty source unless `--allow-dirty` is passed). From 87c7e121fe85732a8b5a139fd2db69cc58bf345c Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Fri, 6 Nov 2026 14:03:40 -0500 Subject: [PATCH 6/7] Update README.md Co-authored-by: Gloire Rubambiza Signed-off-by: Jim Cadden --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64564af..ec4ddb5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ RepoMan is developed across three repositories: | Repository | Role | |------------|------| -| [rossoctl/automation](https://github.com/rossoctl/automation) | Engine source of truth: the flat scripts in `scripts/`, plus tests and specs. All engine changes are made and tested here first. | +| [rossoctl/automation](https://github.com/rossoctl/automation) | Engine source of truth: the flat scripts in `scripts/`, plus tests and specs. All engine changes are made and tested there first. | | [rossoctl/agent-skills](https://github.com/rossoctl/agent-skills) | Skills designed to work standalone as well as with RepoMan. | | [rossoctl/repoman](https://github.com/rossoctl/repoman) (this repo) | Distribution (`rossoctl-repoman` marketplace) and the canonical home for RepoMan-specific `SKILL.md`s. The engine is not authored here. | From 1606d1d94845600bb7ffb62f570ec1df25c8d259 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Fri, 6 Nov 2026 14:52:13 -0500 Subject: [PATCH 7/7] fix: Fail closed when the vendor source is not a git checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dirty-source guard read 'git status --porcelain' failing silently (non-git source, e.g. a tarball extract) as 'clean', so the run only died at the stamp step — after overwriting the dest, leaving it half-vendored with a stale _vendor.json. Now an up-front 'git rev-parse --git-dir' check rejects an uninspectable source before anything is copied, unconditionally: the stamp needs a source HEAD even with --allow-dirty. Regression case added: a source holding the full manifest but no .git must fail (with and without --allow-dirty) leaving the dest uncreated. Assisted-By: Claude (Anthropic AI) Signed-off-by: Jim Cadden --- scripts/vendor-engine.sh | 9 +++++++++ tests/test-vendor-engine.sh | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/scripts/vendor-engine.sh b/scripts/vendor-engine.sh index 8009b2c..f22b82f 100644 --- a/scripts/vendor-engine.sh +++ b/scripts/vendor-engine.sh @@ -21,6 +21,15 @@ while [ $# -gt 0 ]; do done [ -n "$SRC" ] && [ -n "$DEST" ] || { echo "ERROR: --source and --dest are required" >&2; exit 2; } +# Fail closed: if $SRC cannot be inspected at all (not a git checkout — e.g. +# a tarball extract — or git missing), the dirty check below would read the +# resulting empty output as "clean", the run would only die at the stamp +# step, and the dest would be left half-vendored with a stale _vendor.json. +# Unconditional: the stamp needs a source HEAD even with --allow-dirty. +if ! git -C "$SRC" rev-parse --git-dir >/dev/null 2>&1; then + echo "ERROR: cannot inspect $SRC (not a git checkout?); the provenance stamp requires a source HEAD" >&2; exit 1 +fi + # Provenance guard: the stamp records the source HEAD SHA, so vendoring a # dirty tree would stamp a commit that does not contain the copied bytes # (see repoman#20). Reject a dirty source unless --allow-dirty is passed diff --git a/tests/test-vendor-engine.sh b/tests/test-vendor-engine.sh index 762fe9c..0e9b68d 100644 --- a/tests/test-vendor-engine.sh +++ b/tests/test-vendor-engine.sh @@ -85,4 +85,23 @@ if ! missing_source_fails; then echo "PASS: missing source fails loud"; else ech copied=$(find "$BADDEST" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l | tr -d ' ') if [ "${copied:-0}" = "0" ]; then echo "PASS: no half-vendored dir (dest empty on failure)"; else echo "FAIL: $copied files copied before the abort (half-vendored)"; fail=1; fi +# --- Non-git source (full manifest, no .git): rejected before any copy --- +# The dirty guard's `git status --porcelain` is empty for a non-git dir, which +# used to read as "clean"; the run then died only at the stamp step, after +# overwriting the dest. Must fail closed — with and without --allow-dirty +# (the stamp needs a source HEAD either way). +NOGITDEST="$TMP/nogitdest" +NOGIT="$TMP/nogit"; mkdir -p "$NOGIT/scripts" +# shellcheck disable=SC2086 +for f in $MANIFEST; do echo x > "$NOGIT/scripts/$f.sh"; done +# shellcheck disable=SC2086 +for f in $EXTRA_FILES; do echo x > "$NOGIT/scripts/$f"; done +if bash "$VENDOR" --source "$NOGIT" --dest "$NOGITDEST" >/dev/null 2>&1; then + echo "FAIL: non-git source was not rejected"; fail=1 +else echo "PASS: non-git source rejected"; fi +if [ ! -d "$NOGITDEST" ]; then echo "PASS: non-git rejection left dest uncreated"; else echo "FAIL: non-git rejection still created dest"; fail=1; fi +if bash "$VENDOR" --source "$NOGIT" --dest "$NOGITDEST" --allow-dirty >/dev/null 2>&1; then + echo "FAIL: non-git source accepted with --allow-dirty"; fail=1 +else echo "PASS: non-git source rejected even with --allow-dirty"; fi + [ "$fail" -eq 0 ] && echo "PASS" || { echo "FAILURES"; exit 1; }