Report the endpoints actually called, not the ones declared #250
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| paths-ignore: | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| ollama_mlx: | |
| description: >- | |
| Run the Ollama tests on macOS against the MLX build of Gemma 4. | |
| The hosted macOS runner leaves Ollama about 4GB, which is under | |
| what gemma4:e2b-mlx needs, so this needs a larger runner. | |
| type: boolean | |
| default: false | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: ESLint | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run lint 2>&1 | tee out.txt || { | |
| tail -60 out.txt | while read -r line; do | |
| echo "::error::ESLint: $line" | |
| done | |
| exit 1 | |
| } | |
| test: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| node-version: ['22.x', '24.x'] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| - run: npm install | |
| - name: Generation, CLI and Bruno tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm test 2>&1 | tee out.txt || { | |
| (grep -A 18 "^not ok" out.txt | head -80; echo "--- tail ---"; tail -20 out.txt) | while read -r line; do | |
| echo "::error::Generation, CLI and Bruno tests: $line" | |
| done | |
| exit 1 | |
| } | |
| - name: Editor UI tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run test:ui 2>&1 | tee out.txt || { | |
| (grep -A 18 "^not ok" out.txt | head -80; echo "--- tail ---"; tail -20 out.txt) | while read -r line; do | |
| echo "::error::Editor UI tests: $line" | |
| done | |
| exit 1 | |
| } | |
| - name: Node-RED integration tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run test:nodered 2>&1 | tee out.txt || { | |
| (grep -A 18 "^not ok" out.txt | head -80; echo "--- tail ---"; tail -20 out.txt) | while read -r line; do | |
| echo "::error::Node-RED integration tests: $line" | |
| done | |
| exit 1 | |
| } | |
| - name: Command line picker tests | |
| if: runner.os == 'Linux' | |
| run: npm run test:wizard | |
| - name: Response scanning tests | |
| run: npm run test:errorwords | |
| - name: Bruno conversion equivalence tests | |
| run: npm run test:converters | |
| - name: Cross format equivalence tests | |
| run: npm run test:formats | |
| - name: Flow lint tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run test:nrlint 2>&1 | tee out.txt || { | |
| (grep -A 18 "^not ok" out.txt | head -80; echo "--- tail ---"; tail -20 out.txt) | while read -r line; do | |
| echo "::error::Flow lint tests: $line" | |
| done | |
| exit 1 | |
| } | |
| - name: Plugin package tests | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run test:plugin 2>&1 | tee out.txt || { | |
| (grep -A 18 "^not ok" out.txt | head -80; echo "--- tail ---"; tail -20 out.txt) | while read -r line; do | |
| echo "::error::Plugin package tests: $line" | |
| done | |
| exit 1 | |
| } | |
| browser: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| browser: chromium | |
| - os: ubuntu-latest | |
| browser: firefox | |
| - os: ubuntu-latest | |
| browser: webkit | |
| - os: windows-latest | |
| browser: msedge | |
| - os: macos-latest | |
| browser: webkit | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Install the browser | |
| run: npx playwright install --with-deps ${{ matrix.browser == 'msedge' && 'msedge' || matrix.browser }} | |
| - name: Editor tests in ${{ matrix.browser }} | |
| id: browser-tests | |
| shell: bash | |
| env: | |
| BROWSERS: ${{ matrix.browser }} | |
| run: npm run test:browser 2>&1 | tee browser.log | |
| - name: Show the browser log | |
| if: failure() && steps.browser-tests.outcome != 'success' | |
| shell: bash | |
| run: | | |
| echo "===== browser.log =====" | |
| cat browser.log || echo "no log produced" | |
| echo "===== end =====" | |
| # The failures, not the tail: a passing test that happens to run last | |
| # would otherwise be all the annotation shows. | |
| (grep -E "^ *not ok" browser.log | head -20; \ | |
| echo '--- detail ---'; \ | |
| grep -A 14 -E "^ *not ok" browser.log | grep -E \ | |
| "error:|Expected|actual:|expected:|AssertionError|Timeout|at " | head -40) \ | |
| | while read -r line; do | |
| echo "::error::${{ matrix.browser }}: ${line}" | |
| done | |
| exit 1 | |
| live: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Check the bundled httpbingo definition against the live service | |
| run: npm run check:httpbingo | |
| - name: Compare the Bruno starter guide against Node-RED | |
| run: npm run check:bruno-guide | |
| - name: Call real public APIs through Node-RED | |
| env: | |
| LIVE_RESULTS: tests/live-results/latest.json | |
| run: npm run test:live | |
| - name: Keep the comparison results as an artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: live-comparisons | |
| path: tests/live-results/ | |
| if-no-files-found: ignore | |
| - name: Call a corpus of public APIs through Node-RED | |
| env: | |
| LIVE_CORPUS: '1' | |
| run: npm run test:live | |
| node-floor: | |
| # The declared floor has to be a runtime where the editor actually works, | |
| # in every browser, not merely one where Node-RED boots. | |
| runs-on: ubuntu-latest | |
| needs: test | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| browser: [chromium, firefox, webkit] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - run: npx playwright install --with-deps ${{ matrix.browser }} | |
| - name: Check the declared node floor in ${{ matrix.browser }} | |
| shell: bash | |
| env: | |
| BROWSERS: ${{ matrix.browser }} | |
| RED_VERSION: 4.1.13 | |
| run: npm run check:node-floor | |
| corpus-matrix: | |
| # One job per definition. A public API that is down or rate limiting should | |
| # cost one red square, not the whole build, and twenty of them in sequence | |
| # would take longer than everything else combined. | |
| runs-on: ubuntu-latest | |
| needs: test | |
| outputs: | |
| definitions: ${{ steps.read.outputs.definitions }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - id: read | |
| run: | | |
| echo "definitions=$(node -e " | |
| const c = require('./tests/specs/corpus.json'); | |
| process.stdout.write(JSON.stringify(c.definitions.map(d => ( | |
| { id: d.id, spec: d.spec })))); | |
| ")" >> "$GITHUB_OUTPUT" | |
| corpus: | |
| runs-on: ubuntu-latest | |
| needs: corpus-matrix | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 20 | |
| matrix: | |
| definition: ${{ fromJson(needs.corpus-matrix.outputs.definitions) }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Call ${{ matrix.definition.spec }} for real | |
| shell: bash | |
| env: | |
| SPEC: ${{ matrix.definition.spec }} | |
| run: | | |
| set -o pipefail | |
| node tests/test-corpus.js 2>&1 | tee corpus.log || { | |
| # Only the verdict lines. A dump of an eight megabyte response | |
| # would otherwise be the whole annotation, which is what happened | |
| # with the apis.guru catalogue. | |
| grep -E "^FAIL" corpus.log | head -20 | cut -c1-300 | while read -r line; do | |
| echo "::error::${{ matrix.definition.id }}: ${line}" | |
| done | |
| exit 1 | |
| } | |
| corpus-progress: | |
| # Records what the corpus jobs found and retires the definitions that | |
| # passed, so the next run spends its time on ones not yet proven. | |
| runs-on: ubuntu-latest | |
| # The default token is read only for contents, and pushing without this | |
| # fails with a bare exit code 128. | |
| permissions: | |
| contents: write | |
| needs: corpus | |
| if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - name: Record the results | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node tests/record-progress.js "${{ github.run_id }}" | |
| - name: Commit the progress | |
| run: | | |
| set -e | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add tests/progress.md tests/specs/corpus.json | |
| git diff --cached --quiet && exit 0 | |
| git commit -m "Record corpus progress [skip ci]" | |
| git pull --rebase origin main || true | |
| git push origin HEAD:main | |
| ollama: | |
| # An MLX build needs the Metal stack on Apple silicon, so the Gemma 4 run | |
| # goes to macOS. The hosted macOS runner only leaves Ollama around 4GB | |
| # though, and gemma4:e2b-mlx asks for 6GB, so that path is opt in and the | |
| # everyday run stays on Linux with a model that fits. | |
| runs-on: ${{ inputs.ollama_mlx && 'macos-latest' || 'ubuntu-latest' }} | |
| needs: test | |
| # A multi gigabyte pull plus local inference, so far longer than a unit | |
| # test run. | |
| timeout-minutes: 60 | |
| env: | |
| OLLAMA_MODEL: ${{ inputs.ollama_mlx && 'gemma4:e2b-mlx' || 'gemma3:4b' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Install Ollama | |
| run: | | |
| set -e | |
| if [ "$RUNNER_OS" = "macOS" ]; then | |
| brew install --cask ollama-app || brew install ollama | |
| else | |
| curl -fsSL https://ollama.com/install.sh | sh | |
| fi | |
| - name: Start Ollama and pull the model | |
| run: | | |
| set -e | |
| OLLAMA_BIN="$(command -v ollama || echo /Applications/Ollama.app/Contents/Resources/ollama)" | |
| echo "OLLAMA_BIN=$OLLAMA_BIN" >> "$GITHUB_ENV" | |
| "$OLLAMA_BIN" serve > ollama.log 2>&1 & | |
| for i in $(seq 1 90); do | |
| curl -sf http://127.0.0.1:11434/api/version && break | |
| sleep 2 | |
| done | |
| curl -sf http://127.0.0.1:11434/api/version || { | |
| echo '::error::ollama: the server never came up' | |
| tail -40 ollama.log | |
| exit 1 | |
| } | |
| "$OLLAMA_BIN" pull "$OLLAMA_MODEL" | |
| "$OLLAMA_BIN" list | |
| - name: Exercise every Ollama endpoint through Node-RED | |
| shell: bash | |
| env: | |
| OLLAMA_URL: http://127.0.0.1:11434 | |
| run: | | |
| set -o pipefail | |
| npm run test:ollama 2>&1 | tee ollama-tests.log || { | |
| (grep -E "^not ok| error:" ollama-tests.log | head -25; \ | |
| echo '--- server log ---'; tail -30 ollama.log) | while read -r line; do | |
| echo "::error::ollama: ${line}" | |
| done | |
| exit 1 | |
| } | |
| samples: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Generate the JavaScript for every endpoint | |
| run: npm run samples | |
| - uses: actions/upload-artifact@v7 | |
| id: samples-artifact | |
| with: | |
| name: generated-code | |
| path: samples | |
| - name: Report where to download the generated code | |
| run: | | |
| echo "::notice::Generated code: ${{ steps.samples-artifact.outputs.artifact-url }}" | |
| echo "Generated code: ${{ steps.samples-artifact.outputs.artifact-url }}" \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| coverage: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| - run: npm install | |
| - name: Measure coverage | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| npm run coverage 2>&1 | tee coverage.log || { | |
| (grep -E "^not ok" coverage.log | head -25; \ | |
| echo '--- detail ---'; \ | |
| grep -A 12 "^not ok" coverage.log | head -80) | while read -r line; do | |
| echo "::error::coverage: ${line}" | |
| done | |
| exit 1 | |
| } | |
| { | |
| echo '## Coverage' | |
| echo | |
| echo '```' | |
| sed -n '/Coverage summary/,/^=\{10,\}$/p' coverage.log | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - uses: actions/upload-artifact@v7 | |
| id: coverage-artifact | |
| with: | |
| name: coverage-report | |
| path: coverage | |
| - name: Report where to download the coverage report | |
| run: | | |
| echo "::notice::Coverage report: ${{ steps.coverage-artifact.outputs.artifact-url }}" | |
| echo "Coverage report: ${{ steps.coverage-artifact.outputs.artifact-url }}" \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| release: | |
| runs-on: ubuntu-latest | |
| needs: [lint, test, browser, live, coverage, ollama, samples, node-floor] | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| registry-url: https://registry.npmjs.org | |
| - run: npm install | |
| - name: Build the tarball | |
| run: | | |
| set -e | |
| npm pack | |
| mv node-red-flowgen-*.tgz node-red-flowgen.tgz | |
| tar tzf node-red-flowgen.tgz | |
| - name: Check the tag matches package.json | |
| run: | | |
| set -e | |
| PKG_VERSION=$(node -p "require('./package.json').version") | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then | |
| echo "package.json is $PKG_VERSION but the tag is $TAG_VERSION" >&2 | |
| exit 1 | |
| fi | |
| - name: Remove older releases and tags | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -e | |
| gh release list --repo "$GITHUB_REPOSITORY" --limit 200 \ | |
| --json tagName --jq '.[].tagName' > releases.txt || true | |
| while read -r tag; do | |
| [ -z "$tag" ] && continue | |
| [ "$tag" = "$GITHUB_REF_NAME" ] && continue | |
| gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag || true | |
| done < releases.txt | |
| git ls-remote --tags origin | awk '{print $2}' | sed 's|refs/tags/||' \ | |
| | grep -v '\^{}' | sort -u > tags.txt || true | |
| while read -r tag; do | |
| [ -z "$tag" ] && continue | |
| [ "$tag" = "$GITHUB_REF_NAME" ] && continue | |
| git push origin ":refs/tags/$tag" || true | |
| done < tags.txt | |
| - name: Publish the release with the tarball | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -e | |
| gh release create "$GITHUB_REF_NAME" node-red-flowgen.tgz \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --title "$GITHUB_REF_NAME" \ | |
| --notes "" \ | |
| || gh release upload "$GITHUB_REF_NAME" node-red-flowgen.tgz \ | |
| --repo "$GITHUB_REPOSITORY" --clobber | |
| - name: Publish to npm | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -e | |
| if [ -z "$NODE_AUTH_TOKEN" ]; then | |
| echo "::warning::NPM_TOKEN is not set; skipping npm publish" | |
| exit 0 | |
| fi | |
| npm publish --access public |