diff --git a/.github/actions/build-and-run-wodin/action.yml b/.github/actions/build-and-run-wodin/action.yml new file mode 100644 index 000000000..991df3434 --- /dev/null +++ b/.github/actions/build-and-run-wodin/action.yml @@ -0,0 +1,15 @@ +name: Build and run WODIN (and deps) + +runs: + using: "composite" + steps: + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Build and run server + shell: bash + run: | + ./scripts/build.sh + ./scripts/run-dependencies.sh + npm run serve --prefix=app/server & diff --git a/.github/actions/ci-env-and-ghcr-login/action.yml b/.github/actions/ci-env-and-ghcr-login/action.yml new file mode 100644 index 000000000..9e2f8a665 --- /dev/null +++ b/.github/actions/ci-env-and-ghcr-login/action.yml @@ -0,0 +1,39 @@ +name: Setup CI Env and login to GHCR + +inputs: + ghcr-username: + required: true + ghcr-password: + required: true + +outputs: + CI_SHA: + description: "Short SHA of current commit" + value: ${{ steps.ci-env.outputs.CI_SHA }} + CI_BRANCH: + description: "Current branch" + value: ${{ steps.ci-env.outputs.CI_BRANCH }} + +runs: + using: "composite" + steps: + - id: ci-env + name: Setup Environment + shell: bash + run: | + if [ "${{github.event_name}}" = "pull_request" ]; + then + long_sha=${{ github.event.pull_request.head.sha }} + echo "CI_BRANCH=${{ github.head_ref }}" >> $GITHUB_OUTPUT + else + long_sha=${GITHUB_SHA} + echo "CI_BRANCH=${{ github.ref_name }}" >> $GITHUB_OUTPUT + fi + echo "CI_SHA=${long_sha:0:7}" >> $GITHUB_OUTPUT + + - name: Login to GHCR (GitHub Packages) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ inputs.ghcr-username }} + password: ${{ inputs.ghcr-password }} diff --git a/.github/actions/install-npm-packages/action.yml b/.github/actions/install-npm-packages/action.yml new file mode 100644 index 000000000..2eb81cba6 --- /dev/null +++ b/.github/actions/install-npm-packages/action.yml @@ -0,0 +1,28 @@ +name: Install NPM packages + +inputs: + server: + required: false + default: 'false' + static: + required: false + default: 'false' + +runs: + using: "composite" + steps: + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install server (backend) NPM packages + if: inputs.server == 'true' + shell: bash + run: | + npm ci --prefix=app/server + + - name: Install static (frontend) NPM packages + if: inputs.static == 'true' + shell: bash + run: | + npm ci --prefix=app/static diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml new file mode 100644 index 000000000..73caba959 --- /dev/null +++ b/.github/workflows/build-test-publish.yml @@ -0,0 +1,135 @@ +name: CI + +on: [push] + +env: + TAG_GHCR: mrc-ide/wodin + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: ci-env + uses: ./.github/actions/ci-env-and-ghcr-login + with: + ghcr-username: ${{ github.actor }} + ghcr-password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push docker + uses: docker/build-push-action@v5 + with: + file: ./docker/Dockerfile + push: true + tags: | + ghcr.io/${{ env.TAG_GHCR }}:${{ steps.ci-env.outputs.CI_SHA }} + ghcr.io/${{ env.TAG_GHCR }}:${{ steps.ci-env.outputs.CI_BRANCH }} + - name: Smoke test + run: | + ./scripts/run-version.sh --app ${{ steps.ci-env.outputs.CI_BRANCH }} & + ./scripts/smoke-test.sh + + fe-unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-npm-packages + with: + static: true + + - name: Test front end + run: npm run coverage --prefix=app/static + - uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./app/static/coverage/coverage-final.json + token: ${{ secrets.CODECOV_TOKEN }} + codecov_yml_path: ./codecov.yml + + be-unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-and-run-wodin + + - name: Test back end + run: npm test --prefix=app/server + - name: Check versions + run: npm run genversion --prefix=app/server -- --check-only + - uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true # optional (default = false) + files: ./app/server/coverage/coverage-final.json + token: ${{ secrets.CODECOV_TOKEN }} # required + + integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-and-run-wodin + + - name: Test back end integration + run: npm run integration-test --prefix=app/server + + playwright-tests: + runs-on: ubuntu-latest + strategy: + matrix: + shard: [1, 2, 3, 4, 5, 6] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-and-run-wodin + + - name: Get installed Playwright version + run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./app/static/package-lock.json').packages['node_modules/@playwright/test'].version)")" >> $GITHUB_ENV + - name: Cache binaries for playwright version + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright@${{ env.PLAYWRIGHT_VERSION }} install --with-deps + - name: Test e2e + run: npm run test:e2e --prefix=app/static -- --shard=${{ matrix.shard }}/6 + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-npm-packages + with: + static: true + server: true + + - name: Lint back end + run: npm run lint --prefix=app/server + - name: Lint front end + run: npm run lint --prefix=app/static + + publish-latest-image: + runs-on: ubuntu-latest + # change this ref to publish to "latest" tag from another branch + if: github.ref == 'refs/heads/main' + needs: [build-and-push, fe-unit-tests, be-unit-tests, integration-tests, playwright-tests, lint] + steps: + - uses: actions/checkout@v4 + - id: ci-env + uses: ./.github/actions/ci-env-and-ghcr-login + with: + ghcr-username: ${{ github.actor }} + ghcr-password: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish image manifest to latest + run: | + GHCR_TOKEN=$(echo ${{ secrets.GITHUB_TOKEN }} | base64) + curl "https://ghcr.io/v2/mrc-ide/wodin/manifests/${{ steps.ci-env.outputs.CI_BRANCH }}" \ + -H "accept: application/vnd.docker.distribution.manifest.v2+json" \ + -H "Authorization: Bearer ${GHCR_TOKEN}" \ + > manifest.json + curl -XPUT "https://ghcr.io/v2/mrc-ide/wodin/manifests/latest" \ + -H "content-type: application/vnd.docker.distribution.manifest.v2+json" \ + -H "Authorization: Bearer ${GHCR_TOKEN}" \ + -d '@manifest.json' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 419240a5c..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: test -on: [push] -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 20.x - - name: Install dependencies - run: npm install -g typescript - - name: Build - run: ./scripts/build.sh - - name: Run dependencies - run: ./scripts/run-dependencies.sh - - name: Run server - run: npm run serve --prefix=app/server & - - name: Test back end - run: npm test --prefix=app/server - - name: Test back end integration - run: npm run integration-test --prefix=app/server - - name: Test front end - run: npm run coverage --prefix=app/static - - name: Lint back end - run: npm run lint --prefix=app/server - - name: Lint front end - run: npm run lint --prefix=app/static - - name: Check versions - run: npm run genversion --prefix=app/server -- --check-only - - uses: codecov/codecov-action@v4 - with: - fail_ci_if_error: true # optional (default = false) - files: ./app/server/coverage/coverage-final.json,./app/static/coverage/coverage-final.json - token: ${{ secrets.CODECOV_TOKEN }} # required diff --git a/app/static/playwright.config.ts b/app/static/playwright.config.ts index d72979853..51add3618 100644 --- a/app/static/playwright.config.ts +++ b/app/static/playwright.config.ts @@ -26,8 +26,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 1, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + fullyParallel: true, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ // reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -54,16 +53,4 @@ export default defineConfig({ /* Folder for test artifacts such as screenshots, videos, traces, etc. */ // outputDir: 'test-results/', - - /* Run your local dev server before starting the tests */ - webServer: { - /** - * Use the dev server by default for faster feedback loop. - * Use the preview server on CI for more realistic testing. - * Playwright will re-use the local server if there is already a dev-server running. - */ - command: process.env.CI ? 'npm run build' : 'npm run dev', - port: 5173, - reuseExistingServer: !process.env.CI - } }) diff --git a/app/static/tests/e2e/index.etest.ts b/app/static/tests/e2e/index.etest.ts index 034f495a2..6da8f7249 100644 --- a/app/static/tests/e2e/index.etest.ts +++ b/app/static/tests/e2e/index.etest.ts @@ -3,17 +3,18 @@ import * as fs from "fs"; import { realisticFitData } from "./utils"; test.describe("Index tests", () => { - const tmpPath = "tmp"; + let tmpPath: string; - test.beforeAll(() => { + test.beforeEach(() => { + tmpPath = `${Math.random()}`; if (fs.existsSync(tmpPath)) { - fs.rmdirSync(tmpPath, { recursive: true }); + fs.rmSync(tmpPath, { recursive: true }); } fs.mkdirSync(tmpPath); }); - test.afterAll(() => { - fs.rmdirSync(tmpPath, { recursive: true }); + test.afterEach(() => { + fs.rmSync(tmpPath, { recursive: true }); }); test("renders heading", async ({ page }) => { diff --git a/app/static/tests/e2e/stochastic.etest.ts b/app/static/tests/e2e/stochastic.etest.ts index 3e6013f66..6cde88ed5 100644 --- a/app/static/tests/e2e/stochastic.etest.ts +++ b/app/static/tests/e2e/stochastic.etest.ts @@ -1,16 +1,21 @@ -import { expect, test } from "@playwright/test"; -import PlaywrightConfig from "../../playwright.config"; +import { expect, Page, test } from "@playwright/test"; import { expectSummaryValues } from "./utils"; test.describe("stochastic app", () => { - const { timeout } = PlaywrightConfig; - test.beforeEach(async ({ page }) => { await page.goto("/apps/day3"); await page.click(":nth-match(.wodin-left .nav-tabs a, 2)"); // Options await page.click(":nth-match(.wodin-right .nav-tabs a, 2)"); // Run }); + const expectChangedNumberOfReplicatesMessage = async (page: Page, newReplicates: string) => { + await expect(await page.locator(".action-required-msg")).toHaveText(""); + await page.fill(":nth-match(#run-options input, 2)", newReplicates); + await expect(await page.locator(".action-required-msg")).toHaveText( + "Plot is out of date: number of replicates has changed. Run model to update." + ); + }; + test("can display number of replicates", async ({ page }) => { await expect(await page.innerText(":nth-match(.collapse-title, 2)")).toContain("Run Options"); await expect(await page.getAttribute(":nth-match(.collapse-title i, 2)", "data-name")).toBe("chevron-up"); @@ -22,18 +27,11 @@ test.describe("stochastic app", () => { }); test("can change number of replicates and re-run model", async ({ page }) => { - await page.fill(":nth-match(#run-options input, 2)", "6"); - - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText( - "Plot is out of date: number of replicates has changed. Run model to update.", - { - timeout - } - ); + await expectChangedNumberOfReplicatesMessage(page, "6"); // Re-run model await page.click("#run-btn"); - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText(""); + await expect(await page.locator(".action-required-msg")).toHaveText(""); // number of series should have increased by 2 const summary = ".wodin-plot-data-summary-series"; @@ -48,30 +46,18 @@ test.describe("stochastic app", () => { }); test("traces are hidden if replicates are above maxReplicatesDisplay", async ({ page }) => { - await page.fill(":nth-match(#run-options input, 2)", "50"); - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText( - "Plot is out of date: number of replicates has changed. Run model to update.", - { - timeout - } - ); + await expectChangedNumberOfReplicatesMessage(page, "50"); await page.click("#run-btn"); - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText(""); + await expect(await page.locator(".action-required-msg")).toHaveText(""); const summary = ".wodin-plot-data-summary-series"; expect(await page.locator(summary).count()).toBe(104); - await page.fill(":nth-match(#run-options input, 2)", "51"); - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText( - "Plot is out of date: number of replicates has changed. Run model to update.", - { - timeout - } - ); + await expectChangedNumberOfReplicatesMessage(page, "51"); await page.click("#run-btn"); - await expect(await page.locator(".run-tab .action-required-msg")).toHaveText(""); + await expect(await page.locator(".action-required-msg")).toHaveText(""); expect(await page.locator(summary).count()).toBe(4); @@ -93,7 +79,7 @@ test.describe("stochastic app", () => { // Can see summary traces const summary = ".wodin-plot-data-summary-series"; - await expect(await page.locator(summary)).toHaveCount(44, { timeout }); + await expect(await page.locator(summary)).toHaveCount(44); await expectSummaryValues(page, 1, "I_det (beta=0.450)", 1001, "#2e5cb8"); await expectSummaryValues(page, 2, "I (beta=0.450)", 1001, "#6ab74d"); diff --git a/buildkite/pipeline.yml b/buildkite/pipeline.yml deleted file mode 100644 index 9826f85b8..000000000 --- a/buildkite/pipeline.yml +++ /dev/null @@ -1,13 +0,0 @@ -steps: - - label: ":whale::node: Build" - command: docker/build - - - wait - - - label: ":playwright::chrome: Browser tests" - command: docker/browser_tests - - - wait - - - label: ":shipit: Push images" - command: docker/push diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..a477e82ab --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +coverage: + precision: 2 + round: down + range: "95...100" + + status: + project: + default: + threshold: 2% diff --git a/docker/Dockerfile b/docker/Dockerfile index 82b034d1f..fc42c103b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,4 @@ -FROM ubuntu:jammy - -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl \ - && curl -sL https://deb.nodesource.com/setup_20.x | bash \ - && apt-get install -y git nodejs +FROM ghcr.io/mrc-ide/wodin-base-image:main COPY . /wodin RUN /wodin/scripts/build.sh && \ diff --git a/docker/browser_tests b/docker/browser_tests deleted file mode 100755 index 68944a6f2..000000000 --- a/docker/browser_tests +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -eux - -function remove_containers() { - docker kill odin.api wodin wodin-redis wodin-playwright 2>/dev/null || /bin/true -} - -function cleanup() { - set +x - remove_containers -} -trap cleanup EXIT - -ROOT=$(dirname $(dirname $0)) - -ROOT=$PWD -. $ROOT/docker/common - -remove_containers - -## all deps -./scripts/run-dependencies.sh - -## main app -docker pull $TAG_SHA -docker run --rm -d --name wodin --network=host $TAG_SHA /wodin/config - -## run the tests -docker run --rm --name wodin-playwright --network=host \ - -w /wodin/app/static \ - --entrypoint /wodin/docker/run_browser_tests \ - $TAG_SHA diff --git a/docker/build b/docker/build deleted file mode 100755 index 18bbdb94e..000000000 --- a/docker/build +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -ex -HERE=$(dirname $0) -. $HERE/common - -docker build --pull \ - --tag $TAG_SHA \ - -f docker/Dockerfile \ - . - -# We always push the SHA tagged versions, for debugging if the tests -# after this step fail -docker push $TAG_SHA diff --git a/docker/common b/docker/common deleted file mode 100644 index c6eb206f1..000000000 --- a/docker/common +++ /dev/null @@ -1,23 +0,0 @@ -# -*-sh-*- -PACKAGE_NAME=wodin -PACKAGE_ORG=mrcide - -# Buildkite doesn't check out a full history from the remote (just the -# single commit) so you end up with a detached head and git rev-parse -# doesn't work -if [ "$BUILDKITE" = "true" ]; then - GIT_SHA=${BUILDKITE_COMMIT:0:7} -else - GIT_SHA=$(git -C "$PACKAGE_ROOT" rev-parse --short=7 HEAD) -fi - - -if [ "$BUILDKITE" = "true" ]; then - GIT_BRANCH=$BUILDKITE_BRANCH -else - GIT_BRANCH=$(git -C "$PACKAGE_ROOT" symbolic-ref --short HEAD) -fi - -TAG_SHA="${PACKAGE_ORG}/${PACKAGE_NAME}:${GIT_SHA}" -TAG_BRANCH="${PACKAGE_ORG}/${PACKAGE_NAME}:${GIT_BRANCH}" -TAG_LATEST="${PACKAGE_ORG}/${PACKAGE_NAME}:latest" diff --git a/docker/push b/docker/push deleted file mode 100755 index 11a17be1d..000000000 --- a/docker/push +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e -HERE=$(dirname $0) -. $HERE/common - -# In case we switch agents between steps -[ ! -z $(docker images -q $TAG_SHA) ] || docker pull $TAG_SHA - -docker tag $TAG_SHA $TAG_BRANCH -docker push $TAG_BRANCH - -if [ $GIT_BRANCH == "master" ]; then - docker tag $TAG_SHA $TAG_LATEST - docker tag $TAG_SHA $TAG_VERSION - docker push $TAG_LATEST - docker push $TAG_VERSION -fi diff --git a/docker/run_browser_tests b/docker/run_browser_tests deleted file mode 100755 index 33a302607..000000000 --- a/docker/run_browser_tests +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -eux - -if [ ! -d tests/e2e ]; then - echo "Expected this script to be run from app/static" - exit 1 -fi - -npm install @playwright/test -npx playwright install -npx playwright install-deps -npm run test:e2e diff --git a/scripts/build-backend.sh b/scripts/build-backend.sh index c99991fb7..80bedef68 100644 --- a/scripts/build-backend.sh +++ b/scripts/build-backend.sh @@ -1,5 +1,5 @@ set -ex ROOT=$(realpath $(dirname $0)/..) -npm install --prefix=$ROOT/app/server +npm ci --prefix=$ROOT/app/server npm run build --prefix=$ROOT/app/server diff --git a/scripts/build.sh b/scripts/build.sh index 26f98f27e..e52e8c312 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,7 +1,7 @@ set -ex ROOT=$(realpath $(dirname $0)/..) -npm install --prefix=$ROOT/app/static +npm ci --prefix=$ROOT/app/static npm run build-with-check --prefix=$ROOT/app/static . $ROOT/scripts/build-backend.sh diff --git a/scripts/run-version.sh b/scripts/run-version.sh index 7290cd534..eb9367f09 100755 --- a/scripts/run-version.sh +++ b/scripts/run-version.sh @@ -41,7 +41,7 @@ while [[ $# -gt 0 ]]; do done API_IMAGE=mrcide/odin.api:$API_BRANCH -APP_IMAGE=mrcide/wodin:$APP_BRANCH +APP_IMAGE=ghcr.io/mrc-ide/wodin:$APP_BRANCH REDIS_IMAGE=redis:6 API_NAME=odin.api diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 000000000..54792d517 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +retries=10 +sleep 20; + +while ((retries > 0)); do + curl http://localhost:3000 | grep "Example WODIN configuration" && break + + echo "retrying connection with server in 1 seconds" + sleep 1 + ((retries --)) +done + +if ((retries == 0 )); then + echo "Didn't get expected response from the server" + exit 1 +fi