@usine/orchestrator 0.0.1 → 0.2.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 +146 -2
- package/dist/board/items.d.ts +18 -0
- package/dist/board/items.d.ts.map +1 -0
- package/dist/board/items.js +46 -0
- package/dist/board/items.js.map +1 -0
- package/dist/board/projection.d.ts +9 -0
- package/dist/board/projection.d.ts.map +1 -0
- package/dist/board/projection.js +111 -0
- package/dist/board/projection.js.map +1 -0
- package/dist/board/tasks.d.ts +8 -0
- package/dist/board/tasks.d.ts.map +1 -0
- package/dist/board/tasks.js +86 -0
- package/dist/board/tasks.js.map +1 -0
- package/dist/credentials.d.ts +12 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +62 -0
- package/dist/credentials.js.map +1 -0
- package/dist/exe-dev.d.ts +8 -0
- package/dist/exe-dev.d.ts.map +1 -0
- package/dist/exe-dev.js +17 -0
- package/dist/exe-dev.js.map +1 -0
- package/dist/idle-triggers.d.ts +6 -0
- package/dist/idle-triggers.d.ts.map +1 -0
- package/dist/idle-triggers.js +31 -0
- package/dist/idle-triggers.js.map +1 -0
- package/dist/label-reconciler.d.ts +30 -0
- package/dist/label-reconciler.d.ts.map +1 -0
- package/dist/label-reconciler.js +119 -0
- package/dist/label-reconciler.js.map +1 -0
- package/dist/local.d.ts +25 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +53 -0
- package/dist/local.js.map +1 -0
- package/dist/operator/attempt.d.ts +66 -0
- package/dist/operator/attempt.d.ts.map +1 -0
- package/dist/operator/attempt.js +58 -0
- package/dist/operator/attempt.js.map +1 -0
- package/dist/operator/board.d.ts +119 -0
- package/dist/operator/board.d.ts.map +1 -0
- package/dist/operator/board.js +101 -0
- package/dist/operator/board.js.map +1 -0
- package/dist/operator/http.d.ts +114 -0
- package/dist/operator/http.d.ts.map +1 -0
- package/dist/operator/http.js +139 -0
- package/dist/operator/http.js.map +1 -0
- package/dist/operator/need.d.ts +66 -0
- package/dist/operator/need.d.ts.map +1 -0
- package/dist/operator/need.js +70 -0
- package/dist/operator/need.js.map +1 -0
- package/dist/operator/operator.d.ts +64 -0
- package/dist/operator/operator.d.ts.map +1 -0
- package/dist/operator/operator.js +363 -0
- package/dist/operator/operator.js.map +1 -0
- package/dist/operator/overview.d.ts +51 -0
- package/dist/operator/overview.d.ts.map +1 -0
- package/dist/operator/overview.js +48 -0
- package/dist/operator/overview.js.map +1 -0
- package/dist/operator/projection.d.ts +17 -0
- package/dist/operator/projection.d.ts.map +1 -0
- package/dist/operator/projection.js +138 -0
- package/dist/operator/projection.js.map +1 -0
- package/dist/operator/run.d.ts +38 -0
- package/dist/operator/run.d.ts.map +1 -0
- package/dist/operator/run.js +41 -0
- package/dist/operator/run.js.map +1 -0
- package/dist/operator/transcript.d.ts +57 -0
- package/dist/operator/transcript.d.ts.map +1 -0
- package/dist/operator/transcript.js +59 -0
- package/dist/operator/transcript.js.map +1 -0
- package/dist/orchestrator.d.ts +14 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +14 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/polling/cycle.d.ts +51 -0
- package/dist/polling/cycle.d.ts.map +1 -0
- package/dist/polling/cycle.js +44 -0
- package/dist/polling/cycle.js.map +1 -0
- package/dist/polling/hints.d.ts +20 -0
- package/dist/polling/hints.d.ts.map +1 -0
- package/dist/polling/hints.js +54 -0
- package/dist/polling/hints.js.map +1 -0
- package/dist/polling/loop-evaluation.d.ts +21 -0
- package/dist/polling/loop-evaluation.d.ts.map +1 -0
- package/dist/polling/loop-evaluation.js +131 -0
- package/dist/polling/loop-evaluation.js.map +1 -0
- package/dist/polling/polling.d.ts +36 -0
- package/dist/polling/polling.d.ts.map +1 -0
- package/dist/polling/polling.js +296 -0
- package/dist/polling/polling.js.map +1 -0
- package/dist/polling/repository-sync.d.ts +45 -0
- package/dist/polling/repository-sync.d.ts.map +1 -0
- package/dist/polling/repository-sync.js +169 -0
- package/dist/polling/repository-sync.js.map +1 -0
- package/dist/polling-triggers.d.ts +4 -0
- package/dist/polling-triggers.d.ts.map +1 -0
- package/dist/polling-triggers.js +4 -0
- package/dist/polling-triggers.js.map +1 -0
- package/dist/schedule-triggers.d.ts +67 -0
- package/dist/schedule-triggers.d.ts.map +1 -0
- package/dist/schedule-triggers.js +401 -0
- package/dist/schedule-triggers.js.map +1 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +107 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/trigger-submission.d.ts +69 -0
- package/dist/trigger-submission.d.ts.map +1 -0
- package/dist/trigger-submission.js +108 -0
- package/dist/trigger-submission.js.map +1 -0
- package/package.json +67 -5
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @usine/orchestrator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@usine/orchestrator` connects loaded loop files to GitHub polling, the durable run engine, and configured workers for [usine](https://github.com/basaltbytes/usine).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Its platform-free entry points provide `PollingTriggers` at `/polling-triggers`, `ScheduleTriggers` at `/schedule-triggers`, `Orchestrator` at `/orchestrator`, and `Operator` with its request and response types at `/operator`. `Orchestrator.run(instance)` runs both trigger streams; `Operator` provides run and Board observation, structured protocol transcript pages, label-backed Board moves, approve/retry/cancel, and database-backed Loop pause/unpause. Transcript payloads, Attempt brains/outcomes, and items needing attention remain typed data. Captured executor defects appear as `DefectFailure` with a safe message; original causes stay out of operator responses. Consuming apps own their wording and presentation. `OperatorObservation` exposes the same read-only projection without constructing execution services. The latest polling and schedule failures remain visible through `Operator.overview(instance)` after their sibling work continues. Concurrent overview reads with the same durable revision share one projection computation.
|
|
6
|
+
|
|
7
|
+
`Operator.overview(instance)` joins the latest 500 polled Items into Task cards in each repository's authored lanes. Explicit same-repository issue closing links and saved Run PR results join an issue to a PR. Repositories without configured lanes receive proposals from the repository label catalog, including unused labels and repositories with no Items. Items that match no source appear in a read-only `Unmapped` lane, and label matches take precedence over state sources. Each card retains every saved Run on its member Items, oldest first. The newest open Run determines the current Item; between Runs an open PR, then the latest settled PR, takes over from the issue. Its labels and state determine the lane, so removing the issue's trigger label does not strand a task whose PR is under review. See ADR 0024. A malformed Item projection is quarantined and reported in the Board and needs-you strip while valid siblings remain visible. Failed declared-label creation also appears as a repository preflight finding without blocking trigger work.
|
|
8
|
+
|
|
9
|
+
`Operator.moveBoardItem(instance, move)` accepts authored label-backed lanes, adds the target lane labels, and removes only labels owned by the authored map. Proposed lanes are read-only; direct move requests return `BoardLaneReadOnlyError`. GitHub labels outside the authored map remain unchanged even when the Item snapshot is stale or its label connection is truncated. Configure `repos[].lanes` in `usine.yaml` and restart the orchestrator to enable moves; see the [lane configuration example](../config/README.md). Every attempted GitHub label write queues an Item Hint, including an uncertain or partially applied failure: the running poller fetches the current GitHub snapshot, projects its Board position, and evaluates its Loops immediately unless GitHub has requested backoff.
|
|
10
|
+
|
|
11
|
+
`Operator.detail` accepts distinct cold, live-tail, and earlier-page requests. A cold read keeps the newest 500 rows per Attempt and returns an earlier cursor for every truncated Attempt; clients can page each cursor backward until the entire transcript is reachable without ballooning the first drawer response. A live-tail response returns at most 500 rows across the Run and advances its cursor from the last returned row.
|
|
12
|
+
|
|
13
|
+
## Operator HTTP
|
|
14
|
+
|
|
15
|
+
Import `FactoryOverview`, `RunView`, `RunDetailView`, Board views, operator needs,
|
|
16
|
+
and transcript request types from `@usine/orchestrator/operator`. The `Operator`
|
|
17
|
+
service and its HTTP contract use the same schemas for these views and requests.
|
|
18
|
+
|
|
19
|
+
`@usine/orchestrator/operator/http` exports `OperatorApi`, the shared HTTP contract
|
|
20
|
+
for operator observation and interventions. Use Effect's `HttpApiClient.make` with
|
|
21
|
+
an `HttpClient` layer to call it, or `HttpApiBuilder.group` to implement its
|
|
22
|
+
`operator` group. Server implementations provide `OperatorHttpPolicy` to enforce
|
|
23
|
+
origin checks and translate request failures; `apps/server` provides the local
|
|
24
|
+
server implementation.
|
|
25
|
+
|
|
26
|
+
The generated client accepts the same `ColdTranscriptRequest`,
|
|
27
|
+
`TailTranscriptRequest`, and `EarlierTranscriptRequest` values as `Operator.detail`.
|
|
28
|
+
It encodes their exclusive query parameters and decodes responses through the
|
|
29
|
+
shared read-model schemas. `OperatorHttpError` preserves the failure's `error`
|
|
30
|
+
discriminator, operator-readable `message`, and associated `status`. The wire
|
|
31
|
+
response remains `{ error, message }`. Successful actions return `{ ok: true }`;
|
|
32
|
+
clients observe subsequent Run or Board progress through the read endpoints.
|
|
33
|
+
|
|
34
|
+
## Polling triggers
|
|
35
|
+
|
|
36
|
+
`PollingTriggers.poll(instance)` performs one cycle; `PollingTriggers.run(instance)` repeats cycles using the instance's factory-wide `poll_interval` and waits longer when GitHub reports rate-limit pressure. Per-repository failures are isolated and reported on the cycle — one failing repository never stops its siblings. `PollingTriggers.hint(repository, item)` queues a bounded, coalesced self-write nudge. The running poller serializes Hint snapshots with regular cycles and uses the same projection and Event dedup path. Hints leave REST cursors, the normal conditional-poll cadence, and the last complete cycle’s findings intact; dropped or failed Hints are repaired by the polling backstop. GitHub request backoff, including deadlines reported by a failed writer, also delays Hint fetches. The original write outcome still reaches its caller.
|
|
37
|
+
|
|
38
|
+
Each cycle first refreshes the complete label catalog in every managed repository and reconciles the factory-wide declarations from `usine.yaml`. Usine creates missing labels before change detection or Item evaluation. It leaves existing labels untouched and logs color or description drift. Creation failures become preflight findings, and the cycle continues into triggers. The updated catalog remains the Board's source for lane colors.
|
|
39
|
+
|
|
40
|
+
After label preflight, the cycle asks `GitHubPolling` for changed items, fetches one payload snapshot per changed item, updates the durable Item projection, and evaluates each stream-matching loop's CEL `if:` expression against that snapshot. A match renders every `{{ CEL expression }}` in the loop body, resolves the named agent and effective parent model, then stores that immutable execution context with the Event. The Event identity covers the loop, canonical repository, item number, and timestamp-free content hash. An unmatch revokes pending or approval-waiting work for that loop and item. Repositories without matching loops are still polled for their Board projection.
|
|
41
|
+
|
|
42
|
+
Polling cursors live in the engine's `CursorStore`. When a loop starts watching a repository or its trigger expression changes, the orchestrator discards that repository's efficiency cursor and performs a cold poll, while the permanent Event ledger absorbs repeated state.
|
|
43
|
+
|
|
44
|
+
PR-triggered Attempts fetch and check out the immutable `headRefOid` saved in their Event payload. A later push creates a new Event with its new head commit. Issue and schedule Attempts use the repository's default branch.
|
|
45
|
+
|
|
46
|
+
`PollingTriggers.layer` requires `CursorStore`, `LoopControl`, `RunEngine`, `ConfigParser`, `ExpressionEngine`, `GitHubPolling`, the repository-label projection reader/writer, and `LabelReconciler`. `LabelReconciler.layer` in turn requires `GitHubLabelWriter`. The engine's local SQLite layer supplies the engine services:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Layer } from "effect"
|
|
50
|
+
import { PollingTriggers } from "@usine/orchestrator/polling-triggers"
|
|
51
|
+
|
|
52
|
+
const dependencies = Layer.mergeAll(
|
|
53
|
+
engineLayer,
|
|
54
|
+
configParserLayer,
|
|
55
|
+
expressionLayer,
|
|
56
|
+
githubPollingLayer,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export const triggersLayer = PollingTriggers.layer.pipe(
|
|
60
|
+
Layer.provide(dependencies),
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Call `poll` when another scheduler owns timing. Use `run` for the built-in endless polling loop.
|
|
65
|
+
|
|
66
|
+
## Schedule triggers
|
|
67
|
+
|
|
68
|
+
`ScheduleTriggers.tick(instance)` evaluates the most recent due Tick for every `on: schedule` loop and fans it out to the repositories selected by that loop. Each selected repository receives one Event whose payload is `item.tick = { scheduledAt, scheduledAtLocal, cron, tz }`; `scheduledAt` is the UTC time the Tick was due, `scheduledAtLocal` the same instant in the loop's `tz:`, while the Event's `receivedAt` records when the orchestrator observed it.
|
|
69
|
+
|
|
70
|
+
The Event key covers the loop, canonical repository, and scheduled time. An in-memory fired-tick memory skips already-submitted boundaries, so calling `tick` repeatedly costs nothing, and waking after downtime (or a restart, which clears that memory) emits only the latest missed Tick. A cron whose boundaries cannot be computed at an instant — effect's Cron can fail inside a DST spring-forward gap — fails only that schedule for that cycle and is reported on the returned `ScheduleCycle`. The repository is the schedule stream's Item, so a later Tick follows the same supersede rules as issue and pull-request state. Schedule Runs go through the unchanged `RunEngine`, approval gate, worker scheduler, executor, and agent layer. Because they have no GitHub issue or pull request to comment on, their blocking moments remain UI-only.
|
|
71
|
+
|
|
72
|
+
`ScheduleTriggers.run(instance)` evaluates once at startup for catch-up, then sleeps until the earliest next Tick. Its layer requires `RunEngine`, `ConfigParser`, and `ExpressionEngine`.
|
|
73
|
+
|
|
74
|
+
## Worker scheduling
|
|
75
|
+
|
|
76
|
+
`@usine/orchestrator/scheduler` exports the platform-free `stepExecutorLayer`. `stepExecutorLayer(config, brains)` accepts the roster’s `BrainSpec` values. At startup it verifies every configured worker against that roster, creates one capacity slot per `max_concurrent_attempts`, and assigns slots by Event receipt time. Runs stay pending while every slot is occupied. Each Attempt receives the configured `max_children` child-session budget. The layer rejects worker kinds that its supplied executor cannot run. Worker capability failures during preparation become non-retryable `AgentFailure` outcomes, like unavailable model credentials.
|
|
77
|
+
|
|
78
|
+
`@usine/orchestrator/local` composes the scheduler with the Node local executor and the engine's local SQLite adapter. It requires a caller-owned `CredentialProvider`; use `gitHubCredentialLayer` with `@usine/github`, or supply another provider that mints one repository credential per Attempt.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { layer as localLayer } from "@usine/orchestrator/local"
|
|
82
|
+
|
|
83
|
+
const EngineLive = localLayer(instanceDirectory, config, brains, {
|
|
84
|
+
repositoryUrl: (repository) => `https://github.com/${repository}.git`,
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Provide `CredentialProvider` to `EngineLive`, then request `RunEngine` from the resulting Effect layer. The local composition caches repositories under `<instance>/.usine/workers/<worker>/repos` and removes Attempt worktrees after completion or cancellation.
|
|
89
|
+
|
|
90
|
+
For the whole local application, `runtimeLayer(instanceDirectory, instance, options)` additionally composes GitHub polling, schedule triggers, CEL parsing, `Orchestrator`, and `Operator`. It derives worker preflight requirements from `instance.agents` and requires only `GitHubCredentials`; the server or another host owns how those credentials are loaded. `operatorObservationLayer(instanceDirectory)` is the read-only Node composition for setup and maintenance surfaces: it reads durable Runs and Loop runtime state while trigger execution remains idle.
|
|
91
|
+
|
|
92
|
+
For an exe.dev Worker, use the `@usine/orchestrator/exe-dev` composition. It keeps the engine and SQLite runtime local while the unchanged Worker contract runs on the named VM:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { RepositoryName } from "@usine/core/repository"
|
|
96
|
+
import { GitHubIntegration, IntegrationName } from "@usine/executor/exe-dev"
|
|
97
|
+
import { layer as exeDevLayer } from "@usine/orchestrator/exe-dev"
|
|
98
|
+
|
|
99
|
+
const EngineLive = exeDevLayer(instanceDirectory, config, brains, {
|
|
100
|
+
githubIntegrations: [
|
|
101
|
+
new GitHubIntegration({
|
|
102
|
+
repository: RepositoryName.make("acme/widget"),
|
|
103
|
+
name: IntegrationName.make("usine-acme-widget"),
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The composition sets the GitHub integration grant to five minutes beyond `max_attempt_minutes`. Pass `credentialGrantMinutes` to use another bounded window. The scheduler also bounds every launch at `max_attempt_minutes`: a hung run host or dead link is interrupted through the normal cancel handshake and reported as a typed infrastructure failure before its credential grant lapses.
|
|
111
|
+
|
|
112
|
+
The live exe.dev check requires an existing baked VM and its GitHub integration:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
export USINE_EXE_DEV_INTEGRATION_WORKER=worker-name
|
|
116
|
+
export USINE_EXE_DEV_INTEGRATION_REPO=owner/repo
|
|
117
|
+
export USINE_EXE_DEV_INTEGRATION_REVISION=main
|
|
118
|
+
export USINE_EXE_DEV_INTEGRATION_GITHUB_INTEGRATION=integration-name
|
|
119
|
+
pnpm --filter @usine/orchestrator test:exe-dev
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Live GitHub check
|
|
123
|
+
|
|
124
|
+
The opt-in integration test needs a disposable issue in a repository covered by the configured GitHub App. It creates a uniquely named repository label, applies it to the issue, checks the resulting Event and restart deduplication, then deletes the label.
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
|
|
128
|
+
export USINE_GITHUB_INTEGRATION_REPO=owner/repo
|
|
129
|
+
export USINE_GITHUB_INTEGRATION_ISSUE_NUMBER=123
|
|
130
|
+
pnpm --filter @usine/orchestrator test:integration
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The agent contract test also starts the local worker and production run host. The Pi parent delegates to Claude Code, which reads the issue with `gh` and posts one signed comment as the App bot. Build the workspace first, provide working Pi and Claude credentials in the worker user's home directory, then run:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
pnpm build
|
|
137
|
+
export USINE_GITHUB_INTEGRATION_BOT_LOGIN='your-app[bot]'
|
|
138
|
+
export USINE_AGENT_INTEGRATION_PARENT_MODEL=provider/model
|
|
139
|
+
export USINE_AGENT_INTEGRATION_CHILD_MODEL=claude-model-id
|
|
140
|
+
pnpm --filter @usine/orchestrator test:integration:agent
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The test removes its kick and result comments during cleanup.
|
|
144
|
+
|
|
145
|
+
Everything here is 0.x, so APIs can change between minor releases. Supply `effect` at the version pinned by the usine workspace.
|
|
146
|
+
|
|
147
|
+
Item cycle exhaustion appears as a `CycleBudgetNeed` in the overview and marks the Item’s Board card as needing attention. `Operator.resetCycleBudget` records an answer to the exact Ask; polling restarts the repository cursor, fetches current Items, and evaluates active Loops before opening further Runs. A failed refresh is retried. Each Item fetch captures its budget generation; resets supersede in-flight observations and fresh Events use the current Loop configuration. GitHub delivery of these Asks is tracked separately in [#36](https://github.com/basaltbytes/usine/issues/36).
|
|
148
|
+
|
|
149
|
+
The operator HTTP API exposes `POST /api/cycle-budgets/reset` with `{ "askId": "…" }`. A stale or differently answered Ask returns HTTP 409.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GitHubLabelName } from "@usine/config/label";
|
|
2
|
+
import type { LaneItemState } from "@usine/config/board";
|
|
3
|
+
import { ItemKey, ItemNumber } from "@usine/core/item";
|
|
4
|
+
import type { ItemProjection } from "@usine/engine/item-projection";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
import { BoardProjectionError } from "../operator/board.ts";
|
|
7
|
+
export interface ProjectedItem {
|
|
8
|
+
readonly itemKey: ItemKey;
|
|
9
|
+
readonly kind: "issue" | "pr";
|
|
10
|
+
readonly number: ItemNumber;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly state: LaneItemState;
|
|
14
|
+
readonly linkedPullRequests: ReadonlyArray<string>;
|
|
15
|
+
readonly labels: ReadonlyArray<GitHubLabelName>;
|
|
16
|
+
}
|
|
17
|
+
export declare const projectedItem: (item: ItemProjection) => Effect.Effect<ProjectedItem, BoardProjectionError>;
|
|
18
|
+
//# sourceMappingURL=items.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACjD;AAWD,eAAO,MAAM,aAAa,SAClB,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,oBAAoB,CA0ChD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { GitHubLabelName } from "@usine/config/label";
|
|
2
|
+
import { ItemKey, ItemNumber } from "@usine/core/item";
|
|
3
|
+
import { IssuePayload, PullRequestPayload } from "@usine/github/polling";
|
|
4
|
+
import { Effect, Schema } from "effect";
|
|
5
|
+
import { BoardProjectionError } from "../operator/board.js";
|
|
6
|
+
const labelsFrom = Schema.decodeUnknownEffect(Schema.Array(GitHubLabelName));
|
|
7
|
+
const projectionError = (item, message) => new BoardProjectionError({
|
|
8
|
+
repository: item.repository,
|
|
9
|
+
itemKey: item.itemKey,
|
|
10
|
+
message,
|
|
11
|
+
});
|
|
12
|
+
export const projectedItem = (item) => Effect.gen(function* () {
|
|
13
|
+
if (item.source === "issue") {
|
|
14
|
+
const payload = yield* Schema.decodeUnknownEffect(IssuePayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
|
|
15
|
+
return {
|
|
16
|
+
itemKey: item.itemKey,
|
|
17
|
+
kind: "issue",
|
|
18
|
+
number: ItemNumber.make(item.number),
|
|
19
|
+
title: payload.title,
|
|
20
|
+
url: payload.url,
|
|
21
|
+
state: payload.state === "OPEN" ? "issue-open" : "issue-closed",
|
|
22
|
+
linkedPullRequests: payload.closedByPullRequestsReferences.nodes.map(({ url }) => url),
|
|
23
|
+
labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (item.source === "pr") {
|
|
27
|
+
const payload = yield* Schema.decodeUnknownEffect(PullRequestPayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
|
|
28
|
+
const state = payload.merged
|
|
29
|
+
? "pr-merged"
|
|
30
|
+
: payload.state === "OPEN"
|
|
31
|
+
? "pr-open"
|
|
32
|
+
: "pr-closed";
|
|
33
|
+
return {
|
|
34
|
+
itemKey: item.itemKey,
|
|
35
|
+
kind: "pr",
|
|
36
|
+
number: ItemNumber.make(item.number),
|
|
37
|
+
title: payload.title,
|
|
38
|
+
url: payload.url,
|
|
39
|
+
state,
|
|
40
|
+
linkedPullRequests: [],
|
|
41
|
+
labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return yield* projectionError(item, `Unsupported Item projection source: ${item.source}`);
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=items.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ManagedRepo } from "@usine/config/factory";
|
|
2
|
+
import type { ItemProjection } from "@usine/engine/item-projection";
|
|
3
|
+
import { ItemLabelMutation } from "@usine/github/item-writer";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { BoardView, WritableBoardLaneView } from "../operator/board.ts";
|
|
6
|
+
import { type RunView } from "../operator/run.ts";
|
|
7
|
+
export declare const projectBoard: (repository: ManagedRepo, repositoryLabels: readonly string[], storedItems: readonly ItemProjection[], runs: readonly RunView[], cycleBlockedItems?: readonly (string & import("effect/Brand").Brand<"ItemKey">)[] | undefined) => Effect.Effect<BoardView, never, never>;
|
|
8
|
+
export declare const labelMutationForBoardMove: (board: BoardView, target: WritableBoardLaneView) => ItemLabelMutation;
|
|
9
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/board/projection.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAIL,SAAS,EAET,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAgElD,eAAO,MAAM,YAAY,2QAwExB,CAAC;AAEF,eAAO,MAAM,yBAAyB,UAC7B,SAAS,UACR,qBAAqB,KAC5B,iBAaF,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { GitHubLabelName } from "@usine/config/label";
|
|
2
|
+
import { LabelLaneSource, Lane, LaneName, } from "@usine/config/board";
|
|
3
|
+
import { ItemKey } from "@usine/core/item";
|
|
4
|
+
import { ItemLabelMutation } from "@usine/github/item-writer";
|
|
5
|
+
import { Effect, Result } from "effect";
|
|
6
|
+
import { BoardTaskView, BoardLaneId, BoardProjectionError, BoardView, ReadOnlyBoardLaneView, WritableBoardLaneView, } from "../operator/board.js";
|
|
7
|
+
import {} from "../operator/run.js";
|
|
8
|
+
import { projectedItem } from "./items.js";
|
|
9
|
+
import { taskCards } from "./tasks.js";
|
|
10
|
+
const labelSources = (sources) => sources.flatMap((source) => ("label" in source ? [source.label] : []));
|
|
11
|
+
const stateSources = (sources) => sources.flatMap((source) => ("state" in source ? [source.state] : []));
|
|
12
|
+
const proposedLanes = (repositoryLabels) => {
|
|
13
|
+
const labels = [...repositoryLabels]
|
|
14
|
+
.sort((left, right) => left.localeCompare(right))
|
|
15
|
+
.map((label) => GitHubLabelName.make(label));
|
|
16
|
+
return labels.map((label) => ({
|
|
17
|
+
lane: new Lane({
|
|
18
|
+
name: LaneName.make(label),
|
|
19
|
+
sources: [new LabelLaneSource({ label })],
|
|
20
|
+
}),
|
|
21
|
+
origin: "proposed",
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
const effectiveLanes = (repository, repositoryLabels) => repository.lanes.length > 0
|
|
25
|
+
? repository.lanes.map((lane) => ({ lane, origin: "authored" }))
|
|
26
|
+
: proposedLanes(repositoryLabels);
|
|
27
|
+
const laneId = (definition) => BoardLaneId.make(`${definition.origin}:${definition.lane.name}`);
|
|
28
|
+
const SETTLED_STATES = ["pr-merged", "pr-closed", "issue-closed"];
|
|
29
|
+
const isSettled = (item) => SETTLED_STATES.includes(item.state);
|
|
30
|
+
const labelLaneFor = (item, lanes) => lanes.find(({ lane }) => labelSources(lane.sources).some((label) => item.labels.includes(label)));
|
|
31
|
+
const stateLaneFor = (item, lanes) => lanes.find(({ lane }) => stateSources(lane.sources).includes(item.state));
|
|
32
|
+
const laneFor = (item, lanes) => isSettled(item)
|
|
33
|
+
? stateLaneFor(item, lanes)
|
|
34
|
+
: (labelLaneFor(item, lanes) ?? stateLaneFor(item, lanes));
|
|
35
|
+
export const projectBoard = Effect.fn("Board.project")((repository, repositoryLabels, storedItems, runs, cycleBlockedItems = []) => Effect.gen(function* () {
|
|
36
|
+
yield* Effect.annotateCurrentSpan({ repository: repository.name });
|
|
37
|
+
const results = yield* Effect.forEach(storedItems, (item) => projectedItem(item).pipe(Effect.result));
|
|
38
|
+
const items = [];
|
|
39
|
+
const failures = [];
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
if (Result.isSuccess(result)) {
|
|
42
|
+
items.push(result.success);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
failures.push(result.failure);
|
|
46
|
+
yield* Effect.logError("Item projection quarantined from Board; siblings continue", {
|
|
47
|
+
repository: result.failure.repository,
|
|
48
|
+
itemKey: result.failure.itemKey,
|
|
49
|
+
errorTag: result.failure._tag,
|
|
50
|
+
message: result.failure.message,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const lanes = effectiveLanes(repository, repositoryLabels);
|
|
54
|
+
const cards = taskCards(repository.name, items, runs, cycleBlockedItems);
|
|
55
|
+
yield* Effect.logDebug("Projected Task cards from linked Items", {
|
|
56
|
+
repository: repository.name,
|
|
57
|
+
items: items.length,
|
|
58
|
+
tasks: cards.length,
|
|
59
|
+
runs: runs.length,
|
|
60
|
+
});
|
|
61
|
+
const grouped = new Map();
|
|
62
|
+
for (const definition of lanes)
|
|
63
|
+
grouped.set(laneId(definition), []);
|
|
64
|
+
const unmatched = [];
|
|
65
|
+
for (const card of cards) {
|
|
66
|
+
const definition = laneFor(card, lanes);
|
|
67
|
+
if (definition === undefined)
|
|
68
|
+
unmatched.push(card);
|
|
69
|
+
else
|
|
70
|
+
grouped.get(laneId(definition))?.push(card);
|
|
71
|
+
}
|
|
72
|
+
const sortItems = (values) => values.sort((left, right) => right.number - left.number);
|
|
73
|
+
return new BoardView({
|
|
74
|
+
failures,
|
|
75
|
+
lanes: [
|
|
76
|
+
...lanes.map((definition) => {
|
|
77
|
+
const labels = labelSources(definition.lane.sources);
|
|
78
|
+
const fields = {
|
|
79
|
+
id: laneId(definition),
|
|
80
|
+
name: definition.lane.name,
|
|
81
|
+
sources: definition.lane.sources,
|
|
82
|
+
origin: definition.origin,
|
|
83
|
+
items: sortItems(grouped.get(laneId(definition)) ?? []),
|
|
84
|
+
};
|
|
85
|
+
return definition.origin === "authored" && labels.length > 0
|
|
86
|
+
? new WritableBoardLaneView({ ...fields, origin: definition.origin, labels })
|
|
87
|
+
: new ReadOnlyBoardLaneView(fields);
|
|
88
|
+
}),
|
|
89
|
+
new ReadOnlyBoardLaneView({
|
|
90
|
+
id: BoardLaneId.make("fallback:unmapped"),
|
|
91
|
+
name: LaneName.make("Unmapped"),
|
|
92
|
+
sources: [],
|
|
93
|
+
origin: "fallback",
|
|
94
|
+
items: sortItems(unmatched),
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
}));
|
|
99
|
+
export const labelMutationForBoardMove = (board, target) => {
|
|
100
|
+
const laneOwned = new Set(board.lanes
|
|
101
|
+
.filter(({ origin }) => origin === target.origin)
|
|
102
|
+
.flatMap(({ sources }) => labelSources(sources)));
|
|
103
|
+
const targetLabels = new Set(target.labels);
|
|
104
|
+
return new ItemLabelMutation({
|
|
105
|
+
add: [...targetLabels].sort((left, right) => left.localeCompare(right)),
|
|
106
|
+
remove: [...laneOwned]
|
|
107
|
+
.filter((label) => !targetLabels.has(label))
|
|
108
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.js","sourceRoot":"","sources":["../../src/board/projection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ItemKey } from "@usine/core/item";
|
|
2
|
+
import { type RepositoryName } from "@usine/core/repository";
|
|
3
|
+
import { BoardTaskView } from "../operator/board.ts";
|
|
4
|
+
import type { RunView } from "../operator/run.ts";
|
|
5
|
+
import type { ProjectedItem } from "./items.ts";
|
|
6
|
+
/** Joins explicit issue/PR relationships into Task cards without creating stored Task identities (ADR 0024). */
|
|
7
|
+
export declare const taskCards: (repository: RepositoryName, items: ReadonlyArray<ProjectedItem>, runs: ReadonlyArray<RunView>, cycleBlockedItems: ReadonlyArray<ItemKey>) => ReadonlyArray<BoardTaskView>;
|
|
8
|
+
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/board/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAEtF,OAAO,EAAiB,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqBhD,gHAAgH;AAChH,eAAO,MAAM,SAAS,eACR,cAAc,SACnB,aAAa,CAAC,aAAa,CAAC,QAC7B,aAAa,CAAC,OAAO,CAAC,qBACT,aAAa,CAAC,OAAO,CAAC,KACxC,aAAa,CAAC,aAAa,CAuF7B,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { canonicalRepositoryName } from "@usine/core/repository";
|
|
2
|
+
import { Array as EffectArray, Option, Order, Schema } from "effect";
|
|
3
|
+
import { BoardItemLink, BoardTaskView } from "../operator/board.js";
|
|
4
|
+
const decodeUrl = Schema.decodeUnknownOption(Schema.URLFromString);
|
|
5
|
+
const itemLink = (item) => new BoardItemLink({
|
|
6
|
+
itemKey: item.itemKey,
|
|
7
|
+
kind: item.kind,
|
|
8
|
+
number: item.number,
|
|
9
|
+
title: item.title,
|
|
10
|
+
url: item.url,
|
|
11
|
+
});
|
|
12
|
+
/** Joins explicit issue/PR relationships into Task cards without creating stored Task identities (ADR 0024). */
|
|
13
|
+
export const taskCards = (repository, items, runs, cycleBlockedItems) => {
|
|
14
|
+
const runsByItem = EffectArray.groupBy(runs, ({ itemKey }) => itemKey);
|
|
15
|
+
const nodes = items.map((item) => ({
|
|
16
|
+
item,
|
|
17
|
+
runs: runsByItem[item.itemKey] ?? [],
|
|
18
|
+
related: new Set(),
|
|
19
|
+
}));
|
|
20
|
+
const repositoryPath = `/${canonicalRepositoryName(repository)}/pull/`;
|
|
21
|
+
const pullRequests = new Map(nodes
|
|
22
|
+
.filter(({ item }) => item.kind === "pr")
|
|
23
|
+
.map((node) => [`${repositoryPath}${node.item.number}`, node]));
|
|
24
|
+
const blocked = new Set(cycleBlockedItems);
|
|
25
|
+
for (const node of nodes) {
|
|
26
|
+
if (node.item.kind !== "issue")
|
|
27
|
+
continue;
|
|
28
|
+
const urls = [
|
|
29
|
+
...node.item.linkedPullRequests,
|
|
30
|
+
...node.runs.flatMap(({ pullRequestUrl }) => pullRequestUrl === null ? [] : [pullRequestUrl]),
|
|
31
|
+
];
|
|
32
|
+
for (const value of urls) {
|
|
33
|
+
const url = decodeUrl(value);
|
|
34
|
+
if (Option.isNone(url) || url.value.protocol !== "https:" || url.value.host !== "github.com")
|
|
35
|
+
continue;
|
|
36
|
+
// Match the canonical PR path, not a numeric coercion of its last segment.
|
|
37
|
+
// Number-only payload references can point to another repository.
|
|
38
|
+
const pr = pullRequests.get(url.value.pathname.toLowerCase());
|
|
39
|
+
if (pr === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
node.related.add(pr);
|
|
42
|
+
pr.related.add(node);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const visited = new Set();
|
|
46
|
+
const cards = [];
|
|
47
|
+
for (const seed of nodes) {
|
|
48
|
+
if (visited.has(seed))
|
|
49
|
+
continue;
|
|
50
|
+
visited.add(seed);
|
|
51
|
+
const members = [seed];
|
|
52
|
+
for (const member of members) {
|
|
53
|
+
for (const related of member.related) {
|
|
54
|
+
if (visited.has(related))
|
|
55
|
+
continue;
|
|
56
|
+
visited.add(related);
|
|
57
|
+
members.push(related);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const ordered = EffectArray.sort(members, ({ item: a }, { item: b }) => (a.kind === b.kind ? 0 : a.kind === "issue" ? -1 : 1) || Order.Number(a.number, b.number));
|
|
61
|
+
const origin = ordered[0];
|
|
62
|
+
const history = members
|
|
63
|
+
.flatMap(({ item, runs }) => runs.map((run) => ({ item, run })))
|
|
64
|
+
.sort(({ run: a }, { run: b }) => b.openedAt.localeCompare(a.openedAt) || b.id.localeCompare(a.id));
|
|
65
|
+
const active = history.find(({ run }) => run.canCancel);
|
|
66
|
+
const prs = members
|
|
67
|
+
.filter(({ item }) => item.kind === "pr")
|
|
68
|
+
.sort(({ item: a }, { item: b }) => Number(b.state === "pr-open") - Number(a.state === "pr-open") || b.number - a.number);
|
|
69
|
+
// An open Run controls the lane. Between stages, a PR takes over from its issue.
|
|
70
|
+
const current = active?.item ?? prs[0]?.item ?? origin.item;
|
|
71
|
+
const preferred = history.find(({ run }) => run.canApprove || run.canRetry) ?? active ?? history[0];
|
|
72
|
+
cards.push(new BoardTaskView({
|
|
73
|
+
...itemLink(current),
|
|
74
|
+
title: origin.item.title,
|
|
75
|
+
state: current.state,
|
|
76
|
+
labels: current.labels,
|
|
77
|
+
items: ordered.map(({ item }) => itemLink(item)),
|
|
78
|
+
runs: history.map(({ run }) => run).reverse(),
|
|
79
|
+
run: preferred?.run ?? null,
|
|
80
|
+
needsYou: history.some(({ run }) => run.canApprove || run.canRetry) ||
|
|
81
|
+
members.some(({ item }) => blocked.has(item.itemKey)),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return cards;
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/board/tasks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UsineConfig } from "@usine/config/factory";
|
|
2
|
+
import { CredentialProvider } from "@usine/executor";
|
|
3
|
+
import { GitHubApp } from "@usine/github/github-app";
|
|
4
|
+
import { Layer } from "effect";
|
|
5
|
+
/**
|
|
6
|
+
* Mints one fresh single-repository GitHub App token per Attempt. Project
|
|
7
|
+
* access is discovered on the first mint and cached, so a GitHub outage cannot
|
|
8
|
+
* block startup, and a repository added to the installation later is found by
|
|
9
|
+
* the refresh that follows a miss.
|
|
10
|
+
*/
|
|
11
|
+
export declare const gitHubCredentialLayer: (config: UsineConfig) => Layer.Layer<CredentialProvider, never, GitHubApp>;
|
|
12
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,kBAAkB,EAA8B,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,SAAS,EAAkB,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAU,KAAK,EAA2B,MAAM,QAAQ,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,WACxB,WAAW,KAClB,KAAK,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,EAAE,SAAS,CAoFhD,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CredentialProvider, CredentialUnavailableError } from "@usine/executor";
|
|
2
|
+
import { GitHubApp, CoveredProject } from "@usine/github/github-app";
|
|
3
|
+
import { Effect, Layer, Option, SynchronizedRef } from "effect";
|
|
4
|
+
/**
|
|
5
|
+
* Mints one fresh single-repository GitHub App token per Attempt. Project
|
|
6
|
+
* access is discovered on the first mint and cached, so a GitHub outage cannot
|
|
7
|
+
* block startup, and a repository added to the installation later is found by
|
|
8
|
+
* the refresh that follows a miss.
|
|
9
|
+
*/
|
|
10
|
+
export const gitHubCredentialLayer = (config) => Layer.effect(CredentialProvider, Effect.gen(function* () {
|
|
11
|
+
const app = yield* GitHubApp;
|
|
12
|
+
const projectsRef = yield* SynchronizedRef.make(Option.none());
|
|
13
|
+
const discover = (repository) => app.projectAccess(config.repos.map(({ name }) => name)).pipe(Effect.map((access) => new Map(access
|
|
14
|
+
.filter((project) => project._tag === "CoveredProject")
|
|
15
|
+
.map((project) => [project.repository, project]))), Effect.mapError((cause) => new CredentialUnavailableError({
|
|
16
|
+
repository,
|
|
17
|
+
message: "Could not discover GitHub App repository access",
|
|
18
|
+
cause,
|
|
19
|
+
})));
|
|
20
|
+
/** Refreshes the cached project map once on a miss before answering "not covered". */
|
|
21
|
+
const projectFor = Effect.fn("GitHubCredentialProvider.projectFor")(function* (repository) {
|
|
22
|
+
const project = yield* SynchronizedRef.modifyEffect(projectsRef, (current) => Effect.gen(function* () {
|
|
23
|
+
if (Option.isSome(current)) {
|
|
24
|
+
const found = current.value.get(repository);
|
|
25
|
+
if (found !== undefined) {
|
|
26
|
+
const kept = [
|
|
27
|
+
Option.some(found),
|
|
28
|
+
current,
|
|
29
|
+
];
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const refreshed = yield* discover(repository);
|
|
34
|
+
const refreshedResult = [Option.fromNullishOr(refreshed.get(repository)), Option.some(refreshed)];
|
|
35
|
+
return refreshedResult;
|
|
36
|
+
}));
|
|
37
|
+
if (Option.isNone(project)) {
|
|
38
|
+
return yield* new CredentialUnavailableError({
|
|
39
|
+
repository,
|
|
40
|
+
message: `GitHub App installation does not cover ${repository}`,
|
|
41
|
+
cause: new Error("no project access after refresh"),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return project.value;
|
|
45
|
+
});
|
|
46
|
+
return CredentialProvider.of({
|
|
47
|
+
issue: Effect.fn("GitHubCredentialProvider.issue")(function* (repository, attemptId) {
|
|
48
|
+
const project = yield* projectFor(repository);
|
|
49
|
+
const token = yield* app.workerToken(project).pipe(Effect.mapError((cause) => new CredentialUnavailableError({
|
|
50
|
+
repository,
|
|
51
|
+
message: `Could not mint a worker token for ${repository}`,
|
|
52
|
+
cause,
|
|
53
|
+
})));
|
|
54
|
+
yield* Effect.logInfo("Minted a single-repository worker token", {
|
|
55
|
+
repository,
|
|
56
|
+
attemptId,
|
|
57
|
+
});
|
|
58
|
+
return token.value;
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}));
|
|
62
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BrainSpec } from "@usine/core/agent/brain";
|
|
2
|
+
import type { UsineConfig } from "@usine/config/factory";
|
|
3
|
+
import type { InstanceDirectory } from "@usine/core/instance";
|
|
4
|
+
import { type ExeDevExecutorOptions } from "@usine/executor/exe-dev";
|
|
5
|
+
import { Layer } from "effect";
|
|
6
|
+
/** The full engine composition for a local orchestrator driving exe.dev Workers; brains are the instance roster checked at startup. */
|
|
7
|
+
export declare const layer: (instanceDirectory: InstanceDirectory, config: UsineConfig, brains: ReadonlyArray<BrainSpec>, options: ExeDevExecutorOptions) => Layer.Layer<import("@usine/engine/cursor").CursorStore | import("@usine/engine/cycle-budget").CycleBudgets | import("@usine/engine/item-projection").ItemProjectionReader | import("@usine/engine/item-projection").ItemProjectionWriter | import("@usine/engine/loop-control").LoopControl | import("@usine/engine/repository-label-projection").RepositoryLabelProjectionReader | import("@usine/engine/repository-label-projection").RepositoryLabelProjectionWriter | import("@usine/engine/engine").RunEngine | import("@usine/engine/run-reader").RunReader, import("@usine/engine/engine").AttemptCancellationError | import("@usine/executor").CredentialUnavailableError | import("@usine/engine/engine").EnginePersistenceError | import("@usine/executor").ExecutorOperationError | import("@usine/engine/engine").RunNotFoundError | import("@usine/engine/engine").RunOperationRejectedError | import("./scheduler.ts").UnsupportedWorkerError | import("@usine/executor").WorkerCapabilityError, never>;
|
|
8
|
+
//# sourceMappingURL=exe-dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exe-dev.d.ts","sourceRoot":"","sources":["../src/exe-dev.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAA0B,KAAK,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAE7F,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAG/B,uIAAuI;AACvI,eAAO,MAAM,KAAK,sBACG,iBAAiB,UAC5B,WAAW,UACX,aAAa,CAAC,SAAS,CAAC,WACvB,qBAAqB,09BAU/B,CAAC"}
|
package/dist/exe-dev.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NodeServices } from "@effect/platform-node";
|
|
2
|
+
import { layer as exeDevExecutorLayer } from "@usine/executor/exe-dev";
|
|
3
|
+
import { CredentialGrantMinutes } from "@usine/executor/exe-dev";
|
|
4
|
+
import { layer as localEngineLayer } from "@usine/engine/local-sqlite";
|
|
5
|
+
import { Layer } from "effect";
|
|
6
|
+
import { stepExecutorLayer } from "./scheduler.js";
|
|
7
|
+
/** The full engine composition for a local orchestrator driving exe.dev Workers; brains are the instance roster checked at startup. */
|
|
8
|
+
export const layer = (instanceDirectory, config, brains, options) => {
|
|
9
|
+
const executor = exeDevExecutorLayer({
|
|
10
|
+
...options,
|
|
11
|
+
credentialGrantMinutes: options.credentialGrantMinutes ??
|
|
12
|
+
CredentialGrantMinutes.make(config.settings.maxAttemptMinutes + 5),
|
|
13
|
+
}).pipe(Layer.provide(NodeServices.layer));
|
|
14
|
+
const stepExecutor = stepExecutorLayer(config, brains).pipe(Layer.provide(executor));
|
|
15
|
+
return localEngineLayer(instanceDirectory).pipe(Layer.provide(stepExecutor));
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=exe-dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exe-dev.js","sourceRoot":"","sources":["../src/exe-dev.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAAE,KAAK,IAAI,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAA8B,MAAM,yBAAyB,CAAC;AAC7F,OAAO,EAAE,KAAK,IAAI,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEnD,uIAAuI;AACvI,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,iBAAoC,EACpC,MAAmB,EACnB,MAAgC,EAChC,OAA8B,EAC9B,EAAE;IACF,MAAM,QAAQ,GAAG,mBAAmB,CAAC;QACnC,GAAG,OAAO;QACV,sBAAsB,EACpB,OAAO,CAAC,sBAAsB;YAC9B,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,GAAG,CAAC,CAAC;KACrE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrF,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;AAC/E,CAAC,CAAC"}
|