From 3e0fcaf4efecd08280c41dd1ac05a3157dfcda54 Mon Sep 17 00:00:00 2001 From: Blake Bertuccelli-Booth <46652+bbertucc@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:42:24 -0400 Subject: [PATCH 1/4] ci: required test and security-scan checks; deploy only after they pass ci.yml runs typecheck, unit, e2e and Trivy (image and lockfile; high or critical with a fix blocks) on every PR and push to main, and nightly on main, opening one issue on failure. notify-uic-deploy.yml now waits for ci to pass on the pushed commit. Adds dependabot.yml and SECURITY.md. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 14 ++++ .github/workflows/ci.yml | 90 +++++++++++++++++++++++++ .github/workflows/notify-uic-deploy.yml | 20 +++++- SECURITY.md | 3 + docs/ci.md | 23 +++++-- 5 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 SECURITY.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ea0a4225 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: docker + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b4be19fc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: ci + +# The checks a merge waits on. `main`'s ruleset requires `test` and `scan`, and +# notify-uic-deploy.yml deploys a commit only after this workflow passed on it. +# The nightly run re-scans `main` for advisories published since it merged. +on: + pull_request: + push: + branches: [main] + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + if: github.event_name != 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm test + - run: ./test/e2e.sh + + # High or critical, with a fix available. An advisory nobody can fix yet is + # reported in the log and does not block. + scan: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - name: Build the image + run: docker build -t iris-ci:${{ github.sha }} . + # The base image's own npm is skipped: Iris never runs it, and on 2026-09-30 it + # held 7 fixed-upstream advisories that even npm 12.2.0 still bundled 3 of. + # Dependabot's docker updates bring in the base image's next npm. + - name: Scan the image (OS packages and runtime node_modules) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: iris-ci:${{ github.sha }} + skip-dirs: /usr/local/lib/node_modules/npm + scanners: vuln + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: "1" + - name: Scan the lockfile (dev dependencies included) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: package-lock.json + scanners: vuln + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: "1" + skip-setup-trivy: true + + # A nightly failure opens one issue, or comments on the open one. + report: + needs: scan + if: failure() && github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + issues: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + steps: + - run: | + set -euo pipefail + TITLE="Nightly security scan failed on main" + NUM=$(gh issue list --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') + if [ -n "$NUM" ]; then + gh issue comment "$NUM" --body "Still failing: $RUN_URL" + else + gh issue create --title "$TITLE" --body "A high or critical advisory with a fix now affects \`main\`. Details: $RUN_URL" + fi diff --git a/.github/workflows/notify-uic-deploy.yml b/.github/workflows/notify-uic-deploy.yml index f3edc0ee..ff557152 100644 --- a/.github/workflows/notify-uic-deploy.yml +++ b/.github/workflows/notify-uic-deploy.yml @@ -9,8 +9,13 @@ name: Notify UIC deploy # a revoked one — or an unreachable API — warns; neither fails the job, so anyone # forking this repo gets a harmless no-op. Other deployments should ignore this # file, or copy it with their own target. +# +# It waits for ci.yml to pass on the pushed commit, so a failing test or a blocking +# advisory stops the deploy as well as the merge. on: - push: + workflow_run: + workflows: [ci] + types: [completed] branches: [main] workflow_dispatch: @@ -40,10 +45,19 @@ jobs: # It takes repo write access to do, so it is a maintainer capability rather than # a hole — but "only what has actually landed on main gets deployed" should be # enforced here, not left to the far end to reject. - if: ${{ github.repository == 'EqualifyEverything/equalify-iris' && github.ref == 'refs/heads/main' }} + # + # On `workflow_run`, `github.sha` is main's tip, not the commit ci checked, so + # the SHA comes from the run itself. + if: >- + github.repository == 'EqualifyEverything/equalify-iris' && ( + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.conclusion == 'success')) env: TOKEN: ${{ secrets.UIC_DEPLOY_DISPATCH_TOKEN }} - SHA: ${{ github.sha }} + SHA: ${{ github.event.workflow_run.head_sha || github.sha }} run: | set -euo pipefail if [ -z "${TOKEN:-}" ]; then diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ce97818a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security + +Report a vulnerability privately through [GitHub's advisory form](https://github.com/EqualifyEverything/equalify-iris/security/advisories/new), not in a public issue. diff --git a/docs/ci.md b/docs/ci.md index 4c867c78..f6e1f39e 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,14 +1,27 @@ # The repo's own automation -Five GitHub Actions workflows run this repository: they review pull requests, close duplicate -issues, triage and rank open ones, tell the deployment when `main` moves, and file a weekly -quality report. Each section below says what one does, what it costs, and what it deliberately +Six GitHub Actions workflows run this repository: they gate merges on tests and security scans, +review pull requests, close duplicate issues, triage and rank open ones, tell the deployment when +`main` moves, and file a weekly quality report. Each section below says what one does, what it costs, and what it deliberately does not do. This is for maintainers and for anyone whose PR just got reviewed by a bot. Nothing here is needed to run Iris — see the [README](../README.md) for that, and [CONTRIBUTING.md](../CONTRIBUTING.md) for how to open a PR in the first place. +## Required checks and security scans + +`ci.yml` runs on every PR and push to `main`, and `main`'s ruleset requires it to pass: + +- **`test`**: `npm ci`, typecheck, `npm test` and `./test/e2e.sh`. +- **`scan`**: Trivy over the built image (minus the base image's own npm, which Iris never runs) + and `package-lock.json`. It fails on a high or critical + advisory that has a fix; one with no fix is logged and does not block. + +It also runs nightly on `main`, and a failed scan opens or updates one issue. Outside the +workflows, GitHub's CodeQL (the ruleset blocks on high-severity alerts), Dependabot and secret +scanning with push protection are on. Report vulnerabilities as [SECURITY.md](../SECURITY.md) says. + ## Automated code review Every PR is reviewed by Claude in CI before a human reads it @@ -347,8 +360,8 @@ two diffs, not something to decide by timestamp. ## Telling a deployment that main moved -`notify-uic-deploy.yml` posts a `repository_dispatch` on every push to `main`, so the UIC test -deployment at `iris.equalify.uic.edu` can ship the exact SHA that just landed. That is all it +`notify-uic-deploy.yml` posts a `repository_dispatch` once `ci.yml` passes on a push to `main`, +so the UIC test deployment at `iris.equalify.uic.edu` can ship the exact SHA that just landed. That is all it does: it holds no infrastructure knowledge, and whether or how the commit is rolled out is the private deployment repo's business. From 3192ec117ae56eb5477d68a87d2f01e8622563b6 Mon Sep 17 00:00:00 2001 From: Blake Bertuccelli-Booth <46652+bbertucc@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:57:31 -0400 Subject: [PATCH 2/4] ci: gate actionlint and shellcheck; install poppler; review round 1 Round 1 notes: actionlint and shellcheck join `test`; the fork-guard clause is commented; the concurrency comment points at ci.yml's group; docs say only `scan` runs nightly; a failed issue search still files. Also installs poppler, without which the PDF tests skip and report as passes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 17 ++++++++++++++++- .github/workflows/notify-uic-deploy.yml | 11 ++++++----- docs/ci.md | 4 ++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4be19fc..b4904bd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@v7 + # No error tolerance here, unlike code-review.yml: without poppler the PDF tests + # skip themselves and report as passes. + - run: sudo apt-get update && sudo apt-get install -y poppler-utils - uses: actions/setup-node@v7 with: node-version-file: .nvmrc @@ -33,6 +36,17 @@ jobs: - run: npm run typecheck - run: npm test - run: ./test/e2e.sh + # Same pinned version and checksum as code-review.yml. + - name: actionlint + run: | + set -euo pipefail + V=1.7.12 + curl -fsSL -o /tmp/actionlint.tgz \ + "https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz" + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 /tmp/actionlint.tgz" | sha256sum -c - + tar -xzf /tmp/actionlint.tgz -C /tmp actionlint + /tmp/actionlint -shellcheck= -pyflakes= .github/workflows/*.yml + - run: shellcheck -s bash -S warning .github/scripts/*.sh # High or critical, with a fix available. An advisory nobody can fix yet is # reported in the log and does not block. @@ -82,7 +96,8 @@ jobs: - run: | set -euo pipefail TITLE="Nightly security scan failed on main" - NUM=$(gh issue list --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty') + # A failed search files a new issue rather than none. + NUM=$(gh issue list --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number // empty' || true) if [ -n "$NUM" ]; then gh issue comment "$NUM" --body "Still failing: $RUN_URL" else diff --git a/.github/workflows/notify-uic-deploy.yml b/.github/workflows/notify-uic-deploy.yml index ff557152..cf427b17 100644 --- a/.github/workflows/notify-uic-deploy.yml +++ b/.github/workflows/notify-uic-deploy.yml @@ -23,10 +23,9 @@ on: # capability comes from the PAT below, which is scoped to the deployment repo. permissions: {} -# Newest wins, explicitly. Two merges seconds apart would otherwise race with no -# ordering guarantee, and if the older SHA's dispatch arrives last the far end -# deploys the older commit until something else is pushed — the exact drift this -# workflow exists to remove. +# Newest wins. Push order is kept by ci.yml's concurrency group, which runs `main`'s +# pushes one at a time, so they finish, and dispatch, in push order. One exception: +# re-running an older `ci` run on `main` redeploys that older SHA. concurrency: group: notify-uic-deploy cancel-in-progress: true @@ -47,7 +46,9 @@ jobs: # enforced here, not left to the far end to reject. # # On `workflow_run`, `github.sha` is main's tip, not the commit ci checked, so - # the SHA comes from the run itself. + # the SHA comes from the run itself. `event == 'push'` is the clause that keeps a + # fork out: a fork PR's branch can be named `main`, and `branches:` above only + # filters on that name. Never drop it. if: >- github.repository == 'EqualifyEverything/equalify-iris' && ( (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || diff --git a/docs/ci.md b/docs/ci.md index f6e1f39e..4a181937 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -13,12 +13,12 @@ needed to run Iris — see the [README](../README.md) for that, and `ci.yml` runs on every PR and push to `main`, and `main`'s ruleset requires it to pass: -- **`test`**: `npm ci`, typecheck, `npm test` and `./test/e2e.sh`. +- **`test`**: `npm ci`, typecheck, `npm test`, `./test/e2e.sh`, `actionlint` and `shellcheck`. - **`scan`**: Trivy over the built image (minus the base image's own npm, which Iris never runs) and `package-lock.json`. It fails on a high or critical advisory that has a fix; one with no fix is logged and does not block. -It also runs nightly on `main`, and a failed scan opens or updates one issue. Outside the +`scan` also runs nightly on `main`, and a failure opens or updates one issue. Outside the workflows, GitHub's CodeQL (the ruleset blocks on high-severity alerts), Dependabot and secret scanning with push protection are on. Report vulnerabilities as [SECURITY.md](../SECURITY.md) says. From 88681fb3de0c220fdfe9663f26d7aa1080a54e1e Mon Sep 17 00:00:00 2001 From: Blake Bertuccelli-Booth <46652+bbertucc@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:10:59 -0400 Subject: [PATCH 3/4] ci: separate the nightly's concurrency group; say actionlint's download is a hard gate Review round 2. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4904bd9..3739e89e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,8 @@ permissions: contents: read concurrency: - group: ci-${{ github.ref }} + # By event too, so the nightly and a push to `main` cannot cancel each other's queued run. + group: ci-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: @@ -36,12 +37,13 @@ jobs: - run: npm run typecheck - run: npm test - run: ./test/e2e.sh - # Same pinned version and checksum as code-review.yml. + # Same pinned version and checksum as code-review.yml; bump both together. + # Unlike there, a failed download fails the check: a gate that skips is no gate. - name: actionlint run: | set -euo pipefail V=1.7.12 - curl -fsSL -o /tmp/actionlint.tgz \ + curl -fsSL --retry 3 -o /tmp/actionlint.tgz \ "https://github.com/rhysd/actionlint/releases/download/v${V}/actionlint_${V}_linux_amd64.tar.gz" echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 /tmp/actionlint.tgz" | sha256sum -c - tar -xzf /tmp/actionlint.tgz -C /tmp actionlint From 6c232a40feeb193b5a219c9ca415e9d2eb5bdf06 Mon Sep 17 00:00:00 2001 From: Blake Bertuccelli-Booth <46652+bbertucc@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:23:59 -0400 Subject: [PATCH 4/4] ci: the npm skip is permanent until someone removes it Review round 3. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3739e89e..7e7d5ce2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,8 @@ jobs: run: docker build -t iris-ci:${{ github.sha }} . # The base image's own npm is skipped: Iris never runs it, and on 2026-09-30 it # held 7 fixed-upstream advisories that even npm 12.2.0 still bundled 3 of. - # Dependabot's docker updates bring in the base image's next npm. + # Nothing retires this skip on its own (`24-slim` floats, so Dependabot never + # proposes a rebuild); to check, drop `skip-dirs` and see whether `scan` passes. - name: Scan the image (OS packages and runtime node_modules) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: