Skip to content
Open
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
64 changes: 64 additions & 0 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,86 @@ into a common interface for consumption by `@percy/client`.

## Supported Environments

Auto-detected based on environment variables that the CI provider sets during a build.

- [AppVeyor](https://www.browserstack.com/docs/percy/ci-cd/appveyor)
- [Atlassian Bamboo](#supported-environments) (needs doc)
- [AWS CodeBuild](#supported-environments) (needs doc)
- [Azure Pipelines](https://www.browserstack.com/docs/percy/ci-cd/azure-pipelines)
- [Bitbucket Pipelines](https://www.browserstack.com/docs/percy/ci-cd/bitbucket-pipeline)
- [Bitrise](#supported-environments) (needs doc)
- [Buildkite](https://www.browserstack.com/docs/percy/ci-cd/buildkite)
- [CircleCI](https://www.browserstack.com/docs/percy/ci-cd/circleci)
- [Cloudflare Pages](#supported-environments) (needs doc)
- [Codemagic](#supported-environments) (needs doc)
- [Codeship](https://www.browserstack.com/docs/percy/ci-cd/codeship)
- [Drone CI](https://docs.percy.io/docs/drone)
- [GitHub Actions](https://www.browserstack.com/docs/percy/ci-cd/github-actions)
- [GitLab CI](https://www.browserstack.com/docs/percy/ci-cd/gitlab)
- [GoCD](#supported-environments) (needs doc)
- [Google Cloud Build](#supported-environments) (needs doc)
- [Harness CI](#supported-environments) (needs doc)
- [Heroku CI](#supported-environments) (needs doc)
- [Jenkins](https://www.browserstack.com/docs/percy/ci-cd/jenkins)
- [Jenkins PRB](https://www.browserstack.com/docs/percy/ci-cd/jenkins)
- [Netlify](https://www.browserstack.com/docs/percy/ci-cd/netlify)
- [Probo.CI](#supported-environments) (needs doc)
- [Semaphore](https://www.browserstack.com/docs/percy/ci-cd/semaphore)
- [TeamCity](#supported-environments) (needs doc)
- [Travis CI](https://www.browserstack.com/docs/percy/ci-cd/travis-ci)
- [Vercel](#vercel) — see note below
- [Woodpecker CI](#supported-environments) (needs doc)

## Opt-in Environments

Kubernetes-native pipelines do not inject provider-identifying environment variables
into step containers by default. To enable Percy detection on these systems, expose
the following variables via template substitution in your pipeline definition.

### Tekton Pipelines

```yaml
steps:
- name: percy
image: node:20
env:
- name: TEKTON_PIPELINE_RUN # required — triggers detection
value: "$(context.pipelineRun.name)"
- name: TEKTON_COMMIT_SHA
value: "$(params.commit-sha)"
- name: TEKTON_BRANCH
value: "$(params.branch)"
- name: TEKTON_PULL_REQUEST # optional
value: "$(params.pr-number)"
```

### Argo Workflows

```yaml
- name: percy
container:
image: node:20
env:
- name: ARGO_WORKFLOW_NAME # required — triggers detection
value: "{{workflow.name}}"
- name: ARGO_WORKFLOW_UID # recommended — used as parallel nonce
value: "{{workflow.uid}}"
- name: ARGO_COMMIT_SHA
value: "{{workflow.parameters.commit-sha}}"
- name: ARGO_BRANCH
value: "{{workflow.parameters.branch}}"
- name: ARGO_PULL_REQUEST # optional
value: "{{workflow.parameters.pr-number}}"
```

### Vercel

Vercel exposes its `VERCEL_*` system environment variables to the build step only
when **Automatically expose System Environment Variables** is enabled on the project
(Settings → Environment Variables). Percy also needs `PERCY_PARALLEL_TOTAL=-1`
set in the project environment for the parallel nonce to populate from
`VERCEL_DEPLOYMENT_ID` — otherwise reruns of the same deploy will create separate
Percy builds instead of deduping.

## Percy Environment Variables

Expand Down
112 changes: 112 additions & 0 deletions packages/env/src/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class PercyEnv {
return 'circle';
} else if (this.vars.CI_NAME === 'codeship') {
return 'codeship';
} else if (this.vars.CI_SYSTEM_NAME === 'woodpecker' || this.vars.CI === 'woodpecker') {
return 'woodpecker';
} else if (this.vars.DRONE === 'true') {
return 'drone';
} else if (this.vars.SEMAPHORE === 'true') {
Expand All @@ -47,6 +49,28 @@ export class PercyEnv {
return 'netlify';
} else if (this.vars.HARNESS_PROJECT_ID) {
return 'harness';
} else if (this.vars.TEAMCITY_VERSION) {
return 'teamcity';
} else if (this.vars.CODEBUILD_BUILD_ID) {
return 'aws-codebuild';
} else if (this.vars.bamboo_buildKey) {
return 'bamboo';
} else if (this.vars.BITRISE_IO === 'true') {
return 'bitrise';
} else if (this.vars.CM_BUILD_ID) {
return 'codemagic';
} else if (this.vars.VERCEL === '1') {
return 'vercel';
} else if (this.vars.CF_PAGES === '1') {
return 'cloudflare-pages';
} else if (this.vars.GO_PIPELINE_NAME && this.vars.GO_SERVER_URL) {
return 'gocd';
} else if (this.vars.BUILD_ID && this.vars.PROJECT_ID && !this.vars.JENKINS_URL) {
return 'gcb';
} else if (this.vars.TEKTON_PIPELINE_RUN) {
return 'tekton';
} else if (this.vars.ARGO_WORKFLOW_NAME) {
return 'argo-workflows';
} else if (this.vars.CI) {
return 'CI/unknown';
} else {
Expand Down Expand Up @@ -109,6 +133,30 @@ export class PercyEnv {
return github(this.vars).pull_request?.head.sha || this.vars.GITHUB_SHA;
case 'harness':
return this.vars.DRONE_COMMIT_SHA;
case 'woodpecker':
return this.vars.CI_COMMIT_SHA;
case 'teamcity':
return this.vars.BUILD_VCS_NUMBER;
case 'aws-codebuild':
return this.vars.CODEBUILD_RESOLVED_SOURCE_VERSION;
case 'bamboo':
return this.vars.bamboo_planRepository_revision;
case 'bitrise':
return this.vars.BITRISE_GIT_COMMIT;
case 'codemagic':
return this.vars.CM_COMMIT;
case 'vercel':
return this.vars.VERCEL_GIT_COMMIT_SHA;
case 'cloudflare-pages':
return this.vars.CF_PAGES_COMMIT_SHA;
case 'gocd':
return this.vars.GO_REVISION;
case 'gcb':
return this.vars.COMMIT_SHA;
case 'tekton':
return this.vars.TEKTON_COMMIT_SHA;
case 'argo-workflows':
return this.vars.ARGO_COMMIT_SHA;
}
})();

Expand Down Expand Up @@ -157,6 +205,26 @@ export class PercyEnv {
return this.vars.HEAD;
case 'harness':
return this.vars.DRONE_SOURCE_BRANCH || this.vars.DRONE_COMMIT_BRANCH;
case 'woodpecker':
return this.vars.CI_COMMIT_BRANCH;
case 'aws-codebuild':
return this.vars.CODEBUILD_WEBHOOK_HEAD_REF;
case 'bamboo':
return this.vars.bamboo_planRepository_branchName;
case 'bitrise':
return this.vars.BITRISE_GIT_BRANCH;
case 'codemagic':
return this.vars.CM_BRANCH;
case 'vercel':
return this.vars.VERCEL_GIT_COMMIT_REF;
case 'cloudflare-pages':
return this.vars.CF_PAGES_BRANCH;
case 'gcb':
return this.vars.BRANCH_NAME;
case 'tekton':
return this.vars.TEKTON_BRANCH;
case 'argo-workflows':
return this.vars.ARGO_BRANCH;
}
})();

Expand Down Expand Up @@ -203,6 +271,24 @@ export class PercyEnv {
return github(this.vars).pull_request?.number;
case 'harness':
return this.vars.DRONE_BUILD_EVENT === 'pull_request' && this.vars.DRONE_COMMIT_LINK?.split('/').slice(-1)[0];
case 'woodpecker':
return this.vars.CI_PIPELINE_EVENT === 'pull_request' && this.vars.CI_COMMIT_PULL_REQUEST;
case 'aws-codebuild':
return this.vars.CODEBUILD_WEBHOOK_TRIGGER?.match(/^pr\/(\d+)$/)?.[1];
case 'bamboo':
return this.vars.bamboo_repository_pr_key;
case 'bitrise':
return this.vars.BITRISE_PULL_REQUEST;
case 'codemagic':
return this.vars.CM_PULL_REQUEST === 'true' && this.vars.CM_PULL_REQUEST_NUMBER;
case 'vercel':
return this.vars.VERCEL_GIT_PULL_REQUEST_ID;
case 'gcb':
return this.vars._PR_NUMBER;
case 'tekton':
return this.vars.TEKTON_PULL_REQUEST;
case 'argo-workflows':
return this.vars.ARGO_PULL_REQUEST;
}
})();

Expand Down Expand Up @@ -261,6 +347,32 @@ export class PercyEnv {
return this.vars.GITHUB_RUN_ID;
case 'harness':
return this.vars.HARNESS_BUILD_ID;
case 'woodpecker':
return this.vars.CI_PIPELINE_NUMBER;
case 'teamcity':
return this.vars.BUILD_NUMBER;
case 'aws-codebuild':
return this.vars.CODEBUILD_BUILD_ID;
case 'bamboo':
return this.vars.bamboo_buildResultKey;
case 'bitrise':
return this.vars.BITRISE_BUILD_NUMBER;
case 'codemagic':
return this.vars.CM_BUILD_ID;
case 'vercel':
return this.vars.VERCEL_DEPLOYMENT_ID;
case 'cloudflare-pages':
return this.vars.CF_PAGES_COMMIT_SHA || null;
case 'gocd':
return this.vars.GO_PIPELINE_COUNTER && this.vars.GO_STAGE_COUNTER
? `${this.vars.GO_PIPELINE_COUNTER}.${this.vars.GO_STAGE_COUNTER}`
: this.vars.GO_PIPELINE_COUNTER;
case 'gcb':
return this.vars.BUILD_ID;
case 'tekton':
return this.vars.TEKTON_PIPELINE_RUN;
case 'argo-workflows':
return this.vars.ARGO_WORKFLOW_UID || this.vars.ARGO_WORKFLOW_NAME;
}
})();

Expand Down
54 changes: 54 additions & 0 deletions packages/env/test/argo-workflows.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import PercyEnv from '@percy/env';

describe('Argo Workflows', () => {
let env;

beforeEach(() => {
env = new PercyEnv({
PERCY_PARALLEL_TOTAL: '-1',
ARGO_WORKFLOW_NAME: 'my-workflow-42',
ARGO_WORKFLOW_UID: 'argo-uid-xyz',
ARGO_COMMIT_SHA: 'argo-commit-sha',
ARGO_BRANCH: 'argo-branch'
});
});

it('has the correct properties', () => {
expect(env).toHaveProperty('ci', 'argo-workflows');
expect(env).toHaveProperty('commit', 'argo-commit-sha');
expect(env).toHaveProperty('branch', 'argo-branch');
expect(env).toHaveProperty('pullRequest', null);
expect(env).toHaveProperty('parallel.nonce', 'argo-uid-xyz');
expect(env).toHaveProperty('parallel.total', -1);
});

it('has the correct properties for PR triggers', () => {
env = new PercyEnv({ ...env.vars, ARGO_PULL_REQUEST: '42' });
expect(env).toHaveProperty('pullRequest', '42');
});

it('falls back to workflow name when UID is absent', () => {
env = new PercyEnv({ ...env.vars, ARGO_WORKFLOW_UID: undefined });
expect(env).toHaveProperty('parallel.nonce', 'my-workflow-42');
});

it('is not detected when ARGO_WORKFLOW_NAME is unset (opt-in)', () => {
env = new PercyEnv({
ARGO_WORKFLOW_UID: 'argo-uid-xyz',
ARGO_COMMIT_SHA: 'argo-commit-sha'
});
expect(env).toHaveProperty('ci', null);
});

it('respects PERCY_* overrides', () => {
env = new PercyEnv({
...env.vars,
PERCY_COMMIT: 'override-commit',
PERCY_BRANCH: 'override-branch',
PERCY_PULL_REQUEST: '999'
});
expect(env).toHaveProperty('commit', 'override-commit');
expect(env).toHaveProperty('branch', 'override-branch');
expect(env).toHaveProperty('pullRequest', '999');
});
});
64 changes: 64 additions & 0 deletions packages/env/test/aws-codebuild.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import PercyEnv from '@percy/env';

describe('AWS CodeBuild', () => {
let env;

beforeEach(() => {
env = new PercyEnv({
PERCY_PARALLEL_TOTAL: '-1',
CODEBUILD_BUILD_ID: 'codebuild:build-id',
CODEBUILD_RESOLVED_SOURCE_VERSION: 'codebuild-commit-sha',
CODEBUILD_WEBHOOK_HEAD_REF: 'refs/heads/codebuild-branch',
CODEBUILD_WEBHOOK_TRIGGER: 'branch/codebuild-branch'
});
});

it('has the correct properties', () => {
expect(env).toHaveProperty('ci', 'aws-codebuild');
expect(env).toHaveProperty('commit', 'codebuild-commit-sha');
expect(env).toHaveProperty('branch', 'codebuild-branch');
expect(env).toHaveProperty('pullRequest', null);
expect(env).toHaveProperty('parallel.nonce', 'codebuild:build-id');
expect(env).toHaveProperty('parallel.total', -1);
});

it('parses pull-request number from CODEBUILD_WEBHOOK_TRIGGER', () => {
env = new PercyEnv({
...env.vars,
CODEBUILD_WEBHOOK_TRIGGER: 'pr/42'
});
expect(env).toHaveProperty('pullRequest', '42');
});

it('does not misattribute tag triggers as pull requests', () => {
env = new PercyEnv({
...env.vars,
CODEBUILD_WEBHOOK_TRIGGER: 'tag/v1.0.0'
});
expect(env).toHaveProperty('pullRequest', null);
});

it('returns null for branch and PR on manual or EventBridge triggers (no webhook vars)', () => {
env = new PercyEnv({
PERCY_PARALLEL_TOTAL: '-1',
CODEBUILD_BUILD_ID: 'codebuild:build-id',
CODEBUILD_RESOLVED_SOURCE_VERSION: 'codebuild-commit-sha'
});
expect(env).toHaveProperty('ci', 'aws-codebuild');
expect(env).toHaveProperty('commit', 'codebuild-commit-sha');
expect(env).toHaveProperty('branch', null);
expect(env).toHaveProperty('pullRequest', null);
});

it('respects PERCY_* overrides', () => {
env = new PercyEnv({
...env.vars,
PERCY_COMMIT: 'override-commit',
PERCY_BRANCH: 'override-branch',
PERCY_PULL_REQUEST: '999'
});
expect(env).toHaveProperty('commit', 'override-commit');
expect(env).toHaveProperty('branch', 'override-branch');
expect(env).toHaveProperty('pullRequest', '999');
});
});
Loading
Loading