redline sync
Lands the current standards on every registered repository as a pull request, from the register derived nightly off the estate.
What this is
Lands the current standards on every registered repository as a pull request, quoting the version it came from. Targets come from registry.json, the register derived nightly from the estate — nobody maintains a list.
How to onboard it
Nothing to install in a product repository: sync runs in the Redline source repo, on a push to standards/ and on demand. It needs REDLINE_SYNC_TOKEN with contents:write, pull_requests:write and workflows:write on every target — without the workflows scope the push of .github/workflows/redline.yml is rejected and the whole pull request fails.
How to use it
$ npx redlinegate@latest sync --dry-run # print the plan; pushes nothing, opens nothing $ npx redlinegate@latest sync # open a pull request on every repo that is behind $ npx redlinegate@latest sync --repo acme/web-app # one repository $ npx redlinegate@latest sync --force # re-render a repo already at the current version
The flags that change behaviour materially:
--dry-run— Plans and renders but never pushes a branch or opens a pull request. Unlike redline init --dry-run it still reads from the host — it has to fetch each target's .redline.json and current artifacts to know what would change — so it needs a read credential.--repo <owner/name>— One repository instead of the estate. Every other registered repository is reported as skipped with the reason, so a narrowed run still shows you the whole picture.--force— Re-renders a repository already recording the current standards version. For a renderer change that alters output without moving the standards version — otherwise nothing would be behind and nothing would sync.
Expected output
One pull request per target that is behind, titled with the standards version and listing the generated files it changes. A repository whose artifacts already match gets nothing — no branch, no empty pull request. A target with an unmerged sync pull request already open has its branch updated and that pull request's body refreshed, never a second one opened. One unreachable repository is reported and the rest of the estate still syncs, but the run exits non-zero, because a distribution that reports success while missing repositories is how coverage silently rots.
How to edit it
cli/sync/ — plan.ts decides who is behind, render.ts produces each target's artifacts, run.ts drives the estate. The host calls live in cli/platforms/github/push.ts.
Run npm test and npm run typecheck before pushing: the command surface is unit-tested against a fake host client, so a behaviour change shows up as a failing assertion rather than as a surprise on someone's repository.
The full file
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { RedlineError } from '../core/errors.ts';
import { parseRegistry } from '../registry/serialize.ts';
import type { Registry } from '../registry/types.ts';
import { runSync, type SyncHost, type SyncReport } from '../sync/run.ts';
export interface SyncOptions {
// Where standards/ and the manifest live. These ship inside the package, so
// this is the package root — the same root every other command renders from.
root: string;
// Where registry.json lives: the checkout of the source repository sync is
// being run from. It is a source-repo artifact committed by the registry
// workflow, not something that ships in the published package, so it is found
// relative to the working directory rather than the package.
cwd: string;
repo?: string;
force?: boolean;
dryRun?: boolean;
// Overridable for tests; the register is a file in the source repo.
registryPath?: string;
}
export const REGISTRY_FILE = 'registry.json';
export function readRegistry(cwd: string, path = REGISTRY_FILE): Registry {
let raw: string;
try {
// resolve, not join: an absolute override has to stay absolute, and join
// would silently nest it under cwd and report the register as missing.
raw = readFileSync(resolve(cwd, path), 'utf8');
} catch {
throw new RedlineError(
'usage',
`no ${path} in ${cwd} — the register of onboarded repositories has not been derived yet`,
'run the Redline Registry workflow, or: node scripts/build-registry.mjs'
);
}
return parseRegistry(raw);
}
export function standardsVersion(root: string): string {
const manifest = JSON.parse(readFileSync(resolve(root, 'standards/manifest.json'), 'utf8')) as {
version?: unknown;
};
if (typeof manifest.version !== 'string') {
throw new RedlineError('failed', 'standards/manifest.json has no string "version"');
}
return manifest.version;
}
// Distribute the current standards to every registered repository that is behind.
//
// It opens pull requests and never merges them, and it never pushes to a default
// branch. A repository that ignores its sync pull request drifts, and the estate
// dashboard's coverage figure is what makes that visible — sync's job is to make
// the change available, not to impose it.
export async function sync(host: SyncHost, opts: SyncOptions): Promise<SyncReport> {
const registry = readRegistry(opts.cwd, opts.registryPath);
return runSync(host, registry, {
root: opts.root,
standardsVersion: standardsVersion(opts.root),
...(opts.repo ? { repo: opts.repo } : {}),
...(opts.force ? { force: true } : {}),
...(opts.dryRun ? { dryRun: true } : {}),
});
}