ci: Forgejo Actions workflow with a boot-from-COPY check #31

Closed
maximus wants to merge 0 commits from issue-24-forgejo-ci into main
Owner

Fixes #24

What

  • .forgejo/workflows/ci.yml: one job in container: node:22-alpine (the Dockerfile base), running npm ci, npm test, bash test-curl.sh, sh scripts/boot-from-copy.sh. It triggers on every pull request, with no branches: filter so intermediate stack links are built, and on push to main.
  • scripts/boot-from-copy.sh: copies only the Dockerfile COPY sources into an empty directory and runs the CMD as node. It expects 401 without a token, 200 with one, and non-zero disk and memory metrics. This replaces a real docker build, which would need the Docker socket in the instance-scoped runner and would therefore give every workflow root on the prod VPS.
  • __tests__/ci.test.js: 7 pins. The container image must equal FROM, all four steps must be present, pull_request must have no branch filter while push targets main, no runner-rejected forms, USER node, and an exec-form CMD.
  • CLAUDE.md: new CI section; test count updated to 308.

Local verification (glibc host, no Docker available)

  • npm test: 308/308
  • bash test-curl.sh: 45/45
  • boot-from-copy.sh: PASS. With metrics.js removed from COPY it FAILs and prints Cannot find module './metrics.js' (MODULE_NOT_FOUND).
  • ci.test.js mutation (branches: [main] under pull_request, plus container: node:22): both targeted pins go red (2 failed, 5 passed). Tree restored.

CI evidence

Commit statuses and job logs, the logs read from the Forgejo database on the VPS:

Run SHA Tree Status Log
1 4e7879d (this PR) healthy success, 21 s node v22.23.3 (alpine), vitest 308 passed, test-curl 45/45, boot-from-copy: PASS including non-zero disk/memory under busybox df
2 93a59a2 (throwaway PR #32, stacked on this branch) metrics.js removed from COPY failure, 18 s Image file set (2): index.js package.json, Error: Cannot find module './metrics.js', code: 'MODULE_NOT_FOUND', FAIL: the server died at startup

The red ran on a throwaway stacked PR, so no broken commit reaches main history. It also shows that a PR whose base is not main does get built. PR #32 is closed and its branch deleted.

🤖 Generated with Claude Code

Fixes #24 ## What - `.forgejo/workflows/ci.yml`: one job in `container: node:22-alpine` (the Dockerfile base), running `npm ci`, `npm test`, `bash test-curl.sh`, `sh scripts/boot-from-copy.sh`. It triggers on every pull request, with no `branches:` filter so intermediate stack links are built, and on push to `main`. - `scripts/boot-from-copy.sh`: copies only the Dockerfile `COPY` sources into an empty directory and runs the `CMD` as `node`. It expects 401 without a token, 200 with one, and non-zero disk and memory metrics. This replaces a real `docker build`, which would need the Docker socket in the instance-scoped runner and would therefore give every workflow root on the prod VPS. - `__tests__/ci.test.js`: 7 pins. The container image must equal `FROM`, all four steps must be present, `pull_request` must have no branch filter while `push` targets `main`, no runner-rejected forms, `USER node`, and an exec-form `CMD`. - `CLAUDE.md`: new CI section; test count updated to 308. ## Local verification (glibc host, no Docker available) - `npm test`: 308/308 - `bash test-curl.sh`: 45/45 - `boot-from-copy.sh`: PASS. With `metrics.js` removed from `COPY` it FAILs and prints `Cannot find module './metrics.js'` (`MODULE_NOT_FOUND`). - `ci.test.js` mutation (`branches: [main]` under `pull_request`, plus `container: node:22`): both targeted pins go red (2 failed, 5 passed). Tree restored. ## CI evidence Commit statuses and job logs, the logs read from the Forgejo database on the VPS: | Run | SHA | Tree | Status | Log | |---|---|---|---|---| | 1 | `4e7879d` (this PR) | healthy | **success**, 21 s | node v22.23.3 (alpine), vitest 308 passed, test-curl 45/45, `boot-from-copy: PASS` including non-zero disk/memory under busybox `df` | | 2 | `93a59a2` (throwaway PR #32, stacked on this branch) | `metrics.js` removed from `COPY` | **failure**, 18 s | `Image file set (2): index.js package.json`, `Error: Cannot find module './metrics.js'`, `code: 'MODULE_NOT_FOUND'`, `FAIL: the server died at startup` | The red ran on a throwaway stacked PR, so no broken commit reaches `main` history. It also shows that a PR whose base is not `main` does get built. PR #32 is closed and its branch deleted. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-09-28 00:23:01 +00:00
ci: add a Forgejo Actions workflow with a boot-from-COPY check (#24)
All checks were successful
CI / verification (pull_request) Successful in 21s
CI / verification (push) Successful in 19s
4e7879da6c
Nothing verified this repository until now. The workflow runs on every pull
request (no branches filter, so intermediate stack links get a status) and on
pushes to main, in node:22-alpine, the Dockerfile's own base: npm ci, npm test,
the socket-level pass, then scripts/boot-from-copy.sh.

The script stands in for a docker build, which would need the Docker socket in
the instance-scoped runner and so root on the production VPS for every
workflow. It starts the CMD from the COPY sources alone and fails on the
MODULE_NOT_FOUND that #11 had to fix by hand. __tests__/ci.test.js pins the
workflow and what the script assumes of the Dockerfile.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Revue adversariale — PR #31 (issue #24)

Verdict : APPROVE

Résumé : le workflow et boot-from-copy.sh font ce que la PR annonce, et le contrôle du COPY attrape le défaut réel de #11. Les preuves de CI ont été vérifiées dans le log du run et non sur parole. Rien de bloquant.

Vérifications faites

  • Statut de commit 4e7879d : CI / verification (pull_request) = success, 21 s. Le log du run 1 (route web /actions/runs/1/jobs/0/logs) montre node v22.23.3, vitest 308 passed, test-curl 45 passed, 0 failed, Image file set (3), les trois ok et boot-from-copy: PASS, puis Job succeeded.
  • En local (ports 3097/3098/3099 libres avant le lancement) : vitest donne 308/308 et boot-from-copy.sh renvoie PASS.
  • Mutations faites par le relecteur sur une copie hors dépôt :
    • metrics.js retiré du COPY : FAIL, the server died at startup (MODULE_NOT_FOUND)
    • execSync cassé dans metrics.js, donc des métriques disque à zéro : FAIL, /health reported empty disk or memory metrics
    • CMD node index.js (forme shell) : le script échoue bruyamment, et 1 épingle sur 7 tombe
    • branches: [main] sous pull_request : 1 épingle sur 7 tombe
    • run: bash test-curl.sh remplacé par echo skipped : 1 épingle sur 7 tombe
    • continue-on-error: true sur l'étape du boot check : 0 épingle tombe (voir la suggestion 1)
  • Sécurité : aucun secret dans le diff (le token boot-from-copy-token est local et éphémère). Le refus du socket Docker est justifié. L'interpolation dans su -c ne porte que sur des valeurs contrôlées (le CMD est filtré par ^[A-Za-z0-9._/-]+$, et APP_DIR vient de mktemp).
  • Les formes que le script n'accepte pas (plusieurs COPY, des flags, la forme JSON, une source absente ou un dossier) échouent toutes bruyamment. Je n'ai trouvé aucun chemin de faux PASS silencieux sur le COPY.

Suggestions (non bloquantes)

  1. continue-on-error n'est pas épinglé (__tests__/ci.test.js). Un continue-on-error: true ajouté sur une étape la rend verte même quand elle échoue, et les 7 épingles restent vertes. C'est précisément le « succès apparent » qu'on cherche à éviter. Il suffirait d'une ligne : expect(ciCode).not.toMatch(/continue-on-error:/).
  2. Fermeture de l'issue : l'unique commit porte Refs #24, pas Fixes #24. Le Fixes #24 du corps de la PR ne sert à rien si le merge est poussé en fast-forward. Il faut donc fermer #24 à la main après le merge, ou reformuler le commit.
  3. Port fixe dans boot-from-copy.sh : si un autre serveur tient déjà :3097, le curl de la boucle d'attente peut obtenir une réponse de ce voisin avant que le serveur lancé ne meure en EADDRINUSE, et les assertions visent alors le mauvais processus. Ce n'est pas un risque en CI, où le conteneur est neuf. En local, BOOT_PORT permet de contourner. Une garde peu coûteuse : vérifier que le port est libre avant le démarrage, ou refaire un kill -0 $SERVER_PID après les assertions.
  4. Portée du contrôle : seul /health est sollicité. Un require() paresseux, placé dans un handler d'une autre route, échapperait au contrôle. Ce n'est pas le cas aujourd'hui, puisque tous les require sont au niveau module dans index.js et metrics.js. On pourrait le noter dans l'en-tête du script.

🤖 Generated with Claude Code

## Revue adversariale — PR #31 (issue #24) **Verdict : APPROVE** **Résumé** : le workflow et `boot-from-copy.sh` font ce que la PR annonce, et le contrôle du `COPY` attrape le défaut réel de #11. Les preuves de CI ont été vérifiées dans le log du run et non sur parole. Rien de bloquant. ### Vérifications faites - Statut de commit `4e7879d` : `CI / verification (pull_request)` = **success**, 21 s. Le log du run 1 (route web `/actions/runs/1/jobs/0/logs`) montre node v22.23.3, vitest **308 passed**, test-curl **45 passed, 0 failed**, `Image file set (3)`, les trois `ok` et `boot-from-copy: PASS`, puis `Job succeeded`. - En local (ports 3097/3098/3099 libres avant le lancement) : `vitest` donne 308/308 et `boot-from-copy.sh` renvoie PASS. - Mutations faites par le relecteur sur une copie hors dépôt : - `metrics.js` retiré du `COPY` : **FAIL**, `the server died at startup` (MODULE_NOT_FOUND) - `execSync` cassé dans `metrics.js`, donc des métriques disque à zéro : **FAIL**, `/health reported empty disk or memory metrics` - `CMD node index.js` (forme shell) : le script échoue bruyamment, et 1 épingle sur 7 tombe - `branches: [main]` sous `pull_request` : 1 épingle sur 7 tombe - `run: bash test-curl.sh` remplacé par `echo skipped` : 1 épingle sur 7 tombe - `continue-on-error: true` sur l'étape du boot check : **0 épingle tombe** (voir la suggestion 1) - Sécurité : aucun secret dans le diff (le token `boot-from-copy-token` est local et éphémère). Le refus du socket Docker est justifié. L'interpolation dans `su -c` ne porte que sur des valeurs contrôlées (le `CMD` est filtré par `^[A-Za-z0-9._/-]+$`, et `APP_DIR` vient de `mktemp`). - Les formes que le script n'accepte pas (plusieurs `COPY`, des flags, la forme JSON, une source absente ou un dossier) échouent toutes bruyamment. Je n'ai trouvé aucun chemin de faux PASS silencieux sur le `COPY`. ### Suggestions (non bloquantes) 1. **`continue-on-error` n'est pas épinglé** (`__tests__/ci.test.js`). Un `continue-on-error: true` ajouté sur une étape la rend verte même quand elle échoue, et les 7 épingles restent vertes. C'est précisément le « succès apparent » qu'on cherche à éviter. Il suffirait d'une ligne : `expect(ciCode).not.toMatch(/continue-on-error:/)`. 2. **Fermeture de l'issue** : l'unique commit porte `Refs #24`, pas `Fixes #24`. Le `Fixes #24` du corps de la PR ne sert à rien si le merge est poussé en fast-forward. Il faut donc fermer #24 à la main après le merge, ou reformuler le commit. 3. **Port fixe dans `boot-from-copy.sh`** : si un autre serveur tient déjà `:3097`, le `curl` de la boucle d'attente peut obtenir une réponse de ce voisin avant que le serveur lancé ne meure en `EADDRINUSE`, et les assertions visent alors le mauvais processus. Ce n'est pas un risque en CI, où le conteneur est neuf. En local, `BOOT_PORT` permet de contourner. Une garde peu coûteuse : vérifier que le port est libre avant le démarrage, ou refaire un `kill -0 $SERVER_PID` après les assertions. 4. **Portée du contrôle** : seul `/health` est sollicité. Un `require()` paresseux, placé dans un handler d'une autre route, échapperait au contrôle. Ce n'est pas le cas aujourd'hui, puisque tous les `require` sont au niveau module dans `index.js` et `metrics.js`. On pourrait le noter dans l'en-tête du script. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Mergé en fast-forward sur main (4e7879d) après rejeu local du tip : npm ci, 308/308 vitest, 45/45 test-curl.sh, boot-from-copy: PASS. Forgejo ne détecte pas le ff local, fermeture manuelle.

Mergé en fast-forward sur `main` (`4e7879d`) après rejeu local du tip : `npm ci`, 308/308 vitest, 45/45 `test-curl.sh`, `boot-from-copy: PASS`. Forgejo ne détecte pas le ff local, fermeture manuelle.
maximus closed this pull request 2026-09-28 00:51:02 +00:00
All checks were successful
CI / verification (pull_request) Successful in 21s
CI / verification (push) Successful in 19s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#31
No description provided.