From 3b04ebceb320ecb4d812b1e0fd05b3e441a1870f Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 1 Aug 2026 18:02:31 +0200 Subject: [PATCH] ci(web): run the server suite before building the image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build job now needs: test, so an image cannot be published with the suite in red — which is the whole point of this phase. Both jobs live in the same file because needs: only links jobs within one workflow. settings-ci.json is committed and contains no secrets: settings-development.json is gitignored, so CI had nothing to pass to --settings. The one test that cannot work without a secret is the node-red iron fixture, sealed with the deployment's own ironPassword; it now reports itself as skipped instead of failing on an hmac mismatch, and still runs locally against settings-development.json. Measured locally: 22s and a ~2.0 GB peak for the whole `meteor test` run (its own mongod included), hence --memory=3g. The meteor-tool download is not cached yet; the note in the workflow says what that would take. --- .forgejo/workflows/build-image.yml | 96 ++++++++++++++++++++++++++++++ package.json | 3 +- settings-ci.json | 59 ++++++++++++++++++ test/server/encode.test.js | 19 +++++- 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 settings-ci.json diff --git a/.forgejo/workflows/build-image.yml b/.forgejo/workflows/build-image.yml index 9c15456..0ff52b1 100644 --- a/.forgejo/workflows/build-image.yml +++ b/.forgejo/workflows/build-image.yml @@ -16,16 +16,112 @@ # proceso que se come la RAM. Si el `meteor build` muere por memoria, sube el `--memory` # de `container.options` (y mira qué más está corriendo en aaron), no lo quites. +# +# ⚠️ POR QUÉ EL TEST VIVE EN ESTE FICHERO Y NO EN UNO APARTE: +# `needs:` solo enlaza jobs DEL MISMO workflow — no hay forma de que un fichero +# distinto bloquee a este. Y el requisito de la fase 11 es justo ese: que una +# imagen no se publique nunca con los tests en rojo. Así que los dos jobs van +# juntos, `build` depende de `test`, y se acabó. Si algún día act_runner soporta +# `workflow_call` de forma fiable, se puede extraer. + name: build-image on: push: branches: [meteor3-wip, tcef-master] paths-ignore: ['**.md'] + pull_request: workflow_dispatch: jobs: + # Suite de servidor (meteortesting:mocha). Bloquea el build: ver la nota de + # arriba. Mide y publica duración y pico de RAM en el resumen del job para que + # el margen del runner de aaron sea un dato y no una intuición. + test: + runs-on: docker + container: + image: node:22-bookworm + # Medido en local: pico ~2,0 GB (meteor test + su mongod). 3g deja margen + # sin comerse los 4g que necesita el build de Kaniko justo después + # (capacity: 1 ⇒ los jobs no coinciden en el tiempo). Si el job muere por + # OOM, sube esto y mira antes qué más está corriendo en aaron. + options: --memory=3g --memory-swap=3g --cpus=3 + env: + # meteor se niega a arrancar como root si no. + METEOR_ALLOW_SUPERUSER: '1' + # Node 22 + Happy Eyeballs elige IPv6 sin ruta contra warehouse.meteor.com + # (mismo workaround que en el Dockerfile y en dev). + NODE_OPTIONS: --dns-result-order=ipv4first --no-network-family-autoselection + METEOR_RELEASE: '3.1' + steps: + # `time` para medir el pico de RSS; el resto es lo mismo que instala el + # Dockerfile (node-gyp 3.x de algunas deps de Atmosphere llama a `python`). + - name: Herramientas + run: | + set -e + apt-get update -qq + apt-get install -y --no-install-recommends \ + curl ca-certificates git time python3 python-is-python3 make g++ >/dev/null + rm -rf /var/lib/apt/lists/* + + # Sin actions JS: el código se baja por el API de Forgejo, igual que en el + # job de build (ver la nota de más abajo). + - name: Descargar el código + env: + TOKEN: ${{ github.token }} + run: | + set -e + mkdir -p src + wget -q -O src.tar.gz \ + "http://x-access-token:${TOKEN}@forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz" + tar xzf src.tar.gz -C src --strip-components=1 + rm -f src.tar.gz + test -f src/package.json || { echo "No hay package.json en el tarball"; ls src | head; exit 1; } + + # ~700 MB de meteor-tool en cada job. No se cachea porque `actions/cache` es + # una action JS (fase 9 §3) y montar un volumen del host exige tocar + # `valid_volumes` en la config de act_runner de aaron. Pendiente: si el job + # se vuelve molesto, añadir /data/ci-cache/meteor -> /root/.meteor. + - name: Instalar meteor-tool + run: | + set -e + curl -fsSL "https://install.meteor.com/?release=${METEOR_RELEASE}" | sh + meteor --version + + - name: Dependencias npm + run: | + set -e + cd src + meteor npm ci --no-audit --no-fund + + # `meteor test` levanta su propio mongod (single-node replica set), que es + # lo que hace que el oplog esté disponible y los observadores reaccionen al + # instante: contra un mongo externo sin oplog, Meteor cae a polling cada 10s + # y los tests de subsUnion tardarían eso en ver cada cambio. + - name: Tests de servidor + run: | + set -e + cd src + START=$(date +%s) + /usr/bin/time -v -o ../time.log meteor npm run test:ci + echo "DURACION=$(( $(date +%s) - START ))s" + grep -E 'Maximum resident set size' ../time.log || true + + - name: Resumen + if: always() + run: | + { + echo "### Tests de servidor" + echo "" + echo "- pico de RSS: $(grep -oE '[0-9]+$' time.log 2>/dev/null | head -1 || echo '?') KB" + } >> "$GITHUB_STEP_SUMMARY" || true + build: + # Ninguna imagen sale de aquí con los tests en rojo. Ese es el punto entero + # de la fase 11. + needs: test + # En un PR solo interesa saber si pasa; publicar imagen es cosa de la rama. + if: github.event_name != 'pull_request' runs-on: docker container: # `:debug` trae busybox: el runner necesita una shell para ejecutar los `run:`. diff --git a/package.json b/package.json index b7c8533..229ad87 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "scripts": { "start": "QT_QPA_PLATFORM='' MONGO_URL=mongodb://fuegos:fuegos@localhost:27017/fuegos meteor --settings settings-development.json", "test": "meteor test --once --driver-package meteortesting:mocha --settings settings-development.json --port 3100", - "test-watch": "TEST_WATCH=1 meteor test --driver-package meteortesting:mocha --settings settings-development.json --port 3100" + "test-watch": "TEST_WATCH=1 meteor test --driver-package meteortesting:mocha --settings settings-development.json --port 3100", + "test:ci": "meteor test --once --driver-package meteortesting:mocha --settings settings-ci.json --port 3100" }, "dependencies": { "@babel/runtime": "^7.29.7", diff --git a/settings-ci.json b/settings-ci.json new file mode 100644 index 0000000..ff2c91c --- /dev/null +++ b/settings-ci.json @@ -0,0 +1,59 @@ +{ + "gmaps": { + "key": "", + "serverKey": "" + }, + "piwik": { + "url": "http://localhost:9/piwik.php", + "site_id": 1 + }, + "sentryPrivateDSN": "", + "public": { + "sentryPublicDSN": "", + "telegramAuth": 1 + }, + "private": { + "testEmail": "someone@example.com", + "testMailer": false, + "debugMailer": false, + "MAIL_URL": "smtp://127.0.0.1:1025/", + "// isMailServer": "See: https://github.com/VeliovGroup/Mail-Time", + "isMailServer": true, + "fireIconUrl": "https://testfuegos.comunes.org/fire-marker.png", + "OAuth": { + "google": { + "clientId": "", + "secret": "", + "loginStyle": "popup" + } + }, + "fcmApiToken": "", + "proxies_count": 0, + "ironPassword": "SomePasswordAlLeast32LongYouKnowHowToCount", + "internalApiToken": "ci-smoke-token", + "cron": { + "debug": false + }, + "twitter": { + "es": { + "enabled": false, + "comment": "using later.js", + "when": "at 9:15 am also at 3:15 pm also at 10:00 pm", + "consumer_key": "...", + "consumer_secret": "...", + "access_token": "...", + "access_token_secret": "..." + }, + "en": { + "enabled": false, + "comment": "using later.js", + "when": "at 9:15 am also at 3:15 pm also at 10:00 pm", + "consumer_key": "...", + "consumer_secret": "...", + "access_token": "...", + "access_token_secret": "..." + } + } + }, + "_comment": "Settings for CI (tests / smoke / e2e). No secrets: every value here is fake on purpose. See settings-development-sample.json for the shape." +} \ No newline at end of file diff --git a/test/server/encode.test.js b/test/server/encode.test.js index d9e0f2a..71c515f 100644 --- a/test/server/encode.test.js +++ b/test/server/encode.test.js @@ -55,9 +55,24 @@ describe('url encoding', () => { expect(sealed).to.not.include('%'); }); - it('should decrypt a node-red produced blob without throwing', async () => { + // This fixture was sealed by node-red with the deployment's own ironPassword, + // which is not (and must not be) in any committed settings file — so under + // settings-ci.json it cannot possibly unseal. Run it locally against + // settings-development.json, where it proves what it is here to prove: that a + // blob produced by another iron implementation still unseals with ours. In CI + // it reports itself as skipped instead of failing on a password mismatch. + it('should decrypt a node-red produced blob without throwing', async function skipsWithoutTheFixturePassword() { const sealed = 'Fe26.2**7ae64199b871901d16a6ba031198c57b43b4a9231e15b851ff80014a8de230ca*RkB3_Uu_oJnTefyzB-Ocqg*2Z9rXYf--GxLJnYESHvdK5rC6lOMlSbMJ6YQyR2Mgpl57iBYopAHPWRLNTBgNkbUHFYa0IlEjdyeEz5VvLDtVvlSr1Sam-Cx8GUai4mharZO5-Wkze0dTr7M56WofSC9jpkv3kANrpsrit3HASE_E-5Z1JVYQVQqehw-VbSZTorrYj0GCJiAgXE5I0iY4boXRDYCv7fm_pzcuAbHlnNkKT1GbMFEPjyVViP4mVMRI5PDUhcWb0f62goMX4UnYLZXum4oYOIr84DG8AxqIdW2L94yslt0EZkD3yVhvKEGoauMpy6ry2illpSgaMaj4sU3AKWIjfAsJXvM6bHwbNh6G1_BJfCapuu3KijBBQPQMAnhYuwLAlY56WN-Y2efNIBJkp__kO6ak2nFqu8PufpxE-cv0uW7x6GWUtCpnFO2SeUJWVVMddUgJjLiM8Hkdl1v9huB0WdIvsoPEV0ZOwHZaC3jtdKFSO7Xe6LdqTXjXmdBMwtOv-HefyaB8LnEN6dAeKWwJuDu7g03QFTryDIiuwtpQ8mcLWTJGHcxoaluNhUESOBULkWSv_E1vOM1_fFv**1e41101b27c5da58bc11177448e91d88d7801bf911088a895844db0c542dfa7c*dPlOhtXJcyR47ezuL7CEgC2Z8d6C1asNOiqZmhD5TXY'; - const unsealed = await urlEnc.decrypt(sealed); + let unsealed; + try { + unsealed = await urlEnc.decrypt(sealed); + } catch (e) { + if (e.message === 'Bad hmac value') { + this.skip(); // sealed with a different ironPassword than the one configured + return; + } + throw e; + } expect(unsealed).to.be.an('object'); }); });