@h-rig/linear-plugin 0.0.6-alpha.100
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/README.md +227 -0
- package/dist/src/fixture-transport.d.ts +61 -0
- package/dist/src/fixture-transport.js +56 -0
- package/dist/src/fixtures.d.ts +109 -0
- package/dist/src/fixtures.js +155 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/index.js +492 -0
- package/dist/src/linear-issues-source.d.ts +82 -0
- package/dist/src/linear-issues-source.js +361 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# @rig/linear-plugin
|
|
2
|
+
|
|
3
|
+
Linear as a Rig task source — and the canonical example of a plugin that
|
|
4
|
+
adapts an **external HTTP API** into Rig's task-source contract.
|
|
5
|
+
|
|
6
|
+
The two first-party adapters in `@rig/standard-plugin` cover a CLI wrapper
|
|
7
|
+
(`github-issues` shells out to `gh`) and the local filesystem (`files`). This
|
|
8
|
+
package covers the third shape you'll actually build most often: a remote
|
|
9
|
+
API spoken over HTTP, with credentials, pagination, write-back mutations, and
|
|
10
|
+
tests that never touch the network. If you are writing a Jira/Asana/Shortcut/
|
|
11
|
+
internal-queue adapter, copy this package.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
packages/linear-plugin/
|
|
15
|
+
├── package.json ← workspace package, mirrors standard-plugin
|
|
16
|
+
├── tsconfig.json
|
|
17
|
+
└── src/
|
|
18
|
+
├── index.ts ← definePlugin wiring (metadata + runtime channels)
|
|
19
|
+
├── linear-issues-source.ts ← the adapter: GraphQL client, mapping, write-back
|
|
20
|
+
├── fixture-transport.ts ← fixture-driven fake transport for tests
|
|
21
|
+
├── fixtures.ts ← canned Linear GraphQL payloads
|
|
22
|
+
├── contract.test.ts ← @rig/plugin-testkit contract suite + factory wiring
|
|
23
|
+
└── linear-issues-source.test.ts ← mapping, lifecycle, pagination, error surfaces
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Using it
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// rig.config.ts
|
|
30
|
+
import { defineConfig } from "@rig/core";
|
|
31
|
+
import linear from "@rig/linear-plugin";
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
plugins: [linear()],
|
|
35
|
+
taskSource: {
|
|
36
|
+
kind: "linear",
|
|
37
|
+
options: {
|
|
38
|
+
teamKey: "ENG", // required — the "ENG" in ENG-123
|
|
39
|
+
states: ["Todo", "In Progress"], // optional: workflow state names
|
|
40
|
+
assignee: "alex@example.com", // optional: email or display name
|
|
41
|
+
labels: ["rig"], // optional: issue must carry ALL listed labels
|
|
42
|
+
// pageSize: 50, // optional: issues per GraphQL page (max 250)
|
|
43
|
+
// listLimit: 1000, // optional: fail-loud ceiling, no silent truncation
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
// ... workspace, runtime, etc.
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Authentication comes from the environment, never from the committed config:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export LINEAR_API_KEY=lin_api_... # personal API key (Settings → API)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Personal API keys (`lin_api_…`) are sent raw in the `Authorization` header;
|
|
57
|
+
OAuth access tokens (`lin_oauth_…`) are automatically sent as `Bearer <token>`.
|
|
58
|
+
You can also pass the key programmatically: `linear({ apiKey })`.
|
|
59
|
+
|
|
60
|
+
## Anatomy of an external-API task source
|
|
61
|
+
|
|
62
|
+
### 1. Two channels, one `definePlugin` call
|
|
63
|
+
|
|
64
|
+
Rig plugins declare *what they contribute* as Effect-Schema-validated metadata
|
|
65
|
+
and ship *executable code* on a sibling runtime channel — schemas can't carry
|
|
66
|
+
functions. `src/index.ts`:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export default function linearPlugin(opts: LinearPluginOptions = {}) {
|
|
70
|
+
return definePlugin(
|
|
71
|
+
// Channel 1: metadata. Validated against the RigPlugin schema at load time.
|
|
72
|
+
{
|
|
73
|
+
name: "rig-linear",
|
|
74
|
+
version: "0.1.0",
|
|
75
|
+
contributes: {
|
|
76
|
+
taskSources: [{ id: "linear:issues", kind: "linear", description: "…" }],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
// Channel 2: runtime. The factory turns a TaskSourceConfig into a live source.
|
|
80
|
+
{
|
|
81
|
+
taskSources: [{ id: "linear:issues", kind: "linear", factory(config) { … } }],
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`definePlugin` enforces referential integrity: every factory must match a
|
|
88
|
+
metadata entry by `id` with the same `kind`, and (once a plugin opts into the
|
|
89
|
+
runtime channel) every metadata entry must be backed by a factory. Drift
|
|
90
|
+
throws at config-load time, not at dispatch time.
|
|
91
|
+
|
|
92
|
+
At boot, `buildTaskSourceRegistry(config, pluginHost)` in `@rig/runtime` reads
|
|
93
|
+
`config.taskSource.kind` (`"linear"`), resolves the factory through the plugin
|
|
94
|
+
host (which also rejects duplicate kinds across plugins), and instantiates the
|
|
95
|
+
source.
|
|
96
|
+
|
|
97
|
+
### 2. Plugin-specific config rides in `options`
|
|
98
|
+
|
|
99
|
+
`TaskSourceConfig`'s top-level fields (`path`, `owner`, `repo`, …) are
|
|
100
|
+
reserved for the built-in adapters. Everything Linear-specific lives in the
|
|
101
|
+
documented `options` extension bag — the factory pulls its own fields out and
|
|
102
|
+
validates them with actionable errors:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const teamKey = optionString(config, "teamKey");
|
|
106
|
+
if (!teamKey) {
|
|
107
|
+
throw new Error('task source linear: options.teamKey is required — …');
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Process-level concerns (API key, endpoint override, transport injection)
|
|
112
|
+
arrive through the plugin function's own options instead, because they belong
|
|
113
|
+
to the environment, not to the committed project config.
|
|
114
|
+
|
|
115
|
+
### 3. An injectable transport is the whole testing story
|
|
116
|
+
|
|
117
|
+
The adapter never calls `fetch` directly. It calls a fetch-*shaped* function
|
|
118
|
+
it was constructed with:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
export type LinearTransport = (
|
|
122
|
+
url: string,
|
|
123
|
+
init: { method: "POST"; headers: Record<string, string>; body: string },
|
|
124
|
+
) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;
|
|
125
|
+
|
|
126
|
+
const transport: LinearTransport = opts.transport ?? fetch; // global fetch satisfies it
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
That one seam is why this package's test suite makes **zero live API calls**.
|
|
130
|
+
`fixture-transport.ts` exports `createFixtureTransport(routes)`, which routes
|
|
131
|
+
requests by GraphQL *operation name* (every query/mutation in the adapter is
|
|
132
|
+
deliberately named: `RigListIssues`, `RigTeamStates`, `RigIssueUpdate`, …),
|
|
133
|
+
records every call for payload assertions, and fails loudly on any operation
|
|
134
|
+
a test didn't explicitly allow:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const fixture = createFixtureTransport({
|
|
138
|
+
RigListIssues: () => listIssuesPage(issueFixtures),
|
|
139
|
+
RigIssueUpdate: () => ({ issueUpdate: { success: true } }),
|
|
140
|
+
});
|
|
141
|
+
const source = createLinearIssuesTaskSource({
|
|
142
|
+
teamKey: "ENG",
|
|
143
|
+
apiKey: "lin_api_test",
|
|
144
|
+
transport: fixture.transport,
|
|
145
|
+
});
|
|
146
|
+
// …act…
|
|
147
|
+
expect(fixture.callsFor("RigIssueUpdate")[0].variables.input).toEqual({ stateId: … });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`graphqlErrors([...])` and `httpFailure(status, body)` simulate the two
|
|
151
|
+
failure layers a GraphQL API has (200-with-errors vs. non-200 HTTP).
|
|
152
|
+
|
|
153
|
+
### 4. Mapping external records to `TaskRecord`
|
|
154
|
+
|
|
155
|
+
`TaskRecord` requires `id`, `deps`, `status` and is open-ended beyond that.
|
|
156
|
+
The mapping decisions here:
|
|
157
|
+
|
|
158
|
+
| TaskRecord field | Source |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `id` | Linear identifier (`ENG-123`) — human-facing, like GitHub issue numbers |
|
|
161
|
+
| `status` | Linear workflow-state **type** (see table below) |
|
|
162
|
+
| `title`, `body`/`description` | issue title/description (`body` is Rig convention; `description` mirrors Linear) |
|
|
163
|
+
| `priority`, `priorityLabel` | Linear priority (0–4) and its label |
|
|
164
|
+
| `labels` | label names; `scope:`/`role:`/`validator:` prefixes get the same treatment as the GitHub adapter |
|
|
165
|
+
| `externalRef` | `linear:<identifier>` |
|
|
166
|
+
| `sourceIssueId` | the identifier |
|
|
167
|
+
| `linearIssueId` | Linear's internal UUID — kept so mutations can skip a lookup |
|
|
168
|
+
| `url`, `raw` | issue URL, full raw node |
|
|
169
|
+
|
|
170
|
+
Status mapping is keyed off Linear's fixed state-type taxonomy, not the
|
|
171
|
+
user-renamable state names:
|
|
172
|
+
|
|
173
|
+
| Linear state type | Rig status |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `triage`, `backlog`, `unstarted` | `ready` |
|
|
176
|
+
| `started` | `in_progress` |
|
|
177
|
+
| `completed` | `completed` |
|
|
178
|
+
| `canceled` | `cancelled` |
|
|
179
|
+
| anything unknown | `open` (fail-soft) |
|
|
180
|
+
|
|
181
|
+
### 5. Lifecycle write-back
|
|
182
|
+
|
|
183
|
+
Rig drives the source through `updateStatus(id, status)` and
|
|
184
|
+
`updateTask(id, update)` (status/title/body/comment/metadata). The Linear
|
|
185
|
+
implementation mirrors the GitHub adapter's responsibilities with GraphQL
|
|
186
|
+
mutations:
|
|
187
|
+
|
|
188
|
+
- **Status** → resolve the team's workflow states (`RigTeamStates`, cached per
|
|
189
|
+
source instance), pick the lowest-`position` state whose `type` matches the
|
|
190
|
+
Rig status (`in_progress` → `started`, `closed`/`completed` → `completed`,
|
|
191
|
+
`cancelled` → `canceled`, `ready` → `unstarted`), then `issueUpdate` with
|
|
192
|
+
`stateId`. Statuses Linear has no workflow concept for (`blocked`,
|
|
193
|
+
`failed`, `needs_attention`) deliberately leave the state untouched — they
|
|
194
|
+
surface through comments instead of mangling the board.
|
|
195
|
+
- **Title / body / metadata** → folded into the same single `issueUpdate`
|
|
196
|
+
call. `update.metadata` is rendered into the Rig-owned
|
|
197
|
+
`<!-- rig:metadata:start/end -->` block (same markers as the GitHub
|
|
198
|
+
adapter, so operator surfaces parse both identically), replacing a previous
|
|
199
|
+
block in the existing description when present.
|
|
200
|
+
- **Comment** → `commentCreate` after the update.
|
|
201
|
+
- Task ids are accepted as either the identifier (`ENG-123`) or Linear's UUID;
|
|
202
|
+
identifiers are resolved with one extra `issue(id:)` query.
|
|
203
|
+
|
|
204
|
+
Every write fires the optional `onTaskChanged` callback so the host can
|
|
205
|
+
refresh snapshots — same pattern as the GitHub adapter.
|
|
206
|
+
|
|
207
|
+
### 6. Error surfaces are part of the contract
|
|
208
|
+
|
|
209
|
+
- No key configured → `"Linear task source: no API key configured. Pass apiKey in plugin options or set the LINEAR_API_KEY environment variable."` — thrown before any request.
|
|
210
|
+
- Bad token (HTTP 401 or a GraphQL `AUTHENTICATION_ERROR`) → `"Linear API authentication failed (…). Check that the configured API key (options.apiKey / LINEAR_API_KEY) is a valid Linear key…"`.
|
|
211
|
+
- Other GraphQL errors → surfaced with the server's message.
|
|
212
|
+
- Non-200 HTTP → status + body excerpt.
|
|
213
|
+
- More matching issues than `listLimit` → a loud refusal to truncate, telling
|
|
214
|
+
you exactly which knob to turn — never a silently short task list.
|
|
215
|
+
|
|
216
|
+
## Running the tests
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
cd packages/linear-plugin
|
|
220
|
+
bun test # 39 tests, no network
|
|
221
|
+
bunx tsc --noEmit # typecheck
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`contract.test.ts` runs `pluginContractTests` from `@rig/plugin-testkit`
|
|
225
|
+
(schema validity, namespaced ids, duplicate-id detection) plus factory-wiring
|
|
226
|
+
assertions; `linear-issues-source.test.ts` covers mapping, filters,
|
|
227
|
+
pagination, lifecycle mutation payloads, and every error surface above.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture-driven fake transport for testing the Linear adapter (and plugins
|
|
3
|
+
* built on the same pattern) without any live API calls.
|
|
4
|
+
*
|
|
5
|
+
* The adapter names every GraphQL operation (`RigListIssues`,
|
|
6
|
+
* `RigTeamStates`, …), so fixtures route on the operation name instead of
|
|
7
|
+
* fragile query-string matching. Each route returns either:
|
|
8
|
+
* - a plain object → wrapped as `{ data: <object> }` with HTTP 200
|
|
9
|
+
* - `graphqlErrors([...])` → `{ errors: [...] }` with HTTP 200
|
|
10
|
+
* - `httpFailure(status, body)` → a raw non-200 HTTP response
|
|
11
|
+
*/
|
|
12
|
+
import type { LinearTransport } from "./linear-issues-source";
|
|
13
|
+
export interface RecordedLinearCall {
|
|
14
|
+
url: string;
|
|
15
|
+
/** Raw Authorization header the adapter sent. */
|
|
16
|
+
authorization: string | undefined;
|
|
17
|
+
/** GraphQL operation name parsed from the request body. */
|
|
18
|
+
operation: string;
|
|
19
|
+
query: string;
|
|
20
|
+
variables: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
declare const HTTP_FAILURE: unique symbol;
|
|
23
|
+
declare const GRAPHQL_ERRORS: unique symbol;
|
|
24
|
+
interface HttpFailureFixture {
|
|
25
|
+
[HTTP_FAILURE]: true;
|
|
26
|
+
status: number;
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
interface GraphQLErrorsFixture {
|
|
30
|
+
[GRAPHQL_ERRORS]: true;
|
|
31
|
+
errors: ReadonlyArray<{
|
|
32
|
+
message: string;
|
|
33
|
+
extensions?: {
|
|
34
|
+
code?: string;
|
|
35
|
+
};
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Make a route fail at the HTTP layer (e.g. `httpFailure(401, "Unauthorized")`). */
|
|
39
|
+
export declare function httpFailure(status: number, body: string): HttpFailureFixture;
|
|
40
|
+
/** Make a route return a GraphQL-level errors payload with HTTP 200. */
|
|
41
|
+
export declare function graphqlErrors(errors: ReadonlyArray<{
|
|
42
|
+
message: string;
|
|
43
|
+
extensions?: {
|
|
44
|
+
code?: string;
|
|
45
|
+
};
|
|
46
|
+
}>): GraphQLErrorsFixture;
|
|
47
|
+
export type FixtureRoute = (variables: Record<string, unknown>, call: RecordedLinearCall) => unknown;
|
|
48
|
+
export interface FixtureTransport {
|
|
49
|
+
transport: LinearTransport;
|
|
50
|
+
/** Every request the adapter made, in order. Assert payloads against this. */
|
|
51
|
+
calls: RecordedLinearCall[];
|
|
52
|
+
/** Calls filtered to one operation name. */
|
|
53
|
+
callsFor(operation: string): RecordedLinearCall[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a transport whose responses come from fixtures keyed by GraphQL
|
|
57
|
+
* operation name. Unrouted operations fail loudly so tests never silently
|
|
58
|
+
* exercise a request they didn't mean to allow.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createFixtureTransport(routes: Record<string, FixtureRoute>): FixtureTransport;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/linear-plugin/src/fixture-transport.ts
|
|
3
|
+
var HTTP_FAILURE = Symbol("linear-fixture-http-failure");
|
|
4
|
+
var GRAPHQL_ERRORS = Symbol("linear-fixture-graphql-errors");
|
|
5
|
+
function httpFailure(status, body) {
|
|
6
|
+
return { [HTTP_FAILURE]: true, status, body };
|
|
7
|
+
}
|
|
8
|
+
function graphqlErrors(errors) {
|
|
9
|
+
return { [GRAPHQL_ERRORS]: true, errors };
|
|
10
|
+
}
|
|
11
|
+
function isHttpFailure(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && HTTP_FAILURE in value;
|
|
13
|
+
}
|
|
14
|
+
function isGraphQLErrors(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && GRAPHQL_ERRORS in value;
|
|
16
|
+
}
|
|
17
|
+
function jsonResponse(status, body) {
|
|
18
|
+
return { ok: status >= 200 && status < 300, status, text: async () => body };
|
|
19
|
+
}
|
|
20
|
+
function createFixtureTransport(routes) {
|
|
21
|
+
const calls = [];
|
|
22
|
+
const transport = async (url, init) => {
|
|
23
|
+
const body = JSON.parse(init.body);
|
|
24
|
+
const operation = /(?:query|mutation)\s+([A-Za-z0-9_]+)/.exec(body.query)?.[1] ?? "<anonymous>";
|
|
25
|
+
const call = {
|
|
26
|
+
url,
|
|
27
|
+
authorization: init.headers["Authorization"],
|
|
28
|
+
operation,
|
|
29
|
+
query: body.query,
|
|
30
|
+
variables: body.variables ?? {}
|
|
31
|
+
};
|
|
32
|
+
calls.push(call);
|
|
33
|
+
const route = routes[operation];
|
|
34
|
+
if (!route) {
|
|
35
|
+
throw new Error(`createFixtureTransport: no fixture route for operation "${operation}"`);
|
|
36
|
+
}
|
|
37
|
+
const result = route(call.variables, call);
|
|
38
|
+
if (isHttpFailure(result)) {
|
|
39
|
+
return jsonResponse(result.status, result.body);
|
|
40
|
+
}
|
|
41
|
+
if (isGraphQLErrors(result)) {
|
|
42
|
+
return jsonResponse(200, JSON.stringify({ errors: result.errors }));
|
|
43
|
+
}
|
|
44
|
+
return jsonResponse(200, JSON.stringify({ data: result }));
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
transport,
|
|
48
|
+
calls,
|
|
49
|
+
callsFor: (operation) => calls.filter((c) => c.operation === operation)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
httpFailure,
|
|
54
|
+
graphqlErrors,
|
|
55
|
+
createFixtureTransport
|
|
56
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GraphQL fixtures for the test suite — shaped exactly like real
|
|
3
|
+
* Linear API responses (the `data` payload of each operation). No live API
|
|
4
|
+
* calls happen anywhere in this package's tests.
|
|
5
|
+
*/
|
|
6
|
+
export declare const TEAM_KEY = "ENG";
|
|
7
|
+
export declare const STATE_IDS: {
|
|
8
|
+
readonly triage: "11111111-1111-4111-8111-111111111101";
|
|
9
|
+
readonly backlog: "11111111-1111-4111-8111-111111111102";
|
|
10
|
+
readonly todo: "11111111-1111-4111-8111-111111111103";
|
|
11
|
+
readonly inProgress: "11111111-1111-4111-8111-111111111104";
|
|
12
|
+
readonly inReview: "11111111-1111-4111-8111-111111111105";
|
|
13
|
+
readonly done: "11111111-1111-4111-8111-111111111106";
|
|
14
|
+
readonly canceled: "11111111-1111-4111-8111-111111111107";
|
|
15
|
+
};
|
|
16
|
+
/** Response payload for the RigTeamStates operation. */
|
|
17
|
+
export declare const teamStatesFixture: {
|
|
18
|
+
teams: {
|
|
19
|
+
nodes: {
|
|
20
|
+
id: string;
|
|
21
|
+
key: string;
|
|
22
|
+
states: {
|
|
23
|
+
nodes: ({
|
|
24
|
+
id: "11111111-1111-4111-8111-111111111101";
|
|
25
|
+
name: string;
|
|
26
|
+
type: string;
|
|
27
|
+
position: number;
|
|
28
|
+
} | {
|
|
29
|
+
id: "11111111-1111-4111-8111-111111111102";
|
|
30
|
+
name: string;
|
|
31
|
+
type: string;
|
|
32
|
+
position: number;
|
|
33
|
+
} | {
|
|
34
|
+
id: "11111111-1111-4111-8111-111111111103";
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
position: number;
|
|
38
|
+
} | {
|
|
39
|
+
id: "11111111-1111-4111-8111-111111111105";
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
position: number;
|
|
43
|
+
} | {
|
|
44
|
+
id: "11111111-1111-4111-8111-111111111104";
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
position: number;
|
|
48
|
+
} | {
|
|
49
|
+
id: "11111111-1111-4111-8111-111111111106";
|
|
50
|
+
name: string;
|
|
51
|
+
type: string;
|
|
52
|
+
position: number;
|
|
53
|
+
} | {
|
|
54
|
+
id: "11111111-1111-4111-8111-111111111107";
|
|
55
|
+
name: string;
|
|
56
|
+
type: string;
|
|
57
|
+
position: number;
|
|
58
|
+
})[];
|
|
59
|
+
};
|
|
60
|
+
}[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export declare const ISSUE_UUIDS: {
|
|
64
|
+
readonly eng1: "33333333-3333-4333-8333-333333333301";
|
|
65
|
+
readonly eng2: "33333333-3333-4333-8333-333333333302";
|
|
66
|
+
readonly eng3: "33333333-3333-4333-8333-333333333303";
|
|
67
|
+
readonly eng4: "33333333-3333-4333-8333-333333333304";
|
|
68
|
+
readonly eng5: "33333333-3333-4333-8333-333333333305";
|
|
69
|
+
readonly eng6: "33333333-3333-4333-8333-333333333306";
|
|
70
|
+
readonly eng7: "33333333-3333-4333-8333-333333333307";
|
|
71
|
+
};
|
|
72
|
+
export interface FixtureIssue {
|
|
73
|
+
id: string;
|
|
74
|
+
identifier: string;
|
|
75
|
+
title: string;
|
|
76
|
+
description: string | null;
|
|
77
|
+
priority: number | null;
|
|
78
|
+
priorityLabel: string | null;
|
|
79
|
+
url: string;
|
|
80
|
+
state: {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
type: string;
|
|
84
|
+
};
|
|
85
|
+
labels: {
|
|
86
|
+
nodes: Array<{
|
|
87
|
+
name: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
assignee: {
|
|
91
|
+
name: string;
|
|
92
|
+
email: string;
|
|
93
|
+
} | null;
|
|
94
|
+
}
|
|
95
|
+
export declare const issueFixtures: FixtureIssue[];
|
|
96
|
+
/** Single-page RigListIssues payload containing every fixture issue. */
|
|
97
|
+
export declare function listIssuesPage(issues: readonly FixtureIssue[], pageInfo?: {
|
|
98
|
+
hasNextPage: boolean;
|
|
99
|
+
endCursor?: string | null;
|
|
100
|
+
}): {
|
|
101
|
+
issues: {
|
|
102
|
+
pageInfo: {
|
|
103
|
+
hasNextPage: boolean;
|
|
104
|
+
endCursor?: string | null;
|
|
105
|
+
};
|
|
106
|
+
nodes: readonly FixtureIssue[];
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export declare function issueByIdentifier(identifier: string): FixtureIssue;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/linear-plugin/src/fixtures.ts
|
|
3
|
+
var TEAM_KEY = "ENG";
|
|
4
|
+
var STATE_IDS = {
|
|
5
|
+
triage: "11111111-1111-4111-8111-111111111101",
|
|
6
|
+
backlog: "11111111-1111-4111-8111-111111111102",
|
|
7
|
+
todo: "11111111-1111-4111-8111-111111111103",
|
|
8
|
+
inProgress: "11111111-1111-4111-8111-111111111104",
|
|
9
|
+
inReview: "11111111-1111-4111-8111-111111111105",
|
|
10
|
+
done: "11111111-1111-4111-8111-111111111106",
|
|
11
|
+
canceled: "11111111-1111-4111-8111-111111111107"
|
|
12
|
+
};
|
|
13
|
+
var teamStatesFixture = {
|
|
14
|
+
teams: {
|
|
15
|
+
nodes: [
|
|
16
|
+
{
|
|
17
|
+
id: "22222222-2222-4222-8222-222222222201",
|
|
18
|
+
key: TEAM_KEY,
|
|
19
|
+
states: {
|
|
20
|
+
nodes: [
|
|
21
|
+
{ id: STATE_IDS.triage, name: "Triage", type: "triage", position: 0 },
|
|
22
|
+
{ id: STATE_IDS.backlog, name: "Backlog", type: "backlog", position: 1 },
|
|
23
|
+
{ id: STATE_IDS.todo, name: "Todo", type: "unstarted", position: 2 },
|
|
24
|
+
{ id: STATE_IDS.inReview, name: "In Review", type: "started", position: 4 },
|
|
25
|
+
{ id: STATE_IDS.inProgress, name: "In Progress", type: "started", position: 3 },
|
|
26
|
+
{ id: STATE_IDS.done, name: "Done", type: "completed", position: 5 },
|
|
27
|
+
{ id: STATE_IDS.canceled, name: "Canceled", type: "canceled", position: 6 }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var ISSUE_UUIDS = {
|
|
35
|
+
eng1: "33333333-3333-4333-8333-333333333301",
|
|
36
|
+
eng2: "33333333-3333-4333-8333-333333333302",
|
|
37
|
+
eng3: "33333333-3333-4333-8333-333333333303",
|
|
38
|
+
eng4: "33333333-3333-4333-8333-333333333304",
|
|
39
|
+
eng5: "33333333-3333-4333-8333-333333333305",
|
|
40
|
+
eng6: "33333333-3333-4333-8333-333333333306",
|
|
41
|
+
eng7: "33333333-3333-4333-8333-333333333307"
|
|
42
|
+
};
|
|
43
|
+
var issueFixtures = [
|
|
44
|
+
{
|
|
45
|
+
id: ISSUE_UUIDS.eng1,
|
|
46
|
+
identifier: "ENG-1",
|
|
47
|
+
title: "Untriaged crash report",
|
|
48
|
+
description: null,
|
|
49
|
+
priority: 0,
|
|
50
|
+
priorityLabel: "No priority",
|
|
51
|
+
url: "https://linear.app/acme/issue/ENG-1",
|
|
52
|
+
state: { id: STATE_IDS.triage, name: "Triage", type: "triage" },
|
|
53
|
+
labels: { nodes: [] },
|
|
54
|
+
assignee: null
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: ISSUE_UUIDS.eng2,
|
|
58
|
+
identifier: "ENG-2",
|
|
59
|
+
title: "Someday refactor",
|
|
60
|
+
description: "Park it in the backlog.",
|
|
61
|
+
priority: 4,
|
|
62
|
+
priorityLabel: "Low",
|
|
63
|
+
url: "https://linear.app/acme/issue/ENG-2",
|
|
64
|
+
state: { id: STATE_IDS.backlog, name: "Backlog", type: "backlog" },
|
|
65
|
+
labels: { nodes: [{ name: "tech-debt" }] },
|
|
66
|
+
assignee: null
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: ISSUE_UUIDS.eng3,
|
|
70
|
+
identifier: "ENG-3",
|
|
71
|
+
title: "Add retry to webhook dispatcher",
|
|
72
|
+
description: `Retries with backoff.
|
|
73
|
+
|
|
74
|
+
See the incident doc.`,
|
|
75
|
+
priority: 2,
|
|
76
|
+
priorityLabel: "High",
|
|
77
|
+
url: "https://linear.app/acme/issue/ENG-3",
|
|
78
|
+
state: { id: STATE_IDS.todo, name: "Todo", type: "unstarted" },
|
|
79
|
+
labels: {
|
|
80
|
+
nodes: [
|
|
81
|
+
{ name: "rig" },
|
|
82
|
+
{ name: "scope:packages/server/**" },
|
|
83
|
+
{ name: "role:builder" },
|
|
84
|
+
{ name: "validator:typecheck" }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
assignee: { name: "Alex", email: "alex@example.com" }
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: ISSUE_UUIDS.eng4,
|
|
91
|
+
identifier: "ENG-4",
|
|
92
|
+
title: "Migrate auth tokens",
|
|
93
|
+
description: "In flight.",
|
|
94
|
+
priority: 1,
|
|
95
|
+
priorityLabel: "Urgent",
|
|
96
|
+
url: "https://linear.app/acme/issue/ENG-4",
|
|
97
|
+
state: { id: STATE_IDS.inProgress, name: "In Progress", type: "started" },
|
|
98
|
+
labels: { nodes: [{ name: "rig" }] },
|
|
99
|
+
assignee: { name: "Alex", email: "alex@example.com" }
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: ISSUE_UUIDS.eng5,
|
|
103
|
+
identifier: "ENG-5",
|
|
104
|
+
title: "Ship the dashboard",
|
|
105
|
+
description: "Done last sprint.",
|
|
106
|
+
priority: 3,
|
|
107
|
+
priorityLabel: "Medium",
|
|
108
|
+
url: "https://linear.app/acme/issue/ENG-5",
|
|
109
|
+
state: { id: STATE_IDS.done, name: "Done", type: "completed" },
|
|
110
|
+
labels: { nodes: [] },
|
|
111
|
+
assignee: null
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: ISSUE_UUIDS.eng6,
|
|
115
|
+
identifier: "ENG-6",
|
|
116
|
+
title: "Dead-end spike",
|
|
117
|
+
description: null,
|
|
118
|
+
priority: null,
|
|
119
|
+
priorityLabel: null,
|
|
120
|
+
url: "https://linear.app/acme/issue/ENG-6",
|
|
121
|
+
state: { id: STATE_IDS.canceled, name: "Canceled", type: "canceled" },
|
|
122
|
+
labels: { nodes: [] },
|
|
123
|
+
assignee: null
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: ISSUE_UUIDS.eng7,
|
|
127
|
+
identifier: "ENG-7",
|
|
128
|
+
title: "Issue in a state type this adapter has never heard of",
|
|
129
|
+
description: null,
|
|
130
|
+
priority: null,
|
|
131
|
+
priorityLabel: null,
|
|
132
|
+
url: "https://linear.app/acme/issue/ENG-7",
|
|
133
|
+
state: { id: "11111111-1111-4111-8111-111111111199", name: "Weird", type: "someday-new-type" },
|
|
134
|
+
labels: { nodes: [] },
|
|
135
|
+
assignee: null
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
function listIssuesPage(issues, pageInfo = { hasNextPage: false, endCursor: null }) {
|
|
139
|
+
return { issues: { pageInfo, nodes: issues } };
|
|
140
|
+
}
|
|
141
|
+
function issueByIdentifier(identifier) {
|
|
142
|
+
const issue = issueFixtures.find((i) => i.identifier === identifier);
|
|
143
|
+
if (!issue)
|
|
144
|
+
throw new Error(`fixture issue not found: ${identifier}`);
|
|
145
|
+
return issue;
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
teamStatesFixture,
|
|
149
|
+
listIssuesPage,
|
|
150
|
+
issueFixtures,
|
|
151
|
+
issueByIdentifier,
|
|
152
|
+
TEAM_KEY,
|
|
153
|
+
STATE_IDS,
|
|
154
|
+
ISSUE_UUIDS
|
|
155
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rig/linear-plugin` — Linear as a Rig task source.
|
|
3
|
+
*
|
|
4
|
+
* This package is the canonical example of a plugin that adapts an external
|
|
5
|
+
* HTTP API into Rig's task-source contract. The README walks through the
|
|
6
|
+
* anatomy; `linear-issues-source.ts` holds the adapter, this file holds the
|
|
7
|
+
* `definePlugin` wiring (metadata channel + executable runtime channel).
|
|
8
|
+
*
|
|
9
|
+
* Usage in rig.config.ts:
|
|
10
|
+
*
|
|
11
|
+
* import { defineConfig } from "@rig/core";
|
|
12
|
+
* import linear from "@rig/linear-plugin";
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* plugins: [linear()],
|
|
16
|
+
* taskSource: {
|
|
17
|
+
* kind: "linear",
|
|
18
|
+
* options: { teamKey: "ENG", states: ["Todo"], labels: ["rig"] },
|
|
19
|
+
* },
|
|
20
|
+
* // ...
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* The API key comes from LINEAR_API_KEY (or `linear({ apiKey })`) — never
|
|
24
|
+
* from rig.config.ts, which is committed.
|
|
25
|
+
*/
|
|
26
|
+
import { type RigPluginWithRuntime } from "@rig/core";
|
|
27
|
+
import { type LinearIssuesOptions, type LinearTransport } from "./linear-issues-source";
|
|
28
|
+
export { createLinearIssuesTaskSource, linearStateTypeForRigStatus, rigStatusForLinearStateType, updateRigOwnedMetadataBlock, RIG_METADATA_START, RIG_METADATA_END, } from "./linear-issues-source";
|
|
29
|
+
export type { LinearIssuesOptions, LinearIssuesTaskSource, LinearTransport, LinearTransportRequest, LinearTransportResponse, } from "./linear-issues-source";
|
|
30
|
+
export { createFixtureTransport, graphqlErrors, httpFailure, } from "./fixture-transport";
|
|
31
|
+
export type { FixtureTransport, FixtureRoute, RecordedLinearCall } from "./fixture-transport";
|
|
32
|
+
/**
|
|
33
|
+
* Process-level options passed where the plugin is instantiated (rig.config.ts).
|
|
34
|
+
* Per-project source configuration (team key, filters) lives in
|
|
35
|
+
* `taskSource.options` instead — see the factory below.
|
|
36
|
+
*/
|
|
37
|
+
export interface LinearPluginOptions {
|
|
38
|
+
/** Linear API key. Defaults to process.env.LINEAR_API_KEY at request time. */
|
|
39
|
+
apiKey?: string;
|
|
40
|
+
/** GraphQL endpoint override (self-hosted proxies, test servers). */
|
|
41
|
+
endpoint?: string;
|
|
42
|
+
/** fetch-shaped transport override — tests inject a fixture transport here. */
|
|
43
|
+
transport?: LinearTransport;
|
|
44
|
+
/** Notify the host that issue-backed task state changed. */
|
|
45
|
+
onTaskChanged?: LinearIssuesOptions["onTaskChanged"];
|
|
46
|
+
}
|
|
47
|
+
export default function linearPlugin(opts?: LinearPluginOptions): RigPluginWithRuntime;
|