@decohere/ci-tools 0.0.1-dev.123 → 0.0.1-dev.124
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
CHANGED
|
@@ -7,14 +7,13 @@ const MS_PER_DAY = 86_400_000;
|
|
|
7
7
|
const DEFAULT_REPO = "typesmiths/decohere";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* One profile per credential. Date math, GitHub issue
|
|
11
|
-
*
|
|
10
|
+
* One profile per credential (repo secret + repo variable). Date math, GitHub issue
|
|
11
|
+
* upsert, and fail-closed live below.
|
|
12
12
|
*
|
|
13
13
|
* @typedef {{
|
|
14
14
|
* id: string,
|
|
15
15
|
* secret: string,
|
|
16
16
|
* expiresVar: string,
|
|
17
|
-
* ghEnv?: string,
|
|
18
17
|
* mint: string,
|
|
19
18
|
* failImpact: string,
|
|
20
19
|
* rejectedWhy: string,
|
|
@@ -24,17 +23,6 @@ const DEFAULT_REPO = "typesmiths/decohere";
|
|
|
24
23
|
|
|
25
24
|
/** @type {Record<string, CredentialProfile>} */
|
|
26
25
|
export const CREDENTIALS = {
|
|
27
|
-
ghcr: {
|
|
28
|
-
id: "ghcr",
|
|
29
|
-
secret: "GHCR_PUSH_TOKEN",
|
|
30
|
-
expiresVar: "GHCR_TOKEN_EXPIRES_AT",
|
|
31
|
-
ghEnv: "ghcr-publish",
|
|
32
|
-
mint: "Create a **classic** GitHub PAT (fine-grained tokens cannot push to GHCR): https://github.com/settings/tokens/new?scopes=write:packages",
|
|
33
|
-
failImpact:
|
|
34
|
-
"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`.",
|
|
35
|
-
rejectedWhy: "GHCR `docker login` returned 401 (expired, revoked, or wrong secret name).",
|
|
36
|
-
revokeUrl: "https://github.com/settings/tokens",
|
|
37
|
-
},
|
|
38
26
|
npm: {
|
|
39
27
|
id: "npm",
|
|
40
28
|
secret: "NODE_AUTH_TOKEN",
|
|
@@ -85,11 +73,6 @@ export function resolveRepo() {
|
|
|
85
73
|
return process.env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
/** @param {CredentialProfile} credential */
|
|
89
|
-
export function ghScopeFlags(credential) {
|
|
90
|
-
return credential.ghEnv ? `--env ${credential.ghEnv} ` : "";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
76
|
/** @param {string} id */
|
|
94
77
|
export function credentialProfile(id) {
|
|
95
78
|
const credential = CREDENTIALS[id];
|
|
@@ -109,15 +92,14 @@ export function issueTitle(credential, expiresYmd) {
|
|
|
109
92
|
/** @param {CredentialProfile} credential */
|
|
110
93
|
export function rotationCommands(credential, nextExpiresYmd, repo = resolveRepo()) {
|
|
111
94
|
const next = nextExpiresYmd ?? "YYYY-MM-DD";
|
|
112
|
-
const flags = ghScopeFlags(credential);
|
|
113
95
|
return [
|
|
114
96
|
credential.mint,
|
|
115
97
|
"",
|
|
116
98
|
"Then from a trusted machine (do not paste the token into Actions or this issue):",
|
|
117
99
|
"",
|
|
118
100
|
"```bash",
|
|
119
|
-
`gh secret set ${credential.secret}
|
|
120
|
-
`gh variable set ${credential.expiresVar}
|
|
101
|
+
`gh secret set ${credential.secret} --repo ${repo}`,
|
|
102
|
+
`gh variable set ${credential.expiresVar} --repo ${repo} --body ${next}`,
|
|
121
103
|
"```",
|
|
122
104
|
"",
|
|
123
105
|
`Revoke the old token: ${credential.revokeUrl}`,
|
|
@@ -199,13 +181,12 @@ export async function runNudge(env = process.env, upsert = upsertIssue) {
|
|
|
199
181
|
const expiresYmd = String(env[credential.expiresVar] ?? "").trim();
|
|
200
182
|
const expiresAtMs = parseExpiresAt(expiresYmd);
|
|
201
183
|
const assessment = assessTokenLifetime({ expiresAtMs });
|
|
202
|
-
const flags = ghScopeFlags(credential);
|
|
203
184
|
const repo = env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
|
|
204
185
|
|
|
205
186
|
if (assessment.kind === "missing" && !rejected) {
|
|
206
187
|
process.stderr.write(
|
|
207
|
-
|
|
208
|
-
` gh variable set ${credential.expiresVar}
|
|
188
|
+
`repository variable ${credential.expiresVar}=YYYY-MM-DD is required\n` +
|
|
189
|
+
` gh variable set ${credential.expiresVar} --repo ${repo} --body YYYY-MM-DD\n`,
|
|
209
190
|
);
|
|
210
191
|
return 1;
|
|
211
192
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
buildDescription,
|
|
5
5
|
CREDENTIALS,
|
|
6
6
|
credentialProfile,
|
|
7
|
-
ghScopeFlags,
|
|
8
7
|
issueTitle,
|
|
9
8
|
parseExpiresAt,
|
|
10
9
|
rotationCommands,
|
|
@@ -54,14 +53,8 @@ describe("credential-nudge", () => {
|
|
|
54
53
|
expect(assessTokenLifetime({ expiresAtMs: null }).kind).toBe("missing");
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
it("
|
|
58
|
-
const ghcr = credentialProfile("ghcr");
|
|
56
|
+
it("builds the rotation issue for a credential profile", () => {
|
|
59
57
|
const npm = credentialProfile("npm");
|
|
60
|
-
expect(ghScopeFlags(ghcr)).toBe("--env ghcr-publish ");
|
|
61
|
-
expect(ghScopeFlags(npm)).toBe("");
|
|
62
|
-
expect(rotationCommands(ghcr, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
63
|
-
"gh secret set GHCR_PUSH_TOKEN --env ghcr-publish --repo typesmiths/decohere",
|
|
64
|
-
);
|
|
65
58
|
expect(rotationCommands(npm, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
66
59
|
"gh secret set NODE_AUTH_TOKEN --repo typesmiths/decohere",
|
|
67
60
|
);
|
|
@@ -71,16 +64,15 @@ describe("credential-nudge", () => {
|
|
|
71
64
|
expect(issueTitle(npm, "2026-11-19")).toBe(
|
|
72
65
|
"chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)",
|
|
73
66
|
);
|
|
74
|
-
const body = buildDescription(
|
|
67
|
+
const body = buildDescription(npm, {
|
|
75
68
|
kind: "warn",
|
|
76
69
|
expiresYmd: "2026-11-19",
|
|
77
70
|
daysLeft: 14,
|
|
78
71
|
rejected: false,
|
|
79
72
|
});
|
|
80
|
-
expect(body).toContain(
|
|
81
|
-
expect(CREDENTIALS.npm.secret).toBe("NODE_AUTH_TOKEN");
|
|
73
|
+
expect(body).toContain(CREDENTIALS.npm.failImpact);
|
|
82
74
|
expect(suggestedNextExpiry(Date.UTC(2026, 7, 21), 90)).toBe("2026-11-19");
|
|
83
|
-
expect(() => credentialProfile("
|
|
75
|
+
expect(() => credentialProfile("ghcr")).toThrow(/Unknown NUDGE_CREDENTIAL/);
|
|
84
76
|
});
|
|
85
77
|
|
|
86
78
|
it("files nothing and passes while the token has more than 14 days left", async () => {
|
|
@@ -104,13 +96,17 @@ describe("credential-nudge", () => {
|
|
|
104
96
|
|
|
105
97
|
it("files a rotation issue and stops Publish once the token has expired or was rejected", async () => {
|
|
106
98
|
const expired = recordUpserts();
|
|
107
|
-
const env = { NUDGE_CREDENTIAL: "
|
|
99
|
+
const env = { NUDGE_CREDENTIAL: "npm", NODE_AUTH_TOKEN_EXPIRES_AT: inDays(0) };
|
|
108
100
|
expect(await runNudge(env, expired.upsert)).toBe(1);
|
|
109
101
|
expect(expired.upserts).toHaveLength(1);
|
|
110
102
|
|
|
111
103
|
const rejected = recordUpserts();
|
|
112
|
-
const rejectedEnv = {
|
|
104
|
+
const rejectedEnv = {
|
|
105
|
+
...env,
|
|
106
|
+
NODE_AUTH_TOKEN_EXPIRES_AT: inDays(60),
|
|
107
|
+
NUDGE_REJECTED: "1",
|
|
108
|
+
};
|
|
113
109
|
expect(await runNudge(rejectedEnv, rejected.upsert)).toBe(1);
|
|
114
|
-
expect(rejected.upserts[0].body).toContain(CREDENTIALS.
|
|
110
|
+
expect(rejected.upserts[0].body).toContain(CREDENTIALS.npm.rejectedWhy);
|
|
115
111
|
});
|
|
116
112
|
});
|