Skip to content

docs+chore: private-copy workflow guide; stop ignoring run manifests - #20

Merged
21tmccauley merged 3 commits into
mainfrom
docs/customer-fork-workflow
Jul 28, 2026
Merged

docs+chore: private-copy workflow guide; stop ignoring run manifests#20
21tmccauley merged 3 commits into
mainfrom
docs/customer-fork-workflow

Conversation

@21tmccauley

@21tmccauley 21tmccauley commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Two related changes: a customer-facing guide for keeping a private copy of this repo, and the manifest-tracking fix that guide turned up.

1. docs/private_mirror_workflow.md

A guide for teams whose fetcher work can't be public — written to be handed to their engineers directly. First reader is the Epicor CR-26 engagement.

A GitHub fork can't be a private working copy: forks inherit the upstream's visibility and GitHub disables the visibility control on them, and every commit in a fork network stays reachable by SHA from the public upstream forever — deleting the branch, the fork, or force-pushing does not revoke access. That matters here because fetcher code carries tenant and subscription IDs, resource group and endpoint names, and each control's population scope; with narratives alongside it, the boundary's exceptions and limitations too.

So the guide documents a private mirror instead: mirror this repo into a private repo, add ours as a plain read-only remote (public needs no access grant), and merge release tags. A fork becomes optional and, in the recommended path, unnecessary — their Paramify contact already has collaborator access to the private repo and can carry any general-purpose change upstream.

Points verified empirically against a scratch upstream/mirror pair rather than written from memory:

  • Upstream tags namespaced into refs/tags/paramify/* on fetch. Tags are one flat namespace. Once a customer cuts their own v0.4.0-beta and we publish v0.4.0-beta, a plain git fetch upstream --tags prints ! [rejected] (would clobber existing tag) and exits 0 — the rejection scrolls past, the command looks successful, and git merge v0.4.0-beta takes their commit while everyone believes they took ours.
  • Mirror verification is a git ls-remote ref diff plus a default-branch check. Tags being present only proves tags copied. Separately, if the destination repo's HEAD is unset, cloning yields a working copy with no branch checked out and the first commit silently starts an orphan history — the same refusing to merge unrelated histories breakage the mirror exists to prevent. The ref diff does not catch that, which is why both checks are there.
  • Leads with GitHub's importer, not git push --mirror. --mirror force-writes every ref at the destination and deletes destination refs absent locally, so in a repo with both remotes configured one transposed invocation both leaks private branches and destroys upstream history. The CLI path stays as a fallback, sequenced through a throwaway bare clone deleted before a second remote exists, with git remote set-url --push upstream no_push as a further guard.
  • Single main in the customer repo. A local pristine main advanced with --ff-only plus a second working branch was considered and rejected: redundant with upstream/main, which is already an unwritable tracking ref, and it adds a permanent failure mode where one stray commit breaks --ff-only forever with reset --hard as the only recovery.
  • Append-only upstream history, stated as a commitment to the reader — a rewrite on our side leaves every mirror on commits that no longer exist, with no clean recovery.
  • A "Questions your security review will ask" table, since the privacy argument is the reason the workflow exists and needs to travel to people who won't read the git mechanics.

2. Run manifests are no longer gitignored

Writing that guide surfaced a real defect. .gitignore listed /manifest.yaml and /manifests/ while a manifest.yaml was also tracked (it entered the index at c2f1c49). Ignore rules don't apply to already-tracked files, so the entry was inert: edits to the shipped manifest staged by default, and anyone working from a copy of this repo hit a conflict at that path on every upgrade.

The ignore was the wrong instinct regardless. A manifest records which evidence you collect, so under a compliance program it belongs in version control — and it holds no secret values, because ${env:VAR} resolves from the environment at run time, so a manifest names its secrets without containing them.

  • Renames the tracked manifest.yaml to example_manifest.yaml — content unchanged (13 Datadog entries; paramify validate reports valid). It now reads as the reference sample it always was instead of shadowing the path the builder writes to.
  • Drops both lines from .gitignore, with a comment explaining why nothing there is ignored.
  • Nothing ships at ./manifest.yaml anymore, so a manifest you create there cannot collide with ours on merge.

No code change. paramify manifest init's ./manifest.yaml default and the manifests/*.yaml picker convention are untouched. list_manifests already guarded on existence and read_manifest returns an empty manifest for an absent path, so a fresh clone with no manifest degrades to "no fetchers" rather than an error. 233 tests pass; nothing referenced the tracked file.

Upgrade note is in CHANGELOG.md under Unreleased: anyone relying on the tracked manifest.yaml copies example_manifest.yaml over it or passes --manifest explicitly.

Tate McCauley and others added 2 commits July 28, 2026 13:41
Customers who build fetchers against this framework need a private working
copy, but forks of a public repo are permanently public and fork-network
commits stay reachable by SHA from upstream forever. Fetcher code carries
tenant identifiers and control scope, so a public fork publishes a map of the
customer's compliance posture.

Documents the private-mirror model instead: mirror the repo into a private
repo, track upstream as a plain git remote (public needs no permission grant
or fork relationship), and merge our release tags.

Key details the guide pins down:

- Upstream tags are namespaced into refs/tags/paramify/* on fetch. Tags are
  one flat namespace, and a colliding customer tag makes `git fetch --tags`
  print a rejection but exit 0 — so a merge takes their tag while everyone
  believes they took ours.
- `git push --mirror` is marked Paramify-side and run-once, kept out of any
  runbook the customer holds: a transposed invocation both leaks private
  branches upstream and deletes upstream refs absent locally.
- Mirror verification is a `git ls-remote` diff plus a default-branch check.
  An unset HEAD on the destination yields a clone with no branch checked out,
  and the first commit then starts an orphan history — reintroducing the
  unrelated-histories failure the mirror exists to prevent.
- Root manifest.yaml is tracked despite being in .gitignore (ignore rules do
  not apply to tracked files), so customer edits stage by default and conflict
  on every sync. Both exits documented.
- Published upstream history is append-only from the first mirror onward; any
  history rewrite permanently diverges every customer copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide was written from Paramify's side of the table — it described what
"the customer" does, instructed us on what not to hand them, and put
`git push --mirror` in front of a reader who was never its audience. Rewritten
to address the engineer who actually runs these steps.

Substantive changes, not just voice:

- Leads with GitHub's repository importer instead of `git push --mirror`. The
  importer copies full history with no local commands and no way to point a
  destructive ref-deleting push at the wrong remote. The CLI path stays as a
  fallback for instances that can't reach external URLs, sequenced through a
  throwaway bare clone that is deleted before any second remote exists, with
  the reason spelled out rather than asserted.
- Adds a "Questions your security review will ask" table. The privacy argument
  is the reason this workflow exists, so it belongs in a form a reader can take
  to their security team: fork visibility, fork-network recoverability, what
  access Paramify needs, how to revoke it, and where secrets live.
- Reframes append-only upstream history as a commitment made *to* the reader,
  with its consequence for them stated, rather than a note to ourselves.
- Renames to docs/private_mirror_workflow.md: every other doc here is
  snake_case, and "customer fork workflow" names neither the audience's
  perspective nor the thing being built, which is a mirror and not a fork.

Content verified earlier this session stands unchanged: tag namespacing into
refs/tags/paramify/*, the fetch-collision exit-0 behavior, the unset-HEAD
orphan-history trap, and the tracked-manifest.yaml conflict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@21tmccauley
21tmccauley force-pushed the docs/customer-fork-workflow branch from 8a45b1b to 10a2bf0 Compare July 28, 2026 19:47
@21tmccauley 21tmccauley changed the title docs: add customer fork workflow guide docs: add private-copy workflow guide for customers Jul 28, 2026
…est.yaml

`.gitignore` listed /manifest.yaml and /manifests/ while a manifest.yaml was
also tracked. Ignore rules don't apply to already-tracked files, so the entry
was inert: edits to the shipped manifest staged by default, and anyone working
from a copy of this repo hit a conflict at that path on every upgrade.

The ignore was the wrong instinct anyway. A manifest records which evidence you
collect, so under a compliance program it belongs in version control — and it
holds no secret values, because `${env:VAR}` resolves from the environment at
run time, so a manifest names its secrets without containing them.

- Rename the tracked manifest.yaml to example_manifest.yaml (content unchanged;
  13 Datadog entries, validates). It reads as the reference sample it always
  was, rather than shadowing the path the builder writes to.
- Drop /manifest.yaml and /manifests/ from .gitignore, with a comment saying
  why nothing there is ignored.
- Nothing now ships at ./manifest.yaml, so a manifest you create there cannot
  collide with ours on merge — which is the point.

No code change. `paramify manifest init`'s ./manifest.yaml default and the
manifests/*.yaml picker convention are untouched; discovery already guarded on
existence, and read_manifest returns an empty manifest for an absent path, so a
fresh clone with no manifest degrades to "no fetchers" rather than an error.
233 tests pass; nothing referenced the tracked file.

Also updates docs/private_mirror_workflow.md, which documented the old conflict
and its two workarounds — both now moot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@21tmccauley 21tmccauley changed the title docs: add private-copy workflow guide for customers docs+chore: private-copy workflow guide; stop ignoring run manifests Jul 28, 2026
@21tmccauley
21tmccauley merged commit 3b8755b into main Jul 28, 2026
7 checks passed
@21tmccauley
21tmccauley deleted the docs/customer-fork-workflow branch August 4, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant