@usine/github 0.0.1 → 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/README.md +107 -2
- package/dist/github-app.d.ts +23 -0
- package/dist/github-app.d.ts.map +1 -0
- package/dist/github-app.js +169 -0
- package/dist/github-app.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +68 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/item-writer.d.ts +22 -0
- package/dist/item-writer.d.ts.map +1 -0
- package/dist/item-writer.js +66 -0
- package/dist/item-writer.js.map +1 -0
- package/dist/jwt.d.ts +10 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +72 -0
- package/dist/jwt.js.map +1 -0
- package/dist/local-secrets.d.ts +28 -0
- package/dist/local-secrets.d.ts.map +1 -0
- package/dist/local-secrets.js +56 -0
- package/dist/local-secrets.js.map +1 -0
- package/dist/model.d.ts +128 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +133 -0
- package/dist/model.js.map +1 -0
- package/dist/polling/model.d.ts +624 -0
- package/dist/polling/model.d.ts.map +1 -0
- package/dist/polling/model.js +188 -0
- package/dist/polling/model.js.map +1 -0
- package/dist/polling/polling.d.ts +18 -0
- package/dist/polling/polling.d.ts.map +1 -0
- package/dist/polling/polling.js +320 -0
- package/dist/polling/polling.js.map +1 -0
- package/dist/polling/queries.d.ts +3 -0
- package/dist/polling/queries.d.ts.map +1 -0
- package/dist/polling/queries.js +125 -0
- package/dist/polling/queries.js.map +1 -0
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
# @usine/github
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@usine/github` gives an usine factory one GitHub App identity. It signs App JWTs, finds the App's installation, reports registry repositories that the installation does not cover, and mints two kinds of installation token:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- an installation-wide token for orchestrator polling and writes;
|
|
6
|
+
- a token restricted to one repository for a worker attempt.
|
|
7
|
+
|
|
8
|
+
Both token requests use a one-hour maximum lifetime. Token values and the App private key stay inside Effect `Redacted` values.
|
|
9
|
+
|
|
10
|
+
## Create the App manually
|
|
11
|
+
|
|
12
|
+
Open your GitHub account or organization settings, select **Developer settings → GitHub Apps**, then select **New GitHub App**. Use a name that identifies the factory and set **Where can this GitHub App be installed?** to **Only on this account**. User authorization isn't required. You can turn off active webhooks for a polling-only development factory.
|
|
13
|
+
|
|
14
|
+
Set these repository permissions:
|
|
15
|
+
|
|
16
|
+
| Permission | Access |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Metadata | Read-only |
|
|
19
|
+
| Contents | Read and write |
|
|
20
|
+
| Issues | Read and write |
|
|
21
|
+
| Pull requests | Read and write |
|
|
22
|
+
| Checks | Read-only |
|
|
23
|
+
|
|
24
|
+
Leave Workflows, Administration, every organization permission, and every account permission at **No access**. GitHub grants Metadata read access automatically. The Contents permission lets installation tokens authenticate Git over HTTPS; Workflows remains off, so worker tokens cannot edit files under `.github/workflows/`.
|
|
25
|
+
|
|
26
|
+
Create the App, generate a private key, and note the numeric App ID. On the App's **Install App** page, install it on the account that owns the factory's managed repositories. Select every repository listed in `usine.yaml`. A factory expects one App installation, so don't install the same App on a second account.
|
|
27
|
+
|
|
28
|
+
GitHub's current setup references are [Registering a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app) and [Choosing permissions for a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app).
|
|
29
|
+
|
|
30
|
+
## Store the credentials
|
|
31
|
+
|
|
32
|
+
The local runtime file is `<instance>/.usine/github-app.json`. Keep `.usine/` in the instance repo's `.gitignore`, and set the file mode to `0600`.
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"app_id": 123456,
|
|
37
|
+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This command creates the JSON without copying PEM newlines by hand:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
mkdir -p .usine
|
|
45
|
+
install -m 600 /dev/null .usine/github-app.json
|
|
46
|
+
jq -n --argjson app_id "$APP_ID" --rawfile private_key factory.private-key.pem \
|
|
47
|
+
'{app_id: $app_id, private_key: $private_key}' > .usine/github-app.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`USINE_GITHUB_APP_ID` and `USINE_GITHUB_PRIVATE_KEY` override their matching file fields. When both variables exist, the loader does not read the file.
|
|
51
|
+
|
|
52
|
+
## Compose the services
|
|
53
|
+
|
|
54
|
+
The package root has no Node dependency. Supply an HTTP client plus platform file and path layers at the application edge:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
|
58
|
+
import { Instance } from "@usine/core"
|
|
59
|
+
import { GitHubApp } from "@usine/github"
|
|
60
|
+
import { localSecretsLayer } from "@usine/github/local-secrets"
|
|
61
|
+
import { Layer } from "effect"
|
|
62
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
63
|
+
|
|
64
|
+
const instance = Instance.Directory.make("/path/to/instance")
|
|
65
|
+
const platformLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer)
|
|
66
|
+
const credentialsLayer = localSecretsLayer(instance).pipe(
|
|
67
|
+
Layer.provide(platformLayer),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
export const githubLayer = GitHubApp.layer.pipe(
|
|
71
|
+
Layer.provide(credentialsLayer),
|
|
72
|
+
Layer.provide(FetchHttpClient.layer),
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`GitHubApp.layer` imports the private key once, when the layer is built: a key WebCrypto cannot import fails composition with `AppAuthenticationError` instead of failing the first request. Another credential source provides `GitHubCredentials` with an `AppCredentials` whose key went through the `RedactedPrivateKey` schema.
|
|
77
|
+
|
|
78
|
+
`installation()`, `installationToken()`, and `projectAccess()` share one lease: a single installation discovery and installation-wide mint, reused until five minutes before the token expires. Polling therefore costs no App requests between renewals. `workerToken()` mints on every call, so each attempt gets its own token.
|
|
79
|
+
|
|
80
|
+
Call `GitHubApp.projectAccess(registry)` before scheduling work. Each result is either a `CoveredProject`, which carries the repository ID needed for scoped minting, or a `ProjectNotInstalledError`. A missing repository does not block covered projects.
|
|
81
|
+
|
|
82
|
+
Pass a `CoveredProject` to `GitHubApp.workerToken`. GitHub must confirm in the token response that the token covers exactly that repository ID; otherwise the call fails with `InvalidGitHubResponseError`.
|
|
83
|
+
|
|
84
|
+
## Poll repository changes
|
|
85
|
+
|
|
86
|
+
`GitHubPollingLayer` supplies the operations the orchestrator needs:
|
|
87
|
+
|
|
88
|
+
- `changes(repository, cursor)` makes one conditional Issues REST request. It returns changed issue and pull-request numbers, the next ETag and watermark, plus any rate-limit delay GitHub requested.
|
|
89
|
+
- `labels(repository)` reads every page of the repository label catalog for Board lane proposals.
|
|
90
|
+
- `snapshot(repository, item)` fetches the issue or pull-request payload through GraphQL and computes its SHA-256 content hash. The stored payload retains timestamps, while the hash excludes timestamp fields at every nesting level.
|
|
91
|
+
|
|
92
|
+
The REST cursor overlaps its watermark by five seconds. Repeated items are expected; the engine's permanent Event ledger owns deduplication. Provide `GitHubPollingLayer` with `GitHubApp`, an Effect HTTP client, and `Crypto.Crypto`. The package root remains platform-free.
|
|
93
|
+
|
|
94
|
+
`GitHubItemWriter.updateLabels(repository, item, mutation)` adds destination lane labels and removes only explicitly lane-owned labels from an issue or pull request. It never replaces the complete applied-label set or deletes repository label definitions, so unrelated and concurrently added labels survive stale or truncated Item projections. The service requires `GitHubApp` and an Effect HTTP client.
|
|
95
|
+
|
|
96
|
+
## Real-App integration check
|
|
97
|
+
|
|
98
|
+
Run the opt-in integration test against a disposable issue in a repository covered by the App:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
|
|
102
|
+
export USINE_GITHUB_INTEGRATION_REPO=owner/repo
|
|
103
|
+
export USINE_GITHUB_INTEGRATION_ISSUE_NUMBER=123
|
|
104
|
+
export 'USINE_GITHUB_INTEGRATION_BOT_LOGIN=your-app-slug[bot]'
|
|
105
|
+
pnpm --filter @usine/github test:integration
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The test mints both token shapes, posts one issue comment with the repository-scoped token, checks the comment author against the expected `<app>[bot]` login, and deletes the comment during cleanup. `@usine/orchestrator` has a separate live polling test that temporarily creates and applies a repository label.
|
|
109
|
+
|
|
110
|
+
All APIs remain 0.x and can change between minor releases.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Repository } from "@usine/core";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import { type AppAuthenticationError, type CoveredProject, type GitHubAppError, GitHubCredentials, Installation, InstallationToken, type ProjectAccess } from "./model.ts";
|
|
5
|
+
declare const GitHubApp_base: Context.ServiceClass<GitHubApp, "@usine/github/GitHubApp", {
|
|
6
|
+
readonly installation: () => Effect.Effect<Installation, GitHubAppError>;
|
|
7
|
+
readonly installationToken: () => Effect.Effect<InstallationToken, GitHubAppError>;
|
|
8
|
+
readonly projectAccess: (registry: ReadonlyArray<Repository.Name>) => Effect.Effect<ReadonlyArray<ProjectAccess>, GitHubAppError>;
|
|
9
|
+
readonly workerToken: (project: CoveredProject) => Effect.Effect<InstallationToken, GitHubAppError>;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Whole GitHub identity for one usine instance. The transport is supplied as
|
|
13
|
+
* an Effect HttpClient, so the root entry remains platform-free.
|
|
14
|
+
*
|
|
15
|
+
* The orchestrator-side operations share one lease — a single discovery and
|
|
16
|
+
* installation-wide mint reused until the token nears expiry — so polling does
|
|
17
|
+
* not spend two App requests per call. Worker tokens are minted per call.
|
|
18
|
+
*/
|
|
19
|
+
export declare class GitHubApp extends GitHubApp_base {
|
|
20
|
+
static readonly layer: Layer.Layer<GitHubApp, AppAuthenticationError, GitHubCredentials | HttpClient.HttpClient>;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=github-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-app.d.ts","sourceRoot":"","sources":["../src/github-app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAEL,OAAO,EAEP,MAAM,EACN,KAAK,EAKN,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,UAAU,EAAyC,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,cAAc,EAEnB,KAAK,cAAc,EAEnB,iBAAiB,EAEjB,YAAY,EAGZ,iBAAiB,EAIjB,KAAK,aAAa,EAOnB,MAAM,YAAY,CAAC;;2BA+DO,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC;gCAC5C,MAAM,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC;4BAC1D,CACtB,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,KACrC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,cAAc,CAAC;0BAC1C,CACpB,OAAO,EAAE,cAAc,KACpB,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC;;AAlBzD;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,cAYD;IAC5B,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAChC,SAAS,EACT,sBAAsB,EACtB,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAC1C,CA+KC;CACH"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Repository } from "@usine/core";
|
|
2
|
+
import { Clock, Context, DateTime, Effect, Layer, Option, Redacted, Schema, SynchronizedRef, } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
4
|
+
import { appJwtSigner } from "./jwt.js";
|
|
5
|
+
import { API_ROOT, authorized, invalidResponse, statusError, transportError } from "./http.js";
|
|
6
|
+
import { AppPermissionError, GitHubCredentials, Installation, InstallationDiscoveryError, InstallationId, InstallationToken, InstallationWide, InvalidGitHubResponseError, REQUIRED_REPOSITORY_PERMISSIONS, RepositoryId, SingleRepository, deriveProjectAccess, missingPermissions, } from "./model.js";
|
|
7
|
+
const TOKEN_MAX_LIFETIME_MS = 60 * 60 * 1_000;
|
|
8
|
+
/** Renew the shared lease once its token has less than this left to live. */
|
|
9
|
+
const LEASE_RENEWAL_MARGIN_MS = 5 * 60 * 1_000;
|
|
10
|
+
const InstallationResponse = Schema.Struct({
|
|
11
|
+
id: InstallationId,
|
|
12
|
+
account: Schema.Struct({ login: Schema.NonEmptyString }),
|
|
13
|
+
permissions: Schema.Record(Schema.String, Schema.String),
|
|
14
|
+
suspendedAt: Schema.NullOr(Schema.String),
|
|
15
|
+
}).pipe(Schema.encodeKeys({ suspendedAt: "suspended_at" }));
|
|
16
|
+
const InstallationsResponse = Schema.Array(InstallationResponse);
|
|
17
|
+
const RepositoryResponse = Schema.Struct({
|
|
18
|
+
id: RepositoryId,
|
|
19
|
+
name: Repository.Name,
|
|
20
|
+
}).pipe(Schema.encodeKeys({ name: "full_name" }));
|
|
21
|
+
const RepositoriesResponse = Schema.Struct({
|
|
22
|
+
totalCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
23
|
+
repositories: Schema.Array(RepositoryResponse),
|
|
24
|
+
}).pipe(Schema.encodeKeys({ totalCount: "total_count" }));
|
|
25
|
+
const TokenResponse = Schema.Struct({
|
|
26
|
+
token: Schema.NonEmptyString,
|
|
27
|
+
expiresAt: Schema.DateTimeUtcFromString,
|
|
28
|
+
repositories: Schema.OptionFromOptionalKey(Schema.Array(RepositoryResponse)),
|
|
29
|
+
}).pipe(Schema.encodeKeys({ expiresAt: "expires_at" }));
|
|
30
|
+
/** One token mint, described by its scope: what to ask for and what to call it. */
|
|
31
|
+
const mintPlan = (scope) => scope._tag === "InstallationWide"
|
|
32
|
+
? {
|
|
33
|
+
operation: "mint-installation-token",
|
|
34
|
+
body: { permissions: REQUIRED_REPOSITORY_PERMISSIONS },
|
|
35
|
+
}
|
|
36
|
+
: {
|
|
37
|
+
operation: "mint-worker-token",
|
|
38
|
+
body: {
|
|
39
|
+
permissions: REQUIRED_REPOSITORY_PERMISSIONS,
|
|
40
|
+
repository_ids: [scope.repositoryId],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Whole GitHub identity for one usine instance. The transport is supplied as
|
|
45
|
+
* an Effect HttpClient, so the root entry remains platform-free.
|
|
46
|
+
*
|
|
47
|
+
* The orchestrator-side operations share one lease — a single discovery and
|
|
48
|
+
* installation-wide mint reused until the token nears expiry — so polling does
|
|
49
|
+
* not spend two App requests per call. Worker tokens are minted per call.
|
|
50
|
+
*/
|
|
51
|
+
export class GitHubApp extends Context.Service()("@usine/github/GitHubApp") {
|
|
52
|
+
static layer = Layer.effect(GitHubApp, Effect.gen(function* () {
|
|
53
|
+
const credentials = yield* GitHubCredentials;
|
|
54
|
+
const sign = yield* appJwtSigner(credentials);
|
|
55
|
+
const http = yield* HttpClient.HttpClient;
|
|
56
|
+
/** Send one request and parse its JSON body; every other outcome is a typed error. */
|
|
57
|
+
const fetchJson = (operation, request, schema) => Effect.gen(function* () {
|
|
58
|
+
const response = yield* http
|
|
59
|
+
.execute(request)
|
|
60
|
+
.pipe(Effect.mapError((error) => transportError(operation, error)));
|
|
61
|
+
if (response.status < 200 || response.status >= 300) {
|
|
62
|
+
return yield* statusError(operation, response, yield* Clock.currentTimeMillis);
|
|
63
|
+
}
|
|
64
|
+
return yield* HttpClientResponse.schemaBodyJson(schema)(response).pipe(Effect.mapError((error) => invalidResponse(operation, error.message)));
|
|
65
|
+
});
|
|
66
|
+
const discoverInstallation = Effect.fn("GitHubApp.discoverInstallation")(function* () {
|
|
67
|
+
const jwt = yield* sign();
|
|
68
|
+
const [found, ...others] = yield* fetchJson("discover-installation", HttpClientRequest.get(`${API_ROOT}/app/installations?per_page=2`).pipe(authorized(jwt)), InstallationsResponse);
|
|
69
|
+
if (found === undefined) {
|
|
70
|
+
return yield* new InstallationDiscoveryError({
|
|
71
|
+
reason: "missing",
|
|
72
|
+
message: "The GitHub App is not installed on an account",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (others.length > 0) {
|
|
76
|
+
return yield* new InstallationDiscoveryError({
|
|
77
|
+
reason: "multiple",
|
|
78
|
+
message: "The GitHub App must have exactly one installation for this factory",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (found.suspendedAt !== null) {
|
|
82
|
+
return yield* new InstallationDiscoveryError({
|
|
83
|
+
reason: "suspended",
|
|
84
|
+
message: "The GitHub App installation is suspended",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const missing = missingPermissions(found.permissions);
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
return yield* new AppPermissionError({
|
|
90
|
+
missing,
|
|
91
|
+
message: `The GitHub App installation is missing permissions: ${missing.join(", ")}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return new Installation({ id: found.id, accountLogin: found.account.login });
|
|
95
|
+
});
|
|
96
|
+
const mintToken = Effect.fn("GitHubApp.mintToken")(function* (installationId, scope) {
|
|
97
|
+
const { operation, body } = mintPlan(scope);
|
|
98
|
+
const jwt = yield* sign();
|
|
99
|
+
const response = yield* fetchJson(operation, HttpClientRequest.post(`${API_ROOT}/app/installations/${installationId}/access_tokens`).pipe(HttpClientRequest.bodyJsonUnsafe(body), authorized(jwt)), TokenResponse);
|
|
100
|
+
const lifetime = DateTime.toEpochMillis(response.expiresAt) - (yield* Clock.currentTimeMillis);
|
|
101
|
+
if (lifetime <= 0 || lifetime > TOKEN_MAX_LIFETIME_MS) {
|
|
102
|
+
return yield* new InvalidGitHubResponseError({
|
|
103
|
+
operation,
|
|
104
|
+
message: "GitHub returned an installation token outside the one-hour lifetime bound",
|
|
105
|
+
detail: `expires_at ${DateTime.formatIso(response.expiresAt)} leaves ${lifetime}ms to live`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const confirmed = Option.getOrElse(response.repositories, () => []);
|
|
109
|
+
if (scope._tag === "SingleRepository" &&
|
|
110
|
+
!(confirmed.length === 1 && confirmed.every((repo) => repo.id === scope.repositoryId))) {
|
|
111
|
+
return yield* new InvalidGitHubResponseError({
|
|
112
|
+
operation,
|
|
113
|
+
message: "GitHub did not confirm that the worker token is scoped to exactly one repo",
|
|
114
|
+
detail: `expected repository id ${scope.repositoryId}, confirmed [${confirmed.map((repo) => repo.id).join(", ")}]`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return new InstallationToken({
|
|
118
|
+
value: Redacted.make(response.token),
|
|
119
|
+
expiresAt: response.expiresAt,
|
|
120
|
+
scope,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
const installedRepositories = Effect.fn("GitHubApp.installedRepositories")(function* (token) {
|
|
124
|
+
const installed = [];
|
|
125
|
+
for (let page = 1;; page += 1) {
|
|
126
|
+
const response = yield* fetchJson("list-installation-repositories", HttpClientRequest.get(`${API_ROOT}/installation/repositories?per_page=100&page=${page}`).pipe(authorized(token.value)), RepositoriesResponse);
|
|
127
|
+
installed.push(...response.repositories);
|
|
128
|
+
if (installed.length >= response.totalCount)
|
|
129
|
+
return installed;
|
|
130
|
+
if (response.repositories.length === 0) {
|
|
131
|
+
return yield* new InvalidGitHubResponseError({
|
|
132
|
+
operation: "list-installation-repositories",
|
|
133
|
+
message: "GitHub pagination ended before total_count repositories were returned",
|
|
134
|
+
detail: `page ${page} was empty after ${installed.length} of ${response.totalCount} repositories`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const leaseRef = yield* SynchronizedRef.make(Option.none());
|
|
140
|
+
/** The current lease, renewed under the margin; renewals are serialized by the ref. */
|
|
141
|
+
const lease = Effect.fn("GitHubApp.lease")(() => SynchronizedRef.modifyEffect(leaseRef, (current) => Effect.gen(function* () {
|
|
142
|
+
const now = yield* Clock.currentTimeMillis;
|
|
143
|
+
if (Option.isSome(current) &&
|
|
144
|
+
DateTime.toEpochMillis(current.value.token.expiresAt) - now > LEASE_RENEWAL_MARGIN_MS) {
|
|
145
|
+
const kept = [current.value, current];
|
|
146
|
+
return kept;
|
|
147
|
+
}
|
|
148
|
+
const installation = yield* discoverInstallation();
|
|
149
|
+
const token = yield* mintToken(installation.id, new InstallationWide());
|
|
150
|
+
const renewed = { installation, token };
|
|
151
|
+
const stored = [renewed, Option.some(renewed)];
|
|
152
|
+
return stored;
|
|
153
|
+
})));
|
|
154
|
+
return {
|
|
155
|
+
installation: Effect.fn("GitHubApp.installation")(() => Effect.map(lease(), (current) => current.installation)),
|
|
156
|
+
installationToken: Effect.fn("GitHubApp.installationToken")(() => Effect.map(lease(), (current) => current.token)),
|
|
157
|
+
projectAccess: Effect.fn("GitHubApp.projectAccess")(function* (registry) {
|
|
158
|
+
const { installation, token } = yield* lease();
|
|
159
|
+
const installed = yield* installedRepositories(token);
|
|
160
|
+
return deriveProjectAccess(installation, registry, installed);
|
|
161
|
+
}),
|
|
162
|
+
workerToken: Effect.fn("GitHubApp.workerToken")((project) => mintToken(project.installationId, new SingleRepository({
|
|
163
|
+
repository: project.repository,
|
|
164
|
+
repositoryId: project.repositoryId,
|
|
165
|
+
}))),
|
|
166
|
+
};
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=github-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-app.js","sourceRoot":"","sources":["../src/github-app.ts"],"names":[],"mappings":"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"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Option, Redacted } from "effect";
|
|
2
|
+
import { HttpClientRequest, type HttpClientResponse } from "effect/unstable/http";
|
|
3
|
+
import type { HttpClientError } from "effect/unstable/http/HttpClientError";
|
|
4
|
+
import { GitHubApiError, GitHubRateLimitError, InvalidGitHubResponseError, type GitHubOperation } from "./model.ts";
|
|
5
|
+
export declare const API_ROOT = "https://api.github.com";
|
|
6
|
+
export declare const API_VERSION = "2026-03-10";
|
|
7
|
+
export declare const authorized: (credential: Redacted.Redacted<string>) => (self: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest;
|
|
8
|
+
export declare const transportError: (operation: GitHubOperation, error: HttpClientError) => GitHubApiError;
|
|
9
|
+
export declare const invalidResponse: (operation: GitHubOperation, detail: string) => InvalidGitHubResponseError;
|
|
10
|
+
type Headers = HttpClientResponse.HttpClientResponse["headers"];
|
|
11
|
+
export declare const retryAfterMillis: (value: string | undefined, now: number) => Option.Option<number>;
|
|
12
|
+
export declare const rateLimitResetMillis: (headers: Headers, threshold: number) => Option.Option<number>;
|
|
13
|
+
export declare const pollIntervalMillis: (headers: Headers, now: number) => Option.Option<number>;
|
|
14
|
+
/** When GitHub asked us to come back, from `retry-after` and an exhausted primary budget. */
|
|
15
|
+
export declare const rateLimitRetryEpoch: (headers: Headers, now: number) => Option.Option<number>;
|
|
16
|
+
/** One classification for every non-2xx GitHub status: rate-limit pauses become typed waits. */
|
|
17
|
+
export declare const statusError: (operation: GitHubOperation, response: HttpClientResponse.HttpClientResponse, now: number) => GitHubApiError | GitHubRateLimitError;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,QAAQ,2BAA2B,CAAC;AACjD,eAAO,MAAM,WAAW,eAAe,CAAC;AAExC,eAAO,MAAM,UAAU,eAAgB,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,uFAM5D,CAAC;AAEL,eAAO,MAAM,cAAc,cAAe,eAAe,SAAS,eAAe,mBAQ7E,CAAC;AAEL,eAAO,MAAM,eAAe,cAAe,eAAe,UAAU,MAAM,+BAKtE,CAAC;AAEL,KAAK,OAAO,GAAG,kBAAkB,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAQhE,eAAO,MAAM,gBAAgB,UAAW,MAAM,GAAG,SAAS,OAAO,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAM7F,CAAC;AAEF,eAAO,MAAM,oBAAoB,YACtB,OAAO,aACL,MAAM,KAChB,MAAM,CAAC,MAAM,CAAC,MAAM,CAMtB,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAAa,OAAO,OAAO,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CACG,CAAC;AAS3F,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,YAAa,OAAO,OAAO,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CACQ,CAAC;AAEjG,gGAAgG;AAChG,eAAO,MAAM,WAAW,cACX,eAAe,YAChB,kBAAkB,CAAC,kBAAkB,OAC1C,MAAM,KACV,cAAc,GAAG,oBAanB,CAAC"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DateTime, Option, Redacted } from "effect";
|
|
2
|
+
import { HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import { GitHubApiError, GitHubRateLimitError, InvalidGitHubResponseError, } from "./model.js";
|
|
4
|
+
export const API_ROOT = "https://api.github.com";
|
|
5
|
+
export const API_VERSION = "2026-03-10";
|
|
6
|
+
export const authorized = (credential) => HttpClientRequest.setHeaders({
|
|
7
|
+
accept: "application/vnd.github+json",
|
|
8
|
+
authorization: `Bearer ${Redacted.value(credential)}`,
|
|
9
|
+
"user-agent": "usine",
|
|
10
|
+
"x-github-api-version": API_VERSION,
|
|
11
|
+
});
|
|
12
|
+
export const transportError = (operation, error) => new GitHubApiError({
|
|
13
|
+
operation,
|
|
14
|
+
status: error.response === undefined ? Option.none() : Option.some(error.response.status),
|
|
15
|
+
message: error.response === undefined
|
|
16
|
+
? "The request to GitHub could not be sent"
|
|
17
|
+
: `GitHub rejected the request with status ${error.response.status}`,
|
|
18
|
+
});
|
|
19
|
+
export const invalidResponse = (operation, detail) => new InvalidGitHubResponseError({
|
|
20
|
+
operation,
|
|
21
|
+
message: "GitHub returned a response that does not match its API contract",
|
|
22
|
+
detail,
|
|
23
|
+
});
|
|
24
|
+
const parseInteger = (value) => {
|
|
25
|
+
if (value === undefined || !/^\d+$/u.test(value))
|
|
26
|
+
return Option.none();
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
return Number.isSafeInteger(parsed) ? Option.some(parsed) : Option.none();
|
|
29
|
+
};
|
|
30
|
+
export const retryAfterMillis = (value, now) => {
|
|
31
|
+
const seconds = parseInteger(value);
|
|
32
|
+
if (Option.isSome(seconds))
|
|
33
|
+
return Option.some(now + seconds.value * 1_000);
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return Option.none();
|
|
36
|
+
const parsed = Date.parse(value);
|
|
37
|
+
return Number.isFinite(parsed) ? Option.some(parsed) : Option.none();
|
|
38
|
+
};
|
|
39
|
+
export const rateLimitResetMillis = (headers, threshold) => {
|
|
40
|
+
const remaining = parseInteger(headers["x-ratelimit-remaining"]);
|
|
41
|
+
const reset = parseInteger(headers["x-ratelimit-reset"]);
|
|
42
|
+
return Option.isSome(remaining) && remaining.value <= threshold && Option.isSome(reset)
|
|
43
|
+
? Option.some(reset.value * 1_000)
|
|
44
|
+
: Option.none();
|
|
45
|
+
};
|
|
46
|
+
export const pollIntervalMillis = (headers, now) => Option.map(parseInteger(headers["x-poll-interval"]), (seconds) => now + seconds * 1_000);
|
|
47
|
+
const latestEpoch = (candidates) => {
|
|
48
|
+
const epochs = candidates.flatMap((candidate) => Option.match(candidate, { onNone: () => [], onSome: (value) => [value] }));
|
|
49
|
+
return epochs.length === 0 ? Option.none() : Option.some(Math.max(...epochs));
|
|
50
|
+
};
|
|
51
|
+
/** When GitHub asked us to come back, from `retry-after` and an exhausted primary budget. */
|
|
52
|
+
export const rateLimitRetryEpoch = (headers, now) => latestEpoch([retryAfterMillis(headers["retry-after"], now), rateLimitResetMillis(headers, 0)]);
|
|
53
|
+
/** One classification for every non-2xx GitHub status: rate-limit pauses become typed waits. */
|
|
54
|
+
export const statusError = (operation, response, now) => {
|
|
55
|
+
const retryAt = Option.flatMap(rateLimitRetryEpoch(response.headers, now), DateTime.make);
|
|
56
|
+
return (response.status === 403 || response.status === 429) && Option.isSome(retryAt)
|
|
57
|
+
? new GitHubRateLimitError({
|
|
58
|
+
operation,
|
|
59
|
+
retryAt: retryAt.value,
|
|
60
|
+
message: "GitHub asked the poller to wait before retrying",
|
|
61
|
+
})
|
|
62
|
+
: new GitHubApiError({
|
|
63
|
+
operation,
|
|
64
|
+
status: Option.some(response.status),
|
|
65
|
+
message: `GitHub rejected the request with status ${response.status}`,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export {};
|
|
1
|
+
/** @usine/github — GitHub App identity and scoped installation tokens. */
|
|
2
|
+
export { GitHubApp } from "./github-app.ts";
|
|
3
|
+
export { GitHubItemWriter, ItemLabelMutation } from "./item-writer.ts";
|
|
4
|
+
export { AppAuthenticationError, AppCredentials, AppId, AppPermissionError, CoveredProject, GitHubApiError, GitHubAppError, GitHubCredentials, GitHubOperation, GitHubRateLimitError, Installation, InstallationDiscoveryError, InstallationDiscoveryReason, InstallationId, InstallationToken, InstallationWide, InvalidGitHubResponseError, ProjectAccess, ProjectNotInstalledError, PrivateKey, REQUIRED_REPOSITORY_PERMISSIONS, RedactedPrivateKey, RepositoryId, SingleRepository, TokenScope, deriveProjectAccess, missingPermissions, type InstalledRepository, type RequiredRepositoryPermission, } from "./model.ts";
|
|
5
|
+
export { ChangedIssue, ChangedItem, ChangedPullRequest, ChangeDetection, ContentHash, EntityTag, IssuePayload, IssueSnapshot, PayloadSnapshot, PollCursor, PullRequestPayload, PullRequestSnapshot, RepositoryLabelCatalog, RepositoryLabelDetection, RepositoryLabelName, RepositoryLabelsNotModified, RepositoryChanges, RepositoryNotModified, } from "./polling/model.ts";
|
|
6
|
+
export { GitHubPolling } from "./polling/polling.ts";
|
|
8
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,KAAK,EACL,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,0BAA0B,EAC1B,2BAA2B,EAC3B,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,+BAA+B,EAC/B,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,GAClC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,EACT,YAAY,EACZ,aAAa,EACb,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export {};
|
|
1
|
+
/** @usine/github — GitHub App identity and scoped installation tokens. */
|
|
2
|
+
export { GitHubApp } from "./github-app.js";
|
|
3
|
+
export { GitHubItemWriter, ItemLabelMutation } from "./item-writer.js";
|
|
4
|
+
export { AppAuthenticationError, AppCredentials, AppId, AppPermissionError, CoveredProject, GitHubApiError, GitHubAppError, GitHubCredentials, GitHubOperation, GitHubRateLimitError, Installation, InstallationDiscoveryError, InstallationDiscoveryReason, InstallationId, InstallationToken, InstallationWide, InvalidGitHubResponseError, ProjectAccess, ProjectNotInstalledError, PrivateKey, REQUIRED_REPOSITORY_PERMISSIONS, RedactedPrivateKey, RepositoryId, SingleRepository, TokenScope, deriveProjectAccess, missingPermissions, } from "./model.js";
|
|
5
|
+
export { ChangedIssue, ChangedItem, ChangedPullRequest, ChangeDetection, ContentHash, EntityTag, IssuePayload, IssueSnapshot, PayloadSnapshot, PollCursor, PullRequestPayload, PullRequestSnapshot, RepositoryLabelCatalog, RepositoryLabelDetection, RepositoryLabelName, RepositoryLabelsNotModified, RepositoryChanges, RepositoryNotModified, } from "./polling/model.js";
|
|
6
|
+
export { GitHubPolling } from "./polling/polling.js";
|
|
8
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,KAAK,EACL,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,0BAA0B,EAC1B,2BAA2B,EAC3B,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,+BAA+B,EAC/B,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,mBAAmB,EACnB,kBAAkB,GAGnB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,EACT,YAAY,EACZ,aAAa,EACb,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Item, Repository } from "@usine/core";
|
|
2
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import { GitHubApp } from "./github-app.ts";
|
|
5
|
+
import type { GitHubAppError } from "./model.ts";
|
|
6
|
+
declare const ItemLabelMutation_base: Schema.Class<ItemLabelMutation, Schema.Struct<{
|
|
7
|
+
readonly add: Schema.$Array<Schema.NonEmptyString>;
|
|
8
|
+
readonly remove: Schema.$Array<Schema.NonEmptyString>;
|
|
9
|
+
}>, {}>;
|
|
10
|
+
/** Lane-scoped label changes. Labels outside these two sets are never rewritten. */
|
|
11
|
+
export declare class ItemLabelMutation extends ItemLabelMutation_base {
|
|
12
|
+
}
|
|
13
|
+
declare const GitHubItemWriter_base: Context.ServiceClass<GitHubItemWriter, "@usine/github/GitHubItemWriter", {
|
|
14
|
+
/** Adds/removes only the named labels; every other applied label is preserved. */
|
|
15
|
+
readonly updateLabels: (repository: Repository.Name, item: Item.Number, mutation: ItemLabelMutation) => Effect.Effect<ReadonlyArray<string>, GitHubAppError>;
|
|
16
|
+
}>;
|
|
17
|
+
/** Authenticated GitHub Item writes used by the Board trigger surface. */
|
|
18
|
+
export declare class GitHubItemWriter extends GitHubItemWriter_base {
|
|
19
|
+
static readonly layer: Layer.Layer<GitHubItemWriter, never, GitHubApp | HttpClient.HttpClient>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=item-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item-writer.d.ts","sourceRoot":"","sources":["../src/item-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAS,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAyC,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;;;;;AAKjD,oFAAoF;AACpF,qBAAa,iBAAkB,SAAQ,sBAKrC;CAAG;;IAcD,kFAAkF;2BAC3D,CACrB,UAAU,EAAE,UAAU,CAAC,IAAI,EAC3B,IAAI,EAAE,IAAI,CAAC,MAAM,EACjB,QAAQ,EAAE,iBAAiB,KACxB,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;;AAT7D,0EAA0E;AAC1E,qBAAa,gBAAiB,SAAQ,qBAUD;IACnC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CA4E1F;CACL"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Item, Repository } from "@usine/core";
|
|
2
|
+
import { Clock, Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
4
|
+
import { GitHubApp } from "./github-app.js";
|
|
5
|
+
import { API_ROOT, authorized, invalidResponse, statusError, transportError } from "./http.js";
|
|
6
|
+
const LabelResponse = Schema.Array(Schema.Struct({ name: Schema.NonEmptyString }));
|
|
7
|
+
const LabelNames = Schema.Array(Schema.NonEmptyString).check(Schema.isUnique());
|
|
8
|
+
/** Lane-scoped label changes. Labels outside these two sets are never rewritten. */
|
|
9
|
+
export class ItemLabelMutation extends Schema.Class("@usine/github/ItemLabelMutation")({
|
|
10
|
+
add: LabelNames.check(Schema.isNonEmpty()),
|
|
11
|
+
remove: LabelNames,
|
|
12
|
+
}) {
|
|
13
|
+
}
|
|
14
|
+
const labelsUrl = (repository, item) => {
|
|
15
|
+
const { name, owner } = Repository.parts(repository);
|
|
16
|
+
return `${API_ROOT}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues/${item}/labels`;
|
|
17
|
+
};
|
|
18
|
+
const labelUrl = (repository, item, label) => `${labelsUrl(repository, item)}/${encodeURIComponent(label)}`;
|
|
19
|
+
/** Authenticated GitHub Item writes used by the Board trigger surface. */
|
|
20
|
+
export class GitHubItemWriter extends Context.Service()("@usine/github/GitHubItemWriter") {
|
|
21
|
+
static layer = Layer.effect(GitHubItemWriter, Effect.gen(function* () {
|
|
22
|
+
const app = yield* GitHubApp;
|
|
23
|
+
const http = yield* HttpClient.HttpClient;
|
|
24
|
+
const send = Effect.fn("GitHubItemWriter.sendLabelUpdate")(function* (request) {
|
|
25
|
+
return yield* http
|
|
26
|
+
.execute(request)
|
|
27
|
+
.pipe(Effect.mapError((error) => transportError("update-item-labels", error)));
|
|
28
|
+
});
|
|
29
|
+
const decode = (response) => HttpClientResponse.schemaBodyJson(LabelResponse)(response).pipe(Effect.mapError((error) => invalidResponse("update-item-labels", error.message)));
|
|
30
|
+
return {
|
|
31
|
+
updateLabels: Effect.fn("GitHubItemWriter.updateLabels")(function* (repository, item, mutation) {
|
|
32
|
+
yield* Effect.annotateCurrentSpan({ repository, item });
|
|
33
|
+
const token = yield* app.installationToken();
|
|
34
|
+
const addResponse = yield* send(HttpClientRequest.post(labelsUrl(repository, item)).pipe(HttpClientRequest.bodyJsonUnsafe({ labels: mutation.add }), authorized(token.value)));
|
|
35
|
+
if (addResponse.status < 200 || addResponse.status >= 300) {
|
|
36
|
+
return yield* statusError("update-item-labels", addResponse, yield* Clock.currentTimeMillis);
|
|
37
|
+
}
|
|
38
|
+
let applied = yield* decode(addResponse);
|
|
39
|
+
for (const label of mutation.remove) {
|
|
40
|
+
const removeResponse = yield* send(HttpClientRequest.delete(labelUrl(repository, item, label)).pipe(authorized(token.value)));
|
|
41
|
+
if (removeResponse.status === 404) {
|
|
42
|
+
yield* Effect.logDebug("Lane label was already absent during Board move", {
|
|
43
|
+
repository,
|
|
44
|
+
item,
|
|
45
|
+
label,
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (removeResponse.status < 200 || removeResponse.status >= 300) {
|
|
50
|
+
return yield* statusError("update-item-labels", removeResponse, yield* Clock.currentTimeMillis);
|
|
51
|
+
}
|
|
52
|
+
applied = yield* decode(removeResponse);
|
|
53
|
+
}
|
|
54
|
+
yield* Effect.logInfo("GitHub Item lane labels updated from a Board move", {
|
|
55
|
+
repository,
|
|
56
|
+
item,
|
|
57
|
+
addedLabels: mutation.add.join(", "),
|
|
58
|
+
removedLabels: mutation.remove.join(", "),
|
|
59
|
+
labels: applied.map(({ name }) => name).join(", "),
|
|
60
|
+
});
|
|
61
|
+
return applied.map(({ name }) => name);
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=item-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item-writer.js","sourceRoot":"","sources":["../src/item-writer.ts"],"names":[],"mappings":"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"}
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect, Redacted } from "effect";
|
|
2
|
+
import { AppAuthenticationError, type AppCredentials } from "./model.ts";
|
|
3
|
+
/** Signs App JWTs: `iat` backdated a minute for clock skew, `exp` under GitHub's ten-minute cap. */
|
|
4
|
+
export type AppJwtSigner = () => Effect.Effect<Redacted.Redacted<string>>;
|
|
5
|
+
/**
|
|
6
|
+
* Import the App private key once (platform-free WebCrypto, RS256). A key
|
|
7
|
+
* that cannot be imported fails here, at composition, not on the first request.
|
|
8
|
+
*/
|
|
9
|
+
export declare const appJwtSigner: (credentials: AppCredentials) => Effect.Effect<AppJwtSigner, AppAuthenticationError, never>;
|
|
10
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAoEzE,oGAAoG;AACpG,MAAM,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,YAAY,6FA6BvB,CAAC"}
|