@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,494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The queue: brings the branches `agent/<ticket>` the integration committed onto main, one at a time,
|
|
3
|
+
// in their order of arrival (scripts/integration/file-attente.mjs). Run from the main tree by the
|
|
4
|
+
// circuit of a ticket (scripts/circuit.mjs), once it has put the ticket's branch in the queue, and
|
|
5
|
+
// by the supervisor by hand.
|
|
6
|
+
//
|
|
7
|
+
// npx grillhouse file
|
|
8
|
+
//
|
|
9
|
+
// No argument (exit 2 and the usage otherwise). It takes the branches waiting one by one until none is
|
|
10
|
+
// left, then exits 0, whatever became of each branch. A queue launched while another runs exits 0 at
|
|
11
|
+
// once, refusing nothing: the running queue takes the branch that arrived (it reads the entries
|
|
12
|
+
// again before it lets go of its lock, `.git/file-integration.lock`).
|
|
13
|
+
//
|
|
14
|
+
// For each branch, against the main of the moment:
|
|
15
|
+
// - main is an ancestor of the branch (main has not moved since its base): main moves forward onto
|
|
16
|
+
// the branch (`merge --ff-only`), nothing replayed;
|
|
17
|
+
// - main has moved: the branch is replaced on main in the queue's own copy
|
|
18
|
+
// (`.claude/worktrees/file`, made ready by scripts/integration/rejeu.mjs), its dependencies made
|
|
19
|
+
// to agree with the lock file and the manifests of the branch once replaced (an install that
|
|
20
|
+
// fails makes the branch leave the queue, « Les dépendances de la copie ne s'installent pas. »,
|
|
21
|
+
// its tests not played), and the same command as the integration's replays it there:
|
|
22
|
+
// `npm run verifier` (Turborepo: the build, then the guards and every test of the day beside
|
|
23
|
+
// each other, each task replayed from the cache of the main tree when its inputs are
|
|
24
|
+
// unchanged), every command of the project run in the copy leaving its index up to date
|
|
25
|
+
// (rejeu.mjs); main moves forward onto the replaced branch when `npm run verifier` is green;
|
|
26
|
+
// - a branch with nothing ahead of main does not enter: it leaves the queue, its ticket loses
|
|
27
|
+
// `en-file` and takes a comment, and stays as it is;
|
|
28
|
+
// - a branch that does not replace (the files in conflict) or whose `npm run verifier` is red once
|
|
29
|
+
// replaced leaves the queue: its ticket loses `en-file`, reopens with a comment that
|
|
30
|
+
// names the files or ends with the output of the tests, and the branch stays where it is, for
|
|
31
|
+
// the relaunch of its ticket (scripts/enveloppe/lancer.sh);
|
|
32
|
+
// - a branch whose replay cannot be done (the copy does not get ready, main does not take the
|
|
33
|
+
// branch, the main tree holds a file of the lot), or whose `npm run verifier` is « à repasser »
|
|
34
|
+
// once replaced (75: a failure of duration under load, which found no rest in its bound,
|
|
35
|
+
// scripts/verifier.mjs; neither green nor red), stays in the queue, the reason written to its
|
|
36
|
+
// ticket, which is neither reopened nor closed, and the queue goes on; the next queue takes it
|
|
37
|
+
// again. Any other code than 0 and 75 is red.
|
|
38
|
+
// At its entry on main, the branch's ticket closes « intégré : <commit> », loses `en-file`, the
|
|
39
|
+
// working directory of its circuit (`.claude/worktrees/<ticket>.travail`, scripts/circuit.mjs),
|
|
40
|
+
// which lives as long as the ticket, is removed, and the branch is deleted; a branch git does not delete (its copy still in place) is written to the
|
|
41
|
+
// ticket, which closes all the same. Then main is pushed to origin, on the branch
|
|
42
|
+
// `grillhouse.poussee.commit` of main's package.json (`main` without the key); a refused push is
|
|
43
|
+
// written to the ticket and does not stop the queue.
|
|
44
|
+
// Once pushed, the queue reads the CI of the commit pushed, as a merge queue waits for its checks.
|
|
45
|
+
// As GitHub's required checks name what a merge waits for, a check that does not exist is not
|
|
46
|
+
// waited for: the queue first reads, in the commit pushed, the workflows that run on a push to the
|
|
47
|
+
// branch it pushes to (`fluxDeLaBranche`, the files `.github/workflows/*.yml` and `*.yaml`, their
|
|
48
|
+
// `on` and its filters as GitHub reads them); with none, the ticket is told in one line that
|
|
49
|
+
// there is no CI for that branch, and nothing is waited for. A workflow the queue cannot judge (a
|
|
50
|
+
// file it cannot read, a filter on the paths) counts as one that runs. With one, it looks for its
|
|
51
|
+
// run (`gh run list --commit <sha> --json databaseId,status,conclusion`) for 2 min at most
|
|
52
|
+
// (GRILLHOUSE_CI_RECHERCHE_MS replaces it, in milliseconds), the time the CI takes to create it,
|
|
53
|
+
// then reads it (`gh run view <id> --json status,conclusion,jobs`) every 10 s
|
|
54
|
+
// (GRILLHOUSE_CI_PAS_MS), as `gh run watch` does, until it completes, for 30 min at most
|
|
55
|
+
// (GRILLHOUSE_CI_ATTENTE_MS): green on the conclusion `success`, red on any other. Under the
|
|
56
|
+
// circuit's progress directory (GRILLHOUSE_AVANCEMENT, scripts/avancement.mjs), each reading writes
|
|
57
|
+
// there the steps of the run done over planned, which the board shows; the queue's own runs of
|
|
58
|
+
// `npm run verifier` play out of it. A red CI reopens the ticket (`bd reopen`), its comment giving the
|
|
59
|
+
// address of the run (`gh run view <id> --json url`) and the end of `gh run view <id>
|
|
60
|
+
// --log-failed`. Without `gh`, without a run found, with `gh` refusing or past the 30 min, the
|
|
61
|
+
// ticket is told that the CI was not read, and why, and stays closed.
|
|
62
|
+
// Every comment and change of the queue at a ticket is written under the author `file`, which bd
|
|
63
|
+
// reads in BEADS_ACTOR, as the circuit writes its own lines under `circuit`.
|
|
64
|
+
import { spawnSync } from 'node:child_process';
|
|
65
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
66
|
+
import { createRequire } from 'node:module';
|
|
67
|
+
import path from 'node:path';
|
|
68
|
+
import { pathToFileURL } from 'node:url';
|
|
69
|
+
import { lireCommit } from '../poussee-cles.mjs';
|
|
70
|
+
import { attendreUnPeu, lacherVerrou, prendreVerrou } from '../verrou-de-fichier.mjs';
|
|
71
|
+
import { dossierCommun, lireEntrees, retirerEntree } from './file-attente.mjs';
|
|
72
|
+
import { ouvrirCopie } from './rejeu.mjs';
|
|
73
|
+
import { A_REPASSER } from '../vitest-config/pression.mjs';
|
|
74
|
+
import { AVANCEMENT, ecrireTache } from '../vitest-config/avancement.mjs';
|
|
75
|
+
|
|
76
|
+
const ESSAIS_PAR_BRANCHE = 5;
|
|
77
|
+
// The variable in which bd reads the author of a comment, and the queue's author.
|
|
78
|
+
const ACTEUR = 'BEADS_ACTOR';
|
|
79
|
+
export const AUTEUR_DE_LA_FILE = 'file';
|
|
80
|
+
// How long the queue looks for the run of the CI, then waits for it.
|
|
81
|
+
const RECHERCHE_CI_MS = 2 * 60 * 1000;
|
|
82
|
+
const ATTENTE_CI_MS = 30 * 60 * 1000;
|
|
83
|
+
// The gap between two readings of the run, as `gh run watch` reads it again.
|
|
84
|
+
const PAS_CI_MS = 10 * 1000;
|
|
85
|
+
// The steps done over planned of the view <vue> of a run (`gh run view --json jobs`), written to
|
|
86
|
+
// the progress directory <avancement> as the record `etapes-de-la-ci`; nothing without one.
|
|
87
|
+
function ecrireLesEtapes(avancement, vue) {
|
|
88
|
+
if (!avancement) return;
|
|
89
|
+
const etapes = (Array.isArray(vue?.jobs) ? vue.jobs : []).flatMap(j =>
|
|
90
|
+
Array.isArray(j?.steps) ? j.steps : []
|
|
91
|
+
);
|
|
92
|
+
try {
|
|
93
|
+
ecrireTache(avancement, 'etapes-de-la-ci', {
|
|
94
|
+
faits: etapes.filter(e => e?.status === 'completed').length,
|
|
95
|
+
prevus: etapes.length,
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
// A directory that does not write leaves the queue going, without its progress.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const delai = (variable, defaut) => {
|
|
103
|
+
const v = Number(process.env[variable]);
|
|
104
|
+
return Number.isFinite(v) && process.env[variable] !== '' && v >= 0 ? v : defaut;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// The directory of the workflows GitHub reads, and the extensions it reads in it.
|
|
108
|
+
const DOSSIER_DES_FLUX = '.github/workflows';
|
|
109
|
+
const EXTENSIONS_DES_FLUX = /\.ya?ml$/;
|
|
110
|
+
|
|
111
|
+
// The regular expression of a filter pattern of GitHub (« Filter pattern cheat sheet »): `*` any
|
|
112
|
+
// characters but `/`, `**` any characters, `?` zero or one and `+` one or more of the preceding
|
|
113
|
+
// character, `[…]` one character of the set, `\` escaping the next.
|
|
114
|
+
function motifDeGitHub(motif) {
|
|
115
|
+
let re = '';
|
|
116
|
+
for (let i = 0; i < motif.length; i++) {
|
|
117
|
+
const c = motif[i];
|
|
118
|
+
if (c === '\\' && i + 1 < motif.length) {
|
|
119
|
+
re += motif[++i].replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
120
|
+
} else if (c === '*') {
|
|
121
|
+
if (motif[i + 1] === '*') {
|
|
122
|
+
re += '.*';
|
|
123
|
+
i++;
|
|
124
|
+
} else {
|
|
125
|
+
re += '[^/]*';
|
|
126
|
+
}
|
|
127
|
+
} else if (c === '?' || c === '+') {
|
|
128
|
+
re += c;
|
|
129
|
+
} else if (c === '[') {
|
|
130
|
+
const fin = motif.indexOf(']', i + 1);
|
|
131
|
+
if (fin < 0) throw new Error(`motif sans fin : ${motif}`);
|
|
132
|
+
re += motif.slice(i, fin + 1);
|
|
133
|
+
i = fin;
|
|
134
|
+
} else {
|
|
135
|
+
re += c.replace(/[.^${}()|\]\\/]/g, '\\$&');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return new RegExp(`^${re}$`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Whether the `on.push` of a workflow <push> runs on a push to <branche>, as GitHub reads its
|
|
142
|
+
// filters: none runs on every branch; `branches` in its order, a `!` pattern taking out what the
|
|
143
|
+
// ones before took in, the last match deciding; `branches-ignore` takes out what it matches;
|
|
144
|
+
// `tags` or `tags-ignore` alone run on no branch. A filter on the paths, or a form the queue does
|
|
145
|
+
// not judge, counts as one that runs.
|
|
146
|
+
function pousseeSurLaBranche(push, branche) {
|
|
147
|
+
if (push === null || push === undefined) return true;
|
|
148
|
+
if (typeof push !== 'object' || Array.isArray(push)) return true;
|
|
149
|
+
if (push.paths || push['paths-ignore']) return true;
|
|
150
|
+
const { branches, 'branches-ignore': ignorees } = push;
|
|
151
|
+
if (branches && ignorees) return true;
|
|
152
|
+
const motifs = l => (Array.isArray(l) ? l : [l]).map(String);
|
|
153
|
+
if (branches) {
|
|
154
|
+
let dedans = false;
|
|
155
|
+
for (const m of motifs(branches)) {
|
|
156
|
+
const negatif = m.startsWith('!');
|
|
157
|
+
if (motifDeGitHub(negatif ? m.slice(1) : m).test(branche)) dedans = !negatif;
|
|
158
|
+
}
|
|
159
|
+
return dedans;
|
|
160
|
+
}
|
|
161
|
+
if (ignorees) {
|
|
162
|
+
const l = motifs(ignorees);
|
|
163
|
+
if (l.some(m => m.startsWith('!'))) return true;
|
|
164
|
+
return !l.some(m => motifDeGitHub(m).test(branche));
|
|
165
|
+
}
|
|
166
|
+
return !(push.tags || push['tags-ignore']);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The workflows of <fichiers> (`{ chemin, texte }`, `texte` null for a file git does not give)
|
|
170
|
+
// that run on a push to <branche>: their paths. A file the queue cannot read or judge counts as
|
|
171
|
+
// one that runs.
|
|
172
|
+
export function fluxDeLaBranche(fichiers, branche) {
|
|
173
|
+
return fichiers
|
|
174
|
+
.filter(({ texte }) => {
|
|
175
|
+
if (texte === null) return true;
|
|
176
|
+
try {
|
|
177
|
+
// Loaded only for a workflow to read: a project without one, or before its `npm install`,
|
|
178
|
+
// runs its queue without the library (which, absent, counts the workflow as one that runs).
|
|
179
|
+
const require = createRequire(import.meta.url);
|
|
180
|
+
const on = require('yaml').parse(texte)?.on;
|
|
181
|
+
if (typeof on === 'string') return on === 'push';
|
|
182
|
+
if (Array.isArray(on)) return on.includes('push');
|
|
183
|
+
if (on && typeof on === 'object')
|
|
184
|
+
return 'push' in on && pousseeSurLaBranche(on.push, branche);
|
|
185
|
+
return false;
|
|
186
|
+
} catch {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
.map(({ chemin }) => chemin);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function main(argv) {
|
|
194
|
+
if (argv.length) {
|
|
195
|
+
process.stderr.write('usage: file.mjs (sans argument)\n');
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
// The progress directory of the circuit's step (scripts/avancement.mjs), where the queue writes
|
|
199
|
+
// the steps of the CI; its own runs of `npm run verifier` play out of it, as measures of their
|
|
200
|
+
// own.
|
|
201
|
+
const avancement = process.env[AVANCEMENT] || null;
|
|
202
|
+
delete process.env[AVANCEMENT];
|
|
203
|
+
// The main tree, from git's common directory: launched from a copy, the queue still works on it.
|
|
204
|
+
const racine = path.dirname(
|
|
205
|
+
spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
}).stdout.trim()
|
|
208
|
+
);
|
|
209
|
+
const git = (...args) => spawnSync('git', ['-C', racine, ...args], { encoding: 'utf8' });
|
|
210
|
+
// bd runs under the queue's author (BEADS_ACTOR), apart from the supervisor's and the owner's.
|
|
211
|
+
const bd = (...a) => {
|
|
212
|
+
const r = spawnSync('bd', a, {
|
|
213
|
+
cwd: racine,
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
216
|
+
env: { ...process.env, [ACTEUR]: AUTEUR_DE_LA_FILE },
|
|
217
|
+
});
|
|
218
|
+
if (r.status !== 0) process.stderr.write(`bd ${a[0]} ${a[1] ?? ''} : ${r.stderr}`);
|
|
219
|
+
return r;
|
|
220
|
+
};
|
|
221
|
+
const dire = s => process.stdout.write(`${s}\n`);
|
|
222
|
+
const principale = git('symbolic-ref', '--short', '-q', 'HEAD').stdout.trim();
|
|
223
|
+
if (!principale) {
|
|
224
|
+
process.stderr.write("⛔ L'arbre principal n'est pas sur une branche.\n");
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const refPrincipale = `refs/heads/${principale}`;
|
|
228
|
+
const copie = path.join(racine, '.claude', 'worktrees', 'file');
|
|
229
|
+
const verrou = path.join(dossierCommun(racine), 'file-integration.lock');
|
|
230
|
+
|
|
231
|
+
const commenter = (ticket, texte) => bd('comments', 'add', ticket, texte);
|
|
232
|
+
// The branch leaves the queue and stays in place; its ticket reopens.
|
|
233
|
+
const sortir = (entree, titre, detail) => {
|
|
234
|
+
const texte = `## Sortie de la file\n\nLa branche ${entree.branche} ne peut pas entrer sur ${principale} : ${titre}\n${detail}\n\nLa branche reste en place ; la relance du ticket remet son lot en travail local sur ${principale}.`;
|
|
235
|
+
bd('update', entree.ticket, '--status', 'open', '--remove-label', 'en-file');
|
|
236
|
+
bd('comments', 'add', entree.ticket, texte);
|
|
237
|
+
retirerEntree(entree);
|
|
238
|
+
dire(`✗ ${entree.branche} sort de la file : ${titre}`);
|
|
239
|
+
};
|
|
240
|
+
// The branch has nothing ahead of main: it leaves the queue, its ticket stays as it is and loses
|
|
241
|
+
// `en-file`.
|
|
242
|
+
const ecarter = entree => {
|
|
243
|
+
const texte = `## Sortie de la file\n\nLa branche ${entree.branche} n'a rien en avance sur ${principale} : elle n'entre pas, et le ticket reste ouvert.`;
|
|
244
|
+
bd('update', entree.ticket, '--remove-label', 'en-file');
|
|
245
|
+
bd('comments', 'add', entree.ticket, texte);
|
|
246
|
+
retirerEntree(entree);
|
|
247
|
+
dire(`⚠ ${entree.branche} n'a rien en avance sur ${principale} : elle sort de la file`);
|
|
248
|
+
};
|
|
249
|
+
// The branch stays in the queue, the reason written to its ticket.
|
|
250
|
+
const garder = (entree, raison) => {
|
|
251
|
+
commenter(
|
|
252
|
+
entree.ticket,
|
|
253
|
+
`## La file garde la branche\n\nLa branche ${entree.branche} reste en file : ${raison}`
|
|
254
|
+
);
|
|
255
|
+
dire(`⚠ ${entree.branche} reste en file : ${raison}`);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// The branch replaced on main, replayed: { nouveau } (the commit to move main onto), or
|
|
259
|
+
// { sortie: [titre, détail] } (it leaves the queue), or { garde } (it stays, the reason).
|
|
260
|
+
const rejouer = (tip, tete) => {
|
|
261
|
+
if (existsSync(copie))
|
|
262
|
+
spawnSync('git', ['-C', copie, 'rebase', '--abort'], { encoding: 'utf8' });
|
|
263
|
+
const ouverte = ouvrirCopie({ racine, copie, ref: tete, dire });
|
|
264
|
+
if (ouverte.refus) return { garde: ouverte.refus };
|
|
265
|
+
const { dansCopie, installer, tester } = ouverte;
|
|
266
|
+
if (dansCopie('checkout', '-q', '--detach', tip).status !== 0)
|
|
267
|
+
return { garde: `la copie ne prend pas ${tip.slice(0, 7)}` };
|
|
268
|
+
const rebase = dansCopie('rebase', tete);
|
|
269
|
+
if (rebase.status !== 0) {
|
|
270
|
+
const conflits = dansCopie('diff', '--name-only', '--diff-filter=U')
|
|
271
|
+
.stdout.split('\n')
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
dansCopie('rebase', '--abort');
|
|
274
|
+
return {
|
|
275
|
+
sortie: [
|
|
276
|
+
'elle ne se replace pas sur main.',
|
|
277
|
+
conflits.length
|
|
278
|
+
? `Fichiers en conflit :\n${conflits.map(f => ` ${f}`).join('\n')}`
|
|
279
|
+
: `Sortie de git :\n${rebase.stdout}${rebase.stderr}`,
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const installation = installer();
|
|
284
|
+
if (installation) {
|
|
285
|
+
return { sortie: [installation, "Le verrou de la branche replacée ne s'installe pas."] };
|
|
286
|
+
}
|
|
287
|
+
dire('— npm run verifier (construction, gardes, tests)');
|
|
288
|
+
const t = tester();
|
|
289
|
+
if (t.status === A_REPASSER) {
|
|
290
|
+
return {
|
|
291
|
+
garde: `son \`npm run verifier\` est à repasser (code ${A_REPASSER}) une fois replacée : un échec de durée sous charge, sans repos dans la borne. Ni vert ni rouge ; la prochaine file la rejoue.`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (t.status !== 0) {
|
|
295
|
+
const fin = `${t.stdout}${t.stderr}`.trim().split('\n').slice(-40).join('\n');
|
|
296
|
+
return { sortie: ['ses tests sont rouges une fois la branche replacée.', fin] };
|
|
297
|
+
}
|
|
298
|
+
return { nouveau: dansCopie('rev-parse', 'HEAD').stdout.trim() };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Main is at <nouveau>: the ticket closes, the branch is deleted, main is pushed.
|
|
302
|
+
const entrer = (entree, nouveau) => {
|
|
303
|
+
const court = nouveau.slice(0, 7);
|
|
304
|
+
bd('update', entree.ticket, '--remove-label', 'en-file');
|
|
305
|
+
bd('close', entree.ticket, '--reason', `intégré : ${court}`);
|
|
306
|
+
rmSync(path.join(racine, '.claude', 'worktrees', `${entree.ticket}.travail`), {
|
|
307
|
+
recursive: true,
|
|
308
|
+
force: true,
|
|
309
|
+
});
|
|
310
|
+
retirerEntree(entree);
|
|
311
|
+
dire(`✓ ${entree.branche} entre sur ${principale} : ${court}`);
|
|
312
|
+
const efface = git('branch', '-D', entree.branche);
|
|
313
|
+
if (efface.status !== 0) {
|
|
314
|
+
commenter(
|
|
315
|
+
entree.ticket,
|
|
316
|
+
`La branche ${entree.branche} reste en place après son entrée sur ${principale} (${efface.stderr.trim()}) : sa copie est encore en place. Le ticket est fermé.`
|
|
317
|
+
);
|
|
318
|
+
dire(`⚠ ${entree.branche} n'est pas effacée`);
|
|
319
|
+
}
|
|
320
|
+
const paquet = git('show', `${refPrincipale}:package.json`);
|
|
321
|
+
const { commit, faute } = lireCommit(paquet.status === 0 ? JSON.parse(paquet.stdout) : null);
|
|
322
|
+
if (faute) {
|
|
323
|
+
commenter(
|
|
324
|
+
entree.ticket,
|
|
325
|
+
`Poussée non faite : grillhouse.poussee.commit vaut ${faute}, pas un nom de branche.`
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const pousse = spawnSync(
|
|
330
|
+
'git',
|
|
331
|
+
['-C', racine, 'push', 'origin', `${nouveau}:refs/heads/${commit}`],
|
|
332
|
+
{ encoding: 'utf8', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }
|
|
333
|
+
);
|
|
334
|
+
if (pousse.status !== 0) {
|
|
335
|
+
commenter(
|
|
336
|
+
entree.ticket,
|
|
337
|
+
`La poussée de ${court} vers origin/${commit} (grillhouse.poussee.commit) est refusée :\n${pousse.stderr.trim()}`
|
|
338
|
+
);
|
|
339
|
+
dire(`⚠ poussée de ${court} refusée`);
|
|
340
|
+
} else {
|
|
341
|
+
dire(`✓ poussée : ${court} est sur origin/${commit}`);
|
|
342
|
+
lireCI(entree, nouveau, commit);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// The workflows of the commit <sha> that run on a push to <branche>: their paths.
|
|
347
|
+
const fluxDuCommit = (sha, branche) => {
|
|
348
|
+
const l = git('ls-tree', '--name-only', `${sha}:${DOSSIER_DES_FLUX}`);
|
|
349
|
+
const noms =
|
|
350
|
+
l.status === 0 ? l.stdout.split('\n').filter(n => EXTENSIONS_DES_FLUX.test(n)) : [];
|
|
351
|
+
const fichiers = noms.map(n => {
|
|
352
|
+
const chemin = `${DOSSIER_DES_FLUX}/${n}`;
|
|
353
|
+
const f = git('show', `${sha}:${chemin}`);
|
|
354
|
+
return { chemin, texte: f.status === 0 ? f.stdout : null };
|
|
355
|
+
});
|
|
356
|
+
return fluxDeLaBranche(fichiers, branche);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// The CI of the commit <sha> pushed to <branche>: none, the ticket is told so in one line; green,
|
|
360
|
+
// the ticket stays closed; red, it reopens; not read, the ticket is told why.
|
|
361
|
+
const lireCI = (entree, sha, branche) => {
|
|
362
|
+
const court = sha.slice(0, 7);
|
|
363
|
+
if (fluxDuCommit(sha, branche).length === 0) {
|
|
364
|
+
const texte = `Aucune CI pour origin/${branche} : aucun flux de ${DOSSIER_DES_FLUX} de ${court} ne se lance sur la branche ${branche}, la file n'en attend pas.`;
|
|
365
|
+
commenter(entree.ticket, texte);
|
|
366
|
+
dire(`✓ ${texte}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const gh = (args, options = {}) =>
|
|
370
|
+
spawnSync('gh', args, {
|
|
371
|
+
cwd: racine,
|
|
372
|
+
encoding: 'utf8',
|
|
373
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
374
|
+
...options,
|
|
375
|
+
});
|
|
376
|
+
const nonLue = raison => {
|
|
377
|
+
commenter(entree.ticket, `## CI non lue\n\nLa CI de ${court} n'a pas été lue : ${raison}`);
|
|
378
|
+
dire(`⚠ CI de ${court} non lue : ${raison}`);
|
|
379
|
+
};
|
|
380
|
+
const recherche = delai('GRILLHOUSE_CI_RECHERCHE_MS', RECHERCHE_CI_MS);
|
|
381
|
+
const debut = Date.now();
|
|
382
|
+
let passage = null;
|
|
383
|
+
for (;;) {
|
|
384
|
+
const r = gh(['run', 'list', '--commit', sha, '--json', 'databaseId,status,conclusion']);
|
|
385
|
+
if (r.error) return nonLue(`gh ne se lance pas (${r.error.code ?? r.error.message}).`);
|
|
386
|
+
if (r.status !== 0) return nonLue(`gh run list refuse :\n${`${r.stderr}${r.stdout}`.trim()}`);
|
|
387
|
+
let passages = [];
|
|
388
|
+
try {
|
|
389
|
+
passages = JSON.parse(r.stdout || '[]');
|
|
390
|
+
} catch {
|
|
391
|
+
return nonLue(`gh run list rend une sortie illisible :\n${r.stdout.trim()}`);
|
|
392
|
+
}
|
|
393
|
+
passage = passages.find(x => x?.databaseId);
|
|
394
|
+
const reste = recherche - (Date.now() - debut);
|
|
395
|
+
if (passage || reste <= 0) break;
|
|
396
|
+
attendreUnPeu(Math.min(10_000, reste));
|
|
397
|
+
}
|
|
398
|
+
if (!passage) {
|
|
399
|
+
return nonLue(`aucun passage de la CI pour ce commit en ${Math.round(recherche / 1000)} s.`);
|
|
400
|
+
}
|
|
401
|
+
const id = String(passage.databaseId);
|
|
402
|
+
const attente = delai('GRILLHOUSE_CI_ATTENTE_MS', ATTENTE_CI_MS);
|
|
403
|
+
const pas = delai('GRILLHOUSE_CI_PAS_MS', PAS_CI_MS);
|
|
404
|
+
const limite = Date.now() + attente;
|
|
405
|
+
let vue = null;
|
|
406
|
+
for (;;) {
|
|
407
|
+
const v = gh(['run', 'view', id, '--json', 'status,conclusion,jobs'], {
|
|
408
|
+
timeout: 60_000,
|
|
409
|
+
killSignal: 'SIGKILL',
|
|
410
|
+
});
|
|
411
|
+
if (v.error) return nonLue(`gh ne se lance pas (${v.error.code ?? v.error.message}).`);
|
|
412
|
+
if (v.status !== 0) return nonLue(`gh run view refuse :\n${`${v.stderr}${v.stdout}`.trim()}`);
|
|
413
|
+
try {
|
|
414
|
+
vue = JSON.parse(v.stdout || '{}');
|
|
415
|
+
} catch {
|
|
416
|
+
return nonLue(`gh run view rend une sortie illisible :\n${v.stdout.trim()}`);
|
|
417
|
+
}
|
|
418
|
+
ecrireLesEtapes(avancement, vue);
|
|
419
|
+
if (vue?.status === 'completed') break;
|
|
420
|
+
const reste = limite - Date.now();
|
|
421
|
+
if (reste <= 0) {
|
|
422
|
+
return nonLue(`le passage ${id} n'a pas fini en ${Math.round(attente / 60_000)} min.`);
|
|
423
|
+
}
|
|
424
|
+
attendreUnPeu(Math.min(pas, reste));
|
|
425
|
+
}
|
|
426
|
+
if (vue.conclusion === 'success') {
|
|
427
|
+
dire(`✓ CI de ${court} verte (passage ${id})`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const url = gh(['run', 'view', id, '--json', 'url', '--jq', '.url']).stdout?.trim() || id;
|
|
431
|
+
const journal = `${gh(['run', 'view', id, '--log-failed']).stdout ?? ''}`
|
|
432
|
+
.trim()
|
|
433
|
+
.split('\n')
|
|
434
|
+
.slice(-60)
|
|
435
|
+
.join('\n');
|
|
436
|
+
bd('reopen', entree.ticket);
|
|
437
|
+
commenter(
|
|
438
|
+
entree.ticket,
|
|
439
|
+
`## CI rouge\n\nLa CI de ${court} sur ${principale} est rouge : ${url}\n\nLa fin de gh run view ${id} --log-failed :\n\n${journal}\n\nLe ticket est rouvert : sa correction entre sous son numéro.`
|
|
440
|
+
);
|
|
441
|
+
dire(`✗ CI de ${court} rouge (passage ${id}) : ${entree.ticket} rouvert`);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// One branch of the queue, against the main of the moment.
|
|
445
|
+
const traiter = entree => {
|
|
446
|
+
const tip = git('rev-parse', '--verify', '-q', `refs/heads/${entree.branche}`).stdout.trim();
|
|
447
|
+
if (!tip) return sortir(entree, "la branche n'existe plus.", '');
|
|
448
|
+
if (Number(git('rev-list', '--count', `${refPrincipale}..${tip}`).stdout.trim()) === 0)
|
|
449
|
+
return ecarter(entree);
|
|
450
|
+
for (let essai = 0; essai < ESSAIS_PAR_BRANCHE; essai++) {
|
|
451
|
+
const tete = git('rev-parse', refPrincipale).stdout.trim();
|
|
452
|
+
let nouveau = tip;
|
|
453
|
+
if (git('merge-base', '--is-ancestor', tete, tip).status !== 0) {
|
|
454
|
+
const r = rejouer(tip, tete);
|
|
455
|
+
if (r.sortie) return sortir(entree, ...r.sortie);
|
|
456
|
+
if (r.garde) return garder(entree, r.garde);
|
|
457
|
+
nouveau = r.nouveau;
|
|
458
|
+
}
|
|
459
|
+
if (nouveau !== tete) {
|
|
460
|
+
const avance = git('merge', '-q', '--ff-only', nouveau);
|
|
461
|
+
if (avance.status !== 0) {
|
|
462
|
+
if (git('rev-parse', refPrincipale).stdout.trim() !== tete) continue;
|
|
463
|
+
return garder(
|
|
464
|
+
entree,
|
|
465
|
+
`${principale} n'avance pas sur ${nouveau.slice(0, 7)} :\n${avance.stdout}${avance.stderr}`.trim()
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return entrer(entree, nouveau);
|
|
470
|
+
}
|
|
471
|
+
return garder(entree, `${principale} bouge sans cesse : cinq essais n'ont pas suffi.`);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const vues = new Set();
|
|
475
|
+
for (;;) {
|
|
476
|
+
if (!prendreVerrou(verrou, { attendre: false })) return 0;
|
|
477
|
+
try {
|
|
478
|
+
for (;;) {
|
|
479
|
+
const entree = lireEntrees(racine).find(e => !vues.has(e.fichier));
|
|
480
|
+
if (!entree) break;
|
|
481
|
+
vues.add(entree.fichier);
|
|
482
|
+
traiter(entree);
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
lacherVerrou(verrou);
|
|
486
|
+
}
|
|
487
|
+
// An entry written while the lock was let go has no queue running to take it.
|
|
488
|
+
if (!lireEntrees(racine).some(e => !vues.has(e.fichier))) return 0;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
493
|
+
process.exitCode = main(process.argv.slice(2));
|
|
494
|
+
}
|