diff --git a/.github/workflows/qa-icons.yml b/.github/workflows/qa-icons.yml new file mode 100644 index 00000000..79e554a0 --- /dev/null +++ b/.github/workflows/qa-icons.yml @@ -0,0 +1,51 @@ +name: QA + +on: + # the release workflow runs every gate on a tag and publishes only + # once they all pass, so each one has to be callable. + workflow_call: + push: + branches: + - main + paths: + - web/app/utils/icon.ts + - server/core/internal/assistant/icon/icons.json + - scripts/check-icons.mjs + - .github/workflows/qa-icons.yml + pull_request: + branches: + - main + paths: + - web/app/utils/icon.ts + - server/core/internal/assistant/icon/icons.json + - scripts/check-icons.mjs + - .github/workflows/qa-icons.yml + +# a newer push supersedes the still-running workflow of the stale commit +# and cancels it. The group carries a hardcoded per-file prefix because +# every QA workflow shares the display name "QA", so github.workflow +# would put them all in one group and let a run of one workflow cancel +# an in-flight run of another. +concurrency: + group: qa-icons-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + check: + name: Comparing the web and core icon lists + runs-on: ubuntu-latest + steps: + - name: Check out the code + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Compare the icon lists + run: node scripts/check-icons.mjs diff --git a/.github/workflows/qa-web.yml b/.github/workflows/qa-web.yml index a9854e10..d0897b79 100644 --- a/.github/workflows/qa-web.yml +++ b/.github/workflows/qa-web.yml @@ -9,16 +9,12 @@ on: - main paths: - web/** - # the icon list test compares against core's copy. - - server/core/internal/assistant/icon/icons.json - .github/workflows/qa-web.yml pull_request: branches: - main paths: - web/** - # the icon list test compares against core's copy. - - server/core/internal/assistant/icon/icons.json - .github/workflows/qa-web.yml # a newer push supersedes the still-running workflow of the stale commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a81bebbd..8d80231c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,9 @@ jobs: e2e-prod: uses: ./.github/workflows/qa-e2e-prod.yml + icons: + uses: ./.github/workflows/qa-icons.yml + publish: name: Publishing the image needs: @@ -60,6 +63,7 @@ jobs: - datagen - launcher - e2e-prod + - icons runs-on: ubuntu-latest timeout-minutes: 60 permissions: diff --git a/Makefile b/Makefile index 98cae986..8b329af4 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,7 @@ check-lint: @$(QUIET) "lint-checking datagen" sh -c 'cd datagen && make check-lint' @$(QUIET) "lint-checking e2e" sh -c 'cd e2e && pnpm run check-lint' @$(QUIET) "lint-checking launcher" sh -c 'cd docker/prod/launcher && pnpm run check-lint' + @$(QUIET) "checking icon lists" node scripts/check-icons.mjs # unit tests of every component, with the coverage gates CI applies. .PHONY: test diff --git a/scripts/check-icons.mjs b/scripts/check-icons.mjs new file mode 100644 index 00000000..83d1971c --- /dev/null +++ b/scripts/check-icons.mjs @@ -0,0 +1,39 @@ +// compare the icons the web picker offers with core's copy, which the +// assistant validates icons against. Print what differs and exit 1 when +// they drift apart. +// +// usage: node scripts/check-icons.mjs +import { readFileSync } from "node:fs" +import { selectableIcons } from "../web/app/utils/icon.ts" + +const corePath = "server/core/internal/assistant/icon/icons.json" +const core = JSON.parse( + readFileSync(new URL(`../${corePath}`, import.meta.url), "utf8"), +) + +const entry = (icon) => `${icon.id} (${icon.name})` +const web = selectableIcons.map(entry) +const coreEntries = core.map(entry) + +if (web.join("\n") === coreEntries.join("\n")) { + process.exit(0) +} + +const onlyWeb = web.filter((e) => !coreEntries.includes(e)) +const onlyCore = coreEntries.filter((e) => !web.includes(e)) + +console.error(`web/app/utils/icon.ts and ${corePath} differ.`) + +for (const e of onlyWeb) { + console.error(` only in icon.ts: ${e}`) +} + +for (const e of onlyCore) { + console.error(` only in icons.json: ${e}`) +} + +if (onlyWeb.length === 0 && onlyCore.length === 0) { + console.error(" same entries in a different order") +} + +process.exit(1) diff --git a/server/core/internal/assistant/icon/icon.go b/server/core/internal/assistant/icon/icon.go index b4fd43b2..2e2fc687 100644 --- a/server/core/internal/assistant/icon/icon.go +++ b/server/core/internal/assistant/icon/icon.go @@ -33,8 +33,8 @@ const ( _minPartialWord = 4 ) -// _iconsJSON is the picker's icon list, in the picker's order. The web -// test in web/app/utils/icon.test.ts keeps it equal to the list there. +// _iconsJSON is the picker's icon list, in the picker's order. +// scripts/check-icons.mjs keeps it equal to web/app/utils/icon.ts. // //go:embed icons.json var _iconsJSON []byte diff --git a/web/app/utils/icon.test.ts b/web/app/utils/icon.test.ts index 62a13d55..e798f464 100644 --- a/web/app/utils/icon.test.ts +++ b/web/app/utils/icon.test.ts @@ -9,7 +9,6 @@ import { icons as ph } from "@iconify-json/ph" import { icons as ri } from "@iconify-json/ri" import { icons as simpleIcons } from "@iconify-json/simple-icons" import { describe, it } from "vitest" -import coreIcons from "~/../../server/core/internal/assistant/icon/icons.json" import { selectableIconList, selectableIcons } from "./icon" const collections: Record = { @@ -24,15 +23,6 @@ const collections: Record = { "simple-icons": simpleIcons, } -describe("selectableIcons", () => { - // the assistant refuses icons outside its own copy of this list. - it("matches the list core validates assistant icons against", ({ - expect, - }) => { - expect(selectableIcons).toEqual(coreIcons) - }) -}) - describe("selectableIconList", () => { it("returns the id of every selectable icon in order", ({ expect }) => { const list = selectableIconList()