Skip to main content
Version: Current

Multi-project setup

If you want to use Happo in multiple projects in your organization, you can do so with the project config option. You can have separate projects per repository, or even have separate projects within the same repository.

By default, Happo posts one build status per project:

Two Happo projects with statuses for a PR Two Happo projects posting separate PR build statuses.

Combining projects​

Since happo v6.23.0, you can have Happo combine every project that runs for the same commits into a single job, which posts a single Happo status check for all of them. Set group to true in the config of each project, and run them the way you normally would: each joins the job when it starts.

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
project: 'components',
group: true,
// ... rest of config
});

Two Happo projects with a combined status for a PR Two Happo projects posting a combined PR build status.

Changed in happo v7: group: true is the default, so projects combine without setting it. Set group: false to keep a project out of the combined job. See Upgrading to happo v7.

The status stays pending until every project that has started is done. A project that starts after the others have finished joins the job and sets the status back to pending, so if one of your projects only starts once another CI job is done, give the projects separate groups (see below) or make sure their CI jobs start together.

Separate status checks with groups​

Most teams need only one group. Some want a status check of their own for some projects: to make the component library a required check but not the end-to-end tests, say, or so that a project that runs later doesn't hold the others' check pending (more reasons). Give those projects a named group instead of true:

happo.e2e.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
project: 'e2e',
group: 'e2e',
// ... rest of config
});

Each group gets its own status check: Happo for the default group (group: true), and Happo (e2e) for the projects in the e2e group. The groups still share one job, and its page lists the projects by group.

Pull requests without Happo runs​

If Happo is a required status check and you only run it when some files change (using paths filters in GitHub Actions, for instance), a pull request that doesn't touch them never gets a Happo status, and can't be merged. Happo can't tell that a run isn't coming, so tell it, from the CI step that decides not to run Happo. Either:

  • Skip the project: Happo posts a passing status on the commit, under the status check the project would have reported to. For a project with a group, that's the group's check (Happo or Happo (e2e), say); otherwise it's the project's own. Skip once for each project, or each group, that doesn't run.
  • Or, if you use orchestration, orchestrate with no projects: Happo posts a passing Happo status for a job with nothing to check.

Skipping with the happo package​

npx happo skip

It reads the project and group from your Happo config, like happo does, and posts for the current commit (--afterSha to name another). If a Happo run for the project, or for another project in its group, has already started on the commit, it posts nothing and leaves that run to post the status.

Skipping with curl​

You don't need to check out your code or install anything to skip: one request to the skip endpoint does it. Here it is in GitHub Actions, as a job that runs when the one running Happo doesn't:

.github/workflows/happo.yml
skip-happo:
needs: changes
if: needs.changes.outputs.ui != 'true'
runs-on: ubuntu-latest
steps:
- run: |
curl --fail-with-body --silent --show-error \
-u "$HAPPO_API_KEY:$HAPPO_API_SECRET" \
--json '{"project": "components", "group": true, "ifNotStarted": true}' \
"https://happo.io/api/skip/$HEAD_SHA"
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}
  • HEAD_SHA is the full SHA of the pull request's head commit, the one your required check is on. Elsewhere, git rev-parse HEAD in a checkout of the branch gives it.
  • project is the project's name in Happo. Leave it out for your account's default project.
  • group is what the project's config sets: true, a group's name, or leave it out for a project without a group.
  • ifNotStarted makes the call harmless if Happo does run for the commit: it then posts nothing, and answers {"posted": false}. Without it, a project that already has a Happo run is taken out of that run.
  • --json needs curl 7.82 or later. With an older curl, use -H 'Content-Type: application/json' -d '{…}' instead.

Orchestrating with no projects​

With orchestration, call the orchestrate endpoint with an empty list of projects instead of the list you'd otherwise run. Like the orchestrate call itself, it needs the merge base of the pull request, so check out its history first, but there's still nothing to install:

.github/workflows/happo.yml
skip-happo:
needs: changes
if: needs.changes.outputs.ui != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- run: |
before_sha=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
curl --fail-with-body --silent --show-error \
-u "$HAPPO_API_KEY:$HAPPO_API_SECRET" \
--json '{"projects": []}' \
"https://happo.io/api/jobs/$before_sha/$HEAD_SHA/orchestrate"
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

This posts the Happo status only. For a named group's Happo (<group>) status, skip instead.

Orchestration​

note

With happo v6.23.0 and later, group combines projects without an orchestration call. Use orchestration with older versions of the happo package, or if you call the Happo API without it. Use one or the other for the same commits: an orchestrated job and a combined job each post their own statuses, and both post plain Happo.

If you know beforehand what projects you are going to run Happo for in CI, you can call the Orchestration API endpoint to tell Happo what projects you intend to process. Happo then combines them into one job, and posts one status for all of them.

Call the endpoint before any of the projects start running, with the same "before" and "after" commits that the happo command uses. For a pull request, that's the merge base of the PR's base branch and its head commit, and the head commit itself.

Here's an example GitHub Actions workflow with two projects: components, a Storybook configured in happo.config.ts, and e2e, a Playwright test suite configured in happo.e2e.config.ts. Pushes to main only create the reports that PRs are compared against, so there's nothing to orchestrate there.

.github/workflows/happo.yml
name: Happo

on:
push:
branches: [main]
pull_request:

jobs:
orchestrate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- run: |
before_sha=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
jq -n --arg link "$PR_URL" --arg message "$PR_TITLE" '{
projects: ["components", "e2e"],
link: $link,
message: $message
}' | curl --fail-with-body --silent --show-error --json @- \
-u "$HAPPO_API_KEY:$HAPPO_API_SECRET" \
"https://happo.io/api/jobs/$before_sha/$HEAD_SHA/orchestrate"
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_TITLE: ${{ github.event.pull_request.title }}
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

components:
needs: orchestrate
# Also run on pushes to main, where orchestrate is skipped.
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-node@v4
- run: npm ci
- run: npx happo
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

e2e:
needs: orchestrate
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx happo --config happo.e2e.config.ts -- playwright test
env:
HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }}
HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }}

The project names in the orchestration call have to match the project option in each config file.