Skip to content

Commit 48a6cf6

Browse files
committed
feat: publish subgraphs as plugins
1 parent 428c7ac commit 48a6cf6

File tree

5 files changed

+443
-248
lines changed

5 files changed

+443
-248
lines changed

cli/src/commands/demo/command.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
import {
1717
checkDockerReadiness,
1818
clearScreen,
19+
getDemoLogPath,
1920
prepareSupportingData,
2021
printLogo,
22+
publishAllPlugins,
2123
resetScreen,
2224
updateScreenWithUserInfo,
2325
} from './util.js';
@@ -164,13 +166,20 @@ async function handleStep2(
164166
{
165167
onboarding,
166168
userInfo,
169+
supportDir,
170+
signal,
167171
}: {
168-
onboarding: {
169-
finishedAt?: string;
170-
};
172+
onboarding: { finishedAt?: string };
171173
userInfo: UserInfo;
174+
supportDir: string;
175+
signal: AbortSignal;
172176
},
173177
) {
178+
function retryFn() {
179+
resetScreen(userInfo);
180+
return handleStep2(opts, { onboarding, userInfo, supportDir, signal });
181+
}
182+
174183
const graphData = await handleGetFederatedGraphResponse(opts.client, {
175184
onboarding,
176185
userInfo,
@@ -199,6 +208,26 @@ async function handleStep2(
199208
onboarding,
200209
userInfo,
201210
});
211+
212+
const logPath = getDemoLogPath();
213+
console.log(`\nPublishing plugins… ${pc.dim(`(logs: ${logPath})`)}`);
214+
215+
const publishResult = await publishAllPlugins({
216+
client: opts.client,
217+
supportDir,
218+
signal,
219+
logPath,
220+
});
221+
222+
if (publishResult.error) {
223+
await waitForKeyPress(
224+
{
225+
r: retryFn,
226+
R: retryFn,
227+
},
228+
'Hit [r] to retry. CTRL+C to quit.',
229+
);
230+
}
202231
}
203232

204233
async function handleGetOnboardingResponse(client: BaseCommandOptions['client'], userInfo: UserInfo) {
@@ -252,28 +281,38 @@ async function getUserInfo(client: BaseCommandOptions['client']) {
252281

253282
export default function (opts: BaseCommandOptions) {
254283
return async function handleCommand() {
255-
clearScreen();
256-
printHello();
257-
await prepareSupportingData();
258-
await checkDockerReadiness();
259-
const userInfo = await getUserInfo(opts.client);
260-
updateScreenWithUserInfo(userInfo);
261-
262-
await waitForKeyPress(
263-
{
264-
Enter: () => undefined,
265-
},
266-
`It is recommended you run this command along the onboarding wizard at ${config.baseURL}/onboarding with the same account.\nPress ENTER to continue…`,
267-
);
268-
269-
resetScreen(userInfo);
270-
271-
const onboardingCheck = await handleStep1(opts, userInfo);
272-
273-
if (!onboardingCheck) {
274-
return;
284+
const controller = new AbortController();
285+
const cleanup = () => controller.abort();
286+
process.on('SIGINT', cleanup);
287+
process.on('SIGTERM', cleanup);
288+
289+
try {
290+
clearScreen();
291+
printHello();
292+
const supportDir = await prepareSupportingData();
293+
await checkDockerReadiness();
294+
const userInfo = await getUserInfo(opts.client);
295+
updateScreenWithUserInfo(userInfo);
296+
297+
await waitForKeyPress(
298+
{
299+
Enter: () => undefined,
300+
},
301+
`It is recommended you run this command along the onboarding wizard at ${config.baseURL}/onboarding with the same account.\nPress ENTER to continue…`,
302+
);
303+
304+
resetScreen(userInfo);
305+
306+
const onboardingCheck = await handleStep1(opts, userInfo);
307+
308+
if (!onboardingCheck) {
309+
return;
310+
}
311+
312+
await handleStep2(opts, { onboarding: onboardingCheck, userInfo, supportDir, signal: controller.signal });
313+
} finally {
314+
process.off('SIGINT', cleanup);
315+
process.off('SIGTERM', cleanup);
275316
}
276-
277-
await handleStep2(opts, { onboarding: onboardingCheck, userInfo });
278317
};
279318
}

cli/src/commands/demo/util.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import fs from 'node:fs/promises';
2+
import { createWriteStream, existsSync, mkdirSync, type WriteStream } from 'node:fs';
23
import path from 'node:path';
34
import { program } from 'commander';
4-
import { execa } from 'execa';
5+
import { execa, type ResultPromise } from 'execa';
56
import ora from 'ora';
67
import pc from 'picocolors';
78
import { z } from 'zod';
89
import { config, cacheDir } from '../../core/config.js';
10+
import { getDefaultPlatforms, publishPluginPipeline, readPluginFiles } from '../../core/plugin-publish.js';
11+
import type { BaseCommandOptions } from '../../core/types/types.js';
912
import { visibleLength } from '../../utils.js';
1013
import type { UserInfo } from './types.js';
1114

@@ -245,3 +248,77 @@ export async function checkDockerReadiness(): Promise<void> {
245248

246249
spinner.succeed('Docker is ready.');
247250
}
251+
252+
/**
253+
* Returns the path to the demo log file at ~/.cache/cosmo/demo/demo.log.
254+
* Creates the parent directory if needed.
255+
*/
256+
export function getDemoLogPath(): string {
257+
const cosmoDir = path.join(cacheDir, 'demo');
258+
if (!existsSync(cosmoDir)) {
259+
mkdirSync(cosmoDir, { recursive: true });
260+
}
261+
return path.join(cosmoDir, 'demo.log');
262+
}
263+
264+
function pipeToLog(logStream: WriteStream, proc: ResultPromise) {
265+
proc.stdout?.pipe(logStream, { end: false });
266+
proc.stderr?.pipe(logStream, { end: false });
267+
}
268+
269+
/**
270+
* Publishes demo plugins sequentially.
271+
* Returns [error] on first failure; spinner shows which plugin failed.
272+
*/
273+
export async function publishAllPlugins({
274+
client,
275+
supportDir,
276+
signal,
277+
logPath,
278+
}: {
279+
client: BaseCommandOptions['client'];
280+
supportDir: string;
281+
signal: AbortSignal;
282+
logPath: string;
283+
}) {
284+
const pluginNames = config.demoPluginNames;
285+
const namespace = config.demoNamespace;
286+
const labels = [config.demoLabelMatcher];
287+
// The demo router always runs in a Linux Docker container, so we need
288+
// linux builds for both architectures regardless of the host OS.
289+
const platforms = [...new Set([...getDefaultPlatforms(), 'linux/amd64', 'linux/arm64'])];
290+
const logStream = createWriteStream(logPath, { flags: 'w' });
291+
292+
try {
293+
for (let i = 0; i < pluginNames.length; i++) {
294+
const pluginName = pluginNames[i];
295+
const pluginDir = path.join(supportDir, 'plugins', pluginName);
296+
297+
const spinner = ora(`Publishing plugin ${pc.bold(pluginName)} (${i + 1}/${pluginNames.length})…`).start();
298+
299+
const files = await readPluginFiles(pluginDir);
300+
const result = await publishPluginPipeline({
301+
client,
302+
pluginDir,
303+
pluginName,
304+
namespace,
305+
labels,
306+
platforms,
307+
files,
308+
cancelSignal: signal,
309+
onProcess: (proc) => pipeToLog(logStream, proc),
310+
});
311+
312+
if (result.error) {
313+
spinner.fail(`Failed to publish plugin ${pc.bold(pluginName)}: ${result.error.message}`);
314+
return { error: result.error };
315+
}
316+
317+
spinner.succeed(`Plugin ${pc.bold(pluginName)} published.`);
318+
}
319+
} finally {
320+
logStream.end();
321+
}
322+
323+
return { error: null };
324+
}

0 commit comments

Comments
 (0)