@decohere/ci-tools 0.0.1-dev.109 → 0.0.1-dev.11
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/package.json +3 -8
- package/src/audit/enforce.mjs +5 -7
- package/src/audit/summarize.mjs +32 -10
- package/src/audit/upsert-issue.mjs +37 -22
- package/src/bench/bench.mjs +3 -13
- package/src/bench/trace.mjs +14 -18
- package/src/coverage/enforce-thresholds.mjs +144 -0
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/release/sync-dependencies.mjs +2 -1
- package/src/audit/format.mjs +0 -133
- package/src/audit/upsert-clickup.mjs +0 -117
- package/src/github/credential-nudge.mjs +0 -320
- package/src/github/oidc-bootstrap-nudge.mjs +0 -368
- package/src/github/publish-verify.mjs +0 -209
- package/src/github/smoke-create-launcher.mjs +0 -70
- package/tests/audit/format.test.mjs +0 -115
- package/tests/github/credential-nudge.test.mjs +0 -65
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -104
- package/tests/github/publish-verify.test.mjs +0 -95
- package/tests/github/smoke-create-launcher.test.mjs +0 -71
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upsert a ClickUp task with the current npm audit report.
|
|
3
|
-
*
|
|
4
|
-
* Env: CLICKUP_API_KEY (required), CLICKUP_LIST_ID (default Decohere list).
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { formatMarkdown, parseAudit } from "./format.mjs";
|
|
8
|
-
|
|
9
|
-
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
10
|
-
const TASK_NAME = "chore(security): npm audit findings";
|
|
11
|
-
const LIST_ID = process.env.CLICKUP_LIST_ID ?? "901220171084";
|
|
12
|
-
const API = "https://api.clickup.com/api/v2";
|
|
13
|
-
|
|
14
|
-
function runUrl() {
|
|
15
|
-
if (
|
|
16
|
-
process.env.GITHUB_SERVER_URL &&
|
|
17
|
-
process.env.GITHUB_REPOSITORY &&
|
|
18
|
-
process.env.GITHUB_RUN_ID
|
|
19
|
-
) {
|
|
20
|
-
return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
21
|
-
}
|
|
22
|
-
return "(run URL unavailable)";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function clickup(method, path, body) {
|
|
26
|
-
const key = process.env.CLICKUP_API_KEY;
|
|
27
|
-
if (key == null || key.length === 0) {
|
|
28
|
-
throw new Error("Missing CLICKUP_API_KEY");
|
|
29
|
-
}
|
|
30
|
-
const response = await fetch(`${API}${path}`, {
|
|
31
|
-
method,
|
|
32
|
-
headers: {
|
|
33
|
-
Authorization: key,
|
|
34
|
-
"Content-Type": "application/json",
|
|
35
|
-
},
|
|
36
|
-
body: body == null ? undefined : JSON.stringify(body),
|
|
37
|
-
});
|
|
38
|
-
const text = await response.text();
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`ClickUp ${method} ${path} failed (${response.status}): ${text.slice(0, 500)}`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return text.length === 0 ? undefined : JSON.parse(text);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function findTaskByName(name) {
|
|
48
|
-
for (let page = 0; page < 10; page++) {
|
|
49
|
-
const qs = new URLSearchParams({
|
|
50
|
-
include_closed: "true",
|
|
51
|
-
subtasks: "false",
|
|
52
|
-
page: String(page),
|
|
53
|
-
});
|
|
54
|
-
const data = await clickup("GET", `/list/${LIST_ID}/task?${qs}`);
|
|
55
|
-
const tasks = Array.isArray(data) ? data : (data?.tasks ?? []);
|
|
56
|
-
const match = tasks.find((t) => t.name === name);
|
|
57
|
-
if (match) return match;
|
|
58
|
-
if (tasks.length < 100) break;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function firstOpenStatus() {
|
|
64
|
-
const list = await clickup("GET", `/list/${LIST_ID}`);
|
|
65
|
-
return (list.statuses ?? []).find((s) => s.type === "open")?.status;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildDescription(parsed) {
|
|
69
|
-
const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 12) : "unknown";
|
|
70
|
-
return [
|
|
71
|
-
formatMarkdown(parsed).trim(),
|
|
72
|
-
"",
|
|
73
|
-
`- Commit: \`${sha}\``,
|
|
74
|
-
`- Workflow run: ${runUrl()}`,
|
|
75
|
-
"",
|
|
76
|
-
"Updated by the nightly npm-audit workflow. PR CI still gates high/critical.",
|
|
77
|
-
].join("\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!existsSync(auditFilePath)) {
|
|
81
|
-
console.log(`No audit file at ${auditFilePath}; skipping ClickUp upsert.`);
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const parsed = parseAudit(JSON.parse(readFileSync(auditFilePath, "utf8")));
|
|
86
|
-
const existing = await findTaskByName(TASK_NAME);
|
|
87
|
-
|
|
88
|
-
if (parsed.total === 0) {
|
|
89
|
-
if (existing) {
|
|
90
|
-
await clickup("PUT", `/task/${existing.id}`, {
|
|
91
|
-
markdown_description: buildDescription(parsed),
|
|
92
|
-
});
|
|
93
|
-
console.log(`Cleared findings on ClickUp task ${existing.id}`);
|
|
94
|
-
} else {
|
|
95
|
-
console.log("No vulnerabilities and no existing ClickUp task; nothing to upsert.");
|
|
96
|
-
}
|
|
97
|
-
process.exit(0);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const payload = {
|
|
101
|
-
name: TASK_NAME,
|
|
102
|
-
markdown_description: buildDescription(parsed),
|
|
103
|
-
custom_item_id: 1004,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (existing) {
|
|
107
|
-
const update = { markdown_description: payload.markdown_description };
|
|
108
|
-
if (existing.closed || existing.status?.type === "closed") {
|
|
109
|
-
const status = await firstOpenStatus();
|
|
110
|
-
if (status) update.status = status;
|
|
111
|
-
}
|
|
112
|
-
await clickup("PUT", `/task/${existing.id}`, update);
|
|
113
|
-
console.log(`Updated ClickUp task ${existing.id} (${existing.url ?? ""})`);
|
|
114
|
-
} else {
|
|
115
|
-
const created = await clickup("POST", `/list/${LIST_ID}/task`, payload);
|
|
116
|
-
console.log(`Created ClickUp task ${created.id} ${created.url ?? ""}`);
|
|
117
|
-
}
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
|
|
4
|
-
const LIST_ID = process.env.CLICKUP_LIST_ID ?? "901220171084";
|
|
5
|
-
const API = "https://api.clickup.com/api/v2";
|
|
6
|
-
const WARN_DAYS = 14;
|
|
7
|
-
const MS_PER_DAY = 86_400_000;
|
|
8
|
-
const CHORE_TYPE = 1004;
|
|
9
|
-
const DEFAULT_REPO = "typesmiths/decohere";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* One profile per credential. Date math, ClickUp upsert, and fail-closed live below.
|
|
13
|
-
* `ghEnv` unset means repo secret / repo variable (not an Actions environment).
|
|
14
|
-
*
|
|
15
|
-
* @typedef {{
|
|
16
|
-
* id: string,
|
|
17
|
-
* secret: string,
|
|
18
|
-
* expiresVar: string,
|
|
19
|
-
* ghEnv?: string,
|
|
20
|
-
* mint: string,
|
|
21
|
-
* failImpact: string,
|
|
22
|
-
* rejectedWhy: string,
|
|
23
|
-
* revokeUrl: string,
|
|
24
|
-
* }} CredentialProfile
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/** @type {Record<string, CredentialProfile>} */
|
|
28
|
-
export const CREDENTIALS = {
|
|
29
|
-
ghcr: {
|
|
30
|
-
id: "ghcr",
|
|
31
|
-
secret: "GHCR_PUSH_TOKEN",
|
|
32
|
-
expiresVar: "GHCR_TOKEN_EXPIRES_AT",
|
|
33
|
-
ghEnv: "ghcr-publish",
|
|
34
|
-
mint: "Create a **classic** GitHub PAT (fine-grained tokens cannot push to GHCR): https://github.com/settings/tokens/new?scopes=write:packages",
|
|
35
|
-
failImpact:
|
|
36
|
-
"npm may already have published. Docker login/push and the `v*` tag will **fail**. The SHA stays untagged, so the next successful Publish of that tip can finish Docker + tag after you rotate. Images are not pushed with `GITHUB_TOKEN`.",
|
|
37
|
-
rejectedWhy: "GHCR `docker login` returned 401 (expired, revoked, or wrong secret name).",
|
|
38
|
-
revokeUrl: "https://github.com/settings/tokens",
|
|
39
|
-
},
|
|
40
|
-
npm: {
|
|
41
|
-
id: "npm",
|
|
42
|
-
secret: "NODE_AUTH_TOKEN",
|
|
43
|
-
expiresVar: "NODE_AUTH_TOKEN_EXPIRES_AT",
|
|
44
|
-
mint: "Create an npm **read** token (granular/classic) at https://www.npmjs.com/settings/~/tokens — Publish PUTs are OIDC; this token is only for private `npm view` and create smoke.",
|
|
45
|
-
failImpact:
|
|
46
|
-
"Verify cannot see private packages, so skipIfPublished and create smoke are unsafe. Publish **fails closed** before OIDC/Docker. After you rotate, re-run the failed Publish jobs.",
|
|
47
|
-
rejectedWhy: "`npm whoami` failed (expired, revoked, or missing `NODE_AUTH_TOKEN`).",
|
|
48
|
-
revokeUrl: "https://www.npmjs.com/settings/~/tokens",
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function utcDayMs(date) {
|
|
53
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** @param {string | undefined} raw */
|
|
57
|
-
export function parseExpiresAt(raw) {
|
|
58
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(raw ?? "").trim());
|
|
59
|
-
if (!match) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {{ now?: number, expiresAtMs: number | null, warnDays?: number }} options
|
|
67
|
-
* @returns {{ kind: "missing" | "ok" | "warn" | "expired", daysLeft: number | null }}
|
|
68
|
-
*/
|
|
69
|
-
export function assessTokenLifetime(options) {
|
|
70
|
-
const { expiresAtMs, warnDays = WARN_DAYS } = options;
|
|
71
|
-
if (expiresAtMs == null) {
|
|
72
|
-
return { kind: "missing", daysLeft: null };
|
|
73
|
-
}
|
|
74
|
-
const today = utcDayMs(new Date(options.now ?? Date.now()));
|
|
75
|
-
const daysLeft = Math.round((expiresAtMs - today) / MS_PER_DAY);
|
|
76
|
-
if (daysLeft <= 0) {
|
|
77
|
-
return { kind: "expired", daysLeft };
|
|
78
|
-
}
|
|
79
|
-
if (daysLeft <= warnDays) {
|
|
80
|
-
return { kind: "warn", daysLeft };
|
|
81
|
-
}
|
|
82
|
-
return { kind: "ok", daysLeft };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** @param {CredentialProfile} credential */
|
|
86
|
-
export function resolveRepo() {
|
|
87
|
-
return process.env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** @param {CredentialProfile} credential */
|
|
91
|
-
export function ghScopeFlags(credential) {
|
|
92
|
-
return credential.ghEnv ? `--env ${credential.ghEnv} ` : "";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** @param {string} id */
|
|
96
|
-
export function credentialProfile(id) {
|
|
97
|
-
const credential = CREDENTIALS[id];
|
|
98
|
-
if (!credential) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Unknown NUDGE_CREDENTIAL=${id}. Expected one of: ${Object.keys(CREDENTIALS).join(", ")}`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return credential;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** @param {CredentialProfile} credential */
|
|
107
|
-
export function taskName(credential, expiresYmd) {
|
|
108
|
-
return `chore(security): rotate ${credential.secret} (expires ${expiresYmd})`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** @param {CredentialProfile} credential */
|
|
112
|
-
export function rotationCommands(credential, nextExpiresYmd, repo = resolveRepo()) {
|
|
113
|
-
const next = nextExpiresYmd ?? "YYYY-MM-DD";
|
|
114
|
-
const flags = ghScopeFlags(credential);
|
|
115
|
-
return [
|
|
116
|
-
credential.mint,
|
|
117
|
-
"",
|
|
118
|
-
"Then from a trusted machine (do not paste the token into Actions or this task):",
|
|
119
|
-
"",
|
|
120
|
-
"```bash",
|
|
121
|
-
`gh secret set ${credential.secret} ${flags}--repo ${repo}`,
|
|
122
|
-
`gh variable set ${credential.expiresVar} ${flags}--repo ${repo} --body ${next}`,
|
|
123
|
-
"```",
|
|
124
|
-
"",
|
|
125
|
-
`Revoke the old token: ${credential.revokeUrl}`,
|
|
126
|
-
"Mark this task complete, then retry Publish:",
|
|
127
|
-
"",
|
|
128
|
-
"```bash",
|
|
129
|
-
`gh run list --repo ${repo} --workflow Publish --limit 5`,
|
|
130
|
-
`gh run rerun --failed <run-id> --repo ${repo}`,
|
|
131
|
-
"```",
|
|
132
|
-
].join("\n");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function suggestedNextExpiry(fromMs, days = 90) {
|
|
136
|
-
const start = utcDayMs(new Date(fromMs));
|
|
137
|
-
const next = new Date(start + days * MS_PER_DAY);
|
|
138
|
-
const y = next.getUTCFullYear();
|
|
139
|
-
const m = String(next.getUTCMonth() + 1).padStart(2, "0");
|
|
140
|
-
const d = String(next.getUTCDate()).padStart(2, "0");
|
|
141
|
-
return `${y}-${m}-${d}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** @param {CredentialProfile} credential */
|
|
145
|
-
export function buildDescription(credential, options) {
|
|
146
|
-
const { kind, expiresYmd, daysLeft, rejected, runUrl } = options;
|
|
147
|
-
const next = suggestedNextExpiry(Date.now());
|
|
148
|
-
const why = rejected
|
|
149
|
-
? credential.rejectedWhy
|
|
150
|
-
: kind === "expired"
|
|
151
|
-
? `${credential.expiresVar} (${expiresYmd}) is today or in the past. Rotate ${credential.secret} and this date before Publish can continue.`
|
|
152
|
-
: `${credential.secret} expires on ${expiresYmd} (${daysLeft} day(s) left). Rotate before that date so Publish does not fail closed.`;
|
|
153
|
-
return [
|
|
154
|
-
"## Summary",
|
|
155
|
-
"",
|
|
156
|
-
why,
|
|
157
|
-
"",
|
|
158
|
-
"## If this is not done in time",
|
|
159
|
-
"",
|
|
160
|
-
credential.failImpact,
|
|
161
|
-
"",
|
|
162
|
-
"## Commands",
|
|
163
|
-
"",
|
|
164
|
-
rotationCommands(credential, next),
|
|
165
|
-
"",
|
|
166
|
-
runUrl ? `- Workflow run: ${runUrl}` : null,
|
|
167
|
-
]
|
|
168
|
-
.filter((line) => line != null)
|
|
169
|
-
.join("\n");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function clickup(method, path, body) {
|
|
173
|
-
const key = process.env.CLICKUP_API_KEY;
|
|
174
|
-
if (key == null || key.length === 0) {
|
|
175
|
-
throw new Error("Missing CLICKUP_API_KEY");
|
|
176
|
-
}
|
|
177
|
-
const response = await fetch(`${API}${path}`, {
|
|
178
|
-
method,
|
|
179
|
-
headers: {
|
|
180
|
-
Authorization: key,
|
|
181
|
-
"Content-Type": "application/json",
|
|
182
|
-
},
|
|
183
|
-
body: body == null ? undefined : JSON.stringify(body),
|
|
184
|
-
});
|
|
185
|
-
const text = await response.text();
|
|
186
|
-
if (!response.ok) {
|
|
187
|
-
throw new Error(
|
|
188
|
-
`ClickUp ${method} ${path} failed (${response.status}): ${text.slice(0, 500)}`,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
return text.length === 0 ? undefined : JSON.parse(text);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function findTaskByName(name) {
|
|
195
|
-
for (let page = 0; page < 10; page += 1) {
|
|
196
|
-
const qs = new URLSearchParams({
|
|
197
|
-
include_closed: "true",
|
|
198
|
-
subtasks: "false",
|
|
199
|
-
page: String(page),
|
|
200
|
-
});
|
|
201
|
-
const data = await clickup("GET", `/list/${LIST_ID}/task?${qs}`);
|
|
202
|
-
const tasks = Array.isArray(data) ? data : (data?.tasks ?? []);
|
|
203
|
-
const match = tasks.find((task) => task.name === name);
|
|
204
|
-
if (match) {
|
|
205
|
-
return match;
|
|
206
|
-
}
|
|
207
|
-
if (tasks.length < 100) {
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function firstOpenStatus() {
|
|
215
|
-
const list = await clickup("GET", `/list/${LIST_ID}`);
|
|
216
|
-
return (list.statuses ?? []).find((status) => status.type === "open")?.status;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function runUrl() {
|
|
220
|
-
if (
|
|
221
|
-
process.env.GITHUB_SERVER_URL &&
|
|
222
|
-
process.env.GITHUB_REPOSITORY &&
|
|
223
|
-
process.env.GITHUB_RUN_ID
|
|
224
|
-
) {
|
|
225
|
-
return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
226
|
-
}
|
|
227
|
-
return "";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** @param {CredentialProfile} credential */
|
|
231
|
-
export async function upsertRotationTask(credential, options) {
|
|
232
|
-
const { expiresYmd, kind, daysLeft, rejected } = options;
|
|
233
|
-
const name = taskName(credential, expiresYmd ?? "unknown");
|
|
234
|
-
const existing = await findTaskByName(name);
|
|
235
|
-
const dueMs = parseExpiresAt(expiresYmd);
|
|
236
|
-
const payload = {
|
|
237
|
-
name,
|
|
238
|
-
markdown_description: buildDescription(credential, {
|
|
239
|
-
kind,
|
|
240
|
-
expiresYmd,
|
|
241
|
-
daysLeft,
|
|
242
|
-
rejected,
|
|
243
|
-
runUrl: runUrl(),
|
|
244
|
-
}),
|
|
245
|
-
custom_item_id: CHORE_TYPE,
|
|
246
|
-
priority: rejected || kind === "expired" ? 1 : 2,
|
|
247
|
-
due_date: dueMs == null ? undefined : dueMs,
|
|
248
|
-
due_date_time: false,
|
|
249
|
-
};
|
|
250
|
-
const assignee = process.env.CLICKUP_ASSIGNEE_ID?.trim();
|
|
251
|
-
if (assignee) {
|
|
252
|
-
payload.assignees = [Number(assignee)];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (existing) {
|
|
256
|
-
const update = {
|
|
257
|
-
markdown_description: payload.markdown_description,
|
|
258
|
-
priority: payload.priority,
|
|
259
|
-
due_date: payload.due_date,
|
|
260
|
-
due_date_time: false,
|
|
261
|
-
};
|
|
262
|
-
if (existing.closed || existing.status?.type === "closed") {
|
|
263
|
-
const status = await firstOpenStatus();
|
|
264
|
-
if (status) {
|
|
265
|
-
update.status = status;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
await clickup("PUT", `/task/${existing.id}`, update);
|
|
269
|
-
return { id: existing.id, url: existing.url, created: false };
|
|
270
|
-
}
|
|
271
|
-
const created = await clickup("POST", `/list/${LIST_ID}/task`, payload);
|
|
272
|
-
return { id: created.id, url: created.url, created: true };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export async function runNudge(env = process.env) {
|
|
276
|
-
const credential = credentialProfile(String(env.NUDGE_CREDENTIAL ?? "").trim());
|
|
277
|
-
const rejected = env.NUDGE_REJECTED === "1";
|
|
278
|
-
const expiresYmd = String(env[credential.expiresVar] ?? "").trim();
|
|
279
|
-
const expiresAtMs = parseExpiresAt(expiresYmd);
|
|
280
|
-
const assessment = assessTokenLifetime({ expiresAtMs });
|
|
281
|
-
const flags = ghScopeFlags(credential);
|
|
282
|
-
const repo = env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
|
|
283
|
-
|
|
284
|
-
if (assessment.kind === "missing" && !rejected) {
|
|
285
|
-
process.stderr.write(
|
|
286
|
-
`${credential.ghEnv ? `${credential.ghEnv} environment` : "repository"} variable ${credential.expiresVar}=YYYY-MM-DD is required\n` +
|
|
287
|
-
` gh variable set ${credential.expiresVar} ${flags}--repo ${repo} --body YYYY-MM-DD\n`,
|
|
288
|
-
);
|
|
289
|
-
return 1;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (rejected || assessment.kind === "warn" || assessment.kind === "expired") {
|
|
293
|
-
const result = await upsertRotationTask(credential, {
|
|
294
|
-
expiresYmd: expiresYmd || "unknown",
|
|
295
|
-
kind: assessment.kind,
|
|
296
|
-
daysLeft: assessment.daysLeft,
|
|
297
|
-
rejected,
|
|
298
|
-
});
|
|
299
|
-
process.stdout.write(
|
|
300
|
-
`${result.created ? "Created" : "Updated"} ClickUp ${result.id} ${result.url ?? ""}\n`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (rejected || assessment.kind === "expired" || assessment.kind === "missing") {
|
|
305
|
-
if (assessment.kind === "expired") {
|
|
306
|
-
process.stderr.write(
|
|
307
|
-
`${credential.secret} calendar expiry ${expiresYmd} has been reached — refusing to continue\n`,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
return 1;
|
|
311
|
-
}
|
|
312
|
-
return 0;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const isCli =
|
|
316
|
-
process.argv[1] != null && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
317
|
-
|
|
318
|
-
if (isCli) {
|
|
319
|
-
process.exit(await runNudge());
|
|
320
|
-
}
|