@janga/norna 0.7.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/LICENSE +674 -0
- package/README.md +109 -0
- package/astro.config.mjs +17 -0
- package/bin/norna.mjs +170 -0
- package/docs/README.md +48 -0
- package/docs/command-organization.md +402 -0
- package/docs/commands.md +152 -0
- package/docs/configuration.md +376 -0
- package/docs/content.md +384 -0
- package/docs/engine-development.md +164 -0
- package/docs/getting-started.md +96 -0
- package/docs/images-and-metadata.md +88 -0
- package/docs/local-development.md +61 -0
- package/docs/publishing.md +81 -0
- package/docs/site-examples-structure-note.md +105 -0
- package/docs/site-structure.md +81 -0
- package/fixtures/basic/site/.norna/generated-images.json +1 -0
- package/fixtures/basic/site/config.mjs +59 -0
- package/fixtures/basic/site/content.md +17 -0
- package/fixtures/basic/site/images/work/.gitkeep +1 -0
- package/fixtures/basic/site/public/robots.txt +2 -0
- package/fixtures/basic/site/theme.md +7 -0
- package/package.json +90 -0
- package/scripts/build-site.mjs +16 -0
- package/scripts/check-config.mjs +37 -0
- package/scripts/deploy-site.mjs +389 -0
- package/scripts/dev-local.mjs +313 -0
- package/scripts/doctor.mjs +38 -0
- package/scripts/engine-version.mjs +137 -0
- package/scripts/generate-images.mjs +369 -0
- package/scripts/init-site.mjs +249 -0
- package/scripts/lib/astro-command.mjs +34 -0
- package/scripts/lib/ci-lockfile.mjs +34 -0
- package/scripts/lib/image-dimensions.mjs +78 -0
- package/scripts/lib/presentation.mjs +72 -0
- package/scripts/lib/project-config.mjs +322 -0
- package/scripts/lib/run-command.mjs +21 -0
- package/scripts/lib/site-content.mjs +392 -0
- package/scripts/lib/site-paths.mjs +127 -0
- package/scripts/lib/typography.mjs +166 -0
- package/scripts/release.mjs +77 -0
- package/scripts/show-typography.mjs +210 -0
- package/scripts/sync-content-sections.mjs +610 -0
- package/scripts/sync-site-public.mjs +42 -0
- package/scripts/test-ci-lockfile.mjs +65 -0
- package/scripts/test-content-check.mjs +364 -0
- package/scripts/test-engine-commands.mjs +128 -0
- package/scripts/test-navigation-preview.mjs +108 -0
- package/scripts/test-navigation.mjs +116 -0
- package/scripts/test-package-check.mjs +394 -0
- package/scripts/test-site-public.mjs +85 -0
- package/scripts/test-temporary-visibility.mjs +99 -0
- package/scripts/update-engine.mjs +127 -0
- package/scripts/watch-pages-deploy.mjs +430 -0
- package/src/components/GalleryGrid.astro +221 -0
- package/src/components/SiteNavigation.astro +410 -0
- package/src/components/SitePage.astro +69 -0
- package/src/components/SiteSection.astro +174 -0
- package/src/content.config.ts +171 -0
- package/src/layouts/BaseLayout.astro +90 -0
- package/src/lib/generatedImages.ts +63 -0
- package/src/lib/sectionContent.ts +125 -0
- package/src/lib/sitePages.ts +80 -0
- package/src/lib/sitePublicAssets.ts +39 -0
- package/src/lib/visibility.ts +35 -0
- package/src/pages/[slug].astro +31 -0
- package/src/pages/index.astro +16 -0
- package/src/styles/global.css +872 -0
- package/starters/basic/.github/workflows/deploy.yml +65 -0
- package/starters/basic/README.md +55 -0
- package/starters/basic/package.json +35 -0
- package/starters/basic/site/config.mjs +60 -0
- package/starters/basic/site/content.md +21 -0
- package/starters/basic/site/images/work/.gitkeep +1 -0
- package/starters/basic/site/public/robots.txt +2 -0
- package/starters/basic/site/theme.md +53 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { runInherit } from './lib/run-command.mjs';
|
|
6
|
+
import { normalizeCiLockfile, verifyCiLockfile } from './lib/ci-lockfile.mjs';
|
|
7
|
+
import {
|
|
8
|
+
siteProjectRoot,
|
|
9
|
+
siteProjectRootLabel,
|
|
10
|
+
} from './lib/site-paths.mjs';
|
|
11
|
+
|
|
12
|
+
const packageName = '@janga/norna';
|
|
13
|
+
const usage = `
|
|
14
|
+
Usage: norna engine:update [version|latest] [--skip-checks]
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
norna engine:update
|
|
18
|
+
norna engine:update latest
|
|
19
|
+
norna engine:update 0.1.15
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--skip-checks Update package files without running config/content/build checks
|
|
23
|
+
`.trim();
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
let targetVersion = 'latest';
|
|
27
|
+
let hasTargetVersion = false;
|
|
28
|
+
let skipChecks = false;
|
|
29
|
+
|
|
30
|
+
for (const arg of args) {
|
|
31
|
+
if (arg === '--skip-checks') {
|
|
32
|
+
skipChecks = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (arg.startsWith('-')) {
|
|
37
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hasTargetVersion) {
|
|
41
|
+
throw new Error(`Unexpected argument: ${arg}\n${usage}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
targetVersion = arg;
|
|
45
|
+
hasTargetVersion = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const readJson = async (filePath) => JSON.parse(await readFile(filePath, 'utf8'));
|
|
49
|
+
|
|
50
|
+
const sitePackageJsonPath = path.join(siteProjectRoot, 'package.json');
|
|
51
|
+
const sitePackageJson = await readJson(sitePackageJsonPath);
|
|
52
|
+
const dependencyVersion = sitePackageJson.dependencies?.[packageName]
|
|
53
|
+
?? sitePackageJson.devDependencies?.[packageName]
|
|
54
|
+
?? sitePackageJson.optionalDependencies?.[packageName];
|
|
55
|
+
|
|
56
|
+
if (sitePackageJson.name === packageName) {
|
|
57
|
+
throw new Error('engine:update must be run from a site repository, not from the norna engine repository.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!dependencyVersion) {
|
|
61
|
+
throw new Error(`${siteProjectRootLabel}/package.json does not declare ${packageName}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
65
|
+
const configuredNpmCache = process.env.npm_config_cache ?? process.env.NPM_CONFIG_CACHE;
|
|
66
|
+
const npmCachePath = path.resolve(siteProjectRoot, configuredNpmCache ?? path.join('node_modules', '.cache', 'norna-npm'));
|
|
67
|
+
const nornaBin = path.join(
|
|
68
|
+
siteProjectRoot,
|
|
69
|
+
'node_modules',
|
|
70
|
+
'.bin',
|
|
71
|
+
process.platform === 'win32' ? 'norna.cmd' : 'norna',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
console.log(`Updating ${packageName} in ${siteProjectRootLabel}`);
|
|
75
|
+
console.log(`Current dependency: ${dependencyVersion}`);
|
|
76
|
+
console.log(`Target version: ${targetVersion}`);
|
|
77
|
+
console.log(`npm cache: ${npmCachePath}`);
|
|
78
|
+
|
|
79
|
+
await mkdir(npmCachePath, { recursive: true });
|
|
80
|
+
await runInherit(npmBin, [
|
|
81
|
+
'install',
|
|
82
|
+
`${packageName}@${targetVersion}`,
|
|
83
|
+
'--save-exact',
|
|
84
|
+
'--fetch-retries=0',
|
|
85
|
+
], {
|
|
86
|
+
cwd: siteProjectRoot,
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
npm_config_cache: npmCachePath,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log('Normalizing package-lock.json for the GitHub Actions Linux npm environment.');
|
|
94
|
+
await normalizeCiLockfile(siteProjectRoot, {
|
|
95
|
+
env: {
|
|
96
|
+
...process.env,
|
|
97
|
+
npm_config_cache: npmCachePath,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log('Verifying package-lock.json with a clean GitHub Actions npm install.');
|
|
102
|
+
await verifyCiLockfile(siteProjectRoot, {
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
npm_config_cache: npmCachePath,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (skipChecks) {
|
|
110
|
+
console.log('Skipped checks.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!existsSync(nornaBin)) {
|
|
115
|
+
throw new Error(`Installed norna binary was not found: ${nornaBin}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const command of ['config:check', 'content:check', 'build']) {
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(`Running norna ${command}`);
|
|
121
|
+
await runInherit(nornaBin, [command], {
|
|
122
|
+
cwd: siteProjectRoot,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log('Engine update complete. Commit package.json and package-lock.json together.');
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { projectConfig } from './lib/project-config.mjs';
|
|
4
|
+
import { siteProjectRoot } from './lib/site-paths.mjs';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
const root = siteProjectRoot;
|
|
9
|
+
const defaultRepo = projectConfig.github.repo;
|
|
10
|
+
const defaultWorkflow = projectConfig.github.pagesWorkflow;
|
|
11
|
+
const defaultBranch = projectConfig.github.branch;
|
|
12
|
+
const defaultSiteUrl = projectConfig.site.url;
|
|
13
|
+
const defaultPollIntervalMs = projectConfig.deploy.watch.intervalMs;
|
|
14
|
+
const defaultTimeoutMs = projectConfig.deploy.watch.timeoutMs;
|
|
15
|
+
const defaultRunLimit = projectConfig.deploy.watch.runLimit;
|
|
16
|
+
const runListFields = [
|
|
17
|
+
'conclusion',
|
|
18
|
+
'createdAt',
|
|
19
|
+
'databaseId',
|
|
20
|
+
'displayTitle',
|
|
21
|
+
'event',
|
|
22
|
+
'headBranch',
|
|
23
|
+
'headSha',
|
|
24
|
+
'name',
|
|
25
|
+
'startedAt',
|
|
26
|
+
'status',
|
|
27
|
+
'updatedAt',
|
|
28
|
+
'url',
|
|
29
|
+
'workflowName',
|
|
30
|
+
].join(',');
|
|
31
|
+
const runViewFields = [
|
|
32
|
+
'conclusion',
|
|
33
|
+
'createdAt',
|
|
34
|
+
'databaseId',
|
|
35
|
+
'displayTitle',
|
|
36
|
+
'event',
|
|
37
|
+
'headBranch',
|
|
38
|
+
'headSha',
|
|
39
|
+
'jobs',
|
|
40
|
+
'name',
|
|
41
|
+
'startedAt',
|
|
42
|
+
'status',
|
|
43
|
+
'updatedAt',
|
|
44
|
+
'url',
|
|
45
|
+
'workflowName',
|
|
46
|
+
].join(',');
|
|
47
|
+
|
|
48
|
+
const usage = `
|
|
49
|
+
Usage: norna deploy:watch [options]
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--repo <owner/name> GitHub repository. Default: ${defaultRepo}
|
|
53
|
+
--workflow <name> Workflow name. Default: ${defaultWorkflow}
|
|
54
|
+
--branch <name> Branch to monitor. Default: ${defaultBranch}
|
|
55
|
+
--sha <sha> Commit SHA to monitor. Default: current HEAD
|
|
56
|
+
--site-url <url> Public site URL to print. Default: ${defaultSiteUrl}
|
|
57
|
+
--interval <duration> Poll interval, for example 5s or 0.5m. Default: 10s
|
|
58
|
+
--timeout <duration> Timeout, for example 15m or 900s. Default: 15m
|
|
59
|
+
--limit <count> Recent workflow runs to scan. Default: ${defaultRunLimit}
|
|
60
|
+
-h, --help Show this help.
|
|
61
|
+
`.trim();
|
|
62
|
+
|
|
63
|
+
const terminalStatus = new Set(['completed']);
|
|
64
|
+
const successfulConclusions = new Set(['success']);
|
|
65
|
+
|
|
66
|
+
const fail = (message) => {
|
|
67
|
+
console.error(message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const parseDuration = (value, optionName) => {
|
|
72
|
+
const match = String(value).trim().match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/);
|
|
73
|
+
|
|
74
|
+
if (!match) {
|
|
75
|
+
fail(`${optionName} must be a duration such as 10s, 0.5m, or 900s.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const amount = Number(match[1]);
|
|
79
|
+
const unit = match[2] ?? 's';
|
|
80
|
+
|
|
81
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
82
|
+
fail(`${optionName} must be greater than zero.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (unit === 'ms') return Math.round(amount);
|
|
86
|
+
if (unit === 's') return Math.round(amount * 1000);
|
|
87
|
+
return Math.round(amount * 60_000);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const readOptionValue = (args, index, optionName) => {
|
|
91
|
+
const value = args[index + 1];
|
|
92
|
+
|
|
93
|
+
if (!value || value.startsWith('--')) {
|
|
94
|
+
fail(`${optionName} requires a value.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const parseArgs = (args) => {
|
|
101
|
+
const options = {
|
|
102
|
+
branch: defaultBranch,
|
|
103
|
+
intervalMs: defaultPollIntervalMs,
|
|
104
|
+
limit: defaultRunLimit,
|
|
105
|
+
repo: defaultRepo,
|
|
106
|
+
sha: null,
|
|
107
|
+
siteUrl: defaultSiteUrl,
|
|
108
|
+
timeoutMs: defaultTimeoutMs,
|
|
109
|
+
workflow: defaultWorkflow,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
113
|
+
const arg = args[index];
|
|
114
|
+
|
|
115
|
+
if (arg === '-h' || arg === '--help') {
|
|
116
|
+
console.log(usage);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (arg === '--repo') {
|
|
121
|
+
options.repo = readOptionValue(args, index, arg);
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (arg === '--workflow') {
|
|
127
|
+
options.workflow = readOptionValue(args, index, arg);
|
|
128
|
+
index += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (arg === '--branch') {
|
|
133
|
+
options.branch = readOptionValue(args, index, arg);
|
|
134
|
+
index += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (arg === '--sha') {
|
|
139
|
+
options.sha = readOptionValue(args, index, arg);
|
|
140
|
+
index += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arg === '--site-url') {
|
|
145
|
+
options.siteUrl = readOptionValue(args, index, arg);
|
|
146
|
+
index += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (arg === '--interval') {
|
|
151
|
+
options.intervalMs = parseDuration(readOptionValue(args, index, arg), arg);
|
|
152
|
+
index += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg === '--timeout') {
|
|
157
|
+
options.timeoutMs = parseDuration(readOptionValue(args, index, arg), arg);
|
|
158
|
+
index += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (arg === '--limit') {
|
|
163
|
+
const limit = Number(readOptionValue(args, index, arg));
|
|
164
|
+
|
|
165
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
166
|
+
fail('--limit must be a positive integer.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
options.limit = limit;
|
|
170
|
+
index += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fail(`Unknown option: ${arg}\n${usage}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return options;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const formatCommandError = (command, args, error) => [
|
|
181
|
+
`${[command, ...args].join(' ')} failed.`,
|
|
182
|
+
error.stdout?.trim(),
|
|
183
|
+
error.stderr?.trim(),
|
|
184
|
+
error.message,
|
|
185
|
+
].filter(Boolean).join('\n');
|
|
186
|
+
|
|
187
|
+
const runCapture = async (command, args, options = {}) => {
|
|
188
|
+
const { allowFailure = false, maxBuffer = 1024 * 1024 * 20 } = options;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
192
|
+
cwd: root,
|
|
193
|
+
maxBuffer,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return stdout.trimEnd();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const output = [error.stdout, error.stderr].filter(Boolean).join('\n').trimEnd();
|
|
199
|
+
|
|
200
|
+
if (allowFailure) {
|
|
201
|
+
return output || error.message;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(formatCommandError(command, args, error));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const getCurrentSha = async () => (await runCapture('git', ['rev-parse', 'HEAD'])).trim();
|
|
209
|
+
|
|
210
|
+
const shortSha = (sha) => sha.slice(0, 7);
|
|
211
|
+
|
|
212
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
213
|
+
setTimeout(resolve, ms);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const formatElapsed = (ms) => {
|
|
217
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
218
|
+
const seconds = totalSeconds % 60;
|
|
219
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
220
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
221
|
+
|
|
222
|
+
if (hours > 0) {
|
|
223
|
+
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const formatRunState = (run) => `${run.status ?? 'unknown'}${run.conclusion ? `/${run.conclusion}` : ''}`;
|
|
230
|
+
|
|
231
|
+
const getActionsUrl = (repo, branch) => (
|
|
232
|
+
`https://github.com/${repo}/actions?query=branch%3A${encodeURIComponent(branch)}`
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const getRecentRuns = async ({ branch, limit, repo, workflow }) => {
|
|
236
|
+
const output = await runCapture('gh', [
|
|
237
|
+
'run',
|
|
238
|
+
'list',
|
|
239
|
+
'--repo',
|
|
240
|
+
repo,
|
|
241
|
+
'--workflow',
|
|
242
|
+
workflow,
|
|
243
|
+
'--branch',
|
|
244
|
+
branch,
|
|
245
|
+
'--limit',
|
|
246
|
+
String(limit),
|
|
247
|
+
'--json',
|
|
248
|
+
runListFields,
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
return JSON.parse(output || '[]');
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const getRunDetails = async (repo, runId) => {
|
|
255
|
+
const output = await runCapture('gh', [
|
|
256
|
+
'run',
|
|
257
|
+
'view',
|
|
258
|
+
String(runId),
|
|
259
|
+
'--repo',
|
|
260
|
+
repo,
|
|
261
|
+
'--json',
|
|
262
|
+
runViewFields,
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
return JSON.parse(output || '{}');
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getFailedLogOutput = async (repo, runId) => runCapture('gh', [
|
|
269
|
+
'run',
|
|
270
|
+
'view',
|
|
271
|
+
String(runId),
|
|
272
|
+
'--repo',
|
|
273
|
+
repo,
|
|
274
|
+
'--log-failed',
|
|
275
|
+
], { allowFailure: true, maxBuffer: 1024 * 1024 * 50 });
|
|
276
|
+
|
|
277
|
+
const findRunForSha = (runs, sha) => runs.find((run) => run.headSha === sha) ?? null;
|
|
278
|
+
|
|
279
|
+
const hasTerminalState = (run) => terminalStatus.has(run.status) || Boolean(run.conclusion);
|
|
280
|
+
|
|
281
|
+
const isSuccess = (run) => successfulConclusions.has(run.conclusion);
|
|
282
|
+
|
|
283
|
+
const isProblemConclusion = (conclusion) => Boolean(conclusion) && !successfulConclusions.has(conclusion);
|
|
284
|
+
|
|
285
|
+
const getProblemJobs = (jobs = []) => jobs.filter((job) => (
|
|
286
|
+
isProblemConclusion(job.conclusion)
|
|
287
|
+
|| (job.status && job.status !== 'completed' && job.status !== 'success')
|
|
288
|
+
));
|
|
289
|
+
|
|
290
|
+
const getProblemSteps = (steps = []) => steps.filter((step) => isProblemConclusion(step.conclusion));
|
|
291
|
+
|
|
292
|
+
const getLogExcerpt = (output, maxLines = 160) => {
|
|
293
|
+
const lines = output.trim().split(/\r?\n/).filter(Boolean);
|
|
294
|
+
|
|
295
|
+
if (lines.length <= maxLines) {
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return [
|
|
300
|
+
`... ${lines.length - maxLines} earlier log lines omitted ...`,
|
|
301
|
+
...lines.slice(-maxLines),
|
|
302
|
+
].join('\n');
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const printRunSummary = (run, options, elapsedMs, stream = console.log) => {
|
|
306
|
+
stream(`${options.workflow}: ${formatRunState(run)} after ${formatElapsed(elapsedMs)}`);
|
|
307
|
+
stream(`Run ID: ${run.databaseId}`);
|
|
308
|
+
stream(`Run URL: ${run.url ?? '(unknown)'}`);
|
|
309
|
+
stream(`Actions URL: ${getActionsUrl(options.repo, options.branch)}`);
|
|
310
|
+
stream(`Branch: ${run.headBranch ?? options.branch}`);
|
|
311
|
+
stream(`Commit: ${run.headSha ?? options.sha}`);
|
|
312
|
+
stream(`Site URL: ${options.siteUrl}`);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const printFailureDetails = async (run, options, elapsedMs) => {
|
|
316
|
+
let details = run;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
details = {
|
|
320
|
+
...run,
|
|
321
|
+
...await getRunDetails(options.repo, run.databaseId),
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`Could not fetch run details: ${error.message}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.error('');
|
|
328
|
+
console.error('Deployment workflow did not complete successfully.');
|
|
329
|
+
printRunSummary(details, options, elapsedMs, console.error);
|
|
330
|
+
|
|
331
|
+
const jobs = Array.isArray(details.jobs) ? details.jobs : [];
|
|
332
|
+
const problemJobs = getProblemJobs(jobs);
|
|
333
|
+
|
|
334
|
+
if (problemJobs.length > 0) {
|
|
335
|
+
console.error('');
|
|
336
|
+
console.error('Failed or incomplete jobs:');
|
|
337
|
+
|
|
338
|
+
for (const job of problemJobs) {
|
|
339
|
+
console.error(`- ${job.name ?? '(unnamed job)'}`);
|
|
340
|
+
console.error(` Job ID: ${job.databaseId ?? '(unknown)'}`);
|
|
341
|
+
console.error(` Status: ${job.status ?? 'unknown'}${job.conclusion ? `/${job.conclusion}` : ''}`);
|
|
342
|
+
console.error(` Job URL: ${job.url ?? '(unknown)'}`);
|
|
343
|
+
|
|
344
|
+
const problemSteps = getProblemSteps(job.steps);
|
|
345
|
+
|
|
346
|
+
for (const step of problemSteps) {
|
|
347
|
+
console.error(` Step ${step.number ?? '?'}: ${step.name ?? '(unnamed step)'} (${step.conclusion})`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else if (jobs.length > 0) {
|
|
351
|
+
console.error('');
|
|
352
|
+
console.error('No failed job was reported by GitHub CLI, but the run conclusion was not success.');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const failedLog = await getFailedLogOutput(options.repo, run.databaseId);
|
|
356
|
+
const failedLogExcerpt = getLogExcerpt(failedLog);
|
|
357
|
+
|
|
358
|
+
if (failedLogExcerpt) {
|
|
359
|
+
console.error('');
|
|
360
|
+
console.error('Failed log excerpt:');
|
|
361
|
+
console.error(failedLogExcerpt);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.error('');
|
|
365
|
+
console.error(`Full failed logs: gh run view ${run.databaseId} --repo ${options.repo} --log-failed`);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const monitor = async () => {
|
|
369
|
+
const options = parseArgs(process.argv.slice(2));
|
|
370
|
+
options.sha ??= await getCurrentSha();
|
|
371
|
+
|
|
372
|
+
const startedAt = Date.now();
|
|
373
|
+
let lastRun = null;
|
|
374
|
+
|
|
375
|
+
console.log(`Monitoring ${options.workflow} for ${options.repo}`);
|
|
376
|
+
console.log(`Branch: ${options.branch}`);
|
|
377
|
+
console.log(`Commit: ${shortSha(options.sha)} (${options.sha})`);
|
|
378
|
+
console.log(`Poll interval: ${formatElapsed(options.intervalMs)}`);
|
|
379
|
+
console.log(`Timeout: ${formatElapsed(options.timeoutMs)}`);
|
|
380
|
+
console.log(`Actions URL: ${getActionsUrl(options.repo, options.branch)}`);
|
|
381
|
+
console.log('');
|
|
382
|
+
|
|
383
|
+
while (Date.now() - startedAt <= options.timeoutMs) {
|
|
384
|
+
const elapsedMs = Date.now() - startedAt;
|
|
385
|
+
const runs = await getRecentRuns(options);
|
|
386
|
+
const run = findRunForSha(runs, options.sha);
|
|
387
|
+
|
|
388
|
+
if (!run) {
|
|
389
|
+
console.log(`[${formatElapsed(elapsedMs)}] Waiting for a run for ${shortSha(options.sha)}. Checked ${runs.length} recent runs.`);
|
|
390
|
+
} else {
|
|
391
|
+
lastRun = run;
|
|
392
|
+
console.log(`[${formatElapsed(elapsedMs)}] Run ${run.databaseId}: ${formatRunState(run)} (${run.url})`);
|
|
393
|
+
|
|
394
|
+
if (hasTerminalState(run)) {
|
|
395
|
+
if (isSuccess(run)) {
|
|
396
|
+
console.log('');
|
|
397
|
+
printRunSummary(run, options, Date.now() - startedAt);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await printFailureDetails(run, options, Date.now() - startedAt);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const remainingMs = options.timeoutMs - (Date.now() - startedAt);
|
|
407
|
+
if (remainingMs <= 0) break;
|
|
408
|
+
|
|
409
|
+
await sleep(Math.min(options.intervalMs, remainingMs));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.error('');
|
|
413
|
+
console.error(`Timed out after ${formatElapsed(Date.now() - startedAt)} while waiting for ${options.workflow}.`);
|
|
414
|
+
|
|
415
|
+
if (lastRun) {
|
|
416
|
+
printRunSummary(lastRun, options, Date.now() - startedAt, console.error);
|
|
417
|
+
} else {
|
|
418
|
+
console.error(`No matching run was found for commit ${options.sha}.`);
|
|
419
|
+
console.error(`Actions URL: ${getActionsUrl(options.repo, options.branch)}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
process.exit(1);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
await monitor();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error(error.message);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|