@norskvideo/ctl-dev-kit 0.1.84 → 0.1.86
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/conventions/build-docs.yml +102 -21
- package/conventions/build-image.yml +204 -52
- package/conventions/check-drift.ts +37 -0
- package/conventions/checks.yml +153 -39
- package/conventions/integration-testing.md +40 -0
- package/conventions/integration.yml +435 -0
- package/conventions/publish-docs.yml +102 -21
- package/conventions/smoke.yml +102 -13
- package/conventions/sync-ctl-packages.yml +51 -13
- package/conventions/sync-dev-kit.yml +102 -26
- package/conventions/sync-drift.ts +30 -0
- package/conventions/upgrade-latest.yml +102 -26
- package/conventions/workflow-shell.ts +136 -0
- package/package.json +1 -1
|
@@ -25,3 +25,139 @@ export function bashBlockLines(yaml: string): Set<number> {
|
|
|
25
25
|
});
|
|
26
26
|
return inside;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/** The `run: |` steps that pipe into `tee` without `pipefail` set on the
|
|
30
|
+
* block's first non-comment line — each reported at its `run: |` line. The
|
|
31
|
+
* default `bash -e {0}` has no pipefail, so such a step exits with TEE's
|
|
32
|
+
* status and a dead suite reads green; `set -euo pipefail` inside a nested
|
|
33
|
+
* `bash -c '...'` body cannot reach the pipeline one shell up. Read from the
|
|
34
|
+
* raw text, not a YAML parse: what matters is the ORDER of lines in the script
|
|
35
|
+
* the runner executes. */
|
|
36
|
+
export function teeWithoutPipefail(yaml: string): { line: number; text: string }[] {
|
|
37
|
+
const out: { line: number; text: string }[] = [];
|
|
38
|
+
const lines = yaml.split("\n");
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const head = lines[i] as string;
|
|
41
|
+
if (!/^\s*run: \|\s*$/.test(head)) continue;
|
|
42
|
+
const indent = (lines[i + 1] ?? "").match(/^\s*/)?.[0].length ?? 0;
|
|
43
|
+
const body: string[] = [];
|
|
44
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
45
|
+
const line = lines[j] as string;
|
|
46
|
+
if (line.trim() === "") continue;
|
|
47
|
+
if ((line.match(/^\s*/)?.[0].length ?? 0) < indent) break;
|
|
48
|
+
body.push(line.trim());
|
|
49
|
+
}
|
|
50
|
+
if (!body.some((l) => /\|\s*tee\b/.test(l))) continue;
|
|
51
|
+
const first = body.find((l) => !l.startsWith("#"));
|
|
52
|
+
const pipefailFirst =
|
|
53
|
+
first !== undefined && /^set\s+(-[a-zA-Z]*o\s+pipefail|-euo\s+pipefail|-o\s+pipefail)/.test(first);
|
|
54
|
+
if (!pipefailFirst) out.push({ line: i + 1, text: head });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The step every convention workflow runs before `actions/checkout`, named so
|
|
60
|
+
* the guard below can find it. */
|
|
61
|
+
export const TEST_TEMP_STEP = "Route test-temp out of the workspace";
|
|
62
|
+
|
|
63
|
+
// What that step must do, and what breaks when it doesn't. All four were learned
|
|
64
|
+
// the hard way over three months on the shared DooD runners:
|
|
65
|
+
//
|
|
66
|
+
// route -- THE fix. These runners are docker-outside-of-docker, so the daemon
|
|
67
|
+
// a launch talks to is the HOST's, and a bind-mount source it has not
|
|
68
|
+
// seen is created by dockerd as ROOT on the host. --container-user
|
|
69
|
+
// does not help: it governs what the container WRITES, not what the
|
|
70
|
+
// daemon CREATES. Leave the harness on its repo-local default and
|
|
71
|
+
// every run seeds the checkout with root-owned dirs. The override
|
|
72
|
+
// must therefore point outside $GITHUB_WORKSPACE.
|
|
73
|
+
// reap -- rm cannot unlink a live mountpoint even as ROOT (EBUSY), and a
|
|
74
|
+
// RESTARTING leaked container re-creates the path as root seconds
|
|
75
|
+
// after any clean. The holders are untracked by construction (a
|
|
76
|
+
// killed run labels nothing), so they are found by mount SOURCE.
|
|
77
|
+
// base -- clearing only the CONTENTS leaves the base dir itself, and an
|
|
78
|
+
// empty-but-root-owned base still EACCESes a later mkdtemp.
|
|
79
|
+
// verify -- the old form swallowed every failure into `2>/dev/null || true`,
|
|
80
|
+
// so a clean that removed nothing looked identical to one that
|
|
81
|
+
// worked. The checkout then died with a bare "EACCES rmdir" and no
|
|
82
|
+
// clue what held the path. It stayed that way for a week.
|
|
83
|
+
// The exported base must TRACE to $RUNNER_TEMP. Checking only that the export
|
|
84
|
+
// exists would pass a step that dutifully exports the workspace path it was
|
|
85
|
+
// already defaulting to, which is the whole bug.
|
|
86
|
+
function routesOutOfWorkspace(body: string): boolean {
|
|
87
|
+
const exported = body.match(/NORSK_CTL_TEST_TMP=(\$\{?\w+\}?|[^\s"]+)[^\n]*>> "\$GITHUB_ENV"/);
|
|
88
|
+
if (!exported) return false;
|
|
89
|
+
const value = exported[1] ?? "";
|
|
90
|
+
if (value.startsWith("$RUNNER_TEMP")) return true;
|
|
91
|
+
const name = value.replace(/^\$\{?/, "").replace(/\}$/, "");
|
|
92
|
+
return name.length > 0 && new RegExp(`\\b${name}="\\$RUNNER_TEMP/`).test(body);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const TEST_TEMP_REQUIREMENTS: { name: string; ok: (body: string) => boolean }[] = [
|
|
96
|
+
{ name: "route", ok: (b) => routesOutOfWorkspace(b) },
|
|
97
|
+
{ name: "reap", ok: (b) => /\.Mounts\b/.test(b) && /docker rm -f/.test(b) },
|
|
98
|
+
{ name: "base", ok: (b) => /rm -rf [^\n]*\/test-temp(?![\w/*])/.test(b) },
|
|
99
|
+
{ name: "verify", ok: (b) => /\[ -e "\$tt" \]/.test(b) && /\bexit 1\b/.test(b) },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** The `run: |` body of the step starting at `nameLine`, de-indented. */
|
|
103
|
+
function stepBody(lines: string[], nameLine: number): string {
|
|
104
|
+
const runAt = lines.findIndex((l, i) => i > nameLine && /^\s*run: \|\s*$/.test(l));
|
|
105
|
+
if (runAt < 0) return "";
|
|
106
|
+
const indent = (lines[runAt + 1] ?? "").match(/^\s*/)?.[0].length ?? 0;
|
|
107
|
+
const body: string[] = [];
|
|
108
|
+
for (let j = runAt + 1; j < lines.length; j++) {
|
|
109
|
+
const line = lines[j] as string;
|
|
110
|
+
if (line.trim() === "") continue;
|
|
111
|
+
if ((line.match(/^\s*/)?.[0].length ?? 0) < indent) break;
|
|
112
|
+
body.push(line.trim());
|
|
113
|
+
}
|
|
114
|
+
return body.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Pre-checkout test-temp steps that would leave the checkout exposed, each
|
|
118
|
+
* reported at its `- name:` line with the requirements it does not meet. */
|
|
119
|
+
export function testTempStepProblems(yaml: string): { line: number; missing: string[] }[] {
|
|
120
|
+
const lines = yaml.split("\n");
|
|
121
|
+
const out: { line: number; missing: string[] }[] = [];
|
|
122
|
+
lines.forEach((text, i) => {
|
|
123
|
+
if (!text.includes(`- name: ${TEST_TEMP_STEP}`)) return;
|
|
124
|
+
const body = stepBody(lines, i);
|
|
125
|
+
const missing = TEST_TEMP_REQUIREMENTS.filter((r) => !r.ok(body)).map((r) => r.name);
|
|
126
|
+
if (missing.length > 0) out.push({ line: i + 1, missing });
|
|
127
|
+
});
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Jobs that check out without a {@link TEST_TEMP_STEP} of their own.
|
|
132
|
+
*
|
|
133
|
+
* The per-step guard above can only inspect steps it finds BY NAME, so
|
|
134
|
+
* deleting the step -- or renaming it back -- reads as "no problems". This is
|
|
135
|
+
* the presence half. It is per JOB because both halves of the step are
|
|
136
|
+
* per-job: `$GITHUB_ENV` does not cross jobs, and neither does the runner's
|
|
137
|
+
* workspace state. Job ids are read off the two-space indent under `jobs:`
|
|
138
|
+
* rather than a YAML parse, to stay consistent with the line-oriented guards
|
|
139
|
+
* here and to keep reporting real line numbers. */
|
|
140
|
+
export function jobsMissingTestTempStep(yaml: string): string[] {
|
|
141
|
+
const lines = yaml.split("\n");
|
|
142
|
+
const bad: string[] = [];
|
|
143
|
+
let job: string | undefined;
|
|
144
|
+
let checkouts = 0;
|
|
145
|
+
let steps = 0;
|
|
146
|
+
const settle = () => {
|
|
147
|
+
if (job !== undefined && checkouts > steps) bad.push(job);
|
|
148
|
+
};
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const head = line.match(/^ {2}([A-Za-z_][\w-]*):\s*$/);
|
|
151
|
+
if (head) {
|
|
152
|
+
settle();
|
|
153
|
+
job = head[1];
|
|
154
|
+
checkouts = 0;
|
|
155
|
+
steps = 0;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (/^\s*-?\s*uses: actions\/checkout@/.test(line)) checkouts++;
|
|
159
|
+
if (line.includes(`- name: ${TEST_TEMP_STEP}`)) steps++;
|
|
160
|
+
}
|
|
161
|
+
settle();
|
|
162
|
+
return bad;
|
|
163
|
+
}
|