Skip to content

Commit 4ed5ac0

Browse files
pranavz28claude
andauthored
feat(env): auto-detect 10 additional CI providers (PER-7828) (#2194)
* feat(env): auto-detect 10 additional CI providers (PER-7828) Add detection plus commit/branch/PR/parallel-nonce extraction for: TeamCity, AWS CodeBuild, Google Cloud Build, Atlassian Bamboo, Bitrise, Codemagic, Vercel, Cloudflare Pages, GoCD, Woodpecker. Detection ordering: - Woodpecker placed before Drone (guards against pre-3.x Woodpecker installs that set DRONE=true for backwards compatibility). - GCB placed last before the CI/unknown fallback because BUILD_ID + PROJECT_ID is the most generic marker; defensive !JENKINS_URL guard added. Parallel-nonce rerun-stability choices: - Bamboo uses bamboo_buildResultKey (includes build-N suffix) rather than bamboo_buildNumber (reused on rerun). - Cloudflare Pages uses a composite \${commit}-\${url} nonce with a strict null-guard on commit SHA so we never emit "undefined". - GoCD uses a composite \${pipeline}.\${stage} counter so stage reruns do not collide. Each provider gets a dedicated test file covering detection, PR builds (where applicable), edge cases (CodeBuild manual triggers, GCB manual submits, Vercel system-env-vars-off, Woodpecker Drone-compat collision, Jenkins-over-GCB precedence), and PERCY_* override precedence. API side is a no-op: the 'source' field is free-form metadata with no allowlist. Tekton/Argo excluded — no standard git env vars. Documentation for the 10 new providers (plus doc gaps for Harness, Heroku CI, Probo.CI) ships in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(env): fix CF Pages nonce + add Tekton/Argo detection E2E testing revealed the Cloudflare Pages composite nonce (CF_PAGES_COMMIT_SHA-CF_PAGES_URL) exceeded Percy's 64-char API limit, causing build creation to fail. Switched to CF_PAGES_COMMIT_SHA alone — this also gives correct rerun dedup behavior since the URL changes per redeploy. Added opt-in detection for Tekton Pipelines and Argo Workflows. Neither auto-injects identifying env vars into step containers, so users set them via template substitution: # Tekton env: - name: TEKTON_PIPELINE_RUN value: "$(context.pipelineRun.name)" - name: TEKTON_COMMIT_SHA value: "$(params.commit-sha)" - name: TEKTON_BRANCH value: "$(params.branch)" # Argo Workflows env: - name: ARGO_WORKFLOW_NAME value: "{{workflow.name}}" - name: ARGO_WORKFLOW_UID value: "{{workflow.uid}}" - name: ARGO_COMMIT_SHA value: "{{workflow.parameters.commit-sha}}" - name: ARGO_BRANCH value: "{{workflow.parameters.branch}}" 🤖 Generated with Claude Opus 4.7 (1M context) via Claude Code + Compound Engineering v2.50.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(env): list all 12 new CI providers + opt-in setup Adds README entries for the 10 auto-detect providers (TeamCity, AWS CodeBuild, GCB, Bamboo, Bitrise, Codemagic, Vercel, Cloudflare Pages, GoCD, Woodpecker), backfills Harness CI (was detected in code but missing from the list), and documents the opt-in setup for Tekton Pipelines and Argo Workflows with copy-paste YAML snippets. Also calls out the Vercel System Env Vars + PERCY_PARALLEL_TOTAL=-1 requirement surfaced during E2E testing. 🤖 Generated with Claude Opus 4.7 (1M context) via Claude Code + Compound Engineering v2.50.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4fb6a91 commit 4ed5ac0

14 files changed

Lines changed: 821 additions & 0 deletions

packages/env/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,86 @@ into a common interface for consumption by `@percy/client`.
55

66
## Supported Environments
77

8+
Auto-detected based on environment variables that the CI provider sets during a build.
9+
810
- [AppVeyor](https://www.browserstack.com/docs/percy/ci-cd/appveyor)
11+
- [Atlassian Bamboo](#supported-environments) (needs doc)
12+
- [AWS CodeBuild](#supported-environments) (needs doc)
913
- [Azure Pipelines](https://www.browserstack.com/docs/percy/ci-cd/azure-pipelines)
1014
- [Bitbucket Pipelines](https://www.browserstack.com/docs/percy/ci-cd/bitbucket-pipeline)
15+
- [Bitrise](#supported-environments) (needs doc)
1116
- [Buildkite](https://www.browserstack.com/docs/percy/ci-cd/buildkite)
1217
- [CircleCI](https://www.browserstack.com/docs/percy/ci-cd/circleci)
18+
- [Cloudflare Pages](#supported-environments) (needs doc)
19+
- [Codemagic](#supported-environments) (needs doc)
1320
- [Codeship](https://www.browserstack.com/docs/percy/ci-cd/codeship)
1421
- [Drone CI](https://docs.percy.io/docs/drone)
1522
- [GitHub Actions](https://www.browserstack.com/docs/percy/ci-cd/github-actions)
1623
- [GitLab CI](https://www.browserstack.com/docs/percy/ci-cd/gitlab)
24+
- [GoCD](#supported-environments) (needs doc)
25+
- [Google Cloud Build](#supported-environments) (needs doc)
26+
- [Harness CI](#supported-environments) (needs doc)
1727
- [Heroku CI](#supported-environments) (needs doc)
1828
- [Jenkins](https://www.browserstack.com/docs/percy/ci-cd/jenkins)
1929
- [Jenkins PRB](https://www.browserstack.com/docs/percy/ci-cd/jenkins)
2030
- [Netlify](https://www.browserstack.com/docs/percy/ci-cd/netlify)
2131
- [Probo.CI](#supported-environments) (needs doc)
2232
- [Semaphore](https://www.browserstack.com/docs/percy/ci-cd/semaphore)
33+
- [TeamCity](#supported-environments) (needs doc)
2334
- [Travis CI](https://www.browserstack.com/docs/percy/ci-cd/travis-ci)
35+
- [Vercel](#vercel) — see note below
36+
- [Woodpecker CI](#supported-environments) (needs doc)
37+
38+
## Opt-in Environments
39+
40+
Kubernetes-native pipelines do not inject provider-identifying environment variables
41+
into step containers by default. To enable Percy detection on these systems, expose
42+
the following variables via template substitution in your pipeline definition.
43+
44+
### Tekton Pipelines
45+
46+
```yaml
47+
steps:
48+
- name: percy
49+
image: node:20
50+
env:
51+
- name: TEKTON_PIPELINE_RUN # required — triggers detection
52+
value: "$(context.pipelineRun.name)"
53+
- name: TEKTON_COMMIT_SHA
54+
value: "$(params.commit-sha)"
55+
- name: TEKTON_BRANCH
56+
value: "$(params.branch)"
57+
- name: TEKTON_PULL_REQUEST # optional
58+
value: "$(params.pr-number)"
59+
```
60+
61+
### Argo Workflows
62+
63+
```yaml
64+
- name: percy
65+
container:
66+
image: node:20
67+
env:
68+
- name: ARGO_WORKFLOW_NAME # required — triggers detection
69+
value: "{{workflow.name}}"
70+
- name: ARGO_WORKFLOW_UID # recommended — used as parallel nonce
71+
value: "{{workflow.uid}}"
72+
- name: ARGO_COMMIT_SHA
73+
value: "{{workflow.parameters.commit-sha}}"
74+
- name: ARGO_BRANCH
75+
value: "{{workflow.parameters.branch}}"
76+
- name: ARGO_PULL_REQUEST # optional
77+
value: "{{workflow.parameters.pr-number}}"
78+
```
79+
80+
### Vercel
81+
82+
Vercel exposes its `VERCEL_*` system environment variables to the build step only
83+
when **Automatically expose System Environment Variables** is enabled on the project
84+
(Settings → Environment Variables). Percy also needs `PERCY_PARALLEL_TOTAL=-1`
85+
set in the project environment for the parallel nonce to populate from
86+
`VERCEL_DEPLOYMENT_ID` — otherwise reruns of the same deploy will create separate
87+
Percy builds instead of deduping.
2488

2589
## Percy Environment Variables
2690

packages/env/src/environment.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class PercyEnv {
2323
return 'circle';
2424
} else if (this.vars.CI_NAME === 'codeship') {
2525
return 'codeship';
26+
} else if (this.vars.CI_SYSTEM_NAME === 'woodpecker' || this.vars.CI === 'woodpecker') {
27+
return 'woodpecker';
2628
} else if (this.vars.DRONE === 'true') {
2729
return 'drone';
2830
} else if (this.vars.SEMAPHORE === 'true') {
@@ -47,6 +49,28 @@ export class PercyEnv {
4749
return 'netlify';
4850
} else if (this.vars.HARNESS_PROJECT_ID) {
4951
return 'harness';
52+
} else if (this.vars.TEAMCITY_VERSION) {
53+
return 'teamcity';
54+
} else if (this.vars.CODEBUILD_BUILD_ID) {
55+
return 'aws-codebuild';
56+
} else if (this.vars.bamboo_buildKey) {
57+
return 'bamboo';
58+
} else if (this.vars.BITRISE_IO === 'true') {
59+
return 'bitrise';
60+
} else if (this.vars.CM_BUILD_ID) {
61+
return 'codemagic';
62+
} else if (this.vars.VERCEL === '1') {
63+
return 'vercel';
64+
} else if (this.vars.CF_PAGES === '1') {
65+
return 'cloudflare-pages';
66+
} else if (this.vars.GO_PIPELINE_NAME && this.vars.GO_SERVER_URL) {
67+
return 'gocd';
68+
} else if (this.vars.BUILD_ID && this.vars.PROJECT_ID && !this.vars.JENKINS_URL) {
69+
return 'gcb';
70+
} else if (this.vars.TEKTON_PIPELINE_RUN) {
71+
return 'tekton';
72+
} else if (this.vars.ARGO_WORKFLOW_NAME) {
73+
return 'argo-workflows';
5074
} else if (this.vars.CI) {
5175
return 'CI/unknown';
5276
} else {
@@ -109,6 +133,30 @@ export class PercyEnv {
109133
return github(this.vars).pull_request?.head.sha || this.vars.GITHUB_SHA;
110134
case 'harness':
111135
return this.vars.DRONE_COMMIT_SHA;
136+
case 'woodpecker':
137+
return this.vars.CI_COMMIT_SHA;
138+
case 'teamcity':
139+
return this.vars.BUILD_VCS_NUMBER;
140+
case 'aws-codebuild':
141+
return this.vars.CODEBUILD_RESOLVED_SOURCE_VERSION;
142+
case 'bamboo':
143+
return this.vars.bamboo_planRepository_revision;
144+
case 'bitrise':
145+
return this.vars.BITRISE_GIT_COMMIT;
146+
case 'codemagic':
147+
return this.vars.CM_COMMIT;
148+
case 'vercel':
149+
return this.vars.VERCEL_GIT_COMMIT_SHA;
150+
case 'cloudflare-pages':
151+
return this.vars.CF_PAGES_COMMIT_SHA;
152+
case 'gocd':
153+
return this.vars.GO_REVISION;
154+
case 'gcb':
155+
return this.vars.COMMIT_SHA;
156+
case 'tekton':
157+
return this.vars.TEKTON_COMMIT_SHA;
158+
case 'argo-workflows':
159+
return this.vars.ARGO_COMMIT_SHA;
112160
}
113161
})();
114162

@@ -157,6 +205,26 @@ export class PercyEnv {
157205
return this.vars.HEAD;
158206
case 'harness':
159207
return this.vars.DRONE_SOURCE_BRANCH || this.vars.DRONE_COMMIT_BRANCH;
208+
case 'woodpecker':
209+
return this.vars.CI_COMMIT_BRANCH;
210+
case 'aws-codebuild':
211+
return this.vars.CODEBUILD_WEBHOOK_HEAD_REF;
212+
case 'bamboo':
213+
return this.vars.bamboo_planRepository_branchName;
214+
case 'bitrise':
215+
return this.vars.BITRISE_GIT_BRANCH;
216+
case 'codemagic':
217+
return this.vars.CM_BRANCH;
218+
case 'vercel':
219+
return this.vars.VERCEL_GIT_COMMIT_REF;
220+
case 'cloudflare-pages':
221+
return this.vars.CF_PAGES_BRANCH;
222+
case 'gcb':
223+
return this.vars.BRANCH_NAME;
224+
case 'tekton':
225+
return this.vars.TEKTON_BRANCH;
226+
case 'argo-workflows':
227+
return this.vars.ARGO_BRANCH;
160228
}
161229
})();
162230

@@ -203,6 +271,24 @@ export class PercyEnv {
203271
return github(this.vars).pull_request?.number;
204272
case 'harness':
205273
return this.vars.DRONE_BUILD_EVENT === 'pull_request' && this.vars.DRONE_COMMIT_LINK?.split('/').slice(-1)[0];
274+
case 'woodpecker':
275+
return this.vars.CI_PIPELINE_EVENT === 'pull_request' && this.vars.CI_COMMIT_PULL_REQUEST;
276+
case 'aws-codebuild':
277+
return this.vars.CODEBUILD_WEBHOOK_TRIGGER?.match(/^pr\/(\d+)$/)?.[1];
278+
case 'bamboo':
279+
return this.vars.bamboo_repository_pr_key;
280+
case 'bitrise':
281+
return this.vars.BITRISE_PULL_REQUEST;
282+
case 'codemagic':
283+
return this.vars.CM_PULL_REQUEST === 'true' && this.vars.CM_PULL_REQUEST_NUMBER;
284+
case 'vercel':
285+
return this.vars.VERCEL_GIT_PULL_REQUEST_ID;
286+
case 'gcb':
287+
return this.vars._PR_NUMBER;
288+
case 'tekton':
289+
return this.vars.TEKTON_PULL_REQUEST;
290+
case 'argo-workflows':
291+
return this.vars.ARGO_PULL_REQUEST;
206292
}
207293
})();
208294

@@ -261,6 +347,32 @@ export class PercyEnv {
261347
return this.vars.GITHUB_RUN_ID;
262348
case 'harness':
263349
return this.vars.HARNESS_BUILD_ID;
350+
case 'woodpecker':
351+
return this.vars.CI_PIPELINE_NUMBER;
352+
case 'teamcity':
353+
return this.vars.BUILD_NUMBER;
354+
case 'aws-codebuild':
355+
return this.vars.CODEBUILD_BUILD_ID;
356+
case 'bamboo':
357+
return this.vars.bamboo_buildResultKey;
358+
case 'bitrise':
359+
return this.vars.BITRISE_BUILD_NUMBER;
360+
case 'codemagic':
361+
return this.vars.CM_BUILD_ID;
362+
case 'vercel':
363+
return this.vars.VERCEL_DEPLOYMENT_ID;
364+
case 'cloudflare-pages':
365+
return this.vars.CF_PAGES_COMMIT_SHA || null;
366+
case 'gocd':
367+
return this.vars.GO_PIPELINE_COUNTER && this.vars.GO_STAGE_COUNTER
368+
? `${this.vars.GO_PIPELINE_COUNTER}.${this.vars.GO_STAGE_COUNTER}`
369+
: this.vars.GO_PIPELINE_COUNTER;
370+
case 'gcb':
371+
return this.vars.BUILD_ID;
372+
case 'tekton':
373+
return this.vars.TEKTON_PIPELINE_RUN;
374+
case 'argo-workflows':
375+
return this.vars.ARGO_WORKFLOW_UID || this.vars.ARGO_WORKFLOW_NAME;
264376
}
265377
})();
266378

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import PercyEnv from '@percy/env';
2+
3+
describe('Argo Workflows', () => {
4+
let env;
5+
6+
beforeEach(() => {
7+
env = new PercyEnv({
8+
PERCY_PARALLEL_TOTAL: '-1',
9+
ARGO_WORKFLOW_NAME: 'my-workflow-42',
10+
ARGO_WORKFLOW_UID: 'argo-uid-xyz',
11+
ARGO_COMMIT_SHA: 'argo-commit-sha',
12+
ARGO_BRANCH: 'argo-branch'
13+
});
14+
});
15+
16+
it('has the correct properties', () => {
17+
expect(env).toHaveProperty('ci', 'argo-workflows');
18+
expect(env).toHaveProperty('commit', 'argo-commit-sha');
19+
expect(env).toHaveProperty('branch', 'argo-branch');
20+
expect(env).toHaveProperty('pullRequest', null);
21+
expect(env).toHaveProperty('parallel.nonce', 'argo-uid-xyz');
22+
expect(env).toHaveProperty('parallel.total', -1);
23+
});
24+
25+
it('has the correct properties for PR triggers', () => {
26+
env = new PercyEnv({ ...env.vars, ARGO_PULL_REQUEST: '42' });
27+
expect(env).toHaveProperty('pullRequest', '42');
28+
});
29+
30+
it('falls back to workflow name when UID is absent', () => {
31+
env = new PercyEnv({ ...env.vars, ARGO_WORKFLOW_UID: undefined });
32+
expect(env).toHaveProperty('parallel.nonce', 'my-workflow-42');
33+
});
34+
35+
it('is not detected when ARGO_WORKFLOW_NAME is unset (opt-in)', () => {
36+
env = new PercyEnv({
37+
ARGO_WORKFLOW_UID: 'argo-uid-xyz',
38+
ARGO_COMMIT_SHA: 'argo-commit-sha'
39+
});
40+
expect(env).toHaveProperty('ci', null);
41+
});
42+
43+
it('respects PERCY_* overrides', () => {
44+
env = new PercyEnv({
45+
...env.vars,
46+
PERCY_COMMIT: 'override-commit',
47+
PERCY_BRANCH: 'override-branch',
48+
PERCY_PULL_REQUEST: '999'
49+
});
50+
expect(env).toHaveProperty('commit', 'override-commit');
51+
expect(env).toHaveProperty('branch', 'override-branch');
52+
expect(env).toHaveProperty('pullRequest', '999');
53+
});
54+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import PercyEnv from '@percy/env';
2+
3+
describe('AWS CodeBuild', () => {
4+
let env;
5+
6+
beforeEach(() => {
7+
env = new PercyEnv({
8+
PERCY_PARALLEL_TOTAL: '-1',
9+
CODEBUILD_BUILD_ID: 'codebuild:build-id',
10+
CODEBUILD_RESOLVED_SOURCE_VERSION: 'codebuild-commit-sha',
11+
CODEBUILD_WEBHOOK_HEAD_REF: 'refs/heads/codebuild-branch',
12+
CODEBUILD_WEBHOOK_TRIGGER: 'branch/codebuild-branch'
13+
});
14+
});
15+
16+
it('has the correct properties', () => {
17+
expect(env).toHaveProperty('ci', 'aws-codebuild');
18+
expect(env).toHaveProperty('commit', 'codebuild-commit-sha');
19+
expect(env).toHaveProperty('branch', 'codebuild-branch');
20+
expect(env).toHaveProperty('pullRequest', null);
21+
expect(env).toHaveProperty('parallel.nonce', 'codebuild:build-id');
22+
expect(env).toHaveProperty('parallel.total', -1);
23+
});
24+
25+
it('parses pull-request number from CODEBUILD_WEBHOOK_TRIGGER', () => {
26+
env = new PercyEnv({
27+
...env.vars,
28+
CODEBUILD_WEBHOOK_TRIGGER: 'pr/42'
29+
});
30+
expect(env).toHaveProperty('pullRequest', '42');
31+
});
32+
33+
it('does not misattribute tag triggers as pull requests', () => {
34+
env = new PercyEnv({
35+
...env.vars,
36+
CODEBUILD_WEBHOOK_TRIGGER: 'tag/v1.0.0'
37+
});
38+
expect(env).toHaveProperty('pullRequest', null);
39+
});
40+
41+
it('returns null for branch and PR on manual or EventBridge triggers (no webhook vars)', () => {
42+
env = new PercyEnv({
43+
PERCY_PARALLEL_TOTAL: '-1',
44+
CODEBUILD_BUILD_ID: 'codebuild:build-id',
45+
CODEBUILD_RESOLVED_SOURCE_VERSION: 'codebuild-commit-sha'
46+
});
47+
expect(env).toHaveProperty('ci', 'aws-codebuild');
48+
expect(env).toHaveProperty('commit', 'codebuild-commit-sha');
49+
expect(env).toHaveProperty('branch', null);
50+
expect(env).toHaveProperty('pullRequest', null);
51+
});
52+
53+
it('respects PERCY_* overrides', () => {
54+
env = new PercyEnv({
55+
...env.vars,
56+
PERCY_COMMIT: 'override-commit',
57+
PERCY_BRANCH: 'override-branch',
58+
PERCY_PULL_REQUEST: '999'
59+
});
60+
expect(env).toHaveProperty('commit', 'override-commit');
61+
expect(env).toHaveProperty('branch', 'override-branch');
62+
expect(env).toHaveProperty('pullRequest', '999');
63+
});
64+
});

0 commit comments

Comments
 (0)