Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f76d47b
feat: add multi-ecosystem shim distribution (PyPI, NuGet, Go, Maven, …
rhuanbarreto May 28, 2026
ff31931
docs: regenerate llms-full.txt
rhuanbarreto May 28, 2026
fd3dc50
chore: update agent memory index
rhuanbarreto May 28, 2026
093608c
chore: rename DIST-001 to ARCH-017, adopt trusted publishing for Ruby…
rhuanbarreto May 28, 2026
f3034f8
docs(i18n): add missing package manager sections to Portuguese instal…
rhuanbarreto May 28, 2026
28fad5c
feat(rules): add i18n-translation-drift rule to detect content drift
rhuanbarreto May 28, 2026
80078f6
fix(rules): resolve no-await-in-loop lint warning in i18n-translation…
rhuanbarreto May 28, 2026
b967bbd
fix(tests): replace dynamic imports with static imports to prevent mo…
rhuanbarreto May 28, 2026
2b7181a
fix(tests): format test files and remove remaining dynamic imports
rhuanbarreto May 28, 2026
cc7accb
fix(tests): extract auth-poll and credential-store-impl to isolate mo…
rhuanbarreto May 28, 2026
fdde73d
fix(tests): use wrapper functions instead of live re-exports to break…
rhuanbarreto May 28, 2026
43ab307
fix(ci): add fetch-depth 0 to shim-tests checkout so origin/main is a…
rhuanbarreto May 28, 2026
056eeaf
feat(ci): add Maven, NuGet, and RubyGem shim tests to PR validation
rhuanbarreto May 28, 2026
10c38db
feat(shims): add unit tests for npm shim and wire into CI
rhuanbarreto May 28, 2026
5cd1db3
fix(shims): add InternalsVisibleTo so NuGet test project can access i…
rhuanbarreto May 28, 2026
855ba72
fix(shims): replace .NET 9-only ToHexStringLower with .NET 8 equivalent
rhuanbarreto May 28, 2026
5383d03
fix(shims): add missing using Xunit directive to NuGet test file
rhuanbarreto May 28, 2026
065d3cf
refactor(ci): split shim tests into independent matrix jobs
rhuanbarreto May 28, 2026
ebc80b6
fix(ci): format code-pull-request.yml with oxfmt
rhuanbarreto May 28, 2026
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
24 changes: 18 additions & 6 deletions .archgate/adrs/ARCH-013-version-synchronization.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: ARCH-013
title: Version Synchronization
domain: architecture
rules: true
files: ["package.json", "docs/**"]
files: ["package.json", "docs/**", "shims/**"]
---

# Version Synchronization
Expand All @@ -14,25 +14,35 @@ The CLI version appears in multiple locations that must stay in sync:

1. `package.json` `version` — canonical source of truth
2. `docs/astro.config.mjs` — `softwareVersion` in the JSON-LD structured data
3. `shims/pypi/pyproject.toml` — PyPI package version
4. `shims/pypi/archgate/_version.py` — Python `__version__` constant
5. `shims/nuget/Archgate.Tool/Archgate.Tool.csproj` — NuGet package version
6. `shims/go/internal/shim/shim.go` — Go `Version` constant
7. `shims/maven/pom.xml` — Maven artifact version
8. `shims/rubygem/lib/archgate/version.rb` — RubyGem `VERSION` constant

When versions diverge, search engines display outdated version info. 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`.
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`.

## 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 `softwareVersion` in `docs/astro.config.mjs` to match `package.json`. This is fully automated and requires no manual intervention.
**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.

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.

## Do's and Don'ts

### Do

- Rely on `.simple-release.js` for `softwareVersion` sync (do not update manually)
- 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

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

## Consequences

Expand All @@ -49,10 +59,12 @@ When versions diverge, search engines display outdated version info. This was di

### Automated Enforcement

- **Release hook** `.simple-release.js`: Syncs `docs/astro.config.mjs` `softwareVersion` during `bump()`. Fully automated.
- **Release hook** `.simple-release.js`: Syncs all version locations 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`.

## References

- [GEN-001 — Documentation Site](./GEN-001-documentation-site.md) — Docs site structure and configuration
- [`.simple-release.js`](../../.simple-release.js) — Release bump hook that syncs softwareVersion
- [ARCH-017 — Multi-Ecosystem Distribution](./ARCH-017-multi-ecosystem-distribution.md) — Shim pattern and behavioral contract
- [`.simple-release.js`](../../.simple-release.js) — Release bump hook that syncs all version locations
70 changes: 70 additions & 0 deletions .archgate/adrs/ARCH-013-version-synchronization.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,75 @@ export default {
}
},
},
"shim-version-sync": {
description: "All shim package versions must match package.json version",
severity: "error",
async check(ctx) {
const pkgJson = await ctx.readJSON("package.json");
if (!pkgJson.version) return;
const expected = pkgJson.version as string;

const shimFiles: Array<{
file: string;
pattern: RegExp;
label: string;
}> = [
{
file: "shims/pypi/pyproject.toml",
pattern: /^version\s*=\s*"([^"]+)"/mu,
label: "PyPI pyproject.toml",
},
{
file: "shims/pypi/archgate/_version.py",
pattern: /__version__\s*=\s*"([^"]+)"/u,
label: "PyPI _version.py",
},
{
file: "shims/nuget/Archgate.Tool/Archgate.Tool.csproj",
pattern: /<Version>([^<]+)<\/Version>/u,
label: "NuGet .csproj",
},
{
file: "shims/go/internal/shim/shim.go",
pattern: /const Version = "([^"]+)"/u,
label: "Go shim.go",
},
{
file: "shims/maven/pom.xml",
pattern:
/<artifactId>archgate-cli<\/artifactId>\s*<version>([^<]+)<\/version>/u,
label: "Maven pom.xml",
},
{
file: "shims/rubygem/lib/archgate/version.rb",
pattern: /VERSION\s*=\s*"([^"]+)"/u,
label: "RubyGem version.rb",
},
];

for (const { file, pattern, label } of shimFiles) {
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 file may not exist yet
continue;
}

const match = content.match(pattern);
if (!match) continue;

const shimVersion = match[1];
if (shimVersion !== expected) {
ctx.report.violation({
message: `${label} version "${shimVersion}" does not match package.json version "${expected}"`,
file,
fix: `Update version to "${expected}" in ${file} (automated by .simple-release.js)`,
});
}
}
},
},
},
} satisfies RuleSet;
84 changes: 84 additions & 0 deletions .archgate/adrs/ARCH-017-multi-ecosystem-distribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
id: ARCH-017
title: Multi-Ecosystem Distribution
domain: distribution
rules: false
---

# Multi-Ecosystem Distribution

## Context

The archgate CLI is a standalone binary compiled with Bun. To maximize reach, it is distributed through multiple package managers (npm, PyPI, NuGet, Go, Maven Central, RubyGems) using a "thin shim" pattern: each package contains a minimal wrapper in the target ecosystem's language that downloads and caches the platform binary from GitHub Releases on first invocation.

## Decision

All distribution shims live under `shims/` in the main repository. Each shim is a self-contained package for its target ecosystem with zero runtime dependencies beyond the ecosystem's own standard library.

### Shared Behavioral Contract

Every shim implements the same algorithm:

1. Detect platform/architecture and map to artifact name (`archgate-darwin-arm64`, `archgate-linux-x64`, `archgate-win32-x64`)
2. Check for cached binary at `~/.archgate/bin/archgate[.exe]`
3. If missing, download from `https://github.com/archgate/cli/releases/download/v{VERSION}/{artifact}.{ext}`
4. Verify SHA256 checksum against the companion `.sha256` file
5. Extract binary with proper permissions (0755 on Unix)
6. Execute the binary, forwarding all arguments and inheriting stdio
7. Propagate the exit code

### Shared Cache

All shim packages share the same cache directory (`~/.archgate/bin/`). If the binary is already cached by any install method (npm, pip, standalone installer, etc.), no download occurs.

### Error Messages

All shims produce identical user-facing error messages on stderr:

- Unsupported platform: `archgate: Unsupported platform: {os}/{arch}\narchgate supports darwin/arm64, linux/x64, and win32/x64.`
- Download failure: `archgate: failed to download binary: {detail}\nVisit https://cli.archgate.dev/getting-started/installation/ for alternative install methods.`
- Checksum mismatch: `archgate: checksum verification failed for v{version} (expected {expected}, got {actual})`
- Download started: `archgate: binary not found, downloading v{version}...`
- Download complete: `archgate: binary downloaded successfully.`

### Version Synchronization

`package.json` `version` is the single source of truth. The `.simple-release.js` bump hook updates all shim version files automatically during the release commit. See ARCH-013 for enforcement details.

## Do's and Don'ts

### Do

- Use only the target ecosystem's standard library (zero runtime dependencies)
- Share the `~/.archgate/bin/` cache directory across all shim packages
- Verify SHA256 checksums before extracting downloaded archives
- Use identical error messages across all shims
- Add new shim version files to `.simple-release.js` and the ARCH-013 companion rules

### Don't

- Don't bundle the compiled binary into any shim package (download on demand)
- Don't add runtime dependencies to any shim package
- Don't use a different cache location per ecosystem
- Don't skip SHA256 verification

## Consequences

### Positive

- Users can install archgate through their preferred package manager without requiring Node.js or Bun
- All install methods converge on the same cached binary, avoiding duplicate downloads
- Thin packages are fast to install and have minimal footprint in each registry
- Version synchronization is automated via the release hook

### Negative

- First-run latency: the binary must be downloaded on the first invocation after install
- Multiple codebases to maintain (one per ecosystem), though the logic is simple and rarely changes
- Network dependency on GitHub Releases for the initial download

## References

- [ARCH-013 -- Version Synchronization](./ARCH-013-version-synchronization.md) -- Enforces version parity across all shim packages
- [CI-001 -- Pin GitHub Actions by Commit SHA](./CI-001-pin-github-actions-by-hash.md) -- SHA pinning for the publish-shims workflow
- [`.simple-release.js`](../../.simple-release.js) -- Release bump hook that syncs all shim versions
4 changes: 3 additions & 1 deletion .archgate/adrs/GEN-002-docs-i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ The sidebar in `docs/astro.config.mjs` does NOT need per-locale duplication. Sta

### Automated Enforcement

The companion rules file (`GEN-002-docs-i18n.rules.ts`) defines one rule:
The companion rules file (`GEN-002-docs-i18n.rules.ts`) defines three rules:

- **`i18n-page-parity`** (severity: `error`) -- Verifies that every root MDX file has a corresponding translation in each configured locale directory, and that no orphan translations exist without a root source file. Runs as part of `archgate check`.
- **`i18n-translation-drift`** (severity: `error`) -- When running against a changeset (PR, staged files), verifies that if an English docs file was modified, the corresponding locale file was also modified. Catches content drift within existing files that page-parity alone would miss.
- **`no-locale-prefix-in-links`** (severity: `error`) -- Verifies that locale pages do not use locale-prefixed internal links (e.g., `/pt-br/guides/...`). Starlight resolves locale routes automatically.

### Manual Enforcement

Expand Down
48 changes: 48 additions & 0 deletions .archgate/adrs/GEN-002-docs-i18n.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,53 @@ export default {
}
},
},
"i18n-translation-drift": {
description:
"When an English docs file is modified, the corresponding locale file must also be modified in the same changeset",
severity: "error",
async check(ctx) {
// Only meaningful when running against a changeset (PR, staged, etc.)
if (ctx.changedFiles.length === 0) return;

const changedSet = new Set(ctx.changedFiles);
const rootPrefix = `${CONTENT_ROOT}/`;

// Find changed root (English) MDX files that aren't inside a locale dir
const changedRootFiles = ctx.changedFiles.filter(
(f) =>
f.startsWith(rootPrefix) &&
f.endsWith(".mdx") &&
!LOCALES.some((l) => f.startsWith(`${rootPrefix}${l}/`))
);

if (changedRootFiles.length === 0) return;

// Pre-build a set of all existing locale files for fast lookup
const localeFileArrays = await Promise.all(
LOCALES.map((locale) =>
ctx.glob(`${CONTENT_ROOT}/${locale}/**/*.mdx`)
)
);
const allLocaleFiles = new Set<string>(localeFileArrays.flat());

for (const rootFile of changedRootFiles) {
const relativePath = rootFile.replace(rootPrefix, "");

for (const locale of LOCALES) {
const localePath = `${CONTENT_ROOT}/${locale}/${relativePath}`;

// Only flag if the locale file exists but wasn't changed.
// Missing locale files are already caught by i18n-page-parity.
if (allLocaleFiles.has(localePath) && !changedSet.has(localePath)) {
ctx.report.violation({
message: `English file "${relativePath}" was modified but the ${locale} translation was not updated`,
file: localePath,
fix: `Update ${localePath} to reflect the changes in ${rootFile}`,
});
}
}
}
},
},
},
} 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 @@ -61,6 +61,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv
- **macOS `/var` → `/private/var` symlink breaks temp dir path comparisons in tests** — On macOS, `/var` is a symlink to `/private/var`. `mkdtempSync(join(tmpdir(), ...))` returns `/var/folders/...` but `process.cwd()` after `chdir()` resolves the symlink to `/private/var/folders/...`. Tests that compare `tempDir` against paths derived from `process.cwd()` or `findProjectRoot()` will fail. Fix: always wrap `mkdtempSync` with `realpathSync` in test setup: `tempDir = realpathSync(mkdtempSync(join(tmpdir(), "archgate-test-")))`. This normalizes the path upfront. Discovered in v0.38.0/v0.39.0 release builds — PR CI runs on ubuntu-latest only, so macOS-specific issues are invisible until the release workflow.
- **Always use `bun run test`, never bare `bun test`, in CI workflows** — The package.json `test` script includes `--timeout 60000`, but bare `bun test` uses Bun's default 5000ms timeout. Tests that perform filesystem operations or spawn subprocesses (e.g., session-context tests) can exceed 5s on slow CI runners. The `release-binaries.yml` Windows step originally used `bun test` and hit timeout failures. Same principle as "never use `bunx prettier` directly" — always prefer `bun run <script>` to pick up script-level flags.
- **Don't test that well-known tools exist on PATH** — Tests like `expect(resolveCommand("bun")).toBe("bun")` assert CI environment state, not application logic. They fail when the runner installs tools via shims (e.g., proto on macOS ARM64 where `Bun.which` returns null). Delete such tests entirely — the "returns null for non-existent command" tests already cover `resolveCommand`'s actual logic, and WSL-specific tests cover the `.exe` fallback path.
- **`mock.module` + `require()` of the same module is unreliable on macOS ARM64** — Using `require("../path/to/module")` inside a `mock.module("../path/to/module", factory)` factory to spread the real exports is unreliable on darwin-arm64. The `require()` can return a circular/empty object instead of the real module, causing the spread to produce an incomplete mock (missing functions). Fix: provide explicit inline implementations of all needed functions in the mock factory. See `sync.test.ts` for the reliable pattern (all exports declared explicitly). Applied in `tests/commands/adr/import.test.ts` — PR [#355](https://github.com/archgate/cli/pull/355).

## Validation Pipeline

Expand Down
Loading
Loading