@buildinternet/releases-api-types 0.34.1 → 0.36.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 +2 -2
- package/src/api-types.ts +25 -8
- package/src/schemas/errors.test.ts +55 -0
- package/src/schemas/errors.ts +81 -0
- package/src/schemas/orgs.ts +24 -0
- package/src/schemas/releases.ts +33 -0
- package/src/schemas/shared.ts +7 -11
- package/src/schemas/well-known.test.ts +11 -0
- package/src/schemas/well-known.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/releases-api-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@buildinternet/releases-core": "^0.
|
|
20
|
+
"@buildinternet/releases-core": "^0.25.0",
|
|
21
21
|
"zod": "^4.4.3"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
package/src/api-types.ts
CHANGED
|
@@ -18,7 +18,6 @@ import type {
|
|
|
18
18
|
MediaItemSchema,
|
|
19
19
|
PaginationSchema,
|
|
20
20
|
StatsSchema,
|
|
21
|
-
ErrorResponseSchema,
|
|
22
21
|
ReleaseTypeSchema,
|
|
23
22
|
ReleaseCompositionSchema,
|
|
24
23
|
ReleaseItemSchema,
|
|
@@ -258,7 +257,6 @@ export {
|
|
|
258
257
|
PaginationSchema,
|
|
259
258
|
ListResponseSchema,
|
|
260
259
|
StatsSchema,
|
|
261
|
-
ErrorResponseSchema,
|
|
262
260
|
ReleaseTypeSchema,
|
|
263
261
|
ReleaseCompositionSchema,
|
|
264
262
|
ReleaseItemSchema,
|
|
@@ -453,6 +451,8 @@ export {
|
|
|
453
451
|
FeedbackDeleteResponseSchema,
|
|
454
452
|
} from "./schemas/feedback.js";
|
|
455
453
|
export { SitemapSourceSchema, SitemapPayloadSchema } from "./schemas/sitemap.js";
|
|
454
|
+
export { errorEnvelopeSchema, decodeApiError, isApiError } from "./schemas/errors.js";
|
|
455
|
+
export type { ErrorEnvelope, DecodedApiError } from "./schemas/errors.js";
|
|
456
456
|
export {
|
|
457
457
|
RelatedScopeSchema,
|
|
458
458
|
RelatedReleaseThumbnailSchema,
|
|
@@ -554,8 +554,6 @@ export interface CreateUserApiKeyBody {
|
|
|
554
554
|
expiresInDays?: number;
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
-
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
558
|
-
|
|
559
557
|
// ── Sitemap (bulk URL emission) ──
|
|
560
558
|
|
|
561
559
|
export type SitemapSource = z.infer<typeof SitemapSourceSchema>;
|
|
@@ -820,6 +818,11 @@ export interface ReleaseDetail {
|
|
|
820
818
|
/** Explicit upgrade/migration steps lifted from the body (#1696); null when the body gives none. */
|
|
821
819
|
migrationNotes?: string | null;
|
|
822
820
|
url: string | null;
|
|
821
|
+
/**
|
|
822
|
+
* Canonical releases.sh detail-page URL (friendly form: ID + current
|
|
823
|
+
* title slug). Additive — older servers omit it.
|
|
824
|
+
*/
|
|
825
|
+
webUrl?: string;
|
|
823
826
|
media: MediaItem[];
|
|
824
827
|
publishedAt: string | null;
|
|
825
828
|
fetchedAt: string;
|
|
@@ -1236,7 +1239,13 @@ export type UpdatePlaybookNotesResponse = z.infer<typeof UpdatePlaybookNotesResp
|
|
|
1236
1239
|
// ── Overview Manifest (admin planning) ──
|
|
1237
1240
|
|
|
1238
1241
|
export type OverviewStaleness = "missing" | "behind" | "fresh";
|
|
1239
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* `opted_out` — the org has `autoGenerateContent=false`, so the overview-regen
|
|
1244
|
+
* eligibility filter skips it regardless of staleness. Surfaced so an
|
|
1245
|
+
* orchestrator planning a sweep doesn't queue a regen that will never run
|
|
1246
|
+
* (#1795).
|
|
1247
|
+
*/
|
|
1248
|
+
export type OverviewPlanAction = "missing" | "refresh" | "skip" | "opted_out";
|
|
1240
1249
|
|
|
1241
1250
|
/**
|
|
1242
1251
|
* Per-org row returned by GET /v1/admin/overviews. Designed for orchestrators
|
|
@@ -1255,6 +1264,12 @@ export interface OverviewManifestRow {
|
|
|
1255
1264
|
releasesSinceOverview: number;
|
|
1256
1265
|
recentReleaseCount: number;
|
|
1257
1266
|
staleness: OverviewStaleness;
|
|
1267
|
+
/**
|
|
1268
|
+
* Whether the org is opted into automatic AI content. When `false`, the
|
|
1269
|
+
* overview-regen cron skips it — the staleness signal alone is
|
|
1270
|
+
* misleading without this (#1795). Older workers may omit it mid-deploy.
|
|
1271
|
+
*/
|
|
1272
|
+
autoGenerateContent?: boolean;
|
|
1258
1273
|
/** Only populated when ?format=plan. */
|
|
1259
1274
|
action?: OverviewPlanAction;
|
|
1260
1275
|
/** Only populated when ?format=plan. */
|
|
@@ -1582,14 +1597,16 @@ export interface Session {
|
|
|
1582
1597
|
* Sub-agent **role** label that ran this session — `"coordinator"` is the
|
|
1583
1598
|
* parent orchestrator (typically Sonnet) when a multi-agent session
|
|
1584
1599
|
* delegates, `"sonnet"` / `"haiku"` are direct single-agent runs.
|
|
1585
|
-
*
|
|
1600
|
+
* `"deterministic"` marks the #1878/#1881 update path that skips the
|
|
1601
|
+
* Managed-Agents (Haiku) session entirely — no Anthropic session runs, so
|
|
1602
|
+
* it must never be reported as `"haiku"`. Surfaces on the detail GET.
|
|
1586
1603
|
*
|
|
1587
1604
|
* This is a logical role label, not the runtime model identifier. The
|
|
1588
|
-
* resolved Anthropic model string (e.g. `claude-sonnet-
|
|
1605
|
+
* resolved Anthropic model string (e.g. `claude-sonnet-5`,
|
|
1589
1606
|
* `claude-haiku-4-5`) lives on `usage.model` when the session reported
|
|
1590
1607
|
* one — consult that field for the concrete model.
|
|
1591
1608
|
*/
|
|
1592
|
-
agent?: "sonnet" | "haiku" | "coordinator";
|
|
1609
|
+
agent?: "sonnet" | "haiku" | "coordinator" | "deterministic";
|
|
1593
1610
|
/** Identifies the client that started this session (e.g. hostname). */
|
|
1594
1611
|
runner?: string;
|
|
1595
1612
|
/** Correlation ID for end-to-end tracing across CLI → API → managed agent. */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { ERROR_TYPES } from "@buildinternet/releases-core/errors";
|
|
3
|
+
import { errorEnvelopeSchema, decodeApiError, isApiError } from "./errors";
|
|
4
|
+
|
|
5
|
+
test("a valid envelope parses", () => {
|
|
6
|
+
const ok = errorEnvelopeSchema.safeParse({
|
|
7
|
+
error: { code: "not_found", type: "not_found", message: "Nope" },
|
|
8
|
+
});
|
|
9
|
+
expect(ok.success).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("malformed envelopes reject", () => {
|
|
13
|
+
expect(errorEnvelopeSchema.safeParse({}).success).toBe(false);
|
|
14
|
+
expect(
|
|
15
|
+
errorEnvelopeSchema.safeParse({
|
|
16
|
+
error: { code: 1, type: "not_found", message: "x" },
|
|
17
|
+
}).success,
|
|
18
|
+
).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("schema type enum matches the core union (anti-drift invariant 2)", () => {
|
|
22
|
+
const options = errorEnvelopeSchema.shape.error.shape.type.options;
|
|
23
|
+
expect([...options].sort()).toEqual([...ERROR_TYPES].sort());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("decodeApiError normalizes an unknown type to internal, preserves unknown code", () => {
|
|
27
|
+
const decoded = decodeApiError({
|
|
28
|
+
error: { code: "brand_new_code", type: "teapot", message: "?" },
|
|
29
|
+
});
|
|
30
|
+
expect(decoded.type).toBe("internal");
|
|
31
|
+
expect(decoded.code).toBe("brand_new_code");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("decodeApiError degrades a malformed body without throwing", () => {
|
|
35
|
+
expect(decodeApiError(null)).toEqual({
|
|
36
|
+
code: "internal_error",
|
|
37
|
+
type: "internal",
|
|
38
|
+
message: "Unknown error",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("decodeApiError carries details through when present", () => {
|
|
43
|
+
const decoded = decodeApiError({
|
|
44
|
+
error: { code: "not_found", type: "not_found", message: "x", details: { id: "src_1" } },
|
|
45
|
+
});
|
|
46
|
+
expect(decoded.details).toEqual({ id: "src_1" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("isApiError matches shape and optional code", () => {
|
|
50
|
+
const body = { error: { code: "not_found", type: "not_found", message: "x" } };
|
|
51
|
+
expect(isApiError(body)).toBe(true);
|
|
52
|
+
expect(isApiError(body, "not_found")).toBe(true);
|
|
53
|
+
expect(isApiError(body, "conflict")).toBe(false);
|
|
54
|
+
expect(isApiError({ nope: true })).toBe(false);
|
|
55
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ERROR_TYPES, type ErrorType, type ErrorCode } from "@buildinternet/releases-core/errors";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one on-the-wire error shape. `type` is constrained (documents the
|
|
6
|
+
* contract, powers OpenAPI); `code` is an open string so a new server code an
|
|
7
|
+
* older client doesn't recognize still parses. The throwable hierarchy in
|
|
8
|
+
* `@releases/lib/releases-error` serializes to this via `toWire()`.
|
|
9
|
+
*/
|
|
10
|
+
export const errorEnvelopeSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
error: z.object({
|
|
13
|
+
code: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(
|
|
16
|
+
"Stable, specific machine code (e.g. `not_found`, `rate_limited`). Never reworded once shipped, but open-ended — branch defensively on codes you don't recognize.",
|
|
17
|
+
),
|
|
18
|
+
type: z
|
|
19
|
+
.enum(ERROR_TYPES)
|
|
20
|
+
.describe(
|
|
21
|
+
"Coarse category from a fixed set; determines the HTTP status. Safe to switch on.",
|
|
22
|
+
),
|
|
23
|
+
message: z.string().describe("Human-readable explanation. May change; do not parse."),
|
|
24
|
+
details: z
|
|
25
|
+
.unknown()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(
|
|
28
|
+
"Optional structured context carried by select codes (e.g. `database_not_initialized`).",
|
|
29
|
+
),
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
.describe(
|
|
33
|
+
"Standardized error response — every non-2xx response uses this shape. Switch on `error.type` (fixed set → HTTP status) or match `error.code` (specific, stable). See https://releases.sh/docs/api/errors.",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export type ErrorEnvelope = z.infer<typeof errorEnvelopeSchema>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lenient variant used ONLY for decode: `type` is left open so an unknown
|
|
40
|
+
* category (older client, newer server) does not reject.
|
|
41
|
+
*/
|
|
42
|
+
const lenientEnvelopeSchema = z.object({
|
|
43
|
+
error: z.object({
|
|
44
|
+
code: z.string(),
|
|
45
|
+
type: z.string(),
|
|
46
|
+
message: z.string(),
|
|
47
|
+
details: z.unknown().optional(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export interface DecodedApiError {
|
|
52
|
+
code: string;
|
|
53
|
+
type: ErrorType;
|
|
54
|
+
message: string;
|
|
55
|
+
details?: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse an API error body into a typed, normalized shape. Forward-compatible: a
|
|
60
|
+
* malformed body or an unknown `type` degrades to `internal` and never throws.
|
|
61
|
+
*/
|
|
62
|
+
export function decodeApiError(body: unknown): DecodedApiError {
|
|
63
|
+
const parsed = lenientEnvelopeSchema.safeParse(body);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
return { code: "internal_error", type: "internal", message: "Unknown error" };
|
|
66
|
+
}
|
|
67
|
+
const { code, type, message, details } = parsed.data.error;
|
|
68
|
+
const normalizedType: ErrorType = (ERROR_TYPES as readonly string[]).includes(type)
|
|
69
|
+
? (type as ErrorType)
|
|
70
|
+
: "internal";
|
|
71
|
+
return details === undefined
|
|
72
|
+
? { code, type: normalizedType, message }
|
|
73
|
+
: { code, type: normalizedType, message, details };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True if `body` is an API error envelope; if `code` is given, also matches it. */
|
|
77
|
+
export function isApiError(body: unknown, code?: ErrorCode): boolean {
|
|
78
|
+
const parsed = lenientEnvelopeSchema.safeParse(body);
|
|
79
|
+
if (!parsed.success) return false;
|
|
80
|
+
return code === undefined || parsed.data.error.code === code;
|
|
81
|
+
}
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
3
|
+
import { BREAKING_LEVELS } from "@buildinternet/releases-core/breaking";
|
|
3
4
|
import {
|
|
4
5
|
AppStoreSourceInfoSchema,
|
|
5
6
|
CategorySchema,
|
|
@@ -89,6 +90,14 @@ export const CreateOrgBodySchema = z.object({
|
|
|
89
90
|
description: z.string().optional(),
|
|
90
91
|
category: z.string().min(1).optional(),
|
|
91
92
|
tags: z.array(z.string()).optional(),
|
|
93
|
+
/**
|
|
94
|
+
* Admin-only: opt the new org into automatic AI content (overviews +
|
|
95
|
+
* per-release summaries). Omitted → defaults to `true` server-side, since
|
|
96
|
+
* `POST /v1/orgs` only ever mints `curated` orgs and onboarding into the
|
|
97
|
+
* registry should mean it gets overviews. Pass `false` for a deliberate
|
|
98
|
+
* opt-out (#1795).
|
|
99
|
+
*/
|
|
100
|
+
autoGenerateContent: z.boolean().optional(),
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
/**
|
|
@@ -111,6 +120,13 @@ export const UpdateOrgBodySchema = z.object({
|
|
|
111
120
|
isHidden: z.boolean().optional(),
|
|
112
121
|
/** Admin-only: opt the org into automatic AI content — org overviews AND per-release summaries (single backend flag `auto_generate_content`). */
|
|
113
122
|
autoGenerateContent: z.boolean().optional(),
|
|
123
|
+
/**
|
|
124
|
+
* Admin-only: manual override for the automated overview-regen cadence in
|
|
125
|
+
* days (#1895). `null` clears back to automatic (velocity-tiered: 7d
|
|
126
|
+
* default, 2d at high release velocity). A set value pins the cadence in
|
|
127
|
+
* either direction — faster for a hot org, slower for a bursty one.
|
|
128
|
+
*/
|
|
129
|
+
overviewCadenceDays: z.number().int().min(1).max(90).nullable().optional(),
|
|
114
130
|
/** Admin-only: promote this org on the home-page featured rail. */
|
|
115
131
|
featured: z.boolean().optional(),
|
|
116
132
|
/** Admin-only: promote or demote the org's discovery status (curated/agent/on_demand). */
|
|
@@ -416,6 +432,12 @@ export const OrgRecentReleaseItemSchema = z.object({
|
|
|
416
432
|
summary: z.string().nullable(),
|
|
417
433
|
titleGenerated: z.string().nullable().optional(),
|
|
418
434
|
titleShort: z.string().nullable().optional(),
|
|
435
|
+
/**
|
|
436
|
+
* Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
437
|
+
* #1710). `.optional()` for mid-deploy tolerance; absent or `"unknown"`
|
|
438
|
+
* means not classified. List paths carry `breaking` only.
|
|
439
|
+
*/
|
|
440
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
419
441
|
url: z.string().nullable(),
|
|
420
442
|
contentHash: z.string().nullable(),
|
|
421
443
|
metadata: z.string().nullable(),
|
|
@@ -450,6 +472,8 @@ export const OrgDetailSchema = z.object({
|
|
|
450
472
|
isHidden: z.boolean().optional(),
|
|
451
473
|
/** Admin display flag: org is opted into automatic overviews + per-release summaries. Optional on the wire for older workers mid-deploy. */
|
|
452
474
|
autoGenerateContent: z.boolean().optional(),
|
|
475
|
+
/** Manual overview-regen cadence override in days (#1895); null = automatic velocity tier. Optional on the wire for older workers mid-deploy. */
|
|
476
|
+
overviewCadenceDays: z.number().int().nullable().optional(),
|
|
453
477
|
/** Admin display flag: org is promoted on the home-page featured rail. Optional on the wire for older workers mid-deploy. */
|
|
454
478
|
featured: z.boolean().optional(),
|
|
455
479
|
/** Admin display flag: all ingest paused for this org. */
|
package/src/schemas/releases.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SOURCE_TYPES } from "@buildinternet/releases-core/source-enums";
|
|
3
|
+
import { BREAKING_LEVELS } from "@buildinternet/releases-core/breaking";
|
|
3
4
|
import {
|
|
4
5
|
AppStoreSourceInfoSchema,
|
|
5
6
|
MediaItemSchema,
|
|
@@ -51,8 +52,21 @@ export const ReleaseLatestItemSchema = z.object({
|
|
|
51
52
|
summary: z.string().nullable(),
|
|
52
53
|
titleGenerated: z.string().nullable(),
|
|
53
54
|
titleShort: z.string().nullable(),
|
|
55
|
+
/**
|
|
56
|
+
* Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
57
|
+
* #1710). `.optional()` for mid-deploy / pinned-worker tolerance and because
|
|
58
|
+
* rows predating the column omit it; absent or `"unknown"` means not
|
|
59
|
+
* classified (the fail-open default). List paths carry `breaking` only —
|
|
60
|
+
* `migrationNotes` stays detail-route-only to keep list payloads slim.
|
|
61
|
+
*/
|
|
62
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
54
63
|
publishedAt: z.string().nullable(),
|
|
55
64
|
url: z.string().nullable(),
|
|
65
|
+
/**
|
|
66
|
+
* Canonical releases.sh detail-page URL (friendly form: ID + current
|
|
67
|
+
* title slug). Additive — older servers and the digest-cron path omit it.
|
|
68
|
+
*/
|
|
69
|
+
webUrl: z.string().optional(),
|
|
56
70
|
media: z.array(MediaItemSchema),
|
|
57
71
|
source: ReleaseLatestSourceSchema,
|
|
58
72
|
/**
|
|
@@ -215,7 +229,26 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
215
229
|
summary: z.string().nullable(),
|
|
216
230
|
titleGenerated: z.string().nullable(),
|
|
217
231
|
titleShort: z.string().nullable(),
|
|
232
|
+
/**
|
|
233
|
+
* Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
234
|
+
* #1710). `.optional()` — rows predating the column omit it; absent or
|
|
235
|
+
* `"unknown"` means not classified (the fail-open default). Mirrors
|
|
236
|
+
* `ReleaseDetail.breaking` in api-types.ts.
|
|
237
|
+
*/
|
|
238
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
239
|
+
/**
|
|
240
|
+
* Explicit upgrade/migration steps lifted from the body (#1696); omitted
|
|
241
|
+
* when the body gives none. Detail-route-only — list paths never carry it.
|
|
242
|
+
*/
|
|
243
|
+
migrationNotes: z.string().nullable().optional(),
|
|
218
244
|
url: z.string().nullable(),
|
|
245
|
+
/**
|
|
246
|
+
* Canonical releases.sh detail-page URL, `https://releases.sh/release/
|
|
247
|
+
* rel_<id>-<slug>` (Zendesk-style: immutable ID + current title slug;
|
|
248
|
+
* the slug follows title regeneration, the ID keeps old links alive).
|
|
249
|
+
* Additive — older servers omit it.
|
|
250
|
+
*/
|
|
251
|
+
webUrl: z.string().optional(),
|
|
219
252
|
contentHash: z.string().nullable(),
|
|
220
253
|
media: z.array(MediaItemSchema),
|
|
221
254
|
publishedAt: z.string().nullable(),
|
package/src/schemas/shared.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { RELEASE_TYPES } from "@buildinternet/releases-core/schema";
|
|
3
|
+
import { BREAKING_LEVELS } from "@buildinternet/releases-core/breaking";
|
|
3
4
|
import { CATEGORIES } from "@buildinternet/releases-core/categories";
|
|
4
5
|
import { isValidNoticeCoordinate } from "@buildinternet/releases-core/notice";
|
|
5
6
|
|
|
@@ -58,17 +59,6 @@ export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
|
|
|
58
59
|
pagination: PaginationSchema,
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
export const ErrorResponseSchema = z.object({
|
|
62
|
-
error: z.string(),
|
|
63
|
-
message: z.string(),
|
|
64
|
-
// Stable D1 error classification from `classifyDbError()` in
|
|
65
|
-
// `@releases/lib/db-errors`. Set on 5xx responses that originate from a
|
|
66
|
-
// classified D1 failure so callers can branch on a stable code instead of
|
|
67
|
-
// substring-matching the upstream message. Absent on non-D1 errors and on
|
|
68
|
-
// routes that have not yet been wired. See DbErrorCode in packages/lib.
|
|
69
|
-
errorCode: z.string().optional(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
62
|
export const StatsSchema = z.object({
|
|
73
63
|
orgs: z.number().int().min(0),
|
|
74
64
|
sources: z.number().int().min(0),
|
|
@@ -120,6 +110,12 @@ export const ReleaseItemSchema = z.object({
|
|
|
120
110
|
// `.optional()` for the same mid-deploy / pinned-worker reason as `type`.
|
|
121
111
|
titleGenerated: z.string().nullable().optional(),
|
|
122
112
|
titleShort: z.string().nullable().optional(),
|
|
113
|
+
// Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
114
|
+
// #1710). `.optional()` for mid-deploy / pinned-worker tolerance and because
|
|
115
|
+
// rows predating the column omit it; absent or `"unknown"` means not
|
|
116
|
+
// classified (the fail-open default). List paths carry `breaking` only —
|
|
117
|
+
// `migrationNotes` stays detail-route-only to keep list payloads slim.
|
|
118
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
123
119
|
// Count of demoted siblings rolling up into this row via `release_coverage`
|
|
124
120
|
// (0 when standalone). Lets list views show a cluster indicator without a
|
|
125
121
|
// detail-page round trip. `.optional()` — older API responses and pinned
|
|
@@ -9,6 +9,7 @@ describe("ReleasesJsonConfigSchema", () => {
|
|
|
9
9
|
it("accepts a full org-scope file", () => {
|
|
10
10
|
const r = ReleasesJsonConfigSchema.safeParse({
|
|
11
11
|
$schema: "https://releases.sh/schemas/releases.json",
|
|
12
|
+
version: 1,
|
|
12
13
|
name: "Acme",
|
|
13
14
|
description: "CI for teams.",
|
|
14
15
|
category: "developer-tools",
|
|
@@ -48,6 +49,16 @@ describe("ReleasesJsonConfigSchema", () => {
|
|
|
48
49
|
expect(r.success).toBe(false);
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it("accepts an integer version", () => {
|
|
53
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: 1, name: "Acme" }).success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects a zero, negative, or non-integer version", () => {
|
|
57
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: 0 }).success).toBe(false);
|
|
58
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: -1 }).success).toBe(false);
|
|
59
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: 1.5 }).success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
51
62
|
it("strips unknown top-level keys", () => {
|
|
52
63
|
const r = ReleasesJsonConfigSchema.parse({ name: "Acme", bogus: 1 });
|
|
53
64
|
expect("bogus" in r).toBe(false);
|
|
@@ -26,6 +26,11 @@ export const ReleasesJsonProductSchema = z
|
|
|
26
26
|
export const ReleasesJsonConfigSchema = z
|
|
27
27
|
.object({
|
|
28
28
|
$schema: z.url().optional(),
|
|
29
|
+
// Manifest format version. Currently 1; optional — a missing value is read as
|
|
30
|
+
// v1. Bumped only on a breaking change to this file's shape, so a consumer can
|
|
31
|
+
// branch on it without fetching the JSON Schema. Lenient/fail-open: the
|
|
32
|
+
// reconciler ignores it entirely, so an unexpected value never fails the sync.
|
|
33
|
+
version: z.number().int().positive().max(1000).optional(),
|
|
29
34
|
// Org scope
|
|
30
35
|
name: z.string().min(1).max(120).optional(),
|
|
31
36
|
description: z.string().max(2000).optional(),
|