@effected/github 0.1.0 → 0.2.1
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/ArtifactMetadata.js +5 -3
- package/GitHubApp.js +16 -0
- package/GitHubCommit.js +17 -3
- package/GitHubRelease.js +2 -1
- package/PullRequest.js +8 -1
- package/README.md +172 -0
- package/index.d.ts +129 -56
- package/index.js +1 -1
- package/package.json +2 -2
package/ArtifactMetadata.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitHubClient } from "./GitHubClient.js";
|
|
2
|
+
import { Repo } from "./Repo.js";
|
|
2
3
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
//#region src/ArtifactMetadata.ts
|
|
@@ -42,13 +43,14 @@ var ArtifactMetadata = class ArtifactMetadata extends Context.Service()("@effect
|
|
|
42
43
|
const unstubbed = (member) => {
|
|
43
44
|
throw new Error(`ArtifactMetadata.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
44
45
|
};
|
|
45
|
-
const make = (client) => ({ createStorageRecord: Effect.fn("ArtifactMetadata.createStorageRecord")(function* (
|
|
46
|
+
const make = (client) => ({ createStorageRecord: Effect.fn("ArtifactMetadata.createStorageRecord")(function* (input) {
|
|
47
|
+
const { owner } = yield* Repo;
|
|
46
48
|
yield* Effect.annotateCurrentSpan({
|
|
47
|
-
org,
|
|
49
|
+
org: owner,
|
|
48
50
|
artifact: input.name
|
|
49
51
|
});
|
|
50
52
|
return ((yield* client.request("POST /orgs/{org}/artifacts/metadata/storage-record", {
|
|
51
|
-
org,
|
|
53
|
+
org: owner,
|
|
52
54
|
name: input.name,
|
|
53
55
|
digest: input.digest,
|
|
54
56
|
registry_url: input.registryUrl,
|
package/GitHubApp.js
CHANGED
|
@@ -122,6 +122,22 @@ var BotIdentity = class BotIdentity extends Schema.Class("BotIdentity")({
|
|
|
122
122
|
name: "github-actions[bot]",
|
|
123
123
|
email: "41898282+github-actions[bot]@users.noreply.github.com"
|
|
124
124
|
});
|
|
125
|
+
/**
|
|
126
|
+
* The DCO sign-off trailer for this identity.
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* `Signed-off-by: name <email>` — DCO 1.1's fixed casing and spacing, with
|
|
130
|
+
* only the email in angle brackets, rendered by the type that owns the
|
|
131
|
+
* data. Commits created
|
|
132
|
+
* through the Git Data API bypass `git commit -s`, so no porcelain adds
|
|
133
|
+
* the trailer, and a hand-built one that is subtly wrong fails late as a
|
|
134
|
+
* red DCO check on someone else's pull request. Whether a missing
|
|
135
|
+
* identity falls back to {@link BotIdentity.githubActions} stays the
|
|
136
|
+
* caller's policy.
|
|
137
|
+
*/
|
|
138
|
+
get signoff() {
|
|
139
|
+
return `Signed-off-by: ${this.name} <${this.email}>`;
|
|
140
|
+
}
|
|
125
141
|
};
|
|
126
142
|
/**
|
|
127
143
|
* What GitHub knows about the app itself.
|
package/GitHubCommit.js
CHANGED
|
@@ -19,7 +19,16 @@ var CommitSummary = class extends Schema.Class("CommitSummary")({
|
|
|
19
19
|
/** The GitHub login of the authoring account, when GitHub could attribute one. */
|
|
20
20
|
authorLogin: Schema.optionalKey(Schema.String),
|
|
21
21
|
/** The web URL for the commit. */
|
|
22
|
-
url: Schema.String
|
|
22
|
+
url: Schema.String,
|
|
23
|
+
/**
|
|
24
|
+
* The parent commit shas, in the order GitHub lists them.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Empty for a root commit, two or more for a merge commit. A required field,
|
|
28
|
+
* not an optional one: every commit endpoint this package reads reports
|
|
29
|
+
* `parents`, so "which commit(s) did this come from" never needs a raw route.
|
|
30
|
+
*/
|
|
31
|
+
parents: Schema.Array(Schema.String)
|
|
23
32
|
}) {
|
|
24
33
|
/** The message's first line. */
|
|
25
34
|
get subject() {
|
|
@@ -104,8 +113,13 @@ const summarize = (raw) => CommitSummary.make({
|
|
|
104
113
|
message: raw.commit.message,
|
|
105
114
|
author: raw.commit.author?.name ?? "Unknown",
|
|
106
115
|
...raw.author?.login !== void 0 ? { authorLogin: raw.author.login } : {},
|
|
107
|
-
url: raw.html_url
|
|
116
|
+
url: raw.html_url,
|
|
117
|
+
parents: raw.parents.map((parent) => parent.sha)
|
|
108
118
|
});
|
|
119
|
+
/**
|
|
120
|
+
* Project a `diff-entry` to a {@link CommitFile}. Shared with
|
|
121
|
+
* `PullRequest.listFiles`; not re-exported from the package entrypoint.
|
|
122
|
+
*/
|
|
109
123
|
const fileOf = (raw) => CommitFile.make({
|
|
110
124
|
path: raw.filename,
|
|
111
125
|
status: raw.status,
|
|
@@ -195,4 +209,4 @@ const make = (client) => ({
|
|
|
195
209
|
});
|
|
196
210
|
|
|
197
211
|
//#endregion
|
|
198
|
-
export { CommitComparison, CommitFile, CommitSummary, FileStatus, GitHubCommit };
|
|
212
|
+
export { CommitComparison, CommitFile, CommitSummary, FileStatus, GitHubCommit, fileOf };
|
package/GitHubRelease.js
CHANGED
|
@@ -155,11 +155,12 @@ const make = (client) => {
|
|
|
155
155
|
release: release.id,
|
|
156
156
|
asset: asset.name
|
|
157
157
|
});
|
|
158
|
-
const raw = yield* client.requestDecoded("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
|
|
158
|
+
const raw = yield* client.requestDecoded(asset.label === void 0 ? "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name}" : "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}", {
|
|
159
159
|
owner,
|
|
160
160
|
repo,
|
|
161
161
|
release_id: release.id,
|
|
162
162
|
name: asset.name,
|
|
163
|
+
...asset.label === void 0 ? {} : { label: asset.label },
|
|
163
164
|
data: asset.data,
|
|
164
165
|
baseUrl: UPLOADS_BASE_URL,
|
|
165
166
|
headers: { "content-type": asset.contentType }
|
package/PullRequest.js
CHANGED
|
@@ -2,6 +2,7 @@ import { GitHubError } from "./GitHubError.js";
|
|
|
2
2
|
import { GraphQLDocument } from "./GraphQL.js";
|
|
3
3
|
import { GitHubClient } from "./GitHubClient.js";
|
|
4
4
|
import { Repo } from "./Repo.js";
|
|
5
|
+
import { fileOf } from "./GitHubCommit.js";
|
|
5
6
|
import { PageOptions } from "./Rest.js";
|
|
6
7
|
import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
|
|
7
8
|
|
|
@@ -28,8 +29,12 @@ var PullRequestInfo = class extends Schema.Class("PullRequestInfo")({
|
|
|
28
29
|
state: Schema.Literals(["open", "closed"]),
|
|
29
30
|
/** The source branch name. */
|
|
30
31
|
head: Schema.String,
|
|
32
|
+
/** The sha the source branch pointed at when GitHub answered. */
|
|
33
|
+
headSha: Schema.String,
|
|
31
34
|
/** The target branch name. */
|
|
32
35
|
base: Schema.String,
|
|
36
|
+
/** The sha the target branch pointed at when GitHub answered — the commit the pull request branches from. */
|
|
37
|
+
baseSha: Schema.String,
|
|
33
38
|
draft: Schema.Boolean,
|
|
34
39
|
merged: Schema.Boolean,
|
|
35
40
|
/**
|
|
@@ -104,7 +109,9 @@ const project = (raw) => Effect.try({
|
|
|
104
109
|
title: raw.title,
|
|
105
110
|
state: raw.state === "closed" ? "closed" : "open",
|
|
106
111
|
head: raw.head.ref,
|
|
112
|
+
headSha: raw.head.sha,
|
|
107
113
|
base: raw.base.ref,
|
|
114
|
+
baseSha: raw.base.sha,
|
|
108
115
|
draft: raw.draft ?? false,
|
|
109
116
|
merged: raw.merged ?? raw.merged_at != null,
|
|
110
117
|
mergedAt: raw.merged_at == null ? Option.none() : Option.some(DateTime.makeUnsafe(raw.merged_at)),
|
|
@@ -193,7 +200,7 @@ const make = (client) => {
|
|
|
193
200
|
owner,
|
|
194
201
|
repo,
|
|
195
202
|
pull_number: number
|
|
196
|
-
}, options?.page)).map(
|
|
203
|
+
}, options?.page)).map(fileOf);
|
|
197
204
|
}),
|
|
198
205
|
listAssociatedWithCommit: Effect.fn("PullRequest.listAssociatedWithCommit")(function* (sha, options) {
|
|
199
206
|
const { owner, repo } = yield* Repo;
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @effected/github
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@effected/github)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
Typed GitHub REST and GraphQL for [Effect](https://effect.website) v4. `client.request("GET /repos/{owner}/{repo}", { owner, repo })` types both the parameters and the returned `data` from the route literal alone — no `operation: string`, no callback, no cast. One `GitHubError` covers every REST failure with a `kind` you branch on instead of grepping a message, one pagination engine backs every paginating route and `client.request`'s `Stream` form, and a set of resource services (`GitBranch`, `GitTag`, `CheckRun`, `PullRequest`, `PullRequestComment`, `GitHubRelease`, `Attestation`) turn multi-call dances — "does this branch already exist?", "conclude this check run no matter how the program exits" — into one call. `GitHubApp` mints and revokes installation tokens for App auth.
|
|
9
|
+
|
|
10
|
+
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
11
|
+
> development against a single pinned Effect v4 beta. Packages graduate to
|
|
12
|
+
> `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
|
|
13
|
+
> exactly the ones the kit is built and tested against, install
|
|
14
|
+
> [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
|
|
15
|
+
>
|
|
16
|
+
> **Stability: unstable.** This package's API surface is not yet considered
|
|
17
|
+
> complete and may change across `0.x` releases. Pin an exact version — even a
|
|
18
|
+
> package marked *stable* before `1.0.0` can introduce a breaking change by
|
|
19
|
+
> accident, and an exact pin turns that into a type-check error rather than a
|
|
20
|
+
> runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
|
|
21
|
+
|
|
22
|
+
## Why @effected/github
|
|
23
|
+
|
|
24
|
+
Hand-rolled octokit wrappers tend to converge on the same shape: a `rest<T>(operation: string, fn: (octokit) => Promise<{ data: T }>)` helper where `T` is whatever the caller wrote and nothing connects it to the endpoint. This package's route-keyed `request` closes that gap — `@octokit/types` already generates a map from GitHub's own OpenAPI description, and `@octokit/core`'s `request` already consumes it, so there was no reason to reinvent either.
|
|
25
|
+
|
|
26
|
+
The error model gets the same treatment. `GitHubError.kind` is a literal union (`notFound`, `alreadyExists`, `rejected`, `unauthorized`, `rateLimited`, `transport`, `decode`) produced by one classification step, `GitHubError.fromOctokit`, instead of one hand-written mapper per resource. `kind: "alreadyExists"` is what makes `GitBranch.upsert` and `GitTag.upsert` a single round trip in the common case, rather than a create-then-catch-then-check-then-reset dance repeated at every call site that needs it.
|
|
27
|
+
|
|
28
|
+
This package also owns the octokit runtime so nothing downstream has to. `@octokit/rest` and `@octokit/auth-app` are deliberately absent — the rest wrapper is a request-logging plugin plus 1.4 MB of duplicate endpoint types, and `auth-app` drags in `createOAuthUserAuth`, roughly 492 KB of OAuth machinery this package never calls. GitHub App JWTs are signed by `universal-github-app-jwt` instead, the same zero-dependency signer `@octokit/auth-app` itself uses. A consumer that only authenticates with a token it already holds never links the JWT signer at all: the App-authenticated layer lives in its own module (`GitHubApp`), and nothing here is gathered into a namespace object that would defeat that split.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @effected/github effect
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @effected/github effect
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Node.js >=24.11.0. `effect` v4 is a peer dependency.
|
|
41
|
+
|
|
42
|
+
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { GitHubClient, Repo, RepoRef } from "@effected/github";
|
|
48
|
+
import { Effect } from "effect";
|
|
49
|
+
|
|
50
|
+
const program = Effect.gen(function* () {
|
|
51
|
+
const client = yield* GitHubClient;
|
|
52
|
+
const repo = yield* client.request("GET /repos/{owner}/{repo}", { owner: "effect-ts", repo: "effect" });
|
|
53
|
+
return repo.default_branch; // string — typed from the route literal, no cast
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const ClientLayer = GitHubClient.layerFromConfig(); // reads GITHUB_TOKEN through the ambient ConfigProvider
|
|
57
|
+
const RepoLayer = Repo.layer(RepoRef.make({ owner: "effect-ts", repo: "effect" }));
|
|
58
|
+
|
|
59
|
+
Effect.runPromise(program.pipe(Effect.provide(ClientLayer), Effect.provide(RepoLayer))).then(console.log);
|
|
60
|
+
// the repository's default branch name, e.g. "main"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`Repo` carries the `{ owner, repo }` coordinate in `R` rather than as a per-call argument, so every resource method below reads as a single expression. A program acting on more than one repository uses `Repo.provide(otherRef)` around the part that needs it.
|
|
64
|
+
|
|
65
|
+
## Resource services
|
|
66
|
+
|
|
67
|
+
Each resource is a service over `GitHubClient`, with its own `layer`, `makeTest` and `layerTest`. `GitBranch.upsert` is the case that motivated the whole set:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { GitBranch, GitHubClient, Repo } from "@effected/github";
|
|
71
|
+
import { Effect } from "effect";
|
|
72
|
+
|
|
73
|
+
const program = Effect.gen(function* () {
|
|
74
|
+
const branches = yield* GitBranch;
|
|
75
|
+
return yield* branches.upsert("release/1.2", "abc123");
|
|
76
|
+
});
|
|
77
|
+
// Effect<"created" | "reset", GitHubError, GitBranch | Repo>
|
|
78
|
+
// one round trip in the common case, two in the raced one — never four
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`CheckRun.withCheckRun` runs a program inside a check run and concludes it on every exit path — success, typed failure, defect or interrupt — so a run never gets stuck `in_progress`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { CheckRun } from "@effected/github";
|
|
85
|
+
import { Effect } from "effect";
|
|
86
|
+
|
|
87
|
+
declare const lint: () => Effect.Effect<ReadonlyArray<string>>;
|
|
88
|
+
declare const deriveConclusion: (findings: ReadonlyArray<string>) => "success" | "neutral" | "failure";
|
|
89
|
+
|
|
90
|
+
const program = Effect.gen(function* () {
|
|
91
|
+
const check = yield* CheckRun;
|
|
92
|
+
return yield* check.withCheckRun("lint", "abc123", (_id, conclude) =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
const findings = yield* lint();
|
|
95
|
+
yield* conclude(deriveConclusion(findings));
|
|
96
|
+
return findings;
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`GitTag.latestSemver` walks tags and picks the newest version-shaped one in a single pass, over `@effected/semver`'s synchronous comparator — no round trip per candidate. `PullRequest.upsert` and `PullRequestComment.upsert` (a marker-tagged sticky comment) follow the same one-call-one-intent shape.
|
|
103
|
+
|
|
104
|
+
## GitHub App authentication
|
|
105
|
+
|
|
106
|
+
`GitHubApp.clientLayer` builds a `GitHubClient` authenticated as an app installation. The token is minted on build, re-minted a minute before it expires, and revoked on release — best effort — so a workflow does not leave live credentials behind:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { GitHubApp } from "@effected/github";
|
|
110
|
+
import { Redacted } from "effect";
|
|
111
|
+
|
|
112
|
+
// bind once — layers memoize by reference
|
|
113
|
+
const AppClient = GitHubApp.clientLayer({
|
|
114
|
+
appId: "123456",
|
|
115
|
+
privateKey: Redacted.make(process.env.GITHUB_APP_PRIVATE_KEY as string),
|
|
116
|
+
owner: "effect-ts",
|
|
117
|
+
});
|
|
118
|
+
// Layer<GitHubClient, GitHubAppError> — provide it wherever a GitHubClient is needed
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Only `GitHubApp` and its statics import the JWT signer — a consumer authenticating with a plain token never reaches it.
|
|
122
|
+
|
|
123
|
+
## Errors
|
|
124
|
+
|
|
125
|
+
`GitHubError` covers REST, `GitHubGraphQLError` covers GraphQL, `GitHubAppError` covers app authentication, and `TokenPermissions` produces its own `TokenPermissionError` — four kinds, not one per resource:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { GitHubError } from "@effected/github";
|
|
129
|
+
import { Effect } from "effect";
|
|
130
|
+
|
|
131
|
+
declare const upsertBranch: Effect.Effect<"created" | "reset", GitHubError>;
|
|
132
|
+
|
|
133
|
+
const program = upsertBranch.pipe(
|
|
134
|
+
Effect.catchIf(GitHubError.hasKind("rateLimited"), (error) => Effect.logWarning(`retry after ${error.retryAfterMillis}ms`)),
|
|
135
|
+
);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Retrying is handled once, in the client: `RetryPolicy.default` retries a `transport` or `rateLimited` failure with full-jitter backoff, honoring GitHub's `retry-after` header up to a configurable ceiling. `RetryPolicy.none` disables it. A `notFound`, `rejected` or `unauthorized` failure never retries — it cannot change its mind between attempts.
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
Every resource ships `makeTest(overrides?)` and `layerTest(overrides?)`: stub the members a test exercises, and every other member dies naming itself, so a test proves it touches nothing it did not stub:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { GitBranch } from "@effected/github";
|
|
146
|
+
import { Effect } from "effect";
|
|
147
|
+
|
|
148
|
+
const TestBranches = GitBranch.layerTest({
|
|
149
|
+
upsert: () => Effect.succeed("created"),
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`GitHubClient.layerFixture(fixtures)` is the one recorded-response double that pages for real: it builds a `PageSource` over the recorded array and hands it to the same pagination engine the live client uses, so a truncation path behaves identically under test and in production.
|
|
154
|
+
|
|
155
|
+
## Features
|
|
156
|
+
|
|
157
|
+
- `GitHubClient` — the typed transport: `request`, `requestDecoded` (a mandatory-schema escape hatch for routes outside the generated map), `paginate` / `paginateStream`, `graphql`, and `rateLimit` (observation only — nothing here throttles on your behalf).
|
|
158
|
+
- `Repo` / `RepoRef` — the `{ owner, repo }` coordinate, resolved per call through `R`, with `Repo.provide` for multi-repository programs.
|
|
159
|
+
- `GitHubError` / `GitHubGraphQLError` — one error per transport, `kind`-routed with `hasKind` for `Effect.catchIf`.
|
|
160
|
+
- `RetryPolicy` — the client's one retry policy: full-jitter backoff, server-advised delays honored up to a ceiling.
|
|
161
|
+
- `GitHubApp` — App JWT signing, installation token minting/revocation, app and installation identity, and `clientLayer` for an App-authenticated `GitHubClient`.
|
|
162
|
+
- `GitBranch` / `GitTag` — Git Database API refs, with `upsert` collapsing the create-or-reset dance to one call and `GitTag.latestSemver` picking the newest version-shaped tag in one pass.
|
|
163
|
+
- `CheckRun` — `withCheckRun` concludes on every exit path; `CheckRunOutput.truncated()` cuts rendered output to GitHub's byte limits.
|
|
164
|
+
- `PullRequest` / `PullRequestComment` — upserts for both, `listFiles` answering with the same full `CommitFile` records a commit read returns, `headSha`/`baseSha` on `PullRequestInfo`, plus `CommentMarker` for finding a sticky comment again.
|
|
165
|
+
- `GitHubRelease` — releases and asset uploads, including the one route (`uploadAsset`, with the endpoint's optional display label) outside GitHub's generated endpoint map.
|
|
166
|
+
- `BotIdentity` — the author and committer a bot commits as, with `signoff` rendering the DCO trailer that a commit made through the Git Data API never gets from `git commit -s`.
|
|
167
|
+
- `Attestation` — upload and list attestations against a subject digest; building and signing the bundle is `@effected/sbom`'s job.
|
|
168
|
+
- `TokenPermissions` — a pure comparator between granted and required permissions, reaching nothing but `effect`.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
[MIT](LICENSE)
|
package/index.d.ts
CHANGED
|
@@ -684,60 +684,6 @@ declare class GitHubClient extends GitHubClient_base {
|
|
|
684
684
|
static readonly layerFixture: (fixtures: GitHubFixtures) => Layer.Layer<GitHubClient>;
|
|
685
685
|
}
|
|
686
686
|
//#endregion
|
|
687
|
-
//#region src/ArtifactMetadata.d.ts
|
|
688
|
-
declare const StorageRecordInput_base: Schema.Class<StorageRecordInput, Schema.Struct<{
|
|
689
|
-
/** The artifact's package URL (purl). */
|
|
690
|
-
readonly name: Schema.NonEmptyString;
|
|
691
|
-
/** Its content digest, as `algorithm:hex`. */
|
|
692
|
-
readonly digest: Schema.NonEmptyString;
|
|
693
|
-
/** The registry's base URL. */
|
|
694
|
-
readonly registryUrl: Schema.NonEmptyString;
|
|
695
|
-
/** The repository name **within the registry**. */
|
|
696
|
-
readonly repository: Schema.NonEmptyString;
|
|
697
|
-
/** Where the artifact is stored, when there is a direct URL. */
|
|
698
|
-
readonly artifactUrl: Schema.optionalKey<Schema.String>;
|
|
699
|
-
/** The artifact's path within the registry, when there is one. */
|
|
700
|
-
readonly path: Schema.optionalKey<Schema.String>;
|
|
701
|
-
}>, {}>;
|
|
702
|
-
/**
|
|
703
|
-
* What to record about a published artifact.
|
|
704
|
-
*
|
|
705
|
-
* @remarks
|
|
706
|
-
* These are the fields the endpoint actually accepts. The version this replaces
|
|
707
|
-
* declared a `version` field the endpoint has no notion of — a fabricated key
|
|
708
|
-
* that a `Record<string, unknown>` body accepted silently and the generated
|
|
709
|
-
* types reject outright.
|
|
710
|
-
*
|
|
711
|
-
* @public
|
|
712
|
-
*/
|
|
713
|
-
declare class StorageRecordInput extends StorageRecordInput_base {}
|
|
714
|
-
/**
|
|
715
|
-
* Organization-level artifact metadata.
|
|
716
|
-
*
|
|
717
|
-
* @remarks
|
|
718
|
-
* Org-scoped rather than repository-scoped, so it takes the organization as an
|
|
719
|
-
* argument and does **not** read {@link Repo}.
|
|
720
|
-
*
|
|
721
|
-
* @public
|
|
722
|
-
*/
|
|
723
|
-
interface ArtifactMetadataShape {
|
|
724
|
-
/** Record where a published artifact lives; returns the ids GitHub stored. */
|
|
725
|
-
readonly createStorageRecord: (org: string, input: StorageRecordInput) => Effect.Effect<ReadonlyArray<number>, GitHubError>;
|
|
726
|
-
}
|
|
727
|
-
declare const ArtifactMetadata_base: Context.ServiceClass<ArtifactMetadata, "@effected/github/ArtifactMetadata", ArtifactMetadataShape>;
|
|
728
|
-
/**
|
|
729
|
-
* Artifact metadata.
|
|
730
|
-
*
|
|
731
|
-
* @public
|
|
732
|
-
*/
|
|
733
|
-
declare class ArtifactMetadata extends ArtifactMetadata_base {
|
|
734
|
-
static readonly layer: Layer.Layer<ArtifactMetadata, never, GitHubClient>;
|
|
735
|
-
/** An in-memory double; unstubbed members die naming themselves. */
|
|
736
|
-
static readonly makeTest: (overrides?: Partial<ArtifactMetadataShape>) => ArtifactMetadataShape;
|
|
737
|
-
/** {@link ArtifactMetadata.makeTest} behind a `Layer`. */
|
|
738
|
-
static readonly layerTest: (overrides?: Partial<ArtifactMetadataShape>) => Layer.Layer<ArtifactMetadata>;
|
|
739
|
-
}
|
|
740
|
-
//#endregion
|
|
741
687
|
//#region src/Repo.d.ts
|
|
742
688
|
declare const InvalidRepoRefError_base: Schema.Class<InvalidRepoRefError, Schema.TaggedStruct<"InvalidRepoRefError", {
|
|
743
689
|
/** What was handed in. */
|
|
@@ -835,6 +781,63 @@ declare class Repo extends Repo_base {
|
|
|
835
781
|
static readonly provide: (ref: RepoRef) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Repo>>;
|
|
836
782
|
}
|
|
837
783
|
//#endregion
|
|
784
|
+
//#region src/ArtifactMetadata.d.ts
|
|
785
|
+
declare const StorageRecordInput_base: Schema.Class<StorageRecordInput, Schema.Struct<{
|
|
786
|
+
/** The artifact's package URL (purl). */
|
|
787
|
+
readonly name: Schema.NonEmptyString;
|
|
788
|
+
/** Its content digest, as `algorithm:hex`. */
|
|
789
|
+
readonly digest: Schema.NonEmptyString;
|
|
790
|
+
/** The registry's base URL. */
|
|
791
|
+
readonly registryUrl: Schema.NonEmptyString;
|
|
792
|
+
/** The repository name **within the registry**. */
|
|
793
|
+
readonly repository: Schema.NonEmptyString;
|
|
794
|
+
/** Where the artifact is stored, when there is a direct URL. */
|
|
795
|
+
readonly artifactUrl: Schema.optionalKey<Schema.String>;
|
|
796
|
+
/** The artifact's path within the registry, when there is one. */
|
|
797
|
+
readonly path: Schema.optionalKey<Schema.String>;
|
|
798
|
+
}>, {}>;
|
|
799
|
+
/**
|
|
800
|
+
* What to record about a published artifact.
|
|
801
|
+
*
|
|
802
|
+
* @remarks
|
|
803
|
+
* These are the fields the endpoint actually accepts. The version this replaces
|
|
804
|
+
* declared a `version` field the endpoint has no notion of — a fabricated key
|
|
805
|
+
* that a `Record<string, unknown>` body accepted silently and the generated
|
|
806
|
+
* types reject outright.
|
|
807
|
+
*
|
|
808
|
+
* @public
|
|
809
|
+
*/
|
|
810
|
+
declare class StorageRecordInput extends StorageRecordInput_base {}
|
|
811
|
+
/**
|
|
812
|
+
* Organization-level artifact metadata.
|
|
813
|
+
*
|
|
814
|
+
* @remarks
|
|
815
|
+
* The endpoint is org-scoped rather than repository-scoped, but the
|
|
816
|
+
* organization is resolved from {@link Repo}'s `owner` per call like every
|
|
817
|
+
* other resource — an earlier version took it as a positional argument, the
|
|
818
|
+
* one method on the surface that did. `Repo.provide` covers the cross-org
|
|
819
|
+
* case, exactly as it covers the cross-repository one.
|
|
820
|
+
*
|
|
821
|
+
* @public
|
|
822
|
+
*/
|
|
823
|
+
interface ArtifactMetadataShape {
|
|
824
|
+
/** Record where a published artifact lives; returns the ids GitHub stored. */
|
|
825
|
+
readonly createStorageRecord: (input: StorageRecordInput) => Effect.Effect<ReadonlyArray<number>, GitHubError, Repo>;
|
|
826
|
+
}
|
|
827
|
+
declare const ArtifactMetadata_base: Context.ServiceClass<ArtifactMetadata, "@effected/github/ArtifactMetadata", ArtifactMetadataShape>;
|
|
828
|
+
/**
|
|
829
|
+
* Artifact metadata.
|
|
830
|
+
*
|
|
831
|
+
* @public
|
|
832
|
+
*/
|
|
833
|
+
declare class ArtifactMetadata extends ArtifactMetadata_base {
|
|
834
|
+
static readonly layer: Layer.Layer<ArtifactMetadata, never, GitHubClient>;
|
|
835
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
836
|
+
static readonly makeTest: (overrides?: Partial<ArtifactMetadataShape>) => ArtifactMetadataShape;
|
|
837
|
+
/** {@link ArtifactMetadata.makeTest} behind a `Layer`. */
|
|
838
|
+
static readonly layerTest: (overrides?: Partial<ArtifactMetadataShape>) => Layer.Layer<ArtifactMetadata>;
|
|
839
|
+
}
|
|
840
|
+
//#endregion
|
|
838
841
|
//#region src/Attestation.d.ts
|
|
839
842
|
declare const AttestationRecord_base: Schema.Class<AttestationRecord, Schema.Struct<{
|
|
840
843
|
/** GitHub's id for it, when the response carried one. */
|
|
@@ -1094,6 +1097,18 @@ interface GitBranchShape {
|
|
|
1094
1097
|
* This is one round trip in the common case and two in the raced one, and the
|
|
1095
1098
|
* recovery **resets** rather than inheriting a branch a concurrent creator
|
|
1096
1099
|
* rooted somewhere else — which is the semantics that comment was defending.
|
|
1100
|
+
*
|
|
1101
|
+
* **A reset is observable, and open pull requests react to it.** Resetting a
|
|
1102
|
+
* branch to its PR's base — `upsert(branch, targetHead)` — makes that PR's
|
|
1103
|
+
* head equal its base, and GitHub **auto-closes a PR whose diff is empty**.
|
|
1104
|
+
* The window is invisible at this call site: a consumer ran
|
|
1105
|
+
* `upsert(releaseBranch, mainHead)` intending to re-add content with a commit
|
|
1106
|
+
* ~3 seconds later, and GitHub closed the open release PR inside that window
|
|
1107
|
+
* while the run reported success. When the end state is "the target head plus
|
|
1108
|
+
* a commit", build the commit first — `GitCommit.get` the target for its
|
|
1109
|
+
* `treeSha`, `createTree` on it, `createCommit` with the target as parent —
|
|
1110
|
+
* and upsert **once**, straight to the finished sha, so the ref never rests
|
|
1111
|
+
* on the bare target head.
|
|
1097
1112
|
*/
|
|
1098
1113
|
readonly upsert: (name: string, sha: string) => Effect.Effect<BranchOutcome, GitHubError, Repo>;
|
|
1099
1114
|
/** Is the branch there? A 404 is `false`, not an error. */
|
|
@@ -1233,6 +1248,19 @@ interface GitCommitShape {
|
|
|
1233
1248
|
* tree, create the commit, move the ref — as one operation. The ref update is
|
|
1234
1249
|
* **not** forced: a branch that moved underneath you is a conflict worth
|
|
1235
1250
|
* hearing about, not one to overwrite.
|
|
1251
|
+
*
|
|
1252
|
+
* **This is "commit onto a branch you own", not a rebase.** Putting a commit
|
|
1253
|
+
* on top of *another* branch's head — the release-branch pattern — is a
|
|
1254
|
+
* different operation, and it composes from the members above with no
|
|
1255
|
+
* observable intermediate state: {@link GitCommitShape.get} the target head
|
|
1256
|
+
* for its `treeSha`, {@link GitCommitShape.createTree} on it,
|
|
1257
|
+
* {@link GitCommitShape.createCommit} with the target as parent, then one
|
|
1258
|
+
* `GitBranch.upsert` straight to the finished commit. Do **not** spell a
|
|
1259
|
+
* rebase as `upsert(branch, targetHead)` followed by `commitFiles`: between
|
|
1260
|
+
* those calls the branch *is* the target head, an open pull request from it
|
|
1261
|
+
* has an empty diff, and GitHub auto-closes PRs in that state — a real
|
|
1262
|
+
* consumer lost its open release PR to that ~3-second window while the run
|
|
1263
|
+
* went green.
|
|
1236
1264
|
*/
|
|
1237
1265
|
readonly commitFiles: (options: {
|
|
1238
1266
|
readonly branch: string;
|
|
@@ -1390,6 +1418,20 @@ declare class BotIdentity extends BotIdentity_base {
|
|
|
1390
1418
|
}): BotIdentity;
|
|
1391
1419
|
/** The well-known identity of the `github-actions` bot. */
|
|
1392
1420
|
static readonly githubActions: BotIdentity;
|
|
1421
|
+
/**
|
|
1422
|
+
* The DCO sign-off trailer for this identity.
|
|
1423
|
+
*
|
|
1424
|
+
* @remarks
|
|
1425
|
+
* `Signed-off-by: name <email>` — DCO 1.1's fixed casing and spacing, with
|
|
1426
|
+
* only the email in angle brackets, rendered by the type that owns the
|
|
1427
|
+
* data. Commits created
|
|
1428
|
+
* through the Git Data API bypass `git commit -s`, so no porcelain adds
|
|
1429
|
+
* the trailer, and a hand-built one that is subtly wrong fails late as a
|
|
1430
|
+
* red DCO check on someone else's pull request. Whether a missing
|
|
1431
|
+
* identity falls back to {@link BotIdentity.githubActions} stays the
|
|
1432
|
+
* caller's policy.
|
|
1433
|
+
*/
|
|
1434
|
+
get signoff(): string;
|
|
1393
1435
|
}
|
|
1394
1436
|
declare const AppIdentity_base: Schema.Class<AppIdentity, Schema.Struct<{
|
|
1395
1437
|
/** The URL slug, e.g. `"my-app"`. */
|
|
@@ -1559,6 +1601,15 @@ declare const CommitSummary_base: Schema.Class<CommitSummary, Schema.Struct<{
|
|
|
1559
1601
|
readonly authorLogin: Schema.optionalKey<Schema.String>;
|
|
1560
1602
|
/** The web URL for the commit. */
|
|
1561
1603
|
readonly url: Schema.String;
|
|
1604
|
+
/**
|
|
1605
|
+
* The parent commit shas, in the order GitHub lists them.
|
|
1606
|
+
*
|
|
1607
|
+
* @remarks
|
|
1608
|
+
* Empty for a root commit, two or more for a merge commit. A required field,
|
|
1609
|
+
* not an optional one: every commit endpoint this package reads reports
|
|
1610
|
+
* `parents`, so "which commit(s) did this come from" never needs a raw route.
|
|
1611
|
+
*/
|
|
1612
|
+
readonly parents: Schema.$Array<Schema.String>;
|
|
1562
1613
|
}>, {}>;
|
|
1563
1614
|
/**
|
|
1564
1615
|
* A commit, projected to what callers read.
|
|
@@ -1835,11 +1886,19 @@ interface GitHubReleaseShape {
|
|
|
1835
1886
|
* endpoint map: asset upload goes to `uploads.github.com` with a raw binary
|
|
1836
1887
|
* body, and the map omits it. So it goes through `requestDecoded` with an
|
|
1837
1888
|
* owned schema — the escape hatch is from the route table, never from typing.
|
|
1889
|
+
*
|
|
1890
|
+
* Being outside the map cuts the other way too: octokit has no schema
|
|
1891
|
+
* saying `name` is a **query** parameter, so the route template must carry
|
|
1892
|
+
* it (`assets{?name}`) or octokit silently drops it and GitHub answers 400
|
|
1893
|
+
* `Invalid name for request` — a hand-written route owns its query
|
|
1894
|
+
* parameters in the template, always. `label` is the endpoint's optional
|
|
1895
|
+
* display label, shown in place of the file name on the release page.
|
|
1838
1896
|
*/
|
|
1839
1897
|
readonly uploadAsset: (release: ReleaseInfo, asset: {
|
|
1840
1898
|
readonly name: string;
|
|
1841
1899
|
readonly data: Uint8Array | string;
|
|
1842
1900
|
readonly contentType: string;
|
|
1901
|
+
readonly label?: string | undefined;
|
|
1843
1902
|
}) => Effect.Effect<ReleaseAsset, GitHubError, Repo>;
|
|
1844
1903
|
readonly listAssets: (id: number, options?: {
|
|
1845
1904
|
readonly page?: PageOptions | undefined;
|
|
@@ -2039,8 +2098,12 @@ declare const PullRequestInfo_base: Schema.Class<PullRequestInfo, Schema.Struct<
|
|
|
2039
2098
|
readonly state: Schema.Literals<readonly ["open", "closed"]>;
|
|
2040
2099
|
/** The source branch name. */
|
|
2041
2100
|
readonly head: Schema.String;
|
|
2101
|
+
/** The sha the source branch pointed at when GitHub answered. */
|
|
2102
|
+
readonly headSha: Schema.String;
|
|
2042
2103
|
/** The target branch name. */
|
|
2043
2104
|
readonly base: Schema.String;
|
|
2105
|
+
/** The sha the target branch pointed at when GitHub answered — the commit the pull request branches from. */
|
|
2106
|
+
readonly baseSha: Schema.String;
|
|
2044
2107
|
readonly draft: Schema.Boolean;
|
|
2045
2108
|
readonly merged: Schema.Boolean;
|
|
2046
2109
|
/**
|
|
@@ -2081,10 +2144,20 @@ interface PullRequestShape {
|
|
|
2081
2144
|
readonly state?: "open" | "closed" | "all" | undefined;
|
|
2082
2145
|
readonly page?: PageOptions | undefined;
|
|
2083
2146
|
}) => Effect.Effect<ReadonlyArray<PullRequestInfo>, GitHubError, Repo>;
|
|
2084
|
-
/**
|
|
2147
|
+
/**
|
|
2148
|
+
* The files a pull request changes.
|
|
2149
|
+
*
|
|
2150
|
+
* @remarks
|
|
2151
|
+
* Each entry is a full {@link CommitFile} — path **and** status, plus the
|
|
2152
|
+
* line counts and any pre-rename path — the same projection
|
|
2153
|
+
* `GitHubCommit.changedFiles` returns, because GitHub answers both
|
|
2154
|
+
* endpoints with the same `diff-entry` shape. An earlier version projected
|
|
2155
|
+
* to the path alone, and consumers who needed the status fell back to a raw
|
|
2156
|
+
* route.
|
|
2157
|
+
*/
|
|
2085
2158
|
readonly listFiles: (number: number, options?: {
|
|
2086
2159
|
readonly page?: PageOptions | undefined;
|
|
2087
|
-
}) => Effect.Effect<ReadonlyArray<
|
|
2160
|
+
}) => Effect.Effect<ReadonlyArray<CommitFile>, GitHubError, Repo>;
|
|
2088
2161
|
/**
|
|
2089
2162
|
* The pull requests associated with a commit.
|
|
2090
2163
|
*
|
package/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import { GitHubError, GitHubErrorKind } from "./GitHubError.js";
|
|
|
2
2
|
import { GitHubGraphQLError, GraphQLDocument, GraphQLErrorEntry } from "./GraphQL.js";
|
|
3
3
|
import { RateLimitSnapshot, RetryPolicy } from "./Resilience.js";
|
|
4
4
|
import { GitHubClient } from "./GitHubClient.js";
|
|
5
|
-
import { ArtifactMetadata, StorageRecordInput } from "./ArtifactMetadata.js";
|
|
6
5
|
import { InvalidRepoRefError, Repo, RepoRef } from "./Repo.js";
|
|
6
|
+
import { ArtifactMetadata, StorageRecordInput } from "./ArtifactMetadata.js";
|
|
7
7
|
import { Attestation, AttestationListEntry, AttestationRecord } from "./Attestation.js";
|
|
8
8
|
import { Annotation, AnnotationLevel, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef } from "./CheckRun.js";
|
|
9
9
|
import { GitBranch } from "./GitBranch.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/github",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Typed GitHub REST and GraphQL services over the octokit core request surface, with app auth and resource helpers.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"./package.json": "./package.json"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@effected/semver": "~0.
|
|
41
|
+
"@effected/semver": "~0.3.0",
|
|
42
42
|
"@octokit/core": "^7.0.6",
|
|
43
43
|
"@octokit/plugin-paginate-rest": "^14.0.0",
|
|
44
44
|
"@octokit/types": "^16.0.0",
|