@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.
@@ -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* (org, input) {
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((file) => file.filename);
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
+ [![npm](https://img.shields.io/npm/v/@effected%2Fgithub?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/github)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](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
- /** The files a pull request changes. */
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<string>, GitHubError, Repo>;
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.0",
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.2.1",
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",