@buildinternet/releases-api-types 0.35.0 → 0.37.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 +1 -1
- package/src/api-types.ts +27 -3
- package/src/schemas/orgs.ts +9 -0
- package/src/schemas/releases.ts +12 -0
- package/src/schemas/well-known.test.ts +134 -37
- package/src/schemas/well-known.ts +152 -34
package/package.json
CHANGED
package/src/api-types.ts
CHANGED
|
@@ -309,10 +309,23 @@ export {
|
|
|
309
309
|
} from "./schemas/account-profile.js";
|
|
310
310
|
export {
|
|
311
311
|
ReleasesJsonConfigSchema,
|
|
312
|
+
ReleasesJsonDomainReleaseSchema,
|
|
313
|
+
ReleasesJsonDomainSchema,
|
|
312
314
|
ReleasesJsonProductSchema,
|
|
315
|
+
ReleasesJsonRegistriesSchema,
|
|
316
|
+
ReleasesJsonRepoProductSchema,
|
|
317
|
+
ReleasesJsonRepoReleaseSchema,
|
|
318
|
+
ReleasesJsonRepoSchema,
|
|
313
319
|
SyncWellKnownResponseSchema,
|
|
314
320
|
} from "./schemas/well-known.js";
|
|
315
|
-
export type {
|
|
321
|
+
export type {
|
|
322
|
+
ReleasesJsonConfig,
|
|
323
|
+
ReleasesJsonDomain,
|
|
324
|
+
ReleasesJsonDomainRelease,
|
|
325
|
+
ReleasesJsonProduct,
|
|
326
|
+
ReleasesJsonRepo,
|
|
327
|
+
ReleasesJsonRepoRelease,
|
|
328
|
+
} from "./schemas/well-known.js";
|
|
316
329
|
export {
|
|
317
330
|
SourceListItemSchema,
|
|
318
331
|
SourceWithOrgSchema,
|
|
@@ -766,6 +779,12 @@ export interface WhatsChangedEntry {
|
|
|
766
779
|
breaking: BreakingLevel;
|
|
767
780
|
migrationNotes: string | null;
|
|
768
781
|
url: string | null;
|
|
782
|
+
/**
|
|
783
|
+
* Absolute canonical web URL — the slugged `/release/<id>-<slug>` form
|
|
784
|
+
* (#1906), built from `WEB_BASE_URL`. Distinct from `url` (the upstream
|
|
785
|
+
* source URL); lets an upgrade-planning agent link the release detail page.
|
|
786
|
+
*/
|
|
787
|
+
webUrl: string | null;
|
|
769
788
|
}
|
|
770
789
|
|
|
771
790
|
/**
|
|
@@ -818,6 +837,11 @@ export interface ReleaseDetail {
|
|
|
818
837
|
/** Explicit upgrade/migration steps lifted from the body (#1696); null when the body gives none. */
|
|
819
838
|
migrationNotes?: string | null;
|
|
820
839
|
url: string | null;
|
|
840
|
+
/**
|
|
841
|
+
* Canonical releases.sh detail-page URL (friendly form: ID + current
|
|
842
|
+
* title slug). Additive — older servers omit it.
|
|
843
|
+
*/
|
|
844
|
+
webUrl?: string;
|
|
821
845
|
media: MediaItem[];
|
|
822
846
|
publishedAt: string | null;
|
|
823
847
|
fetchedAt: string;
|
|
@@ -1235,7 +1259,7 @@ export type UpdatePlaybookNotesResponse = z.infer<typeof UpdatePlaybookNotesResp
|
|
|
1235
1259
|
|
|
1236
1260
|
export type OverviewStaleness = "missing" | "behind" | "fresh";
|
|
1237
1261
|
/**
|
|
1238
|
-
* `opted_out` — the org has `autoGenerateContent=false`, so the
|
|
1262
|
+
* `opted_out` — the org has `autoGenerateContent=false`, so the overview-regen
|
|
1239
1263
|
* eligibility filter skips it regardless of staleness. Surfaced so an
|
|
1240
1264
|
* orchestrator planning a sweep doesn't queue a regen that will never run
|
|
1241
1265
|
* (#1795).
|
|
@@ -1261,7 +1285,7 @@ export interface OverviewManifestRow {
|
|
|
1261
1285
|
staleness: OverviewStaleness;
|
|
1262
1286
|
/**
|
|
1263
1287
|
* Whether the org is opted into automatic AI content. When `false`, the
|
|
1264
|
-
*
|
|
1288
|
+
* overview-regen cron skips it — the staleness signal alone is
|
|
1265
1289
|
* misleading without this (#1795). Older workers may omit it mid-deploy.
|
|
1266
1290
|
*/
|
|
1267
1291
|
autoGenerateContent?: boolean;
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -120,6 +120,13 @@ export const UpdateOrgBodySchema = z.object({
|
|
|
120
120
|
isHidden: z.boolean().optional(),
|
|
121
121
|
/** Admin-only: opt the org into automatic AI content — org overviews AND per-release summaries (single backend flag `auto_generate_content`). */
|
|
122
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(),
|
|
123
130
|
/** Admin-only: promote this org on the home-page featured rail. */
|
|
124
131
|
featured: z.boolean().optional(),
|
|
125
132
|
/** Admin-only: promote or demote the org's discovery status (curated/agent/on_demand). */
|
|
@@ -465,6 +472,8 @@ export const OrgDetailSchema = z.object({
|
|
|
465
472
|
isHidden: z.boolean().optional(),
|
|
466
473
|
/** Admin display flag: org is opted into automatic overviews + per-release summaries. Optional on the wire for older workers mid-deploy. */
|
|
467
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(),
|
|
468
477
|
/** Admin display flag: org is promoted on the home-page featured rail. Optional on the wire for older workers mid-deploy. */
|
|
469
478
|
featured: z.boolean().optional(),
|
|
470
479
|
/** Admin display flag: all ingest paused for this org. */
|
package/src/schemas/releases.ts
CHANGED
|
@@ -62,6 +62,11 @@ export const ReleaseLatestItemSchema = z.object({
|
|
|
62
62
|
breaking: z.enum(BREAKING_LEVELS).optional(),
|
|
63
63
|
publishedAt: z.string().nullable(),
|
|
64
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(),
|
|
65
70
|
media: z.array(MediaItemSchema),
|
|
66
71
|
source: ReleaseLatestSourceSchema,
|
|
67
72
|
/**
|
|
@@ -237,6 +242,13 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
237
242
|
*/
|
|
238
243
|
migrationNotes: z.string().nullable().optional(),
|
|
239
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(),
|
|
240
252
|
contentHash: z.string().nullable(),
|
|
241
253
|
media: z.array(MediaItemSchema),
|
|
242
254
|
publishedAt: z.string().nullable(),
|
|
@@ -1,66 +1,163 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
ReleasesJsonConfigSchema,
|
|
4
|
+
ReleasesJsonDomainSchema,
|
|
5
|
+
ReleasesJsonRepoSchema,
|
|
6
|
+
} from "./well-known.js";
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
const location = (n: number) => ({ url: `https://acme.com/releases/${n}` });
|
|
9
|
+
|
|
10
|
+
describe("releases.json v2 schemas", () => {
|
|
11
|
+
it("accepts the minimal domain manifest", () => {
|
|
12
|
+
expect(
|
|
13
|
+
ReleasesJsonDomainSchema.parse({
|
|
14
|
+
version: 2,
|
|
15
|
+
releases: [{ url: "https://acme.com/updates", feed: "https://acme.com/updates.xml" }],
|
|
16
|
+
}),
|
|
17
|
+
).toEqual({
|
|
18
|
+
version: 2,
|
|
19
|
+
releases: [{ url: "https://acme.com/updates", feed: "https://acme.com/updates.xml" }],
|
|
20
|
+
});
|
|
7
21
|
});
|
|
8
22
|
|
|
9
|
-
it("accepts
|
|
10
|
-
const
|
|
23
|
+
it("accepts the full domain shape with advisory taxonomy", () => {
|
|
24
|
+
const parsed = ReleasesJsonDomainSchema.parse({
|
|
11
25
|
$schema: "https://releases.sh/schemas/releases.json",
|
|
12
|
-
version:
|
|
26
|
+
version: 2,
|
|
13
27
|
name: "Acme",
|
|
14
28
|
description: "CI for teams.",
|
|
15
|
-
category: "
|
|
29
|
+
category: "future-category",
|
|
16
30
|
avatar: "https://acme.com/logo.png",
|
|
17
|
-
tags: ["
|
|
31
|
+
tags: ["future-tag"],
|
|
18
32
|
social: { twitter: "acmehq", github: "acme" },
|
|
19
|
-
|
|
33
|
+
products: [
|
|
34
|
+
{
|
|
35
|
+
name: "Acme Cloud",
|
|
36
|
+
slug: "acme-cloud",
|
|
37
|
+
kind: "future-kind",
|
|
38
|
+
category: "future-category",
|
|
39
|
+
description: "Managed CI runners.",
|
|
40
|
+
website: "https://acme.com/cloud",
|
|
41
|
+
docs: "https://docs.acme.com/cloud",
|
|
42
|
+
support: "https://acme.com/support",
|
|
43
|
+
social: { twitter: "acmecloud" },
|
|
44
|
+
archived: true,
|
|
45
|
+
releases: [{ github: "acme/cloud", canonical: true }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
registries: {
|
|
49
|
+
"releases.sh": {
|
|
50
|
+
org: "org_abc123",
|
|
51
|
+
verification: "verification-token",
|
|
52
|
+
futureKey: true,
|
|
53
|
+
},
|
|
54
|
+
"example.com": { anything: "goes" },
|
|
55
|
+
},
|
|
20
56
|
});
|
|
21
|
-
|
|
57
|
+
|
|
58
|
+
expect(parsed.products?.[0]?.kind).toBe("future-kind");
|
|
59
|
+
expect(parsed.registries?.["example.com"]).toEqual({ anything: "goes" });
|
|
22
60
|
});
|
|
23
61
|
|
|
24
|
-
it("accepts
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
it("accepts the repo variant including github self", () => {
|
|
63
|
+
expect(
|
|
64
|
+
ReleasesJsonRepoSchema.safeParse({
|
|
65
|
+
version: 2,
|
|
66
|
+
product: { name: "Acme Cloud", slug: "acme-cloud" },
|
|
67
|
+
releases: [{ github: "self", canonical: true }],
|
|
68
|
+
registries: { "releases.sh": { product: "prd_abc123" } },
|
|
69
|
+
}).success,
|
|
70
|
+
).toBe(true);
|
|
29
71
|
});
|
|
30
72
|
|
|
31
|
-
it("
|
|
32
|
-
expect(ReleasesJsonConfigSchema.safeParse({
|
|
33
|
-
|
|
34
|
-
);
|
|
73
|
+
it("requires the version 2 literal with no v1 compatibility", () => {
|
|
74
|
+
expect(ReleasesJsonConfigSchema.safeParse({ releases: [location(1)] }).success).toBe(false);
|
|
75
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: 1 }).success).toBe(false);
|
|
76
|
+
expect(ReleasesJsonConfigSchema.safeParse({ version: 3 }).success).toBe(false);
|
|
35
77
|
});
|
|
36
78
|
|
|
37
|
-
it("
|
|
38
|
-
expect(
|
|
79
|
+
it("requires at least one locator per release location", () => {
|
|
80
|
+
expect(
|
|
81
|
+
ReleasesJsonDomainSchema.safeParse({ version: 2, releases: [{ title: "Updates" }] }).success,
|
|
82
|
+
).toBe(false);
|
|
39
83
|
});
|
|
40
84
|
|
|
41
|
-
it("
|
|
85
|
+
it("allows github self only in repo files", () => {
|
|
42
86
|
expect(
|
|
43
|
-
|
|
87
|
+
ReleasesJsonDomainSchema.safeParse({ version: 2, releases: [{ github: "self" }] }).success,
|
|
44
88
|
).toBe(false);
|
|
89
|
+
expect(
|
|
90
|
+
ReleasesJsonRepoSchema.safeParse({ version: 2, releases: [{ github: "self" }] }).success,
|
|
91
|
+
).toBe(true);
|
|
45
92
|
});
|
|
46
93
|
|
|
47
|
-
it("
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
it("enforces product and release-location caps", () => {
|
|
95
|
+
expect(
|
|
96
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
97
|
+
version: 2,
|
|
98
|
+
products: Array.from({ length: 25 }, (_, i) => ({ name: `Product ${i}` })),
|
|
99
|
+
}).success,
|
|
100
|
+
).toBe(false);
|
|
101
|
+
expect(
|
|
102
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
103
|
+
version: 2,
|
|
104
|
+
products: [{ name: "Cloud", releases: Array.from({ length: 9 }, (_, i) => location(i)) }],
|
|
105
|
+
}).success,
|
|
106
|
+
).toBe(false);
|
|
107
|
+
expect(
|
|
108
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
109
|
+
version: 2,
|
|
110
|
+
releases: Array.from({ length: 33 }, (_, i) => location(i)),
|
|
111
|
+
}).success,
|
|
112
|
+
).toBe(false);
|
|
113
|
+
expect(
|
|
114
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
115
|
+
version: 2,
|
|
116
|
+
releases: Array.from({ length: 25 }, (_, i) => location(i)),
|
|
117
|
+
products: [
|
|
118
|
+
{ name: "Cloud", releases: Array.from({ length: 8 }, (_, i) => location(i + 25)) },
|
|
119
|
+
],
|
|
120
|
+
}).success,
|
|
121
|
+
).toBe(false);
|
|
50
122
|
});
|
|
51
123
|
|
|
52
|
-
it("
|
|
53
|
-
|
|
124
|
+
it("allows at most one canonical location per scope", () => {
|
|
125
|
+
const canonical = (n: number) => ({ ...location(n), canonical: true });
|
|
126
|
+
expect(
|
|
127
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
128
|
+
version: 2,
|
|
129
|
+
releases: [canonical(1), canonical(2)],
|
|
130
|
+
}).success,
|
|
131
|
+
).toBe(false);
|
|
132
|
+
expect(
|
|
133
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
134
|
+
version: 2,
|
|
135
|
+
products: [{ name: "Cloud", releases: [canonical(1), canonical(2)] }],
|
|
136
|
+
}).success,
|
|
137
|
+
).toBe(false);
|
|
54
138
|
});
|
|
55
139
|
|
|
56
|
-
it("rejects
|
|
57
|
-
expect(
|
|
58
|
-
|
|
59
|
-
|
|
140
|
+
it("rejects removed v1 identity fields", () => {
|
|
141
|
+
expect(
|
|
142
|
+
ReleasesJsonDomainSchema.safeParse({ version: 2, website: "https://acme.com" }).success,
|
|
143
|
+
).toBe(false);
|
|
144
|
+
expect(
|
|
145
|
+
ReleasesJsonDomainSchema.safeParse({ version: 2, notice: { message: "Moved" } }).success,
|
|
146
|
+
).toBe(false);
|
|
60
147
|
});
|
|
61
148
|
|
|
62
|
-
it("
|
|
63
|
-
|
|
64
|
-
|
|
149
|
+
it("requires typed stable ids for the releases.sh registry", () => {
|
|
150
|
+
expect(
|
|
151
|
+
ReleasesJsonDomainSchema.safeParse({
|
|
152
|
+
version: 2,
|
|
153
|
+
registries: { "releases.sh": { org: "prd_wrong" } },
|
|
154
|
+
}).success,
|
|
155
|
+
).toBe(false);
|
|
156
|
+
expect(
|
|
157
|
+
ReleasesJsonRepoSchema.safeParse({
|
|
158
|
+
version: 2,
|
|
159
|
+
registries: { "releases.sh": { product: "org_wrong" } },
|
|
160
|
+
}).success,
|
|
161
|
+
).toBe(false);
|
|
65
162
|
});
|
|
66
163
|
});
|
|
@@ -1,54 +1,172 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
3
|
-
import { NoticeSchema } from "./shared.js";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
const MAX_PRODUCTS = 24;
|
|
4
|
+
const MAX_PRODUCT_RELEASES = 8;
|
|
5
|
+
const MAX_FILE_RELEASES = 32;
|
|
6
|
+
|
|
7
|
+
/** A single social handle/URL. Bare handles are intentionally accepted. */
|
|
6
8
|
const SocialValueSchema = z.string().min(1).max(200);
|
|
9
|
+
const SocialSchema = z.record(z.string().min(1).max(40), SocialValueSchema);
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const HttpsUrlSchema = z
|
|
12
|
+
.url()
|
|
13
|
+
.refine((url) => url.startsWith("https://"), "locator must be an https URL");
|
|
14
|
+
|
|
15
|
+
const ReleaseLocationFields = {
|
|
16
|
+
url: HttpsUrlSchema.optional(),
|
|
17
|
+
feed: HttpsUrlSchema.optional(),
|
|
18
|
+
appstore: HttpsUrlSchema.optional(),
|
|
19
|
+
file: HttpsUrlSchema.optional(),
|
|
20
|
+
title: z.string().min(1).max(200).optional(),
|
|
21
|
+
canonical: z.boolean().optional(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function hasLocator(value: {
|
|
25
|
+
url?: string;
|
|
26
|
+
feed?: string;
|
|
27
|
+
github?: string;
|
|
28
|
+
appstore?: string;
|
|
29
|
+
file?: string;
|
|
30
|
+
}): boolean {
|
|
31
|
+
return Boolean(value.url || value.feed || value.github || value.appstore || value.file);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A domain manifest can name a concrete GitHub repository, but never `self`. */
|
|
35
|
+
export const ReleasesJsonDomainReleaseSchema = z
|
|
36
|
+
.strictObject({
|
|
37
|
+
...ReleaseLocationFields,
|
|
38
|
+
github: z
|
|
39
|
+
.string()
|
|
40
|
+
.regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "github must be owner/repo")
|
|
41
|
+
.optional(),
|
|
17
42
|
})
|
|
18
|
-
.
|
|
43
|
+
.refine(hasLocator, "at least one release locator is required");
|
|
44
|
+
|
|
45
|
+
/** A repo manifest additionally accepts `github: "self"`. */
|
|
46
|
+
export const ReleasesJsonRepoReleaseSchema = z
|
|
47
|
+
.strictObject({
|
|
48
|
+
...ReleaseLocationFields,
|
|
49
|
+
github: z
|
|
50
|
+
.union([
|
|
51
|
+
z.literal("self"),
|
|
52
|
+
z.string().regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "github must be owner/repo"),
|
|
53
|
+
])
|
|
54
|
+
.optional(),
|
|
55
|
+
})
|
|
56
|
+
.refine(hasLocator, "at least one release locator is required");
|
|
57
|
+
|
|
58
|
+
function releasesArray<T extends z.ZodType>(item: T, max: number) {
|
|
59
|
+
return z
|
|
60
|
+
.array(item)
|
|
61
|
+
.max(max)
|
|
62
|
+
.refine(
|
|
63
|
+
(items) =>
|
|
64
|
+
items.filter((entry) => Boolean((entry as { canonical?: boolean }).canonical)).length <= 1,
|
|
65
|
+
"at most one canonical release location is allowed per scope",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DomainProductReleasesSchema = releasesArray(
|
|
70
|
+
ReleasesJsonDomainReleaseSchema,
|
|
71
|
+
MAX_PRODUCT_RELEASES,
|
|
72
|
+
);
|
|
73
|
+
const RepoReleasesSchema = releasesArray(ReleasesJsonRepoReleaseSchema, MAX_FILE_RELEASES);
|
|
74
|
+
const DomainReleasesSchema = releasesArray(ReleasesJsonDomainReleaseSchema, MAX_FILE_RELEASES);
|
|
19
75
|
|
|
20
76
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* file. The server enforces which keys it honors based on the host the file came
|
|
24
|
-
* from — this schema only validates shape.
|
|
77
|
+
* Product declaration in a domain manifest. Taxonomy strings are deliberately
|
|
78
|
+
* lenient here; the reconciler resolves known values and ignores the rest.
|
|
25
79
|
*/
|
|
26
|
-
export const
|
|
80
|
+
export const ReleasesJsonProductSchema = z.strictObject({
|
|
81
|
+
name: z.string().min(1).max(120),
|
|
82
|
+
slug: z.string().min(1).max(120).optional(),
|
|
83
|
+
kind: z.string().min(1).max(120).optional(),
|
|
84
|
+
category: z.string().min(1).max(120).optional(),
|
|
85
|
+
description: z.string().max(2000).optional(),
|
|
86
|
+
website: z.url().optional(),
|
|
87
|
+
docs: z.url().optional(),
|
|
88
|
+
support: z.url().optional(),
|
|
89
|
+
social: SocialSchema.optional(),
|
|
90
|
+
archived: z.boolean().optional(),
|
|
91
|
+
releases: DomainProductReleasesSchema.optional(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Repo-scope product binding. Slug is only a creation suggestion. */
|
|
95
|
+
export const ReleasesJsonRepoProductSchema = z.strictObject({
|
|
96
|
+
name: z.string().min(1).max(120),
|
|
97
|
+
slug: z.string().min(1).max(120).optional(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const ReleasesRegistrySchema = z
|
|
101
|
+
.object({
|
|
102
|
+
org: z
|
|
103
|
+
.string()
|
|
104
|
+
.regex(/^org_[A-Za-z0-9]+$/)
|
|
105
|
+
.optional(),
|
|
106
|
+
product: z
|
|
107
|
+
.string()
|
|
108
|
+
.regex(/^prd_[A-Za-z0-9]+$/)
|
|
109
|
+
.optional(),
|
|
110
|
+
verification: z.string().min(1).max(500).optional(),
|
|
111
|
+
})
|
|
112
|
+
.catchall(z.unknown());
|
|
113
|
+
|
|
114
|
+
/** Registry-specific extensions are forward-compatible by design. */
|
|
115
|
+
export const ReleasesJsonRegistriesSchema = z
|
|
27
116
|
.object({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
117
|
+
"releases.sh": ReleasesRegistrySchema.optional(),
|
|
118
|
+
})
|
|
119
|
+
.catchall(z.unknown());
|
|
120
|
+
|
|
121
|
+
/** Top-level fields shared by every manifest scope. */
|
|
122
|
+
const BaseFields = {
|
|
123
|
+
$schema: z.url().optional(),
|
|
124
|
+
version: z.literal(2),
|
|
125
|
+
registries: ReleasesJsonRegistriesSchema.optional(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const CommonFields = {
|
|
129
|
+
...BaseFields,
|
|
130
|
+
releases: DomainReleasesSchema.optional(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Domain-hosted variant: flat org identity plus product and release declarations. */
|
|
134
|
+
export const ReleasesJsonDomainSchema = z
|
|
135
|
+
.strictObject({
|
|
136
|
+
...CommonFields,
|
|
35
137
|
name: z.string().min(1).max(120).optional(),
|
|
36
138
|
description: z.string().max(2000).optional(),
|
|
37
139
|
category: z.string().min(1).max(120).optional(),
|
|
38
|
-
avatar:
|
|
39
|
-
.url()
|
|
40
|
-
.refine((u) => u.startsWith("https://"), "avatar must be an https URL")
|
|
41
|
-
.optional(),
|
|
140
|
+
avatar: HttpsUrlSchema.optional(),
|
|
42
141
|
tags: z.array(z.string().min(1).max(60)).max(50).optional(),
|
|
43
|
-
social:
|
|
44
|
-
|
|
45
|
-
// Source/product scope
|
|
46
|
-
product: ReleasesJsonProductSchema.optional(),
|
|
142
|
+
social: SocialSchema.optional(),
|
|
143
|
+
products: z.array(ReleasesJsonProductSchema).max(MAX_PRODUCTS).optional(),
|
|
47
144
|
})
|
|
48
|
-
.
|
|
145
|
+
.refine(
|
|
146
|
+
(manifest) =>
|
|
147
|
+
(manifest.releases?.length ?? 0) +
|
|
148
|
+
(manifest.products?.reduce((sum, product) => sum + (product.releases?.length ?? 0), 0) ??
|
|
149
|
+
0) <=
|
|
150
|
+
MAX_FILE_RELEASES,
|
|
151
|
+
`at most ${MAX_FILE_RELEASES} release locations are allowed per file`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/** Repo-root variant: product binding plus repo-scoped release declarations. */
|
|
155
|
+
export const ReleasesJsonRepoSchema = z.strictObject({
|
|
156
|
+
...BaseFields,
|
|
157
|
+
product: ReleasesJsonRepoProductSchema.optional(),
|
|
158
|
+
releases: RepoReleasesSchema.optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/** Public schema accepts either hosting scope; reconcilers use the scoped schema. */
|
|
162
|
+
export const ReleasesJsonConfigSchema = z.union([ReleasesJsonDomainSchema, ReleasesJsonRepoSchema]);
|
|
49
163
|
|
|
50
164
|
export type ReleasesJsonConfig = z.infer<typeof ReleasesJsonConfigSchema>;
|
|
165
|
+
export type ReleasesJsonDomain = z.infer<typeof ReleasesJsonDomainSchema>;
|
|
166
|
+
export type ReleasesJsonRepo = z.infer<typeof ReleasesJsonRepoSchema>;
|
|
51
167
|
export type ReleasesJsonProduct = z.infer<typeof ReleasesJsonProductSchema>;
|
|
168
|
+
export type ReleasesJsonDomainRelease = z.infer<typeof ReleasesJsonDomainReleaseSchema>;
|
|
169
|
+
export type ReleasesJsonRepoRelease = z.infer<typeof ReleasesJsonRepoReleaseSchema>;
|
|
52
170
|
|
|
53
171
|
/** Response shape of POST /v1/orgs/:slug/sync-well-known. */
|
|
54
172
|
export const SyncWellKnownResponseSchema = z.object({
|