@opensoftware/contracts 2.3.0 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoftware/contracts",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The shared formats of the open software factory: JSON Schemas, TypeScript types and the osf-contracts CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "@typespec/openapi": "1.17.0",
45
45
  "@typespec/openapi3": "1.17.0",
46
46
  "openapi-typescript": "7.13.0",
47
- "@opensoftware/testkit": "0.33.0"
47
+ "@opensoftware/testkit": "0.34.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node scripts/build.ts",
@@ -39,12 +39,24 @@ model CloudEvent<Type extends string, Data> {
39
39
  /** intake: a work item was accepted. */
40
40
  model WorkReceived is CloudEvent<"work.received@v1", WorkItem>;
41
41
 
42
+ /** intake: a person asked to run a work item's run again (ADR 0105). The ID is `<run ID>.retry`. */
43
+ model WorkRetryRequested is CloudEvent<"work.retry-requested@v1", WorkRetryRequestedData>;
44
+
42
45
  /** foreman: a run started (ADR 0067). */
43
46
  model RunStarted is CloudEvent<"run.started@v1", RunStartedData>;
44
47
 
45
48
  /** foreman: a run ended. */
46
49
  model RunCompleted is CloudEvent<"run.completed@v1", RunCompletedData>;
47
50
 
51
+ /** foreman: a stage of a run waits for the checks of its pull request (ADR 0127). The ID is `<run ID>.blocked.<stage>`. */
52
+ model RunBlocked is CloudEvent<"run.blocked@v1", RunBlockedData>;
53
+
54
+ /**
55
+ * console: `console doctor --deep` checks that an event reaches the foreman (ADR 0120). The ID is fresh and random;
56
+ * the foreman shows it on `GET /v1/doctor/pings/{id}` and acts on nothing else.
57
+ */
58
+ model DoctorPing is CloudEvent<"doctor.ping@v1", DoctorPingData>;
59
+
48
60
  /** gatekeeper: a decision is needed before a stage starts. */
49
61
  model GateRequested is CloudEvent<"gate.requested@v1", GateRequestedData>;
50
62
 
@@ -81,6 +93,9 @@ model RunStartedData {
81
93
  startedAt: utcDateTime;
82
94
  requester: Actor;
83
95
  source: Name;
96
+
97
+ /** The run before, when this run runs a work item again (ADR 0105). */
98
+ retryOf?: Identifier;
84
99
  }
85
100
 
86
101
  model RunCompletedData {
@@ -100,6 +115,49 @@ model RunCompletedData {
100
115
  traceId?: TraceId;
101
116
  pullRequests?: url[];
102
117
  evidenceBundle?: url;
118
+
119
+ /** The run before, when this run runs a work item again (ADR 0105). */
120
+ retryOf?: Identifier;
121
+
122
+ /** Why the stage the run ended at failed, when the foreman knows it (ADR 0127). */
123
+ reason?: string;
124
+ }
125
+
126
+ /** What a blocked stage waits for: the checks of the run's pull request. A gate has its own `gate.requested@v1`. */
127
+ enum BlockedOn {
128
+ checks,
129
+ }
130
+
131
+ model RunBlockedData {
132
+ runId: Identifier;
133
+ workItemId: Identifier;
134
+
135
+ /** The stage that waits. */
136
+ stage: Name;
137
+
138
+ waitsFor: BlockedOn;
139
+
140
+ /** The pull request whose checks the stage waits for. */
141
+ pullRequest?: url;
142
+
143
+ /** The names of the checks the stage waits for. */
144
+ checks?: string[];
145
+
146
+ requester: Actor;
147
+ source: Name;
148
+ }
149
+
150
+ /** A ping carries nothing but its ID. */
151
+ model DoctorPingData {}
152
+
153
+ model WorkRetryRequestedData {
154
+ workItemId: Identifier;
155
+
156
+ /** The run to run again: the run the work item follows. */
157
+ runId: Identifier;
158
+
159
+ /** The person who asked. */
160
+ requester: Actor;
103
161
  }
104
162
 
105
163
  model GateRequestedData {
@@ -36,7 +36,7 @@ model FactoryDefinition {
36
36
  @minProperties(1)
37
37
  agents: Record<Agent>;
38
38
 
39
- /** Stages in order. Each stage runs one agent; the agent defaults to the stage name. */
39
+ /** Stages in order. Each stage runs one agent, which defaults to the stage name, or only waits (ADR 0107). */
40
40
  workflow?: Stage[];
41
41
 
42
42
  /** What starts work without a person asking. */
@@ -155,6 +155,12 @@ model Stage {
155
155
 
156
156
  /** Whether the stage must produce an evidence bundle. */
157
157
  evidence?: EvidenceRequirement;
158
+
159
+ /**
160
+ * The stage runs no agent: it only waits for its checks, its evidence and its gate, then succeeds (ADR 0107). It
161
+ * names no agent and at least one of checks, evidence "required" or gate.
162
+ */
163
+ wait?: boolean;
158
164
  }
159
165
 
160
166
  /** The name of a check on the forge: a commit status context. */
@@ -0,0 +1,56 @@
1
+ import "@typespec/json-schema";
2
+ import "./common.tsp";
3
+
4
+ using TypeSpec.JsonSchema;
5
+
6
+ namespace Osf.Contracts;
7
+
8
+ /**
9
+ * factory.lock next to factory.yaml (ADR 0020, ADR 0125): the exact versions that `factory plan` pinned. factory-spec
10
+ * writes it; the foreman reads the pins of its adapters.
11
+ */
12
+ model FactoryLock {
13
+ /** The version of the lock's format. */
14
+ version: 1;
15
+
16
+ /** The registry MCP servers, by their reference in factory.yaml, for example `registry:io.github.acme/tracker@1.4`. */
17
+ mcp: Record<FactoryLockMcpPin>;
18
+
19
+ /**
20
+ * The adapters under `providers` that `factory plan --adapters` pinned, per port and adapter name (ADR 0125). A pin
21
+ * counts only while its `range` is the range factory.yaml declares.
22
+ */
23
+ adapters?: Record<Record<FactoryLockAdapterPin>>;
24
+ }
25
+
26
+ /** One registry MCP server at its exact version, with the npm package or the remote it starts. */
27
+ model FactoryLockMcpPin {
28
+ @minLength(1)
29
+ name: string;
30
+
31
+ @minLength(1)
32
+ version: string;
33
+
34
+ package?: {
35
+ registryType: "npm";
36
+ @minLength(1) identifier: string;
37
+ @minLength(1) version: string;
38
+ };
39
+
40
+ remote?: {
41
+ @minLength(1) type: string;
42
+ @minLength(1) url: string;
43
+ };
44
+ }
45
+
46
+ /** One adapter's npm package `@opensoftware/<port>-<name>` at the newest version inside its range. */
47
+ @extension("additionalProperties", Json<false>)
48
+ model FactoryLockAdapterPin {
49
+ /** The range under `providers` that the pin was resolved from, for example "^2". */
50
+ @minLength(1)
51
+ range: string;
52
+
53
+ /** The exact version, for example "2.3.1". */
54
+ @minLength(1)
55
+ version: string;
56
+ }
@@ -10,7 +10,7 @@ using TypeSpec.Http;
10
10
  namespace Osf.Contracts;
11
11
 
12
12
  namespace ForemanApi {
13
- /** Start a run of the factory's workflow for a work item. The same work item again answers the run it started. */
13
+ /** Start a run of the factory's workflow for a work item. The same work item again answers its newest run. */
14
14
  @route("/v1/runs")
15
15
  @post
16
16
  op startRun(@body workItem: WorkItem):
@@ -25,10 +25,18 @@ namespace ForemanApi {
25
25
  | GatekeeperApi.Failure<400>
26
26
  | GatekeeperApi.Failure<401>;
27
27
 
28
- /** The runs of the server, running runs first, then the runs that ended, the one that ended last first; with `state`, only the runs in that state (ADR 0071). */
28
+ /**
29
+ * A page of the runs of the server, running runs first, then the runs that ended, the one that ended last first; with
30
+ * `state`, only the runs in that state (ADR 0071). `limit` runs at most, 50 by default; `cursor` is the `next` of the
31
+ * page before (ADR 0115). With `state`, a page may hold fewer runs than `limit` and still have a `next`.
32
+ */
29
33
  @route("/v1/runs")
30
34
  @get
31
- op listRuns(@query state?: RunState): RunList | GatekeeperApi.Failure<400> | GatekeeperApi.Failure<401>;
35
+ op listRuns(
36
+ @query state?: RunState,
37
+ @query @minValue(1) @maxValue(500) limit?: int32,
38
+ @query @minLength(1) cursor?: string,
39
+ ): RunList | GatekeeperApi.Failure<400> | GatekeeperApi.Failure<401>;
32
40
 
33
41
  /** A run: its state and the state of each stage. */
34
42
  @route("/v1/runs/{runId}")
@@ -47,4 +55,25 @@ namespace ForemanApi {
47
55
  | GatekeeperApi.Failure<401>
48
56
  | GatekeeperApi.Failure<404>
49
57
  | GatekeeperApi.Failure<409>;
58
+
59
+ /** Run a run that failed or was cancelled again, as a new run that names it (ADR 0105). */
60
+ @route("/v1/runs/{runId}/retry")
61
+ @post
62
+ op retryRun(@path runId: string, @body retry: RunRetry):
63
+ | {
64
+ @statusCode statusCode: 201;
65
+ @body run: RunView;
66
+ }
67
+ | GatekeeperApi.Failure<400>
68
+ | GatekeeperApi.Failure<401>
69
+ | GatekeeperApi.Failure<404>
70
+ | GatekeeperApi.Failure<409>;
71
+
72
+ /**
73
+ * The `doctor.ping@v1` event with this ID, when the foreman's consumer received it since the server started (ADR
74
+ * 0120). `console doctor --deep` asks for it after it published the event.
75
+ */
76
+ @route("/v1/doctor/pings/{id}")
77
+ @get
78
+ op getDoctorPing(@path id: string): DoctorPingSeen | GatekeeperApi.Failure<401> | GatekeeperApi.Failure<404>;
50
79
  }
@@ -45,14 +45,24 @@ model CheckView {
45
45
  model StageView {
46
46
  stage: Name;
47
47
 
48
- /** The agent that runs the stage. */
48
+ /** The agent that runs the stage; the stage's name for a stage that only waits (ADR 0107). */
49
49
  agent: Name;
50
50
 
51
+ /** True for a stage that only waits: it runs no agent and starts no session (ADR 0107). */
52
+ wait?: boolean;
53
+
51
54
  state: StageState;
52
55
 
53
56
  /** The harness session of the stage's agent, once it started. */
54
57
  sessionId?: string;
55
58
 
59
+ /**
60
+ * The span of the stage in the run's trace, once it started: the first 16 hex digits of the SHA-256 of
61
+ * `<runId>:<stage index>` (ADR 0087, ADR 0121).
62
+ */
63
+ @pattern("^[0-9a-f]{16}$")
64
+ spanId?: string;
65
+
56
66
  /** The gate that decides whether the stage starts, once it was requested. */
57
67
  @pattern("^g[0-9a-f]{16}$")
58
68
  gateId?: string;
@@ -60,6 +70,9 @@ model StageView {
60
70
  /** The checks the stage waits for, once the foreman read them (ADR 0069). */
61
71
  checks?: CheckView[];
62
72
 
73
+ /** Since when the stage waits for its checks, its gate or a person, while it is blocked (ADR 0106). */
74
+ waitingSince?: utcDateTime;
75
+
63
76
  /** When a person took the stage over from its agent (ADR 0070). */
64
77
  takenOverAt?: utcDateTime;
65
78
 
@@ -67,16 +80,51 @@ model StageView {
67
80
  @minLength(1)
68
81
  reason?: string;
69
82
 
83
+ /** Why the stage gave the run no pull request: a refused link, a branch the forge has none of, or the forge adapter's error (ADR 0109). */
84
+ @minLength(1)
85
+ note?: string;
86
+
70
87
  startedAt?: utcDateTime;
71
88
  endedAt?: utcDateTime;
72
89
  }
73
90
 
74
91
  /** foreman: a run of the factory's workflow for one work item, as the HTTP API shows it (ADR 0067). */
92
+ /** An adapter of the factory's `providers` and the version a run of it asks for (ADR 0110). */
93
+ model AdapterVersion {
94
+ /** The port under `providers`: harness, sandbox, forge or inference. */
95
+ port: "harness" | "sandbox" | "forge" | "inference";
96
+
97
+ @minLength(1)
98
+ name: string;
99
+
100
+ /** The exact version that factory.lock pins for the range under `providers.<port>.<name>`, else that range (ADR 0125). */
101
+ @minLength(1)
102
+ version?: string;
103
+
104
+ /** The command line the foreman builds when no map names the adapter: npx at the version. */
105
+ command?: string[];
106
+
107
+ /** The variable whose map gives the adapter's command line instead, for example "FOREMAN_HARNESSES". */
108
+ @minLength(1)
109
+ configured?: string;
110
+ }
111
+
75
112
  model RunView {
76
- /** "run_" and 20 hex digits of the SHA-256 of the work item's ID. */
113
+ /** "run_" and 20 hex digits of the SHA-256 of the work item's ID; for the n-th run of the work item, n ≥ 2, of "<work item ID>#<n>" (ADR 0105). */
77
114
  @pattern("^run_[0-9a-f]{20}$")
78
115
  runId: string;
79
116
 
117
+ /** Which run of the work item this is: 1 when absent (ADR 0105). */
118
+ @minValue(1)
119
+ attempt?: safeint;
120
+
121
+ /** The run before this one, which a person ran again (ADR 0105). */
122
+ @pattern("^run_[0-9a-f]{20}$")
123
+ retryOf?: string;
124
+
125
+ /** The person who ran the run before again (ADR 0105). */
126
+ retriedBy?: Actor;
127
+
80
128
  workItem: WorkItem;
81
129
 
82
130
  /** The name of the factory. */
@@ -85,9 +133,12 @@ model RunView {
85
133
  /** The commit of the factory definition the run started with. */
86
134
  factoryVersion: CommitHash;
87
135
 
136
+ /** The adapters of the factory's `providers` and the versions the run asks for (ADR 0110). */
137
+ adapters?: AdapterVersion[];
138
+
88
139
  state: RunState;
89
140
 
90
- /** The last pull request link that an agent of the run wrote (ADR 0069). */
141
+ /** The run's pull request: the forge's of the run's branch, the one an agent stated, or else the last link an agent wrote (ADR 0069, ADR 0109). */
91
142
  pullRequest?: url;
92
143
 
93
144
  /** The run's folder on the foreman's machine, where a person who took over a stage goes on (ADR 0070). */
@@ -102,11 +153,18 @@ model RunView {
102
153
 
103
154
  /** The stages of the workflow, in order. */
104
155
  stages: StageView[];
156
+
157
+ /** The signals that named their person, oldest first (ADR 0118). */
158
+ actions?: RunAction[];
105
159
  }
106
160
 
107
161
  /** foreman: the runs of the server, running runs first, then the runs that ended, the one that ended last first, as `GET /v1/runs` answers them (ADR 0071). */
108
162
  model RunList {
109
163
  runs: RunView[];
164
+
165
+ /** The cursor of the next page, while more runs follow (ADR 0115). */
166
+ @minLength(1)
167
+ next?: string;
110
168
  }
111
169
 
112
170
  /** foreman: what a person tells a run. */
@@ -127,4 +185,42 @@ model RunSignal {
127
185
  /** The steering message. Required with "steer". */
128
186
  @minLength(1)
129
187
  message?: string;
188
+
189
+ /** The person who sends the signal; the run then records it in `actions` (ADR 0118). */
190
+ by?: Actor;
191
+ }
192
+
193
+ /** foreman: a signal a person sent a run, as the run records it (ADR 0118). */
194
+ model RunAction {
195
+ type: RunSignalType;
196
+
197
+ /** The person who sent it. */
198
+ by: Actor;
199
+
200
+ /** When the foreman took it. */
201
+ at: utcDateTime;
202
+
203
+ /** The steering message of "steer". */
204
+ @minLength(1)
205
+ message?: string;
206
+ }
207
+
208
+ /**
209
+ * foreman: run a run that failed or was cancelled again, as a new run (ADR 0105). The new run starts at the first stage
210
+ * that did not succeed, or at `from`, a stage at or before it.
211
+ */
212
+ model RunRetry {
213
+ /** The person who asks. */
214
+ by: Actor;
215
+
216
+ from?: Name;
217
+ }
218
+
219
+ /** foreman: a `doctor.ping@v1` event that its consumer received (ADR 0120). */
220
+ model DoctorPingSeen {
221
+ /** The event's ID. */
222
+ id: Identifier;
223
+
224
+ /** When the foreman received it. */
225
+ receivedAt: utcDateTime;
130
226
  }
@@ -36,4 +36,18 @@ namespace GatekeeperApi {
36
36
  @route("/v1/gates/{gateId}")
37
37
  @get
38
38
  op getGate(@path gateId: string): GateView | Failure<401> | Failure<404>;
39
+
40
+ /**
41
+ * A person decides a pending gate (ADR 0118): 403 when they are in no group the gate waits for, 409 when the gate
42
+ * is no longer pending.
43
+ */
44
+ @route("/v1/gates/{gateId}/decisions")
45
+ @post
46
+ op decideGate(@path gateId: string, @body decision: GateDecisionRequest):
47
+ | GateView
48
+ | Failure<400>
49
+ | Failure<401>
50
+ | Failure<403>
51
+ | Failure<404>
52
+ | Failure<409>;
39
53
  }
@@ -132,3 +132,18 @@ model ApiProblem {
132
132
  /** What went wrong this time, for example the field that is missing. */
133
133
  detail?: string;
134
134
  }
135
+
136
+ /**
137
+ * gatekeeper: a person's decision of a pending gate through the API, as a member of a group the gate waits for
138
+ * (ADR 0118). It decides like a reply "approve" or "reject <reason>" in the gate's channel.
139
+ */
140
+ model GateDecisionRequest {
141
+ decision: "approved" | "rejected";
142
+
143
+ /** The person who decides; their `id` must be a member of a group in `waitingFor`. */
144
+ by: Actor;
145
+
146
+ /** The reason of a rejection. */
147
+ @minLength(1)
148
+ reason?: string;
149
+ }
@@ -33,10 +33,17 @@ namespace IntakeApi {
33
33
  | GatekeeperApi.Failure<401>
34
34
  | GatekeeperApi.Failure<404>;
35
35
 
36
- /** The 50 latest work items, the one received last first; with `status`, only the work items in that status. */
36
+ /**
37
+ * A page of the latest work items, the one received last first; with `status`, only the work items in that status.
38
+ * `limit` work items at most, 50 by default; `cursor` is the `next` of the page before (ADR 0115).
39
+ */
37
40
  @route("/v1/inbox")
38
41
  @get
39
- op listWorkItems(@query status?: WorkItemStatus):
42
+ op listWorkItems(
43
+ @query status?: WorkItemStatus,
44
+ @query @minValue(1) @maxValue(500) limit?: int32,
45
+ @query @minLength(1) cursor?: string,
46
+ ):
40
47
  | WorkItemList
41
48
  | GatekeeperApi.Failure<400>
42
49
  | GatekeeperApi.Failure<401>;
@@ -46,6 +46,10 @@ model WorkRequest {
46
46
  /** intake: the latest work items, as `GET /v1/inbox` answers them, the one received last first. */
47
47
  model WorkItemList {
48
48
  workItems: WorkItemView[];
49
+
50
+ /** The cursor of the next page, while more work items follow (ADR 0115). */
51
+ @minLength(1)
52
+ next?: string;
49
53
  }
50
54
 
51
55
  /** intake: a work item as the HTTP API shows it (ADR 0063). */
package/typespec/main.tsp CHANGED
@@ -15,4 +15,5 @@ import "./intake-api.tsp";
15
15
  import "./foreman.tsp";
16
16
  import "./foreman-api.tsp";
17
17
  import "./sandbox.tsp";
18
+ import "./factory-lock.tsp";
18
19
  import "./health.tsp";