/** * `cleo release open` — Phase 3 verb of the new release pipeline. * * Consumes the plan file at `.cleo/release/.plan.json` (written by * {@link releasePlan} in T9525) and dispatches the `release-prepare.yml` * GitHub Actions workflow via `gh workflow run`. UPDATEs the `releases` * row's `status` to `pr-opened` and persists the resolved workflow run URL * into `releases.workflow_run_url`. * * This verb is **side-effectful** — it shells out to `gh` and writes one * row to the `releases` table. It does NOT push commits, mutate any * source files, or invoke npm/cargo publish. * * Implements SPEC-T9345 §4.3 (R-050 through R-071): * * - R-050 .. R-053 — pre-condition gates (plan exists, releases status, * gh auth, workflow file). * - R-060 .. R-062 — side effects (gh workflow run + DB update; optional * plan-commit when `--commit-plan` is supplied). * - R-070 .. R-071 — post-conditions (status='pr-opened', workflow_run_url * is a valid gh run URL). * * All `gh` and `git` subprocesses run with a 60s timeout per task rules. * * @task T9530 * @epic T9494 * @adr ADR-T9345 * @spec .cleo/rcasd/T9345/research/SPEC-T9345-release-pipeline-v2.md §4.3 */ import type { EngineResult } from '@cleocode/contracts'; import { type PreflightSkipDecision } from './preflight-skip.js'; /** Default workflow file name dispatched by `cleo release open`. */ export declare const DEFAULT_OPEN_WORKFLOW: "release-prepare.yml"; /** * Options accepted by {@link releaseOpen}. * * @task T9530 */ export interface ReleaseOpenOptions { /** The release version, e.g. `v2026.6.0`. Required positional input. */ version: string; /** Workflow file name to dispatch. Defaults to `release-prepare.yml`. */ workflow?: string; /** When true, poll `gh run watch` until the run reaches a terminal state. */ watch?: boolean; /** * When true, commit the plan file to the active branch before dispatching. * * T12092: the claim this doc used to carry — "the workflow can re-derive the * plan from `releases` + tasks.db" — is false. `.cleo/cleo.db` is gitignored * (ADR-013 §9), so a runner has no task store and `cleo release plan` cannot * run there. Committing the plan is the only way a task- or epic-scoped * release can be planned. * * gh#1375: committing is necessary and NOT sufficient. The plan has to reach * the branch `workflow_dispatch` checks out, and this module never pushes — * so `releaseOpen` verifies the plan's presence and content on that remote * ref and refuses before dispatching rather than after a full preflight. */ commitPlan?: boolean; /** * Epic task ID forwarded to the workflow's `epic` input (T12089). * * The workflow regenerates the plan when no `plan-blob-sha256` is supplied, * and `cleo release plan` REQUIRES a scope — so without this (or * {@link tasks}) the dispatched run dies at "Prepare bump-PR" after a full * green preflight. */ epic?: string; /** * Comma-separated task IDs forwarded to the workflow's `tasks` input (T12089). * * Use for a task-scoped release, where an epic would drag in sibling tasks * that legitimately have no evidence yet and fail the plan with * `E_EVIDENCE_INSUFFICIENT`. */ tasks?: string; /** * T13825: dispatch `release-prepare` on this branch instead of the default * branch — the single release PR's `release/plan-` branch, which * holds the plan and CHANGELOG.md in one commit on top of the default * branch's HEAD. The workflow cuts `release/` from it, so the bump * PR carries the plan too. Implies no local plan commit; the plan must * already be on `origin/` with the local plan's exact bytes. Preflight * skips carry over only when the branch adds release-plan files alone * ({@link retargetPreflightSkips}). */ ref?: string; /** * Project root override. Defaults to the canonical project root resolved * via {@link getProjectRoot} (walks up from `process.cwd()` for monorepo * subdir invocations; honours `CLEO_ROOT` / `CLEO_PROJECT_ROOT`). * * @task T9583 */ projectRoot?: string; } /** * Data payload returned by {@link releaseOpen} on success. * * @task T9530 */ export interface ReleaseOpenResult { /** The version dispatched (matches input). */ version: string; /** The GitHub Actions run URL recorded into `releases.workflow_run_url`. */ workflowRunUrl: string; /** True iff `--watch` was supplied (caller waited for the run). */ watching: boolean; /** True iff this invocation was a no-op because status was already `pr-opened`. */ idempotent?: boolean; /** * The run already recorded for this version, when there was one (T14077): * its status and conclusion as GitHub reports them, and whether this call * dispatched a new run because it failed or was cancelled. */ priorRun?: PriorReleaseRun; /** * Plan file sha256. Forwarded as the workflow's `plan-blob-sha256` input * once the same bytes are verified on the dispatch branch (T12092, T13050); * a dispatch never goes out without it. */ planBlobSha256: string; /** * Which `release-prepare` preflight test suites this dispatch asked the * workflow to skip, the commit that decision was verified against, and why. * Absent on an idempotent no-op (nothing was dispatched). */ preflight?: PreflightSkipDecision; } /** * The release-prepare run already recorded for a version (T14077). * * @task T14077 */ export interface PriorReleaseRun { /** The recorded `workflow_run_url`. */ url: string; /** `queued`, `in_progress`, `completed`, …; `null` when GitHub could not be asked. */ status: string | null; /** `success`, `failure`, `cancelled`, …; `null` while running or when unknown. */ conclusion: string | null; /** Whether this call dispatched a new run because the recorded one failed or was cancelled. */ redispatched: boolean; } /** * Internal handle to subprocess runners. Public via {@link __test__} so unit * tests can swap out the real `gh` / `git` callers without `vi.mock` plumbing. * * @internal */ export interface ReleaseOpenRunner { /** * Run `gh ` and return trimmed stdout. Throws on non-zero exit or when * `timeoutMs` (default 60s) elapses. */ runGh: (args: readonly string[], cwd: string, timeoutMs?: number) => string; /** Test whether `gh auth status` exits 0. Returns the boolean directly. */ checkGhAuth: (cwd: string) => boolean; } /** * Default runner — invokes the real `gh` binary on PATH with a 60s timeout. * * @internal */ declare function makeDefaultRunner(): ReleaseOpenRunner; /** * R-050: load and validate the plan file at * `.cleo/release/.plan.json`. * * Returns the raw JSON body (for sha256 hashing) AND the parsed plan, or an * `E_PLAN_NOT_FOUND` / `E_RELEASE_PLAN_INVALID` envelope on failure. * * @internal */ declare function loadPlanForOpen(version: string, projectRoot: string): EngineResult<{ rawBody: string; planPath: string; }>; /** * R-053: assert the workflow file exists under `.github/workflows/`. * * @internal */ declare function assertWorkflowFile(workflow: string, projectRoot: string): EngineResult<{ workflowPath: string; }>; /** * Resolve the most recent workflow run URL for the given workflow file after * dispatch. Returns the URL string or `null` if `gh run list` yields nothing. * * Polls up to {@link maxAttempts} times because `gh workflow run` returns * BEFORE the run is registered in `gh run list` (the API has a small lag). * * @internal */ declare function resolveLatestRunUrl(workflow: string, cwd: string, runner: ReleaseOpenRunner, maxAttempts?: number): string | null; /** * UPDATE the `releases` row to `status='pr-opened'` and persist the * resolved workflow run URL (R-061 + R-070 + R-071). * * @internal */ declare function updateReleaseRow(version: string, workflowRunUrl: string, projectRoot: string): Promise; /** * Read the current status + workflow_run_url of the releases row for a version. * * @internal */ declare function readReleaseStatus(version: string, projectRoot: string): Promise<{ status: string; workflowRunUrl: string | null; } | null>; declare function commitPlanFile(planPath: string, version: string, projectRoot: string): void; /** * Dispatch the `release-prepare.yml` workflow for the given version and * UPDATE the `releases` row to `status='pr-opened'`. * * Idempotency: if the row is ALREADY at `status='pr-opened'` AND has a * non-null `workflow_run_url`, the verb is a no-op modulo `meta.idempotent=true` * (returns the existing URL without re-dispatching) while that run is queued, * running or succeeded. When the recorded run finished with `failure`, * `cancelled`, `timed_out` or `startup_failure`, it dispatches again (T14077). * Either way `priorRun` reports the recorded run's status and conclusion. * * @example * ```ts * const result = await releaseOpen({ version: 'v2026.6.0' }); * if (result.success) { * console.log(`Dispatched: ${result.data.workflowRunUrl}`); * } * ``` * * @task T9530 */ export declare function releaseOpen(opts: ReleaseOpenOptions, runnerOverride?: ReleaseOpenRunner): Promise>; /** * Internal helpers exposed for unit testing. NOT part of the public API. * * @internal */ export declare const __test__: { assertWorkflowFile: typeof assertWorkflowFile; commitPlanFile: typeof commitPlanFile; loadPlanForOpen: typeof loadPlanForOpen; makeDefaultRunner: typeof makeDefaultRunner; readReleaseStatus: typeof readReleaseStatus; resolveLatestRunUrl: typeof resolveLatestRunUrl; updateReleaseRow: typeof updateReleaseRow; }; export {}; //# sourceMappingURL=open.d.ts.map