@monster-orange/den 0.2.6 → 0.2.7
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/CHANGELOG.md +117 -0
- package/bin/den +9 -0
- package/docs/distributions.md +92 -6
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/shared/den-config.ts +38 -1
- package/src/shared/environment-manager/catalog.ts +9 -2
- package/src/shared/environment-manager/default-workspace.ts +45 -14
- package/src/shared/environment-manager/health.ts +51 -8
- package/src/shared/environment-manager/service-runner.ts +51 -5
- package/src/shared/environment-manager/workspace-manager.ts +2 -1
- package/src/shared/environment-manager/workspace-org.ts +264 -0
- package/src/shared/types/index.ts +7 -0
- package/src/ui/cli/clone-plan.ts +243 -0
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/index.ts +249 -40
- package/src/ui/cli/org-command.ts +162 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/mcp/server.ts +24 -1
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/index.ts +31 -4
- package/src/ui/server/release-service.ts +16 -3
- package/src/ui/server/workspace-routes.ts +4 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Display test results in CLI
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { newTable } from "./table.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { resultReader } from "../../shared/results/result-reader.js";
|
|
9
9
|
import type {
|
|
@@ -40,7 +40,7 @@ export class ResultsViewer {
|
|
|
40
40
|
for (const pillar of pillars) {
|
|
41
41
|
console.log(chalk.bold(`\n${this.formatPillarName(pillar)}:`));
|
|
42
42
|
|
|
43
|
-
const table =
|
|
43
|
+
const table = newTable({
|
|
44
44
|
head: [
|
|
45
45
|
chalk.cyan("Environment"),
|
|
46
46
|
chalk.cyan("Total"),
|
|
@@ -115,7 +115,7 @@ export class ResultsViewer {
|
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const table =
|
|
118
|
+
const table = newTable({
|
|
119
119
|
head: [
|
|
120
120
|
chalk.cyan("Test"),
|
|
121
121
|
chalk.cyan("Status"),
|
|
@@ -191,7 +191,7 @@ export class ResultsViewer {
|
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
const table =
|
|
194
|
+
const table = newTable({
|
|
195
195
|
head: [
|
|
196
196
|
chalk.cyan("Journey"),
|
|
197
197
|
chalk.cyan("Verdict"),
|
|
@@ -281,7 +281,7 @@ export class ResultsViewer {
|
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
const table =
|
|
284
|
+
const table = newTable({
|
|
285
285
|
head: [
|
|
286
286
|
chalk.cyan("Test"),
|
|
287
287
|
chalk.cyan("Runs"),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-table3 colours its borders (grey) and head cells (its own default, or
|
|
3
|
+
* the `style` given) through @colors/colors, which ignores Den's colour
|
|
4
|
+
* detection: piped `status` output carried ANSI codes from the table alone.
|
|
5
|
+
* Every CLI table goes through here, and takes no colour when chalk has none
|
|
6
|
+
* (not a TTY, NO_COLOR, FORCE_COLOR=0).
|
|
7
|
+
*/
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import Table from "cli-table3";
|
|
10
|
+
|
|
11
|
+
export function newTable(
|
|
12
|
+
opts: Table.TableConstructorOptions = {},
|
|
13
|
+
colour: boolean = chalk.level > 0,
|
|
14
|
+
): Table.Table {
|
|
15
|
+
if (colour) return new Table(opts);
|
|
16
|
+
return new Table({
|
|
17
|
+
...opts,
|
|
18
|
+
style: { ...(opts.style ?? {}), head: [], border: [] },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bare `<cli> validate` in the distribution root: every manifest the
|
|
3
|
+
* distribution uses, without cloning anything.
|
|
4
|
+
*
|
|
5
|
+
* - Each repo of every workspace, once per distinct checkout path: its
|
|
6
|
+
* layered manifest (own or held, then the override, then the merge).
|
|
7
|
+
* - A repo with no checkout on disk (a managed clone not made yet, a
|
|
8
|
+
* dev-managed path that is gone) falls back to its held manifest, marked
|
|
9
|
+
* "held only, no checkout"; with no held manifest either it is a warning,
|
|
10
|
+
* not a failure.
|
|
11
|
+
* - Each held manifest (and override) in manifests/ that no repo above used.
|
|
12
|
+
*
|
|
13
|
+
* The layered validator comes in as `validate` (validateManifestLayers), so
|
|
14
|
+
* this file stays under the typecheck gate (see validate-layers.ts).
|
|
15
|
+
*
|
|
16
|
+
* `--held` checks only manifests/, as if nothing were checked out: a result
|
|
17
|
+
* that does not depend on the machine (an upgrade gate).
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import { product } from "../../shared/product.js";
|
|
22
|
+
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
23
|
+
import {
|
|
24
|
+
readWorkspacesFile,
|
|
25
|
+
resolveServiceSource,
|
|
26
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
27
|
+
import {
|
|
28
|
+
heldManifestsDir,
|
|
29
|
+
overrideSuffix,
|
|
30
|
+
} from "../../shared/manifest/read-manifest.js";
|
|
31
|
+
import type { LayeredValidation } from "../../shared/manifest/validate-layers.js";
|
|
32
|
+
|
|
33
|
+
export interface RepoValidation {
|
|
34
|
+
repo: string;
|
|
35
|
+
/** The checkout validated, or null when only manifests/ was read. */
|
|
36
|
+
checkout: string | null;
|
|
37
|
+
/** Workspaces that use this checkout. */
|
|
38
|
+
workspaces: string[];
|
|
39
|
+
/** own | held, plus "+ override" when one layered on. */
|
|
40
|
+
source: string;
|
|
41
|
+
/** Set when there was no checkout and the held manifest stood in. */
|
|
42
|
+
note?: string;
|
|
43
|
+
/** No checkout and no held manifest: reported, not failed. */
|
|
44
|
+
missing?: boolean;
|
|
45
|
+
result: LayeredValidation;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A path that never exists, named after the repo, so the layer lookup finds
|
|
50
|
+
* only manifests/<repo>… and never a checkout's own manifest.
|
|
51
|
+
*/
|
|
52
|
+
function heldOnlyPath(repo: string): string {
|
|
53
|
+
return path.join(heldManifestsDir(), ".held-only", repo);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sourceOf(result: LayeredValidation): string {
|
|
57
|
+
const base = result.layers.find(
|
|
58
|
+
(l) => l.layer === "repo" || l.layer === "held",
|
|
59
|
+
);
|
|
60
|
+
const own = base?.layer === "repo" ? "own" : base ? "held" : "none";
|
|
61
|
+
return result.layers.some((l) => l.layer === "override")
|
|
62
|
+
? `${own} + override`
|
|
63
|
+
: own;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The repo keys manifests/ holds a manifest or an override for. */
|
|
67
|
+
function heldKeys(): string[] {
|
|
68
|
+
const dir = heldManifestsDir();
|
|
69
|
+
if (!fs.existsSync(dir)) return [];
|
|
70
|
+
const yml = product().manifestYml;
|
|
71
|
+
const over = overrideSuffix();
|
|
72
|
+
const keys = new Set<string>();
|
|
73
|
+
for (const name of fs.readdirSync(dir)) {
|
|
74
|
+
if (name.endsWith(over)) keys.add(name.slice(0, -over.length));
|
|
75
|
+
else if (name.endsWith(yml)) keys.add(name.slice(0, -yml.length));
|
|
76
|
+
}
|
|
77
|
+
return [...keys].sort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Validate = (repoPath: string) => LayeredValidation;
|
|
81
|
+
|
|
82
|
+
function heldOnly(repo: string, validate: Validate): RepoValidation {
|
|
83
|
+
const result = validate(heldOnlyPath(repo));
|
|
84
|
+
return {
|
|
85
|
+
repo,
|
|
86
|
+
checkout: null,
|
|
87
|
+
workspaces: [],
|
|
88
|
+
source: sourceOf(result),
|
|
89
|
+
result,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateAll(opts: {
|
|
94
|
+
held?: boolean;
|
|
95
|
+
validate: Validate;
|
|
96
|
+
}): RepoValidation[] {
|
|
97
|
+
const { validate } = opts;
|
|
98
|
+
if (opts.held)
|
|
99
|
+
return heldKeys().map((k) => {
|
|
100
|
+
const r = heldOnly(k, validate);
|
|
101
|
+
// An override with no held base layers onto the repo's own manifest,
|
|
102
|
+
// which --held deliberately does not read: noted, not failed.
|
|
103
|
+
const overrideOnly =
|
|
104
|
+
r.result.layers.length > 0 &&
|
|
105
|
+
r.result.layers.every((l) => l.layer === "override");
|
|
106
|
+
return overrideOnly
|
|
107
|
+
? {
|
|
108
|
+
...r,
|
|
109
|
+
source: "override",
|
|
110
|
+
missing: true,
|
|
111
|
+
note: "override only: its base is the repo's own manifest (not read with --held)",
|
|
112
|
+
}
|
|
113
|
+
: r;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const file = readWorkspacesFile();
|
|
117
|
+
const catalog = readCatalog();
|
|
118
|
+
// repo + checkout path → the workspaces that use it.
|
|
119
|
+
const seen = new Map<
|
|
120
|
+
string,
|
|
121
|
+
{ repo: string; path: string; workspaces: string[] }
|
|
122
|
+
>();
|
|
123
|
+
for (const [ws, config] of Object.entries(file.workspaces)) {
|
|
124
|
+
for (const address of Object.keys(config.services ?? {})) {
|
|
125
|
+
const src = resolveServiceSource(address, ws, config, catalog);
|
|
126
|
+
const repo = src?.repoName ?? address.split("/")[0];
|
|
127
|
+
const p = src?.repoPath ? path.resolve(src.repoPath) : "";
|
|
128
|
+
const key = `${repo}\0${p}`;
|
|
129
|
+
const hit = seen.get(key);
|
|
130
|
+
if (hit) {
|
|
131
|
+
if (!hit.workspaces.includes(ws)) hit.workspaces.push(ws);
|
|
132
|
+
} else seen.set(key, { repo, path: p, workspaces: [ws] });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const out: RepoValidation[] = [];
|
|
137
|
+
const used = new Set<string>();
|
|
138
|
+
for (const { repo, path: p, workspaces } of seen.values()) {
|
|
139
|
+
used.add(repo);
|
|
140
|
+
if (p && fs.existsSync(p)) {
|
|
141
|
+
const result = validate(p);
|
|
142
|
+
out.push({
|
|
143
|
+
repo,
|
|
144
|
+
checkout: p,
|
|
145
|
+
workspaces,
|
|
146
|
+
source: sourceOf(result),
|
|
147
|
+
result,
|
|
148
|
+
...(result.layers.length === 0 ? { missing: true } : {}),
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const held = heldOnly(repo, validate);
|
|
153
|
+
out.push({
|
|
154
|
+
...held,
|
|
155
|
+
checkout: p || null,
|
|
156
|
+
workspaces,
|
|
157
|
+
...(held.result.layers.length
|
|
158
|
+
? { note: "held only, no checkout" }
|
|
159
|
+
: { missing: true, note: "no checkout and no held manifest" }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
for (const key of heldKeys())
|
|
163
|
+
if (!used.has(key))
|
|
164
|
+
out.push({ ...heldOnly(key, validate), note: "held, in no workspace" });
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether the run fails: any layer invalid, ignoring `missing` entries. */
|
|
169
|
+
export function validateAllFailed(results: RepoValidation[]): boolean {
|
|
170
|
+
return results.some((r) => !r.missing && !r.result.valid);
|
|
171
|
+
}
|
package/src/ui/mcp/server.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* stdout carries the protocol only: diagnostics go to stderr.
|
|
7
7
|
*/
|
|
8
|
+
import { z } from "zod";
|
|
8
9
|
import { randomUUID } from "crypto";
|
|
9
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -185,7 +186,29 @@ export async function runOneShot(
|
|
|
185
186
|
return 0;
|
|
186
187
|
}
|
|
187
188
|
const ctx: ToolContext = { api: opts.api, cfg: opts.cfg, cli: opts.cli };
|
|
188
|
-
|
|
189
|
+
// The stdio server validates arguments against each tool's schema (the
|
|
190
|
+
// SDK does it); a one-shot must too, and strictly: it is typed by hand, so
|
|
191
|
+
// an unknown or misspelt key is an error, not silently dropped.
|
|
192
|
+
let args = shot.args;
|
|
193
|
+
const tool = toolsFor(opts.cfg).find((t) => t.name === shot.tool);
|
|
194
|
+
if (tool) {
|
|
195
|
+
const parsed = z.object(tool.shape).strict().safeParse(args);
|
|
196
|
+
if (!parsed.success) {
|
|
197
|
+
const err = opts.err ?? ((t: string) => process.stderr.write(t));
|
|
198
|
+
const lines = parsed.error.issues.map((i) => {
|
|
199
|
+
const at = i.path.length ? i.path.join(".") : "(arguments)";
|
|
200
|
+
return ` ${at}: ${i.message}`;
|
|
201
|
+
});
|
|
202
|
+
const keys = Object.keys(tool.shape);
|
|
203
|
+
err(
|
|
204
|
+
`--call ${shot.tool}: invalid arguments\n${lines.join("\n")}\n` +
|
|
205
|
+
` takes: ${keys.length ? keys.join(", ") : "no arguments"}\n`,
|
|
206
|
+
);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
args = parsed.data as Record<string, unknown>;
|
|
210
|
+
}
|
|
211
|
+
const r = await callTool(shot.tool, args, ctx);
|
|
189
212
|
out(r.text.endsWith("\n") ? r.text : `${r.text}\n`);
|
|
190
213
|
return r.isError ? 1 : 0;
|
|
191
214
|
}
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* so an "error" health result is only acted on after a SECOND
|
|
12
12
|
* consecutive "error" check separated by a retry delay. "unknown"
|
|
13
13
|
* results and thrown checks always keep the entry (err on the side of
|
|
14
|
-
* keeping). A daemon whose address no longer resolves
|
|
15
|
-
* workspace/catalog service is process-death-equivalent evidence and
|
|
16
|
-
* removed directly.
|
|
14
|
+
* keeping). A daemon this root registered whose address no longer resolves
|
|
15
|
+
* to any workspace/catalog service is process-death-equivalent evidence and
|
|
16
|
+
* is removed directly. Records another root registered (all roots of a
|
|
17
|
+
* product share the file) are never touched, and an unstamped legacy record
|
|
18
|
+
* is not removed for being unresolvable.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import type { ServiceHealthResult } from "../../shared/environment-manager/health.js";
|
|
@@ -41,6 +43,12 @@ export interface DaemonReaperDeps {
|
|
|
41
43
|
delay: (ms: number) => Promise<void>;
|
|
42
44
|
/** Gap between the first "error" result and the confirming re-check. */
|
|
43
45
|
retryDelayMs: number;
|
|
46
|
+
/**
|
|
47
|
+
* Which root wrote the record (`daemonRootOf`). Every root of a product
|
|
48
|
+
* shares one daemons file: another root's records are never swept here,
|
|
49
|
+
* and an unstamped (pre-0.2.7) one is not dropped for being unresolvable.
|
|
50
|
+
*/
|
|
51
|
+
rootOf: (daemon: DaemonRecord) => "here" | "other" | "unstamped";
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export interface DaemonReapResult {
|
|
@@ -92,9 +100,19 @@ export async function reapDeadDaemons(
|
|
|
92
100
|
await Promise.allSettled(
|
|
93
101
|
deps.listDaemons().map(async (daemon) => {
|
|
94
102
|
try {
|
|
103
|
+
const root = deps.rootOf(daemon);
|
|
104
|
+
if (root === "other") {
|
|
105
|
+
kept.push(daemon.address);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
95
108
|
const service = resolveForDaemon(deps, daemon);
|
|
96
109
|
if (!service) {
|
|
97
|
-
// No longer in any owning workspace/catalog — nothing could restart
|
|
110
|
+
// No longer in any owning workspace/catalog — nothing could restart
|
|
111
|
+
// it. An unstamped record may be another root's, so it stays.
|
|
112
|
+
if (root === "unstamped") {
|
|
113
|
+
kept.push(daemon.address);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
98
116
|
deps.unregisterDaemon(daemon.address);
|
|
99
117
|
removed.push(daemon.address);
|
|
100
118
|
return;
|
package/src/ui/server/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
checkServiceHealth,
|
|
81
81
|
loadDaemonsFromFile,
|
|
82
82
|
pruneDeadDaemonsAsync,
|
|
83
|
+
daemonRootOf,
|
|
83
84
|
isInteractiveServiceCommand,
|
|
84
85
|
rewriteLegacyOpctlKillCommandForContainer,
|
|
85
86
|
shouldDetachServiceCommand,
|
|
@@ -436,6 +437,11 @@ import {
|
|
|
436
437
|
send404,
|
|
437
438
|
} from "./http-helpers.js";
|
|
438
439
|
import { orgOf, listOrgs } from "../../shared/manifest/service-links.js";
|
|
440
|
+
import {
|
|
441
|
+
readOrgContext,
|
|
442
|
+
resolveAllWorkspaceOrgs,
|
|
443
|
+
withOrgMembership,
|
|
444
|
+
} from "../../shared/environment-manager/workspace-org.js";
|
|
439
445
|
import {
|
|
440
446
|
readLatestSummary,
|
|
441
447
|
securityReportsDir,
|
|
@@ -684,9 +690,18 @@ async function handleRequest(
|
|
|
684
690
|
|
|
685
691
|
// ── API: environments ──────────────────────────────────────────────────────
|
|
686
692
|
// Orgs the catalog's repos belong to, with config/den.yml display names.
|
|
693
|
+
// Each entry also says whether it is the default org (`github_org`) and
|
|
694
|
+
// which workspaces on this machine belong to it (ORG-1a). Nothing else
|
|
695
|
+
// changes: same entries, order and fields (ORG-SPEC C3).
|
|
687
696
|
if (pathname === "/api/orgs") {
|
|
688
|
-
const
|
|
689
|
-
const
|
|
697
|
+
const ctx = readOrgContext();
|
|
698
|
+
const catalog = ctx.catalog;
|
|
699
|
+
const orgs = withOrgMembership(
|
|
700
|
+
listOrgs(catalog, ctx.config.orgs ?? {}),
|
|
701
|
+
resolveAllWorkspaceOrgs(),
|
|
702
|
+
ctx.file,
|
|
703
|
+
ctx.config,
|
|
704
|
+
);
|
|
690
705
|
if (url.searchParams.get("repos") !== "1") return sendJson(res, orgs);
|
|
691
706
|
const repos = Object.fromEntries(
|
|
692
707
|
Object.keys(catalog).map((r) => [r, orgOf(r, catalog)]),
|
|
@@ -2375,7 +2390,14 @@ async function handleRequest(
|
|
|
2375
2390
|
// ── API: den config ────────────────────────────────────────────────────
|
|
2376
2391
|
if (pathname === `/api/${product().cli}/config` && req.method === "GET") {
|
|
2377
2392
|
// `org` resolved (id defaults to edge.org) for the header's org label.
|
|
2378
|
-
|
|
2393
|
+
const config = readDenConfig();
|
|
2394
|
+
// `default_org` (ORG-1a) is the default work org, `github_org`; `org`
|
|
2395
|
+
// stays the instance org (readOrg).
|
|
2396
|
+
return sendJson(res, {
|
|
2397
|
+
...config,
|
|
2398
|
+
org: readOrg(),
|
|
2399
|
+
default_org: config.github_org?.trim() || null,
|
|
2400
|
+
});
|
|
2379
2401
|
}
|
|
2380
2402
|
|
|
2381
2403
|
// ── API: server info + staleness (drives the header "stale server" pill) ────
|
|
@@ -3184,7 +3206,7 @@ void (async () => {
|
|
|
3184
3206
|
await new Promise<void>((resolve) =>
|
|
3185
3207
|
setTimeout(resolve, DAEMON_SHUTDOWN_GRACE_MS),
|
|
3186
3208
|
);
|
|
3187
|
-
await reapDeadDaemons({
|
|
3209
|
+
const reaped = await reapDeadDaemons({
|
|
3188
3210
|
listDaemons,
|
|
3189
3211
|
resolveService,
|
|
3190
3212
|
getActiveWorkspaces,
|
|
@@ -3192,7 +3214,12 @@ void (async () => {
|
|
|
3192
3214
|
unregisterDaemon,
|
|
3193
3215
|
delay: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
|
3194
3216
|
retryDelayMs: DAEMON_RETRY_DELAY_MS,
|
|
3217
|
+
rootOf: daemonRootOf,
|
|
3195
3218
|
});
|
|
3219
|
+
if (reaped.removed.length > 0)
|
|
3220
|
+
console.error(
|
|
3221
|
+
`Reaped ${reaped.removed.length} stopped daemon(s): ${reaped.removed.join(", ")}`,
|
|
3222
|
+
);
|
|
3196
3223
|
})();
|
|
3197
3224
|
|
|
3198
3225
|
const prunedSnapshots = pruneDeadSnapshots();
|
|
@@ -19,13 +19,16 @@ import { execFile } from "child_process";
|
|
|
19
19
|
import { promisify } from "util";
|
|
20
20
|
import { GH_API_TIMEOUT_MS } from "../../shared/constants.js";
|
|
21
21
|
import {
|
|
22
|
+
readExplicitReleaseOrg,
|
|
22
23
|
readReleaseConfig,
|
|
24
|
+
releaseOwnerFor,
|
|
23
25
|
type ReleaseConfig,
|
|
24
26
|
type ReleaseRepo,
|
|
25
27
|
type ResolvedReleaseTarget,
|
|
26
28
|
resolveReleaseTarget,
|
|
27
29
|
} from "../../shared/den-config.js";
|
|
28
30
|
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
31
|
+
import { orgOf } from "../../shared/manifest/service-links.js";
|
|
29
32
|
import { listReposWithManifests } from "../../shared/manifest/workspace-manifests.js";
|
|
30
33
|
|
|
31
34
|
import { product } from "../../shared/product.js";
|
|
@@ -262,13 +265,23 @@ export class ReleaseService {
|
|
|
262
265
|
};
|
|
263
266
|
}
|
|
264
267
|
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The owner a repo is released from (REL-1): an explicit `release.org`
|
|
270
|
+
* (den.yml or user.yml) wins; otherwise the repo's own non-empty catalog
|
|
271
|
+
* owner, so a client org's repo releases from its own org; `github_org`
|
|
272
|
+
* (readReleaseConfig's fallback for `cfg.org`) last.
|
|
273
|
+
*/
|
|
274
|
+
private ownerFor(repo: string, cfg: ReleaseConfig): string {
|
|
275
|
+
return releaseOwnerFor({
|
|
276
|
+
explicitOrg: readExplicitReleaseOrg(),
|
|
277
|
+
catalogOwner: orgOf(repo, readCatalog()),
|
|
278
|
+
githubOrg: cfg.org,
|
|
279
|
+
});
|
|
267
280
|
}
|
|
268
281
|
|
|
269
282
|
slug(repo: string, cfg = this.config()): string {
|
|
270
283
|
const gh = cfg.repos.find((r) => r.name === repo)?.gh;
|
|
271
|
-
return gh && gh.includes("/") ? gh : `${this.
|
|
284
|
+
return gh && gh.includes("/") ? gh : `${this.ownerFor(repo, cfg)}/${repo}`;
|
|
272
285
|
}
|
|
273
286
|
|
|
274
287
|
/**
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
getWaitDeps,
|
|
66
66
|
} from "../../shared/environment-manager/dag.js";
|
|
67
67
|
import { deriveGraph } from "../../shared/environment-manager/derivation/derive-graph.js";
|
|
68
|
+
import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
|
|
68
69
|
import {
|
|
69
70
|
deriveStartTargets,
|
|
70
71
|
filterClosureForStart,
|
|
@@ -2158,7 +2159,9 @@ export async function handleWorkspaceRoutes(
|
|
|
2158
2159
|
// flow writes one), which used to read as "not in the catalog" and
|
|
2159
2160
|
// skip the clone entirely. Derive the conventional slug instead, and
|
|
2160
2161
|
// persist it so every other flow (CLI up/setup) stops hitting this.
|
|
2161
|
-
const ghSlug = resolveRepoGhSlug(svc.repoName)
|
|
2162
|
+
const ghSlug = resolveRepoGhSlug(svc.repoName, readCatalog(), () =>
|
|
2163
|
+
githubOrgFor(ws.name),
|
|
2164
|
+
);
|
|
2162
2165
|
if (!readCatalog()[svc.repoName]?.gh) {
|
|
2163
2166
|
upsertCatalogEntry(svc.repoName, { gh: ghSlug });
|
|
2164
2167
|
}
|