@storyshelf/notify-git 0.0.0-stage → 0.7.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/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/describe.mjs +21 -0
- package/dist/describe.mjs.map +1 -0
- package/dist/github-api.mjs +25 -0
- package/dist/github-api.mjs.map +1 -0
- package/dist/github-app.mjs +67 -0
- package/dist/github-app.mjs.map +1 -0
- package/dist/github-check-mapper.mjs +69 -0
- package/dist/github-check-mapper.mjs.map +1 -0
- package/dist/github-check.mjs +86 -0
- package/dist/github-check.mjs.map +1 -0
- package/dist/github-comment.mjs +58 -0
- package/dist/github-comment.mjs.map +1 -0
- package/dist/github-config.d.mts +24 -0
- package/dist/github-config.mjs +31 -0
- package/dist/github-config.mjs.map +1 -0
- package/dist/github-mapper.mjs +22 -0
- package/dist/github-mapper.mjs.map +1 -0
- package/dist/github-pr.mjs +20 -0
- package/dist/github-pr.mjs.map +1 -0
- package/dist/github-status.mjs +44 -0
- package/dist/github-status.mjs.map +1 -0
- package/dist/github.d.mts +13 -0
- package/dist/github.mjs +66 -0
- package/dist/github.mjs.map +1 -0
- package/dist/gitlab-api.mjs +23 -0
- package/dist/gitlab-api.mjs.map +1 -0
- package/dist/gitlab-comment.mjs +65 -0
- package/dist/gitlab-comment.mjs.map +1 -0
- package/dist/gitlab-config.d.mts +14 -0
- package/dist/gitlab-config.mjs +17 -0
- package/dist/gitlab-config.mjs.map +1 -0
- package/dist/gitlab-mapper.mjs +19 -0
- package/dist/gitlab-mapper.mjs.map +1 -0
- package/dist/gitlab-pr.mjs +19 -0
- package/dist/gitlab-pr.mjs.map +1 -0
- package/dist/gitlab-status.mjs +47 -0
- package/dist/gitlab-status.mjs.map +1 -0
- package/dist/gitlab.d.mts +9 -0
- package/dist/gitlab.mjs +50 -0
- package/dist/gitlab.mjs.map +1 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +23 -0
- package/dist/index.mjs.map +1 -0
- package/dist/log.d.mts +12 -0
- package/dist/log.mjs +42 -0
- package/dist/log.mjs.map +1 -0
- package/dist/prefix.mjs +14 -0
- package/dist/prefix.mjs.map +1 -0
- package/dist/review-comment.mjs +54 -0
- package/dist/review-comment.mjs.map +1 -0
- package/dist/state-adapter.mjs +43 -0
- package/dist/state-adapter.mjs.map +1 -0
- package/dist/version.mjs +12 -0
- package/dist/version.mjs.map +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { githubHeaders, repoPath } from "./github-api.mjs";
|
|
5
|
+
import { mapState } from "./github-mapper.mjs";
|
|
6
|
+
import { httpJson } from "@storyshelf/core/utils";
|
|
7
|
+
//#region src/github-status.ts
|
|
8
|
+
/**
|
|
9
|
+
* Post a commit status to GitHub under the full `context` name the caller chose
|
|
10
|
+
* (`<prefix>/testing`); the provider adds no prefix of its own. Posting the same
|
|
11
|
+
* `(sha, context)` again replaces the state, so it is idempotent and latest wins.
|
|
12
|
+
*/
|
|
13
|
+
async function postCommitStatus(repo, input) {
|
|
14
|
+
const state = mapState(input.state);
|
|
15
|
+
repo.logger?.debug({
|
|
16
|
+
context: input.context,
|
|
17
|
+
sha: input.sha,
|
|
18
|
+
state
|
|
19
|
+
}, "posting commit status");
|
|
20
|
+
try {
|
|
21
|
+
await httpJson(repoPath(repo.owner, repo.repo, `/statuses/${encodeURIComponent(input.sha)}`), {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: githubHeaders(repo.token),
|
|
24
|
+
json: {
|
|
25
|
+
state,
|
|
26
|
+
...input.url === void 0 ? {} : { target_url: input.url },
|
|
27
|
+
description: input.description,
|
|
28
|
+
context: input.context
|
|
29
|
+
},
|
|
30
|
+
logger: repo.logger
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
repo.logger?.error({
|
|
34
|
+
err: error,
|
|
35
|
+
context: input.context,
|
|
36
|
+
sha: input.sha
|
|
37
|
+
}, "failed to post commit status");
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { postCommitStatus };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=github-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-status.mjs","names":[],"sources":["../src/github-status.ts"],"sourcesContent":["import type { NotifierState } from \"@storyshelf/core/adapter/notifier\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { httpJson } from \"@storyshelf/core/utils\";\nimport { githubHeaders, repoPath } from \"./github-api.ts\";\nimport { mapState } from \"./github-mapper.ts\";\n\n/** Coordinates and credential of one GitHub repository. */\nexport interface GitHubRepo {\n token: string;\n owner: string;\n repo: string;\n logger?: Logger | undefined;\n}\n\n/**\n * Post a commit status to GitHub under the full `context` name the caller chose\n * (`<prefix>/testing`); the provider adds no prefix of its own. Posting the same\n * `(sha, context)` again replaces the state, so it is idempotent and latest wins.\n */\nexport async function postCommitStatus(\n repo: GitHubRepo,\n input: {\n sha: string;\n context: string;\n state: NotifierState;\n description: string;\n /** Absent while no public origin is known. */\n url?: string | undefined;\n },\n): Promise<void> {\n const state = mapState(input.state);\n repo.logger?.debug({ context: input.context, sha: input.sha, state }, \"posting commit status\");\n try {\n await httpJson(repoPath(repo.owner, repo.repo, `/statuses/${encodeURIComponent(input.sha)}`), {\n method: \"POST\",\n headers: githubHeaders(repo.token),\n json: {\n state,\n ...(input.url === undefined ? {} : { target_url: input.url }),\n description: input.description,\n context: input.context,\n },\n logger: repo.logger,\n });\n } catch (error) {\n repo.logger?.error(\n { err: error, context: input.context, sha: input.sha },\n \"failed to post commit status\",\n );\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,eAAsB,iBACpB,MACA,OAQe;CACf,MAAM,QAAQ,SAAS,MAAM,KAAK;CAClC,KAAK,QAAQ,MAAM;EAAE,SAAS,MAAM;EAAS,KAAK,MAAM;EAAK;CAAM,GAAG,uBAAuB;CAC7F,IAAI;EACF,MAAM,SAAS,SAAS,KAAK,OAAO,KAAK,MAAM,aAAa,mBAAmB,MAAM,GAAG,GAAG,GAAG;GAC5F,QAAQ;GACR,SAAS,cAAc,KAAK,KAAK;GACjC,MAAM;IACJ;IACA,GAAI,MAAM,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,MAAM,IAAI;IAC3D,aAAa,MAAM;IACnB,SAAS,MAAM;GACjB;GACA,QAAQ,KAAK;EACf,CAAC;CACH,SAAS,OAAO;EACd,KAAK,QAAQ,MACX;GAAE,KAAK;GAAO,SAAS,MAAM;GAAS,KAAK,MAAM;EAAI,GACrD,8BACF;EACA,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { StateNotifierProvider } from "@storyshelf/core/adapter/notifier";
|
|
4
|
+
//#region src/github.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* GitHub provider (`state` delivery). In `statuses` mode (default) it posts commit statuses with
|
|
7
|
+
* the channel's token; in `checks` mode it posts check runs as the channel's GitHub App. Either
|
|
8
|
+
* way it also keeps the pull-request comment.
|
|
9
|
+
*/
|
|
10
|
+
declare const gitHubNotifier: StateNotifierProvider;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { gitHubNotifier };
|
|
13
|
+
//# sourceMappingURL=github.d.mts.map
|
package/dist/github.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { installationToken } from "./github-app.mjs";
|
|
5
|
+
import { postCheckRun } from "./github-check.mjs";
|
|
6
|
+
import { upsertPrComment } from "./github-comment.mjs";
|
|
7
|
+
import { githubConfigSchema } from "./github-config.mjs";
|
|
8
|
+
import { postCommitStatus } from "./github-status.mjs";
|
|
9
|
+
import { createStateAdapter, requireToken } from "./state-adapter.mjs";
|
|
10
|
+
import { packageVersion } from "./version.mjs";
|
|
11
|
+
//#region src/github.ts
|
|
12
|
+
/**
|
|
13
|
+
* GitHub provider (`state` delivery). In `statuses` mode (default) it posts commit statuses with
|
|
14
|
+
* the channel's token; in `checks` mode it posts check runs as the channel's GitHub App. Either
|
|
15
|
+
* way it also keeps the pull-request comment.
|
|
16
|
+
*/
|
|
17
|
+
const gitHubNotifier = {
|
|
18
|
+
metadata: getMetadata(),
|
|
19
|
+
create(opts) {
|
|
20
|
+
const cfg = githubConfigSchema.parse(opts.config);
|
|
21
|
+
const logger = opts.logger?.child({ component: "notify-git-github" });
|
|
22
|
+
const token = tokenSource(cfg, requireToken(opts.secret, "GitHub", cfg.mode === "checks" ? "a private key" : "a token"));
|
|
23
|
+
const repo = async () => ({
|
|
24
|
+
token: await token(),
|
|
25
|
+
owner: cfg.owner,
|
|
26
|
+
repo: cfg.repo,
|
|
27
|
+
logger
|
|
28
|
+
});
|
|
29
|
+
return createStateAdapter(getMetadata(), {
|
|
30
|
+
status: async (input) => {
|
|
31
|
+
await (cfg.mode === "checks" ? postCheckRun(await repo(), input) : postCommitStatus(await repo(), input));
|
|
32
|
+
},
|
|
33
|
+
comment: async (input) => await upsertPrComment(await repo(), {
|
|
34
|
+
...input,
|
|
35
|
+
logger
|
|
36
|
+
})
|
|
37
|
+
}, logger);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** The token every request uses: the channel's token, or the App's installation token. */
|
|
41
|
+
function tokenSource(cfg, secret) {
|
|
42
|
+
if (cfg.mode !== "checks" || cfg.appId === void 0 || cfg.installationId === void 0) return async () => await Promise.resolve(secret);
|
|
43
|
+
const creds = {
|
|
44
|
+
appId: cfg.appId,
|
|
45
|
+
installationId: cfg.installationId,
|
|
46
|
+
privateKey: secret
|
|
47
|
+
};
|
|
48
|
+
return async () => await installationToken(creds);
|
|
49
|
+
}
|
|
50
|
+
/** Describe the GitHub provider (name, version, config schema). */
|
|
51
|
+
function getMetadata() {
|
|
52
|
+
return {
|
|
53
|
+
name: "GitHub",
|
|
54
|
+
version: packageVersion(),
|
|
55
|
+
description: "Commit statuses or GitHub App check runs, and pull-request comments, via the GitHub REST API",
|
|
56
|
+
kind: "github",
|
|
57
|
+
category: "notifier",
|
|
58
|
+
delivery: "state",
|
|
59
|
+
logo: "github",
|
|
60
|
+
schema: githubConfigSchema
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { getMetadata, gitHubNotifier };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=github.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.mjs","names":[],"sources":["../src/github.ts"],"sourcesContent":["import type { StateNotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { installationToken } from \"./github-app.ts\";\nimport { postCheckRun } from \"./github-check.ts\";\nimport { upsertPrComment } from \"./github-comment.ts\";\nimport { githubConfigSchema, type GitHubConfig } from \"./github-config.ts\";\nimport { postCommitStatus, type GitHubRepo } from \"./github-status.ts\";\nimport { createStateAdapter, requireToken, type StateWriter } from \"./state-adapter.ts\";\nimport { packageVersion } from \"./version.ts\";\n\n/**\n * GitHub provider (`state` delivery). In `statuses` mode (default) it posts commit statuses with\n * the channel's token; in `checks` mode it posts check runs as the channel's GitHub App. Either\n * way it also keeps the pull-request comment.\n */\nexport const gitHubNotifier: StateNotifierProvider = {\n metadata: getMetadata(),\n create(opts: { config: unknown; secret?: string; logger?: Logger }) {\n const cfg = githubConfigSchema.parse(opts.config);\n const logger = opts.logger?.child({ component: \"notify-git-github\" });\n const secret = requireToken(\n opts.secret,\n \"GitHub\",\n cfg.mode === \"checks\" ? \"a private key\" : \"a token\",\n );\n const token = tokenSource(cfg, secret);\n const repo = async (): Promise<GitHubRepo> => ({\n token: await token(),\n owner: cfg.owner,\n repo: cfg.repo,\n logger,\n });\n const writer: StateWriter = {\n status: async (input) => {\n await (cfg.mode === \"checks\"\n ? postCheckRun(await repo(), input)\n : postCommitStatus(await repo(), input));\n },\n comment: async (input) => await upsertPrComment(await repo(), { ...input, logger }),\n };\n return createStateAdapter(getMetadata(), writer, logger);\n },\n};\n\n/** The token every request uses: the channel's token, or the App's installation token. */\nfunction tokenSource(cfg: GitHubConfig, secret: string): () => Promise<string> {\n if (cfg.mode !== \"checks\" || cfg.appId === undefined || cfg.installationId === undefined) {\n return async () => await Promise.resolve(secret);\n }\n const creds = { appId: cfg.appId, installationId: cfg.installationId, privateKey: secret };\n return async () => await installationToken(creds);\n}\n\n/** Describe the GitHub provider (name, version, config schema). */\nexport function getMetadata(): StateNotifierProvider[\"metadata\"] {\n return {\n name: \"GitHub\",\n version: packageVersion(),\n description:\n \"Commit statuses or GitHub App check runs, and pull-request comments, via the GitHub REST API\",\n kind: \"github\",\n category: \"notifier\",\n delivery: \"state\",\n logo: \"github\",\n schema: githubConfigSchema,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,MAAa,iBAAwC;CACnD,UAAU,YAAY;CACtB,OAAO,MAA6D;EAClE,MAAM,MAAM,mBAAmB,MAAM,KAAK,MAAM;EAChD,MAAM,SAAS,KAAK,QAAQ,MAAM,EAAE,WAAW,oBAAoB,CAAC;EAMpE,MAAM,QAAQ,YAAY,KALX,aACb,KAAK,QACL,UACA,IAAI,SAAS,WAAW,kBAAkB,SAER,CAAC;EACrC,MAAM,OAAO,aAAkC;GAC7C,OAAO,MAAM,MAAM;GACnB,OAAO,IAAI;GACX,MAAM,IAAI;GACV;EACF;EASA,OAAO,mBAAmB,YAAY,GAAG;GAPvC,QAAQ,OAAO,UAAU;IACvB,OAAO,IAAI,SAAS,WAChB,aAAa,MAAM,KAAK,GAAG,KAAK,IAChC,iBAAiB,MAAM,KAAK,GAAG,KAAK;GAC1C;GACA,SAAS,OAAO,UAAU,MAAM,gBAAgB,MAAM,KAAK,GAAG;IAAE,GAAG;IAAO;GAAO,CAAC;EAEtC,GAAG,MAAM;CACzD;AACF;;AAGA,SAAS,YAAY,KAAmB,QAAuC;CAC7E,IAAI,IAAI,SAAS,YAAY,IAAI,UAAU,KAAA,KAAa,IAAI,mBAAmB,KAAA,GAC7E,OAAO,YAAY,MAAM,QAAQ,QAAQ,MAAM;CAEjD,MAAM,QAAQ;EAAE,OAAO,IAAI;EAAO,gBAAgB,IAAI;EAAgB,YAAY;CAAO;CACzF,OAAO,YAAY,MAAM,kBAAkB,KAAK;AAClD;;AAGA,SAAgB,cAAiD;CAC/D,OAAO;EACL,MAAM;EACN,SAAS,eAAe;EACxB,aACE;EACF,MAAM;EACN,UAAU;EACV,UAAU;EACV,MAAM;EACN,QAAQ;CACV;AACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/gitlab-api.ts
|
|
5
|
+
/** URL-encode an `owner/repo` pair as a GitLab project ID. */
|
|
6
|
+
function projectId(owner, repo) {
|
|
7
|
+
return encodeURIComponent(`${owner}/${repo}`);
|
|
8
|
+
}
|
|
9
|
+
/** Normalize a GitLab host URL (defaults to gitlab.com, no trailing slash). */
|
|
10
|
+
function apiBase(host = "https://gitlab.com") {
|
|
11
|
+
return host.replace(/\/+$/u, "");
|
|
12
|
+
}
|
|
13
|
+
/** Build the auth headers for GitLab API requests. */
|
|
14
|
+
function gitlabHeaders(token) {
|
|
15
|
+
return {
|
|
16
|
+
"PRIVATE-TOKEN": token,
|
|
17
|
+
"content-type": "application/json"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { apiBase, gitlabHeaders, projectId };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=gitlab-api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-api.mjs","names":[],"sources":["../src/gitlab-api.ts"],"sourcesContent":["/** URL-encode an `owner/repo` pair as a GitLab project ID. */\nexport function projectId(owner: string, repo: string): string {\n return encodeURIComponent(`${owner}/${repo}`);\n}\n\n/** Normalize a GitLab host URL (defaults to gitlab.com, no trailing slash). */\nexport function apiBase(host = \"https://gitlab.com\"): string {\n return host.replace(/\\/+$/u, \"\");\n}\n\n/** Build the auth headers for GitLab API requests. */\nexport function gitlabHeaders(token: string): Record<string, string> {\n return {\n \"PRIVATE-TOKEN\": token,\n \"content-type\": \"application/json\",\n };\n}\n"],"mappings":";;;;;AACA,SAAgB,UAAU,OAAe,MAAsB;CAC7D,OAAO,mBAAmB,GAAG,MAAM,GAAG,MAAM;AAC9C;;AAGA,SAAgB,QAAQ,OAAO,sBAA8B;CAC3D,OAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;;AAGA,SAAgB,cAAc,OAAuC;CACnE,OAAO;EACL,iBAAiB;EACjB,gBAAgB;CAClB;AACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { upsertReviewComment } from "./review-comment.mjs";
|
|
5
|
+
import { apiBase, gitlabHeaders, projectId } from "./gitlab-api.mjs";
|
|
6
|
+
import { findMrIid } from "./gitlab-pr.mjs";
|
|
7
|
+
import { httpJson } from "@storyshelf/core/utils";
|
|
8
|
+
//#region src/gitlab-comment.ts
|
|
9
|
+
/** Fetch-backed review thread (notes) for a merge request. */
|
|
10
|
+
function createThread(base, pid, iid, headers) {
|
|
11
|
+
const threadUrl = `${base}/api/v4/projects/${pid}/merge_requests/${iid}/notes`;
|
|
12
|
+
return {
|
|
13
|
+
list: async () => {
|
|
14
|
+
try {
|
|
15
|
+
return (await httpJson(`${threadUrl}?per_page=100`, { headers })).map((note) => ({
|
|
16
|
+
id: note.id,
|
|
17
|
+
body: note.body ?? ""
|
|
18
|
+
}));
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
update: async (id, body) => {
|
|
24
|
+
const updated = await httpJson(`${threadUrl}/${String(id)}`, {
|
|
25
|
+
method: "PUT",
|
|
26
|
+
headers,
|
|
27
|
+
json: { body }
|
|
28
|
+
});
|
|
29
|
+
return String(updated.id);
|
|
30
|
+
},
|
|
31
|
+
create: async (body) => {
|
|
32
|
+
const created = await httpJson(threadUrl, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers,
|
|
35
|
+
json: { body }
|
|
36
|
+
});
|
|
37
|
+
return String(created.id);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Create or update the StoryShelf review note on the merge request of a commit. */
|
|
42
|
+
async function upsertMrNote(project, input) {
|
|
43
|
+
const { host, owner, repo, token } = project;
|
|
44
|
+
const base = apiBase(host);
|
|
45
|
+
const pid = projectId(owner, repo);
|
|
46
|
+
const headers = gitlabHeaders(token);
|
|
47
|
+
return await upsertReviewComment({
|
|
48
|
+
url: input.url,
|
|
49
|
+
markdown: input.markdown,
|
|
50
|
+
sha: input.sha,
|
|
51
|
+
logger: input.logger,
|
|
52
|
+
resolveNumber: async () => await findMrIid({
|
|
53
|
+
host,
|
|
54
|
+
owner,
|
|
55
|
+
repo,
|
|
56
|
+
token,
|
|
57
|
+
sha: input.sha
|
|
58
|
+
}),
|
|
59
|
+
thread: (iid) => createThread(base, pid, iid, headers)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { upsertMrNote };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=gitlab-comment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-comment.mjs","names":[],"sources":["../src/gitlab-comment.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport { httpJson } from \"@storyshelf/core/utils\";\nimport { apiBase, gitlabHeaders, projectId } from \"./gitlab-api.ts\";\nimport { findMrIid } from \"./gitlab-pr.ts\";\nimport type { GitLabProject } from \"./gitlab-status.ts\";\nimport { upsertReviewComment, type ReviewThread } from \"./review-comment.ts\";\n\ninterface MergeRequestNote {\n id: number;\n body?: string | null;\n}\n\n/** Fetch-backed review thread (notes) for a merge request. */\nfunction createThread(\n base: string,\n pid: string,\n iid: number,\n headers: Record<string, string>,\n): ReviewThread {\n const threadUrl = `${base}/api/v4/projects/${pid}/merge_requests/${iid}/notes`;\n return {\n list: async () => {\n try {\n const notes = await httpJson<MergeRequestNote[]>(`${threadUrl}?per_page=100`, { headers });\n return notes.map((note) => ({ id: note.id, body: note.body ?? \"\" }));\n } catch {\n return [];\n }\n },\n update: async (id, body) => {\n const updated = await httpJson<MergeRequestNote>(`${threadUrl}/${String(id)}`, {\n method: \"PUT\",\n headers,\n json: { body },\n });\n return String(updated.id);\n },\n create: async (body) => {\n const created = await httpJson<MergeRequestNote>(threadUrl, {\n method: \"POST\",\n headers,\n json: { body },\n });\n return String(created.id);\n },\n };\n}\n\n/** Create or update the StoryShelf review note on the merge request of a commit. */\nexport async function upsertMrNote(\n project: GitLabProject,\n input: { sha: string; url: string; markdown: string; logger?: Logger },\n): Promise<string> {\n const { host, owner, repo, token } = project;\n const base = apiBase(host);\n const pid = projectId(owner, repo);\n const headers = gitlabHeaders(token);\n return await upsertReviewComment({\n url: input.url,\n markdown: input.markdown,\n sha: input.sha,\n logger: input.logger,\n resolveNumber: async () => await findMrIid({ host, owner, repo, token, sha: input.sha }),\n thread: (iid) => createThread(base, pid, iid, headers),\n });\n}\n"],"mappings":";;;;;;;;;AAaA,SAAS,aACP,MACA,KACA,KACA,SACc;CACd,MAAM,YAAY,GAAG,KAAK,mBAAmB,IAAI,kBAAkB,IAAI;CACvE,OAAO;EACL,MAAM,YAAY;GAChB,IAAI;IAEF,QAAO,MADa,SAA6B,GAAG,UAAU,gBAAgB,EAAE,QAAQ,CAAC,EAAA,CAC5E,KAAK,UAAU;KAAE,IAAI,KAAK;KAAI,MAAM,KAAK,QAAQ;IAAG,EAAE;GACrE,QAAQ;IACN,OAAO,CAAC;GACV;EACF;EACA,QAAQ,OAAO,IAAI,SAAS;GAC1B,MAAM,UAAU,MAAM,SAA2B,GAAG,UAAU,GAAG,OAAO,EAAE,KAAK;IAC7E,QAAQ;IACR;IACA,MAAM,EAAE,KAAK;GACf,CAAC;GACD,OAAO,OAAO,QAAQ,EAAE;EAC1B;EACA,QAAQ,OAAO,SAAS;GACtB,MAAM,UAAU,MAAM,SAA2B,WAAW;IAC1D,QAAQ;IACR;IACA,MAAM,EAAE,KAAK;GACf,CAAC;GACD,OAAO,OAAO,QAAQ,EAAE;EAC1B;CACF;AACF;;AAGA,eAAsB,aACpB,SACA,OACiB;CACjB,MAAM,EAAE,MAAM,OAAO,MAAM,UAAU;CACrC,MAAM,OAAO,QAAQ,IAAI;CACzB,MAAM,MAAM,UAAU,OAAO,IAAI;CACjC,MAAM,UAAU,cAAc,KAAK;CACnC,OAAO,MAAM,oBAAoB;EAC/B,KAAK,MAAM;EACX,UAAU,MAAM;EAChB,KAAK,MAAM;EACX,QAAQ,MAAM;EACd,eAAe,YAAY,MAAM,UAAU;GAAE;GAAM;GAAO;GAAM;GAAO,KAAK,MAAM;EAAI,CAAC;EACvF,SAAS,QAAQ,aAAa,MAAM,KAAK,KAAK,OAAO;CACvD,CAAC;AACH"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/gitlab-config.d.ts
|
|
5
|
+
/** Validation schema for a GitLab channel config (`owner` + `repo`, optional `host` and status `prefix`). */
|
|
6
|
+
declare const gitlabConfigSchema: z.ZodObject<{
|
|
7
|
+
owner: z.ZodString;
|
|
8
|
+
repo: z.ZodString;
|
|
9
|
+
host: z.ZodOptional<z.ZodString>;
|
|
10
|
+
prefix: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { gitlabConfigSchema };
|
|
14
|
+
//# sourceMappingURL=gitlab-config.d.mts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { statusPrefixSchema } from "./prefix.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/gitlab-config.ts
|
|
7
|
+
/** Validation schema for a GitLab channel config (`owner` + `repo`, optional `host` and status `prefix`). */
|
|
8
|
+
const gitlabConfigSchema = z.object({
|
|
9
|
+
owner: z.string().min(1),
|
|
10
|
+
repo: z.string().min(1),
|
|
11
|
+
host: z.string().url().optional(),
|
|
12
|
+
prefix: statusPrefixSchema.optional()
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { gitlabConfigSchema };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=gitlab-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-config.mjs","names":[],"sources":["../src/gitlab-config.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { statusPrefixSchema } from \"./prefix.ts\";\n\n/** Validation schema for a GitLab channel config (`owner` + `repo`, optional `host` and status `prefix`). */\nexport const gitlabConfigSchema = z.object({\n owner: z.string().min(1),\n repo: z.string().min(1),\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n host: z.string().url().optional(),\n prefix: statusPrefixSchema.optional(),\n});\n"],"mappings":";;;;;;;AAIA,MAAa,qBAAqB,EAAE,OAAO;CACzC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAEtB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAChC,QAAQ,mBAAmB,SAAS;AACtC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/gitlab-mapper.ts
|
|
5
|
+
/** GitLab's pipeline statuses; a cancelled build is GitLab's own `canceled`. */
|
|
6
|
+
const STATES = {
|
|
7
|
+
pending: "pending",
|
|
8
|
+
success: "success",
|
|
9
|
+
failure: "failed",
|
|
10
|
+
cancelled: "canceled"
|
|
11
|
+
};
|
|
12
|
+
/** Map a StoryShelf build state to a GitLab pipeline status; an unknown state is `failed`. */
|
|
13
|
+
function mapState(state) {
|
|
14
|
+
return STATES[state] ?? "failed";
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { mapState };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=gitlab-mapper.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-mapper.mjs","names":[],"sources":["../src/gitlab-mapper.ts"],"sourcesContent":["import type { NotifierState } from \"@storyshelf/core/adapter/notifier\";\n\n/** GitLab's pipeline statuses; a cancelled build is GitLab's own `canceled`. */\nconst STATES: Record<NotifierState, \"pending\" | \"success\" | \"failed\" | \"canceled\"> = {\n pending: \"pending\",\n success: \"success\",\n failure: \"failed\",\n cancelled: \"canceled\",\n};\n\n/** Map a StoryShelf build state to a GitLab pipeline status; an unknown state is `failed`. */\nexport function mapState(state: NotifierState): \"pending\" | \"success\" | \"failed\" | \"canceled\" {\n return STATES[state] ?? \"failed\";\n}\n"],"mappings":";;;;;AAGA,MAAM,SAA+E;CACnF,SAAS;CACT,SAAS;CACT,SAAS;CACT,WAAW;AACb;;AAGA,SAAgB,SAAS,OAAqE;CAC5F,OAAO,OAAO,UAAU;AAC1B"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { apiBase, gitlabHeaders, projectId } from "./gitlab-api.mjs";
|
|
5
|
+
import { httpJson } from "@storyshelf/core/utils";
|
|
6
|
+
//#region src/gitlab-pr.ts
|
|
7
|
+
/** Find the merge request IID associated with a commit SHA, if any. */
|
|
8
|
+
async function findMrIid(opts) {
|
|
9
|
+
try {
|
|
10
|
+
const url = `${apiBase(opts.host)}/api/v4/projects/${projectId(opts.owner, opts.repo)}/repository/commits/${encodeURIComponent(opts.sha)}/merge_requests`;
|
|
11
|
+
return (await httpJson(url, { headers: gitlabHeaders(opts.token) }))[0]?.iid;
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { findMrIid };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=gitlab-pr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-pr.mjs","names":[],"sources":["../src/gitlab-pr.ts"],"sourcesContent":["import { httpJson } from \"@storyshelf/core/utils\";\nimport { apiBase, gitlabHeaders, projectId } from \"./gitlab-api.ts\";\n\ninterface CommitMergeRequest {\n iid: number;\n}\n\n/** Find the merge request IID associated with a commit SHA, if any. */\nexport async function findMrIid(opts: {\n host?: string;\n owner: string;\n repo: string;\n token: string;\n sha: string;\n}): Promise<number | undefined> {\n try {\n const base = apiBase(opts.host);\n const pid = projectId(opts.owner, opts.repo);\n const url = `${base}/api/v4/projects/${pid}/repository/commits/${encodeURIComponent(opts.sha)}/merge_requests`;\n const data = await httpJson<CommitMergeRequest[]>(url, {\n headers: gitlabHeaders(opts.token),\n });\n return data[0]?.iid;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;AAQA,eAAsB,UAAU,MAMA;CAC9B,IAAI;EAGF,MAAM,MAAM,GAFC,QAAQ,KAAK,IAER,EAAE,mBADR,UAAU,KAAK,OAAO,KAAK,IACE,EAAE,sBAAsB,mBAAmB,KAAK,GAAG,EAAE;EAI9F,QAAO,MAHY,SAA+B,KAAK,EACrD,SAAS,cAAc,KAAK,KAAK,EACnC,CAAC,EAAA,CACW,EAAE,EAAE;CAClB,QAAQ;EACN;CACF;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { apiBase, gitlabHeaders, projectId } from "./gitlab-api.mjs";
|
|
5
|
+
import { mapState } from "./gitlab-mapper.mjs";
|
|
6
|
+
import { httpJson } from "@storyshelf/core/utils";
|
|
7
|
+
//#region src/gitlab-status.ts
|
|
8
|
+
/**
|
|
9
|
+
* Post a commit status to GitLab under the full `context` name the caller chose
|
|
10
|
+
* (`<prefix>/testing`); the provider adds no prefix of its own. Posting the same
|
|
11
|
+
* `(sha, name)` again replaces the state, so it is idempotent and latest wins.
|
|
12
|
+
*/
|
|
13
|
+
async function postCommitStatus(project, input) {
|
|
14
|
+
const base = apiBase(project.host);
|
|
15
|
+
const pid = projectId(project.owner, project.repo);
|
|
16
|
+
const state = mapState(input.state);
|
|
17
|
+
project.logger?.debug({
|
|
18
|
+
context: input.context,
|
|
19
|
+
sha: input.sha,
|
|
20
|
+
state
|
|
21
|
+
}, "posting commit status");
|
|
22
|
+
try {
|
|
23
|
+
await httpJson(`${base}/api/v4/projects/${pid}/statuses/${encodeURIComponent(input.sha)}`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: gitlabHeaders(project.token),
|
|
26
|
+
json: {
|
|
27
|
+
state,
|
|
28
|
+
...input.url === void 0 ? {} : { target_url: input.url },
|
|
29
|
+
description: input.description,
|
|
30
|
+
name: input.context,
|
|
31
|
+
context: input.context
|
|
32
|
+
},
|
|
33
|
+
logger: project.logger
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
project.logger?.error({
|
|
37
|
+
err: error,
|
|
38
|
+
context: input.context,
|
|
39
|
+
sha: input.sha
|
|
40
|
+
}, "failed to post commit status");
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { postCommitStatus };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=gitlab-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-status.mjs","names":[],"sources":["../src/gitlab-status.ts"],"sourcesContent":["import type { NotifierState } from \"@storyshelf/core/adapter/notifier\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { httpJson } from \"@storyshelf/core/utils\";\nimport { apiBase, gitlabHeaders, projectId } from \"./gitlab-api.ts\";\nimport { mapState } from \"./gitlab-mapper.ts\";\n\n/** Coordinates and credential of one GitLab project. */\nexport interface GitLabProject {\n token: string;\n owner: string;\n repo: string;\n /** GitLab host URL; `https://gitlab.com` when undefined. */\n host: string | undefined;\n logger?: Logger | undefined;\n}\n\n/**\n * Post a commit status to GitLab under the full `context` name the caller chose\n * (`<prefix>/testing`); the provider adds no prefix of its own. Posting the same\n * `(sha, name)` again replaces the state, so it is idempotent and latest wins.\n */\nexport async function postCommitStatus(\n project: GitLabProject,\n input: {\n sha: string;\n context: string;\n state: NotifierState;\n description: string;\n /** Absent while no public origin is known. */\n url?: string | undefined;\n },\n): Promise<void> {\n const base = apiBase(project.host);\n const pid = projectId(project.owner, project.repo);\n const state = mapState(input.state);\n project.logger?.debug({ context: input.context, sha: input.sha, state }, \"posting commit status\");\n try {\n await httpJson(`${base}/api/v4/projects/${pid}/statuses/${encodeURIComponent(input.sha)}`, {\n method: \"POST\",\n headers: gitlabHeaders(project.token),\n json: {\n state,\n ...(input.url === undefined ? {} : { target_url: input.url }),\n description: input.description,\n name: input.context,\n context: input.context,\n },\n logger: project.logger,\n });\n } catch (error) {\n project.logger?.error(\n { err: error, context: input.context, sha: input.sha },\n \"failed to post commit status\",\n );\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAqBA,eAAsB,iBACpB,SACA,OAQe;CACf,MAAM,OAAO,QAAQ,QAAQ,IAAI;CACjC,MAAM,MAAM,UAAU,QAAQ,OAAO,QAAQ,IAAI;CACjD,MAAM,QAAQ,SAAS,MAAM,KAAK;CAClC,QAAQ,QAAQ,MAAM;EAAE,SAAS,MAAM;EAAS,KAAK,MAAM;EAAK;CAAM,GAAG,uBAAuB;CAChG,IAAI;EACF,MAAM,SAAS,GAAG,KAAK,mBAAmB,IAAI,YAAY,mBAAmB,MAAM,GAAG,KAAK;GACzF,QAAQ;GACR,SAAS,cAAc,QAAQ,KAAK;GACpC,MAAM;IACJ;IACA,GAAI,MAAM,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,MAAM,IAAI;IAC3D,aAAa,MAAM;IACnB,MAAM,MAAM;IACZ,SAAS,MAAM;GACjB;GACA,QAAQ,QAAQ;EAClB,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,QAAQ,MACd;GAAE,KAAK;GAAO,SAAS,MAAM;GAAS,KAAK,MAAM;EAAI,GACrD,8BACF;EACA,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { StateNotifierProvider } from "@storyshelf/core/adapter/notifier";
|
|
4
|
+
//#region src/gitlab.d.ts
|
|
5
|
+
/** GitLab commit status and merge-request note provider (`state` delivery). */
|
|
6
|
+
declare const gitLabNotifier: StateNotifierProvider;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { gitLabNotifier };
|
|
9
|
+
//# sourceMappingURL=gitlab.d.mts.map
|
package/dist/gitlab.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { createStateAdapter, requireToken } from "./state-adapter.mjs";
|
|
5
|
+
import { packageVersion } from "./version.mjs";
|
|
6
|
+
import { upsertMrNote } from "./gitlab-comment.mjs";
|
|
7
|
+
import { gitlabConfigSchema } from "./gitlab-config.mjs";
|
|
8
|
+
import { postCommitStatus } from "./gitlab-status.mjs";
|
|
9
|
+
//#region src/gitlab.ts
|
|
10
|
+
/** GitLab commit status and merge-request note provider (`state` delivery). */
|
|
11
|
+
const gitLabNotifier = {
|
|
12
|
+
metadata: getMetadata(),
|
|
13
|
+
create(opts) {
|
|
14
|
+
const cfg = gitlabConfigSchema.parse(opts.config);
|
|
15
|
+
const logger = opts.logger?.child({ component: "notify-git-gitlab" });
|
|
16
|
+
const project = {
|
|
17
|
+
token: requireToken(opts.secret, "GitLab"),
|
|
18
|
+
owner: cfg.owner,
|
|
19
|
+
repo: cfg.repo,
|
|
20
|
+
host: cfg.host,
|
|
21
|
+
logger
|
|
22
|
+
};
|
|
23
|
+
return createStateAdapter(getMetadata(), {
|
|
24
|
+
status: async (input) => {
|
|
25
|
+
await postCommitStatus(project, input);
|
|
26
|
+
},
|
|
27
|
+
comment: async (input) => await upsertMrNote(project, {
|
|
28
|
+
...input,
|
|
29
|
+
logger
|
|
30
|
+
})
|
|
31
|
+
}, logger);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Describe the GitLab provider (name, version, config schema). */
|
|
35
|
+
function getMetadata() {
|
|
36
|
+
return {
|
|
37
|
+
name: "GitLab",
|
|
38
|
+
version: packageVersion(),
|
|
39
|
+
description: "Commit statuses and merge-request notes via the GitLab API",
|
|
40
|
+
kind: "gitlab",
|
|
41
|
+
category: "notifier",
|
|
42
|
+
delivery: "state",
|
|
43
|
+
logo: "gitlab",
|
|
44
|
+
schema: gitlabConfigSchema
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { getMetadata, gitLabNotifier };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=gitlab.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab.mjs","names":[],"sources":["../src/gitlab.ts"],"sourcesContent":["import type { StateNotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { upsertMrNote } from \"./gitlab-comment.ts\";\nimport { gitlabConfigSchema } from \"./gitlab-config.ts\";\nimport { postCommitStatus, type GitLabProject } from \"./gitlab-status.ts\";\nimport { createStateAdapter, requireToken } from \"./state-adapter.ts\";\nimport { packageVersion } from \"./version.ts\";\n\n/** GitLab commit status and merge-request note provider (`state` delivery). */\nexport const gitLabNotifier: StateNotifierProvider = {\n metadata: getMetadata(),\n create(opts: { config: unknown; secret?: string; logger?: Logger }) {\n const cfg = gitlabConfigSchema.parse(opts.config);\n const logger = opts.logger?.child({ component: \"notify-git-gitlab\" });\n const project: GitLabProject = {\n token: requireToken(opts.secret, \"GitLab\"),\n owner: cfg.owner,\n repo: cfg.repo,\n host: cfg.host,\n logger,\n };\n return createStateAdapter(\n getMetadata(),\n {\n status: async (input) => {\n await postCommitStatus(project, input);\n },\n comment: async (input) => await upsertMrNote(project, { ...input, logger }),\n },\n logger,\n );\n },\n};\n\n/** Describe the GitLab provider (name, version, config schema). */\nexport function getMetadata(): StateNotifierProvider[\"metadata\"] {\n return {\n name: \"GitLab\",\n version: packageVersion(),\n description: \"Commit statuses and merge-request notes via the GitLab API\",\n kind: \"gitlab\",\n category: \"notifier\",\n delivery: \"state\",\n logo: \"gitlab\",\n schema: gitlabConfigSchema,\n };\n}\n"],"mappings":";;;;;;;;;;AASA,MAAa,iBAAwC;CACnD,UAAU,YAAY;CACtB,OAAO,MAA6D;EAClE,MAAM,MAAM,mBAAmB,MAAM,KAAK,MAAM;EAChD,MAAM,SAAS,KAAK,QAAQ,MAAM,EAAE,WAAW,oBAAoB,CAAC;EACpE,MAAM,UAAyB;GAC7B,OAAO,aAAa,KAAK,QAAQ,QAAQ;GACzC,OAAO,IAAI;GACX,MAAM,IAAI;GACV,MAAM,IAAI;GACV;EACF;EACA,OAAO,mBACL,YAAY,GACZ;GACE,QAAQ,OAAO,UAAU;IACvB,MAAM,iBAAiB,SAAS,KAAK;GACvC;GACA,SAAS,OAAO,UAAU,MAAM,aAAa,SAAS;IAAE,GAAG;IAAO;GAAO,CAAC;EAC5E,GACA,MACF;CACF;AACF;;AAGA,SAAgB,cAAiD;CAC/D,OAAO;EACL,MAAM;EACN,SAAS,eAAe;EACxB,aAAa;EACb,MAAM;EACN,UAAU;EACV,UAAU;EACV,MAAM;EACN,QAAQ;CACV;AACF"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { gitHubNotifier } from "./github.mjs";
|
|
4
|
+
import { gitLabNotifier } from "./gitlab.mjs";
|
|
5
|
+
import { gitLogNotifier, logConfigSchema } from "./log.mjs";
|
|
6
|
+
import { githubConfigSchema } from "./github-config.mjs";
|
|
7
|
+
import { gitlabConfigSchema } from "./gitlab-config.mjs";
|
|
8
|
+
//#region src/index.d.ts
|
|
9
|
+
/** All git providers in one list for `ShelfOptions.notifications.notifiers`. */
|
|
10
|
+
declare const gitNotifiers: import("@storyshelf/core/adapter/notifier").StateNotifierProvider[];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { gitHubNotifier, gitLabNotifier, gitLogNotifier, gitNotifiers, githubConfigSchema, gitlabConfigSchema, logConfigSchema };
|
|
13
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { githubConfigSchema } from "./github-config.mjs";
|
|
5
|
+
import { gitHubNotifier } from "./github.mjs";
|
|
6
|
+
import { gitlabConfigSchema } from "./gitlab-config.mjs";
|
|
7
|
+
import { gitLabNotifier } from "./gitlab.mjs";
|
|
8
|
+
import { gitLogNotifier, logConfigSchema } from "./log.mjs";
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
/**
|
|
11
|
+
* Git notification providers: `state` channels that make GitHub or GitLab show a build's
|
|
12
|
+
* current commit status and pull-request comment (plus a log provider for tests/dev).
|
|
13
|
+
*/
|
|
14
|
+
/** All git providers in one list for `ShelfOptions.notifications.notifiers`. */
|
|
15
|
+
const gitNotifiers = [
|
|
16
|
+
gitHubNotifier,
|
|
17
|
+
gitLabNotifier,
|
|
18
|
+
gitLogNotifier
|
|
19
|
+
];
|
|
20
|
+
//#endregion
|
|
21
|
+
export { gitHubNotifier, gitLabNotifier, gitLogNotifier, gitNotifiers, githubConfigSchema, gitlabConfigSchema, logConfigSchema };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Git notification providers: `state` channels that make GitHub or GitLab show a build's\n * current commit status and pull-request comment (plus a log provider for tests/dev).\n */\nimport { gitHubNotifier } from \"./github.ts\";\nimport { gitLabNotifier } from \"./gitlab.ts\";\nimport { gitLogNotifier } from \"./log.ts\";\n\n/** GitHub commit status + pull-request comment provider (`github`). */\nexport { gitHubNotifier };\n/** GitLab commit status + merge-request note provider (`gitlab`). */\nexport { gitLabNotifier };\n/** Log-only provider for tests, dev, and unconfigured servers (`git-log`). */\nexport { gitLogNotifier };\n\n/** All git providers in one list for `ShelfOptions.notifications.notifiers`. */\nexport const gitNotifiers = [gitHubNotifier, gitLabNotifier, gitLogNotifier];\n\nexport { githubConfigSchema } from \"./github-config.ts\";\nexport { gitlabConfigSchema } from \"./gitlab-config.ts\";\nexport { logConfigSchema } from \"./log.ts\";\n"],"mappings":";;;;;;;;;;;;;;AAgBA,MAAa,eAAe;CAAC;CAAgB;CAAgB;AAAc"}
|
package/dist/log.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { StateNotifierProvider } from "@storyshelf/core/adapter/notifier";
|
|
5
|
+
//#region src/log.d.ts
|
|
6
|
+
/** Validation schema for the log provider config (accepts anything). */
|
|
7
|
+
declare const logConfigSchema: z.ZodObject<{}, z.core.$loose>;
|
|
8
|
+
/** Log-only git provider for tests, dev, and unconfigured servers (`git-log`). */
|
|
9
|
+
declare const gitLogNotifier: StateNotifierProvider;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { gitLogNotifier, logConfigSchema };
|
|
12
|
+
//# sourceMappingURL=log.d.mts.map
|
package/dist/log.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { packageVersion } from "./version.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/log.ts
|
|
7
|
+
/** Validation schema for the log provider config (accepts anything). */
|
|
8
|
+
const logConfigSchema = z.object({}).loose();
|
|
9
|
+
/** Log-only git provider for tests, dev, and unconfigured servers (`git-log`). */
|
|
10
|
+
const gitLogNotifier = {
|
|
11
|
+
metadata: getMetadata(),
|
|
12
|
+
create(opts) {
|
|
13
|
+
logConfigSchema.parse(opts.config);
|
|
14
|
+
const logger = opts.logger?.child({ component: "notify-git-log" }) ?? opts.logger;
|
|
15
|
+
return {
|
|
16
|
+
metadata: getMetadata(),
|
|
17
|
+
async setState(input) {
|
|
18
|
+
logger?.info({
|
|
19
|
+
channelId: input.channel.id,
|
|
20
|
+
target: input.target,
|
|
21
|
+
state: input.state
|
|
22
|
+
}, "git state (log)");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** Describe the log provider (name, version, config schema). */
|
|
28
|
+
function getMetadata() {
|
|
29
|
+
return {
|
|
30
|
+
name: "Git log",
|
|
31
|
+
version: packageVersion(),
|
|
32
|
+
description: "Git statuses and comments to the server log (tests and dev)",
|
|
33
|
+
kind: "git-log",
|
|
34
|
+
category: "notifier",
|
|
35
|
+
delivery: "state",
|
|
36
|
+
schema: logConfigSchema
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { getMetadata, gitLogNotifier, logConfigSchema };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=log.mjs.map
|
package/dist/log.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.mjs","names":[],"sources":["../src/log.ts"],"sourcesContent":["import type { StateNotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { z } from \"zod\";\nimport { packageVersion } from \"./version.ts\";\n\n/** Validation schema for the log provider config (accepts anything). */\nexport const logConfigSchema = z.object({}).loose();\n\n/** Log-only git provider for tests, dev, and unconfigured servers (`git-log`). */\nexport const gitLogNotifier: StateNotifierProvider = {\n metadata: getMetadata(),\n create(opts: { config: unknown; secret?: string; logger?: Logger }) {\n logConfigSchema.parse(opts.config);\n const logger = opts.logger?.child({ component: \"notify-git-log\" }) ?? opts.logger;\n return {\n metadata: getMetadata(),\n // oxlint-disable-next-line eslint/require-await -- setState is async by contract\n async setState(input): Promise<void> {\n logger?.info(\n { channelId: input.channel.id, target: input.target, state: input.state },\n \"git state (log)\",\n );\n },\n };\n },\n};\n\n/** Describe the log provider (name, version, config schema). */\nexport function getMetadata(): StateNotifierProvider[\"metadata\"] {\n return {\n name: \"Git log\",\n version: packageVersion(),\n description: \"Git statuses and comments to the server log (tests and dev)\",\n kind: \"git-log\",\n category: \"notifier\",\n delivery: \"state\",\n schema: logConfigSchema,\n };\n}\n"],"mappings":";;;;;;;AAMA,MAAa,kBAAkB,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM;;AAGlD,MAAa,iBAAwC;CACnD,UAAU,YAAY;CACtB,OAAO,MAA6D;EAClE,gBAAgB,MAAM,KAAK,MAAM;EACjC,MAAM,SAAS,KAAK,QAAQ,MAAM,EAAE,WAAW,iBAAiB,CAAC,KAAK,KAAK;EAC3E,OAAO;GACL,UAAU,YAAY;GAEtB,MAAM,SAAS,OAAsB;IACnC,QAAQ,KACN;KAAE,WAAW,MAAM,QAAQ;KAAI,QAAQ,MAAM;KAAQ,OAAO,MAAM;IAAM,GACxE,iBACF;GACF;EACF;CACF;AACF;;AAGA,SAAgB,cAAiD;CAC/D,OAAO;EACL,MAAM;EACN,SAAS,eAAe;EACxB,aAAa;EACb,MAAM;EACN,UAAU;EACV,UAAU;EACV,QAAQ;CACV;AACF"}
|
package/dist/prefix.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/prefix.ts
|
|
6
|
+
/**
|
|
7
|
+
* Status-name prefix of a git channel (`<prefix>/testing`). It forms part of the status
|
|
8
|
+
* context on the host, so it is limited to characters both hosts accept in a name.
|
|
9
|
+
*/
|
|
10
|
+
const statusPrefixSchema = z.string().min(1).max(64).regex(/^[\w.-]+$/u, "Use letters, digits, dots, dashes and underscores");
|
|
11
|
+
//#endregion
|
|
12
|
+
export { statusPrefixSchema };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=prefix.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefix.mjs","names":[],"sources":["../src/prefix.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Status-name prefix of a git channel (`<prefix>/testing`). It forms part of the status\n * context on the host, so it is limited to characters both hosts accept in a name.\n */\nexport const statusPrefixSchema = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[\\w.-]+$/u, \"Use letters, digits, dots, dashes and underscores\");\n"],"mappings":";;;;;;;;;AAMA,MAAa,qBAAqB,EAC/B,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,EAAE,CAAC,CACP,MAAM,cAAc,mDAAmD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { commentMarker } from "./describe.mjs";
|
|
5
|
+
//#region src/review-comment.ts
|
|
6
|
+
/** Resolve the thread number, preferring the explicit value. */
|
|
7
|
+
async function resolveThreadNumber(opts) {
|
|
8
|
+
return opts.prNumber ?? await opts.resolveNumber();
|
|
9
|
+
}
|
|
10
|
+
/** Update the marked comment if present, else create it. */
|
|
11
|
+
async function findOrCreateComment(thread, marker, body, prNumber, logger) {
|
|
12
|
+
const existing = (await thread.list()).find((comment) => comment.body.includes(marker));
|
|
13
|
+
if (existing) {
|
|
14
|
+
const id = await thread.update(existing.id, body);
|
|
15
|
+
logger?.info({
|
|
16
|
+
prNumber,
|
|
17
|
+
commentId: id
|
|
18
|
+
}, "review comment updated");
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
const id = await thread.create(body);
|
|
22
|
+
logger?.info({
|
|
23
|
+
prNumber,
|
|
24
|
+
commentId: id
|
|
25
|
+
}, "review comment created");
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create or update the single StoryShelf review comment for a build,
|
|
30
|
+
* idempotent via the url marker. Returns the comment id, or `""` when no
|
|
31
|
+
* review thread exists for the commit.
|
|
32
|
+
*/
|
|
33
|
+
async function upsertReviewComment(opts) {
|
|
34
|
+
const marker = commentMarker(opts.url);
|
|
35
|
+
const body = `${marker}\n${opts.markdown}`;
|
|
36
|
+
const number = await resolveThreadNumber(opts);
|
|
37
|
+
if (number === void 0) {
|
|
38
|
+
opts.logger?.debug({ sha: opts.sha }, "no review thread found for comment, skipping");
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return await findOrCreateComment(opts.thread(number), marker, body, number, opts.logger);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
opts.logger?.error({
|
|
45
|
+
err: error,
|
|
46
|
+
prNumber: number
|
|
47
|
+
}, "failed to upsert review comment");
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { upsertReviewComment };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=review-comment.mjs.map
|