diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b439890..4725055 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [macos-latest, ubuntu-20.04, windows-latest] + platform: [macos-latest, ubuntu-22.04, windows-latest] runs-on: ${{ matrix.platform }} steps: @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3 - name: Install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-20.04' + if: matrix.platform == 'ubuntu-22.04' # You can remove libayatana-appindicator3-dev if you don't use the system tray feature. run: | sudo apt-get update @@ -47,13 +47,27 @@ jobs: - name: Release Drafter uses: release-drafter/release-drafter@v5.21.1 + - name: Validate public search build inputs + env: + TAURI_MEILI_HOST: ${{ secrets.TAURI_MEILI_HOST }} + TAURI_MEILI_SEARCH_KEY: ${{ secrets.TAURI_MEILI_SEARCH_KEY }} + run: node scripts/validate-public-search-config.mjs + - name: Build the app uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Both of these are INLINED INTO THE SHIPPED BUNDLE by Vite -- + # vite.config.ts sets envPrefix: ["VITE_", "TAURI_"]. They are build + # inputs, not secrets: whoever holds a release artifact can read them + # out of it with `strings`. + # + # TAURI_MEILI_SEARCH_KEY must therefore be a Meilisearch SEARCH key, + # scoped read-only to the indexes this app searches. Never the master + # key, which can create keys, write documents, and read every index. TAURI_MEILI_HOST: ${{ secrets.TAURI_MEILI_HOST }} - TAURI_MEILI_API_KEY: ${{ secrets.TAURI_MEILI_API_KEY }} + TAURI_MEILI_SEARCH_KEY: ${{ secrets.TAURI_MEILI_SEARCH_KEY }} with: tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags. diff --git a/.github/workflows/verify_frontend.yml b/.github/workflows/verify_frontend.yml index c1773bd..9985f60 100644 --- a/.github/workflows/verify_frontend.yml +++ b/.github/workflows/verify_frontend.yml @@ -3,6 +3,9 @@ on: paths: - 'src/**' - 'tests/**' + - 'scripts/**' + - 'package.json' + - '.github/workflows/release.yml' - '.github/workflows/verify_frontend.yml' branches: - main @@ -13,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-22.04, windows-latest, macOS-latest] steps: - uses: actions/checkout@v3 @@ -34,11 +37,12 @@ jobs: libgtk-3-dev \ libayatana-appindicator3-dev \ librsvg2-dev - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-22.04' - name: npm install, build and test run: | npm install + npm run test:release-config npm run build npm run tauri build - npm run test run -- -t e \ No newline at end of file + npm run test -- run diff --git a/package.json b/package.json index d8b1f5a..6c5ce1d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "format": "prettier src --ext .ts,.tsx,.js,.jsx -w ", "tauri": "tauri", "start": "tauri dev", - "test": "vitest" + "test": "vitest", + "test:release-config": "node scripts/validate-public-search-config.check.mjs" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", diff --git a/scripts/validate-public-search-config.check.mjs b/scripts/validate-public-search-config.check.mjs new file mode 100644 index 0000000..7cbd023 --- /dev/null +++ b/scripts/validate-public-search-config.check.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const validator = fileURLToPath( + new URL("./validate-public-search-config.mjs", import.meta.url), +); + +function validate(overrides) { + const result = spawnSync(process.execPath, [validator], { + encoding: "utf8", + env: { + ...process.env, + TAURI_MEILI_HOST: "https://search.example.test", + TAURI_MEILI_SEARCH_KEY: "public-search-key", + ...overrides, + }, + }); + + if (result.error) { + assert.fail(`configuration validator could not run: ${result.error.message}`); + } + + return result; +} + +const missingHost = validate({ TAURI_MEILI_HOST: "" }); +assert.notEqual(missingHost.status, 0, "an empty search host must stop the release"); +assert.match(missingHost.stderr, /TAURI_MEILI_HOST/); + +const missingKey = validate({ TAURI_MEILI_SEARCH_KEY: "" }); +assert.notEqual(missingKey.status, 0, "an empty search key must stop the release"); +assert.match(missingKey.stderr, /TAURI_MEILI_SEARCH_KEY/); + +const configured = validate({}); +assert.equal(configured.status, 0, configured.stderr); diff --git a/scripts/validate-public-search-config.mjs b/scripts/validate-public-search-config.mjs new file mode 100644 index 0000000..22c51bc --- /dev/null +++ b/scripts/validate-public-search-config.mjs @@ -0,0 +1,11 @@ +const requiredInputs = ["TAURI_MEILI_HOST", "TAURI_MEILI_SEARCH_KEY"]; +const missingInputs = requiredInputs.filter( + (name) => !process.env[name] || !process.env[name].trim(), +); + +if (missingInputs.length > 0) { + console.error( + `::error::Missing required public search build input(s): ${missingInputs.join(", ")}`, + ); + process.exitCode = 1; +} diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx index 02fbe4e..c589df0 100644 --- a/src/components/search/Search.tsx +++ b/src/components/search/Search.tsx @@ -16,13 +16,28 @@ import { Button } from "../buttons"; import SearchBox from "./SearchBox"; import styles from "./Search.module.scss"; -const { TAURI_MEILI_HOST: host = "", TAURI_MEILI_API_KEY: apiKey = "" } = +// vite.config.ts sets envPrefix: ["VITE_", "TAURI_"], so anything read here +// through import.meta.env is INLINED INTO THE BUNDLE at build time. It is not a +// secret once the app is distributed: `strings` over the binary returns it, and +// so does opening the JavaScript. +// +// There is no way to hide a credential in a client that queries a search server +// directly, so this must be a value the client is ALLOWED to hold — a +// Meilisearch search key, scoped read-only to the indexes this app searches. +// The name says so on purpose. The master key must never be put here: it can +// create keys, write documents and read every index on the instance. +// +// If search ever needs a credential the client may not hold, the request has to +// go through something that holds it instead — a proxy, or a Tauri command +// talking to a server — because the Rust binary is no better a hiding place +// than the JavaScript is. +const { TAURI_MEILI_HOST: host = "", TAURI_MEILI_SEARCH_KEY: searchKey = "" } = import.meta.env; let searchClient: InstantMeiliSearchInstance; -if (host && apiKey) { +if (host && searchKey) { try { - searchClient = instantMeiliSearch(host, apiKey, { + searchClient = instantMeiliSearch(host, searchKey, { primaryKey: "_id", }); } catch (error) { diff --git a/tests/components/modules/DiceModule.test.tsx b/tests/components/modules/DiceModule.test.tsx deleted file mode 100644 index 91046fd..0000000 --- a/tests/components/modules/DiceModule.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { prettyDOM, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, test } from "vitest"; -import React from "react"; -import { expect } from "vitest"; -import DiceModule from '../../../src/components/modules/DiceModule'; - -describe("DiceModule tests", () => { - // make sure the component renders - test("render test", () => { - render( - - ); - - // this means it's rendering properly - expect(screen.queryAllByText("Dice module not available")).toHaveLength(0); - }); -}); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index b3db28f..549b49c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -29,6 +29,5 @@ export default defineConfig({ globals: true, // for access to the DOM environment: "jsdom", - testNamePattern: "./tests/*.test.tsx", }, });