Skip to content
Back to skills

Run Canary Locally

BSecurity

Get a passing local run of the Playwright canary specs in src/ol_concourse/pipelines/canaries. Use when running, debugging, or reproducing a canary journey outside Concourse — triaging a red canary-<property> build, opening a failed run's trace.zip, iterating on a spec before shipping it, or reproducing a pipeline failure in the exact image Concourse uses. Covers npm ci, the browser install npm ci does NOT do, CANARY_BASE_URL, supplying credentials without a lockout, and the trace viewer.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 22, 2026
devopspythongoshellbashnodedockerawstestingdebugginggit

Security analysis

B80/100
  • highPerforms destructive filesystem operations
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned October 4, 2026

npx -y skills add mitodl/agent-kit --skill run-canary-locally --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Run Canary Locally?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Run Canary Locally
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/mitodl-run-canary-locally/badge)](https://www.skillsdirectory.com/skills/mitodl-run-canary-locally)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: run-canary-locally
description: >
  Get a passing local run of the Playwright canary specs in
  src/ol_concourse/pipelines/canaries. Use when running, debugging, or
  reproducing a canary journey outside Concourse — triaging a red
  canary-<property> build, opening a failed run's trace.zip, iterating on a spec
  before shipping it, or reproducing a pipeline failure in the exact image
  Concourse uses. Covers npm ci, the browser install npm ci does NOT do,
  CANARY_BASE_URL, supplying credentials without a lockout, and the trace viewer.
license: BSD-3-Clause
metadata:
  category: process
---

# Running the Canaries Locally

**Repository:** [`mitodl/ol-infrastructure`](https://github.com/mitodl/ol-infrastructure).
Every unqualified path below is relative to that checkout.

## When to use

- A `canary-<property>` build went red and you want to reproduce it.
- You are writing or changing a spec and need the edit/run loop.
- You want to read a failed run's trace.

For *what* to write and where it goes, use the `add-canary-journey` skill. This
skill is only about getting a run to happen on your machine.

The authoritative design notes live in
[`src/ol_concourse/pipelines/canaries/AGENTS.md`](https://github.com/mitodl/ol-infrastructure/blob/main/src/ol_concourse/pipelines/canaries/AGENTS.md)
and [`README.md`](https://github.com/mitodl/ol-infrastructure/blob/main/src/ol_concourse/pipelines/canaries/README.md). This
skill does not restate them; it gets you to a passing run.

## Prerequisites

- Node and `npm` on PATH. There is no `uv`/Python involvement in a canary run —
  Python only renders the *pipeline*, never the tests.
- Network reach to the target property. Every current target is publicly
  reachable, so no VPN.

All commands below run from the canary directory:

```bash
cd src/ol_concourse/pipelines/canaries
```

## Step 1 — Install, including the part `npm ci` does not do

```bash
npm ci
npx playwright install chromium
```

**Both lines are required.** In Playwright 1.63 the `playwright`,
`playwright-core` and `@playwright/test` packages all have
`hasInstallScript=false` and ship no `install.js` — verified in
`package-lock.json` — so `npm ci` installs six packages and **downloads no
browser at all**. Skipping the second line gets you:

```text
browserType.launch: Executable doesn't exist at
/home/<you>/.cache/ms-playwright/chromium_headless_shell-1243/...
```

which reads like a broken install and is not one.

This is also why the pipeline does not need that second line: browsers are baked
into the `mcr.microsoft.com/playwright` image at a revision keyed to the exact
Playwright version, which is the whole reason `package.json`'s pin is the single
source of truth for the image tag.

Install only `chromium` unless you are specifically testing WebKit — the
pipeline schedules Chromium alone.

## Step 2 — Choose a target

`CANARY_BASE_URL` is **required and never defaults** — `playwright.config.ts`
throws without it, because a canary silently pointed at the wrong environment
reports green while the real one burns.

```bash
export CANARY_BASE_URL=https://rc.learn.mit.edu   # what canary-mit-learn targets
```

Point it at a local stack the same way (`http://localhost:8063` or whatever the
app serves). Specs must not branch on the base URL — assert what is true of
every environment.

## Step 3 — Run the anonymous journeys first

Most `mit-learn` journeys need no login, so start here and stay here if you can:

```bash
npx playwright test specs/mit-learn --project=chromium --reporter=list
```

To run a subset — note you **cannot use `.only`**, because `forbidOnly` is on
unconditionally and will fail the whole run on purpose. Filter instead:

```bash
npx playwright test specs/mit-learn/homepage.spec.ts          # one file
npx playwright test specs/mit-learn -g "renders the channel"   # by title
```

Useful while iterating:

| Flag | Why |
|---|---|
| `--headed` | Watch it drive a real window. |
| `--ui` | Time-travel runner; best for building a locator. |
| `--debug` | Step through with the inspector. |
| `--trace on` | Force a trace on a **passing** run (`retain-on-failure` means a pass leaves none). |
| `--repeat-each=3` | Prove a journey is stable, not lucky. |

For `mit-learn`, the journeys needing no credentials are `homepage.spec.ts`,
`search-direct-url.spec.ts` and `channel-and-drawer.spec.ts`;
`login-and-search.spec.ts` is the signed-in one.

## Step 4 — Credentials, which are the sharp edge

> [!WARNING]
> **A mistyped password is not a free retry.** Realm `olapps` is configured
> `failureFactor=10`, `permanentLockout=true`, `maxTemporaryLockouts=1`,
> `maxDeltaTimeSeconds=43200`. Ten consecutive failures earn one temporary
> lockout; the **next ten permanently disable the account**, and only an admin
> can undo that. The 12-hour counter reset means failures accumulate rather than
> ageing out.

Two consequences that are easy to miss:

1. Your local attempts and the pipeline's land on the **same account**. The
   scheduled canary is spending attempts every 10 minutes. Hand-debugging a
   login against `canary_mit_learn` while it is already failing is how the
   account gets disabled.
2. Rotation must change Keycloak **and** the SOPS/Vault value together. The gap
   between the two is itself enough to disable the account, and it presents as a
   captcha error rather than an auth error.

### Prefer your own account

**Do not use the pipeline's credential for local work.** Provision yourself a
separate account in the `olapps` realm on `sso-qa.ol.mit.edu`. A usable one
needs all of:

- `emailVerified=true` and an **empty** `requiredActions` — the realm sets
  `verifyEmail=true` and has `VERIFY_EMAIL` as a *default action*, so a freshly
  created user cannot log in until it is cleared.
- the `fullName` attribute, which the realm's user profile marks required and
  which **rejects parentheses**.
- **one manual login**, to consume first-login onboarding. Otherwise login lands
  on `/onboarding?…&is_new_user=1` and `sign-in.ts` throws by design.

Then supply it without putting the secret in shell history or a file:

```bash
export CANARY_USER_EMAIL='you+canary@mit.edu'
read -rs CANARY_USER_PASSWORD && export CANARY_USER_PASSWORD
npx playwright test specs/mit-learn/login-and-search.spec.ts --project=chromium
```

`read -rs` does not echo and does not enter history. Unset it when done:
`unset CANARY_USER_PASSWORD`.

### If you genuinely need the pipeline credential

Pipe it out of SOPS so the value is never typed, pasted, or written to disk:

```bash
REPO=$(git rev-parse --show-toplevel)
SECRETS="$REPO/src/bridge/secrets/concourse/operations.production.yaml"
KEY='["pipelines"]["infrastructure/canary_mit_learn"]'

CANARY_USER_EMAIL=$(sops -d --extract "$KEY"'["email"]' "$SECRETS") \
CANARY_USER_PASSWORD=$(sops -d --extract "$KEY"'["password"]' "$SECRETS") \
  npx playwright test specs/mit-learn/login-and-search.spec.ts --project=chromium
```

Requires KMS access.

To get the same credential into a Docker run, **export it and forward the
variable *name* only**:

```bash
export CANARY_USER_EMAIL=$(sops -d --extract "$KEY"'["email"]' "$SECRETS")
export CANARY_USER_PASSWORD=$(sops -d --extract "$KEY"'["password"]' "$SECRETS")
docker run --rm -e CANARY_USER_EMAIL -e CANARY_USER_PASSWORD ...
```

`-e NAME` with no `=` tells Docker to copy the value from your environment.
Verified both halves of why that is the right form: the container does receive
the value, and `ps -eo args` on the host shows only `-e CANARY_USER_PASSWORD`,
never the password.

The two forms to avoid, and why:

- **`-e NAME=value`** puts the secret in the command line, where `ps` and your
  shell history both pick it up.
- **`--env-file`** keeps it out of `ps`, but only by writing the secret to disk
  in plaintext — which is the thing this whole section exists to avoid, and the
  file invariably outlives the debugging session that created it.

### The refusal marker will bite you on the second local run

`sign-in.ts` records a rejected credential at
`/tmp/mit-learn-canary-credential-rejected` and refuses to submit it again,
precisely so a bad credential cannot burn ten attempts. In Concourse that file
dies with the container. **On your workstation `/tmp` persists**, so once you
have hit a rejection every later run fails with "Refusing to re-submit a
credential Keycloak already rejected in this run" — including after you fix the
password.

```bash
rm -f /tmp/mit-learn-canary-credential-rejected
```

That is the correct fix, and it is also the moment to be sure the credential is
actually right, because the guard is no longer protecting you.

## Step 5 — Reading a trace

The trace is almost always the fastest route to a diagnosis.

```bash
# A local failure.
npx playwright show-trace canary-results/artifacts/<test-dir>/trace.zip

# The whole local report.
npx playwright show-report canary-results/html
```

For a **pipeline** failure, pull the artifacts Concourse published:

```bash
aws s3 ls --recursive s3://ol-eng-artifacts/canary-results/canary-mit-learn/
aws s3 cp --recursive \
  s3://ol-eng-artifacts/canary-results/canary-mit-learn/<job>/<YYYYMMDDTHHMMSSZ>/ \
  /tmp/canary-failure/
npx playwright show-trace /tmp/canary-failure/artifacts/<test-dir>/trace.zip
```

There is no build number in that path — task containers on our Concourse get no
`BUILD_*` variables — so **match a directory to a build by the build's start
time**. Or drop `trace.zip` into <https://trace.playwright.dev/>, which needs no
local install.

Green runs publish no trace, but every run publishes its small `results.json` to
`s3://ol-eng-artifacts/canary-runs/canary-<property>/<job>/<TS>.json`. That is
where to look for whether a green build had to retry a journey (`stats.flaky`).

## Step 6 — Reproducing a pipeline failure exactly

When a journey passes locally and fails in Concourse, run the same image. Derive
the tag from `package.json` — never hardcode it, and never let the runner and the
image drift:

```bash
TAG=v$(python3 -c "import json;print(json.load(open('package.json'))['devDependencies']['@playwright/test'])")-noble

rm -rf /tmp/canary-run && mkdir -p /tmp/canary-run
rsync -a --exclude node_modules --exclude canary-results . /tmp/canary-run/
docker run --rm -v /tmp/canary-run:/work -w /work \
  -e CANARY_BASE_URL=https://rc.learn.mit.edu \
  -e CANARY_USER_EMAIL -e CANARY_USER_PASSWORD \
  "mcr.microsoft.com/playwright:$TAG" \
  bash -c "npm ci && npx playwright test specs/mit-learn --project=chromium"
```

Copy the tree rather than mounting it in place, so the container's `npm ci`
cannot overwrite your host `node_modules`.

**`specs/mit-learn` includes the signed-in journey, so the credential variables
have to be exported first** (see "If you genuinely need the pipeline credential"
above) — Docker does not inherit them on its own. Without them this is not a
reproduction of anything: `sign-in.ts` throws before the browser opens, and you
get two credential failures instead of the pipeline failure you came to chase.
Measured against the current spec set: 4 passed, 2 failed on
`login-and-search.spec.ts`.

If you only need an anonymous journey, name it instead of the directory and drop
the two credential flags:

```bash
  bash -c "npm ci && npx playwright test specs/mit-learn/homepage.spec.ts --project=chromium"
```

### About `--disable-dev-shm-usage`

`playwright.config.ts` sets it **unconditionally on the chromium project**, not
only under CI, so your local Chromium run matches the pipeline and a local pass
does not diverge from it. Leave it alone.

It exists because a Concourse task container gets the default 64MB `/dev/shm`,
where Chromium puts renderer shared memory; without the flag a journey dies
mid-run with a bare tab crash. It is **Chromium-only** — Firefox and WebKit
reject the flag, which is why it sits on that one project rather than in `use`.

### Running WebKit

Do not try to install WebKit's system libraries on a workstation
(`playwright install-deps` wants root and assumes apt). Run the image instead,
adding `--project=webkit` to the `docker run` above. WebKit is roughly twice
Chromium's wall clock, and that difference is hydration — which is where the
races are, and why shipping a journey means running it once in WebKit.

## Troubleshooting

| Symptom | Cause |
|---|---|
| `Executable doesn't exist at …/chromium_headless_shell-…` | `npx playwright install chromium` not run (Step 1), or runner/image versions have drifted. |
| `BEWARE: your OS is not officially supported by Playwright; downloading fallback build for ubuntu24.04-x64` | Benign, on any non-Ubuntu distro. It fetches the Ubuntu build and works. If a journey then fails in a way the pipeline does not, reproduce in the image (Step 6) before believing it. |
| `CANARY_BASE_URL is required` | Step 2. There is deliberately no default. |
| `CANARY_USER_EMAIL and CANARY_USER_PASSWORD are required` | Signed-in journey without credentials. Run the anonymous ones, or Step 4. |
| `Refusing to re-submit a credential Keycloak already rejected` | Stale `/tmp/mit-learn-canary-credential-rejected`. See Step 4. |
| Reached `…/protocol/openid-connect/registrations` or Touchstone | The account does not exist in the realm. Not a captcha and not an SSO problem. |
| Login landed on `/onboarding` | Account has never completed onboarding. Log in by hand once. |
| `item … with '.only' is not allowed due to the 'forbidOnly' option` | A stray `.only`. Filter by path or `-g` instead. |
| Passes locally, fails in Concourse | Reproduce in the image (Step 6). Suspect a hydration race; try WebKit. |

## Checklist

- [ ] `npm ci` **and** `npx playwright install chromium`
- [ ] `CANARY_BASE_URL` exported
- [ ] Anonymous journeys pass before touching credentials
- [ ] Used your own dev account, not `canary_mit_learn`
- [ ] Password supplied via `read -rs` or `sops -d --extract`, not history or a file
- [ ] `/tmp/mit-learn-canary-credential-rejected` cleared if a rejection was hit
- [ ] `unset CANARY_USER_PASSWORD` afterwards

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.