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
42 changes: 33 additions & 9 deletions .archgate/adrs/ARCH-013-version-synchronization.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,69 @@ The CLI version appears in multiple locations that must stay in sync:

When versions diverge, users installing via different package managers get mismatched binaries. This was discovered during a consistency review where `package.json` was at `0.16.0` but `docs/astro.config.mjs` was still at `0.11.0`.

Version strings are not the only artifact that must stay synchronized across shim packages. Each ecosystem renders a README on its registry page (PyPI, NuGet.org, pkg.go.dev) or ships one inside the published artifact, and users comparing packages across registries expect the same description everywhere. The canonical README is the repository root `README.md` — the exact file the npm package already publishes via the `readme` field in `package.json`. The other ecosystems each carry their own copy that must mirror root:

1. `shims/go/README.md` — rendered by pkg.go.dev for the Go module
2. `shims/pypi/README.md` — referenced by `pyproject.toml` `readme = "README.md"`
3. `shims/nuget/Archgate.Tool/README.md` — declared via `<PackageReadmeFile>` and packed into the `.nupkg`
4. `shims/rubygem/README.md` — included in the gem via the gemspec `spec.files` glob
5. `shims/maven/README.md` — for source/repository readers (Maven Central renders the pom `<description>`, not a README file)

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).

## 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 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` 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.

## Do's and Don'ts

### Do

- Rely on `.simple-release.js` for all version sync (do not update manually)
- Use the companion rules to catch version drift in CI as a safety net
- When adding a new shim ecosystem, add its version file to `.simple-release.js` and the companion rules
- **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

### 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** 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

## Consequences

### Positive

- Consistent version information across user-facing surfaces
- CI catches version drift before it reaches production
- 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

### Negative

- None — all version sync is automated via the release hook
- **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.

### 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.

## Compliance and Enforcement

### Automated Enforcement

- **Release hook** `.simple-release.js`: Syncs all version locations during `bump()`. Fully automated.
- **Release hook** `.simple-release.js`: Syncs all version locations and propagates the root `README.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.

## References

Expand Down
50 changes: 50 additions & 0 deletions .archgate/adrs/ARCH-013-version-synchronization.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,55 @@ export default {
}
},
},
"shim-readme-sync": {
description:
"All shim package READMEs must be byte-identical to the canonical root README.md",
severity: "error",
async check(ctx) {
let canonical: string;
try {
canonical = await ctx.readFile("README.md");
} catch {
// Root README.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 README.md directly (package.json
// "readme" + npm always-include), so it is excluded here. Every other
// shim ecosystem ships its own copy that must mirror the root.
const shimReadmes: string[] = [
"shims/go/README.md",
"shims/maven/README.md",
"shims/nuget/Archgate.Tool/README.md",
"shims/pypi/README.md",
"shims/rubygem/README.md",
];

for (const file of shimReadmes) {
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 README may not exist yet
continue;
}

if (normalize(content) !== expected) {
ctx.report.violation({
message: `${file} is out of sync with the canonical root README.md`,
file,
fix: `Copy README.md to ${file} (e.g. \`cp README.md ${file}\`) so all package READMEs 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 @@ -77,6 +77,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`

## Telemetry Strategy

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
name: shim-publishing-pipeline
description: Build/publish requirements for the multi-ecosystem shims in publish-shims.yml (pypi, rubygem, maven) — non-obvious gotchas that broke the first v0.40.0 release
metadata:
type: project
---

The `publish-shims.yml` workflow publishes thin shim packages to PyPI, RubyGems, Maven Central, NuGet, and Go. Added in [#356](https://github.com/archgate/cli/pull/356); first ran at v0.40.0 ([run 26601268709](https://github.com/archgate/cli/actions/runs/26601268709)) where pypi/rubygem/maven all failed. Each ecosystem has build-time requirements NOT covered by the `shim-version-sync` ADR rule (ARCH-013), which only checks version strings.

**Why:** These are external-tool config requirements (hatchling, bundler, Sonatype central-publishing plugin) that no archgate lint rule enforces — so a green `archgate check` does NOT mean the shims will publish.

**How to apply** — when editing any shim under `shims/` or `publish-shims.yml`:

- **PyPI** (`shims/pypi/`): `pyproject.toml` declares `readme = "README.md"`, so `shims/pypi/README.md` MUST exist or `python -m build` fails with `OSError: Readme file does not exist`. If you change the `readme =` key, keep the referenced file present.
- **RubyGem** (`shims/rubygem/`): `rubygems/release-gem` runs `bundle exec rake release` from its `working-directory` input. Requires (1) `working-directory: shims/rubygem` on BOTH `ruby/setup-ruby` (with `bundler-cache: true` so `bundle install` runs) and `rubygems/release-gem`; and (2) a `shims/rubygem/Rakefile` containing `require "bundler/gem_tasks"` to provide the `release` task. Without the working-directory, bundler errors `Could not locate Gemfile`. Do NOT commit a `Gemfile.lock` — bundler-cache generates it untracked, which keeps `release:guard_clean` happy (an untracked lock isn't seen by `git diff --exit-code`).
- **Maven** (`shims/maven/pom.xml`): the `central-publishing-maven-plugin` with `<waitUntil>published</waitUntil>` blocks the build until Sonatype fully publishes, which routinely exceeds the 15-min job timeout (upload itself succeeds — you'll see `Uploaded bundle successfully` then a hang on `Waiting until Deployment ... is published`). Use `<waitUntil>validated</waitUntil>` with `<autoPublish>true</autoPublish>` so the build returns after validation and publishing finishes async.

**Re-running after a partial failure:** the jobs are not idempotent across a full workflow re-run. `publish-go-tag` (creates a git tag), `publish-nuget`, and a maven deploy that already uploaded will fail on "already exists" the second time. After fixing, prefer applying to the next version bump, or `workflow_dispatch` only the previously-failed ecosystems. Note v0.40.0's maven bundle DID upload (deploymentId d7671fb1) before the timeout, so it may already be published on Central.
4 changes: 4 additions & 0 deletions .github/workflows/publish-shims.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,8 @@ jobs:
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
with:
ruby-version: "3.4.9"
bundler-cache: true
working-directory: shims/rubygem
- uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
with:
working-directory: shims/rubygem
28 changes: 28 additions & 0 deletions .simple-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ class ArchgateProject extends NpmProject {
this.changedFiles.push(rubyVersionPath);
}
}

// ---------------------------------------------------------------
// Sync shim package READMEs to the canonical root README.md
//
// The npm package publishes the root README 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-readme-sync).
// ---------------------------------------------------------------
const rootReadmePath = "README.md";
if (existsSync(rootReadmePath)) {
const rootReadme = readFileSync(rootReadmePath, "utf8");
const shimReadmePaths = [
"shims/go/README.md",
"shims/pypi/README.md",
"shims/nuget/Archgate.Tool/README.md",
"shims/rubygem/README.md",
"shims/maven/README.md",
];
for (const readmePath of shimReadmePaths) {
const existing = existsSync(readmePath)
? readFileSync(readmePath, "utf8")
: null;
if (existing !== rootReadme) {
writeFileSync(readmePath, rootReadme);
this.changedFiles.push(readmePath);
}
}
}
}

return result;
Expand Down
127 changes: 127 additions & 0 deletions shims/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Archgate

<div align="center">

**Enforce Architecture Decision Records as executable rules — for both humans and AI agents.**

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE.md)
[![Release](https://github.com/archgate/cli/actions/workflows/release.yml/badge.svg)](https://github.com/archgate/cli/actions/workflows/release.yml)
[![Docs](https://img.shields.io/badge/docs-cli.archgate.dev-blue)](https://cli.archgate.dev)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12659/badge)](https://www.bestpractices.dev/projects/12659)

</div>

---

Archgate turns your Architecture Decision Records into a governance layer that runs in CI, enforces rules in pre-commit hooks, and feeds live context to AI coding agents — so architectural decisions don't stay in documents, they stay in the code.

**Write an ADR once. Enforce it everywhere.**

## How it works

Archgate has two layers:

1. **ADRs as documents** — markdown files with YAML frontmatter stored in `.archgate/adrs/`. Each ADR records a decision: what was decided, why, and what to do and not do.
2. **ADRs as rules** — each ADR can have a companion `.rules.ts` file that exports automated checks. Archgate runs these checks against your codebase and reports violations.

```
.archgate/
└── adrs/
├── ARCH-001-command-structure.md # human-readable decision
├── ARCH-001-command-structure.rules.ts # machine-executable checks
├── ARCH-002-error-handling.md
└── ARCH-002-error-handling.rules.ts
```

When a rule is violated, `archgate check` reports the file, line, and which ADR was broken. Exit code 1 means violations — wire it into CI and it blocks merges automatically.

**The CLI is free and open source.** Writing ADRs, enforcing rules, running checks in CI, and wiring up pre-commit hooks all work without an account or subscription.

## Installation

**Standalone** (no Node.js required):

```bash
# macOS / Linux
curl -fsSL https://cli.archgate.dev/install-unix | sh

# Windows (PowerShell)
irm https://cli.archgate.dev/install-windows | iex

# Windows (Git Bash / MSYS2)
curl -fsSL https://cli.archgate.dev/install-unix | sh
```

**Via npm** (or any Node.js package manager):

```bash
# npm
npm install -g archgate

# Bun
bun install -g archgate

# Yarn
yarn global add archgate

# pnpm
pnpm add -g archgate
```

You can also install as a dev dependency:

```bash
npm install -D archgate # or: bun add -d archgate
npx archgate check # run via package manager
```

**Requirements:** macOS (arm64), Linux (x86_64), or Windows (x86_64). See the [installation guide](https://cli.archgate.dev/getting-started/installation/) for more options.

## Quick start

```bash
# 1. Install
curl -fsSL https://cli.archgate.dev/install-unix | sh # or: npm install -g archgate

# 2. Initialize governance in your project
cd my-project
archgate init

# 3. Edit the generated ADR to document a real decision
# .archgate/adrs/ARCH-001-*.md

# 4. Add a companion .rules.ts to enforce it automatically
# .archgate/adrs/ARCH-001-*.rules.ts

# 5. Run checks
archgate check
```

## Writing rules

Each ADR can have a companion `.rules.ts` file that exports automated checks. See the [writing rules guide](https://cli.archgate.dev/guides/writing-rules/) for examples and the full [rule API reference](https://cli.archgate.dev/reference/rule-api/).

## Supercharge with AI plugins

> **Make your AI agent architecture-aware.** With the optional editor plugins, your AI coding agent reads ADRs before writing code, validates changes against your rules, and captures new architectural patterns back into ADRs — automatically.
>
> Plugins are available for [**Claude Code**](https://cli.archgate.dev/guides/claude-code-plugin/) and [**Cursor**](https://cli.archgate.dev/guides/cursor-integration/).
>
> ```bash
> archgate login # one-time GitHub auth
> archgate init # installs the plugin automatically
> ```
>
> **[Get started with plugins](https://cli.archgate.dev/guides/claude-code-plugin/)** — the CLI works fully without them, but plugins close the loop between decisions and code.

## Documentation

Full documentation is available at **[cli.archgate.dev](https://cli.archgate.dev)** — including guides for writing ADRs, writing rules, CI integration, editor plugin setup, and the complete CLI reference.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and workflow.

## License

[Apache-2.0](LICENSE.md)
Loading
Loading