Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
108 changes: 108 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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:
# 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:
test:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
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
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm test
- run: ./test/e2e.sh
# 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 --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
/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.
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.
# 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:
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"
# 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
gh issue create --title "$TITLE" --body "A high or critical advisory with a fix now affects \`main\`. Details: $RUN_URL"
fi
29 changes: 22 additions & 7 deletions .github/workflows/notify-uic-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ 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:

# Least privilege: this job reads nothing and writes nothing in THIS repo. Its only
# 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
Expand All @@ -40,10 +44,21 @@ 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. `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') ||
(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
Expand Down
3 changes: 3 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 18 additions & 5 deletions docs/ci.md
Original file line number Diff line number Diff line change
@@ -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`, `./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.

`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.

## Automated code review

Every PR is reviewed by Claude in CI before a human reads it
Expand Down Expand Up @@ -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.

Expand Down
Loading