/** * Workflow upgrade primitive — `cleo upgrade workflows`. * * Phase 4 of T9497 (parent epic) and T9536. Re-renders every shipped * workflow template against current `.cleo/release-config.json` + * ADR-061 tool-resolver state, compares the rendered YAML against the * existing `.github/workflows/release-*.yml` files, and reports a * per-template `UpgradeWorkflowOutcome` envelope. An optional * `.workflow-overrides.yml` file (if present at the project root) * carries operator-declared customizations the upgrade MUST respect so * a re-render does not silently clobber hand-tuned env vars or step * additions. * * The first iteration of the 3-way merge is intentionally SIMPLE: * * - If `rendered === existing` ⇒ `unchanged`. * - If `existing === null` ⇒ `missing` (drift; the user * needs to re-run * `cleo init --workflows`). * - If `rendered !== existing` AND an override key matches the * template name in `.workflow-overrides.yml` ⇒ `override-kept` * (we report drift but * do NOT recommend * overwrite — the operator * owns the file). * - If `rendered !== existing` AND no override applies ⇒ `drift-detected` * (caller decides * `force` or `skip`). * * Full deep-merge of override paths into the rendered template is * deferred — the v1 contract is drift-detection plus a copy of the * rendered output for diff display. The caller (`cleo upgrade * workflows`) consumes this envelope to: * * - Print a per-file status table. * - With `--dry-run`: emit the diff and exit 0. * - With `--check`: exit 0 if every outcome is `unchanged` / * `override-kept`; exit 1 otherwise. * - With `--force`: re-write the file in-place (audit-logged). * * Writes are atomic via tmp-then-rename — exactly the convention * {@link scaffoldWorkflows} already uses (DRY across the init/upgrade * pair). * * @module init/upgrade-workflows * @task T9536 * @epic T9497 * @adr ADR-061 * @adr ADR-065 */ import type { ResolvedToolPlaceholders, ScaffoldReleaseConfig, WorkflowName } from './scaffold-workflows.js'; /** * Per-template upgrade outcome status. * * - `unchanged` — rendered output matches the file already on disk. * - `missing` — `.github/workflows/.yml` is absent. The * operator likely never ran `cleo init --workflows` * (or deleted the file). Run init to bootstrap. * - `override-kept` — content drift detected, BUT a key matching the * template name exists in `.workflow-overrides.yml`. * The drift is operator-declared; the on-disk file * is preserved verbatim. * - `drift-detected` — content drift detected and NO override applies. * Without `force=true`, the file is preserved * verbatim (caller decides next step). * - `updated` — `force=true` was passed and the file was * overwritten with the rendered output. An audit * row landed in `.cleo/audit/upgrade-workflows.jsonl`. * - `dry-run` — `dryRun=true`; the rendered YAML is in the * envelope but no write occurred. */ export type UpgradeWorkflowStatus = 'unchanged' | 'missing' | 'override-kept' | 'drift-detected' | 'updated' | 'dry-run'; /** * Per-template upgrade outcome. * * `rendered` always carries the freshly rendered YAML so the CLI can * surface a diff against `existing` (when present) regardless of * `status`. `existing` is `null` only when `status === 'missing'`. */ export interface UpgradeWorkflowOutcome { /** Which template was re-rendered. */ template: WorkflowName; /** Absolute path to the workflow file on disk. */ targetPath: string; /** Freshly rendered YAML (output of {@link scaffoldWorkflows}-style render). */ rendered: string; /** The on-disk file at `targetPath`, or `null` when the file is missing. */ existing: string | null; /** Disposition — see {@link UpgradeWorkflowStatus} for semantics. */ status: UpgradeWorkflowStatus; /** * `true` iff `.workflow-overrides.yml` carries a top-level key that * matches `template`. Surfaced in the envelope so the CLI can show * "operator override declared" in the status table even when * `status === 'unchanged'`. */ overrideDeclared: boolean; } /** * Options for {@link upgradeWorkflows}. */ export interface UpgradeWorkflowsOptions { /** * Absolute path to the consuming project's root. */ projectRoot: string; /** * Absolute path to the directory containing the `*.yml.tmpl` files — * usually the `templates/workflows/` directory of an installed * `@cleocode/core` package. Exposed as an option so tests can point at * a fixture tree. */ templatesDir: string; /** * Which templates to inspect. Defaults to the full four-template set * declared in {@link DEFAULT_WORKFLOW_TEMPLATES}. */ templates?: ReadonlyArray; /** * `true` ⇒ overwrite `.github/workflows/.yml` even when content * drifts. Audited to `.cleo/audit/upgrade-workflows.jsonl`. Ignored * when `dryRun=true`. */ force?: boolean; /** * `true` ⇒ skip the actual write. Every outcome reports `dry-run` * regardless of drift; the rendered YAML stays in the envelope so the * CLI can show a diff. */ dryRun?: boolean; /** * Override the loaded release config — primarily for unit tests. * When omitted the helper reads `/.cleo/release-config.json`. */ releaseConfigOverride?: ScaffoldReleaseConfig; /** * Override the parsed `.workflow-overrides.yml` body — primarily for * unit tests. When omitted the helper reads * `/.workflow-overrides.yml` (best-effort; parse failure * is treated as "no overrides declared"). */ overridesOverride?: WorkflowOverrides; } /** * Top-level shape of `.workflow-overrides.yml`. Each top-level key * names one of the four canonical templates; the value is treated as an * opaque "operator-declared customization" — the v1 contract only * checks for KEY presence, not value structure. */ export type WorkflowOverrides = Partial>; /** * Result envelope returned by {@link upgradeWorkflows}. */ export interface UpgradeWorkflowsResult { /** Per-template outcomes in the order they were inspected. */ outcomes: UpgradeWorkflowOutcome[]; /** The placeholder set used by the substitution pass. */ resolvedTools: ResolvedToolPlaceholders; /** * `true` iff at least one outcome reports `drift-detected` or * `missing`. Designed to drive the `--check` exit-code contract: * `cleo upgrade workflows --check` exits 1 when this is `true` and * 0 otherwise. */ hasDrift: boolean; } /** * Parse the body of `.workflow-overrides.yml` and return a * {@link WorkflowOverrides} record keyed by template name. The parser * is deliberately minimal — it scans for top-level keys matching the * four canonical workflow names and records `true` for each. Nested * structure is ignored (the v1 contract only checks for KEY presence). * * Exposed for testability — callers should generally use * {@link loadOverridesYaml}. * * @internal */ export declare function parseOverridesYamlBody(body: string): WorkflowOverrides; /** * Re-render every shipped workflow template and report drift against * the on-disk `.github/workflows/release-*.yml` files. Honours * `.workflow-overrides.yml` for operator-declared customizations. * * See the module docstring for the full 3-way merge contract. * * @example * ```ts * // --check semantics — exit 1 when any drift is detected. * const result = await upgradeWorkflows({ * projectRoot: '/path/to/my-project', * templatesDir: '/path/to/@cleocode/core/templates/workflows', * }); * if (result.hasDrift) process.exit(1); * ``` * * @task T9536 */ export declare function upgradeWorkflows(opts: UpgradeWorkflowsOptions): Promise; //# sourceMappingURL=upgrade-workflows.d.ts.map