Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d3a47bd91 | |||
| 073f2cf9ca | |||
| 36bfeb057e | |||
| 7a3c119f36 |
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -41,27 +41,26 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
# @arboretum/shared (dépendance runtime non publiée) est embarquée dans le tarball
|
||||
# via bundleDependencies (matérialisée au prepack). On packe donc UN SEUL tarball
|
||||
# et on l'installe seul, comme un vrai consommateur depuis le registre.
|
||||
# @arboretum/shared (paquet workspace NON publié) est INLINÉ dans dist/_shared au prepack
|
||||
# (scripts/inline-shared.mjs) : le tarball est 100 % autonome — aucun node_modules embarqué,
|
||||
# aucune bundleDependency, aucun symlink. On packe en mode workspace (-w), EXACTEMENT comme
|
||||
# le fait « npm publish » dans release.yml, puis on l'installe seul comme un vrai consommateur.
|
||||
- name: Pack tarball
|
||||
run: |
|
||||
rm -rf /tmp/tarballs && mkdir -p /tmp/tarballs
|
||||
# @arboretum/shared (dépendance runtime non publiée) est embarquée via bundleDependencies,
|
||||
# matérialisée par le hook prepack (copy-web + vendor-shared + copy-meta) comme VRAI dossier
|
||||
# dans packages/server/node_modules/@arboretum/shared.
|
||||
# On packe depuis le CONTEXTE DU PACKAGE (cd), PAS en mode workspace (-w) : « npm pack -w »
|
||||
# résout @arboretum/shared via le symlink workspace hoisté de la racine et n'embarque donc
|
||||
# PAS le dossier vendored local. « cd packages/server && npm pack » lance le lifecycle pack
|
||||
# complet et reproduit fidèlement « npm publish » (qui, lui, embarque correctement).
|
||||
( cd packages/server && npm pack --pack-destination /tmp/tarballs )
|
||||
npm pack -w @johanleroy/git-arboretum --pack-destination /tmp/tarballs
|
||||
ls -l /tmp/tarballs
|
||||
- name: Assert @arboretum/shared is bundled in the tarball
|
||||
- name: Assert the package is self-contained (@arboretum/shared inlined)
|
||||
run: |
|
||||
tgz=$(ls /tmp/tarballs/*.tgz)
|
||||
tar -tzf "$tgz" | grep -q 'node_modules/@arboretum/shared/dist/index.js' \
|
||||
|| { echo "ERREUR: @arboretum/shared non embarqué dans $tgz"; exit 1; }
|
||||
echo "OK: bundleDependency @arboretum/shared présente dans $tgz"
|
||||
rm -rf /tmp/inspect && mkdir -p /tmp/inspect && tar -xzf "$tgz" -C /tmp/inspect
|
||||
test -f /tmp/inspect/package/dist/_shared/index.js \
|
||||
|| { echo "ERREUR: dist/_shared/index.js absent de $tgz — inline-shared n'a pas tourné ?"; exit 1; }
|
||||
if grep -rq '@arboretum/shared' /tmp/inspect/package/dist; then
|
||||
echo "ERREUR: import bare '@arboretum/shared' encore présent dans le JS publié"
|
||||
grep -rn '@arboretum/shared' /tmp/inspect/package/dist; exit 1
|
||||
fi
|
||||
echo "OK: paquet autonome — shared inliné dans dist/_shared, aucun import externe"
|
||||
- name: Install tarball in an empty project
|
||||
run: |
|
||||
mkdir /tmp/smoke
|
||||
|
||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -35,15 +35,39 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: tag $tag == version $pkg"
|
||||
- run: npm publish -w @johanleroy/git-arboretum
|
||||
# Publication idempotente : le signal faisant autorité d'une version déjà présente est le
|
||||
# 409 « already exists » renvoyé par npm publish lui-même (npm view est non fiable contre le
|
||||
# registre npm de Gitea — faux négatif masqué par >/dev/null). On tente toujours le publish ;
|
||||
# un 409 = succès idempotent, tout autre échec reste fatal. Le shell Actions tourne en
|
||||
# `bash -eo pipefail` : on isole l'échec attendu dans la condition d'un `if` pour ne pas
|
||||
# déclencher `set -e`. Le secret du registre est mappé sur NODE_AUTH_TOKEN lu par le .npmrc
|
||||
# de setup-node.
|
||||
- name: Publish (idempotent — tolère un 409 « already exists »)
|
||||
run: |
|
||||
if out="$(npm publish -w @johanleroy/git-arboretum 2>&1)"; then
|
||||
printf '%s\n' "$out"
|
||||
echo "Publication réussie."
|
||||
else
|
||||
code=$?
|
||||
printf '%s\n' "$out"
|
||||
if printf '%s' "$out" | grep -qiE 'E409|409 Conflict|already exists'; then
|
||||
echo "::notice::Version déjà présente sur le registre (409) — publication idempotente, étape ignorée."
|
||||
else
|
||||
echo "::error::Échec de la publication (code $code)."
|
||||
exit "$code"
|
||||
fi
|
||||
fi
|
||||
env:
|
||||
# Mapper le secret du registre (PAT Gitea write:package, ou le token auto
|
||||
# GITEA_TOKEN s'il a ce scope) sur NODE_AUTH_TOKEN lu par le .npmrc de setup-node.
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# SBOM (transparence supply-chain entreprise) : généré pour le paquet publié et exposé en artefact.
|
||||
# SBOM (transparence supply-chain entreprise) : généré pour le paquet publié et exposé en
|
||||
# artefact. C'est un bonus : un échec de génération/upload ne doit jamais faire rougir une
|
||||
# release dont le publish a réussi → continue-on-error.
|
||||
# upload-artifact@v3 : Gitea Actions (GHES-like) ne supporte pas @v4 (@actions/artifact v2+).
|
||||
- name: Generate SBOM (CycloneDX)
|
||||
continue-on-error: true
|
||||
run: npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json -w @johanleroy/git-arboretum || npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: sbom
|
||||
path: sbom.json
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -4840,13 +4840,9 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@johanleroy/git-arboretum",
|
||||
"version": "1.4.0",
|
||||
"bundleDependencies": [
|
||||
"@arboretum/shared"
|
||||
],
|
||||
"version": "1.4.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@arboretum/shared": "0.1.0",
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
@@ -4859,6 +4855,7 @@
|
||||
"arboretum": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arboretum/shared": "0.1.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@types/ws": "^8.5.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@johanleroy/git-arboretum",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -48,14 +48,10 @@
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"dev": "tsc -b --watch & node --watch dist/index.js",
|
||||
"prepack": "node scripts/copy-web.mjs && node scripts/vendor-shared.mjs && node scripts/copy-meta.mjs",
|
||||
"prepack": "node scripts/copy-web.mjs && node scripts/inline-shared.mjs && node scripts/copy-meta.mjs",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@arboretum/shared"
|
||||
],
|
||||
"dependencies": {
|
||||
"@arboretum/shared": "0.1.0",
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
@@ -65,6 +61,7 @@
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arboretum/shared": "0.1.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@types/ws": "^8.5.0"
|
||||
}
|
||||
|
||||
71
packages/server/scripts/inline-shared.mjs
Normal file
71
packages/server/scripts/inline-shared.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
// Inline @arboretum/shared (paquet workspace NON publié) directement dans le dist du serveur,
|
||||
// pour que le tarball npm soit 100 % autonome. Branché sur le hook "prepack".
|
||||
//
|
||||
// Pourquoi PAS bundleDependencies : embarquer une dépendance qui est aussi un *workspace* via
|
||||
// bundleDependencies est instable selon l'environnement npm (mode -w, exécution en root sur un
|
||||
// runner CI, version d'arborist) — npm voit le nœud comme un lien workspace et n'embarque parfois
|
||||
// AUCUN fichier ("bundled files: 0"), produisant un paquet cassé chez le consommateur. On élimine
|
||||
// donc toute magie de bundling : on copie le JS compilé de shared dans dist/_shared et on réécrit
|
||||
// l'import bare '@arboretum/shared' du serveur vers ce chemin relatif. Zéro node_modules embarqué,
|
||||
// zéro symlink, résultat identique partout.
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname, relative, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const serverDist = join(serverDir, 'dist');
|
||||
const sharedDist = join(serverDir, '..', 'shared', 'dist');
|
||||
const inlineDir = join(serverDist, '_shared');
|
||||
|
||||
for (const [label, p] of [['dist serveur', serverDist], ['dist shared', sharedDist]]) {
|
||||
if (!existsSync(p)) {
|
||||
console.error(`inline-shared: ${label} introuvable (${p}) — lance "npm run build" avant le pack.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Copier le JS compilé de shared dans dist/_shared (uniquement *.js : seul le runtime compte ;
|
||||
// les .d.ts/.map ne sont de toute façon pas publiés via le glob `files`). index.js réexporte
|
||||
// ./protocol.js et ./api.js en relatif → la copie complète préserve la résolution interne.
|
||||
rmSync(inlineDir, { recursive: true, force: true });
|
||||
mkdirSync(inlineDir, { recursive: true });
|
||||
let copied = 0;
|
||||
for (const name of readdirSync(sharedDist)) {
|
||||
if (name.endsWith('.js')) {
|
||||
cpSync(join(sharedDist, name), join(inlineDir, name));
|
||||
copied++;
|
||||
}
|
||||
}
|
||||
if (copied === 0) {
|
||||
console.error(`inline-shared: aucun .js dans ${sharedDist} — shared n'est pas compilé.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2) Réécrire l'import bare '@arboretum/shared' de chaque .js du serveur vers le chemin relatif
|
||||
// (POSIX) pointant sur dist/_shared/index.js, calculé par fichier (profondeur variable).
|
||||
const walk = (dir) =>
|
||||
readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) return p === inlineDir ? [] : walk(p); // ne pas se réécrire soi-même
|
||||
return e.name.endsWith('.js') ? [p] : [];
|
||||
});
|
||||
|
||||
let rewritten = 0;
|
||||
for (const file of walk(serverDist)) {
|
||||
const src = readFileSync(file, 'utf8');
|
||||
if (!src.includes('@arboretum/shared')) continue;
|
||||
let rel = relative(dirname(file), join(inlineDir, 'index.js')).split(sep).join('/');
|
||||
if (!rel.startsWith('.')) rel = `./${rel}`;
|
||||
const out = src.replaceAll(`'@arboretum/shared'`, `'${rel}'`).replaceAll(`"@arboretum/shared"`, `"${rel}"`);
|
||||
if (out !== src) {
|
||||
writeFileSync(file, out);
|
||||
rewritten++;
|
||||
}
|
||||
}
|
||||
if (rewritten === 0) {
|
||||
console.error(`inline-shared: aucun import '@arboretum/shared' réécrit dans ${serverDist} — build manquant ?`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`inline-shared: ${copied} fichier(s) shared -> dist/_shared, import réécrit dans ${rewritten} fichier(s) serveur`);
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Embarque @arboretum/shared (dépendance runtime non publiée) DANS le tarball npm.
|
||||
// Branché sur le hook "prepack", aux côtés de bundleDependencies dans package.json.
|
||||
//
|
||||
// Pourquoi un vrai dossier et pas le symlink workspace : sous npm workspaces, shared
|
||||
// est seulement symlinké dans le node_modules racine ; `npm pack` n'embarque une
|
||||
// bundleDependency que si elle existe comme VRAI dossier dans le node_modules du
|
||||
// paquet packé (packages/server/node_modules/@arboretum/shared). On le matérialise ici.
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const sharedDir = join(serverDir, '..', 'shared');
|
||||
const sharedDist = join(sharedDir, 'dist');
|
||||
const target = join(serverDir, 'node_modules', '@arboretum', 'shared');
|
||||
|
||||
if (!existsSync(sharedDist)) {
|
||||
console.error(
|
||||
`vendor-shared: ${sharedDist} introuvable — lance "npm run build" (shared doit être compilé) avant le pack.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// package.json minimal et déterministe : on reprend les champs de résolution réels de
|
||||
// shared (version incluse, pour rester synchro), sans scripts ni files inutiles au runtime.
|
||||
const shared = JSON.parse(readFileSync(join(sharedDir, 'package.json'), 'utf8'));
|
||||
const minimal = {
|
||||
name: shared.name,
|
||||
version: shared.version,
|
||||
type: shared.type,
|
||||
main: shared.main,
|
||||
types: shared.types,
|
||||
exports: shared.exports,
|
||||
};
|
||||
|
||||
// Idempotent : on repart d'un dossier propre à chaque pack.
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
mkdirSync(target, { recursive: true });
|
||||
writeFileSync(join(target, 'package.json'), JSON.stringify(minimal, null, 2) + '\n');
|
||||
cpSync(sharedDist, join(target, 'dist'), { recursive: true });
|
||||
|
||||
console.log(`vendor-shared: embarqué ${shared.name}@${shared.version} -> ${target}`);
|
||||
Reference in New Issue
Block a user