runcloud 0.2.179 → 0.2.182
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/dist/commands/github.js +61 -0
- package/dist/commands/run-cloud-fleet-observations.js +7 -0
- package/dist/commands/run-cloud-fleet-survey.js +13 -10
- package/dist/commands/run-cloud-fleet.js +5 -4
- package/dist/index.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.2.182.tgz +0 -0
- package/runcloud-0.2.179.tgz +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
2
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
3
|
+
export async function getGithubStatus(client, projectId) {
|
|
4
|
+
return (await client.get(`/projects/${encodeURIComponent(projectId)}/github/status?ahead=1`));
|
|
5
|
+
}
|
|
6
|
+
export async function pullGithubBranch(client, projectId, branch) {
|
|
7
|
+
await client.post(`/projects/${encodeURIComponent(projectId)}/git/branches/${encodeURIComponent(branch)}/push`);
|
|
8
|
+
}
|
|
9
|
+
export function formatGithubBranches(status) {
|
|
10
|
+
if (!status.connected)
|
|
11
|
+
return 'This project is not linked to a GitHub repository.';
|
|
12
|
+
const repo = `${status.owner}/${status.name}`;
|
|
13
|
+
if (status.ahead === undefined)
|
|
14
|
+
return `Linked to ${repo}, but GitHub could not be read right now. Try again shortly.`;
|
|
15
|
+
if (status.ahead.length === 0)
|
|
16
|
+
return `Linked to ${repo}. Newly has every GitHub branch at its current tip.`;
|
|
17
|
+
return [
|
|
18
|
+
`Linked to ${repo}. On GitHub, not yet in Newly at their current tip:`,
|
|
19
|
+
...status.ahead.map((b) => ` ${b}`),
|
|
20
|
+
'Bring one in with: newly github pull <branch>',
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
23
|
+
export function registerGithub(program) {
|
|
24
|
+
const github = program
|
|
25
|
+
.command('github')
|
|
26
|
+
.description("Work with the project's linked GitHub repository");
|
|
27
|
+
const run = (action) => async (opts) => {
|
|
28
|
+
try {
|
|
29
|
+
const creds = requireCredentials();
|
|
30
|
+
const projectId = resolveProject(process.cwd(), opts.project);
|
|
31
|
+
await action(new ApiClient(creds.apiUrl, creds.token), projectId);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(friendlyApiError(e));
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
github
|
|
39
|
+
.command('branches')
|
|
40
|
+
.description('List GitHub branches that Newly does not have yet (or has at an older tip)')
|
|
41
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
42
|
+
.action(run(async (client, projectId) => {
|
|
43
|
+
console.log(formatGithubBranches(await getGithubStatus(client, projectId)));
|
|
44
|
+
}));
|
|
45
|
+
github
|
|
46
|
+
.command('pull <branch>')
|
|
47
|
+
.description('Bring one GitHub branch into Newly (a two-way sync of that branch) so it can be fetched and checked out')
|
|
48
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
49
|
+
.action((branch, opts) => run(async (client, projectId) => {
|
|
50
|
+
try {
|
|
51
|
+
await pullGithubBranch(client, projectId, branch);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (e instanceof ApiError && e.status === 403) {
|
|
55
|
+
throw new Error(`Pulling ${branch} needs a project admin. Ask the user to pull it from the GitHub pane in the Deploy tab.`);
|
|
56
|
+
}
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
console.log(`${branch} is in Newly, synced both ways with GitHub. Run: git fetch origin ${branch} && git checkout ${branch}`);
|
|
60
|
+
})(opts));
|
|
61
|
+
}
|
|
@@ -6,6 +6,13 @@ export const num = (value) => typeof value === 'number' && Number.isFinite(value
|
|
|
6
6
|
export const stamp = (value) => str(value) && Number.isFinite(Date.parse(String(value))) ? new Date(String(value)).toISOString() : null;
|
|
7
7
|
export const time = (value) => stamp(value) ? Date.parse(String(value)) : 0;
|
|
8
8
|
export const validFleetId = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/.test(value);
|
|
9
|
+
export function fleetReadError(reason, timeoutMs) {
|
|
10
|
+
if (reason === 'HTTP 401' || reason === 'HTTP 403')
|
|
11
|
+
return new Error(`Fleet access denied (${reason}); use an admin identity with read permission`);
|
|
12
|
+
if (reason === 'request_deadline_exceeded')
|
|
13
|
+
return new Error(`Fleet read timed out after ${timeoutMs / 1000}s (request_deadline_exceeded); retry the read and check API latency if it persists`);
|
|
14
|
+
return new Error(`Fleet read unavailable (${reason ?? 'invalid_response'}); check API connectivity and retry the read`);
|
|
15
|
+
}
|
|
9
16
|
export function safeFleetText(value) {
|
|
10
17
|
const text = str(value);
|
|
11
18
|
return text ? text.slice(0, 8000)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiError } from '../api.js';
|
|
2
|
-
import { componentHosts, hostReport, hostedObservation, num, observation, row, rows, safeFleetText, stamp, str, time, validFleetId } from './run-cloud-fleet-observations.js';
|
|
3
|
-
export const SURVEY_REQUEST_BUDGET_MS =
|
|
2
|
+
import { componentHosts, fleetReadError, hostReport, hostedObservation, num, observation, row, rows, safeFleetText, stamp, str, time, validFleetId } from './run-cloud-fleet-observations.js';
|
|
3
|
+
export const SURVEY_REQUEST_BUDGET_MS = 30000;
|
|
4
4
|
const ROOT = '/run-cloud/admin/simulators';
|
|
5
5
|
const RANGES = ['1h', '6h', '24h', '7d'];
|
|
6
6
|
const DEVICE_LIMIT = 512, POINT_LIMIT = 800, TYPE_LIMIT = 100;
|
|
@@ -122,19 +122,21 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
|
|
|
122
122
|
throw new Error('Invalid survey request budget');
|
|
123
123
|
const started = performance.now(), signal = AbortSignal.timeout(requestBudgetMs);
|
|
124
124
|
const read = async (path, key, body) => {
|
|
125
|
+
const sourceStarted = performance.now();
|
|
126
|
+
const timed = (source) => ({ ...source, durationSeconds: (performance.now() - sourceStarted) / 1000 });
|
|
125
127
|
try {
|
|
126
128
|
const value = body ? await client.post(path, body, { signal }) : await client.get(path, { signal });
|
|
127
129
|
const data = row(value);
|
|
128
130
|
if (value instanceof Response || !Array.isArray(data[key]))
|
|
129
|
-
return unavailable('invalid_response_shape');
|
|
131
|
+
return timed(unavailable('invalid_response_shape'));
|
|
130
132
|
if (path === '/run-cloud/admin/jenkins' && data.state === 'unavailable')
|
|
131
|
-
return { state: 'unavailable', data, reason: 'jenkins_collection_unavailable' };
|
|
133
|
+
return timed({ state: 'unavailable', data, reason: 'jenkins_collection_unavailable' });
|
|
132
134
|
if (path === '/run-cloud/admin/fleet-operations' && (data.unavailable === true || data.configured === false))
|
|
133
|
-
return { state: 'unavailable', data, reason: 'operation_storage_unavailable_or_unconfigured' };
|
|
134
|
-
return { state: 'available', data, reason: null };
|
|
135
|
+
return timed({ state: 'unavailable', data, reason: 'operation_storage_unavailable_or_unconfigured' });
|
|
136
|
+
return timed({ state: 'available', data, reason: null });
|
|
135
137
|
}
|
|
136
138
|
catch (error) {
|
|
137
|
-
return unavailable(error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? '
|
|
139
|
+
return timed(unavailable(error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? 'request_deadline_exceeded' : 'read_failed'));
|
|
138
140
|
}
|
|
139
141
|
};
|
|
140
142
|
const fleetPromise = read(ROOT, 'hosts');
|
|
@@ -163,7 +165,7 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
|
|
|
163
165
|
fleetPromise, inventoryPromise, sharedPromise, jenkinsPromise, operationsPromise, analyticsPromise, lagPromise,
|
|
164
166
|
]);
|
|
165
167
|
if (fleet.state !== 'available')
|
|
166
|
-
throw
|
|
168
|
+
throw fleetReadError(fleet.reason, requestBudgetMs);
|
|
167
169
|
now ??= Date.now();
|
|
168
170
|
const live = new Map(rows(fleet.data.hosts).filter(host => validFleetId(host.id)).map(host => [String(host.id), host]));
|
|
169
171
|
const canonical = new Map(rows(inventory.data.hosts).filter(host => validFleetId(host.id)).map(host => [String(host.id), host]));
|
|
@@ -187,7 +189,7 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
|
|
|
187
189
|
const alerts = alertReport(analytics, now, range, options.hostId);
|
|
188
190
|
const sources = Object.fromEntries(Object.entries({ fleet, inventory, shared, jenkins, operations,
|
|
189
191
|
analytics: { ...analytics, state: alerts.state, reason: alerts.reason }, inputLag: { ...lag, state: inputLag.state, reason: inputLag.reason } }).map(([key, source]) => [key, {
|
|
190
|
-
state: source.state, reason: source.reason, generatedAt: stamp(source.data.generatedAt ?? source.data.observedAt),
|
|
192
|
+
state: source.state, reason: source.reason, durationSeconds: source.durationSeconds ?? null, generatedAt: stamp(source.data.generatedAt ?? source.data.observedAt),
|
|
191
193
|
responseAgeSeconds: stamp(source.data.generatedAt ?? source.data.observedAt) ? (now - time(source.data.generatedAt ?? source.data.observedAt)) / 1000 : null,
|
|
192
194
|
meaning: 'response/collector generation time; does not renew component observations',
|
|
193
195
|
}]));
|
|
@@ -214,8 +216,9 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
|
|
|
214
216
|
function age(value) { return `${value.state}, age ${value.ageSeconds === null ? 'unknown' : `${Math.round(value.ageSeconds)}s`}${value.reason ? `, ${value.reason}` : ''}`; }
|
|
215
217
|
function bytes(value) { return value === null ? 'unknown' : `${(value / 1024 ** 3).toFixed(1)} GiB`; }
|
|
216
218
|
export function surveyText(report) {
|
|
217
|
-
const lines = [`Fleet survey · ${report.scope} · ${report.generatedAt} · ${report.durationSeconds.toFixed(2)}s (
|
|
219
|
+
const lines = [`Fleet survey · ${report.scope} · ${report.generatedAt} · ${report.durationSeconds.toFixed(2)}s (read deadline ${report.requestBudgetSeconds}s)`,
|
|
218
220
|
`${report.summary.hosts} machine records (${report.summary.canonicalHosts ?? 'unknown'} canonical); ${report.coverage.sourcesAvailable}/7 sources available.`,
|
|
221
|
+
`Source read times: ${Object.entries(report.sources).map(([name, source]) => `${name}=${source.durationSeconds === null ? 'not requested' : `${source.durationSeconds.toFixed(2)}s`}`).join(', ')}.`,
|
|
219
222
|
`Diagnosis states: ${Object.entries(report.summary.diagnosisStates).map(([state, count]) => `${state}=${count}`).join(', ')}.`,
|
|
220
223
|
`CPU mean: ${report.summary.cpu.meanBusyPercent?.toFixed(1) ?? 'unknown'}% from ${report.summary.cpu.freshHosts}/${report.summary.cpu.totalHosts} fresh host samples. ${report.summary.cpu.meaning}.`];
|
|
221
224
|
if (report.alerts.state === 'available') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { row, rows, str, num, stamp, time, pick, observation, componentHosts, hostReport, safeFleetText, validFleetId } from './run-cloud-fleet-observations.js';
|
|
1
|
+
import { row, rows, str, num, stamp, time, pick, observation, componentHosts, fleetReadError, hostReport, safeFleetText, validFleetId } from './run-cloud-fleet-observations.js';
|
|
2
2
|
export { safeFleetText } from './run-cloud-fleet-observations.js';
|
|
3
3
|
import { readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { ApiClient, ApiError } from '../api.js';
|
|
@@ -25,14 +25,15 @@ function validate(options) {
|
|
|
25
25
|
bound(options.eventPages, 1, 10, 'event-pages');
|
|
26
26
|
}
|
|
27
27
|
async function readSource(client, path) {
|
|
28
|
+
const signal = AbortSignal.timeout(30000);
|
|
28
29
|
try {
|
|
29
|
-
const data = await client.get(path, { signal
|
|
30
|
+
const data = await client.get(path, { signal });
|
|
30
31
|
if (!data || typeof data !== 'object' || Array.isArray(data) || data instanceof Response)
|
|
31
32
|
return { state: 'unavailable', data: {}, reason: 'invalid_json_response' };
|
|
32
33
|
return { state: 'available', data: row(data), reason: null };
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
35
|
-
return { state: 'unavailable', data: {}, reason: error instanceof ApiError ? `HTTP ${error.status}` : 'read_failed' };
|
|
36
|
+
return { state: 'unavailable', data: {}, reason: error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? 'request_deadline_exceeded' : 'read_failed' };
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
export async function readFleetEvents(client, options = {}) {
|
|
@@ -169,7 +170,7 @@ export async function inspectFleet(client, options = {}, now) {
|
|
|
169
170
|
query.set('include', 'details');
|
|
170
171
|
const fleet = await readSource(client, ROOT);
|
|
171
172
|
if (fleet.state === 'unavailable' || !Array.isArray(fleet.data.hosts))
|
|
172
|
-
throw
|
|
173
|
+
throw fleetReadError(fleet.reason, 30000);
|
|
173
174
|
const [shared, diagnostics, events, history, jenkins] = await Promise.all([
|
|
174
175
|
readSource(client, `${ROOT}/operator-inventory`),
|
|
175
176
|
readSource(client, `${ROOT}/diagnostics?${query}`),
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerPull } from './commands/pull.js';
|
|
|
10
10
|
import { registerDeploy } from './commands/deploy.js';
|
|
11
11
|
import { registerMisc } from './commands/misc.js';
|
|
12
12
|
import { registerStack } from './commands/stack.js';
|
|
13
|
+
import { registerGithub } from './commands/github.js';
|
|
13
14
|
import { registerCreds } from './commands/creds.js';
|
|
14
15
|
import { registerBuild } from './commands/build.js';
|
|
15
16
|
import { registerServices } from './commands/services.js';
|
|
@@ -37,6 +38,7 @@ registerDeploy(program);
|
|
|
37
38
|
registerAi(program);
|
|
38
39
|
registerMisc(program);
|
|
39
40
|
registerStack(program);
|
|
41
|
+
registerGithub(program);
|
|
40
42
|
registerCreds(program);
|
|
41
43
|
registerBuild(program);
|
|
42
44
|
registerServices(program);
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.182';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.179.tgz
DELETED
|
Binary file
|