From 583366e3ee1369a3b52fef97ff175c69beec8d7c Mon Sep 17 00:00:00 2001 From: Renan Alves de Oliveira Date: Sun, 4 Oct 2026 14:17:06 -0300 Subject: [PATCH] fix(dashboard): constrain dependency updates and strengthen CI checks --- .github/dependabot.yml | 7 ++++ .github/workflows/dashboard-ci.yml | 2 + .github/workflows/deploy.yml | 2 + dashboard/README.md | 8 +++- dashboard/package-lock.json | 18 +-------- dashboard/package.json | 2 +- dashboard/scripts/check-build.mjs | 13 +++++- dashboard/tests/check-build.test.mjs | 59 ++++++++++++++++++++++++++++ 8 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 dashboard/tests/check-build.test.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 083ffe2..7cea9c9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,13 @@ updates: directory: /dashboard schedule: interval: monthly + ignore: + # Keep the compiler within typescript-eslint's supported API range. + - dependency-name: typescript + versions: ['>=6.1.0'] + # Node types must match the Node 24 runtime in .nvmrc and CI. + - dependency-name: '@types/node' + versions: ['>=25.0.0'] groups: react: patterns: [react, react-dom, '@types/react*'] diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 6f95a27..91bcdcf 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -28,6 +28,8 @@ jobs: working-directory: dashboard steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: dashboard/.nvmrc diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80f8d46..8a2d11a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,6 +25,8 @@ jobs: working-directory: dashboard steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: dashboard/.nvmrc diff --git a/dashboard/README.md b/dashboard/README.md index 22d4bbe..62273f5 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -12,7 +12,7 @@ Interactive dashboard for exploring astronomical lens data. The dashboard uses Node 24 LTS/npm 11, React 19, Material UI 9 and Vite 8. The MinIO exporter uses Python 3.14, pandas 3 and PyArrow. TypeScript stays -on version 6 because the ESLint integration currently supports versions below +on the 6.0 patch line because the ESLint integration currently supports versions below 6.1; see [typescript-eslint compatibility](https://typescript-eslint.io/users/dependency-versions/). ## Quick Start @@ -70,7 +70,11 @@ shards, WebP assets and omission of redundant source catalogs). Pages artifact after a push to `main` or `streamlit`, or a manual run on either branch. Both workflows use Ubuntu 24.04, Node 24, dependency caches and Actions pinned to release commit hashes. Deployment permissions belong to the deploy -job. Dependabot checks npm dependencies and Actions monthly. +job. Both build checkouts disable credential persistence. Dependabot checks npm +dependencies and Actions monthly, allowing Node 24 type updates and TypeScript +6.0 patches. Newer Node types or TypeScript versions require a coordinated runtime +or ESLint migration; the ignore rules in `.github/dependabot.yml` must be reviewed +when that compatibility changes. Install the reproducible exporter environment with: diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 89f56cd..ac1c0f1 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -26,7 +26,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.13.0", - "typescript": "^6.0.3", + "typescript": "~6.0.3", "typescript-eslint": "^8.71.0", "vite": "^8.3.2" }, @@ -3528,22 +3528,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 9a9f9e2..a7500bb 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -33,7 +33,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.13.0", - "typescript": "^6.0.3", + "typescript": "~6.0.3", "typescript-eslint": "^8.71.0", "vite": "^8.3.2" }, diff --git a/dashboard/scripts/check-build.mjs b/dashboard/scripts/check-build.mjs index 7a90425..c9cd182 100644 --- a/dashboard/scripts/check-build.mjs +++ b/dashboard/scripts/check-build.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { readFile, readdir, access } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; -const directory = fileURLToPath(new URL('../dist/', import.meta.url)); +const directory = process.argv[2] || fileURLToPath(new URL('../dist/', import.meta.url)); const html = await readFile(`${directory}/index.html`, 'utf8'); assert.match(html, /\/slcomp\/assets\//, 'Pages asset URLs must retain /slcomp/'); const index = JSON.parse(await readFile(`${directory}/data/catalog.json`)); @@ -10,6 +10,17 @@ assert.equal((await readdir(`${directory}/data/objects`)).filter(file => file.en const dataFiles = await readdir(`${directory}/data`); for (const source of ['database.json', 'consolidated_database.json', 'cutouts.json']) assert(!dataFiles.includes(source), `Redundant source ${source} must not ship`); const manifest = JSON.parse(await readFile(`${directory}/footprints/manifest.json`)); +const expectedLayerIds = [ + 'legacy', 'des', 'hsc', 'kids', 'rcslens', 'cs82', 'cfhtlens', 'sdss', + 'delve', 'gama', 'ozdes', 'wigglez', '2slaq', '2df', '6df', 'lamost', + 'ssrs', 'lcrs', 'vipers', 'deep2', 'zcosmos', 'cnoc', 'ages', 'mgc', + '2mrs', 'pscz', 'cfa', 'vvds', +]; +assert.deepEqual( + manifest.layers.map(layer => layer.id).sort(), + expectedLayerIds.sort(), + 'Footprint manifest must contain exactly the expected 28 layers', +); for (const layer of manifest.layers) { for (const key of ['image', 'border']) { assert(layer[key].endsWith('.webp')); diff --git a/dashboard/tests/check-build.test.mjs b/dashboard/tests/check-build.test.mjs new file mode 100644 index 0000000..054c84a --- /dev/null +++ b/dashboard/tests/check-build.test.mjs @@ -0,0 +1,59 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, writeFile, mkdir, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { execFile } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const run = promisify(execFile); +const checker = fileURLToPath(new URL('../scripts/check-build.mjs', import.meta.url)); +const manifestSource = new URL('../public/footprints/manifest.json', import.meta.url); + +async function artifact(t) { + const directory = await mkdtemp(join(tmpdir(), 'slcomp-build-check-')); + t.after(() => rm(directory, { recursive: true, force: true })); + for (const path of ['data/objects', 'footprints', 'assets']) { + await mkdir(join(directory, path), { recursive: true }); + } + await writeFile(join(directory, 'index.html'), ''); + await writeFile(join(directory, 'data/catalog.json'), JSON.stringify({ objects: [{ JNAME: 'J0001' }] })); + await Promise.all(Array.from({ length: 256 }, (_, index) => + writeFile(join(directory, 'data/objects', `${index.toString(16).padStart(2, '0')}.json`), '{}'))); + const manifest = JSON.parse(await readFile(manifestSource, 'utf8')); + await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify(manifest)); + for (const layer of manifest.layers) { + for (const key of ['image', 'border']) await writeFile(join(directory, 'footprints', layer[key]), 'asset'); + } + await writeFile(join(directory, 'slcomp.webp'), 'asset'); + await writeFile(join(directory, 'assets/observatory-test.webp'), 'asset'); + return { directory, manifest }; +} + +test('build validation accepts the complete footprint set regardless of order', async t => { + const { directory, manifest } = await artifact(t); + manifest.layers.reverse(); + await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify(manifest)); + await run(process.execPath, [checker, directory]); +}); + +test('build validation rejects missing, duplicate and unexpected footprint IDs', async t => { + const { directory, manifest } = await artifact(t); + const variants = [ + manifest.layers.slice(1), + [...manifest.layers, manifest.layers[0]], + [{ ...manifest.layers[0], id: 'unexpected' }, ...manifest.layers.slice(1)], + ]; + for (const layers of variants) { + await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify({ ...manifest, layers })); + await assert.rejects(run(process.execPath, [checker, directory]), error => + error.stderr.includes('Footprint manifest must contain exactly the expected 28 layers')); + } +}); + +test('build validation rejects a missing overlay file even with a complete manifest', async t => { + const { directory, manifest } = await artifact(t); + await rm(join(directory, 'footprints', manifest.layers[0].border)); + await assert.rejects(run(process.execPath, [checker, directory]), error => error.stderr.includes('ENOENT')); +});