MidstreamDocs
Configuring your project

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 describe titles 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.

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] 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: web

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

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.62.1-noble
  script:
    - npm ci
    - npx playwright test
  variables:
    MIDSTREAM_PROJECT: web
    GIT_COMMIT: $CI_COMMIT_SHA
    MIDSTREAM_API_KEY: $MIDSTREAM_API_KEY

MIDSTREAM_WORKSPACE is a fourth, and you only need it if we ask. A project slug is unique inside a workspace, not across Midstream, so two workspaces can both have a project called web. If your key reaches both, we cannot tell which one you mean and say so rather than write your captures into the wrong one — set MIDSTREAM_WORKSPACE to the workspace slug and the run goes through.

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. The key is the simpler thing to own here, and when both are set, the key wins.

Confirming it arrived

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:

  1. Look at the CI log for lines starting with [Midstream]. A failed registration always says why.
  2. 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 line names both vars: no credentials in the environment.
  3. Check the scene call ran. A test that fails before its scene call never registers anything. So does a test that CI skipped.
  4. 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.

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.