@tokenoftrust/cli 1.4.0-rc.9 → 1.4.1
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 +7 -4
- package/bin/tot.mjs +169 -8
- package/package.json +1 -1
- package/src/activity.mjs +378 -0
- package/src/candidate-state.mjs +56 -16
- package/src/commands/accept.mjs +725 -0
- package/src/commands/branches.mjs +296 -0
- package/src/commands/cleanup.mjs +268 -0
- package/src/commands/clone.mjs +289 -10
- package/src/commands/dev.mjs +479 -118
- package/src/commands/doctor.mjs +2 -1
- package/src/commands/git-credential.mjs +180 -0
- package/src/commands/go-live.mjs +482 -0
- package/src/commands/hotfix.mjs +428 -0
- package/src/commands/pr.mjs +239 -15
- package/src/commands/preview-build.mjs +225 -0
- package/src/commands/preview.mjs +80 -0
- package/src/commands/retire.mjs +203 -0
- package/src/commands/revert.mjs +322 -0
- package/src/commands/rollback.mjs +401 -0
- package/src/commands/ship.mjs +517 -0
- package/src/commands/start.mjs +40 -8
- package/src/commands/submit.mjs +1325 -146
- package/src/commands/sync.mjs +203 -0
- package/src/commands/validate.mjs +6 -1
- package/src/git-credential.mjs +184 -0
- package/src/no-gitea-links.test.mjs +55 -0
- package/src/plan.mjs +262 -0
- package/src/sample.mjs +27 -1
- package/src/validate.mjs +52 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot retire --tenant <t> --pr <N>` — OPERATOR retire/GC (unit U7).
|
|
3
|
+
*
|
|
4
|
+
* Evict a candidate PR's hosted preview — its `ReviewEnvironment` projection +
|
|
5
|
+
* its immutable site version — to RECLAIM SPACE. It calls the
|
|
6
|
+
* session-authenticated storefront endpoint `POST /api/preview/retire` (unit
|
|
7
|
+
* U7), which deletes the candidate record + prunes the tenant index and evicts
|
|
8
|
+
* the candidate version (guarded: it refuses any version a channel points at).
|
|
9
|
+
*
|
|
10
|
+
* DISTINCT FROM `tot accept` / reject: retire touches NO change lifecycle — the
|
|
11
|
+
* change stays open. It is REVERSIBLE-BY-REBUILD: because U1 build-on-demand
|
|
12
|
+
* (`tot preview build`) + the U2 fallback page can rematerialize the PR head on
|
|
13
|
+
* request, a retired PR degrades to "not built yet", NOT a dead 404.
|
|
14
|
+
*
|
|
15
|
+
* IDEMPOTENT — retiring an already-absent candidate is a clean success
|
|
16
|
+
* (nothing to evict). Unlike `tot preview build`, retire needs NO head sha: it
|
|
17
|
+
* targets an EXISTING candidate by `--pr` (→ `pr-<N>`) or an explicit
|
|
18
|
+
* `--change-id`.
|
|
19
|
+
*
|
|
20
|
+
* AUTH — the storefront endpoint accepts the headless Bearer-operator-secret
|
|
21
|
+
* path (`resolveOwnerSession` fallback). This verb sends `Authorization: Bearer
|
|
22
|
+
* <secret>` (from `PREVIEW_RECONCILE_SECRET` / `GRANTS_ADMIN_SECRET` /
|
|
23
|
+
* `TOT_OPERATOR_SECRET`, or `--secret`), `X-Tot-Owner: <tenant>`, and
|
|
24
|
+
* `X-Tot-Capability: ship-on-behalf` — mirroring `tot preview build` and the
|
|
25
|
+
* `/admin` AdminPublishTab retire call.
|
|
26
|
+
*
|
|
27
|
+
* SELF-DECLARING — it prints the EXACT plan (which PR → which tenant → evict +
|
|
28
|
+
* rebuildable) and confirms before acting (`--yes` to skip; a non-TTY without
|
|
29
|
+
* `--yes` aborts rather than acting silently). Retire is INERT re: shared
|
|
30
|
+
* channels — it flips no preview/live channel and touches no live pointer — so
|
|
31
|
+
* this is a teardown, not a deploy. The plan itself is built by the SHARED plan
|
|
32
|
+
* module (`../plan.mjs`, unit U10) — the same affordance every other mutating
|
|
33
|
+
* operator verb (build/accept/ship) and the `/admin` confirm dialog use, per
|
|
34
|
+
* decision `operator-verb-and-hosting-model`.
|
|
35
|
+
*
|
|
36
|
+
* Dependency-free (global fetch + the shared plan module).
|
|
37
|
+
*/
|
|
38
|
+
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
39
|
+
|
|
40
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
41
|
+
|
|
42
|
+
/** Parse `tot retire` argv. Pure — unit-testable. */
|
|
43
|
+
export function parseRetireArgs(argv) {
|
|
44
|
+
const a = {
|
|
45
|
+
tenant: null,
|
|
46
|
+
pr: null,
|
|
47
|
+
changeId: null,
|
|
48
|
+
url: null,
|
|
49
|
+
secret: null,
|
|
50
|
+
yes: false,
|
|
51
|
+
help: false,
|
|
52
|
+
};
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const t = argv[i];
|
|
55
|
+
if (t === "--tenant") a.tenant = argv[++i];
|
|
56
|
+
else if (t === "--pr") a.pr = argv[++i];
|
|
57
|
+
else if (t === "--change-id") a.changeId = argv[++i];
|
|
58
|
+
else if (t === "--url") a.url = argv[++i];
|
|
59
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
60
|
+
else if (t === "--yes" || t === "-y") a.yes = true;
|
|
61
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
62
|
+
}
|
|
63
|
+
return a;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderUsage() {
|
|
67
|
+
return `tot retire — operator retire/GC: evict a candidate PR's hosted preview
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
tot retire --tenant <appDomain> --pr <N> [options]
|
|
71
|
+
tot retire --tenant <appDomain> --change-id <id> [options]
|
|
72
|
+
|
|
73
|
+
Evicts the candidate preview for PR #N of <tenant> — its review environment +
|
|
74
|
+
immutable version — to reclaim space. REVERSIBLE: rebuild it any time with
|
|
75
|
+
\`tot preview build\` (the change stays open; this is NOT reject). It flips NO
|
|
76
|
+
shared channel and is NOT go-live.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
80
|
+
current checkout's tenant when run inside one.
|
|
81
|
+
--pr <N> PR number to retire (identifies the candidate pr-<N>).
|
|
82
|
+
--change-id <id> Optional explicit candidate id (defaults to pr-<N>).
|
|
83
|
+
--url <origin> Storefront origin. Defaults to $TOT_STOREFRONT_URL or
|
|
84
|
+
${DEFAULT_STOREFRONT_URL}.
|
|
85
|
+
--secret <s> Operator secret. Prefer the env vars below.
|
|
86
|
+
--yes, -y Skip the confirmation prompt.
|
|
87
|
+
--help, -h Show this help.
|
|
88
|
+
|
|
89
|
+
Auth (operator secret, from env, first found):
|
|
90
|
+
PREVIEW_RECONCILE_SECRET, GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string[]} argv
|
|
95
|
+
* @param {any} ctx — detected CLI context (ctx.tenant when in a checkout)
|
|
96
|
+
*/
|
|
97
|
+
export async function run(argv, ctx) {
|
|
98
|
+
const args = parseRetireArgs(argv);
|
|
99
|
+
if (args.help) {
|
|
100
|
+
console.log(renderUsage());
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const base = (args.url || process.env.TOT_STOREFRONT_URL || process.env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL)
|
|
105
|
+
.trim()
|
|
106
|
+
.replace(/\/+$/, "");
|
|
107
|
+
|
|
108
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
109
|
+
if (!tenant) {
|
|
110
|
+
console.error(
|
|
111
|
+
"✗ no target tenant.\n\n → next: pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), " +
|
|
112
|
+
"or run inside a store checkout.",
|
|
113
|
+
);
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const prRaw = args.pr;
|
|
118
|
+
const pr = prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
|
|
119
|
+
const changeId = (args.changeId || "").trim() || null;
|
|
120
|
+
if (pr == null && !changeId) {
|
|
121
|
+
console.error(
|
|
122
|
+
"✗ no candidate to retire.\n\n → next: pass --pr <N> (the PR number to retire), or --change-id <id>.",
|
|
123
|
+
);
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const secret = (args.secret || process.env.PREVIEW_RECONCILE_SECRET || process.env.GRANTS_ADMIN_SECRET || process.env.TOT_OPERATOR_SECRET || "").trim();
|
|
128
|
+
if (!secret) {
|
|
129
|
+
console.error(
|
|
130
|
+
"✗ no operator secret.\n\n → next: set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / " +
|
|
131
|
+
"TOT_OPERATOR_SECRET) in the environment.",
|
|
132
|
+
);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Self-declaring: state the EXACT plan before acting (the shared plan module).
|
|
137
|
+
const planLines = planForAction({
|
|
138
|
+
action: "retire",
|
|
139
|
+
tenant,
|
|
140
|
+
pr,
|
|
141
|
+
changeId,
|
|
142
|
+
endpoint: `${base}/api/preview/retire`,
|
|
143
|
+
});
|
|
144
|
+
const { confirmed, reason } = await printPlanAndConfirm(planLines, {
|
|
145
|
+
yes: args.yes,
|
|
146
|
+
question: "Retire this preview now?",
|
|
147
|
+
});
|
|
148
|
+
if (!confirmed) {
|
|
149
|
+
if (reason === "non-tty") {
|
|
150
|
+
console.error("✗ refusing to retire without confirmation on a non-TTY.\n\n → next: re-run with --yes.");
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
console.log("Aborted — nothing was retired.");
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const body = {
|
|
158
|
+
...(pr != null ? { pr } : {}),
|
|
159
|
+
...(changeId ? { changeId } : {}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let res;
|
|
163
|
+
try {
|
|
164
|
+
res = await fetch(`${base}/api/preview/retire`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"content-type": "application/json",
|
|
168
|
+
authorization: `Bearer ${secret}`,
|
|
169
|
+
"x-tot-owner": tenant,
|
|
170
|
+
"x-tot-capability": "ship-on-behalf",
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify(body),
|
|
173
|
+
});
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error(`✗ could not reach ${base}: ${e?.message || e}\n\n → next: check --url / your network.`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let data = {};
|
|
180
|
+
try {
|
|
181
|
+
data = await res.json();
|
|
182
|
+
} catch {
|
|
183
|
+
/* non-JSON error body */
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const label = pr != null ? `PR #${pr}` : changeId;
|
|
187
|
+
|
|
188
|
+
if (res.ok && data?.ok) {
|
|
189
|
+
if (data.evicted) {
|
|
190
|
+
console.log(`✓ Retired ${label} for ${tenant} — the preview was evicted (rebuildable on demand).`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`✓ Retire ${label} for ${tenant}: nothing to evict (already retired).`);
|
|
193
|
+
}
|
|
194
|
+
console.log("\n → rebuild any time: `tot preview build --tenant " + tenant + (pr != null ? " --pr " + pr : "") + "`\n");
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A refusal (e.g. 409 version-pinned) or an auth/other failure.
|
|
199
|
+
const message = data?.error || `HTTP ${res.status}`;
|
|
200
|
+
console.error(`✗ Retire did not succeed (HTTP ${res.status}).`);
|
|
201
|
+
console.error(` • ${message}`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot revert --preview <PR|integration-sha>` (unit b21) — OPERATOR verb: REMOVE
|
|
3
|
+
* already-integrated content from the protected `preview` AGGREGATE by creating a
|
|
4
|
+
* NEW auditable revert commit, then rebuilding the aggregate. It is the explicit,
|
|
5
|
+
* safe answer to "several candidates are accepted, but one must be removed"
|
|
6
|
+
* (branch-lifecycle contract): NEVER a force-reset, NEVER a branch delete — a revert
|
|
7
|
+
* is a new commit on `preview`, and the shared preview moves only when the rebuilt
|
|
8
|
+
* aggregate is green again. Full history is preserved.
|
|
9
|
+
*
|
|
10
|
+
* DISTINCT from `tot ship`: revert is NOT go-live. It touches NO `main` and NO live
|
|
11
|
+
* channel; it only rewinds the SHARED preview aggregate. The verb is `--preview`-only
|
|
12
|
+
* by construction — there is no `--main` revert. To undo something already LIVE, use
|
|
13
|
+
* `tot rollback` (a live re-point), not this.
|
|
14
|
+
*
|
|
15
|
+
* TARGET — `--preview <target>` names EITHER a PR number (`42` or `#42`) OR an exact
|
|
16
|
+
* `preview` integration SHA. A numeric target is sent as `prNumber`; anything else as
|
|
17
|
+
* `integrationSha`. The server resolves either to the exact integration commit to
|
|
18
|
+
* undo (unit b21's resolver, inside the tenant-serialized queue lock), and REFUSES a
|
|
19
|
+
* target that was never integrated or has already been reverted.
|
|
20
|
+
*
|
|
21
|
+
* TRANSPORT — `POST /api/changes/revert` (the ONE call site of b07's
|
|
22
|
+
* `TenantIntegrationQueue.enqueueRevert`). Like `tot accept` / `tot ship`, the CLI
|
|
23
|
+
* reaches it with the OPERATOR-SECRET Bearer transport (`resolveOperatorSecret` +
|
|
24
|
+
* `X-Tot-Owner` + `x-tot-capability`). The response is the honest terminal aggregate
|
|
25
|
+
* state — `queueState` / `runState` / `pointerMoved` / `aggregateSha` /
|
|
26
|
+
* `revertedSha` / `statusMessage` — rendered VERBATIM, never a bare "reverted".
|
|
27
|
+
*
|
|
28
|
+
* HUMAN GATE — rewinding the shared preview is a decision a human makes, so this
|
|
29
|
+
* ALWAYS states the EXACT plan (a NEW revert commit, NO force-reset, NO touch to
|
|
30
|
+
* main/live) and requires an explicit confirm. `--yes` is an explicit affirmative; a
|
|
31
|
+
* non-TTY without `--yes` is refused (mirrors `tot accept` / `tot ship`).
|
|
32
|
+
*
|
|
33
|
+
* Dependency-free (global fetch + the shared plan module).
|
|
34
|
+
*/
|
|
35
|
+
import { fail } from "../errors.mjs";
|
|
36
|
+
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
37
|
+
// Reuse `tot ship`'s operator-secret precedence verbatim so accept + ship + revert
|
|
38
|
+
// speak ONE operator-auth contract.
|
|
39
|
+
import { resolveOperatorSecret } from "./ship.mjs";
|
|
40
|
+
|
|
41
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
42
|
+
|
|
43
|
+
const USAGE = `tot revert — remove already-integrated content from the preview aggregate (auditable)
|
|
44
|
+
|
|
45
|
+
tot revert --preview <PR|integration-sha> --tenant <t>
|
|
46
|
+
|
|
47
|
+
Creates a NEW auditable revert commit on the tenant's protected \`preview\`
|
|
48
|
+
aggregate that undoes one prior integration, then rebuilds the aggregate and
|
|
49
|
+
runs combined evidence. The shared preview advances only when it is GREEN
|
|
50
|
+
again. A revert is NEVER a force-reset and NEVER deletes commits — full history
|
|
51
|
+
is preserved. It does NOT touch main and does NOT go live.
|
|
52
|
+
|
|
53
|
+
<PR|integration-sha> is EITHER a PR number (e.g. 42 or #42) OR an exact preview
|
|
54
|
+
integration SHA. A target that was never integrated, or has already been
|
|
55
|
+
reverted, is refused before any change is made.
|
|
56
|
+
|
|
57
|
+
Rewinding the shared preview is a human decision: this ALWAYS prints the exact
|
|
58
|
+
plan and asks for an explicit confirm. A non-TTY without --yes is refused.
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--preview <PR|sha> REQUIRED. The PR number or integration SHA to revert.
|
|
62
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
63
|
+
current checkout's tenant when run inside one.
|
|
64
|
+
--message <msg> Optional revert-commit message.
|
|
65
|
+
--url <origin> storefront origin (default: env TOT_STOREFRONT_URL)
|
|
66
|
+
--secret <s> operator secret (prefer the env vars below)
|
|
67
|
+
--yes, -y Skip the interactive confirm (still an explicit human
|
|
68
|
+
affirmative — there is no default-yes).
|
|
69
|
+
--help, -h Show this help.
|
|
70
|
+
|
|
71
|
+
To undo something already LIVE, use \`tot rollback\` — not \`tot revert\`.
|
|
72
|
+
|
|
73
|
+
Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
|
|
74
|
+
GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret).`;
|
|
75
|
+
|
|
76
|
+
/** Parse `tot revert` argv. Pure — unit-testable. */
|
|
77
|
+
export function parseRevertArgs(argv) {
|
|
78
|
+
const a = {
|
|
79
|
+
preview: null,
|
|
80
|
+
tenant: null,
|
|
81
|
+
message: null,
|
|
82
|
+
url: null,
|
|
83
|
+
secret: null,
|
|
84
|
+
yes: false,
|
|
85
|
+
help: false,
|
|
86
|
+
};
|
|
87
|
+
for (let i = 0; i < argv.length; i++) {
|
|
88
|
+
const t = argv[i];
|
|
89
|
+
if (t === "--preview") a.preview = argv[++i];
|
|
90
|
+
else if (t === "--tenant") a.tenant = argv[++i];
|
|
91
|
+
else if (t === "--message" || t === "-m") a.message = argv[++i];
|
|
92
|
+
else if (t === "--url") a.url = argv[++i];
|
|
93
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
94
|
+
else if (t === "--yes" || t === "-y") a.yes = true;
|
|
95
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
96
|
+
}
|
|
97
|
+
return a;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Classify a `--preview` target into `{ prNumber }` or `{ integrationSha }`. A bare
|
|
102
|
+
* integer (optionally `#`-prefixed) is a PR number; anything else (a hex-ish SHA) is
|
|
103
|
+
* an integration SHA. Pure — unit-tested.
|
|
104
|
+
* @param {string|null} raw
|
|
105
|
+
* @returns {{ prNumber:number|null, integrationSha:string|null }}
|
|
106
|
+
*/
|
|
107
|
+
export function classifyRevertTarget(raw) {
|
|
108
|
+
const t = (raw || "").trim().replace(/^#/, "");
|
|
109
|
+
if (!t) return { prNumber: null, integrationSha: null };
|
|
110
|
+
if (/^\d+$/.test(t)) return { prNumber: Number(t), integrationSha: null };
|
|
111
|
+
return { prNumber: null, integrationSha: t };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalise a `POST /api/changes/revert` body to the honest terminal aggregate
|
|
116
|
+
* fields this verb renders. Reads defensively so a plausible field rename degrades
|
|
117
|
+
* rather than crashes. Pure — unit-tested.
|
|
118
|
+
* @param {any} data
|
|
119
|
+
*/
|
|
120
|
+
export function normalizeRevertResponse(data) {
|
|
121
|
+
const o = data && typeof data === "object" ? data : {};
|
|
122
|
+
return {
|
|
123
|
+
ok: o.ok === true,
|
|
124
|
+
queueState: typeof o.queueState === "string" ? o.queueState : null,
|
|
125
|
+
runState: typeof o.runState === "string" ? o.runState : null,
|
|
126
|
+
pointerMoved: o.pointerMoved === true,
|
|
127
|
+
aggregateSha: typeof o.aggregateSha === "string" ? o.aggregateSha : null,
|
|
128
|
+
revertedSha: typeof o.revertedSha === "string" ? o.revertedSha : null,
|
|
129
|
+
statusMessage: typeof o.statusMessage === "string" ? o.statusMessage : null,
|
|
130
|
+
reason: typeof o.reason === "string" ? o.reason : null,
|
|
131
|
+
changeId: typeof o.changeId === "string" ? o.changeId : null,
|
|
132
|
+
error: typeof o.error === "string" ? o.error : null,
|
|
133
|
+
raw: data,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Render the terminal aggregate status in house style — the honest queue/run state,
|
|
139
|
+
* NEVER a bare "reverted". Pure given its inputs; returns the process exit code.
|
|
140
|
+
* @param {ReturnType<typeof normalizeRevertResponse>} result
|
|
141
|
+
* @param {{ tenant:string, label:string }} ctx
|
|
142
|
+
* @returns {number}
|
|
143
|
+
*/
|
|
144
|
+
export function reportReverted(result, { tenant, label }) {
|
|
145
|
+
const state = `${result.queueState ?? "?"}/${result.runState ?? "—"}`;
|
|
146
|
+
if (result.ok) {
|
|
147
|
+
console.log(`\n ✓ reverted ${label} out of ${tenant}'s preview aggregate — it is GREEN.`);
|
|
148
|
+
console.log(` aggregate: ${state}${result.aggregateSha ? ` (${result.aggregateSha})` : ""}`);
|
|
149
|
+
if (result.revertedSha) console.log(` reverted integration commit: ${result.revertedSha}`);
|
|
150
|
+
if (result.pointerMoved) console.log(" the shared preview pointer moved to the reverted aggregate.");
|
|
151
|
+
console.log(" → next: review the aggregate preview, then `tot ship` to publish it live.");
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
// Honest non-green: the revert did NOT land in the shippable aggregate.
|
|
155
|
+
console.log(`\n ✗ ${label} was NOT reverted out of ${tenant}'s preview aggregate.`);
|
|
156
|
+
console.log(` aggregate: ${state}${result.reason ? ` (${result.reason})` : ""}`);
|
|
157
|
+
if (result.statusMessage) console.log(` why: ${result.statusMessage}`);
|
|
158
|
+
if (result.reason === "target_not_found" || result.reason === "already_reverted") {
|
|
159
|
+
console.log(` → check the target: \`tot ship\` shows the aggregate's included PRs.`);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(" → the aggregate is unchanged (last green preserved); fix forward or retry.");
|
|
162
|
+
}
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The revert flow after args are parsed: state the exact plan, confirm, then POST
|
|
168
|
+
* `/api/changes/revert` (operator-secret transport) and render the honest terminal
|
|
169
|
+
* aggregate status. `fetch`/`confirmPlan` injected so it is unit-tested with no
|
|
170
|
+
* network/TTY.
|
|
171
|
+
*
|
|
172
|
+
* @param {{ tenant:string, prNumber:number|null, integrationSha:string|null,
|
|
173
|
+
* message:string|null, secret:string, storefrontUrl?:string|null, yes?:boolean }} params
|
|
174
|
+
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm }} [deps]
|
|
175
|
+
* @returns {Promise<number>} process exit code
|
|
176
|
+
*/
|
|
177
|
+
export async function runRevert(
|
|
178
|
+
{ tenant, prNumber, integrationSha, message = null, secret, storefrontUrl = null, yes = false },
|
|
179
|
+
deps = {},
|
|
180
|
+
) {
|
|
181
|
+
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
182
|
+
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
183
|
+
const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
184
|
+
const label = prNumber != null ? `PR #${prNumber}` : `integration ${integrationSha}`;
|
|
185
|
+
|
|
186
|
+
// 1. State the EXACT plan (shared U10 affordance) — a NEW revert commit on
|
|
187
|
+
// preview, NO force-reset, NO touch to main/live — and gate on a confirm.
|
|
188
|
+
const planLines = planForAction({
|
|
189
|
+
action: "revert",
|
|
190
|
+
tenant,
|
|
191
|
+
pr: prNumber,
|
|
192
|
+
integrationSha,
|
|
193
|
+
});
|
|
194
|
+
const { confirmed, reason } = await confirmPlan(planLines, {
|
|
195
|
+
yes,
|
|
196
|
+
question: `Revert ${label} out of ${tenant}'s preview aggregate (a new revert commit)?`,
|
|
197
|
+
});
|
|
198
|
+
if (!confirmed) {
|
|
199
|
+
if (reason === "non-tty") {
|
|
200
|
+
console.error(
|
|
201
|
+
fail(
|
|
202
|
+
"refusing to revert without confirmation on a non-TTY.",
|
|
203
|
+
"re-run with --yes (an explicit human affirmative), or from an interactive terminal.",
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
return 2;
|
|
207
|
+
}
|
|
208
|
+
console.log("Aborted — nothing was reverted.");
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. Operator-secret transport — the CLI holds no storefront cookie.
|
|
213
|
+
if (!secret) {
|
|
214
|
+
console.error(
|
|
215
|
+
fail(
|
|
216
|
+
"reverting the preview aggregate is an OPERATOR action — it needs an operator secret",
|
|
217
|
+
"set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret",
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
const authHeaders = {
|
|
223
|
+
"content-type": "application/json",
|
|
224
|
+
authorization: `Bearer ${secret}`,
|
|
225
|
+
"x-tot-owner": tenant,
|
|
226
|
+
"x-tot-capability": "ship-on-behalf",
|
|
227
|
+
};
|
|
228
|
+
const requestBody = {
|
|
229
|
+
repo: tenant,
|
|
230
|
+
...(prNumber != null ? { prNumber } : {}),
|
|
231
|
+
...(integrationSha ? { integrationSha } : {}),
|
|
232
|
+
...(message ? { message } : {}),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// 3. POST the honest revert path (b07 queue enqueueRevert) and render terminal state.
|
|
236
|
+
let res;
|
|
237
|
+
try {
|
|
238
|
+
res = await fetchImpl(`${base}/api/changes/revert`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: authHeaders,
|
|
241
|
+
body: JSON.stringify(requestBody),
|
|
242
|
+
});
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error(
|
|
245
|
+
fail(`couldn't reach the revert endpoint at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
246
|
+
);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let data = {};
|
|
251
|
+
try {
|
|
252
|
+
data = await res.json();
|
|
253
|
+
} catch {
|
|
254
|
+
/* non-JSON / empty body */
|
|
255
|
+
}
|
|
256
|
+
const result = normalizeRevertResponse(data);
|
|
257
|
+
|
|
258
|
+
// A pre-flight error (auth, unknown tenant, bad body) is an HTTP 4xx with
|
|
259
|
+
// `{ error }` and no queue verdict — surface it distinctly from a red run.
|
|
260
|
+
if (!res.ok && result.queueState == null) {
|
|
261
|
+
const msg = result.error || `HTTP ${res.status}`;
|
|
262
|
+
console.error(
|
|
263
|
+
fail(
|
|
264
|
+
`the revert request was refused: ${msg}`,
|
|
265
|
+
res.status === 401 || res.status === 403
|
|
266
|
+
? "check the operator secret and that it's authorised for this tenant"
|
|
267
|
+
: "check --tenant / --url / --preview, then re-run",
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return reportReverted(result, { tenant, label });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {string[]} argv
|
|
278
|
+
* @param {any} ctx
|
|
279
|
+
*/
|
|
280
|
+
export async function run(argv, ctx) {
|
|
281
|
+
const env = process.env;
|
|
282
|
+
const args = parseRevertArgs(argv);
|
|
283
|
+
if (args.help) {
|
|
284
|
+
console.log(USAGE);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
289
|
+
if (!tenant) {
|
|
290
|
+
console.error(
|
|
291
|
+
fail(
|
|
292
|
+
"no target tenant.",
|
|
293
|
+
"pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { prNumber, integrationSha } = classifyRevertTarget(args.preview);
|
|
300
|
+
if (prNumber == null && !integrationSha) {
|
|
301
|
+
console.error(
|
|
302
|
+
fail(
|
|
303
|
+
"no revert target.",
|
|
304
|
+
"pass --preview <PR|integration-sha> (a PR number like 42, or an exact preview integration SHA).",
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
return 2;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const storefrontUrl =
|
|
311
|
+
args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
312
|
+
|
|
313
|
+
return await runRevert({
|
|
314
|
+
tenant,
|
|
315
|
+
prNumber,
|
|
316
|
+
integrationSha,
|
|
317
|
+
message: (args.message || "").trim() || null,
|
|
318
|
+
secret: resolveOperatorSecret(args.secret, env),
|
|
319
|
+
storefrontUrl,
|
|
320
|
+
yes: args.yes,
|
|
321
|
+
});
|
|
322
|
+
}
|