Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .agents/skills/deps-refresh/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ npm audit --omit=dev # what actually ships
npm outdated --workspaces --include-workspace-root
docker pull ghcr.io/data-fair/<service>:<version> # the released image
trivy image --scanners vuln ghcr.io/data-fair/<service>:<version>
grep -nE 'uses:|node-version' .github/workflows/*.y*ml
grep -hE '^FROM node' Dockerfile; cat .nvmrc; grep -A1 '"engines"' package.json
```

Trivy on the built image is not redundant with npm audit. It sees the alpine OS layer, which is usually where the criticals are, plus the npm bundled in the node base image, and it reports only what is actually shipped. npm audit sees the dev tree that never reaches production. Neither alone is a security answer.
Expand All @@ -39,6 +41,14 @@ Cheapest fix first:
- **Small dated packages** replaceable by a node built-in or by a dependency already in the tree.
- **`patches/`.** A `patch-package` patch often exists only because a fix was unreleased. Upgrading may let you delete the patch — check each one.

### CI workflows

`.github/workflows/` is a dependency manifest too, and nothing like `npm outdated` reports it. Most pins live in the reusable `reuse-quality.yml` and `reuse-build.yml`.

- **Actions to their current major.** Find the latest with `gh api repos/<owner>/<action>/releases/latest --jq .tag_name` (e.g. `actions/checkout`, `actions/setup-node`, `docker/build-push-action`, `docker/metadata-action`, `docker/login-action`, `docker/setup-buildx-action`). Old majors run on a node runtime GitHub deprecates and then removes, so a workflow that is green today can stop working on a date nobody chose. Read each major's release notes for renamed or removed inputs. Keep pinning by major tag (`@v5`), as the repos do. Don't switch to SHA pinning as a side effect of this pass.
- **Unmaintained third-party actions.** These follow the same rule as npm: replace or vendor them. `battila7/get-version-action@v2` only parses the tag, and a shell step does the same job: `echo "major=${GITHUB_REF_NAME#v}" | cut -d. -f1`, written to `$GITHUB_OUTPUT`.
- **One node major everywhere.** The `node-version` in CI, the `FROM node:X` in the Dockerfile, `.nvmrc` and `engines.node` must name the same major. CI exists to test what ships. `backup` once ran its quality checks on node 22 while its image shipped node 24. The Dockerfile is the source of truth: align the other three to it.

## Phase 4 — Majors, only when proven behaviour-neutral

Never take a major on the changelog alone. Install old and new side by side under aliases in the scratchpad and diff real behaviour against the repo's own fixtures:
Expand Down Expand Up @@ -73,14 +83,16 @@ Boot-test the image, do not just build it. Run the deps whose loader changed (co
docker run --rm -w /app/api <image> node --input-type=module -e 'import config from "config"; console.log(config.get("mode"))'
```

Workflow changes are only proven on GitHub. Run `actionlint` locally if it is installed. Push the branch, which triggers `commits.yml` and so `reuse-build.yml`. Open the PR, which triggers `reuse-quality.yml`. Watch both with `gh run watch`. `releases.yml` only runs on a `v*` tag, so nothing before the next release exercises its steps. Review that diff line by line, and say in the PR that it is untested.

Two traps specific to this pass:

- e2e specs run against the already-running dev server, whose module graph predates your upgrade. After changing vite plugins or anything they pull in, e2e results are stale until that server has been restarted — say so, and re-run them once it has.
- A single e2e failure under parallel load is usually a flake. Re-run it alone a few times before calling it a regression.

## Phase 6 — Report, then plan the rest

Split commits so a revert is surgical: security and base image, dead-and-replaced deps, verified majors, ui separate from api.
Split commits so a revert is surgical: security and base image, dead-and-replaced deps, verified majors, CI workflows, ui separate from api.

For every major you declined, write down **the specific blocker** — "the image ships the KML driver but not LIBKML", not "needs more work". That sentence is what saves the next session from re-deriving it. Persist it to memory or a follow-up issue, and offer the deferred list as the suggested next piece of work.

Expand All @@ -91,4 +103,7 @@ For every major you declined, write down **the specific blocker** — "the image
- Adding an `overrides` entry and assuming it applied. Confirm with `npm ls <pkg>`; in an existing lockfile an override often resolves to nothing.
- Reporting "0 vulnerabilities" from npm audit alone, without scanning the image.
- Deleting a dependency because grep found no import, before checking ambient `.d.ts`, `optimizeDeps` and dynamic `import()`.
- Trusting a peer range to catch an incompatible pairing. `@data-fair/lib-vuetify` 2.4.0–2.5.0 imports `resolveTheme` from `@data-fair/lib-vue/session.js`, which only exists since lib-vue 1.29.0, yet declares `lib-vue: ^1.15.0`: `npm ci` passes, `vite build` fails with `MISSING_EXPORT`. Bump the two together (2.5.1+ declares `^1.31`), and prove a bump with a real build.
- Bumping the Dockerfile's node major and leaving CI's `node-version` behind. The checks then pass on a runtime that is never deployed.
- Calling a workflow edit done without a run of it. Edits to `releases.yml` ship untested until the next tag.
- A lockfile diff far larger than your change. Note it, and say whether it is intended drift absorption or accidental churn.
28 changes: 26 additions & 2 deletions .github/workflows/conventional-label.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# automatic labelling of pull request based on conventional commit names in the PR titles
# see https://github.com/marketplace/actions/conventional-release-labels
# labels are consumed by the generated release notes, see .github/release.yaml
# (replaces the unmaintained bcoe/conventional-release-labels, same labels and behaviour)

on:
pull_request_target:
Expand All @@ -9,4 +10,27 @@ jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: bcoe/conventional-release-labels@v1
- name: Label the pull request from its title
env:
GH_TOKEN: ${{ github.token }}
ISSUE: repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}
TITLE: ${{ github.event.pull_request.title }}
run: |
# <type>["(" <scope> ")"]["!"]":"
[[ "$TITLE" =~ ^([a-zA-Z]+)(\([^\)]*\))?(!)?: ]] || exit 0
type="${BASH_REMATCH[1],,}"
labels=()
[ -n "${BASH_REMATCH[3]}" ] && labels+=(breaking)
case "$type" in
feat) labels+=(feature) ;;
fix) labels+=(fix) ;;
chore) labels=(ignore-for-release) ;;
esac
[ ${#labels[@]} -eq 0 ] && exit 0
for label in feature fix breaking ignore-for-release; do
gh api -X DELETE "$ISSUE/labels/$label" --silent 2>/dev/null || true
done
# the REST endpoint creates missing labels (e.g. "breaking")
args=()
for label in "${labels[@]}"; do args+=(-f "labels[]=$label"); done
gh api -X POST "$ISSUE/labels" "${args[@]}" --silent
2 changes: 1 addition & 1 deletion .github/workflows/releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: get_version
uses: battila7/get-version-action@v2
run: echo "major=${GITHUB_REF_NAME#v}" | cut -d. -f1 >> "$GITHUB_OUTPUT"

- name: Send Keel notifications to koumoul.com
run: |
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/reuse-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout git repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Login to Github container registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand All @@ -27,7 +27,7 @@ jobs:

- name: Prepare docker tags for main image
id: docker_meta_main
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
Expand All @@ -39,7 +39,7 @@ jobs:

- name: Build and push main image
id: docker_build_main
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
with:
context: .
push: true
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/reuse-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout git repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- uses: actions/setup-node@v3
- uses: actions/setup-node@v7
with:
node-version: 24
cache: 'npm'
Expand All @@ -38,7 +38,7 @@ jobs:
run: docker compose up -d --wait

- name: Start dev API
run: dotenv -- npm run dev-api &
run: dotenv run -- npm run dev-api &

- name: Install Playwright browser
run: npx playwright install --with-deps chromium
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ normal dev session already has it. Logs go to `dev/logs/dev-bridge.log`. It is o
Configure it in the settings UI as an **OpenAI Compatible** provider with base URL
`http://localhost:3194/v1` and **Compatibility Mode `compatible`** (the default mode
targets `/v1/responses`, which the bridge does not implement). Leave the API key empty.
Model listing for org providers goes through lib-node's SSRF-guarded axios, which refuses
loopback; `npm run dev-api` sets `SSRF_PUBLIC_IPS=127.0.0.1,::1` so the bridge is reachable.

`GET /_bridge/status` reports how many conversations are holding a live `claude` session.
It binds `127.0.0.1` only: it is unauthenticated and spends your subscription.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
##########################
FROM node:24.20.0-alpine3.24 AS base
FROM node:24.21.0-alpine3.24 AS base

WORKDIR /app
ENV NODE_ENV=production
Expand Down
9 changes: 4 additions & 5 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"license": "MIT",
"scripts": {
"dev": "mkdir -p ../dev/logs && NODE_ENV=development EVENTS_LOG_LEVEL=warn nodemon -e js,ts,json index.ts 2>&1 | tee ../dev/logs/dev-api.log",
"dev": "mkdir -p ../dev/logs && NODE_ENV=development EVENTS_LOG_LEVEL=warn SSRF_PUBLIC_IPS=127.0.0.1,::1 nodemon -e js,ts,json index.ts 2>&1 | tee ../dev/logs/dev-api.log",
"dev-benchmark": "NODE_ENV=benchmark node index.ts"
},
"imports": {
Expand All @@ -23,18 +23,17 @@
"@ai-sdk/openai": "^3.0.41",
"@ai-sdk/openai-compatible": "^2.0.52",
"@ai-sdk/provider": "^3.0.15",
"@data-fair/lib-express": "^1.19.0",
"@data-fair/lib-node": "^2.12.0",
"@data-fair/lib-express": "^1.29.0",
"@data-fair/lib-node": "^2.15.0",
"@data-fair/lib-utils": "^1.13.1",
"@data-fair/lib-validation": "^1.0.2",
"@openrouter/ai-sdk-provider": "^2.2.5",
"ai": "^6.0.116",
"ai-sdk-ollama": "^3.8.0",
"ajv-formats": "^3.0.1",
"axios": "^1.7.0",
"config": "^5.0.1",
"debug": "^4.4.3",
"dotenv": "^17.0.0",
"dotenv": "^18.0.7",
"express": "^5.0.0",
"helmet": "^8.1.0",
"http-terminator": "^3.2.0",
Expand Down
4 changes: 3 additions & 1 deletion dev/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/
import { axiosBuilder } from '@data-fair/lib-node/axios.js'
import { axiosAuth } from '@data-fair/lib-node/axios-auth.js'
import { privateHttpAgent, privateHttpsAgent } from '@data-fair/lib-node/http-agents.js'
import { pathToFileURL } from 'node:url'
import { encodeBreakdownKey } from '../api/src/usage/operations.ts'

Expand Down Expand Up @@ -261,7 +262,8 @@ interface UsageFixture {
}

export async function seedConsumption (): Promise<void> {
const ax = axiosBuilder({ baseURL })
// the dev api is on loopback: private agents, lib-node's default ones refuse it
const ax = axiosBuilder({ baseURL, httpAgent: privateHttpAgent, httpsAgent: privateHttpsAgent })

const postUsage = (fixture: UsageFixture) => ax.post('/api/test-env/usage', {
...fixture,
Expand Down
Loading
Loading