@vornrun/connector-gitlab 0.1.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/CHANGELOG.md +29 -0
- package/README.md +138 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +890 -0
- package/package.json +49 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@vornrun/connector-gitlab`.
|
|
4
|
+
|
|
5
|
+
## 0.1.0
|
|
6
|
+
|
|
7
|
+
First release.
|
|
8
|
+
|
|
9
|
+
Trigger a workflow from GitLab issues, merge requests and pipeline results, and
|
|
10
|
+
let a workflow step write back. Works against gitlab.com and self-managed
|
|
11
|
+
instances.
|
|
12
|
+
|
|
13
|
+
- **Triggers:** `issueCreated`, `mergeRequestOpened`, `pipelineFinished`.
|
|
14
|
+
- **Actions:** `createIssue`, `commentOnIssue`, `commentOnMergeRequest`,
|
|
15
|
+
`getProject`, `listOpenMergeRequests`, `getIssue`.
|
|
16
|
+
- **Signing in:** borrows the GitLab CLI's login. `glab auth login` is all it
|
|
17
|
+
needs, and no token is stored here — `glab config get token` supplies it on
|
|
18
|
+
demand. A pasted personal access token is used instead when one is given.
|
|
19
|
+
|
|
20
|
+
Every action but the merge request list is a declared request the SDK sends,
|
|
21
|
+
with the response trimmed to camelCase names; the list is hand-written so it can
|
|
22
|
+
answer with a `count` beside its `items`. The three read actions are idempotent
|
|
23
|
+
and carry sample arguments (`gitlab-org/gitlab`) so a live check can call them.
|
|
24
|
+
|
|
25
|
+
The triggers watermark on the field they filter by — `created_at` for issues
|
|
26
|
+
and merge requests, `updated_at` for pipelines — and only finished pipelines
|
|
27
|
+
are delivered, so one that was running when a poll saw it fires once it ends.
|
|
28
|
+
The first poll looks back one minute rather than replaying the project.
|
|
29
|
+
Ships a conformance receipt covering the mock run and the dedupe replay.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @vornrun/connector-gitlab
|
|
2
|
+
|
|
3
|
+
Trigger Vorn workflows from GitLab issues, merge requests and pipeline results,
|
|
4
|
+
and create or comment on issues and merge requests from a workflow step. Works
|
|
5
|
+
against gitlab.com and self-managed instances.
|
|
6
|
+
|
|
7
|
+
## Signing in
|
|
8
|
+
|
|
9
|
+
There is no token to paste. This connector borrows the GitLab CLI's login:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
brew install glab # or see https://gitlab.com/gitlab-org/cli
|
|
13
|
+
glab auth login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`glab config get token --host <host>` supplies the credential on demand, so it
|
|
17
|
+
lives wherever `glab` keeps it — the OS keychain, usually — and nothing is
|
|
18
|
+
stored in the connection. (`glab auth token` does not exist; `config get` is the
|
|
19
|
+
documented way to read what `glab auth login` stored.)
|
|
20
|
+
|
|
21
|
+
If you would rather not depend on `glab`, paste a
|
|
22
|
+
[personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/)
|
|
23
|
+
into the **Personal access token** field. It is used as-is and `glab` is never
|
|
24
|
+
run. Scope `api` covers everything here; `read_api` is enough for the triggers
|
|
25
|
+
and the read-only actions.
|
|
26
|
+
|
|
27
|
+
The token is sent as `Authorization: Bearer`, which GitLab accepts for both
|
|
28
|
+
personal access tokens and the OAuth tokens `glab auth login --web` stores. A
|
|
29
|
+
borrowed OAuth token lives two hours, so the triggers and the merge request
|
|
30
|
+
list re-read it from `glab` once on a `401` before reporting that you are
|
|
31
|
+
signed out. The other actions are declared requests and send the token the host
|
|
32
|
+
borrowed when it started the connector, which it does afresh on every start.
|
|
33
|
+
|
|
34
|
+
## Settings
|
|
35
|
+
|
|
36
|
+
| Field | Required | What it does |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `baseUrl` | no | Instance URL, default `https://gitlab.com`. Without a trailing slash; `/api/v4` is appended. |
|
|
39
|
+
| `project` | yes | Path such as `gitlab-org/gitlab`, or the numeric id. The triggers poll this project. |
|
|
40
|
+
| `token` | no | Personal access token. Leave empty to borrow `glab`'s login. |
|
|
41
|
+
| `ref` | no | Branch or tag whose pipelines to watch. Blank for every ref. |
|
|
42
|
+
|
|
43
|
+
Actions take their own `project` input, so one connection can act on several
|
|
44
|
+
projects while its triggers watch one.
|
|
45
|
+
|
|
46
|
+
## Triggers
|
|
47
|
+
|
|
48
|
+
**An issue is created**, **A merge request is opened** and **A pipeline
|
|
49
|
+
finishes**. Each polls the project's list endpoint, oldest first, for
|
|
50
|
+
everything at or after the last watermark, and follows `x-next-page` up to ten
|
|
51
|
+
pages of 100 per poll.
|
|
52
|
+
|
|
53
|
+
Things worth knowing if you are reading `src/connector.ts`:
|
|
54
|
+
|
|
55
|
+
- The watermark is the field the request filters on. The issue and merge
|
|
56
|
+
request triggers ask for `created_after`, so their items carry `created_at`
|
|
57
|
+
as `updatedAt`; the real `updated_at` rides along as `changedAt`. A poll cut
|
|
58
|
+
short by a limit could otherwise move the watermark past an item created
|
|
59
|
+
earlier but touched later, and never ask for it again.
|
|
60
|
+
- The very first poll, before any watermark exists, asks for the minute
|
|
61
|
+
before it rather than the project's whole history.
|
|
62
|
+
- GitLab's time filters are inclusive at second precision while timestamps
|
|
63
|
+
carry milliseconds, so the item sitting on the watermark comes back on the
|
|
64
|
+
next poll. The SDK recognises it by its `iid`; adding a second to the cursor
|
|
65
|
+
would skip whatever else was created in that second.
|
|
66
|
+
- Pipelines are polled with `updated_after`, because `updated_at` moves as a
|
|
67
|
+
pipeline runs. Only `success`, `failed`, `canceled` and `skipped` are
|
|
68
|
+
delivered. A running pipeline is neither delivered nor remembered, so it
|
|
69
|
+
fires once it finishes. A retried pipeline keeps its id and is not delivered
|
|
70
|
+
again — the cost of keying on the id.
|
|
71
|
+
- Lists over 10,000 records omit `x-total`, so the connector never reads it.
|
|
72
|
+
|
|
73
|
+
Status suggestions: issues `opened → todo`, `closed → done`; merge requests
|
|
74
|
+
`opened → in_progress`, `merged`/`closed → done`; pipelines `success → done`,
|
|
75
|
+
`failed → todo`, `canceled`/`skipped → cancelled`.
|
|
76
|
+
|
|
77
|
+
## Actions
|
|
78
|
+
|
|
79
|
+
| Action | Idempotent | Notes |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| Create an issue | no | Two identical calls make two issues; GitLab offers no idempotency key |
|
|
82
|
+
| Comment on an issue | no | Posts a note; `internal` hides it from non-members |
|
|
83
|
+
| Comment on a merge request | no | As above, on a merge request |
|
|
84
|
+
| Get a project | yes | `namespace` comes back as `{id, name, path, fullPath, kind}` |
|
|
85
|
+
| List open merge requests | yes | Newest-updated first; `limit` up to 100, optional `targetBranch`. Returns `count` and `items` in the shape the merge request trigger delivers |
|
|
86
|
+
| Get an issue | yes | `author` and `assignees` come back as `{id, username, name}` |
|
|
87
|
+
|
|
88
|
+
Every action but the merge request list is a declared request: the SDK fills
|
|
89
|
+
in the arguments, URL-encodes the project path (`gitlab-org%2Fgitlab`), sends
|
|
90
|
+
the call and keeps the fields named above under camelCase names. The merge
|
|
91
|
+
request list is hand-written, because a declared request cannot count what it
|
|
92
|
+
returns. Issue and merge request numbers are their
|
|
93
|
+
project-scoped `iid`, the number shown in the UI, and are checked to be numbers
|
|
94
|
+
before anything is sent, so a `{{...}}` that resolved to nothing names itself
|
|
95
|
+
rather than returning a 404.
|
|
96
|
+
|
|
97
|
+
## What this connector cannot do
|
|
98
|
+
|
|
99
|
+
- **No webhooks.** It polls. The default seeded workflows run every 5 minutes.
|
|
100
|
+
- **One project per connection** for the triggers.
|
|
101
|
+
- **No pipeline duration.** The list endpoint carries no `duration` or
|
|
102
|
+
`finished_at`; those need a per-pipeline call this connector does not make.
|
|
103
|
+
- **Nothing beyond the token's scope.** A `403` is reported with GitLab's own
|
|
104
|
+
message; an OAuth token lacking scope answers `insufficient_scope`.
|
|
105
|
+
|
|
106
|
+
Rate limits are honoured by the SDK: a `429` waits out `Retry-After` a bounded
|
|
107
|
+
number of times before the poll gives up and the next scheduled one tries again.
|
|
108
|
+
|
|
109
|
+
## Checks
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
yarn typecheck && yarn test && yarn build
|
|
113
|
+
yarn workspace @vornrun/connector-gitlab exec vorn-connector check ./dist/index.js --mock --receipt verified.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`scripts/check.sh` at the repository root runs exactly this for this package.
|
|
117
|
+
`scripts/check-live.sh` runs `vorn-connector check --live` against a real
|
|
118
|
+
instance when `GITLAB_TOKEN` is set, and exits 0 with a note when it is not.
|
|
119
|
+
|
|
120
|
+
## Built from
|
|
121
|
+
|
|
122
|
+
- [REST API overview: base path, URL encoding, pagination](https://docs.gitlab.com/api/rest/)
|
|
123
|
+
- [REST API authentication: PRIVATE-TOKEN, Bearer, OAuth, 401/403](https://docs.gitlab.com/api/rest/authentication/)
|
|
124
|
+
- [Issues API](https://docs.gitlab.com/api/issues/)
|
|
125
|
+
- [Merge requests API](https://docs.gitlab.com/api/merge_requests/)
|
|
126
|
+
- [Pipelines API](https://docs.gitlab.com/api/pipelines/)
|
|
127
|
+
- [Notes API (issue and merge request comments)](https://docs.gitlab.com/api/notes/)
|
|
128
|
+
- [Projects API](https://docs.gitlab.com/api/projects/)
|
|
129
|
+
- [Personal access tokens](https://docs.gitlab.com/user/profile/personal_access_tokens/)
|
|
130
|
+
- [Access token scopes](https://docs.gitlab.com/security/tokens/access_token_scopes/)
|
|
131
|
+
- [Rate limit response headers](https://docs.gitlab.com/administration/settings/user_and_ip_rate_limits/)
|
|
132
|
+
- [GitLab.com rate limits](https://docs.gitlab.com/user/gitlab_com/)
|
|
133
|
+
- [glab CLI repository](https://gitlab.com/gitlab-org/cli):
|
|
134
|
+
[README and environment variables](https://gitlab.com/gitlab-org/cli/-/blob/main/README.md),
|
|
135
|
+
[authentication](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/authentication.md),
|
|
136
|
+
[`glab auth login`](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/auth/login.md),
|
|
137
|
+
[`glab auth status`](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/auth/status.md),
|
|
138
|
+
[`glab config get`](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/config/get.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as _vornrun_connector_sdk from '@vornrun/connector-sdk';
|
|
2
|
+
import { ConnectorConfig, ConnectorItem } from '@vornrun/connector-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitLab's REST API, with the credential borrowed from the GitLab CLI.
|
|
6
|
+
*
|
|
7
|
+
* The connector keeps no token of its own unless someone pastes one. When the
|
|
8
|
+
* `token` field is empty it asks `glab config get token --host <host>` — the
|
|
9
|
+
* documented, machine-readable way to read what `glab auth login` stored —
|
|
10
|
+
* and sends the answer as `Authorization: Bearer`. Bearer rather than
|
|
11
|
+
* `PRIVATE-TOKEN` because `glab auth login --web` stores an OAuth token, which
|
|
12
|
+
* the `PRIVATE-TOKEN` header does not accept; a personal access token is fine
|
|
13
|
+
* either way.
|
|
14
|
+
*
|
|
15
|
+
* A borrowed credential can rotate underneath us — OAuth tokens live two hours
|
|
16
|
+
* — so a 401 re-reads it once and retries before giving up.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
declare function glabInstallHint(platform?: NodeJS.Platform): string;
|
|
20
|
+
declare class GlabNotFoundError extends Error {
|
|
21
|
+
readonly code = "GLAB_NOT_FOUND";
|
|
22
|
+
constructor();
|
|
23
|
+
}
|
|
24
|
+
declare class GlabSignedOutError extends Error {
|
|
25
|
+
readonly code = "GLAB_SIGNED_OUT";
|
|
26
|
+
constructor(detail?: string);
|
|
27
|
+
}
|
|
28
|
+
type RunGlab = (args: string[]) => Promise<string>;
|
|
29
|
+
/** Run `glab`, translating the two failures a user can actually do something about. */
|
|
30
|
+
declare const runGlab: RunGlab;
|
|
31
|
+
/** The instance URL with nothing after the host (or the relative root), trailing slash gone. */
|
|
32
|
+
declare function normalizeBaseUrl(value: unknown): string;
|
|
33
|
+
/** The `--host` `glab` keeps its login under: the host as typed at `glab auth login`. */
|
|
34
|
+
declare function hostOf(baseUrl: string): string;
|
|
35
|
+
declare function apiUrl(baseUrl: string, path: string, query?: Record<string, string | number | undefined>): string;
|
|
36
|
+
/** A project path is sent URL-encoded (`group%2Fproject`); a numeric id passes through. */
|
|
37
|
+
declare function projectSegment(project: unknown): string;
|
|
38
|
+
interface TokenSourceOptions {
|
|
39
|
+
/** A personal access token from the connection. When set, `glab` is never run. */
|
|
40
|
+
token?: string;
|
|
41
|
+
/** The instance the token must belong to. */
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
/** Injected in tests, so nothing spawns a process. */
|
|
44
|
+
glab?: RunGlab;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The current GitLab token, cached until something rejects it.
|
|
48
|
+
*
|
|
49
|
+
* Cached because `glab config get token` is a process spawn and a poll makes
|
|
50
|
+
* several calls; invalidated rather than expired because the CLI, not this
|
|
51
|
+
* connector, knows when a token rotates — the only reliable signal is a 401.
|
|
52
|
+
* A pasted token is never invalidated: re-reading it would give the same one.
|
|
53
|
+
*/
|
|
54
|
+
declare function createTokenSource(options?: TokenSourceOptions): {
|
|
55
|
+
/** Whether the token came from `glab`, and so could be re-read. */
|
|
56
|
+
borrowed: boolean;
|
|
57
|
+
get(): Promise<string>;
|
|
58
|
+
/** Drop a borrowed token so the next `get()` asks `glab` again. */
|
|
59
|
+
invalidate(): void;
|
|
60
|
+
};
|
|
61
|
+
type TokenSource = ReturnType<typeof createTokenSource>;
|
|
62
|
+
interface GitLabClientOptions {
|
|
63
|
+
config: ConnectorConfig;
|
|
64
|
+
/** The SDK's fetch, which already retries rate limits and gateway hiccups. */
|
|
65
|
+
fetch: typeof fetch;
|
|
66
|
+
tokens?: TokenSource;
|
|
67
|
+
glab?: RunGlab;
|
|
68
|
+
}
|
|
69
|
+
interface ListOptions {
|
|
70
|
+
/** Stop once this many items are in hand; the SDK truncates to it anyway. */
|
|
71
|
+
limit?: number;
|
|
72
|
+
/** Longest chain of pages one call follows. */
|
|
73
|
+
maxPages?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A client bound to the connection's token, which re-authenticates itself once.
|
|
77
|
+
*
|
|
78
|
+
* Every call goes through `get`, so the 401 path is shared rather than
|
|
79
|
+
* repeated per endpoint: read the token again, try once more. A second 401 is
|
|
80
|
+
* a real authorization problem and is reported as one.
|
|
81
|
+
*/
|
|
82
|
+
declare function createGitLabClient(options: GitLabClientOptions): {
|
|
83
|
+
baseUrl: string;
|
|
84
|
+
/** One GET, parsed as JSON. Whatever shape comes back is the caller's to judge. */
|
|
85
|
+
getJson<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
86
|
+
/**
|
|
87
|
+
* Every page of a list endpoint, following `x-next-page` (empty when there
|
|
88
|
+
* is no next page). Never `x-total`: lists over 10,000 records omit it.
|
|
89
|
+
*/
|
|
90
|
+
list<T>(path: string, query: Record<string, string | number | undefined>, listOptions?: ListOptions): Promise<T[]>;
|
|
91
|
+
};
|
|
92
|
+
type GitLabClient = ReturnType<typeof createGitLabClient>;
|
|
93
|
+
interface PreflightResult {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
message?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether this connector could run right now.
|
|
99
|
+
*
|
|
100
|
+
* Answers the states a user can correct — `glab` missing, `glab` present but
|
|
101
|
+
* signed out — and says what to do about each. Anything else is reported
|
|
102
|
+
* verbatim rather than guessed at. A pasted token is taken at its word: the
|
|
103
|
+
* first request will say if GitLab disagrees.
|
|
104
|
+
*/
|
|
105
|
+
declare function gitlabPreflight(options?: TokenSourceOptions): Promise<PreflightResult>;
|
|
106
|
+
|
|
107
|
+
interface GitLabConnectorOptions {
|
|
108
|
+
version?: string;
|
|
109
|
+
/** Injected in tests so nothing spawns `glab`. */
|
|
110
|
+
glab?: RunGlab;
|
|
111
|
+
/** Where preflight reads the token and instance from; defaults to the process environment. */
|
|
112
|
+
env?: NodeJS.ProcessEnv;
|
|
113
|
+
}
|
|
114
|
+
declare function createGitLabConnector(options?: GitLabConnectorOptions): _vornrun_connector_sdk.Connector;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* What GitLab returns, and how each becomes the item Vorn indexes.
|
|
118
|
+
*
|
|
119
|
+
* Kept apart from anything that talks to the network so the mappings — and
|
|
120
|
+
* the one non-obvious choice in them, which timestamp is the watermark — are
|
|
121
|
+
* tested directly against the payloads gitlab.com actually returned.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
interface GitLabUser {
|
|
125
|
+
id?: number;
|
|
126
|
+
username: string;
|
|
127
|
+
name?: string;
|
|
128
|
+
}
|
|
129
|
+
interface GitLabIssue {
|
|
130
|
+
id: number;
|
|
131
|
+
iid: number;
|
|
132
|
+
project_id: number;
|
|
133
|
+
title: string;
|
|
134
|
+
description?: string | null;
|
|
135
|
+
state: string;
|
|
136
|
+
created_at: string;
|
|
137
|
+
updated_at: string;
|
|
138
|
+
closed_at?: string | null;
|
|
139
|
+
labels?: string[];
|
|
140
|
+
author?: GitLabUser | null;
|
|
141
|
+
assignees?: GitLabUser[];
|
|
142
|
+
web_url: string;
|
|
143
|
+
issue_type?: string;
|
|
144
|
+
confidential?: boolean;
|
|
145
|
+
}
|
|
146
|
+
interface GitLabMergeRequest {
|
|
147
|
+
id: number;
|
|
148
|
+
iid: number;
|
|
149
|
+
project_id: number;
|
|
150
|
+
title: string;
|
|
151
|
+
description?: string | null;
|
|
152
|
+
state: string;
|
|
153
|
+
draft?: boolean;
|
|
154
|
+
created_at: string;
|
|
155
|
+
updated_at: string;
|
|
156
|
+
merged_at?: string | null;
|
|
157
|
+
closed_at?: string | null;
|
|
158
|
+
source_branch: string;
|
|
159
|
+
target_branch: string;
|
|
160
|
+
sha?: string;
|
|
161
|
+
author?: GitLabUser | null;
|
|
162
|
+
labels?: string[];
|
|
163
|
+
web_url: string;
|
|
164
|
+
has_conflicts?: boolean;
|
|
165
|
+
detailed_merge_status?: string;
|
|
166
|
+
}
|
|
167
|
+
interface GitLabPipeline {
|
|
168
|
+
id: number;
|
|
169
|
+
iid?: number;
|
|
170
|
+
project_id: number;
|
|
171
|
+
sha: string;
|
|
172
|
+
ref: string;
|
|
173
|
+
status: string;
|
|
174
|
+
source?: string;
|
|
175
|
+
created_at: string;
|
|
176
|
+
updated_at: string;
|
|
177
|
+
web_url: string;
|
|
178
|
+
name?: string | null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Pipeline statuses that will not change again, from the documented enum.
|
|
182
|
+
*
|
|
183
|
+
* The rest — `created`, `waiting_for_resource`, `preparing`,
|
|
184
|
+
* `waiting_for_callback`, `pending`, `running`, `canceling`, `manual`,
|
|
185
|
+
* `scheduled` — describe a pipeline still in flight. One of those is neither
|
|
186
|
+
* delivered nor remembered, so it fires once it finishes.
|
|
187
|
+
*/
|
|
188
|
+
declare const TERMINAL_PIPELINE_STATUSES: ReadonlySet<string>;
|
|
189
|
+
declare function isFinishedPipeline(pipeline: Pick<GitLabPipeline, 'status'>): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Map an issue onto the shape Vorn indexes.
|
|
192
|
+
*
|
|
193
|
+
* `updatedAt` is `created_at`, deliberately. The trigger asks GitLab for
|
|
194
|
+
* issues `created_after` its watermark, and the SDK sets that watermark from
|
|
195
|
+
* this field; if it carried `updated_at` instead, a poll cut short by `limit`
|
|
196
|
+
* could move the watermark past an issue created earlier but touched later,
|
|
197
|
+
* and that issue would never be asked for again. The real `updated_at` rides
|
|
198
|
+
* along as `changedAt` (`updatedAt` is a reserved key in `data`).
|
|
199
|
+
*/
|
|
200
|
+
declare function issueToItem(issue: GitLabIssue): ConnectorItem;
|
|
201
|
+
/** Map a merge request onto the shape Vorn indexes. Same watermark rule as issues. */
|
|
202
|
+
declare function mergeRequestToItem(mr: GitLabMergeRequest): ConnectorItem;
|
|
203
|
+
/**
|
|
204
|
+
* Map a pipeline onto the shape Vorn indexes.
|
|
205
|
+
*
|
|
206
|
+
* Here `updatedAt` really is `updated_at`: the trigger filters on
|
|
207
|
+
* `updated_after`, and a pipeline's `updated_at` moving as it runs is what
|
|
208
|
+
* makes a finished one reappear after the poll that first saw it running.
|
|
209
|
+
*/
|
|
210
|
+
declare function pipelineToItem(pipeline: GitLabPipeline): ConnectorItem;
|
|
211
|
+
|
|
212
|
+
declare const gitlabConnector: _vornrun_connector_sdk.Connector;
|
|
213
|
+
|
|
214
|
+
export { type GitLabClient, type GitLabConnectorOptions, type GitLabIssue, type GitLabMergeRequest, type GitLabPipeline, GlabNotFoundError, GlabSignedOutError, type PreflightResult, type RunGlab, TERMINAL_PIPELINE_STATUSES, type TokenSource, apiUrl, gitlabConnector as connector, createGitLabClient, createGitLabConnector, createTokenSource, gitlabConnector as default, gitlabConnector, gitlabPreflight, glabInstallHint, hostOf, isFinishedPipeline, issueToItem, mergeRequestToItem, normalizeBaseUrl, pipelineToItem, projectSegment, runGlab };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
|
|
6
|
+
// src/connector.ts
|
|
7
|
+
import {
|
|
8
|
+
defineConnector
|
|
9
|
+
} from "@vornrun/connector-sdk";
|
|
10
|
+
|
|
11
|
+
// src/client.ts
|
|
12
|
+
import { execFile } from "child_process";
|
|
13
|
+
import { promisify } from "util";
|
|
14
|
+
var execFileAsync = promisify(execFile);
|
|
15
|
+
var GLAB_TIMEOUT_MS = 1e4;
|
|
16
|
+
var API_PATH = "/api/v4";
|
|
17
|
+
var DEFAULT_BASE_URL = "https://gitlab.com";
|
|
18
|
+
var MAX_PAGE_SIZE = 100;
|
|
19
|
+
var MAX_ERROR_BODY = 300;
|
|
20
|
+
function glabInstallHint(platform = process.platform) {
|
|
21
|
+
switch (platform) {
|
|
22
|
+
case "darwin":
|
|
23
|
+
return "Install with Homebrew: `brew install glab`";
|
|
24
|
+
case "win32":
|
|
25
|
+
return "Install with winget: `winget install glab.glab` (or see https://gitlab.com/gitlab-org/cli)";
|
|
26
|
+
default:
|
|
27
|
+
return "Install from https://gitlab.com/gitlab-org/cli (releases carry .deb, .rpm and tarballs)";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var GlabNotFoundError = class extends Error {
|
|
31
|
+
code = "GLAB_NOT_FOUND";
|
|
32
|
+
constructor() {
|
|
33
|
+
super(`GitLab CLI (glab) not found on PATH. ${glabInstallHint()}`);
|
|
34
|
+
this.name = "GlabNotFoundError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var GlabSignedOutError = class extends Error {
|
|
38
|
+
code = "GLAB_SIGNED_OUT";
|
|
39
|
+
constructor(detail) {
|
|
40
|
+
super(`Not signed in to GitLab. Run \`glab auth login\`.${detail ? `
|
|
41
|
+
${detail}` : ""}`);
|
|
42
|
+
this.name = "GlabSignedOutError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var runGlab = async (args) => {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execFileAsync("glab", args, { timeout: GLAB_TIMEOUT_MS });
|
|
48
|
+
return stdout;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === "ENOENT") throw new GlabNotFoundError();
|
|
51
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
52
|
+
throw new Error(stderr || (error instanceof Error ? error.message : String(error)));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function normalizeBaseUrl(value) {
|
|
56
|
+
const trimmed = String(value ?? "").trim();
|
|
57
|
+
const base = trimmed === "" ? DEFAULT_BASE_URL : trimmed;
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(base);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`GITLAB_BASE_URL is not a URL: "${base}"`);
|
|
63
|
+
}
|
|
64
|
+
return url.toString().replace(/\/+$/, "");
|
|
65
|
+
}
|
|
66
|
+
function hostOf(baseUrl) {
|
|
67
|
+
return new URL(normalizeBaseUrl(baseUrl)).host;
|
|
68
|
+
}
|
|
69
|
+
function apiUrl(baseUrl, path, query) {
|
|
70
|
+
const url = new URL(`${normalizeBaseUrl(baseUrl)}${API_PATH}${path}`);
|
|
71
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
72
|
+
if (value === void 0 || value === "") continue;
|
|
73
|
+
url.searchParams.set(key, String(value));
|
|
74
|
+
}
|
|
75
|
+
return url.toString();
|
|
76
|
+
}
|
|
77
|
+
function projectSegment(project) {
|
|
78
|
+
const value = String(project ?? "").trim();
|
|
79
|
+
if (!value) throw new Error("GITLAB_PROJECT is required");
|
|
80
|
+
return encodeURIComponent(value);
|
|
81
|
+
}
|
|
82
|
+
function createTokenSource(options = {}) {
|
|
83
|
+
const glab = options.glab ?? runGlab;
|
|
84
|
+
const pasted = String(options.token ?? "").trim();
|
|
85
|
+
const host = hostOf(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
86
|
+
let cached = pasted || void 0;
|
|
87
|
+
async function read() {
|
|
88
|
+
const token = (await glab(["config", "get", "token", "--host", host])).trim();
|
|
89
|
+
if (!token) throw new GlabSignedOutError(`glab has no token for ${host}.`);
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
/** Whether the token came from `glab`, and so could be re-read. */
|
|
94
|
+
borrowed: !pasted,
|
|
95
|
+
async get() {
|
|
96
|
+
return cached ??= await read();
|
|
97
|
+
},
|
|
98
|
+
/** Drop a borrowed token so the next `get()` asks `glab` again. */
|
|
99
|
+
invalidate() {
|
|
100
|
+
if (!pasted) cached = void 0;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function describeFailure(response) {
|
|
105
|
+
const text2 = await response.text().catch(() => "");
|
|
106
|
+
let detail = text2;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(text2);
|
|
109
|
+
const message = parsed.message ?? parsed.error;
|
|
110
|
+
if (message !== void 0) detail = typeof message === "string" ? message : JSON.stringify(message);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
|
|
114
|
+
return `GitLab API ${response.status}${quoted ? `: ${quoted}` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
var MAX_LIST_PAGES = 10;
|
|
117
|
+
function createGitLabClient(options) {
|
|
118
|
+
const baseUrl = normalizeBaseUrl(options.config.baseUrl);
|
|
119
|
+
const tokens = options.tokens ?? createTokenSource({
|
|
120
|
+
...options.config.token !== void 0 && { token: options.config.token },
|
|
121
|
+
baseUrl,
|
|
122
|
+
...options.glab && { glab: options.glab }
|
|
123
|
+
});
|
|
124
|
+
async function send(url) {
|
|
125
|
+
const token = await tokens.get();
|
|
126
|
+
return options.fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } });
|
|
127
|
+
}
|
|
128
|
+
async function get(url) {
|
|
129
|
+
let response = await send(url);
|
|
130
|
+
if (response.status === 401 && tokens.borrowed) {
|
|
131
|
+
tokens.invalidate();
|
|
132
|
+
response = await send(url);
|
|
133
|
+
if (response.status === 401) {
|
|
134
|
+
throw new GlabSignedOutError("The token from `glab config get token` was rejected twice.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (response.status === 401) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"GitLab rejected the personal access token (401). Check it has not expired and carries the api or read_api scope."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!response.ok) throw new Error(await describeFailure(response));
|
|
143
|
+
return response;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
baseUrl,
|
|
147
|
+
/** One GET, parsed as JSON. Whatever shape comes back is the caller's to judge. */
|
|
148
|
+
async getJson(path, query) {
|
|
149
|
+
const response = await get(apiUrl(baseUrl, path, query));
|
|
150
|
+
return await response.json();
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* Every page of a list endpoint, following `x-next-page` (empty when there
|
|
154
|
+
* is no next page). Never `x-total`: lists over 10,000 records omit it.
|
|
155
|
+
*/
|
|
156
|
+
async list(path, query, listOptions = {}) {
|
|
157
|
+
const maxPages = listOptions.maxPages ?? MAX_LIST_PAGES;
|
|
158
|
+
const collected = [];
|
|
159
|
+
let page = 1;
|
|
160
|
+
for (let index = 0; index < maxPages; index++) {
|
|
161
|
+
const response = await get(apiUrl(baseUrl, path, { ...query, per_page: MAX_PAGE_SIZE, page }));
|
|
162
|
+
const items = await response.json();
|
|
163
|
+
if (!Array.isArray(items)) {
|
|
164
|
+
throw new Error(`GitLab answered ${path} with something other than a list`);
|
|
165
|
+
}
|
|
166
|
+
collected.push(...items);
|
|
167
|
+
if (listOptions.limit !== void 0 && collected.length >= listOptions.limit) break;
|
|
168
|
+
const next = Number(response.headers.get("x-next-page") ?? "");
|
|
169
|
+
if (!Number.isInteger(next) || next <= page) break;
|
|
170
|
+
page = next;
|
|
171
|
+
}
|
|
172
|
+
return collected;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function gitlabPreflight(options = {}) {
|
|
177
|
+
try {
|
|
178
|
+
await createTokenSource(options).get();
|
|
179
|
+
return { ok: true };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/items.ts
|
|
186
|
+
var TERMINAL_PIPELINE_STATUSES = /* @__PURE__ */ new Set([
|
|
187
|
+
"success",
|
|
188
|
+
"failed",
|
|
189
|
+
"canceled",
|
|
190
|
+
"skipped"
|
|
191
|
+
]);
|
|
192
|
+
function isFinishedPipeline(pipeline) {
|
|
193
|
+
return TERMINAL_PIPELINE_STATUSES.has(pipeline.status);
|
|
194
|
+
}
|
|
195
|
+
function issueToItem(issue) {
|
|
196
|
+
return {
|
|
197
|
+
externalId: String(issue.iid),
|
|
198
|
+
title: issue.title,
|
|
199
|
+
url: issue.web_url,
|
|
200
|
+
description: issue.description ?? "",
|
|
201
|
+
status: issue.state,
|
|
202
|
+
labels: issue.labels ?? [],
|
|
203
|
+
...issue.assignees?.[0]?.username && { assignee: issue.assignees[0].username },
|
|
204
|
+
updatedAt: issue.created_at,
|
|
205
|
+
data: {
|
|
206
|
+
id: issue.id,
|
|
207
|
+
iid: issue.iid,
|
|
208
|
+
projectId: issue.project_id,
|
|
209
|
+
author: issue.author?.username ?? "",
|
|
210
|
+
assignees: (issue.assignees ?? []).map((user) => user.username),
|
|
211
|
+
createdAt: issue.created_at,
|
|
212
|
+
changedAt: issue.updated_at,
|
|
213
|
+
closedAt: issue.closed_at ?? null,
|
|
214
|
+
issueType: issue.issue_type ?? "issue",
|
|
215
|
+
confidential: issue.confidential === true
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function mergeRequestToItem(mr) {
|
|
220
|
+
return {
|
|
221
|
+
externalId: String(mr.iid),
|
|
222
|
+
title: mr.title,
|
|
223
|
+
url: mr.web_url,
|
|
224
|
+
description: mr.description ?? "",
|
|
225
|
+
status: mr.state,
|
|
226
|
+
labels: mr.labels ?? [],
|
|
227
|
+
updatedAt: mr.created_at,
|
|
228
|
+
data: {
|
|
229
|
+
id: mr.id,
|
|
230
|
+
iid: mr.iid,
|
|
231
|
+
projectId: mr.project_id,
|
|
232
|
+
sourceBranch: mr.source_branch,
|
|
233
|
+
targetBranch: mr.target_branch,
|
|
234
|
+
draft: mr.draft === true,
|
|
235
|
+
sha: mr.sha ?? "",
|
|
236
|
+
author: mr.author?.username ?? "",
|
|
237
|
+
createdAt: mr.created_at,
|
|
238
|
+
changedAt: mr.updated_at,
|
|
239
|
+
mergedAt: mr.merged_at ?? null,
|
|
240
|
+
closedAt: mr.closed_at ?? null,
|
|
241
|
+
hasConflicts: mr.has_conflicts === true,
|
|
242
|
+
detailedMergeStatus: mr.detailed_merge_status ?? ""
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function pipelineToItem(pipeline) {
|
|
247
|
+
return {
|
|
248
|
+
externalId: String(pipeline.id),
|
|
249
|
+
title: `${pipeline.name || pipeline.ref}: ${pipeline.status}`,
|
|
250
|
+
url: pipeline.web_url,
|
|
251
|
+
status: pipeline.status,
|
|
252
|
+
updatedAt: pipeline.updated_at,
|
|
253
|
+
data: {
|
|
254
|
+
id: pipeline.id,
|
|
255
|
+
iid: pipeline.iid ?? null,
|
|
256
|
+
projectId: pipeline.project_id,
|
|
257
|
+
ref: pipeline.ref,
|
|
258
|
+
sha: pipeline.sha,
|
|
259
|
+
source: pipeline.source ?? "",
|
|
260
|
+
name: pipeline.name ?? "",
|
|
261
|
+
createdAt: pipeline.created_at
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
var SAMPLE_ISSUE = {
|
|
266
|
+
id: 201377309,
|
|
267
|
+
iid: 627684,
|
|
268
|
+
project_id: 278964,
|
|
269
|
+
title: "Restructure Package Metadata Database documentation and split the offline quick start guide",
|
|
270
|
+
description: "This issue tracks a documentation follow-up agreed during review of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/...",
|
|
271
|
+
state: "opened",
|
|
272
|
+
created_at: "2026-09-04T02:23:26.054Z",
|
|
273
|
+
updated_at: "2026-09-04T03:10:09.633Z",
|
|
274
|
+
closed_at: null,
|
|
275
|
+
labels: ["automation:quick-win-judged", "documentation", "group::composition analysis", "type::maintenance"],
|
|
276
|
+
author: { id: 32685309, username: "azaydan", name: "Ahmad Zaydan" },
|
|
277
|
+
assignees: [],
|
|
278
|
+
web_url: "https://gitlab.com/gitlab-org/gitlab/-/work_items/627684",
|
|
279
|
+
issue_type: "issue",
|
|
280
|
+
confidential: false
|
|
281
|
+
};
|
|
282
|
+
var SAMPLE_MERGE_REQUEST = {
|
|
283
|
+
id: 527853893,
|
|
284
|
+
iid: 253583,
|
|
285
|
+
project_id: 278964,
|
|
286
|
+
title: "Add scope, engine and level properties to perform_search",
|
|
287
|
+
description: "Nothing in GitLab measured whether a search returned any results, so the zero-result rate could not be computed. This adds...",
|
|
288
|
+
state: "opened",
|
|
289
|
+
draft: false,
|
|
290
|
+
created_at: "2026-09-04T03:07:05.722Z",
|
|
291
|
+
updated_at: "2026-09-04T03:12:24.611Z",
|
|
292
|
+
merged_at: null,
|
|
293
|
+
closed_at: null,
|
|
294
|
+
source_branch: "wt/telemetry-zero-result",
|
|
295
|
+
target_branch: "master",
|
|
296
|
+
sha: "ae17207bc7b8ea3696979d9888fd25e0629c8bde",
|
|
297
|
+
author: { id: 9717668, username: "johnmason", name: "John Mason" },
|
|
298
|
+
labels: ["analytics instrumentation", "backend", "feature::addition", "type::feature"],
|
|
299
|
+
web_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/253583",
|
|
300
|
+
has_conflicts: false,
|
|
301
|
+
detailed_merge_status: "not_approved"
|
|
302
|
+
};
|
|
303
|
+
var SAMPLE_PIPELINE = {
|
|
304
|
+
id: 2818962738,
|
|
305
|
+
iid: 6291260,
|
|
306
|
+
project_id: 278964,
|
|
307
|
+
sha: "631b08ab8c69929b88af7f06d64500c3e5f400ae",
|
|
308
|
+
ref: "master",
|
|
309
|
+
status: "success",
|
|
310
|
+
source: "push",
|
|
311
|
+
created_at: "2026-09-04T03:12:16.050Z",
|
|
312
|
+
updated_at: "2026-09-04T03:12:20.492Z",
|
|
313
|
+
web_url: "https://gitlab.com/gitlab-org/gitlab/-/pipelines/2818962738",
|
|
314
|
+
name: "Ruby 3.3.12 master branch"
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/connector.ts
|
|
318
|
+
function text(value) {
|
|
319
|
+
const trimmed = String(value ?? "").trim();
|
|
320
|
+
return trimmed || void 0;
|
|
321
|
+
}
|
|
322
|
+
var AUTH_HEADERS = { Authorization: "Bearer {{config.token}}" };
|
|
323
|
+
var API = "{{config.baseUrl}}/api/v4";
|
|
324
|
+
var FIRST_POLL_LOOKBACK_MS = 6e4;
|
|
325
|
+
function pageSize(limit) {
|
|
326
|
+
if (typeof limit !== "number") return void 0;
|
|
327
|
+
return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(limit)));
|
|
328
|
+
}
|
|
329
|
+
function renames(mapping, path) {
|
|
330
|
+
return Object.entries(mapping).map(([from, to]) => ({
|
|
331
|
+
op: "rename",
|
|
332
|
+
from,
|
|
333
|
+
to,
|
|
334
|
+
...path !== void 0 && { path }
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
var ISSUE_SHAPE = [
|
|
338
|
+
{
|
|
339
|
+
op: "pick",
|
|
340
|
+
keys: [
|
|
341
|
+
"id",
|
|
342
|
+
"iid",
|
|
343
|
+
"project_id",
|
|
344
|
+
"title",
|
|
345
|
+
"description",
|
|
346
|
+
"state",
|
|
347
|
+
"web_url",
|
|
348
|
+
"labels",
|
|
349
|
+
"author",
|
|
350
|
+
"assignees",
|
|
351
|
+
"created_at",
|
|
352
|
+
"updated_at",
|
|
353
|
+
"closed_at",
|
|
354
|
+
"issue_type",
|
|
355
|
+
"confidential"
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
...renames({
|
|
359
|
+
project_id: "projectId",
|
|
360
|
+
web_url: "url",
|
|
361
|
+
created_at: "createdAt",
|
|
362
|
+
updated_at: "updatedAt",
|
|
363
|
+
closed_at: "closedAt",
|
|
364
|
+
issue_type: "issueType"
|
|
365
|
+
}),
|
|
366
|
+
{ op: "pick", keys: ["id", "username", "name"], path: "author" },
|
|
367
|
+
{ op: "pick", keys: ["id", "username", "name"], path: "assignees" }
|
|
368
|
+
];
|
|
369
|
+
var NOTE_SHAPE = [
|
|
370
|
+
{
|
|
371
|
+
op: "pick",
|
|
372
|
+
keys: ["id", "body", "author", "created_at", "noteable_iid", "noteable_type", "internal"]
|
|
373
|
+
},
|
|
374
|
+
...renames({ created_at: "createdAt", noteable_iid: "noteableIid", noteable_type: "noteableType" }),
|
|
375
|
+
{ op: "pick", keys: ["id", "username", "name"], path: "author" }
|
|
376
|
+
];
|
|
377
|
+
var NOTE_OUTPUTS = [
|
|
378
|
+
{ key: "id", type: "number", description: "The note id" },
|
|
379
|
+
{ key: "body", description: "The comment as posted" },
|
|
380
|
+
{ key: "author", description: "Who posted it, as {id, username, name}" },
|
|
381
|
+
{ key: "createdAt", description: "When it was posted" },
|
|
382
|
+
{ key: "noteableIid", type: "number", description: "The iid it was posted on" },
|
|
383
|
+
{ key: "noteableType", description: "Issue or MergeRequest" },
|
|
384
|
+
{ key: "internal", type: "boolean", description: "Whether only members can see it" }
|
|
385
|
+
];
|
|
386
|
+
var PROJECT_INPUT = {
|
|
387
|
+
key: "project",
|
|
388
|
+
label: "Project",
|
|
389
|
+
required: true,
|
|
390
|
+
description: "Project path such as group/project, or its numeric id",
|
|
391
|
+
builderHint: "The path is what the URL shows after the host; the id is on the project page. Either is sent URL-encoded as the :id segment."
|
|
392
|
+
};
|
|
393
|
+
var BODY_INPUT = {
|
|
394
|
+
key: "body",
|
|
395
|
+
label: "Comment",
|
|
396
|
+
required: true,
|
|
397
|
+
description: "Markdown, up to 1,000,000 characters"
|
|
398
|
+
};
|
|
399
|
+
var INTERNAL_INPUT = {
|
|
400
|
+
key: "internal",
|
|
401
|
+
label: "Internal note",
|
|
402
|
+
type: "boolean",
|
|
403
|
+
description: "Visible to project members only. Defaults to false.",
|
|
404
|
+
builderHint: "The documented replacement for the deprecated confidential flag on notes."
|
|
405
|
+
};
|
|
406
|
+
function createGitLabConnector(options = {}) {
|
|
407
|
+
const env = options.env ?? process.env;
|
|
408
|
+
const sources = /* @__PURE__ */ new Map();
|
|
409
|
+
function tokensFor(config) {
|
|
410
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
411
|
+
const token = text(config.token);
|
|
412
|
+
const key = `${baseUrl} ${token ?? ""}`;
|
|
413
|
+
let source = sources.get(key);
|
|
414
|
+
if (!source) {
|
|
415
|
+
source = createTokenSource({
|
|
416
|
+
...token !== void 0 && { token },
|
|
417
|
+
baseUrl,
|
|
418
|
+
...options.glab && { glab: options.glab }
|
|
419
|
+
});
|
|
420
|
+
sources.set(key, source);
|
|
421
|
+
}
|
|
422
|
+
return source;
|
|
423
|
+
}
|
|
424
|
+
function client(context) {
|
|
425
|
+
return createGitLabClient({
|
|
426
|
+
config: context.config,
|
|
427
|
+
fetch: context.fetch,
|
|
428
|
+
tokens: tokensFor(context.config)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function sinceOf(context) {
|
|
432
|
+
return context.since ?? new Date(Date.parse(context.now()) - FIRST_POLL_LOOKBACK_MS).toISOString();
|
|
433
|
+
}
|
|
434
|
+
function fetchIssues(context) {
|
|
435
|
+
return client(context).list(
|
|
436
|
+
`/projects/${projectSegment(context.config.project)}/issues`,
|
|
437
|
+
{
|
|
438
|
+
state: "all",
|
|
439
|
+
order_by: "created_at",
|
|
440
|
+
sort: "asc",
|
|
441
|
+
created_after: sinceOf(context)
|
|
442
|
+
},
|
|
443
|
+
{ ...context.limit !== void 0 && { limit: context.limit } }
|
|
444
|
+
).then((issues) => issues.map(issueToItem));
|
|
445
|
+
}
|
|
446
|
+
function fetchMergeRequests(context) {
|
|
447
|
+
return client(context).list(
|
|
448
|
+
`/projects/${projectSegment(context.config.project)}/merge_requests`,
|
|
449
|
+
{
|
|
450
|
+
state: "all",
|
|
451
|
+
order_by: "created_at",
|
|
452
|
+
sort: "asc",
|
|
453
|
+
created_after: sinceOf(context)
|
|
454
|
+
},
|
|
455
|
+
{ ...context.limit !== void 0 && { limit: context.limit } }
|
|
456
|
+
).then((mrs) => mrs.map(mergeRequestToItem));
|
|
457
|
+
}
|
|
458
|
+
async function fetchPipelines(context) {
|
|
459
|
+
const pipelines = await client(context).list(
|
|
460
|
+
`/projects/${projectSegment(context.config.project)}/pipelines`,
|
|
461
|
+
{
|
|
462
|
+
order_by: "updated_at",
|
|
463
|
+
sort: "asc",
|
|
464
|
+
updated_after: sinceOf(context),
|
|
465
|
+
ref: text(context.config.ref)
|
|
466
|
+
}
|
|
467
|
+
// No `limit` here: a page is mostly pipelines still running, and cutting
|
|
468
|
+
// the walk short on the raw count would starve the finished ones behind them.
|
|
469
|
+
);
|
|
470
|
+
return pipelines.filter(isFinishedPipeline).map(pipelineToItem);
|
|
471
|
+
}
|
|
472
|
+
return defineConnector({
|
|
473
|
+
id: "gitlab",
|
|
474
|
+
name: "GitLab",
|
|
475
|
+
...options.version && { version: options.version },
|
|
476
|
+
description: "Trigger workflows from GitLab issues, merge requests and pipeline results, and create or comment on issues and merge requests from a step.",
|
|
477
|
+
// GitLab's own mark.
|
|
478
|
+
icon: {
|
|
479
|
+
viewBox: "0 0 24 24",
|
|
480
|
+
paths: [
|
|
481
|
+
"m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8627 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8627 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"
|
|
482
|
+
]
|
|
483
|
+
},
|
|
484
|
+
auth: {
|
|
485
|
+
rung: "cli",
|
|
486
|
+
probe: { command: "glab", args: ["auth", "status"] },
|
|
487
|
+
// `glab auth token` does not exist; `config get token` is the documented
|
|
488
|
+
// way to read what `glab auth login` stored. The host runs it at spawn
|
|
489
|
+
// and hands the answer over as GITLAB_TOKEN, which is why that variable
|
|
490
|
+
// is also the `token` field's env: a host refuses to borrow a name the
|
|
491
|
+
// connector does not openly read. This is the gitlab.com default; the
|
|
492
|
+
// connector's own token source adds `--host` from `baseUrl` when it has
|
|
493
|
+
// to borrow for itself.
|
|
494
|
+
borrow: {
|
|
495
|
+
env: ["GITLAB_TOKEN"],
|
|
496
|
+
tokenArgs: ["glab", "config", "get", "token"],
|
|
497
|
+
tokenEnv: "GITLAB_TOKEN"
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
config: [
|
|
501
|
+
{
|
|
502
|
+
key: "baseUrl",
|
|
503
|
+
env: "GITLAB_BASE_URL",
|
|
504
|
+
label: "GitLab URL",
|
|
505
|
+
default: DEFAULT_BASE_URL,
|
|
506
|
+
description: "Instance URL without a trailing slash. Leave the default for gitlab.com.",
|
|
507
|
+
builderHint: "Every request goes to <baseUrl>/api/v4. A self-managed instance under a relative root (https://host/gitlab) works as-is. Declared requests append the path verbatim, so a trailing slash here would double it."
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
key: "project",
|
|
511
|
+
env: "GITLAB_PROJECT",
|
|
512
|
+
label: "Project",
|
|
513
|
+
required: true,
|
|
514
|
+
description: "Path such as group/project, or the numeric id. The triggers poll this project.",
|
|
515
|
+
builderHint: "Sent URL-encoded (group%2Fproject) as the :id segment, as the docs require for namespaced paths. Actions take their own project input so one connection can act on several."
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
key: "token",
|
|
519
|
+
env: "GITLAB_TOKEN",
|
|
520
|
+
label: "Personal access token",
|
|
521
|
+
secret: true,
|
|
522
|
+
description: "Leave empty to borrow the glab CLI login. Needs scope api, or read_api for the triggers and read-only actions.",
|
|
523
|
+
builderHint: "Created under Edit profile, Access, Personal access tokens, Generate token; prefixed glpat-, 365-day default expiry. Filled, it is sent as-is and glab is never run; empty, the connector asks glab config get token --host <host>."
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
key: "ref",
|
|
527
|
+
env: "GITLAB_REF",
|
|
528
|
+
label: "Pipeline ref",
|
|
529
|
+
description: "Branch or tag whose pipelines to watch. Blank for every ref.",
|
|
530
|
+
builderHint: "Sent as the ref filter on GET /projects/:id/pipelines; the other two triggers ignore it."
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
preflight: () => gitlabPreflight({
|
|
534
|
+
...text(env.GITLAB_TOKEN) !== void 0 && { token: env.GITLAB_TOKEN },
|
|
535
|
+
baseUrl: text(env.GITLAB_BASE_URL) ?? DEFAULT_BASE_URL,
|
|
536
|
+
...options.glab && { glab: options.glab }
|
|
537
|
+
}),
|
|
538
|
+
triggers: [
|
|
539
|
+
{
|
|
540
|
+
type: "issueCreated",
|
|
541
|
+
label: "An issue is created",
|
|
542
|
+
description: "Fires once for each issue created in the project since the last poll.",
|
|
543
|
+
// The SDK keeps the watermark and the ids sitting on it; the fetch
|
|
544
|
+
// asks GitLab for everything created on or after it.
|
|
545
|
+
dedupe: "timestamp",
|
|
546
|
+
fetch: fetchIssues,
|
|
547
|
+
statusMapping: [
|
|
548
|
+
{ upstream: "opened", suggestedLocal: "todo" },
|
|
549
|
+
{ upstream: "closed", suggestedLocal: "done" }
|
|
550
|
+
],
|
|
551
|
+
defaultWorkflow: { name: "GitLab: issues", defaultCronFromMinutes: 5 },
|
|
552
|
+
sample: [issueToItem(SAMPLE_ISSUE)]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
type: "mergeRequestOpened",
|
|
556
|
+
label: "A merge request is opened",
|
|
557
|
+
description: "Fires once for each merge request opened in the project since the last poll.",
|
|
558
|
+
dedupe: "timestamp",
|
|
559
|
+
fetch: fetchMergeRequests,
|
|
560
|
+
statusMapping: [
|
|
561
|
+
{ upstream: "opened", suggestedLocal: "in_progress" },
|
|
562
|
+
{ upstream: "merged", suggestedLocal: "done" },
|
|
563
|
+
{ upstream: "closed", suggestedLocal: "done" }
|
|
564
|
+
],
|
|
565
|
+
defaultWorkflow: { name: "GitLab: merge requests", defaultCronFromMinutes: 5 },
|
|
566
|
+
sample: [mergeRequestToItem(SAMPLE_MERGE_REQUEST)]
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
type: "pipelineFinished",
|
|
570
|
+
label: "A pipeline finishes",
|
|
571
|
+
description: "Fires once for each pipeline that reaches success, failed, canceled or skipped, with the status on the item.",
|
|
572
|
+
dedupe: "timestamp",
|
|
573
|
+
fetch: fetchPipelines,
|
|
574
|
+
// A failed pipeline is work to pick up; Vorn has no `blocked` status
|
|
575
|
+
// to suggest, so it lands as todo.
|
|
576
|
+
statusMapping: [
|
|
577
|
+
{ upstream: "success", suggestedLocal: "done" },
|
|
578
|
+
{ upstream: "failed", suggestedLocal: "todo" },
|
|
579
|
+
{ upstream: "canceled", suggestedLocal: "cancelled" },
|
|
580
|
+
{ upstream: "skipped", suggestedLocal: "cancelled" }
|
|
581
|
+
],
|
|
582
|
+
defaultWorkflow: { name: "GitLab: pipelines", defaultCronFromMinutes: 5 },
|
|
583
|
+
sample: [pipelineToItem(SAMPLE_PIPELINE)]
|
|
584
|
+
}
|
|
585
|
+
],
|
|
586
|
+
actions: [
|
|
587
|
+
{
|
|
588
|
+
type: "createIssue",
|
|
589
|
+
label: "Create an issue",
|
|
590
|
+
description: "Open a new issue in a project.",
|
|
591
|
+
// Two identical calls make two issues; GitLab offers no idempotency key.
|
|
592
|
+
idempotent: false,
|
|
593
|
+
inputs: [
|
|
594
|
+
PROJECT_INPUT,
|
|
595
|
+
{ key: "title", label: "Title", required: true, description: "Issue title" },
|
|
596
|
+
{
|
|
597
|
+
key: "description",
|
|
598
|
+
label: "Description",
|
|
599
|
+
description: "Markdown body, up to 1,048,576 characters"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
key: "labels",
|
|
603
|
+
label: "Labels",
|
|
604
|
+
description: "Comma-separated label names",
|
|
605
|
+
builderHint: "GitLab takes labels as one comma-separated string, so this is passed through as typed."
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
outputs: [
|
|
609
|
+
{ key: "id", type: "number", description: "Global issue id" },
|
|
610
|
+
{ key: "iid", type: "number", description: "The number shown in the project, as in #12" },
|
|
611
|
+
{ key: "url", description: "Where to read it" },
|
|
612
|
+
{ key: "title", description: "The title as saved" },
|
|
613
|
+
{ key: "state", description: "opened" },
|
|
614
|
+
{ key: "createdAt", description: "When it was created" }
|
|
615
|
+
],
|
|
616
|
+
request: {
|
|
617
|
+
method: "POST",
|
|
618
|
+
url: `${API}/projects/{{args.project}}/issues`,
|
|
619
|
+
headers: AUTH_HEADERS,
|
|
620
|
+
body: {
|
|
621
|
+
title: "{{args.title}}",
|
|
622
|
+
description: "{{args.description}}",
|
|
623
|
+
labels: "{{args.labels}}"
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
postReceive: [
|
|
627
|
+
{ op: "pick", keys: ["id", "iid", "web_url", "title", "state", "created_at"] },
|
|
628
|
+
...renames({ web_url: "url", created_at: "createdAt" })
|
|
629
|
+
]
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: "commentOnIssue",
|
|
633
|
+
label: "Comment on an issue",
|
|
634
|
+
description: "Post a note on an issue.",
|
|
635
|
+
// Two identical calls make two notes.
|
|
636
|
+
idempotent: false,
|
|
637
|
+
inputs: [
|
|
638
|
+
PROJECT_INPUT,
|
|
639
|
+
{
|
|
640
|
+
key: "iid",
|
|
641
|
+
label: "Issue number",
|
|
642
|
+
type: "number",
|
|
643
|
+
required: true,
|
|
644
|
+
description: "The issue number shown in the project (its iid, not the global id)"
|
|
645
|
+
},
|
|
646
|
+
BODY_INPUT,
|
|
647
|
+
INTERNAL_INPUT
|
|
648
|
+
],
|
|
649
|
+
outputs: NOTE_OUTPUTS,
|
|
650
|
+
request: {
|
|
651
|
+
method: "POST",
|
|
652
|
+
url: `${API}/projects/{{args.project}}/issues/{{args.iid}}/notes`,
|
|
653
|
+
headers: AUTH_HEADERS,
|
|
654
|
+
body: { body: "{{args.body}}", internal: "{{args.internal}}" }
|
|
655
|
+
},
|
|
656
|
+
postReceive: NOTE_SHAPE
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
type: "commentOnMergeRequest",
|
|
660
|
+
label: "Comment on a merge request",
|
|
661
|
+
description: "Post a note on a merge request.",
|
|
662
|
+
idempotent: false,
|
|
663
|
+
inputs: [
|
|
664
|
+
PROJECT_INPUT,
|
|
665
|
+
{
|
|
666
|
+
key: "iid",
|
|
667
|
+
label: "Merge request number",
|
|
668
|
+
type: "number",
|
|
669
|
+
required: true,
|
|
670
|
+
description: "The merge request number shown in the project (its iid)"
|
|
671
|
+
},
|
|
672
|
+
BODY_INPUT,
|
|
673
|
+
INTERNAL_INPUT
|
|
674
|
+
],
|
|
675
|
+
outputs: NOTE_OUTPUTS,
|
|
676
|
+
request: {
|
|
677
|
+
method: "POST",
|
|
678
|
+
url: `${API}/projects/{{args.project}}/merge_requests/{{args.iid}}/notes`,
|
|
679
|
+
headers: AUTH_HEADERS,
|
|
680
|
+
body: { body: "{{args.body}}", internal: "{{args.internal}}" }
|
|
681
|
+
},
|
|
682
|
+
postReceive: NOTE_SHAPE
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
type: "getProject",
|
|
686
|
+
label: "Get a project",
|
|
687
|
+
description: "Read a project by path or id.",
|
|
688
|
+
// Reading changes nothing.
|
|
689
|
+
idempotent: true,
|
|
690
|
+
inputs: [PROJECT_INPUT],
|
|
691
|
+
outputs: [
|
|
692
|
+
{ key: "id", type: "number", description: "Project id" },
|
|
693
|
+
{ key: "name", description: "Display name" },
|
|
694
|
+
{ key: "path", description: "The last segment of the path" },
|
|
695
|
+
{ key: "pathWithNamespace", description: "group/project" },
|
|
696
|
+
{ key: "description", description: "Project description" },
|
|
697
|
+
{ key: "defaultBranch", description: "Usually main or master" },
|
|
698
|
+
{ key: "visibility", description: "private, internal or public" },
|
|
699
|
+
{ key: "url", description: "Where to open it" },
|
|
700
|
+
{ key: "httpUrlToRepo", description: "Clone URL over HTTPS" },
|
|
701
|
+
{ key: "sshUrlToRepo", description: "Clone URL over SSH" },
|
|
702
|
+
{ key: "createdAt", description: "When the project was created" },
|
|
703
|
+
{ key: "lastActivityAt", description: "When something last happened in it" },
|
|
704
|
+
{ key: "archived", type: "boolean", description: "Whether it is archived" },
|
|
705
|
+
{
|
|
706
|
+
key: "namespace",
|
|
707
|
+
description: "The owning group or user, as {id, name, path, fullPath, kind}"
|
|
708
|
+
},
|
|
709
|
+
{ key: "starCount", type: "number", description: "Stars" },
|
|
710
|
+
{ key: "forksCount", type: "number", description: "Forks" },
|
|
711
|
+
{ key: "topics", description: "Topic names" }
|
|
712
|
+
],
|
|
713
|
+
// A public project on gitlab.com, so the live check works with read_api.
|
|
714
|
+
sample: { project: "gitlab-org/gitlab" },
|
|
715
|
+
request: {
|
|
716
|
+
url: `${API}/projects/{{args.project}}`,
|
|
717
|
+
headers: AUTH_HEADERS
|
|
718
|
+
},
|
|
719
|
+
postReceive: [
|
|
720
|
+
{
|
|
721
|
+
op: "pick",
|
|
722
|
+
keys: [
|
|
723
|
+
"id",
|
|
724
|
+
"name",
|
|
725
|
+
"path",
|
|
726
|
+
"path_with_namespace",
|
|
727
|
+
"description",
|
|
728
|
+
"default_branch",
|
|
729
|
+
"visibility",
|
|
730
|
+
"web_url",
|
|
731
|
+
"http_url_to_repo",
|
|
732
|
+
"ssh_url_to_repo",
|
|
733
|
+
"created_at",
|
|
734
|
+
"last_activity_at",
|
|
735
|
+
"archived",
|
|
736
|
+
"namespace",
|
|
737
|
+
"star_count",
|
|
738
|
+
"forks_count",
|
|
739
|
+
"topics"
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
...renames({
|
|
743
|
+
path_with_namespace: "pathWithNamespace",
|
|
744
|
+
default_branch: "defaultBranch",
|
|
745
|
+
web_url: "url",
|
|
746
|
+
http_url_to_repo: "httpUrlToRepo",
|
|
747
|
+
ssh_url_to_repo: "sshUrlToRepo",
|
|
748
|
+
created_at: "createdAt",
|
|
749
|
+
last_activity_at: "lastActivityAt",
|
|
750
|
+
star_count: "starCount",
|
|
751
|
+
forks_count: "forksCount"
|
|
752
|
+
}),
|
|
753
|
+
{ op: "pick", keys: ["id", "name", "path", "full_path", "kind"], path: "namespace" },
|
|
754
|
+
...renames({ full_path: "fullPath" }, "namespace")
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
type: "listOpenMergeRequests",
|
|
759
|
+
label: "List open merge requests",
|
|
760
|
+
description: "The open merge requests of a project, most recently updated first.",
|
|
761
|
+
idempotent: true,
|
|
762
|
+
inputs: [
|
|
763
|
+
PROJECT_INPUT,
|
|
764
|
+
{
|
|
765
|
+
key: "limit",
|
|
766
|
+
label: "Maximum",
|
|
767
|
+
type: "number",
|
|
768
|
+
description: "How many to return, 1 to 100. Defaults to 20.",
|
|
769
|
+
builderHint: "Sent as per_page; 100 is the documented maximum."
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
key: "targetBranch",
|
|
773
|
+
label: "Target branch",
|
|
774
|
+
description: "Only merge requests into this branch. Blank for all."
|
|
775
|
+
}
|
|
776
|
+
],
|
|
777
|
+
outputs: [
|
|
778
|
+
{ key: "count", type: "number", description: "How many merge requests came back" },
|
|
779
|
+
{
|
|
780
|
+
key: "items",
|
|
781
|
+
description: "One entry per merge request, shaped as the merge request trigger delivers it: externalId (the iid), title, url, description, status, labels, updatedAt, and data with id, iid, projectId, sourceBranch, targetBranch, draft, sha, author, createdAt, changedAt, mergedAt, closedAt, hasConflicts, detailedMergeStatus"
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
sample: { project: "gitlab-org/gitlab" },
|
|
785
|
+
// Hand-written rather than declared: a declared request cannot count
|
|
786
|
+
// what it returns, and the spec asks for the trigger's own mapping.
|
|
787
|
+
async run(args, context) {
|
|
788
|
+
const answer = await client(context).getJson(
|
|
789
|
+
`/projects/${projectSegment(args.project)}/merge_requests`,
|
|
790
|
+
{
|
|
791
|
+
state: "opened",
|
|
792
|
+
order_by: "updated_at",
|
|
793
|
+
sort: "desc",
|
|
794
|
+
per_page: pageSize(args.limit),
|
|
795
|
+
target_branch: text(args.targetBranch)
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
const mrs = Array.isArray(answer) ? answer : [];
|
|
799
|
+
const items = mrs.map(mergeRequestToItem);
|
|
800
|
+
return { count: items.length, items };
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
type: "getIssue",
|
|
805
|
+
label: "Get an issue",
|
|
806
|
+
description: "Read one issue by its number.",
|
|
807
|
+
idempotent: true,
|
|
808
|
+
inputs: [
|
|
809
|
+
PROJECT_INPUT,
|
|
810
|
+
{
|
|
811
|
+
key: "iid",
|
|
812
|
+
label: "Issue number",
|
|
813
|
+
type: "number",
|
|
814
|
+
required: true,
|
|
815
|
+
description: "The issue number shown in the project (its iid)"
|
|
816
|
+
}
|
|
817
|
+
],
|
|
818
|
+
outputs: [
|
|
819
|
+
{ key: "id", type: "number", description: "Global issue id" },
|
|
820
|
+
{ key: "iid", type: "number", description: "The number shown in the project" },
|
|
821
|
+
{ key: "projectId", type: "number", description: "The project it belongs to" },
|
|
822
|
+
{ key: "title", description: "Issue title" },
|
|
823
|
+
{ key: "description", description: "Markdown body" },
|
|
824
|
+
{ key: "state", description: "opened or closed" },
|
|
825
|
+
{ key: "url", description: "Where to read it" },
|
|
826
|
+
{ key: "labels", description: "Label names" },
|
|
827
|
+
{ key: "author", description: "Who opened it, as {id, username, name}" },
|
|
828
|
+
{ key: "assignees", description: "Each as {id, username, name}" },
|
|
829
|
+
{ key: "createdAt", description: "When it was opened" },
|
|
830
|
+
{ key: "updatedAt", description: "When it last changed" },
|
|
831
|
+
{ key: "closedAt", description: "When it was closed, or null" },
|
|
832
|
+
{ key: "issueType", description: "issue, incident, test_case or task" },
|
|
833
|
+
{ key: "confidential", type: "boolean", description: "Whether only members can see it" }
|
|
834
|
+
],
|
|
835
|
+
sample: { project: "gitlab-org/gitlab", iid: "1" },
|
|
836
|
+
request: {
|
|
837
|
+
url: `${API}/projects/{{args.project}}/issues/{{args.iid}}`,
|
|
838
|
+
headers: AUTH_HEADERS
|
|
839
|
+
},
|
|
840
|
+
postReceive: ISSUE_SHAPE
|
|
841
|
+
}
|
|
842
|
+
]
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/entry.ts
|
|
847
|
+
import { realpathSync } from "fs";
|
|
848
|
+
import { fileURLToPath } from "url";
|
|
849
|
+
import { serveConnector } from "@vornrun/connector-sdk";
|
|
850
|
+
function isEntryPoint(moduleUrl, entry = process.argv[1]) {
|
|
851
|
+
if (!entry) return false;
|
|
852
|
+
try {
|
|
853
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(moduleUrl));
|
|
854
|
+
} catch {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function serveIfEntryPoint(connector, moduleUrl, serve = serveConnector) {
|
|
859
|
+
if (!isEntryPoint(moduleUrl)) return false;
|
|
860
|
+
void serve(connector);
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/index.ts
|
|
865
|
+
var { version } = createRequire(import.meta.url)("../package.json");
|
|
866
|
+
var gitlabConnector = createGitLabConnector({ version });
|
|
867
|
+
var index_default = gitlabConnector;
|
|
868
|
+
serveIfEntryPoint(gitlabConnector, import.meta.url);
|
|
869
|
+
export {
|
|
870
|
+
GlabNotFoundError,
|
|
871
|
+
GlabSignedOutError,
|
|
872
|
+
TERMINAL_PIPELINE_STATUSES,
|
|
873
|
+
apiUrl,
|
|
874
|
+
gitlabConnector as connector,
|
|
875
|
+
createGitLabClient,
|
|
876
|
+
createGitLabConnector,
|
|
877
|
+
createTokenSource,
|
|
878
|
+
index_default as default,
|
|
879
|
+
gitlabConnector,
|
|
880
|
+
gitlabPreflight,
|
|
881
|
+
glabInstallHint,
|
|
882
|
+
hostOf,
|
|
883
|
+
isFinishedPipeline,
|
|
884
|
+
issueToItem,
|
|
885
|
+
mergeRequestToItem,
|
|
886
|
+
normalizeBaseUrl,
|
|
887
|
+
pipelineToItem,
|
|
888
|
+
projectSegment,
|
|
889
|
+
runGlab
|
|
890
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vornrun/connector-gitlab",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Trigger workflows from GitLab issues, merge requests and pipelines.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/vorn-run/connectors.git",
|
|
10
|
+
"directory": "packages/gitlab"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"vorn-connector-gitlab": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --target node22 --clean",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@vornrun/connector-sdk": "^0.7.0-beta.10"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.10.2",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"vitest": "^4.1.10"
|
|
35
|
+
},
|
|
36
|
+
"vorn": {
|
|
37
|
+
"category": "Development",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"gitlab",
|
|
40
|
+
"issues",
|
|
41
|
+
"merge requests",
|
|
42
|
+
"pipelines",
|
|
43
|
+
"ci",
|
|
44
|
+
"code review",
|
|
45
|
+
"repositories"
|
|
46
|
+
],
|
|
47
|
+
"auth": "Borrows the GitLab CLI's login — `glab auth login` is all it needs — or takes a personal access token if you would rather paste one."
|
|
48
|
+
}
|
|
49
|
+
}
|