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
37 changes: 26 additions & 11 deletions .archgate/adrs/ARCH-013-version-synchronization.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,59 +33,74 @@ Version strings are not the only artifact that must stay synchronized across shi

README synchronization is automated by the same `.simple-release.js` bump hook that syncs version strings: during the release commit it copies the root `README.md` over every shim copy. The companion rule is the CI safety net that catches drift between releases (for example, when a `README.md` edit lands in a PR without the copies being regenerated).

The license file is the third artifact that must be present inside each shim package. The repository declares Apache-2.0 in the root `LICENSE.md`, but registries and package indexes detect license information from files **inside the published package**, not from the repository root. This is most acute for the Go shim: it is published as a subdirectory module (`github.com/archgate/cli/shims/go`), and a subdirectory module's zip contains only files under that subtree — so the repo-root `LICENSE.md` is never included and pkg.go.dev reports "no license" until a `LICENSE.md` exists inside `shims/go/`. The same constraint applies to the other ecosystems, and is compounded by the synchronized README: because every shim README is byte-identical to the root `README.md`, it carries the root's relative license link and badge (`[Apache-2.0](LICENSE.md)`), which only resolves when a `LICENSE.md` sits beside the README in each shim directory. Each shim therefore carries its own copy that must mirror root:

1. `shims/go/LICENSE.md` — detected by pkg.go.dev's license scan of the module subtree
2. `shims/pypi/LICENSE.md` — resolves the README license link; complements the `pyproject.toml` license metadata
3. `shims/nuget/Archgate.Tool/LICENSE.md` — resolves the packed README's license link (NuGet.org itself reads `<PackageLicenseExpression>`)
4. `shims/rubygem/LICENSE.md` — shipped in the gem via the gemspec `spec.files` glob
5. `shims/maven/LICENSE.md` — for source/repository readers (Maven Central reads the pom `<licenses>` block)

## Decision

`package.json` `version` is the single source of truth. All other version references MUST match it.

**Automated via release process:** The `.simple-release.js` bump hook updates all version locations — and propagates the root `README.md` to every shim copy — to match `package.json` during the release commit. This is fully automated and requires no manual intervention.
**Automated via release process:** The `.simple-release.js` bump hook updates all version locations — and propagates the root `README.md` and root `LICENSE.md` to every shim copy — to match `package.json` during the release commit. This is fully automated and requires no manual intervention.

The shim packages (npm, PyPI, NuGet, Go, Maven Central, RubyGems) are thin wrappers that download the platform binary from GitHub Releases. Their embedded version determines which release to download, so version drift causes download failures (404) or installs the wrong version.

Every shim package README MUST be byte-identical to the root `README.md` (normalized for line endings and trailing whitespace). The root `README.md` is the single source of README content. The npm package requires no separate copy because it publishes the root README directly; all other ecosystems ship a mirrored copy at the paths listed in the Context, regenerated by the bump hook.

Every shim package directory MUST contain a `LICENSE.md` that is byte-identical to the root `LICENSE.md` (normalized for line endings and trailing whitespace). The root `LICENSE.md` is the single source of license content. The npm package requires no separate copy because npm always publishes the root `LICENSE.md` alongside the root README; all other ecosystems ship a mirrored copy at the paths listed in the Context, regenerated by the bump hook. The `ARCH-013/shim-license-sync` companion rule is the CI safety net that catches drift between releases.

## Do's and Don'ts

### Do

- **DO** rely on `.simple-release.js` for all version and README sync (do not update manually)
- **DO** use the companion rules to catch version and README drift in CI as a safety net
- **DO** edit the root `README.md` as the single source of README content — the bump hook propagates it to every shim
- **DO** keep every shim package README byte-identical to the root `README.md`
- **DO** regenerate the shim copies with `cp README.md <target>` if you need them synced before a release (e.g. to satisfy `shim-readme-sync` in a PR that edits the README)
- **DO**, when adding a new shim ecosystem, add its version file and README path to `.simple-release.js` and to the companion `shim-version-sync` / `shim-readme-sync` rules
- **DO** rely on `.simple-release.js` for all version, README, and LICENSE sync (do not update manually)
- **DO** use the companion rules to catch version, README, and LICENSE drift in CI as a safety net
- **DO** edit the root `README.md` and root `LICENSE.md` as the single source of that content — the bump hook propagates each to every shim
- **DO** keep every shim package README byte-identical to the root `README.md`, and every shim `LICENSE.md` byte-identical to the root `LICENSE.md`
- **DO** regenerate the shim copies with `cp README.md <target>` / `cp LICENSE.md <target>` if you need them synced before a release (e.g. to satisfy `shim-readme-sync` / `shim-license-sync` in a PR that edits the root file)
- **DO**, when adding a new shim ecosystem, add its version file, README path, and `LICENSE.md` path to `.simple-release.js` and to the companion `shim-version-sync` / `shim-readme-sync` / `shim-license-sync` rules
- **DO**, when adding a new shim ecosystem, wire its `LICENSE.md` into the ecosystem's packaging where the license must ship inside the artifact (e.g. add `"LICENSE.md"` to the RubyGem `spec.files` glob)

### Don't

- **DON'T** manually edit `softwareVersion` in `docs/astro.config.mjs` — the release hook handles this
- **DON'T** manually edit version strings in any `shims/` package — the release hook handles this
- **DON'T** hand-edit an individual shim README (e.g. `shims/pypi/README.md`) — edit the root `README.md`; the release hook regenerates the copies
- **DON'T** let shim READMEs diverge from the root, even for ecosystem-specific wording — the content MUST be identical everywhere
- **DON'T** rely on the repository-root `LICENSE.md` to license a subdirectory-published package — pkg.go.dev and the package registries only see files inside the package directory, so a missing per-shim `LICENSE.md` shows up as "no license" and breaks the README's relative license link

## Consequences

### Positive

- Consistent version information across user-facing surfaces
- Consistent README content across every package registry (PyPI, NuGet, pkg.go.dev, RubyGems, npm)
- CI catches both version drift and README drift before it reaches production
- Correct, discoverable license metadata on every registry — pkg.go.dev detects Apache-2.0, and the README license link resolves in each published package
- CI catches version drift, README drift, and LICENSE drift before it reaches production

### Negative

- **Duplicated files in the repo:** Five identical README copies live in the tree, which can surprise contributors who edit a copy instead of the root.
- **Mid-cycle drift window:** Because the bump hook only regenerates the copies at release time, a `README.md` edit merged between releases leaves the committed shim copies stale until the next bump.
- **Duplicated files in the repo:** Five identical README copies — and five identical `LICENSE.md` copies — live in the tree, which can surprise contributors who edit a copy instead of the root.
- **Mid-cycle drift window:** Because the bump hook only regenerates the copies at release time, a `README.md` or `LICENSE.md` edit merged between releases leaves the committed shim copies stale until the next bump.

### Risks

- A contributor edits a shim README copy directly, or edits the root `README.md` without regenerating the copies, causing drift. **Mitigation:** the `ARCH-013/shim-readme-sync` rule fails `archgate check` in CI whenever any copy differs from root (with a `cp` fix suggestion), and the `.simple-release.js` bump hook re-syncs every copy at release time as a backstop.
- A new shim ecosystem is added without a `LICENSE.md`, or the root license changes without the copies being updated, leaving a package with missing or stale license metadata. **Mitigation:** the `ARCH-013/shim-license-sync` rule fails `archgate check` in CI whenever any copy differs from the root `LICENSE.md` (with a `cp` fix suggestion), and the `.simple-release.js` bump hook re-syncs every copy at release time as a backstop — exactly parallel to `shim-readme-sync`.

## Compliance and Enforcement

### Automated Enforcement

- **Release hook** `.simple-release.js`: Syncs all version locations and propagates the root `README.md` to every shim copy during `bump()`. Fully automated.
- **Release hook** `.simple-release.js`: Syncs all version locations and propagates the root `README.md` and root `LICENSE.md` to every shim copy during `bump()`. Fully automated.
- **Archgate rule** `ARCH-013/docs-version-sync`: Checks that `softwareVersion` in `docs/astro.config.mjs` matches `package.json` version. Severity: `error`.
- **Archgate rule** `ARCH-013/shim-version-sync`: Checks that all shim package versions match `package.json` version. Severity: `error`.
- **Archgate rule** `ARCH-013/shim-readme-sync`: Checks that every shim package README (`shims/go/README.md`, `shims/pypi/README.md`, `shims/nuget/Archgate.Tool/README.md`, `shims/rubygem/README.md`, `shims/maven/README.md`) is byte-identical to the canonical root `README.md`, normalized for line endings and trailing whitespace. Severity: `error`. Like `shim-version-sync`, this is the CI safety net for the automated `.simple-release.js` propagation — it catches drift between releases.
- **Archgate rule** `ARCH-013/shim-license-sync`: Checks that every shim package `LICENSE.md` (`shims/go/LICENSE.md`, `shims/maven/LICENSE.md`, `shims/nuget/Archgate.Tool/LICENSE.md`, `shims/pypi/LICENSE.md`, `shims/rubygem/LICENSE.md`) is byte-identical to the canonical root `LICENSE.md`, normalized for line endings and trailing whitespace. Severity: `error`. Parallel to `shim-readme-sync`, this is the CI safety net for the automated `.simple-release.js` propagation — it catches drift between releases.

## References

Expand Down
52 changes: 52 additions & 0 deletions .archgate/adrs/ARCH-013-version-synchronization.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,57 @@ export default {
}
},
},
"shim-license-sync": {
description:
"All shim package LICENSE.md files must be byte-identical to the canonical root LICENSE.md",
severity: "error",
async check(ctx) {
let canonical: string;
try {
canonical = await ctx.readFile("LICENSE.md");
} catch {
// Root LICENSE.md should always exist; skip if running in a context without it
return;
}

// .gitattributes enforces eol=lf, but normalize trailing newlines and
// CRLF defensively so the rule is stable across platforms.
const normalize = (s: string): string =>
s.replaceAll("\r\n", "\n").trimEnd();
const expected = normalize(canonical);

// The npm package publishes the root LICENSE.md directly (npm
// always-include), so it is excluded here. Every other shim ecosystem
// ships its own copy that must mirror the root — registries and
// pkg.go.dev detect the license from files inside the package, not
// from the repository root.
const shimLicenses: string[] = [
"shims/go/LICENSE.md",
"shims/maven/LICENSE.md",
"shims/nuget/Archgate.Tool/LICENSE.md",
"shims/pypi/LICENSE.md",
"shims/rubygem/LICENSE.md",
];

for (const file of shimLicenses) {
let content: string;
try {
// oxlint-disable-next-line no-await-in-loop -- sequential read is intentional; files are few and order-independent but must check each
content = await ctx.readFile(file);
} catch {
// Shim LICENSE may not exist yet
continue;
}

if (normalize(content) !== expected) {
ctx.report.violation({
message: `${file} is out of sync with the canonical root LICENSE.md`,
file,
fix: `Copy LICENSE.md to ${file} (e.g. \`cp LICENSE.md ${file}\`) so all package licenses stay identical`,
});
}
}
},
},
},
} satisfies RuleSet;
1 change: 1 addition & 0 deletions .claude/agent-memory/archgate-developer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv
- **npm shim + GitHub Releases** — The npm package is a thin shim (`bin/archgate.cjs`). On first run, the shim downloads the platform binary from GitHub Releases and caches it to `~/.archgate/bin/`. No platform-specific npm packages.
- **`.cjs` extension is mandatory** — Root `package.json` has `"type": "module"`. Any Node.js CJS wrapper script placed at the package root MUST use `.cjs`, not `.js`, or Node.js will attempt to parse it as ESM and fail.
- [Shim publishing pipeline gotchas](project_shim_publishing.md) — PyPI README, RubyGem Rakefile/working-dir, Maven waitUntil; build reqs not caught by `archgate check`
- **Each shim needs its own `LICENSE.md` — the repo-root license is invisible to registries** — Registries/indexes detect license from files _inside_ the published package, not the repo root. The Go shim is a subdirectory module (`github.com/archgate/cli/shims/go`); a subdir module's zip only contains files under that subtree, so root `LICENSE.md` is excluded and pkg.go.dev shows "no license" until `shims/go/LICENSE.md` exists. Compounded by `shim-readme-sync`: every shim README is byte-identical to root and carries the root's relative `[Apache-2.0](LICENSE.md)` link/badge, which only resolves with a sibling `LICENSE.md`. Fix applied: byte-identical `LICENSE.md` in all 5 shim dirs (go, maven, nuget/Archgate.Tool, pypi, rubygem); rubygem also adds it to `spec.files`. To register a subdir Go module on pkg.go.dev, hit the proxy: `curl https://proxy.golang.org/<module>/@v/<version>.info`. Captured in ARCH-013 and ENFORCED: the `ARCH-013/shim-license-sync` rule (in the ADR's `.rules.ts`) fails `archgate check` on drift, and `.simple-release.js` `bump()` propagates root `LICENSE.md` to every shim at release — both exactly parallel to the README sync.

## Telemetry Strategy

Expand Down
30 changes: 30 additions & 0 deletions .simple-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ class ArchgateProject extends NpmProject {
}
}
}

// ---------------------------------------------------------------
// Sync shim package LICENSE.md to the canonical root LICENSE.md
//
// The npm package publishes the root LICENSE directly, so it needs
// no copy. Every other ecosystem ships its own copy that must stay
// byte-identical to root (enforced by ARCH-013/shim-license-sync).
// Registries and pkg.go.dev detect the license from files inside
// the package, not from the repository root.
// ---------------------------------------------------------------
const rootLicensePath = "LICENSE.md";
if (existsSync(rootLicensePath)) {
const rootLicense = readFileSync(rootLicensePath, "utf8");
const shimLicensePaths = [
"shims/go/LICENSE.md",
"shims/pypi/LICENSE.md",
"shims/nuget/Archgate.Tool/LICENSE.md",
"shims/rubygem/LICENSE.md",
"shims/maven/LICENSE.md",
];
for (const licensePath of shimLicensePaths) {
const existing = existsSync(licensePath)
? readFileSync(licensePath, "utf8")
: null;
if (existing !== rootLicense) {
writeFileSync(licensePath, rootLicense);
this.changedFiles.push(licensePath);
}
}
}
}

return result;
Expand Down
Loading
Loading