@roomi-fields/grillhouse 0.0.0-stage → 0.23.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/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A closed ticket keeps its one delivery: refuses a commit message whose title line names a ticket
|
|
3
|
+
// an earlier commit already delivers, unless that ticket is open in Beads. An open ticket that
|
|
4
|
+
// already has its delivery is a ticket reopened for a fix (by the queue or the CI): the commit is a
|
|
5
|
+
// correction and passes. A closed ticket is finished: new work opens a new ticket, created under it
|
|
6
|
+
// (`--parent`). Run by the commit-msg hook (`grillhouse hook commit-msg`, scripts/crochets.mjs), with
|
|
7
|
+
// the message file as argument.
|
|
8
|
+
//
|
|
9
|
+
// - A ticket is open when its Beads status (`bd show <id> --json`) is anything but `closed`; a
|
|
10
|
+
// status that cannot be read (unknown id, unreadable answer, no `status`) is not open.
|
|
11
|
+
// - A commit delivers the tickets its title line names; a ticket named only in a body (a
|
|
12
|
+
// reference, a `Refs:` trailer) is not delivered.
|
|
13
|
+
// The title line of a message is its first line that is neither blank nor a comment (a line
|
|
14
|
+
// starting with `#`); in the history it is the first line of the commit message.
|
|
15
|
+
// - The tickets are the words `<prefix>-<id>` of the title line, the prefix read from Beads
|
|
16
|
+
// (`bd config get issue_prefix`).
|
|
17
|
+
// - A message that names no ticket passes (the supervisor's commits of the frame).
|
|
18
|
+
// - A reverting commit delivers nothing and passes: it carries both marks git writes, a title line
|
|
19
|
+
// starting `Revert "` or `Reapply "` and a body line `This reverts commit <sha>` (the sha is the one
|
|
20
|
+
// after `reverts commit`, not the one after `changes made to`). It withdraws the commit it names:
|
|
21
|
+
// a delivery counts when no effective reverting commit, on any branch, names it, and a reverting
|
|
22
|
+
// commit is effective when no effective reverting commit names it (a chain reads one link at a
|
|
23
|
+
// time: a revert of a revert, which git writes `Reapply "…"`, makes the first delivery count again).
|
|
24
|
+
// - The commits counted are those of the branches (`refs/heads`) alone: no stash, no remote-tracking
|
|
25
|
+
// reference, no tag. The branch `agent/<ticket>` of the ticket itself does not count for that
|
|
26
|
+
// ticket: the integration commits there before the queue brings the commit onto main, which
|
|
27
|
+
// then holds the one delivery; the branch of another ticket counts.
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const escape = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
|
|
34
|
+
// The title line of a message: its first line neither blank nor a comment, or ''.
|
|
35
|
+
function title(message) {
|
|
36
|
+
return message.split('\n').find(l => l.trim() !== '' && !l.startsWith('#')) ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The ticket ids a line names, without duplicates.
|
|
40
|
+
function named(line, prefix) {
|
|
41
|
+
const re = new RegExp(`(?<![\\w.-])${escape(prefix)}-[a-z0-9]+(?:\\.[0-9]+)*(?![\\w-])`, 'g');
|
|
42
|
+
return [...new Set(line.match(re) ?? [])];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The ticket ids the title line of a message names, without duplicates.
|
|
46
|
+
export function tickets(message, prefix) {
|
|
47
|
+
return named(title(message), prefix);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const REVERT_TITLE = /^(?:Revert|Reapply) "/;
|
|
51
|
+
const REVERTS_LINE = /^This reverts commit ([0-9a-f]{40})\b/m;
|
|
52
|
+
|
|
53
|
+
// The full sha a reverting message names, or null when the message is not a reverting commit (it
|
|
54
|
+
// lacks the title mark or the body mark).
|
|
55
|
+
function revertedBy(message) {
|
|
56
|
+
if (!REVERT_TITLE.test(title(message))) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return REVERTS_LINE.exec(message)?.[1] ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The refusal for this message, or null. `commitsOf(id)` gives the earlier commits delivering the id;
|
|
63
|
+
// `isOpen(id)` says whether the ticket is open (absent: not open). A reverting message delivers
|
|
64
|
+
// nothing and passes; a ticket that is open takes a correction commit and passes.
|
|
65
|
+
export function refusal(message, prefix, commitsOf, isOpen = () => false) {
|
|
66
|
+
if (revertedBy(message) !== null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
for (const id of tickets(message, prefix)) {
|
|
70
|
+
const earlier = commitsOf(id);
|
|
71
|
+
if (earlier.length > 0 && !isOpen(id)) {
|
|
72
|
+
return `Le ticket ${id} a déjà son commit (${earlier.join(', ')}) : un ticket fait une seule livraison. Ce qui reste devient un ticket neuf sous ${id} (bd create --parent ${id}).`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function prefixe() {
|
|
79
|
+
try {
|
|
80
|
+
return execFileSync('bd', ['config', 'get', 'issue_prefix'], {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
stdio: 'pipe',
|
|
83
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
84
|
+
}).trim();
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Whether the ticket is open in Beads: its `status` is anything but `closed`. A status that cannot
|
|
91
|
+
// be read is not open.
|
|
92
|
+
function ticketIsOpen(id) {
|
|
93
|
+
try {
|
|
94
|
+
const out = execFileSync('bd', ['show', id, '--json'], {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: 'pipe',
|
|
97
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
98
|
+
});
|
|
99
|
+
const status = JSON.parse(out)?.[0]?.status;
|
|
100
|
+
return typeof status === 'string' && status !== 'closed';
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `git log` with these arguments, its output as text.
|
|
107
|
+
function log(...args) {
|
|
108
|
+
return execFileSync('git', ['log', ...args], { encoding: 'utf8' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The reverting commits of the history, over the branches, as a map from a commit's full sha to the
|
|
112
|
+
// full sha it reverts. Git keeps the commits whose message holds a `This reverts commit` line and a
|
|
113
|
+
// `Revert "` or `Reapply "` line; the output grows with those commits alone.
|
|
114
|
+
function revertingCommits() {
|
|
115
|
+
const out = log(
|
|
116
|
+
'--branches',
|
|
117
|
+
'--all-match',
|
|
118
|
+
'-P',
|
|
119
|
+
'--grep=^This reverts commit [0-9a-f]{40}',
|
|
120
|
+
'--grep=^(?:Revert|Reapply) "',
|
|
121
|
+
'--format=%x1e%H%n%B'
|
|
122
|
+
);
|
|
123
|
+
const map = new Map();
|
|
124
|
+
for (const entry of out.split('\x1e').filter(Boolean)) {
|
|
125
|
+
const [hash, ...message] = entry.split('\n');
|
|
126
|
+
const target = revertedBy(message.join('\n'));
|
|
127
|
+
if (target !== null) {
|
|
128
|
+
map.set(hash, target);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return map;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The commits among `candidates` (`{ full, short }`) that no effective reverting commit names, and
|
|
135
|
+
// that are not reverting commits themselves. A reverting commit is effective when no effective
|
|
136
|
+
// reverting commit names it.
|
|
137
|
+
function withoutWithdrawn(candidates) {
|
|
138
|
+
const reverts = revertingCommits();
|
|
139
|
+
const namedBy = new Map();
|
|
140
|
+
for (const [hash, target] of reverts) {
|
|
141
|
+
namedBy.set(target, [...(namedBy.get(target) ?? []), hash]);
|
|
142
|
+
}
|
|
143
|
+
const memo = new Map();
|
|
144
|
+
const withdrawn = target => (namedBy.get(target) ?? []).some(effective);
|
|
145
|
+
const effective = hash => {
|
|
146
|
+
if (!memo.has(hash)) {
|
|
147
|
+
memo.set(hash, !withdrawn(hash));
|
|
148
|
+
}
|
|
149
|
+
return memo.get(hash);
|
|
150
|
+
};
|
|
151
|
+
return candidates.filter(c => !reverts.has(c.full) && !withdrawn(c.full));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The commits delivering `id`, over every branch but its own `agent/<id>`: those whose message's first line names it, less
|
|
155
|
+
// the reverting commits and the deliveries they withdraw (`withoutWithdrawn`). Git
|
|
156
|
+
// keeps the commits whose message quotes the id (`--grep`, bounded like an id) and renders their
|
|
157
|
+
// subject (`%s`, the first paragraph with its lines joined by a space); a subject naming the id
|
|
158
|
+
// makes a candidate. Git then renders each candidate's message cut two columns past the byte length
|
|
159
|
+
// of its subject (`%<(N,trunc)%B`), which keeps its first line whole; that line decides. The output
|
|
160
|
+
// grows with the commits quoting the id: by their first paragraphs, and by one byte per line of
|
|
161
|
+
// their bodies, since the cut keeps every line break.
|
|
162
|
+
function commitsDelivering(id, prefix) {
|
|
163
|
+
const subjects = log(
|
|
164
|
+
`--exclude=agent/${id}`,
|
|
165
|
+
'--branches',
|
|
166
|
+
'-P',
|
|
167
|
+
`--grep=(?<![\\w.-])${escape(id)}(?![\\w-]|\\.\\w)`,
|
|
168
|
+
'--format=%H %h %s'
|
|
169
|
+
)
|
|
170
|
+
.split('\n')
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map(l => {
|
|
173
|
+
const [full, short, ...subject] = l.split(' ');
|
|
174
|
+
return { hash: short, full, subject: subject.join(' ') };
|
|
175
|
+
})
|
|
176
|
+
.filter(c => named(c.subject, prefix).includes(id));
|
|
177
|
+
if (subjects.length === 0) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const width = Math.max(...subjects.map(c => Buffer.byteLength(c.subject))) + 2;
|
|
181
|
+
const delivering = log(
|
|
182
|
+
'--no-walk=unsorted',
|
|
183
|
+
`--format=%x1e%h%n%<(${width},trunc)%B`,
|
|
184
|
+
...subjects.map(c => c.hash)
|
|
185
|
+
)
|
|
186
|
+
.split('\x1e')
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.map(entry => entry.split('\n'))
|
|
189
|
+
.filter(([, first = '']) => named(first, prefix).includes(id))
|
|
190
|
+
.map(([hash]) => hash);
|
|
191
|
+
return withoutWithdrawn(
|
|
192
|
+
delivering.map(short => ({ short, full: subjects.find(c => c.hash === short).full }))
|
|
193
|
+
).map(c => c.short);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
197
|
+
const prefix = prefixe();
|
|
198
|
+
// Without a prefix, or with one that is not yet filled in, every commit passes.
|
|
199
|
+
const reason =
|
|
200
|
+
prefix && !prefix.includes(' ')
|
|
201
|
+
? refusal(
|
|
202
|
+
readFileSync(process.argv[2], 'utf8'),
|
|
203
|
+
prefix,
|
|
204
|
+
id => commitsDelivering(id, prefix),
|
|
205
|
+
ticketIsOpen
|
|
206
|
+
)
|
|
207
|
+
: null;
|
|
208
|
+
if (reason) {
|
|
209
|
+
process.stderr.write(`⛔ ${reason}\n`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npm run verifier`: the full pass, what the integration, the queue, the continuous integration
|
|
3
|
+
// and the night play. Its verdict is green (0), red (1) or « à repasser » (75).
|
|
4
|
+
//
|
|
5
|
+
// npx grillhouse verifier [<argument of Turborepo>…] (npm run verifier -- --force)
|
|
6
|
+
// npx grillhouse verifier --tout [<argument>…] every test, played for real
|
|
7
|
+
// npx grillhouse verifier --gardes [<argument>…] the guards alone (the reviewer)
|
|
8
|
+
//
|
|
9
|
+
// - A pass measures itself against the last green (scripts/dernier-vert.mjs), as
|
|
10
|
+
// `vitest --changed` and `jest --onlyChanged` do: the tree of the last pass green on every
|
|
11
|
+
// package, kept per branch (main's for a copy that has none yet). When it counts (its commit an ancestor of HEAD), each package
|
|
12
|
+
// Turborepo plays again plays the tests Vitest relates to the files changed since it, or all
|
|
13
|
+
// its tests when a change lies out of Vitest's graph (scripts/vitest-config/changements.mjs);
|
|
14
|
+
// a package whose inputs did not move replays from Turborepo's cache, the guards play whole,
|
|
15
|
+
// and so do the builds. The changes reach Vitest in a variable no hash counts
|
|
16
|
+
// (`GRILLHOUSE_CHANGEMENTS`). Without a last green that counts, the pass is complete. The
|
|
17
|
+
// line `↳` says which. A pass green, restricted by no filter, keeps its tree as the last green;
|
|
18
|
+
// a red one keeps the last green as it was.
|
|
19
|
+
// - `--tout`, and `--force` (the night, scripts/nuit.sh), play the complete pass: no measure
|
|
20
|
+
// against the last green, and each task of tests played for real (the variable of the tests
|
|
21
|
+
// carries the moment of the pass, `rejouer`, which no cached run holds).
|
|
22
|
+
// - The pass holds the lock of the machine for the heavy measure (scripts/verrou-de-machine.mjs),
|
|
23
|
+
// shared by every repository of the machine: a pass that finds it held waits for the end of the
|
|
24
|
+
// other measure, and says so every 30 s with who holds it. The wait bounded (60 min), the pass
|
|
25
|
+
// ends at 75, « à repasser »; a lock held by an ancestor of the pass ends it at once, red (1);
|
|
26
|
+
// either way without playing, the holder named.
|
|
27
|
+
// - Turborepo plays the build, then the guards (the frame's, `grillhouse:gardes`, and the
|
|
28
|
+
// project's own, its root script `gardes`) and the tests (`test:vitest`) beside each other:
|
|
29
|
+
// `turbo run grillhouse:gardes gardes test:vitest --continue=dependencies-successful
|
|
30
|
+
// --concurrency=<n> --summarize`, each argument given after it. A red does not stop the pass:
|
|
31
|
+
// every task whose prerequisites passed plays, and each red is named. <n>, the tasks side by
|
|
32
|
+
// side, is half the cores of the machine, one at least (`concurrence`): each task of tests is
|
|
33
|
+
// itself parallel (Vitest's workers), as make's `-j` and Bazel's `--local_cpu_resources` bound a
|
|
34
|
+
// build by the machine; a `--concurrency` among the arguments replaces it.
|
|
35
|
+
// - The time witnesses play alone, as Bazel plays a test tagged `exclusive`: the pass above leaves
|
|
36
|
+
// them out, then a pass of their own plays them, one task at a time, one file at a time, nothing
|
|
37
|
+
// beside them: `turbo run test:vitest --filter=<package>… --continue=dependencies-successful
|
|
38
|
+
// --concurrency=1 --summarize`, for each package that holds one (a test file that imports
|
|
39
|
+
// `mesurerAuRepos`, `fichiersTemoins` of scripts/vitest-config/cibles.mjs, which receives the
|
|
40
|
+
// choice in the variable of the tests, `GRILLHOUSE_TESTS`: `temoins: 'hors'`, then `'seuls'`).
|
|
41
|
+
// A repository without a witness plays no such pass. So a witness is measured under no load of
|
|
42
|
+
// its own pass: a load it meets comes from elsewhere on the machine.
|
|
43
|
+
// - A pass restricted by filters of Turborepo among the arguments (`--filter`, `-F`: the circuit's
|
|
44
|
+
// final pass after a review sent back, `--filter=...[<avant>...<après>] --filter=//`,
|
|
45
|
+
// scripts/circuit.mjs) plays the witnesses of the packages those filters choose alone
|
|
46
|
+
// (`turbo ls <filtres> --output=json`): the pass of the witnesses carries no filter of its own
|
|
47
|
+
// beside theirs, which Turborepo would join to them.
|
|
48
|
+
// - `--gardes` plays the two tasks of guards alone, `turbo run grillhouse:gardes gardes …`, with the
|
|
49
|
+
// same verdict: what the reviewer plays on the ticket's copy, the tests aside.
|
|
50
|
+
// - The verdict reads the exit code of each task in the summary of each run
|
|
51
|
+
// (`.turbo/runs/<id>.json`), since Turborepo exits with the greatest code of its tasks (a 75
|
|
52
|
+
// would hide a 1). The summary each run writes is removed, and `.turbo/runs` with it when the
|
|
53
|
+
// run created it; while the pass names a results directory (`GRILLHOUSE_RESULTATS`,
|
|
54
|
+
// scripts/resultats.mjs), it goes there instead, where the circuit reads it.
|
|
55
|
+
// - A task at 0 is green. A task at any other code than 0 and 75, a task at 75 that other tasks
|
|
56
|
+
// wait for, a summary that does not read while Turborepo failed: red, code 1, each task named, a
|
|
57
|
+
// task of the witnesses' pass with « (témoins de temps) ».
|
|
58
|
+
// - A task at 75 (« à repasser »: every failure of its tests depended on a duration and one took
|
|
59
|
+
// place under load, scripts/vitest-config/duree.reporter.mjs) is played again alone, once the CPU
|
|
60
|
+
// pressure is back under the threshold (scripts/vitest-config/pression.mjs; one line per 30 s at
|
|
61
|
+
// most while it waits, with the pressure), one task after the other, in the choice of its run
|
|
62
|
+
// (a task of the witnesses' pass plays its witnesses alone again):
|
|
63
|
+
// `turbo run <task> --filter=<package>`, the arguments given after it, with what it depends on,
|
|
64
|
+
// which passed in the pass and replays from the cache: its hash holds theirs, as in the pass.
|
|
65
|
+
// Green at rest, it is green; at any code other
|
|
66
|
+
// than 0 and 75, red; at 75 again (the load came back during the run), it waits and plays again.
|
|
67
|
+
// - The wait is bounded: 15 min after the pass. The bound reached, the verdict is 75, « à
|
|
68
|
+
// repasser »: neither green nor red; whoever launched the pass presents it again later
|
|
69
|
+
// (scripts/integration/integrer.mjs, scripts/integration/file.mjs, scripts/nuit.sh).
|
|
70
|
+
// - The progress of the pass (scripts/avancement.mjs): the directory its caller names in
|
|
71
|
+
// GRILLHOUSE_AVANCEMENT (the circuit's), else one of its own, which it names beside the lock of
|
|
72
|
+
// the machine while it plays (its repository, its command), as a measure out of a ticket the
|
|
73
|
+
// board shows, and removes at its end, its total kept. Before its first run of Turborepo, the
|
|
74
|
+
// plan of each run that plays tests (`turbo run … --dry=json`) names their tasks and counts done
|
|
75
|
+
// the files of those Turborepo replays from its cache.
|
|
76
|
+
// - The red tasks: while the variable `GRILLHOUSE_ROUGES` (`ROUGES`) names a file, the pass writes
|
|
77
|
+
// there, one Turborepo task id per line (`<package>#<task>`, `//#<task>` for the root), each task
|
|
78
|
+
// red on its own exit code, a task red at rest included, once each; a task à repasser (75) is
|
|
79
|
+
// none of them, nor a task that did not play (its prerequisite red is). Green or à repasser, the
|
|
80
|
+
// file is empty. The night reads it for its bisect commands (scripts/nuit.sh), with no second
|
|
81
|
+
// reading of the summaries.
|
|
82
|
+
// - The pass has a base of its own (scripts/vitest-config/base-de-passe.mjs), in the temporary
|
|
83
|
+
// directory of its environment, which it sweeps first of the bases a killed pass left: every
|
|
84
|
+
// run of Turborepo gets it in TMPDIR, which the root's `globalPassThroughEnv` passes on to every
|
|
85
|
+
// task (scripts/turbo-des-paquets.mjs), and the pass removes it at its end, green or red.
|
|
86
|
+
// - Every run plays without the paths of a targeted run's tests: the variable `GRILLHOUSE_TESTS`
|
|
87
|
+
// (scripts/vitest-config/cibles.mjs) carries only the choice of the witnesses (and `rejouer` for
|
|
88
|
+
// a complete pass).
|
|
89
|
+
import { spawnSync } from 'node:child_process';
|
|
90
|
+
import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
91
|
+
import { availableParallelism, tmpdir } from 'node:os';
|
|
92
|
+
import path from 'node:path';
|
|
93
|
+
import { pathToFileURL } from 'node:url';
|
|
94
|
+
import { ecrireLaMesure, garderLeTotal, ouvrirAvancement, planifier } from './avancement.mjs';
|
|
95
|
+
import { composantDeLEnveloppe } from './enveloppe/composant.mjs';
|
|
96
|
+
import { paquets } from './paquets.mjs';
|
|
97
|
+
import { AttenteDeLaMachine, avecLaMachine } from './verrou-de-machine.mjs';
|
|
98
|
+
import {
|
|
99
|
+
A_REPASSER,
|
|
100
|
+
attendreLeRepos,
|
|
101
|
+
BORNE_ATTENTE_MS,
|
|
102
|
+
lirePression,
|
|
103
|
+
} from './vitest-config/pression.mjs';
|
|
104
|
+
import { garderLesResumes } from './resultats.mjs';
|
|
105
|
+
import { fichiersTemoins, VARIABLE as TESTS_CIBLES } from './vitest-config/cibles.mjs';
|
|
106
|
+
import { AVANCEMENT } from './vitest-config/avancement.mjs';
|
|
107
|
+
import { CHANGEMENTS } from './vitest-config/changements.mjs';
|
|
108
|
+
import { garderLeVert, mesure } from './dernier-vert.mjs';
|
|
109
|
+
import { ouvrirBase } from './vitest-config/base-de-passe.mjs';
|
|
110
|
+
|
|
111
|
+
const GARDES = ['grillhouse:gardes', 'gardes'];
|
|
112
|
+
const TACHES = [...GARDES, 'test:vitest'];
|
|
113
|
+
const TEMOINS = ' (témoins de temps)';
|
|
114
|
+
const CONCURRENCE = /^--concurrency(?:=|$)/;
|
|
115
|
+
// The variable that names the file of the red tasks, which the night reads.
|
|
116
|
+
const ROUGES = 'GRILLHOUSE_ROUGES';
|
|
117
|
+
|
|
118
|
+
const lister = dossier => (existsSync(dossier) ? readdirSync(dossier) : []);
|
|
119
|
+
|
|
120
|
+
// The filters of Turborepo among <args> (`--filter=<f>`, `--filter <f>`, `-F <f>`) and the other
|
|
121
|
+
// arguments: `{ filtres, autres }`, the filters each written `--filter=<f>`.
|
|
122
|
+
function separerLesFiltres(args) {
|
|
123
|
+
const filtres = [];
|
|
124
|
+
const autres = [];
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
if (args[i].startsWith('--filter=')) filtres.push(args[i]);
|
|
127
|
+
else if ((args[i] === '--filter' || args[i] === '-F') && i + 1 < args.length)
|
|
128
|
+
filtres.push(`--filter=${args[++i]}`);
|
|
129
|
+
else autres.push(args[i]);
|
|
130
|
+
}
|
|
131
|
+
return { filtres, autres };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The names of the packages of <racine> the filters <filtres> choose, as Turborepo lists them.
|
|
135
|
+
function paquetsChoisis(racine, filtres, env) {
|
|
136
|
+
const r = spawnSync('turbo', ['ls', ...filtres, '--output=json'], {
|
|
137
|
+
cwd: racine,
|
|
138
|
+
env,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
return new Set(JSON.parse(r.stdout).packages.items.map(p => p.name));
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The tasks a pass plays side by side on a machine of <coeurs> cores: half of them, one at least.
|
|
150
|
+
const concurrence = (coeurs = availableParallelism()) => Math.max(1, Math.floor(coeurs / 2));
|
|
151
|
+
|
|
152
|
+
// <args> without a `--concurrency` (`--concurrency=<n>` or `--concurrency <n>`).
|
|
153
|
+
function sansConcurrence(args) {
|
|
154
|
+
const out = [];
|
|
155
|
+
for (let i = 0; i < args.length; i++) {
|
|
156
|
+
if (args[i] === '--concurrency') i++;
|
|
157
|
+
else if (!CONCURRENCE.test(args[i])) out.push(args[i]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The filters of the packages of <racine> that hold a time witness, `--filter=<name>` each; in a
|
|
163
|
+
// repository without packages, none when its root holds one. Null without a witness.
|
|
164
|
+
function filtresDesTemoins(racine) {
|
|
165
|
+
const liste = paquets(racine);
|
|
166
|
+
if (liste.length === 0) return fichiersTemoins(racine).length ? [] : null;
|
|
167
|
+
const noms = liste
|
|
168
|
+
.filter(p => !p.hors && p.pkg.scripts?.['test:vitest'])
|
|
169
|
+
.filter(p => fichiersTemoins(path.resolve(racine, p.dossier)).length > 0)
|
|
170
|
+
.map(p => p.pkg.name);
|
|
171
|
+
return noms.length ? noms.map(n => `--filter=${n}`) : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The full pass in <racine>, its arguments <args> (`--gardes` among them: the guards alone), under
|
|
175
|
+
// <env> and the lock of the machine; its verdict, 0, 1 or 75 (the stop of the wait for the lock
|
|
176
|
+
// included). `dire` writes a line; `borneMs` and
|
|
177
|
+
// `pasMs` are the bound of the wait and the gap between two readings of the pressure; `coeurs`,
|
|
178
|
+
// the cores of the machine.
|
|
179
|
+
export function verifier({
|
|
180
|
+
racine = process.cwd(),
|
|
181
|
+
args: demandes = [],
|
|
182
|
+
env = process.env,
|
|
183
|
+
borneMs = BORNE_ATTENTE_MS,
|
|
184
|
+
pasMs = 2_000,
|
|
185
|
+
dire = l => process.stdout.write(`${l}\n`),
|
|
186
|
+
coeurs = availableParallelism(),
|
|
187
|
+
}) {
|
|
188
|
+
// The base of the pass, in the temporary directory of <env>: every task writes in it (TMPDIR,
|
|
189
|
+
// which Turborepo passes on), and it goes at the end of the pass.
|
|
190
|
+
const base = ouvrirBase('verification', { dans: env.TMPDIR || tmpdir() });
|
|
191
|
+
try {
|
|
192
|
+
return avecLaMachine(
|
|
193
|
+
envMachine => {
|
|
194
|
+
// The progress of the pass: the directory its caller names (the circuit's), else its own,
|
|
195
|
+
// named beside the lock of the machine while it plays.
|
|
196
|
+
const propre = !envMachine[AVANCEMENT];
|
|
197
|
+
const dossier =
|
|
198
|
+
envMachine[AVANCEMENT] || ouvrirAvancement({ racine, genre: 'verification' });
|
|
199
|
+
const effacer =
|
|
200
|
+
propre && !composantDeLEnveloppe(envMachine)
|
|
201
|
+
? ecrireLaMesure({
|
|
202
|
+
env: envMachine,
|
|
203
|
+
depot: nomDuDepot(racine),
|
|
204
|
+
commande: [
|
|
205
|
+
'npm run verifier',
|
|
206
|
+
...(demandes.length ? ['--', ...demandes] : []),
|
|
207
|
+
].join(' '),
|
|
208
|
+
dossier,
|
|
209
|
+
})
|
|
210
|
+
: () => {};
|
|
211
|
+
try {
|
|
212
|
+
return passe({
|
|
213
|
+
racine,
|
|
214
|
+
demandes,
|
|
215
|
+
env: { ...envMachine, [AVANCEMENT]: dossier, TMPDIR: base.dossier },
|
|
216
|
+
borneMs,
|
|
217
|
+
pasMs,
|
|
218
|
+
dire,
|
|
219
|
+
coeurs,
|
|
220
|
+
});
|
|
221
|
+
} finally {
|
|
222
|
+
effacer();
|
|
223
|
+
if (propre) {
|
|
224
|
+
garderLeTotal(dossier, racine);
|
|
225
|
+
rmSync(dossier, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{ env, dire }
|
|
230
|
+
);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (!(e instanceof AttenteDeLaMachine)) throw e;
|
|
233
|
+
dire(e.message);
|
|
234
|
+
return e.code;
|
|
235
|
+
} finally {
|
|
236
|
+
base.fermer();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The name of the repository at <racine>: the folder of its main tree, its copies included; the
|
|
241
|
+
// folder of <racine> out of git.
|
|
242
|
+
function nomDuDepot(racine) {
|
|
243
|
+
const r = spawnSync(
|
|
244
|
+
'git',
|
|
245
|
+
['-C', racine, 'rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
246
|
+
{
|
|
247
|
+
encoding: 'utf8',
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
return path.basename(r.status === 0 ? path.dirname(r.stdout.trim()) : path.resolve(racine));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Whether a package of <racine>, or its root, plays tests (a script `test:vitest`).
|
|
254
|
+
function joueDesTests(racine) {
|
|
255
|
+
const liste = paquets(racine);
|
|
256
|
+
if (liste.length === 0) {
|
|
257
|
+
try {
|
|
258
|
+
return Boolean(
|
|
259
|
+
JSON.parse(readFileSync(path.join(racine, 'package.json'), 'utf8')).scripts?.['test:vitest']
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return liste.some(p => !p.hors && p.pkg.scripts?.['test:vitest']);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function passe({ racine, demandes, env, borneMs, pasMs, dire, coeurs }) {
|
|
269
|
+
const gardesSeules = demandes.includes('--gardes');
|
|
270
|
+
const tout = demandes.includes('--tout') || demandes.includes('--force');
|
|
271
|
+
const args = demandes.filter(a => a !== '--gardes' && a !== '--tout');
|
|
272
|
+
const { filtres: filtresDeLaPasse } = separerLesFiltres(args);
|
|
273
|
+
// The measure against the last green: the guards alone need none.
|
|
274
|
+
const m = gardesSeules ? null : mesure(racine, { tout, dans: env.TMPDIR });
|
|
275
|
+
if (m) dire(m.ligne);
|
|
276
|
+
const taches = new Set();
|
|
277
|
+
try {
|
|
278
|
+
const code = jouerLaPasse({
|
|
279
|
+
racine,
|
|
280
|
+
gardesSeules,
|
|
281
|
+
args,
|
|
282
|
+
tout,
|
|
283
|
+
ajouts: m?.env ?? {},
|
|
284
|
+
env,
|
|
285
|
+
borneMs,
|
|
286
|
+
pasMs,
|
|
287
|
+
dire,
|
|
288
|
+
coeurs,
|
|
289
|
+
taches,
|
|
290
|
+
});
|
|
291
|
+
if (env[ROUGES]) writeFileSync(env[ROUGES], [...taches].map(t => `${t}\n`).join(''));
|
|
292
|
+
// A pass green on every package keeps its tree as the last green.
|
|
293
|
+
if (code === 0 && m?.etat && filtresDeLaPasse.length === 0) garderLeVert(m.etat);
|
|
294
|
+
return code;
|
|
295
|
+
} finally {
|
|
296
|
+
m?.retirer();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function jouerLaPasse({
|
|
301
|
+
racine,
|
|
302
|
+
gardesSeules,
|
|
303
|
+
args,
|
|
304
|
+
tout,
|
|
305
|
+
ajouts,
|
|
306
|
+
env,
|
|
307
|
+
borneMs,
|
|
308
|
+
pasMs,
|
|
309
|
+
dire,
|
|
310
|
+
coeurs,
|
|
311
|
+
taches,
|
|
312
|
+
}) {
|
|
313
|
+
const { [TESTS_CIBLES]: _, [CHANGEMENTS]: __, ...propre } = env;
|
|
314
|
+
// The moment of a complete pass, which moves the hash of every task of tests: each plays for
|
|
315
|
+
// real.
|
|
316
|
+
const rejouer = tout ? { rejouer: new Date().toISOString() } : {};
|
|
317
|
+
// The environment of a run whose tests leave the witnesses out (`hors`) or play them alone
|
|
318
|
+
// (`seuls`), measured against the last green.
|
|
319
|
+
const choix = temoins => ({
|
|
320
|
+
...propre,
|
|
321
|
+
...ajouts,
|
|
322
|
+
[TESTS_CIBLES]: JSON.stringify({ arguments: [], temoins, ...rejouer }),
|
|
323
|
+
});
|
|
324
|
+
const turbo = (a, e) =>
|
|
325
|
+
spawnSync('turbo', ['run', ...a], { cwd: racine, env: e, stdio: 'inherit' });
|
|
326
|
+
// Each red named, then the verdict 1.
|
|
327
|
+
const rouge = liste => {
|
|
328
|
+
for (const r of liste) dire(`⛔ ${r}`);
|
|
329
|
+
dire('⛔ rouge.');
|
|
330
|
+
return 1;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// One run of Turborepo, and its summary.
|
|
334
|
+
const runs = path.join(racine, '.turbo', 'runs');
|
|
335
|
+
const jouer = (a, e, libelle) => {
|
|
336
|
+
const existait = existsSync(runs);
|
|
337
|
+
const avant = new Set(lister(runs));
|
|
338
|
+
const r = turbo([...a, '--summarize'], e);
|
|
339
|
+
const nouveaux = lister(runs).filter(f => !avant.has(f) && f.endsWith('.json'));
|
|
340
|
+
let resume = null;
|
|
341
|
+
if (nouveaux.length === 1) {
|
|
342
|
+
try {
|
|
343
|
+
resume = JSON.parse(readFileSync(path.join(runs, nouveaux[0]), 'utf8'));
|
|
344
|
+
} catch {
|
|
345
|
+
resume = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
garderLesResumes(racine, avant, e);
|
|
349
|
+
if (!existait && existsSync(runs) && lister(runs).length === 0) rmdirSync(runs);
|
|
350
|
+
return { r, resume, env: e, libelle };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const borne = args.some(a => CONCURRENCE.test(a)) ? [] : [`--concurrency=${concurrence(coeurs)}`];
|
|
354
|
+
const lancements = [
|
|
355
|
+
{
|
|
356
|
+
a: [
|
|
357
|
+
...(gardesSeules ? GARDES : TACHES),
|
|
358
|
+
'--continue=dependencies-successful',
|
|
359
|
+
...borne,
|
|
360
|
+
...args,
|
|
361
|
+
],
|
|
362
|
+
e: gardesSeules ? propre : choix('hors'),
|
|
363
|
+
libelle: '',
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
// The time witnesses, alone: in a pass restricted by filters, those of the packages they choose.
|
|
367
|
+
const { filtres: filtresDeLaPasse, autres } = separerLesFiltres(args);
|
|
368
|
+
let filtres = gardesSeules ? null : filtresDesTemoins(racine);
|
|
369
|
+
if (filtres?.length && filtresDeLaPasse.length) {
|
|
370
|
+
const choisis = paquetsChoisis(racine, filtresDeLaPasse, propre);
|
|
371
|
+
if (choisis) filtres = filtres.filter(f => choisis.has(f.slice('--filter='.length)));
|
|
372
|
+
if (filtres.length === 0) filtres = null;
|
|
373
|
+
}
|
|
374
|
+
if (filtres) {
|
|
375
|
+
lancements.push({
|
|
376
|
+
a: [
|
|
377
|
+
'test:vitest',
|
|
378
|
+
...filtres,
|
|
379
|
+
'--continue=dependencies-successful',
|
|
380
|
+
'--concurrency=1',
|
|
381
|
+
...sansConcurrence(filtres.length || !filtresDeLaPasse.length ? autres : args),
|
|
382
|
+
],
|
|
383
|
+
e: choix('seuls'),
|
|
384
|
+
libelle: TEMOINS,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// The plan of each run that plays tests, before the first: the progress knows all its tasks.
|
|
388
|
+
if (!gardesSeules && joueDesTests(racine)) {
|
|
389
|
+
for (const { a, e } of lancements) planifier(env[AVANCEMENT], { racine, args: a, env: e });
|
|
390
|
+
}
|
|
391
|
+
const passes = [];
|
|
392
|
+
for (const { a, e, libelle } of lancements) {
|
|
393
|
+
if (libelle) {
|
|
394
|
+
const ou = filtres.map(f => f.slice('--filter='.length)).join(', ') || 'la racine';
|
|
395
|
+
dire(`⏱ témoins de temps : ${ou} ; ils jouent seuls, une tâche après l'autre.`);
|
|
396
|
+
}
|
|
397
|
+
passes.push(jouer(a, e, libelle));
|
|
398
|
+
}
|
|
399
|
+
if (passes.every(p => p.r.status === 0)) return 0;
|
|
400
|
+
|
|
401
|
+
// The verdict of each task.
|
|
402
|
+
const rouges = [];
|
|
403
|
+
const aRepasser = [];
|
|
404
|
+
for (const { r, resume, env: e, libelle } of passes) {
|
|
405
|
+
if (r.status === 0) continue;
|
|
406
|
+
if (!Array.isArray(resume?.tasks)) {
|
|
407
|
+
rouges.push(
|
|
408
|
+
`rouge${libelle} : Turborepo sort en ${r.status ?? r.signal}, et le résumé de la passe ne se lit pas.`
|
|
409
|
+
);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
let echecs = 0;
|
|
413
|
+
for (const t of resume.tasks) {
|
|
414
|
+
const code = t.execution?.exitCode;
|
|
415
|
+
if (code === 0) continue;
|
|
416
|
+
echecs++;
|
|
417
|
+
const nom = `${t.taskId}${libelle}`;
|
|
418
|
+
if (code === A_REPASSER && (t.dependents ?? []).length === 0)
|
|
419
|
+
aRepasser.push({ ...t, nom, env: e });
|
|
420
|
+
else if (code === A_REPASSER)
|
|
421
|
+
rouges.push(`${nom} à repasser, mais ${t.dependents.join(', ')} l'attendent`);
|
|
422
|
+
else {
|
|
423
|
+
rouges.push(`${nom} (code ${code ?? 'absent'})`);
|
|
424
|
+
if (typeof code === 'number') taches.add(t.taskId);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (echecs === 0)
|
|
428
|
+
rouges.push(`Turborepo sort en ${r.status ?? r.signal}${libelle} sans tâche en échec`);
|
|
429
|
+
}
|
|
430
|
+
if (rouges.length > 0) return rouge(rouges);
|
|
431
|
+
|
|
432
|
+
// The tasks à repasser, each played again at rest in the choice of its run, until the bound.
|
|
433
|
+
const limite = Date.now() + borneMs;
|
|
434
|
+
const lire = () => lirePression(env.GRILLHOUSE_PRESSION_CPU || undefined);
|
|
435
|
+
let restants = aRepasser;
|
|
436
|
+
dire(
|
|
437
|
+
`⏱ à repasser : ${restants.map(t => t.nom).join(', ')} ; chaque tâche se rejoue seule, au repos.`
|
|
438
|
+
);
|
|
439
|
+
while (restants.length > 0) {
|
|
440
|
+
const suivants = [];
|
|
441
|
+
for (const [i, t] of restants.entries()) {
|
|
442
|
+
if (Date.now() >= limite || !attendreLeRepos({ lire, limite, pasMs, dire })) {
|
|
443
|
+
const ouverts = [...suivants, ...restants.slice(i)].map(x => x.nom).join(', ');
|
|
444
|
+
const borneTexte =
|
|
445
|
+
borneMs >= 60_000 ? `${Math.round(borneMs / 60_000)} min` : `${borneMs / 1000} s`;
|
|
446
|
+
dire(`⏱ pas de repos en ${borneTexte} : ${ouverts} ni vert ni rouge.`);
|
|
447
|
+
// A task red at rest makes the pass red, whatever remains to play.
|
|
448
|
+
if (rouges.length > 0) return rouge(rouges);
|
|
449
|
+
dire(`⏱ à repasser (code ${A_REPASSER}).`);
|
|
450
|
+
return A_REPASSER;
|
|
451
|
+
}
|
|
452
|
+
dire(`↻ ${t.nom} : relance au repos.`);
|
|
453
|
+
const r = turbo([t.task, `--filter=${t.package}`, ...args], t.env);
|
|
454
|
+
if (r.status === 0) dire(`✓ ${t.nom} vert au repos.`);
|
|
455
|
+
else if (r.status === A_REPASSER) suivants.push(t);
|
|
456
|
+
else {
|
|
457
|
+
rouges.push(`${t.nom} rouge au repos (code ${r.status ?? r.signal})`);
|
|
458
|
+
taches.add(t.taskId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
restants = suivants;
|
|
462
|
+
}
|
|
463
|
+
if (rouges.length > 0) return rouge(rouges);
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
468
|
+
process.exitCode = verifier({ args: process.argv.slice(2) });
|
|
469
|
+
}
|