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
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
NODE_ENV=development

APPLICATIONINSIGHTS_CONNECTION_STRING=
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://localhost/

# Optional, but without it GitHub rate limiting will usually stop the rulesets
# and codelists downloading at startup.
GITHUB_OAUTH_APP_CLIENT_ID=
GITHUB_OAUTH_APP_CLIENT_SECRET=

REDIS_PORT=6379
REDIS_HOSTNAME=redis # replace with localhost if you run outside docker

VALIDATOR_SERVICES_URL=https://dev-func-validator-services.azurewebsites.net/api
VALIDATOR_SERVICES_URL=https://func-validator-services-dev.azurewebsites.net/api
VALIDATOR_SERVICES_KEY_NAME=x-functions-key
VALIDATOR_SERVICES_KEY_VALUE=

DATASTORE_SERVICES_URL=https://dev-func-datastore-services.azurewebsites.net/api
DATASTORE_SERVICES_URL=https://func-datastore-services-dev.azurewebsites.net/api
DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME=x-functions-key
DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE=
DATASTORE_SERVICES_IATI_IDENTIFIERS_EXIST_MAX_NUMBER_OF_IDS=5000
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ updates:
directory: '/'
schedule:
interval: 'daily'
ignore:
# Pinned exactly, and upgraded by hand. A libxml2-wasm release can carry a different
# bundled libxml2, which is what decides whether a file is schema valid - and since
# v0.7.0 its libxml2 submodule tracks the maintainer's fork rather than an upstream
# release tag, so a bump also takes whatever else is on that branch. PR CI does not
# run the differential harness that verified verdicts against the Dashboard, and
# dependabot PRs auto-merge once CI is green, so a bump would otherwise land with no
# human reading it. See README "XML Library".
- dependency-name: 'libxml2-wasm'

# Enable version updates for GitHub Actions
- package-ecosystem: 'github-actions'
Expand Down
35 changes: 33 additions & 2 deletions .github/workflows/develop-func-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ on:
- main

jobs:
# Running the integration tests against the Docker container requires temporarily
# setting "authLevel": "anonymous" in the function.json files (see README), because
# the container has no AzureWebJobsStorage and so cannot validate function keys.
# Committing that would leave the deployed endpoints reachable without a key.
# There is a matching .husky/pre-commit guard, but hooks are skippable with
# --no-verify and are absent in a fresh clone until `npm install` has run.
check-auth-level:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@v4

- name: 'Reject authLevel anonymous in function.json'
run: |
if grep -il '"authLevel"[[:space:]]*:[[:space:]]*"anonymous"' */function.json > /tmp/hits; then
echo "::error::function.json committed with authLevel anonymous - the deployed endpoint would need no function key"
echo "Offending files:"
cat /tmp/hits
echo ""
echo "Restore with: git checkout -- '*/function.json'"
exit 1
fi
echo "OK - all function.json files require a function key."

build-test-node:
runs-on: ubuntu-latest
steps:
Expand All @@ -34,8 +58,15 @@ jobs:
npm i -g npm@^10
npm ci

# Runs before the unit tests because it catches a class they cannot: a source file
# importing a package that is not installed. No unit test imports utils.js - it
# pulls in Redis - so a tree where utils.js and package.json disagree about the XML
# library still reports every test passing.
- name: 'Lint'
run: npx eslint .

- name: 'Run unit tests'
run: npm run rules:test
run: npm run unit:test

build-docker:
runs-on: ubuntu-latest
Expand All @@ -48,7 +79,7 @@ jobs:
docker build . -t validator-local:latest

automerge:
needs: [build-test-node, build-docker]
needs: [check-auth-level, build-test-node, build-docker]
runs-on: ubuntu-latest
permissions:
pull-requests: write
Expand Down
25 changes: 25 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Running the integration tests against the Docker container requires temporarily
# setting "authLevel": "anonymous" in the function.json files (see README). The
# container has no AzureWebJobsStorage, so it cannot validate function keys and
# every request 401s otherwise.
#
# Committing that change would leave the deployed endpoints reachable without a
# function key. This guard refuses the commit instead of relying on memory.
#
# Restore with: git checkout -- '*/function.json'
for f in $(git diff --cached --name-only --diff-filter=ACM | grep 'function\.json$'); do
if git show ":$f" | grep -qi '"authLevel"[[:space:]]*:[[:space:]]*"anonymous"'; then
echo ""
echo "ERROR $f is staged with \"authLevel\": \"anonymous\""
echo ""
echo " That would leave the deployed endpoint unauthenticated."
echo " It is only meant to be set temporarily, to run the"
echo " integration tests against the Docker container."
echo ""
echo " Restore all of them with:"
echo " git checkout -- '*/function.json'"
echo ""
exit 1
fi
done

npx --no-install lint-staged
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

### Changed

### Fixed

### Removed

## [2.5.0] - 2026-08-24

### Changed

- Replaced libxmljs2 with libxml2-wasm, moving from libxml2 2.9.9 (2019) to
2.15.1. libxmljs2 is no longer maintained and no release of it carries a newer
libxml2, so the library was replaced rather than upgraded.
- **`?details=true` responses have changed shape.** Error details are now
`{ line, column }`, where they were the raw libxml2 error struct. See Removed.
- Schemas are now compiled once at startup rather than re-parsed for every
activity, so validation of files with schema errors should be faster.

### Added

- Schema unit tests (`schema-unit-tests`) and tests for file parsing and
metadata (`utils-unit-tests`). Neither area had any coverage before.
- A pre-commit hook and CI check that refuse a commit setting
`"authLevel": "anonymous"` in any `function.json`. That value is only meant to
be set temporarily, to run the integration tests against the Docker container.

### Fixed

- Schema validation: sign-only decimals such as `<value> - </value>` are now
reported; over-long decimals no longer are. This is the divergence with the
IATI Dashboard reported in
[#574](https://github.com/IATI/js-validator-api/issues/574).
- `.env.example`: corrected the dev service hostnames

### Removed

- `libxmljs2`, and with it the native build dependency — no node-gyp,
prebuilt binaries or glibc coupling.
- `str1`, `code`, `level`, `domain` and `int1` from the `details` object
returned by `?details=true`. `str1` still appears within the error message;
`level` and `domain` were constant; `str2`, `str3` and `int1` were never
populated. `code`, the numeric libxml2 constraint identifier, has no
replacement — libxml2-wasm does not expose it.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ If it has warnings or errors, you'll see them listed.

APPLICATIONINSIGHTS_CONNECTION_STRING

- Needs to be set for running locally, but will not actually report telemetry to the AppInsights instance in my experience
- **Required — the app will not start without it.** `config/appInsights.js` calls
`appInsights.setup(...).start()` at module load. If the value is empty or malformed the SDK throws
`Instrumentation key not found`, the Functions worker fails to load every function that imports it,
and requests get `503 Function host is not running` (or an empty 500) naming no cause.
For local development any syntactically valid connection string works; no telemetry is sent. The
value shipped in `.env.example` is a usable dummy:

GITHUB_OAUTH_APP_CLIENT_ID
GITHUB_OAUTH_APP_CLIENT_SECRET
Expand All @@ -109,14 +114,19 @@ VALIDATOR_SERVICES_KEY_VALUE=

- URL and API Key for Validator Services, used to get list of Publisher Identifiers

DATASTORE_SERVICES_URL=https://dev-func-datastore-services.azurewebsites.net/api
DATASTORE_SERVICES_URL=https://func-datastore-services-dev.azurewebsites.net/api
DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME=x-functions-key
DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE=
DATASTORE_SERVICES_IATI_IDENTIFIERS_EXIST_MAX_NUMBER_OF_IDS=5000

- URL and API Key for datastore services, used by the advisory system to check for the
existence of IATI Identifiers in the Datastore

- VALIDATOR_SERVICES_KEY_VALUE and DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE are outbound
credentials for Azure Functions. If you are an authorised developer on this
codebase you can get them from the Azure portal for the respective Function Apps.
If you do not have Azure access, ask the team.

### App config defaults (set in `config/config.js`)

```
Expand Down Expand Up @@ -166,7 +176,7 @@ let myEnvVariable = config.ENV_VAR

## Unit Tests

- `npm run rules:test`
- `npm run unit:test`

There is a large set of Mocha unit tests for the Rulesets logic in `ruleset-unit-tests`.

Expand All @@ -185,7 +195,7 @@ In Docker container
- Install newman globally `npm i -g newman`
- Edit `function.json` files to set `"authLevel": "anonymous"`, don't forget to change back!
- Start function `npm run docker:start`
- Run Tests `npm docker:int:test`
- Run Tests `npm run docker:int:test`

### Modifying/Adding

Expand All @@ -203,6 +213,23 @@ Using files:

https://github.com/IATI/IATI-Internal-Wiki#development-process

## XML Library

XML parsing and XSD schema validation use [`libxml2-wasm`](https://github.com/jameslan/libxml2-wasm), a WebAssembly build of libxml2. `libxml2-wasm` builds its libxml2 from a git submodule, and from v0.7.0 that submodule points at the maintainer's own fork rather than upstream — earlier releases such as v0.6.0 pinned a clean upstream release tag. Version 0.7.1 pins commit `f52e859`, which is the **v2.15.1 release plus two unmerged commits** by the `libxml2-wasm` maintainer adding Windows path handling. It contains all of 2.15.1, but sits on a branch off it, so those two commits are not in 2.15.2 or 2.15.3.

Those patches touch `uri.c` and `xmlIO.c`, which is the code resolving `xsd:include`, but they do not change behaviour here: every branch they add is guarded by a runtime flag that defaults to off, and `libxml2-wasm` explicitly disables it on any platform other than Windows (`node_modules/libxml2-wasm/lib/libxml2.mjs`). Behaviour was also compared against v0.6.0, which pins the clean upstream v2.14.5 tag, across 426 real published datasets with no disagreement in verdict or error count.

When upgrading `libxml2-wasm`, check whether the submodule has returned to an upstream tag. While it stays on the fork, each bump also takes whatever else is on that branch.

It replaced the native `libxmljs2`, which is no longer maintained and still bundles libxml2 2.9.9 from 2019 — old enough to accept values that later versions correctly reject, so the Validator disagreed with the Dashboard on whether a file was schema valid. No release of `libxmljs2` carries a newer libxml2, which is why the library was changed rather than upgraded.

Two things to know when working with it:

- Documents and compiled schemas hold memory outside the JS heap and must be `dispose()`d. Whatever creates an `XmlDocument` is responsible for freeing it, normally in a `finally`.
- It throws on a parse failure whenever libxml2 recorded any error at all, even when a usable document was still produced. `libxmljs2` threw only when no document could be built, so problems that do not prevent a tree being built — an undeclared namespace prefix, say — were reported later as schema errors. `utils/xmlParse.js` restores that behaviour; without it such files would be rejected with a file level `0.1.1` instead.

Note the `xmllint --recover` pre-pass is a separate, system-installed libxml2 (see Prerequisities), so it is generally an older version than the one used for validation.

## Customised Dependencies

### xpath
Expand Down
Loading
Loading