diff --git a/.github/actions/build-native/action.yml b/.github/actions/build-native/action.yml new file mode 100644 index 0000000..766777f --- /dev/null +++ b/.github/actions/build-native/action.yml @@ -0,0 +1,17 @@ +name: Build the native tools +description: The CLAP prober, the LV2 measurer, their interposer and the fixture plugins the tests qualify. + +# Shared by ci.yml and publish.yml. deps/ also holds omx-clap-core's LV2 feature provider at the packages' floor, +# built for the measurer to link; asan is the measurer under AddressSanitizer, which the leak check runs. +# src/workflows-agree.test.ts fails if a workflow stops using this action. + +runs: + using: composite + steps: + - name: Build + shell: bash + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends liblilv-dev lv2-dev + make -C native deps all asan diff --git a/.github/actions/install-plugin-hostd/action.yml b/.github/actions/install-plugin-hostd/action.yml new file mode 100644 index 0000000..d370838 --- /dev/null +++ b/.github/actions/install-plugin-hostd/action.yml @@ -0,0 +1,23 @@ +name: Install plugin-hostd and its fake worker +description: plugin-hostd with pin_file (0.2.0 and after) and its fake worker, from the FreeMixer channel, for the pins-file test. + +# Shared by ci.yml and publish.yml: both run the whole suite, and the pins-file test starts the real daemon on a +# qualified pins file. Bookworm is the suite built against the oldest glibc the channel serves, which the runner's +# Ubuntu runs; a floor only, no ceiling. src/workflows-agree.test.ts fails if a workflow stops using this action. + +runs: + using: composite + steps: + - name: Install from the FreeMixer channel + shell: bash + run: | + set -euo pipefail + sudo install -d /etc/apt/keyrings + sudo curl -fsSL --retry 5 --retry-all-errors -o /etc/apt/keyrings/freemixer.asc https://freemixer.github.io/deb/freemixer.asc + echo "deb [signed-by=/etc/apt/keyrings/freemixer.asc] https://freemixer.github.io/deb/debian/bookworm ./" | sudo tee /etc/apt/sources.list.d/freemixer.list + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends plugin-hostd plugin-hostd-tests + # apt takes no version relation on the command line: the floor is checked on what it installed + dpkg --compare-versions "$(dpkg-query -W -f='${Version}' plugin-hostd)" ge 0.2.0 + echo "PLUGIN_HOSTD=/usr/bin/plugin-hostd" >> "$GITHUB_ENV" + echo "PLUGIN_HOSTD_FAKE_HOST=/usr/libexec/plugin-hostd/tests/fake-host" >> "$GITHUB_ENV" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe35bce..78dd819 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,30 +38,9 @@ jobs: - name: No tracked file points into a private repository run: pnpm run check:private-refs - # the CLAP prober, the LV2 measurer, their interposer and the fixture plugins the tests qualify; - # deps/ also holds omx-clap-core's LV2 feature provider at the packages' floor, built for the measurer to link - # asan: the measurer under AddressSanitizer, which the leak check runs - - name: Build the native tools - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends liblilv-dev lv2-dev - make -C native deps all asan - - # plugin-hostd with pin_file (0.2.0 and after) and its fake worker, from the FreeMixer channel: the pins-file - # test starts the real daemon on a qualified pins file. Bookworm is the suite built against the oldest glibc - # the channel serves, which the runner's Ubuntu runs; a floor only, no ceiling. - - name: Install plugin-hostd and its fake worker - run: | - set -euo pipefail - sudo install -d /etc/apt/keyrings - sudo curl -fsSL --retry 5 --retry-all-errors -o /etc/apt/keyrings/freemixer.asc https://freemixer.github.io/deb/freemixer.asc - echo "deb [signed-by=/etc/apt/keyrings/freemixer.asc] https://freemixer.github.io/deb/debian/bookworm ./" | sudo tee /etc/apt/sources.list.d/freemixer.list - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends plugin-hostd plugin-hostd-tests - # apt takes no version relation on the command line: the floor is checked on what it installed - dpkg --compare-versions "$(dpkg-query -W -f='${Version}' plugin-hostd)" ge 0.2.0 - echo "PLUGIN_HOSTD=/usr/bin/plugin-hostd" >> "$GITHUB_ENV" - echo "PLUGIN_HOSTD_FAKE_HOST=/usr/libexec/plugin-hostd/tests/fake-host" >> "$GITHUB_ENV" + - uses: ./.github/actions/build-native + + - uses: ./.github/actions/install-plugin-hostd - name: Type-check run: pnpm lint diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 72468b4..9dd610e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -52,12 +52,16 @@ jobs: - name: Install pnpm run: npm install -g pnpm@10.33.0 + - name: Install + run: pnpm install --frozen-lockfile + + # the same two steps ci.yml runs: the suite on a tag needs what the suite on a branch needs + - uses: ./.github/actions/build-native + + - uses: ./.github/actions/install-plugin-hostd + - name: Build and test run: | - pnpm install --frozen-lockfile - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends liblilv-dev lv2-dev - make -C native deps all asan pnpm lint pnpm test diff --git a/src/workflows-agree.test.ts b/src/workflows-agree.test.ts new file mode 100644 index 0000000..75a02f5 --- /dev/null +++ b/src/workflows-agree.test.ts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Pau Aliagas +/** + * ci.yml and publish.yml run the same suite, so they must prepare the runner the same way. The preparation lives + * once, as composite actions under `.github/actions/`; a workflow that copies the steps inline instead of using the + * action, or leaves them out, drifts from the other (publish.yml once lacked plugin-hostd and failed a tag at + * src/pins-file.test.ts, #84). This test reads both workflows as text and fails on that drift, and on an action + * that no longer sets the variables the suite asks for. + */ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const read = (path: string): string => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); + +const SHARED = ['build-native', 'install-plugin-hostd'] as const; +const WORKFLOWS = ['ci.yml', 'publish.yml'] as const; + +/** The `uses:` lines of a workflow, in order, and the index of the step that runs the suite. */ +function usesOf(workflow: string): string[] { + return [...read(`.github/workflows/${workflow}`).matchAll(/^\s*-?\s*uses:\s*(\S+)/gm)].map((m) => m[1]!); +} + +function indexOfLine(text: string, needle: RegExp): number { + return text.split('\n').findIndex((line) => needle.test(line)); +} + +describe('ci.yml and publish.yml prepare the runner alike', () => { + for (const workflow of WORKFLOWS) { + const text = read(`.github/workflows/${workflow}`); + for (const action of SHARED) { + it(`${workflow} uses the shared ${action} action, once, before it lints`, () => { + const uses = usesOf(workflow).filter((u) => u === `./.github/actions/${action}`); + expect(uses).toHaveLength(1); + const at = indexOfLine(text, new RegExp(`uses: \\./\\.github/actions/${action}$`)); + const lint = indexOfLine(text, /^\s*(run:\s*)?pnpm lint\s*$/); + expect(at).toBeGreaterThan(-1); + expect(lint).toBeGreaterThan(at); + }); + } + + it(`${workflow} carries no inline copy of the shared steps`, () => { + expect(text).not.toMatch(/apt-get install[^\n]*plugin-hostd/); + expect(text).not.toMatch(/make -C native deps/); + expect(text).not.toMatch(/PLUGIN_HOSTD/); + }); + } + + it('the install action sets what src/pins-file.test.ts asks for and checks the floor', () => { + const action = read('.github/actions/install-plugin-hostd/action.yml'); + expect(action).toMatch(/>> "\$GITHUB_ENV"/); + for (const variable of ['PLUGIN_HOSTD', 'PLUGIN_HOSTD_FAKE_HOST']) { + expect(action).toContain(`echo "${variable}=`); + } + expect(action).toMatch(/dpkg --compare-versions .* ge 0\.2\.0/); + }); + + it('publish.yml publishes by OIDC: no token, no secret', () => { + const text = read('.github/workflows/publish.yml'); + expect(text).toMatch(/id-token:\s*write/); + expect(text).not.toMatch(/NPM_TOKEN|NODE_AUTH_TOKEN|secrets\./); + }); +});