@git.zone/cli 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -50,21 +50,21 @@ gitzone release
50
50
 
51
51
  ## Commands
52
52
 
53
- | Command | Purpose |
54
- | --- | --- |
55
- | `commit` | Analyze changes and create one semantic source commit |
56
- | `release` | Turn pending changelog entries into a versioned release and publish targets |
57
- | `format` | Plan or apply project formatting and standardization |
58
- | `config` | Inspect, update, and migrate `.smartconfig.json` |
59
- | `services` | Manage local MongoDB, ObjectStorage, and Elasticsearch containers |
60
- | `tools` | Manage the global `@git.zone` toolchain |
61
- | `template` | Scaffold projects from built-in templates |
62
- | `meta` | Manage multi-repository workspaces |
63
- | `open` | Open repository assets like CI pages |
64
- | `docker` | Report and reclaim Docker resources created by git.zone tooling |
65
- | `deprecate` | Deprecate npm packages across registries |
66
- | `start` | Prepare an existing project for local work |
67
- | `helpers` | Run small helper utilities |
53
+ | Command | Purpose |
54
+ | ----------- | --------------------------------------------------------------------------- |
55
+ | `commit` | Analyze changes and create one semantic source commit |
56
+ | `release` | Turn pending changelog entries into a versioned release and publish targets |
57
+ | `format` | Plan or apply project formatting and standardization |
58
+ | `config` | Inspect, update, and migrate `.smartconfig.json` |
59
+ | `services` | Manage local MongoDB, ObjectStorage, and Elasticsearch containers |
60
+ | `tools` | Manage the global `@git.zone` toolchain |
61
+ | `template` | Scaffold projects from built-in templates |
62
+ | `meta` | Manage multi-repository workspaces |
63
+ | `open` | Open repository assets like CI pages |
64
+ | `docker` | Report and reclaim Docker resources created by git.zone tooling |
65
+ | `deprecate` | Deprecate npm packages across registries |
66
+ | `start` | Prepare an existing project for local work |
67
+ | `helpers` | Run small helper utilities |
68
68
 
69
69
  Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`.
70
70
 
@@ -119,15 +119,15 @@ The commit flow:
119
119
 
120
120
  Commit flags:
121
121
 
122
- | Flag | Meaning |
123
- | --- | --- |
124
- | `-y`, `--yes` | Auto-accept safe recommendations |
122
+ | Flag | Meaning |
123
+ | ------------------ | -------------------------------------------------------------- |
124
+ | `-y`, `--yes` | Auto-accept safe recommendations |
125
125
  | `--allow-breaking` | Allow `-y`/`--yes` to accept `BREAKING CHANGE` recommendations |
126
- | `-t`, `--test` | Add test step |
127
- | `-b`, `--build` | Add build step |
128
- | `-p`, `--push` | Push after the source commit |
129
- | `-f`, `--format` | Run `gitzone format --write` before commit |
130
- | `--plan` | Show resolved workflow only |
126
+ | `-t`, `--test` | Add test step |
127
+ | `-b`, `--build` | Add build step |
128
+ | `-p`, `--push` | Push after the source commit |
129
+ | `-f`, `--format` | Run `gitzone format --write` before commit |
130
+ | `--plan` | Show resolved workflow only |
131
131
 
132
132
  `-r` is intentionally not part of commit anymore. Use `gitzone release`.
133
133
 
@@ -137,7 +137,9 @@ Commit flags:
137
137
 
138
138
  `--merge` is intentionally strict: the source and existing `main` worktree must be clean, the source must be linearly rebased onto current remote `main`, and the Git target must push both the branch and tag. GitZone pins the single configured push URL, compare-and-swap fast-forwards local `main`, updates its verified worktree, and lease-pushes that exact source commit before creating release metadata. It rejects diverged or merged history, local replacement refs or grafts, stale worktrees, incomplete shallow history, changed destinations, and remote races. If the pre-release push fails, GitZone restores local `main` only when doing so cannot overwrite concurrent work. This plumbing-level integration intentionally does not run merge hooks or write `ORIG_HEAD`.
139
139
 
140
- `gitzone release` performs the release core once, then publishes to configured targets. An active Git target must succeed and be verified before npm publication can start.
140
+ `gitzone release` performs the release core once, then publishes to configured targets. When Docker is active, its immutable candidate must qualify before any Git, npm, or OCI destination publication. An active Git target must then succeed and be verified before npm or OCI publication can start.
141
+
142
+ Before interactive confirmation or source mutation, a Docker release verifies the project-local tSDocker capabilities and validates the deterministic request against the source commit. After the release commit and configured build, GitZone validates the final request against the release commit immediately before atomically installing the schema-2 journal.
141
143
 
142
144
  The release core is not configurable plumbing. It always follows the same professional release transaction:
143
145
 
@@ -149,15 +151,18 @@ The release core is not configurable plumbing. It always follows the same profes
149
151
  6. Move pending changelog entries into the new version section.
150
152
  7. Create the local release commit and tag on `main`.
151
153
  8. Run the configured release build, require a clean tree, and revalidate the release checkout.
152
- 9. When npm is selected, pack it once into an exact tarball. Atomically install a durable journal for Git-only and npm releases before final Git release-ref or npm publication.
154
+ 9. When npm is selected, pack it once into an exact tarball.
155
+ 10. Atomically install a durable journal before final publication. Non-Docker releases use journal schema 1; Docker releases use schema 2 in the same storage tree.
156
+ 11. For schema 2, run the project-local tSDocker qualification build and configured image tests, then durably record the candidate digest graph and complete ordered promotion set.
157
+ 12. Publish and verify Git, then each npm registry, then each Docker destination. Remove only the exact qualified candidate after every destination is verified, or after a terminal qualification destination conflict before Git/npm publication.
153
158
 
154
159
  Targets decide what happens after that:
155
160
 
156
- | Target | What it does |
157
- | --- | --- |
158
- | `git` | Atomically pushes the exact `main` release commit and new tag, often triggering remote CI release builds |
159
- | `npm` | Publishes the same journaled tarball to every configured registry and verifies its bytes anonymously |
160
- | `docker` | Fails closed in journal schema v1 until tSDocker provides structured digest-qualified publication results |
161
+ | Target | What it does |
162
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `git` | Atomically pushes the exact `main` release commit and new tag, often triggering remote CI release builds |
164
+ | `npm` | Publishes the same journaled tarball to every configured registry and verifies its bytes anonymously |
165
+ | `docker` | Qualifies immutable OCI digest graphs with project-local tSDocker, promotes each journaled destination under single-writer alias fencing, verifies exact digests, then cleans the candidate; a terminal qualification conflict permits cleanup only |
161
166
 
162
167
  ```bash
163
168
  # Preview the resolved release plan
@@ -181,38 +186,38 @@ gitzone release --minor
181
186
 
182
187
  Release flags:
183
188
 
184
- | Flag | Meaning |
185
- | --- | --- |
186
- | `-y`, `--yes` | Run without interactive confirmation |
187
- | `-t`, `--test` | Enable preflight tests |
188
- | `-b`, `--build` | Enable the release build after local release metadata is created |
189
- | `-p`, `--push` | Explicitly select the `git` target; combine with other target flags as needed |
190
- | `--target <csv>` | Replace configured targets with a non-empty comma-separated subset of `git`, `npm`, and `docker`; empty or unknown elements fail |
191
- | `--npm` | Explicitly select the `npm` target; combine with other target flags as needed |
192
- | `--docker` | Explicitly select `docker`; planning is supported, but a mutating schema v1 release fails before integration or release metadata |
193
- | `--no-publish` | Remove npm and Docker from the resolved target set without implicitly enabling Git |
194
- | `--no-build` | Disable the post-metadata release build for this run |
195
- | `--merge` | Fast-forward and lease-push a cleanly rebased feature branch into `main`, then release from `main` |
196
- | `--major`, `--minor`, `--patch` | Override inferred semver level |
197
- | `--plan` | Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees |
198
- | `inspect [version]` | Read one or all durable release journals; add `--json` for machine-readable output |
199
- | `resume <version>` | Resume only journaled Git/npm publication; fresh-release overrides, including negated forms, are rejected, and `--json` remains inspect-only |
200
- | `--recover-attempt <id>` | Recover the exact 32-character lowercase hexadecimal `attempt.id` shown by `inspect --json`, after independently proving its publisher stopped |
201
-
202
- ### Exact npm artifacts and release journals
203
-
204
- Journal schema v1 supports Git and public npm publication. Fresh and resumed npm publication requires the resolved `pnpm` binary to be exactly the version pinned by this package (`11.21.0`). Registries must be unique canonical credential-free HTTP(S) URLs, and `release.targets.npm.accessLevel` must be `public` so verification does not depend on publisher credentials.
189
+ | Flag | Meaning |
190
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
191
+ | `-y`, `--yes` | Run without interactive confirmation |
192
+ | `-t`, `--test` | Enable preflight tests |
193
+ | `-b`, `--build` | Enable the release build after local release metadata is created |
194
+ | `-p`, `--push` | Explicitly select the `git` target; combine with other target flags as needed |
195
+ | `--target <csv>` | Replace configured targets with a non-empty comma-separated subset of `git`, `npm`, and `docker`; empty or unknown elements fail |
196
+ | `--npm` | Explicitly select the `npm` target; combine with other target flags as needed |
197
+ | `--docker` | Explicitly select digest-qualified Docker publication; requires project-local `@git.zone/tsdocker >= 3.5.1` with protocol v1 |
198
+ | `--no-publish` | Remove npm and Docker from the resolved target set without implicitly enabling Git |
199
+ | `--no-build` | Disable the separate post-metadata project build; Docker qualification still performs its required image build |
200
+ | `--merge` | Fast-forward and lease-push a cleanly rebased feature branch into `main`, then release from `main`; incompatible with an active Docker target |
201
+ | `--major`, `--minor`, `--patch` | Override inferred semver level |
202
+ | `--plan` | Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees |
203
+ | `inspect [version]` | Read one or all durable release journals; add `--json` for machine-readable output |
204
+ | `resume <version>` | Resume journaled Git, npm, qualification, promotion, or cleanup work; fresh-release overrides are rejected, and `--json` remains inspect-only |
205
+ | `--recover-attempt <id>` | Recover the exact 32-character lowercase hexadecimal `attempt.id` shown by `inspect --json`, after independently proving its publisher stopped |
206
+
207
+ ### Exact artifacts and release journals
208
+
209
+ Journal schema 1 supports Git and public npm publication. Schema 2 adds Docker qualification, promotion, and cleanup evidence without changing the storage path or schema-1 parsing and resume behavior. Fresh and resumed npm publication requires the resolved `pnpm` binary to be exactly the version pinned by this package (`11.21.0`). Registries must be unique canonical credential-free HTTP(S) URLs, and `release.targets.npm.accessLevel` must be `public` so verification does not depend on publisher credentials.
205
210
 
206
211
  A selected Git target requires a canonical remote name and exactly one resolved push URL. Before any remote contact, including during `--plan`, GitZone rejects HTTP(S) usernames or passwords, embedded passwords, query strings, fragments, and unsupported protocols. Canonical local paths and SSH destinations may retain their required SSH username.
207
212
 
208
- Every release that reaches final Git/npm publication stores `journal.json` under the repository's Git common directory. npm releases additionally run `pnpm pack` once and store the resulting `package.tgz` beside it:
213
+ Every release that reaches final publication stores `journal.json` under the repository's Git common directory. npm releases additionally run `pnpm pack` once and store the resulting `package.tgz` beside it. The `v1` path names the storage format and intentionally contains both journal schemas:
209
214
 
210
215
  ```text
211
216
  <git-common-dir>/gitzone/releases/v1/v<version>/package.tgz
212
217
  <git-common-dir>/gitzone/releases/v1/v<version>/journal.json
213
218
  ```
214
219
 
215
- The canonical journal binds the release commit, annotated tag object, hashed Git destination, npm registries, per-target attempt states, and, when npm is selected, the package identity, tarball size, SHA-1, SHA-256, and SHA-512 integrity. Journal updates use revision compare-and-swap under an interprocess lock. Release identity and destinations cannot change after journal creation. Inspect and resume reject malformed, future, noncanonical, duplicate-key, or merely reformatted journal JSON rather than repairing or normalizing it.
220
+ The canonical journal binds the release commit, annotated tag object, hashed Git destination, npm registries, per-target attempt states, and, when npm is selected, the package identity, tarball size, SHA-1, SHA-256, and SHA-512 integrity. Schema 2 also binds the exact tSDocker request, candidate identity, qualification result, ordered destination promotions, probe or promotion evidence, and terminal cleanup result. Journal updates use revision compare-and-swap under an interprocess lock. Release identity and destinations cannot change after journal creation. Qualification and promotion evidence is append-only, destination conflicts are terminal, and completed Docker state cannot regress. Inspect and resume reject malformed, future, noncanonical, duplicate-key, or merely reformatted journal JSON rather than repairing or normalizing it.
216
221
 
217
222
  Packing must leave the release tree clean. Each registry receives that exact stored tarball through `pnpm publish <tarball> --ignore-scripts`, so publish lifecycle scripts do not run. Success is recorded only after anonymous no-redirect probes verify version metadata, SHA-1 and integrity fields, the downloaded tarball bytes, and the `latest` dist-tag. A matching pre-existing version follows `release.targets.npm.alreadyPublished`; conflicting bytes always stop the release, while transient responses and dist-tag propagation remain retryable.
218
223
 
@@ -232,11 +237,11 @@ gitzone release resume 6.0.0 -y
232
237
  gitzone release resume 6.0.0 -y --recover-attempt="$ATTEMPT_ID"
233
238
  ```
234
239
 
235
- Resume requires the local `main` commit, annotated release tag, and each journaled destination setting to remain exact. The Git remote must still match when Git is journaled; npm registry, access, and already-published settings must still match when npm is journaled. Non-journaled target configuration is ignored, so releases created with a target subset remain resumable. Fresh-release overrides such as targets, integration, build, test, and version flags are rejected even in negated forms such as `--no-git`, `--no-publish`, and `--no-build`; JSON output is available only through `inspect --json`. Resume probes remote state before acting. Exact remote state is accepted without republishing; positive byte, metadata, or ref conflicts fail closed, while absent, pending, inconclusive, and dist-tag propagation results do not overwrite a previously verified state. A target left in `publishing` state is never reclaimed from an absent remote result unless the exact recorded 32-character lowercase hexadecimal `attempt.id` from `git.attempt` or an npm registry attempt is supplied.
240
+ Resume requires the local `main` commit, annotated release tag, and each journaled destination setting to remain exact. The Git remote must still match when Git is journaled; npm registry, access, and already-published settings must still match when npm is journaled. Before any recovery or publication work, a nonterminal schema-2 resume canonical-compares the current Docker request with the journal, verifies every required project-local tSDocker protocol capability, and validates the journaled request; additive future capabilities are accepted. A completed schema-2 journal trusts its terminal cleanup evidence and does not require candidate state or current Docker configuration. Non-journaled target configuration is ignored, so releases created with a target subset remain resumable. Fresh-release overrides such as targets, integration, build, test, and version flags are rejected even in negated forms such as `--no-git`, `--no-publish`, and `--no-build`; JSON output is available only through `inspect --json`.
236
241
 
237
- Schema v1 resumes only final Git release-ref and npm publication after the journal is atomically installed. It does not recover local version, changelog, commit, tag, build, or pack failures that occur before installation, nor the pre-release `--merge` integration push. Those failures stop before the final release commit/tag or npm artifact is published and require explicit operator reconciliation.
242
+ Resume probes remotely observable targets before acting. Exact state is accepted without republishing; positive byte, metadata, ref, or digest conflicts fail closed, while inconclusive results do not overwrite a previously verified state. A Git, npm, Docker qualification, promotion, or cleanup target left in `publishing` state retains authority until the exact recorded 32-character lowercase hexadecimal `attempt.id` is supplied after independently proving its publisher stopped. An interrupted tSDocker `preparing` record has no qualification evidence and cannot be promoted; when tSDocker reports `RECOVERY_REQUIRED`, the exact recovered qualification owner removes only that incomplete deterministic preparation before claiming a fresh qualification attempt. Promotion recovery first reclaims the retired owner's request files and probes the journaled destination; an exact qualified digest is accepted, pending state retires the old attempt and claims a fresh owner before retry, an inconclusive probe retains the old journal owner, and conflicts halt permanently. Final cleanup of a qualified candidate starts only after every promotion is verified and is itself idempotently recoverable. A terminal qualification destination conflict has no promotions, blocks Git/npm, and permits only exact qualified-candidate cleanup.
238
243
 
239
- Docker target configuration remains available for planning and future journal schemas, but a mutating v6 release with an active Docker target stops before release integration or metadata changes. This restriction remains until tSDocker owns structured immutable digest qualification and promotion.
244
+ Schema 1 resumes final Git release-ref and npm publication after the journal is atomically installed. Schema 2 additionally resumes Docker qualification, ordered promotion, and cleanup. Neither schema recovers local version, changelog, commit, tag, build, or pack failures that occur before installation. `--merge` remains available only for non-Docker releases because its pre-release Git push would violate qualification-before-publication ordering. Pre-installation failures require explicit operator reconciliation.
240
245
 
241
246
  ## Standard Changelog
242
247
 
@@ -258,7 +263,7 @@ The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation
258
263
 
259
264
  ## Configuration
260
265
 
261
- CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Dormant Docker build and registry settings remain under `@git.zone/tsdocker` for the later journal schema that will restore Docker publication.
266
+ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker release selection lives under `@git.zone/cli.release.targets.docker`; canonical registries, repository mappings, and platforms remain owned by `@git.zone/tsdocker`.
262
267
 
263
268
  ```json
264
269
  {
@@ -324,18 +329,21 @@ values should be hosts without `http://` or `https://`:
324
329
  @git.zone/tsdocker.registries
325
330
  ```
326
331
 
327
- Docker release configuration uses the project's pinned `@git.zone/tsdocker`
328
- through `pnpm exec`. Install it in each Docker-producing project, but keep the
329
- target disabled while release journal schema v1 is active:
332
+ Docker release configuration uses the project's absolute project-local
333
+ `node_modules/.bin/tsdocker`. Install tSDocker `3.5.1` or newer in each
334
+ Docker-producing project; fresh release and nonterminal resume verify matching
335
+ package and binary versions plus the required protocol-v1 capabilities before
336
+ use. Additional future protocol capabilities are accepted:
330
337
 
331
338
  ```bash
332
- pnpm add --save-dev @git.zone/tsdocker
339
+ pnpm add --save-dev @git.zone/tsdocker@3.5.1
333
340
  ```
334
341
 
335
- The dormant `release.targets.docker.registry`, `buildRegistries`, and `test`
336
- settings describe the intended tSDocker destination, bounded base-image
337
- authentication, and pre-publication image tests. They do not execute while
338
- journal schema v1 is active.
342
+ `release.targets.docker.registry`, `buildRegistries`, `test`, `patterns`,
343
+ `cached`, `parallel`, and `context` become part of the immutable qualification
344
+ request. `noBuild` must remain false or absent because digest qualification
345
+ always builds. The separate release `--no-build` flag only disables the normal
346
+ project build step and does not bypass Docker qualification.
339
347
 
340
348
  Useful config commands:
341
349
 
@@ -495,11 +503,11 @@ Formatters include cleanup, smartconfig normalization, dependency license checks
495
503
 
496
504
  Supported services:
497
505
 
498
- | Service | Lifecycle/log aliases |
499
- | --- | --- |
500
- | MongoDB | `mongo`, `mongodb` |
506
+ | Service | Lifecycle/log aliases |
507
+ | ----------------------------- | --------------------- |
508
+ | MongoDB | `mongo`, `mongodb` |
501
509
  | ObjectStorage (S3-compatible) | `objectstorage`, `s3` |
502
- | Elasticsearch | `elasticsearch`, `es` |
510
+ | Elasticsearch | `elasticsearch`, `es` |
503
511
 
504
512
  Service-selection commands such as `set`, `enable`, and `disable` also accept
505
513
  `elastic`; lifecycle and log commands do not.
@@ -620,12 +628,12 @@ than interpreting legacy evidence as an active service.
620
628
 
621
629
  Cleanup is tiered, from fully resumable to irreversible:
622
630
 
623
- | Command | Containers | Data | Notes |
624
- | --- | --- | --- | --- |
625
- | `gitzone services stop` | kept (stopped) | kept | fully resumable |
626
- | `gitzone services remove` | removed | kept | resumable; `--yes` to skip the prompt |
627
- | `gitzone services clean` | removed | **removed** | irreversible; needs a typed `yes` or `--yes` |
628
- | `gitzone services prune` | see below | see below | machine-wide; dry run unless `--apply` |
631
+ | Command | Containers | Data | Notes |
632
+ | ------------------------- | -------------- | ----------- | -------------------------------------------- |
633
+ | `gitzone services stop` | kept (stopped) | kept | fully resumable |
634
+ | `gitzone services remove` | removed | kept | resumable; `--yes` to skip the prompt |
635
+ | `gitzone services clean` | removed | **removed** | irreversible; needs a typed `yes` or `--yes` |
636
+ | `gitzone services prune` | see below | see below | machine-wide; dry run unless `--apply` |
629
637
 
630
638
  `clean` and `prune` first persist an exact deletion intent, then atomically rename
631
639
  the canonical directory to a tokenized sibling quarantine before deleting any
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/cli',
6
- version: '6.0.0',
6
+ version: '6.1.0',
7
7
  description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
8
8
  }
@@ -3,13 +3,7 @@ import { getCliConfigValue } from "./helpers.smartconfig.js";
3
3
  export type TConfirmationMode = "prompt" | "auto" | "plan";
4
4
 
5
5
  export type TCommitStep =
6
- | "format"
7
- | "analyze"
8
- | "test"
9
- | "build"
10
- | "changelog"
11
- | "commit"
12
- | "push";
6
+ "format" | "analyze" | "test" | "build" | "changelog" | "commit" | "push";
13
7
 
14
8
  export type TReleaseTarget = "git" | "npm" | "docker";
15
9
 
@@ -134,6 +128,18 @@ export interface IResolvedReleaseResumeConfiguration {
134
128
  npmAlreadyPublished: "success" | "error";
135
129
  }
136
130
 
131
+ export type TResolvedReleaseDockerConfiguration = Pick<
132
+ IResolvedReleaseWorkflow,
133
+ | "dockerRegistry"
134
+ | "dockerBuildRegistries"
135
+ | "dockerTest"
136
+ | "dockerPatterns"
137
+ | "dockerCached"
138
+ | "dockerParallel"
139
+ | "dockerContext"
140
+ | "dockerNoBuild"
141
+ >;
142
+
137
143
  interface ICliWorkflowConfig {
138
144
  commit?: ICommitWorkflowConfig;
139
145
  release?: IReleaseWorkflowConfig;
@@ -179,9 +185,9 @@ export const supportsTsdockerMinimumVersion = (
179
185
  versionOutputArg: string,
180
186
  minimumVersionArg: TTsdockerMinimumVersion,
181
187
  ): boolean => {
182
- const match = versionOutputArg.trim().match(
183
- /(?:^|\s)(\d+)\.(\d+)\.(\d+)(?:\s|$)/,
184
- );
188
+ const match = versionOutputArg
189
+ .trim()
190
+ .match(/(?:^|\s)(\d+)\.(\d+)\.(\d+)(?:\s|$)/);
185
191
  if (!match) {
186
192
  return false;
187
193
  }
@@ -207,6 +213,94 @@ export const resolveDockerTestSetting = (valueArg: unknown): boolean => {
207
213
  return valueArg;
208
214
  };
209
215
 
216
+ const resolveDockerConfiguration = (
217
+ dockerConfigArg: IReleaseDockerTargetConfig,
218
+ ): TResolvedReleaseDockerConfiguration => {
219
+ if (
220
+ dockerConfigArg.engine !== undefined &&
221
+ dockerConfigArg.engine !== "tsdocker"
222
+ ) {
223
+ throw new Error("release.targets.docker.engine must be tsdocker");
224
+ }
225
+ if (
226
+ dockerConfigArg.registry !== undefined &&
227
+ (typeof dockerConfigArg.registry !== "string" ||
228
+ !dockerConfigArg.registry.trim())
229
+ ) {
230
+ throw new Error(
231
+ "release.targets.docker.registry must be a non-empty string",
232
+ );
233
+ }
234
+ const normalizeStringList = (
235
+ valueArg: unknown,
236
+ settingArg: "buildRegistries" | "patterns",
237
+ ): string[] => {
238
+ if (valueArg === undefined) return [];
239
+ if (
240
+ !Array.isArray(valueArg) ||
241
+ valueArg.some(
242
+ (entryArg) => typeof entryArg !== "string" || !entryArg.trim(),
243
+ )
244
+ ) {
245
+ throw new Error(
246
+ `release.targets.docker.${settingArg} must be an array of non-empty strings`,
247
+ );
248
+ }
249
+ const normalized = valueArg.map((entryArg) => entryArg.trim());
250
+ if (new Set(normalized).size !== normalized.length) {
251
+ throw new Error(
252
+ `release.targets.docker.${settingArg} must contain unique values`,
253
+ );
254
+ }
255
+ return normalized;
256
+ };
257
+ if (
258
+ dockerConfigArg.cached !== undefined &&
259
+ typeof dockerConfigArg.cached !== "boolean"
260
+ ) {
261
+ throw new Error("release.targets.docker.cached must be true or false");
262
+ }
263
+ if (
264
+ dockerConfigArg.parallel !== undefined &&
265
+ dockerConfigArg.parallel !== false &&
266
+ dockerConfigArg.parallel !== true &&
267
+ (!Number.isSafeInteger(dockerConfigArg.parallel) ||
268
+ (dockerConfigArg.parallel as number) <= 0)
269
+ ) {
270
+ throw new Error(
271
+ "release.targets.docker.parallel must be false, true, or a positive integer",
272
+ );
273
+ }
274
+ if (
275
+ dockerConfigArg.context !== undefined &&
276
+ (typeof dockerConfigArg.context !== "string" ||
277
+ !dockerConfigArg.context.trim())
278
+ ) {
279
+ throw new Error(
280
+ "release.targets.docker.context must be a non-empty string",
281
+ );
282
+ }
283
+ if (
284
+ dockerConfigArg.noBuild !== undefined &&
285
+ typeof dockerConfigArg.noBuild !== "boolean"
286
+ ) {
287
+ throw new Error("release.targets.docker.noBuild must be true or false");
288
+ }
289
+ return {
290
+ dockerRegistry: dockerConfigArg.registry?.trim() || undefined,
291
+ dockerBuildRegistries: normalizeStringList(
292
+ dockerConfigArg.buildRegistries,
293
+ "buildRegistries",
294
+ ),
295
+ dockerTest: resolveDockerTestSetting(dockerConfigArg.test),
296
+ dockerPatterns: normalizeStringList(dockerConfigArg.patterns, "patterns"),
297
+ dockerCached: dockerConfigArg.cached ?? false,
298
+ dockerParallel: dockerConfigArg.parallel ?? false,
299
+ dockerContext: dockerConfigArg.context?.trim() || undefined,
300
+ dockerNoBuild: dockerConfigArg.noBuild ?? false,
301
+ };
302
+ };
303
+
210
304
  const resolveGitPushSetting = (
211
305
  valueArg: unknown,
212
306
  settingArg: "pushBranch" | "pushTags",
@@ -223,11 +317,19 @@ const resolveGitPushSetting = (
223
317
  return true;
224
318
  };
225
319
 
226
- const isStringRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
227
- return typeof valueArg === "object" && valueArg !== null && !Array.isArray(valueArg);
320
+ const isStringRecord = (
321
+ valueArg: unknown,
322
+ ): valueArg is Record<string, unknown> => {
323
+ return (
324
+ typeof valueArg === "object" &&
325
+ valueArg !== null &&
326
+ !Array.isArray(valueArg)
327
+ );
228
328
  };
229
329
 
230
- export const getDeclaredTsdockerRange = (packageJsonArg: unknown): string | undefined => {
330
+ export const getDeclaredTsdockerRange = (
331
+ packageJsonArg: unknown,
332
+ ): string | undefined => {
231
333
  if (!isStringRecord(packageJsonArg)) {
232
334
  return undefined;
233
335
  }
@@ -256,14 +358,24 @@ const normalizeConfirmation = (
256
358
 
257
359
  const normalizeRegistryUrl = (url: string): string => {
258
360
  let normalizedUrl = url.trim();
259
- if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
361
+ if (
362
+ !normalizedUrl.startsWith("http://") &&
363
+ !normalizedUrl.startsWith("https://")
364
+ ) {
260
365
  normalizedUrl = `https://${normalizedUrl}`;
261
366
  }
262
- return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
367
+ return normalizedUrl.endsWith("/")
368
+ ? normalizedUrl.slice(0, -1)
369
+ : normalizedUrl;
263
370
  };
264
371
 
265
372
  const isDisabled = (argvArg: any, ...keys: string[]): boolean => {
266
- return keys.some((key) => argvArg[key] === false || argvArg[`no-${key}`] || argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`]);
373
+ return keys.some(
374
+ (key) =>
375
+ argvArg[key] === false ||
376
+ argvArg[`no-${key}`] ||
377
+ argvArg[`no${key[0].toUpperCase()}${key.slice(1)}`],
378
+ );
267
379
  };
268
380
 
269
381
  const readCliWorkflowConfig = async (): Promise<ICliWorkflowConfig> => {
@@ -298,13 +410,13 @@ const getOrderedShortFlags = (commandName: string): string[] => {
298
410
  const hasExplicitCommitWorkflowFlags = (argvArg: any): boolean => {
299
411
  return Boolean(
300
412
  argvArg.f ||
301
- argvArg.format ||
302
- argvArg.t ||
303
- argvArg.test ||
304
- argvArg.b ||
305
- argvArg.build ||
306
- argvArg.p ||
307
- argvArg.push,
413
+ argvArg.format ||
414
+ argvArg.t ||
415
+ argvArg.test ||
416
+ argvArg.b ||
417
+ argvArg.build ||
418
+ argvArg.p ||
419
+ argvArg.push,
308
420
  );
309
421
  };
310
422
 
@@ -397,13 +509,23 @@ const buildReleasePlan = (options: {
397
509
  if (options.runBuild) plan.push("core.build");
398
510
  if (options.targets.includes("npm")) plan.push("core.packNpmArtifact");
399
511
  plan.push("core.writeReleaseJournal");
512
+ if (options.targets.includes("docker")) {
513
+ plan.push("target.docker.qualify");
514
+ }
400
515
  for (const target of options.targets) {
401
- plan.push(`target.${target}`);
516
+ plan.push(
517
+ target === "docker" ? "target.docker.promote" : `target.${target}`,
518
+ );
519
+ }
520
+ if (options.targets.includes("docker")) {
521
+ plan.push("target.docker.cleanup");
402
522
  }
403
523
  return plan;
404
524
  };
405
525
 
406
- export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedCommitWorkflow> => {
526
+ export const resolveCommitWorkflow = async (
527
+ argvArg: any,
528
+ ): Promise<IResolvedCommitWorkflow> => {
407
529
  const cliConfig = await readCliWorkflowConfig();
408
530
  const commitConfig = cliConfig.commit || {};
409
531
  const releaseFlagRequested = Boolean(argvArg.r || argvArg.release);
@@ -431,7 +553,10 @@ export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedComm
431
553
  if (argvArg.push && !rawSteps.includes("push")) rawSteps.push("push");
432
554
  rawSteps.push("changelog");
433
555
  rawSteps.push("commit");
434
- } else if (Array.isArray(commitConfig.steps) && commitConfig.steps.length > 0) {
556
+ } else if (
557
+ Array.isArray(commitConfig.steps) &&
558
+ commitConfig.steps.length > 0
559
+ ) {
435
560
  rawSteps = commitConfig.steps;
436
561
  } else {
437
562
  rawSteps = ["analyze"];
@@ -456,7 +581,9 @@ export const resolveCommitWorkflow = async (argvArg: any): Promise<IResolvedComm
456
581
  };
457
582
  };
458
583
 
459
- export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedReleaseWorkflow> => {
584
+ export const resolveReleaseWorkflow = async (
585
+ argvArg: any,
586
+ ): Promise<IResolvedReleaseWorkflow> => {
460
587
  const cliConfig = await readCliWorkflowConfig();
461
588
  const releaseConfig = cliConfig.release || {};
462
589
  const targetConfig = releaseConfig.targets || {};
@@ -481,7 +608,10 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
481
608
  gitEnabled,
482
609
  );
483
610
 
484
- let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
611
+ let confirmation = normalizeConfirmation(
612
+ releaseConfig.confirmation,
613
+ "prompt",
614
+ );
485
615
  if (argvArg.plan) {
486
616
  confirmation = "plan";
487
617
  } else if (argvArg.y || argvArg.yes) {
@@ -517,7 +647,9 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
517
647
  (!Array.isArray(rawNpmRegistries) ||
518
648
  rawNpmRegistries.some((registryArg) => typeof registryArg !== "string"))
519
649
  ) {
520
- throw new Error("release.targets.npm.registries must be an array of strings");
650
+ throw new Error(
651
+ "release.targets.npm.registries must be an array of strings",
652
+ );
521
653
  }
522
654
  npmRegistries = (rawNpmRegistries || []).map(normalizeRegistryUrl);
523
655
  if (new Set(npmRegistries).size !== npmRegistries.length) {
@@ -527,6 +659,19 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
527
659
  }
528
660
  }
529
661
  const mergeRequested = argvArg.merge === true;
662
+ const dockerConfiguration: TResolvedReleaseDockerConfiguration =
663
+ targets.includes("docker")
664
+ ? resolveDockerConfiguration(dockerConfig)
665
+ : {
666
+ dockerRegistry: undefined,
667
+ dockerBuildRegistries: [],
668
+ dockerTest: false,
669
+ dockerPatterns: [],
670
+ dockerCached: false,
671
+ dockerParallel: false,
672
+ dockerContext: undefined,
673
+ dockerNoBuild: false,
674
+ };
530
675
 
531
676
  return {
532
677
  confirmation,
@@ -549,23 +694,7 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
549
694
  npmAlreadyPublished: npmConfig.alreadyPublished || "success",
550
695
  dockerEnabled,
551
696
  dockerEngine: "tsdocker",
552
- dockerRegistry: typeof dockerConfig.registry === "string" && dockerConfig.registry.trim()
553
- ? dockerConfig.registry.trim()
554
- : undefined,
555
- dockerBuildRegistries: Array.isArray(dockerConfig.buildRegistries)
556
- ? dockerConfig.buildRegistries
557
- .filter((registryArg): registryArg is string => typeof registryArg === "string")
558
- .map((registryArg) => registryArg.trim())
559
- .filter(Boolean)
560
- : [],
561
- dockerTest: resolveDockerTestSetting(dockerConfig.test),
562
- dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
563
- dockerCached: dockerConfig.cached ?? false,
564
- dockerParallel: dockerConfig.parallel ?? false,
565
- dockerContext: typeof dockerConfig.context === "string" && dockerConfig.context.trim()
566
- ? dockerConfig.context.trim()
567
- : undefined,
568
- dockerNoBuild: dockerConfig.noBuild ?? false,
697
+ ...dockerConfiguration,
569
698
  };
570
699
  };
571
700
 
@@ -591,9 +720,13 @@ export const resolveReleaseResumeConfiguration = async (optionsArg: {
591
720
  if (
592
721
  npmConfig.registries !== undefined &&
593
722
  (!Array.isArray(npmConfig.registries) ||
594
- npmConfig.registries.some((registryArg) => typeof registryArg !== "string"))
723
+ npmConfig.registries.some(
724
+ (registryArg) => typeof registryArg !== "string",
725
+ ))
595
726
  ) {
596
- throw new Error("release.targets.npm.registries must be an array of strings");
727
+ throw new Error(
728
+ "release.targets.npm.registries must be an array of strings",
729
+ );
597
730
  }
598
731
  npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
599
732
  if (new Set(npmRegistries).size !== npmRegistries.length) {
@@ -606,7 +739,9 @@ export const resolveReleaseResumeConfiguration = async (optionsArg: {
606
739
  npmConfig.accessLevel !== "public" &&
607
740
  npmConfig.accessLevel !== "private"
608
741
  ) {
609
- throw new Error("release.targets.npm.accessLevel must be public or private");
742
+ throw new Error(
743
+ "release.targets.npm.accessLevel must be public or private",
744
+ );
610
745
  }
611
746
  if (
612
747
  npmConfig.alreadyPublished !== undefined &&
@@ -628,3 +763,10 @@ export const resolveReleaseResumeConfiguration = async (optionsArg: {
628
763
  npmAlreadyPublished,
629
764
  };
630
765
  };
766
+
767
+ export const resolveReleaseDockerConfiguration =
768
+ async (): Promise<TResolvedReleaseDockerConfiguration> => {
769
+ const cliConfig = await readCliWorkflowConfig();
770
+ const dockerConfig = cliConfig.release?.targets?.docker || {};
771
+ return resolveDockerConfiguration(dockerConfig);
772
+ };