@ever-works/github-actions-build-plugin 1.0.2 → 1.0.4

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BuildValue, JsonSchema, IBuildPlugin, PluginCategory, BuildStrategy, PluginContext, BuildAuth, PluginManifest, PrepareRepositoryInput, RepositoryWriter, PrepareRepositoryResult, StartBuildInput, BuildRef, BuildSnapshot, AppBuildBlock, RepositoryWriteErrorCode } from '@ever-works/plugin';
1
+ import { BuildValue, JsonSchema, IBuildPlugin, PluginCategory, BuildStrategy, PluginContext, BuildAuth, PluginManifest, PrepareRepositoryInput, RepositoryWriter, PrepareRepositoryResult, StartBuildInput, BuildRef, BuildSnapshot, BuildRunRef, AppBuildBlock, RepositoryWriteErrorCode } from '@ever-works/plugin';
2
2
  import { AppBuildBlockedReason, AppBuildKind, AppBuildRunnerClass, APP_BUILD_GENERATOR_VERSION } from '@ever-works/contracts';
3
3
  import sodiumType from 'libsodium-wrappers';
4
4
 
@@ -47,6 +47,10 @@ interface ActionsRun {
47
47
  readonly pull_requests?: ReadonlyArray<{
48
48
  readonly number?: number | null;
49
49
  }> | null;
50
+ /** The repository the run's head commit lives in — a fork's for a fork pull request (§7.5, ACC-05-06). */
51
+ readonly head_repository?: {
52
+ readonly full_name?: string | null;
53
+ } | null;
50
54
  }
51
55
  /** One job of a run (`GET /repos/{o}/{r}/actions/runs/{id}/jobs`). */
52
56
  interface ActionsJob {
@@ -105,6 +109,24 @@ interface ActionsRunsPort {
105
109
  readonly headSha?: string;
106
110
  readonly event?: string;
107
111
  }): Promise<readonly ActionsRun[]>;
112
+ /**
113
+ * Run discovery's conditional read (§7.4a): the same list as
114
+ * {@link ActionsRunsPort.listWorkflowRuns}, sent with `If-None-Match` when an
115
+ * `etag` is given. A 304 answers `{ notModified: true, runs: [] }`.
116
+ *
117
+ * Optional — an adapter without it is read unconditionally through
118
+ * `listWorkflowRuns`, which costs a request but is never wrong.
119
+ */
120
+ listWorkflowRunsConditional?(input: {
121
+ readonly repository: ActionsRepositoryRef;
122
+ readonly workflowFile: string;
123
+ readonly perPage: number;
124
+ readonly etag?: string;
125
+ }): Promise<{
126
+ readonly notModified: boolean;
127
+ readonly etag?: string;
128
+ readonly runs: readonly ActionsRun[];
129
+ }>;
108
130
  /** `GET /repos/{o}/{r}/actions/runs/{id}`. `null` when GitHub answers 404. */
109
131
  getWorkflowRun(input: {
110
132
  readonly repository: ActionsRepositoryRef;
@@ -331,6 +353,53 @@ declare function isSecretLimitError(error: unknown): boolean;
331
353
  */
332
354
  declare function createBuildValueSecretSync(dependencies: SecretSyncDependencies): BuildValueSecretSync;
333
355
 
356
+ /**
357
+ * APW-05 T16 remainder (outstanding-work A0.4, ledger D19) — the clone-free
358
+ * {@link RepositoryWriter} over GitHub's REST API.
359
+ *
360
+ * ## Why the build plugin carries its own writer
361
+ *
362
+ * `prepareRepository` needs four repository operations — read a file, land one
363
+ * commit, create a branch, open a pull request — and plan §4.1 says the facade
364
+ * binds them "to `GitFacadeService`". That facade has three of the four but no
365
+ * `commitFiles` (APW-03 T22 declares it and has not landed), so every production
366
+ * prepare answered `pluginUnavailable` before the request was even built. This
367
+ * plugin already holds Octokit and the member's token, and every one of the four
368
+ * is a plain REST call, so it supplies the writer itself
369
+ * (`IBuildPlugin.repositoryWriter?`) and the facade prefers it. When APW-03 T22
370
+ * lands a git-provider `commitFiles`, the facade may bind that instead; nothing
371
+ * in `workflow-writer.ts` changes either way.
372
+ *
373
+ * ## The refusal codes the contract names
374
+ *
375
+ * A refusal is THROWN with `code` set, which is what `repositoryWriteErrorCode`
376
+ * reads:
377
+ *
378
+ * - `nonFastForward` — `PATCH …/git/refs/heads/<branch>` answered 422 "not a
379
+ * fast forward": the branch moved under us, and the caller retries from a
380
+ * fresh head (never a force-push);
381
+ * - `refRejectedByRule` — 409/422 mentioning a protection rule or a
382
+ * required status/review: the caller switches to the pull-request path;
383
+ * - `pullRequestExists` — 422 "A pull request already exists";
384
+ * - `workflowPermissionMissing` (2026-10-17) — the credential cannot write
385
+ * under `.github/workflows/`: an OAuth token or classic PAT without the
386
+ * `workflow` scope, or a GitHub App installation without `Workflows: write`
387
+ * (APW-02 plan §4's permission matrix). Checked BEFORE the commit when the
388
+ * scopes are known or readable (`x-oauth-scopes`), and translated from
389
+ * GitHub's own refusal otherwise, so the member is told to reconnect GitHub
390
+ * with the `workflow` scope instead of seeing a bare 403/404.
391
+ *
392
+ * Every other failure is rethrown untouched.
393
+ */
394
+ /** The subset of Octokit's surface this writer uses. */
395
+ interface OctokitRequester {
396
+ request(route: string, params?: Record<string, unknown>): Promise<{
397
+ data: unknown;
398
+ status?: number;
399
+ headers?: Record<string, unknown>;
400
+ }>;
401
+ }
402
+
334
403
  /**
335
404
  * APW-05 T7 — the `github-actions-build` plugin settings (plan §4.4, setting by
336
405
  * setting), and the one place the two platform-managed keys are declared.
@@ -519,10 +588,11 @@ declare const gitHubActionsBuildSettingsSchema: GitHubActionsBuildSettingsSchema
519
588
  * always, and `AppBuildsService.finalize` does the comparison, because it is the
520
589
  * only party holding both the Build row and the `pullToken` setting.
521
590
  *
522
- * `listRecentRuns?` (T12's run discovery, §7.4a) is still deliberately **not
523
- * declared**: it is optional on the contract and a caller materialises the member
524
- * before calling it, so declaring a throwing placeholder would buy nothing and
525
- * would tell a future reader the capability is wired when it is not.
591
+ * `listRecentRuns?` (T21a's run discovery, §7.4a) is declared now that it does
592
+ * something (2026-10-17): it reads the build workflow's newest runs with
593
+ * `If-None-Match` and maps them through contracts' `githubRunToBuildRunRef`, the
594
+ * same mapping the `workflow_run` consumer uses. `repositoryWriter?` (the
595
+ * clone-free writer `prepareRepository` delivers through) landed with it.
526
596
  */
527
597
  declare class GitHubActionsBuildPlugin implements IBuildPlugin {
528
598
  readonly id = "github-actions-build";
@@ -741,6 +811,66 @@ declare class GitHubActionsBuildPlugin implements IBuildPlugin {
741
811
  * nor `ghcr.io`, and that module's spec watches every call for exactly that.
742
812
  */
743
813
  protected registryFetch(): GhcrFetch;
814
+ /**
815
+ * APW-05 T21a / T12 remainder — run discovery (plan §7.4a, `APW05-G01`).
816
+ *
817
+ * The newest runs of the build workflow, so a push or pull-request Build is
818
+ * recorded even when no `workflow_run` delivery reaches the platform (a
819
+ * personal fork connected through OAuth has no App installation to deliver
820
+ * from). `If-None-Match` with the stored `etag` makes an idle repository cost
821
+ * a 304 — which GitHub does not count against the rate limit.
822
+ *
823
+ * The mapping is `githubRunToBuildRunRef` from contracts, the SAME function
824
+ * the webhook consumer uses, so a polled run and a delivered run are the same
825
+ * `BuildRunRef` and converge on one Build row.
826
+ */
827
+ listRecentRuns(input: {
828
+ repository: {
829
+ owner: string;
830
+ name: string;
831
+ };
832
+ workflowPath: string;
833
+ perPage: number;
834
+ etag?: string;
835
+ }, auth: BuildAuth): Promise<{
836
+ notModified: boolean;
837
+ etag?: string;
838
+ runs: BuildRunRef[];
839
+ }>;
840
+ /**
841
+ * APW-05 T16 remainder — the clone-free writer `prepareRepository` delivers the
842
+ * workflow through, over this plugin's own Octokit (see
843
+ * `repo/octokit-repository-writer.ts` on why the plugin supplies it).
844
+ */
845
+ repositoryWriter(repository: {
846
+ readonly owner: string;
847
+ readonly repo: string;
848
+ readonly trackedBranch?: string;
849
+ }, auth: BuildAuth): RepositoryWriter;
850
+ /**
851
+ * Plan §5 — a Rebuild's `commitSha` must be reachable from the tracked branch
852
+ * (2026-10-17). `GET …/compare/{sha}...{branch}`: `ahead` or `identical` means
853
+ * the commit is in the branch's history; `behind` / `diverged` means it is not
854
+ * (a commit only a fork's network carries is `diverged`), and 404/422 means
855
+ * GitHub does not know it here. Any other failure is a throw.
856
+ */
857
+ isCommitReachable(input: {
858
+ readonly repository: {
859
+ readonly owner: string;
860
+ readonly repo: string;
861
+ };
862
+ readonly sha: string;
863
+ readonly branch: string;
864
+ }, auth: BuildAuth): Promise<boolean>;
865
+ /** §4.10 — the per-run prompted-value secret's removal (T12's `secret-sync.ts`, now on the contract). */
866
+ deleteVerifyPromptedSecret(repository: {
867
+ readonly owner: string;
868
+ readonly repo: string;
869
+ }, auth: BuildAuth): Promise<{
870
+ readonly deleted: boolean;
871
+ }>;
872
+ /** The Octokit client for one call — a seam a spec overrides, like {@link actionsPort}. */
873
+ protected octokitFor(auth: BuildAuth): OctokitRequester;
744
874
  /** APW-05 T12 — the run's own page, which is where a member reads its logs. */
745
875
  getLogsUrl(ref: BuildRef, auth: BuildAuth): Promise<string | null>;
746
876
  }
@@ -1447,6 +1577,13 @@ interface WorkflowWriteResult {
1447
1577
  readonly detail: {
1448
1578
  readonly cause: string;
1449
1579
  };
1580
+ } | {
1581
+ readonly reason: 'workflowPermissionMissing';
1582
+ readonly detail: {
1583
+ readonly cause: string;
1584
+ readonly credential: string;
1585
+ readonly missing: string;
1586
+ };
1450
1587
  };
1451
1588
  }
1452
1589
  /** The refusal code of a thrown write error, if it is one of the codes the contract names. */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BuildValue, JsonSchema, IBuildPlugin, PluginCategory, BuildStrategy, PluginContext, BuildAuth, PluginManifest, PrepareRepositoryInput, RepositoryWriter, PrepareRepositoryResult, StartBuildInput, BuildRef, BuildSnapshot, AppBuildBlock, RepositoryWriteErrorCode } from '@ever-works/plugin';
1
+ import { BuildValue, JsonSchema, IBuildPlugin, PluginCategory, BuildStrategy, PluginContext, BuildAuth, PluginManifest, PrepareRepositoryInput, RepositoryWriter, PrepareRepositoryResult, StartBuildInput, BuildRef, BuildSnapshot, BuildRunRef, AppBuildBlock, RepositoryWriteErrorCode } from '@ever-works/plugin';
2
2
  import { AppBuildBlockedReason, AppBuildKind, AppBuildRunnerClass, APP_BUILD_GENERATOR_VERSION } from '@ever-works/contracts';
3
3
  import sodiumType from 'libsodium-wrappers';
4
4
 
@@ -47,6 +47,10 @@ interface ActionsRun {
47
47
  readonly pull_requests?: ReadonlyArray<{
48
48
  readonly number?: number | null;
49
49
  }> | null;
50
+ /** The repository the run's head commit lives in — a fork's for a fork pull request (§7.5, ACC-05-06). */
51
+ readonly head_repository?: {
52
+ readonly full_name?: string | null;
53
+ } | null;
50
54
  }
51
55
  /** One job of a run (`GET /repos/{o}/{r}/actions/runs/{id}/jobs`). */
52
56
  interface ActionsJob {
@@ -105,6 +109,24 @@ interface ActionsRunsPort {
105
109
  readonly headSha?: string;
106
110
  readonly event?: string;
107
111
  }): Promise<readonly ActionsRun[]>;
112
+ /**
113
+ * Run discovery's conditional read (§7.4a): the same list as
114
+ * {@link ActionsRunsPort.listWorkflowRuns}, sent with `If-None-Match` when an
115
+ * `etag` is given. A 304 answers `{ notModified: true, runs: [] }`.
116
+ *
117
+ * Optional — an adapter without it is read unconditionally through
118
+ * `listWorkflowRuns`, which costs a request but is never wrong.
119
+ */
120
+ listWorkflowRunsConditional?(input: {
121
+ readonly repository: ActionsRepositoryRef;
122
+ readonly workflowFile: string;
123
+ readonly perPage: number;
124
+ readonly etag?: string;
125
+ }): Promise<{
126
+ readonly notModified: boolean;
127
+ readonly etag?: string;
128
+ readonly runs: readonly ActionsRun[];
129
+ }>;
108
130
  /** `GET /repos/{o}/{r}/actions/runs/{id}`. `null` when GitHub answers 404. */
109
131
  getWorkflowRun(input: {
110
132
  readonly repository: ActionsRepositoryRef;
@@ -331,6 +353,53 @@ declare function isSecretLimitError(error: unknown): boolean;
331
353
  */
332
354
  declare function createBuildValueSecretSync(dependencies: SecretSyncDependencies): BuildValueSecretSync;
333
355
 
356
+ /**
357
+ * APW-05 T16 remainder (outstanding-work A0.4, ledger D19) — the clone-free
358
+ * {@link RepositoryWriter} over GitHub's REST API.
359
+ *
360
+ * ## Why the build plugin carries its own writer
361
+ *
362
+ * `prepareRepository` needs four repository operations — read a file, land one
363
+ * commit, create a branch, open a pull request — and plan §4.1 says the facade
364
+ * binds them "to `GitFacadeService`". That facade has three of the four but no
365
+ * `commitFiles` (APW-03 T22 declares it and has not landed), so every production
366
+ * prepare answered `pluginUnavailable` before the request was even built. This
367
+ * plugin already holds Octokit and the member's token, and every one of the four
368
+ * is a plain REST call, so it supplies the writer itself
369
+ * (`IBuildPlugin.repositoryWriter?`) and the facade prefers it. When APW-03 T22
370
+ * lands a git-provider `commitFiles`, the facade may bind that instead; nothing
371
+ * in `workflow-writer.ts` changes either way.
372
+ *
373
+ * ## The refusal codes the contract names
374
+ *
375
+ * A refusal is THROWN with `code` set, which is what `repositoryWriteErrorCode`
376
+ * reads:
377
+ *
378
+ * - `nonFastForward` — `PATCH …/git/refs/heads/<branch>` answered 422 "not a
379
+ * fast forward": the branch moved under us, and the caller retries from a
380
+ * fresh head (never a force-push);
381
+ * - `refRejectedByRule` — 409/422 mentioning a protection rule or a
382
+ * required status/review: the caller switches to the pull-request path;
383
+ * - `pullRequestExists` — 422 "A pull request already exists";
384
+ * - `workflowPermissionMissing` (2026-10-17) — the credential cannot write
385
+ * under `.github/workflows/`: an OAuth token or classic PAT without the
386
+ * `workflow` scope, or a GitHub App installation without `Workflows: write`
387
+ * (APW-02 plan §4's permission matrix). Checked BEFORE the commit when the
388
+ * scopes are known or readable (`x-oauth-scopes`), and translated from
389
+ * GitHub's own refusal otherwise, so the member is told to reconnect GitHub
390
+ * with the `workflow` scope instead of seeing a bare 403/404.
391
+ *
392
+ * Every other failure is rethrown untouched.
393
+ */
394
+ /** The subset of Octokit's surface this writer uses. */
395
+ interface OctokitRequester {
396
+ request(route: string, params?: Record<string, unknown>): Promise<{
397
+ data: unknown;
398
+ status?: number;
399
+ headers?: Record<string, unknown>;
400
+ }>;
401
+ }
402
+
334
403
  /**
335
404
  * APW-05 T7 — the `github-actions-build` plugin settings (plan §4.4, setting by
336
405
  * setting), and the one place the two platform-managed keys are declared.
@@ -519,10 +588,11 @@ declare const gitHubActionsBuildSettingsSchema: GitHubActionsBuildSettingsSchema
519
588
  * always, and `AppBuildsService.finalize` does the comparison, because it is the
520
589
  * only party holding both the Build row and the `pullToken` setting.
521
590
  *
522
- * `listRecentRuns?` (T12's run discovery, §7.4a) is still deliberately **not
523
- * declared**: it is optional on the contract and a caller materialises the member
524
- * before calling it, so declaring a throwing placeholder would buy nothing and
525
- * would tell a future reader the capability is wired when it is not.
591
+ * `listRecentRuns?` (T21a's run discovery, §7.4a) is declared now that it does
592
+ * something (2026-10-17): it reads the build workflow's newest runs with
593
+ * `If-None-Match` and maps them through contracts' `githubRunToBuildRunRef`, the
594
+ * same mapping the `workflow_run` consumer uses. `repositoryWriter?` (the
595
+ * clone-free writer `prepareRepository` delivers through) landed with it.
526
596
  */
527
597
  declare class GitHubActionsBuildPlugin implements IBuildPlugin {
528
598
  readonly id = "github-actions-build";
@@ -741,6 +811,66 @@ declare class GitHubActionsBuildPlugin implements IBuildPlugin {
741
811
  * nor `ghcr.io`, and that module's spec watches every call for exactly that.
742
812
  */
743
813
  protected registryFetch(): GhcrFetch;
814
+ /**
815
+ * APW-05 T21a / T12 remainder — run discovery (plan §7.4a, `APW05-G01`).
816
+ *
817
+ * The newest runs of the build workflow, so a push or pull-request Build is
818
+ * recorded even when no `workflow_run` delivery reaches the platform (a
819
+ * personal fork connected through OAuth has no App installation to deliver
820
+ * from). `If-None-Match` with the stored `etag` makes an idle repository cost
821
+ * a 304 — which GitHub does not count against the rate limit.
822
+ *
823
+ * The mapping is `githubRunToBuildRunRef` from contracts, the SAME function
824
+ * the webhook consumer uses, so a polled run and a delivered run are the same
825
+ * `BuildRunRef` and converge on one Build row.
826
+ */
827
+ listRecentRuns(input: {
828
+ repository: {
829
+ owner: string;
830
+ name: string;
831
+ };
832
+ workflowPath: string;
833
+ perPage: number;
834
+ etag?: string;
835
+ }, auth: BuildAuth): Promise<{
836
+ notModified: boolean;
837
+ etag?: string;
838
+ runs: BuildRunRef[];
839
+ }>;
840
+ /**
841
+ * APW-05 T16 remainder — the clone-free writer `prepareRepository` delivers the
842
+ * workflow through, over this plugin's own Octokit (see
843
+ * `repo/octokit-repository-writer.ts` on why the plugin supplies it).
844
+ */
845
+ repositoryWriter(repository: {
846
+ readonly owner: string;
847
+ readonly repo: string;
848
+ readonly trackedBranch?: string;
849
+ }, auth: BuildAuth): RepositoryWriter;
850
+ /**
851
+ * Plan §5 — a Rebuild's `commitSha` must be reachable from the tracked branch
852
+ * (2026-10-17). `GET …/compare/{sha}...{branch}`: `ahead` or `identical` means
853
+ * the commit is in the branch's history; `behind` / `diverged` means it is not
854
+ * (a commit only a fork's network carries is `diverged`), and 404/422 means
855
+ * GitHub does not know it here. Any other failure is a throw.
856
+ */
857
+ isCommitReachable(input: {
858
+ readonly repository: {
859
+ readonly owner: string;
860
+ readonly repo: string;
861
+ };
862
+ readonly sha: string;
863
+ readonly branch: string;
864
+ }, auth: BuildAuth): Promise<boolean>;
865
+ /** §4.10 — the per-run prompted-value secret's removal (T12's `secret-sync.ts`, now on the contract). */
866
+ deleteVerifyPromptedSecret(repository: {
867
+ readonly owner: string;
868
+ readonly repo: string;
869
+ }, auth: BuildAuth): Promise<{
870
+ readonly deleted: boolean;
871
+ }>;
872
+ /** The Octokit client for one call — a seam a spec overrides, like {@link actionsPort}. */
873
+ protected octokitFor(auth: BuildAuth): OctokitRequester;
744
874
  /** APW-05 T12 — the run's own page, which is where a member reads its logs. */
745
875
  getLogsUrl(ref: BuildRef, auth: BuildAuth): Promise<string | null>;
746
876
  }
@@ -1447,6 +1577,13 @@ interface WorkflowWriteResult {
1447
1577
  readonly detail: {
1448
1578
  readonly cause: string;
1449
1579
  };
1580
+ } | {
1581
+ readonly reason: 'workflowPermissionMissing';
1582
+ readonly detail: {
1583
+ readonly cause: string;
1584
+ readonly credential: string;
1585
+ readonly missing: string;
1586
+ };
1450
1587
  };
1451
1588
  }
1452
1589
  /** The refusal code of a thrown write error, if it is one of the codes the contract names. */