@buildinternet/releases-api-types 0.34.0 → 0.35.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 +20 -8
- package/src/schemas/errors.test.ts +55 -0
- package/src/schemas/errors.ts +81 -0
- package/src/schemas/orgs.ts +15 -0
- package/src/schemas/releases.ts +21 -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.35.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>;
|
|
@@ -1236,7 +1234,13 @@ export type UpdatePlaybookNotesResponse = z.infer<typeof UpdatePlaybookNotesResp
|
|
|
1236
1234
|
// ── Overview Manifest (admin planning) ──
|
|
1237
1235
|
|
|
1238
1236
|
export type OverviewStaleness = "missing" | "behind" | "fresh";
|
|
1239
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* `opted_out` — the org has `autoGenerateContent=false`, so the batch-overview
|
|
1239
|
+
* eligibility filter skips it regardless of staleness. Surfaced so an
|
|
1240
|
+
* orchestrator planning a sweep doesn't queue a regen that will never run
|
|
1241
|
+
* (#1795).
|
|
1242
|
+
*/
|
|
1243
|
+
export type OverviewPlanAction = "missing" | "refresh" | "skip" | "opted_out";
|
|
1240
1244
|
|
|
1241
1245
|
/**
|
|
1242
1246
|
* Per-org row returned by GET /v1/admin/overviews. Designed for orchestrators
|
|
@@ -1255,6 +1259,12 @@ export interface OverviewManifestRow {
|
|
|
1255
1259
|
releasesSinceOverview: number;
|
|
1256
1260
|
recentReleaseCount: number;
|
|
1257
1261
|
staleness: OverviewStaleness;
|
|
1262
|
+
/**
|
|
1263
|
+
* Whether the org is opted into automatic AI content. When `false`, the
|
|
1264
|
+
* batch-overview cron/manual regen skips it — the staleness signal alone is
|
|
1265
|
+
* misleading without this (#1795). Older workers may omit it mid-deploy.
|
|
1266
|
+
*/
|
|
1267
|
+
autoGenerateContent?: boolean;
|
|
1258
1268
|
/** Only populated when ?format=plan. */
|
|
1259
1269
|
action?: OverviewPlanAction;
|
|
1260
1270
|
/** Only populated when ?format=plan. */
|
|
@@ -1582,14 +1592,16 @@ export interface Session {
|
|
|
1582
1592
|
* Sub-agent **role** label that ran this session — `"coordinator"` is the
|
|
1583
1593
|
* parent orchestrator (typically Sonnet) when a multi-agent session
|
|
1584
1594
|
* delegates, `"sonnet"` / `"haiku"` are direct single-agent runs.
|
|
1585
|
-
*
|
|
1595
|
+
* `"deterministic"` marks the #1878/#1881 update path that skips the
|
|
1596
|
+
* Managed-Agents (Haiku) session entirely — no Anthropic session runs, so
|
|
1597
|
+
* it must never be reported as `"haiku"`. Surfaces on the detail GET.
|
|
1586
1598
|
*
|
|
1587
1599
|
* This is a logical role label, not the runtime model identifier. The
|
|
1588
|
-
* resolved Anthropic model string (e.g. `claude-sonnet-
|
|
1600
|
+
* resolved Anthropic model string (e.g. `claude-sonnet-5`,
|
|
1589
1601
|
* `claude-haiku-4-5`) lives on `usage.model` when the session reported
|
|
1590
1602
|
* one — consult that field for the concrete model.
|
|
1591
1603
|
*/
|
|
1592
|
-
agent?: "sonnet" | "haiku" | "coordinator";
|
|
1604
|
+
agent?: "sonnet" | "haiku" | "coordinator" | "deterministic";
|
|
1593
1605
|
/** Identifies the client that started this session (e.g. hostname). */
|
|
1594
1606
|
runner?: string;
|
|
1595
1607
|
/** 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
|
/**
|
|
@@ -416,6 +425,12 @@ export const OrgRecentReleaseItemSchema = z.object({
|
|
|
416
425
|
summary: z.string().nullable(),
|
|
417
426
|
titleGenerated: z.string().nullable().optional(),
|
|
418
427
|
titleShort: z.string().nullable().optional(),
|
|
428
|
+
/**
|
|
429
|
+
* Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
430
|
+
* #1710). `.optional()` for mid-deploy tolerance; absent or `"unknown"`
|
|
431
|
+
* means not classified. List paths carry `breaking` only.
|
|
432
|
+
*/
|
|
433
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
419
434
|
url: z.string().nullable(),
|
|
420
435
|
contentHash: z.string().nullable(),
|
|
421
436
|
metadata: z.string().nullable(),
|
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,6 +52,14 @@ 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(),
|
|
56
65
|
media: z.array(MediaItemSchema),
|
|
@@ -215,6 +224,18 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
215
224
|
summary: z.string().nullable(),
|
|
216
225
|
titleGenerated: z.string().nullable(),
|
|
217
226
|
titleShort: z.string().nullable(),
|
|
227
|
+
/**
|
|
228
|
+
* Machine-readable breaking-change level (#1696, surfaced on read paths in
|
|
229
|
+
* #1710). `.optional()` — rows predating the column omit it; absent or
|
|
230
|
+
* `"unknown"` means not classified (the fail-open default). Mirrors
|
|
231
|
+
* `ReleaseDetail.breaking` in api-types.ts.
|
|
232
|
+
*/
|
|
233
|
+
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
234
|
+
/**
|
|
235
|
+
* Explicit upgrade/migration steps lifted from the body (#1696); omitted
|
|
236
|
+
* when the body gives none. Detail-route-only — list paths never carry it.
|
|
237
|
+
*/
|
|
238
|
+
migrationNotes: z.string().nullable().optional(),
|
|
218
239
|
url: z.string().nullable(),
|
|
219
240
|
contentHash: z.string().nullable(),
|
|
220
241
|
media: z.array(MediaItemSchema),
|
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(),
|