@theholocron/holocron-plugin-codecov 0.0.0-stage → 5.0.0-alpha.445
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/LICENSE +21 -0
- package/dist/index.d.mts +73 -0
- package/dist/index.mjs +226 -0
- package/package.json +64 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Newton Koumantzelis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AuthError, Coverage, CoverageTokenCheck, PluginToken, ResolveTokenInput } from "@theholocron/cli";
|
|
2
|
+
import { CodecovClient, CodecovClientOptions, CodecovService, createCodecovClient } from "@theholocron/codecov-client";
|
|
3
|
+
/** Resolves the Codecov token from `--token`, `HOLOCRON_CODECOV_TOKEN`, `CODECOV_TOKEN` or the keyring entry for `codecov`, and throws an `AuthError` when none is set. */
|
|
4
|
+
export declare const resolveToken: import("@theholocron/cli").TokenResolver;
|
|
5
|
+
/** Every token this plugin takes, for `holocron doctor tokens`, with the layers that hold it. */
|
|
6
|
+
export declare const tokens: PluginToken[];
|
|
7
|
+
/** The repository the plugin asks Codecov about. */
|
|
8
|
+
interface CodecovTarget {
|
|
9
|
+
/** The git hosting service Codecov knows the owner by. */
|
|
10
|
+
service: CodecovService;
|
|
11
|
+
/** The owner (user or organization) on that service. */
|
|
12
|
+
owner: string;
|
|
13
|
+
/** The repository name. */
|
|
14
|
+
repo: string;
|
|
15
|
+
}
|
|
16
|
+
/** The options that say which repository to ask about. */
|
|
17
|
+
interface CodecovTargetOptions {
|
|
18
|
+
/** The git hosting service. Default `github`. */
|
|
19
|
+
service?: CodecovService;
|
|
20
|
+
/** The owner. Default: the owner of `repo` when it is `owner/name`, else the active `org`. */
|
|
21
|
+
owner?: string;
|
|
22
|
+
/** The repository: `owner/name`, or a bare name. The CLI passes the repo it is operating on. */
|
|
23
|
+
repo?: string;
|
|
24
|
+
/** The active org, which the CLI passes to every plugin. */
|
|
25
|
+
org?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Works out the repository from the plugin options, or `undefined` when they do not name one. */
|
|
28
|
+
export declare function resolveTarget(options: CodecovTargetOptions): CodecovTarget | undefined;
|
|
29
|
+
/** The `coverage` capability for Codecov. */
|
|
30
|
+
export declare class CodecovCoverage implements Coverage {
|
|
31
|
+
private readonly client;
|
|
32
|
+
private readonly target;
|
|
33
|
+
/** The capability key, always `coverage`. */
|
|
34
|
+
readonly key: "coverage";
|
|
35
|
+
/** The provider name, `codecov`. */
|
|
36
|
+
readonly providerName = "codecov";
|
|
37
|
+
/**
|
|
38
|
+
* `client` is a thunk, so the token is resolved on first use and a plugin load never needs one. `target` is `undefined`
|
|
39
|
+
* when the options name no repository.
|
|
40
|
+
*/
|
|
41
|
+
constructor(client: () => CodecovClient, target: CodecovTarget | undefined);
|
|
42
|
+
checkToken(): Promise<CoverageTokenCheck>;
|
|
43
|
+
}
|
|
44
|
+
/** Options for the Codecov plugin: token resolution, the repository and test overrides. */
|
|
45
|
+
export interface CodecovPluginOptions extends ResolveTokenInput, CodecovTargetOptions {
|
|
46
|
+
/** Override base URL for tests. */
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
/** Override `fetch` for tests. */
|
|
49
|
+
fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
/** What the plugin's capability and probes share: the options and a lazily built client. */
|
|
52
|
+
export interface PluginContext {
|
|
53
|
+
/** The options the plugin was created with. */
|
|
54
|
+
options: CodecovPluginOptions;
|
|
55
|
+
/** Builds the client on first call and reuses it; the token is resolved then, not at plugin load. */
|
|
56
|
+
client: () => CodecovClient;
|
|
57
|
+
}
|
|
58
|
+
/** Creates the shared context for the given options; no token is resolved until `client()` is first called. */
|
|
59
|
+
export declare function createContext(options?: CodecovPluginOptions): PluginContext;
|
|
60
|
+
/** Builds the `coverage` capability from a plugin context. */
|
|
61
|
+
export declare function coverage(ctx: PluginContext): Coverage;
|
|
62
|
+
/** Creates the Codecov plugin: its `connection` and `token` probes and the `coverage` capability. */
|
|
63
|
+
export declare function createPlugin(options?: CodecovPluginOptions): {
|
|
64
|
+
name: string;
|
|
65
|
+
probes: {
|
|
66
|
+
connection: import("@theholocron/cli").Probe;
|
|
67
|
+
token: import("@theholocron/cli").Probe;
|
|
68
|
+
};
|
|
69
|
+
capabilities: {
|
|
70
|
+
coverage: () => Coverage;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export { AuthError, type CodecovClient, type CodecovClientOptions, type CodecovTarget, type CodecovTargetOptions, type ResolveTokenInput, createCodecovClient };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { AuthError, ProviderApiError, connectionProbe, createResolveToken, tokenExpectations } from "@theholocron/cli";
|
|
2
|
+
import { createCodecovClient } from "@theholocron/codecov-client";
|
|
3
|
+
// src/probes/token/expectations.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where the Codecov token is held: CI only (`measure:bundle` uploads bundle stats with it), so `ci` is the one layer
|
|
6
|
+
* declared. Codecov has one token that does one thing, upload, and no permission model, so no permissions are declared and
|
|
7
|
+
* the probe says so instead of guessing. This is the plugin's `tokens` declaration, read through `tokenExpectations()`;
|
|
8
|
+
* change it and the Codecov row in `docs/src/content/docs/tokens.mdx` together.
|
|
9
|
+
*/
|
|
10
|
+
const EXPECTED = { layers: ["ci"] };
|
|
11
|
+
// src/auth.ts
|
|
12
|
+
/** Resolves the Codecov token from `--token`, `HOLOCRON_CODECOV_TOKEN`, `CODECOV_TOKEN` or the keyring entry for `codecov`, and throws an `AuthError` when none is set. */
|
|
13
|
+
const resolveToken = createResolveToken({
|
|
14
|
+
envName: "HOLOCRON_CODECOV_TOKEN",
|
|
15
|
+
vendorEnvName: "CODECOV_TOKEN",
|
|
16
|
+
keyringService: "codecov",
|
|
17
|
+
errorMessage: "no Codecov token found. Pass --token <TOKEN>, set HOLOCRON_CODECOV_TOKEN / CODECOV_TOKEN, or run: holocron new token codecov <TOKEN>"
|
|
18
|
+
});
|
|
19
|
+
/** Every token this plugin takes, for `holocron doctor tokens`, with the layers that hold it. */
|
|
20
|
+
const tokens = [{
|
|
21
|
+
...resolveToken.token,
|
|
22
|
+
expect: EXPECTED
|
|
23
|
+
}];
|
|
24
|
+
// src/target.ts
|
|
25
|
+
/** Works out the repository from the plugin options, or `undefined` when they do not name one. */
|
|
26
|
+
function resolveTarget(options) {
|
|
27
|
+
const [first, second] = (options.repo ?? "").split("/");
|
|
28
|
+
const name = second === void 0 ? first : second;
|
|
29
|
+
const owner = options.owner ?? (second === void 0 ? void 0 : first) ?? options.org;
|
|
30
|
+
if (!owner || !name) return void 0;
|
|
31
|
+
return {
|
|
32
|
+
service: options.service ?? "github",
|
|
33
|
+
owner,
|
|
34
|
+
repo: name
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** `owner/repo`, for messages. */
|
|
38
|
+
function describeTarget(target) {
|
|
39
|
+
return `${target.owner}/${target.repo}`;
|
|
40
|
+
}
|
|
41
|
+
// src/capabilities/coverage.ts
|
|
42
|
+
/** The `coverage` capability for Codecov. */
|
|
43
|
+
var CodecovCoverage = class {
|
|
44
|
+
client;
|
|
45
|
+
target;
|
|
46
|
+
/** The capability key, always `coverage`. */
|
|
47
|
+
key = "coverage";
|
|
48
|
+
/** The provider name, `codecov`. */
|
|
49
|
+
providerName = "codecov";
|
|
50
|
+
/**
|
|
51
|
+
* `client` is a thunk, so the token is resolved on first use and a plugin load never needs one. `target` is `undefined`
|
|
52
|
+
* when the options name no repository.
|
|
53
|
+
*/
|
|
54
|
+
constructor(client, target) {
|
|
55
|
+
this.client = client;
|
|
56
|
+
this.target = target;
|
|
57
|
+
}
|
|
58
|
+
async checkToken() {
|
|
59
|
+
if (!this.target) throw new Error("@theholocron/holocron-plugin-codecov needs a repository (the plugin options owner and repo, or the active repo) for checkToken");
|
|
60
|
+
const { service, owner, repo } = this.target;
|
|
61
|
+
try {
|
|
62
|
+
const found = await this.client().repos.get(service, owner, repo);
|
|
63
|
+
return {
|
|
64
|
+
outcome: "accepted",
|
|
65
|
+
...found.private === void 0 ? {} : { repoPrivate: found.private }
|
|
66
|
+
};
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ProviderApiError) {
|
|
69
|
+
if (err.status === 401 || err.status === 403) return {
|
|
70
|
+
outcome: "refused",
|
|
71
|
+
status: err.status
|
|
72
|
+
};
|
|
73
|
+
if (err.status === 404) return {
|
|
74
|
+
outcome: "not-found",
|
|
75
|
+
status: 404
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Codecov could not be asked about ${describeTarget(this.target)}: ${String(err)}`, { cause: err });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
// src/probes/token/parse-token-args.ts
|
|
83
|
+
/** Reads `--layer <word>` and `--token-name <VAR>` (either spelling) from the words after the probe name. */
|
|
84
|
+
function parseTokenArgs(args) {
|
|
85
|
+
let layer;
|
|
86
|
+
let tokenName;
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
const arg = args[i];
|
|
89
|
+
if (arg === "--layer") layer = args[++i];
|
|
90
|
+
else if (arg.startsWith("--layer=")) layer = arg.slice(8);
|
|
91
|
+
else if (arg === "--token-name") tokenName = args[++i];
|
|
92
|
+
else if (arg.startsWith("--token-name=")) tokenName = arg.slice(13);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...layer ? { layer } : {},
|
|
96
|
+
...tokenName ? { tokenName } : {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// src/probes/token/token.ts
|
|
100
|
+
/**
|
|
101
|
+
* The `token` probe for Codecov. Codecov has no permission model, so the probe cannot judge permissions; it checks what
|
|
102
|
+
* can be proved. It reads the repository once from Codecov's REST API with the token (`coverage.checkToken()`):
|
|
103
|
+
*
|
|
104
|
+
* - **accepted** (200): Codecov answered for the repository with this token. For a public repository that proves little,
|
|
105
|
+
* because the API answers without a token, and the row says so.
|
|
106
|
+
* - **refused** (401, 403): `info`, never `fail`. The repository upload token (`measure:bundle`'s) is not a credential
|
|
107
|
+
* the REST API takes, and a revoked API token answers the same, so Codecov cannot tell this probe which it is.
|
|
108
|
+
* - **not found** (404): `info`. No such repository on Codecov, or the token cannot see it.
|
|
109
|
+
* - anything else (the network, a 5xx): `fail`, because that says nothing about access.
|
|
110
|
+
*
|
|
111
|
+
* With `--layer` (the CLI passes it when `--environment` named a layer) it also says whether that layer is one that
|
|
112
|
+
* should hold the token, against the plugin's `tokens` declaration. It never prints the token.
|
|
113
|
+
*/
|
|
114
|
+
function tokenProbe(ctx) {
|
|
115
|
+
return {
|
|
116
|
+
summary: "Ask Codecov's REST API for the repository with a token, and for a layer check that it is one that should hold the token. Codecov reports no permissions, and an upload token cannot be checked this way. Read-only.",
|
|
117
|
+
writes: false,
|
|
118
|
+
acceptsToken: true,
|
|
119
|
+
async run(probe) {
|
|
120
|
+
probe.secret(ctx.token());
|
|
121
|
+
probe.say({
|
|
122
|
+
status: "ok",
|
|
123
|
+
label: "token",
|
|
124
|
+
detail: "set"
|
|
125
|
+
});
|
|
126
|
+
const { layer, tokenName } = parseTokenArgs(probe.args ?? []);
|
|
127
|
+
if (layer) {
|
|
128
|
+
const declared = tokenExpectations(tokens, {
|
|
129
|
+
layer,
|
|
130
|
+
...tokenName ? { envName: tokenName } : {}
|
|
131
|
+
}).length > 0;
|
|
132
|
+
probe.say(declared ? {
|
|
133
|
+
status: "ok",
|
|
134
|
+
label: layer,
|
|
135
|
+
detail: "a layer that should hold this token"
|
|
136
|
+
} : {
|
|
137
|
+
status: "info",
|
|
138
|
+
label: layer,
|
|
139
|
+
detail: "not a layer this token is declared for, so it is only described"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (!ctx.repo) probe.say({
|
|
143
|
+
status: "info",
|
|
144
|
+
label: "repository",
|
|
145
|
+
detail: "none to ask Codecov about: set the plugin options owner and repo"
|
|
146
|
+
});
|
|
147
|
+
else try {
|
|
148
|
+
const found = await ctx.coverage().checkToken();
|
|
149
|
+
if (found.outcome === "accepted") {
|
|
150
|
+
probe.say({
|
|
151
|
+
status: "ok",
|
|
152
|
+
label: "Codecov API",
|
|
153
|
+
detail: `answered for ${ctx.repo} with this token`
|
|
154
|
+
});
|
|
155
|
+
if (found.repoPrivate === false) probe.say({
|
|
156
|
+
status: "info",
|
|
157
|
+
label: "public repository",
|
|
158
|
+
detail: "Codecov answers a public repository without a token, so this does not prove the token"
|
|
159
|
+
});
|
|
160
|
+
} else if (found.outcome === "refused") probe.say({
|
|
161
|
+
status: "info",
|
|
162
|
+
label: "Codecov API",
|
|
163
|
+
detail: `answered ${found.status} for ${ctx.repo}. A repository upload token is not one the REST API takes, so this cannot be checked for it; a revoked or wrong API token answers the same`
|
|
164
|
+
});
|
|
165
|
+
else probe.say({
|
|
166
|
+
status: "info",
|
|
167
|
+
label: "repository",
|
|
168
|
+
detail: `Codecov has no ${ctx.repo}, or the token cannot see it (404)`
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
probe.say({
|
|
172
|
+
status: "fail",
|
|
173
|
+
label: "Codecov API",
|
|
174
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
probe.say({
|
|
178
|
+
status: "info",
|
|
179
|
+
label: "permissions",
|
|
180
|
+
detail: "Codecov has no permission model and reports none, so they are not checked"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// src/index.ts
|
|
186
|
+
/** Creates the shared context for the given options; no token is resolved until `client()` is first called. */
|
|
187
|
+
function createContext(options = {}) {
|
|
188
|
+
let client;
|
|
189
|
+
return {
|
|
190
|
+
options,
|
|
191
|
+
client: () => client ??= createCodecovClient({
|
|
192
|
+
token: resolveToken(options),
|
|
193
|
+
baseUrl: options.baseUrl,
|
|
194
|
+
fetch: options.fetch
|
|
195
|
+
})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Builds the `coverage` capability from a plugin context. */
|
|
199
|
+
function coverage(ctx) {
|
|
200
|
+
return new CodecovCoverage(ctx.client, resolveTarget(ctx.options));
|
|
201
|
+
}
|
|
202
|
+
/** Creates the Codecov plugin: its `connection` and `token` probes and the `coverage` capability. */
|
|
203
|
+
function createPlugin(options = {}) {
|
|
204
|
+
const ctx = createContext(options);
|
|
205
|
+
const target = resolveTarget(options);
|
|
206
|
+
return {
|
|
207
|
+
name: "@theholocron/holocron-plugin-codecov",
|
|
208
|
+
probes: {
|
|
209
|
+
connection: connectionProbe({
|
|
210
|
+
provider: "Codecov",
|
|
211
|
+
token: () => resolveToken(options),
|
|
212
|
+
verify: async () => ({
|
|
213
|
+
ok: true,
|
|
214
|
+
subject: "a token is set (see `probe codecov token` for what can be checked)"
|
|
215
|
+
})
|
|
216
|
+
}),
|
|
217
|
+
token: tokenProbe({
|
|
218
|
+
token: () => resolveToken(options),
|
|
219
|
+
coverage: () => coverage(ctx),
|
|
220
|
+
repo: target ? describeTarget(target) : void 0
|
|
221
|
+
})
|
|
222
|
+
},
|
|
223
|
+
capabilities: { coverage: () => coverage(ctx) }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export { AuthError, CodecovCoverage, coverage, createCodecovClient, createContext, createPlugin, resolveTarget, resolveToken, tokens };
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-codecov",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "5.0.0-alpha.445",
|
|
4
|
+
"description": "Holocron plugin for Codecov. Implements the coverage capability against Codecov's REST API: checks that the service accepts a token for the repository.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codecov",
|
|
7
|
+
"coverage",
|
|
8
|
+
"holocron",
|
|
9
|
+
"plugin"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://docs.theholocron.dev/holocron/plugins/codecov/",
|
|
12
|
+
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/theholocron/holocron.git",
|
|
16
|
+
"directory": "plugins/holocron-plugin-codecov"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Newton Koumantzelis",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"import": "./dist/index.mjs",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^26",
|
|
34
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
35
|
+
"@vitest/eslint-plugin": "^1.6.28",
|
|
36
|
+
"eslint": "^10.14.0",
|
|
37
|
+
"eslint-plugin-n": "^18.5.0",
|
|
38
|
+
"globals": "^17.14.0",
|
|
39
|
+
"tsdown": "^0.23.5",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^5.1.0",
|
|
42
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.17",
|
|
43
|
+
"@theholocron/codecov-client": "^1.58.0-alpha.32",
|
|
44
|
+
"@theholocron/http-client": "^1.58.0-alpha.32",
|
|
45
|
+
"@theholocron/tsconfig": "^8.9.0-alpha.17",
|
|
46
|
+
"@theholocron/cli": "5.0.0-alpha.445",
|
|
47
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.17",
|
|
48
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.17"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@theholocron/codecov-client": "^1.58.0",
|
|
52
|
+
"@theholocron/cli": "5.0.0-alpha.445"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"lint:code": "eslint .",
|
|
63
|
+
"test:unit": "vitest run --coverage",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
6
67
|
}
|
package/README.md
DELETED