Running Midstream in CI
How a test run authenticates, how we work out which project it belongs to, and a workflow you can copy.
Captures are made by your own test suite, in your own CI. Nothing of ours runs there — the SDK notices it is in CI and registers each scene it passes. On GitHub Actions the only thing you have to arrange is that the run's token is visible to the tests.
What a run sends
Every time a test reaches a scene call, the SDK sends us:
- the scene's slug, and the display name derived from it,
- a screenshot of the page, as a PNG,
- an accessibility snapshot of the page, which is how we tell later whether the scene changed,
- the test file's path and the chain of
describetitles ending in the test's name, which is how a deploy finds the test again, - the commit, and the branch CI reported,
- any parameters the test declared for that scene,
- the version of
@midstream/sdkthat sent it.
That is one capture. It never fails your test: if we are unreachable, or the
token is wrong, or the project cannot be found, the SDK prints a line starting
with [Midstream] saying what went wrong and what to change, and the test
carries on.
No cookies or session data leave your CI. Those are captured later, during a deploy, in our own container.
GitHub Actions
name: E2E
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: npx playwright test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Two lines are doing the work.
GITHUB_TOKEN. Actions creates a token for every run, but does not put it in
your step's environment. Pass it in, as above, and the SDK finds it. That is the
whole of the authentication setup — no secret to create, no key to rotate.
The trigger. on: pull_request and on: push both work, and so does
running on both. On a pull-request run Actions reports a synthetic merge commit
as the commit under test — a commit that heads no branch, and that no pull
request can be matched to. The SDK ignores it and reads the pull request's real
head out of the event payload instead. On a push run the reported commit is
already the right one.
Nothing else needs adding. GITHUB_REPOSITORY, GITHUB_SHA and the event
payload are already in the environment, and the SDK reads them.
How we know which project you mean
Your CI knows a repository, not a Midstream project. So the SDK sends the
repository — GITHUB_REPOSITORY, in the form acme/web — and we look up the
project linked to it. A repository belongs to at most one project, so that is
unambiguous.
Name the project explicitly when the repository will not do:
env:
MIDSTREAM_PROJECT: webYou need that when your CI does not report a repository, when the repository it
reports is a mirror rather than the one connected to Midstream, or when a scene
call has to land in a specific project. A per-call project option does the same
thing for one scene.
On GitHub Actions with the built-in GITHUB_TOKEN there is nothing to name: that
token speaks for one repository, so it can only write to that repository's
project. Naming a different one is refused. Use a workspace API key if a run
genuinely has to write somewhere else.
If neither is available, the capture is dropped with a message saying no project could be derived from the request's repository.
Who the run authenticates as
Every token is checked against GitHub before we accept anything from it. What we check depends on which kind of token it is, and the two kinds are not alike.
A workflow's built-in GITHUB_TOKEN authenticates as the repository. It
belongs to no account — GitHub mints it for the run, scoped to the repository the
workflow is running in — so we ask GitHub which repository that is and accept the
run for the project connected to it. Nobody is added to your workspace, and the
run can write to that project and no other. Setting MIDSTREAM_PROJECT to some
other project is refused rather than obeyed.
A personal access token authenticates as its owner, and that account must have
write access to the repository it claims. Read access is not enough — for a public
repository, any token at all can read it. Like the built-in token, it can write
to the project connected to that repository and no other, and setting
MIDSTREAM_PROJECT to some other project is refused.
A GitHub token is only ever good for registering captures. It does not sign anyone in to the rest of the Midstream API — for that, use a workspace API key.
That account then has to be a member of the Midstream workspace that owns the project. Usually it already is, and if it is not we add it: a GitHub user with write access to a connected repository is added to the workspace as a member the first time their CI run registers a capture. The exception is someone who already has a Midstream account under the same email but is not in that workspace — their captures are refused until a member invites them.
Outside GitHub Actions
The SDK treats a run as CI when CI=true, or when GitHub Actions, GitLab CI, or
CircleCI announce themselves. If your platform sets none of those, set CI=true
yourself — otherwise the scene call does nothing at all, which is what it is
supposed to do on a laptop. Wherever it runs, it needs three things:
MIDSTREAM_API_KEY— a workspace API key, because there is no GitHub run whose token we could check. Create one in workspace settings and store it as a CI secret. See API keys.MIDSTREAM_PROJECT— the project slug, because your CI is not GitHub and reports no repository.GIT_COMMIT— the commit under test. On GitHub Actions this comes for free; elsewhere, set it from whatever your platform calls it. Without a commit a capture cannot be recorded at all.
GitLab CI:
e2e:
image: mcr.microsoft.com/playwright:v1.64.1-noble
script:
- npm ci
- npx playwright test
variables:
MIDSTREAM_PROJECT: web
GIT_COMMIT: $CI_COMMIT_SHA
MIDSTREAM_API_KEY: $MIDSTREAM_API_KEYMIDSTREAM_WORKSPACE is a fourth, and with a key you never need it. A key
reaches only the workspace it was made in, so MIDSTREAM_PROJECT is looked up
there. Set it to any other workspace and the run is refused.
A personal access token works here too, if you would rather mint one. It
authenticates as its owner, under the write-access rules above — the repository
path is only for the token Actions builds in, which you cannot get outside
Actions. Set GITHUB_REPOSITORY to the repository (owner/name) as well, since
a personal access token is only accepted for a repository it can push to. The key is the simpler thing to own here, and when both are set, the
key wins.
Confirming it arrived
Add our reporter to your Playwright config and every run ends with one line saying how many scenes registered:
export default defineConfig({
reporter: [["list"], ["@midstream/sdk/playwright/reporter"]],
});[Midstream] 6 of 8 scenes registered. 2 failed:
project-not-found — checkout-empty, invoice-overdue
Connect the repository to a project in Midstream, or set MIDSTREAM_PROJECT to a project slug.Keep your own reporter in the list — ours only adds that summary. It never fails the build. Without it, the SDK still prints a summary as each Playwright worker exits, but only when something failed.
Open the project. Scenes captured on the commit you just pushed appear under that branch, each with the screenshot from the run. On a branch with an open pull request, a Midstream check appears on the pull request within a minute or so — updates are grouped per commit, so a suite that captures two hundred scenes produces one update, not two hundred.
If nothing shows up, work through these in order:
- Look at the CI log for lines starting with
[Midstream]. A failed registration always says why. - Check the credential is in the step's environment. This is the most
common cause by a distance. With nothing to authenticate with, the log says
no credentials in the environmentand names both vars. - Check the scene call ran. A test that fails before its scene call never registers anything. So does a test that CI skipped.
- Check the repository is linked to a project in the workspace, and that whoever's token ran CI is a member of it. See Connecting a repository.
- Check your SDK version. We refuse requests from an
@midstream/sdkolder than the oldest one we support. Today that is 0.2.2, because older versions record a test path a deploy cannot find. The log names the version to upgrade to:Upgrade @midstream/sdk to 0.2.2 or later.
Scenes appear in the project as soon as the capture arrives, whether or not your suite went on to pass. A red build with a working scene is still a scene you can open.
What we hear about a failed registration
When a scene fails to register, the SDK tells us, so we can see a broken setup
before you have to. Most failures never reach our servers at all — a missing
token, a mistyped MIDSTREAM_API_URL, a network that blocks the request — so
without this we would never know.
The report holds:
- why it failed (for example
no-credentialsorproject-not-found), - the scene's slug and the project slug, if you set one,
- the SDK version,
- the repository CI reported, which on GitHub Actions is
GITHUB_REPOSITORY, - which CI it ran on,
- the error message our API sent back, if there was one.
It never holds a token, page content, or test output, and it is only sent when
a registration fails. It goes to the same midstream.studio address as the
captures themselves.
To turn it off, set MIDSTREAM_TELEMETRY=0 in the step's environment.