diff --git a/packages/env/README.md b/packages/env/README.md index e6f934996..fc8f27e0c 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -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 diff --git a/packages/env/src/environment.js b/packages/env/src/environment.js index 859169b87..222b51d35 100644 --- a/packages/env/src/environment.js +++ b/packages/env/src/environment.js @@ -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') { @@ -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 { @@ -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; } })(); @@ -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; } })(); @@ -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; } })(); @@ -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; } })(); diff --git a/packages/env/test/argo-workflows.test.js b/packages/env/test/argo-workflows.test.js new file mode 100644 index 000000000..a77ece860 --- /dev/null +++ b/packages/env/test/argo-workflows.test.js @@ -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'); + }); +}); diff --git a/packages/env/test/aws-codebuild.test.js b/packages/env/test/aws-codebuild.test.js new file mode 100644 index 000000000..d4c813840 --- /dev/null +++ b/packages/env/test/aws-codebuild.test.js @@ -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'); + }); +}); diff --git a/packages/env/test/bamboo.test.js b/packages/env/test/bamboo.test.js new file mode 100644 index 000000000..8afacb551 --- /dev/null +++ b/packages/env/test/bamboo.test.js @@ -0,0 +1,46 @@ +import PercyEnv from '@percy/env'; + +describe('Bamboo', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + bamboo_buildKey: 'PROJ-PLAN-JOB', + bamboo_planRepository_revision: 'bamboo-commit-sha', + bamboo_planRepository_branchName: 'bamboo-branch', + bamboo_buildResultKey: 'PROJ-PLAN-JOB-42', + bamboo_buildNumber: '42' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'bamboo'); + expect(env).toHaveProperty('commit', 'bamboo-commit-sha'); + expect(env).toHaveProperty('branch', 'bamboo-branch'); + expect(env).toHaveProperty('pullRequest', null); + // buildResultKey (not buildNumber) so reruns don't collide + expect(env).toHaveProperty('parallel.nonce', 'PROJ-PLAN-JOB-42'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR builds', () => { + env = new PercyEnv({ + ...env.vars, + bamboo_repository_pr_key: '7' + }); + expect(env).toHaveProperty('pullRequest', '7'); + }); + + 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'); + }); +}); diff --git a/packages/env/test/bitrise.test.js b/packages/env/test/bitrise.test.js new file mode 100644 index 000000000..10cdf227e --- /dev/null +++ b/packages/env/test/bitrise.test.js @@ -0,0 +1,43 @@ +import PercyEnv from '@percy/env'; + +describe('Bitrise', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + BITRISE_IO: 'true', + BITRISE_GIT_COMMIT: 'bitrise-commit-sha', + BITRISE_GIT_BRANCH: 'bitrise-branch', + BITRISE_PULL_REQUEST: '', + BITRISE_BUILD_NUMBER: 'bitrise-build-number' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'bitrise'); + expect(env).toHaveProperty('commit', 'bitrise-commit-sha'); + expect(env).toHaveProperty('branch', 'bitrise-branch'); + // Bitrise sets empty string on non-PR builds + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'bitrise-build-number'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR builds', () => { + env = new PercyEnv({ ...env.vars, BITRISE_PULL_REQUEST: '42' }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + 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'); + }); +}); diff --git a/packages/env/test/cloudflare-pages.test.js b/packages/env/test/cloudflare-pages.test.js new file mode 100644 index 000000000..996e4b4f7 --- /dev/null +++ b/packages/env/test/cloudflare-pages.test.js @@ -0,0 +1,49 @@ +import PercyEnv from '@percy/env'; + +describe('Cloudflare Pages', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + CF_PAGES: '1', + CF_PAGES_COMMIT_SHA: 'cf-commit-sha', + CF_PAGES_BRANCH: 'cf-branch', + CF_PAGES_URL: 'https://abc123.my-project.pages.dev' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'cloudflare-pages'); + expect(env).toHaveProperty('commit', 'cf-commit-sha'); + expect(env).toHaveProperty('branch', 'cf-branch'); + // Cloudflare Pages does not natively expose PR info + expect(env).toHaveProperty('pullRequest', null); + // Nonce is commit SHA alone — earlier composite (commit + URL) exceeded + // Percy's 64-char nonce limit and caused build creation to fail. + expect(env).toHaveProperty('parallel.nonce', 'cf-commit-sha'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('returns null nonce when CF_PAGES_COMMIT_SHA is absent (never emits "undefined")', () => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + CF_PAGES: '1', + CF_PAGES_BRANCH: 'cf-branch', + CF_PAGES_URL: 'https://abc123.my-project.pages.dev' + }); + expect(env).toHaveProperty('parallel.nonce', 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'); + }); +}); diff --git a/packages/env/test/codemagic.test.js b/packages/env/test/codemagic.test.js new file mode 100644 index 000000000..dac6da53a --- /dev/null +++ b/packages/env/test/codemagic.test.js @@ -0,0 +1,55 @@ +import PercyEnv from '@percy/env'; + +describe('Codemagic', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + CM_BUILD_ID: 'codemagic-build-uuid', + CM_COMMIT: 'codemagic-commit-sha', + CM_BRANCH: 'codemagic-branch', + CM_PULL_REQUEST: 'false' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'codemagic'); + expect(env).toHaveProperty('commit', 'codemagic-commit-sha'); + expect(env).toHaveProperty('branch', 'codemagic-branch'); + // CM_PULL_REQUEST === 'false' (string) means non-PR build + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'codemagic-build-uuid'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR builds', () => { + env = new PercyEnv({ + ...env.vars, + CM_PULL_REQUEST: 'true', + CM_PULL_REQUEST_NUMBER: '42' + }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + it('ignores CM_PULL_REQUEST_NUMBER when CM_PULL_REQUEST is the string "false"', () => { + env = new PercyEnv({ + ...env.vars, + CM_PULL_REQUEST: 'false', + CM_PULL_REQUEST_NUMBER: '42' + }); + 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'); + }); +}); diff --git a/packages/env/test/gcb.test.js b/packages/env/test/gcb.test.js new file mode 100644 index 000000000..53b53ae41 --- /dev/null +++ b/packages/env/test/gcb.test.js @@ -0,0 +1,63 @@ +import PercyEnv from '@percy/env'; + +describe('Google Cloud Build', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + BUILD_ID: 'gcb-build-id', + PROJECT_ID: 'my-gcp-project', + COMMIT_SHA: 'gcb-commit-sha', + BRANCH_NAME: 'gcb-branch' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'gcb'); + expect(env).toHaveProperty('commit', 'gcb-commit-sha'); + expect(env).toHaveProperty('branch', 'gcb-branch'); + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'gcb-build-id'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR triggers', () => { + env = new PercyEnv({ ...env.vars, _PR_NUMBER: '42' }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + it('returns null commit/branch/PR on manual gcloud submits (no trigger vars)', () => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + BUILD_ID: 'gcb-build-id', + PROJECT_ID: 'my-gcp-project' + }); + expect(env).toHaveProperty('ci', 'gcb'); + expect(env).toHaveProperty('commit', null); + expect(env).toHaveProperty('branch', null); + expect(env).toHaveProperty('pullRequest', null); + }); + + it('does not match when JENKINS_URL is also set (Jenkins wins)', () => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + JENKINS_URL: 'https://jenkins.example.com', + BUILD_ID: 'jenkins-build-id', + PROJECT_ID: 'my-gcp-project' + }); + expect(env).toHaveProperty('ci', 'jenkins'); + }); + + 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'); + }); +}); diff --git a/packages/env/test/gocd.test.js b/packages/env/test/gocd.test.js new file mode 100644 index 000000000..35cb8cc6b --- /dev/null +++ b/packages/env/test/gocd.test.js @@ -0,0 +1,61 @@ +import PercyEnv from '@percy/env'; + +describe('GoCD', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + GO_PIPELINE_NAME: 'my-pipeline', + GO_SERVER_URL: 'https://gocd.example.com', + GO_REVISION: 'gocd-commit-sha', + GO_PIPELINE_COUNTER: '42', + GO_STAGE_COUNTER: '1' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'gocd'); + expect(env).toHaveProperty('commit', 'gocd-commit-sha'); + // GoCD does not expose branch/PR via standard env vars + expect(env).toHaveProperty('branch', null); + expect(env).toHaveProperty('pullRequest', null); + // Composite pipeline.stage counter to survive stage reruns + expect(env).toHaveProperty('parallel.nonce', '42.1'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('nonce changes when the stage counter bumps on rerun', () => { + env = new PercyEnv({ ...env.vars, GO_STAGE_COUNTER: '2' }); + expect(env).toHaveProperty('parallel.nonce', '42.2'); + }); + + it('falls back to pipeline counter alone when stage counter is absent', () => { + env = new PercyEnv({ ...env.vars, GO_STAGE_COUNTER: undefined }); + expect(env).toHaveProperty('parallel.nonce', '42'); + }); + + it('returns null commit on multi-material pipelines (GO_REVISION unset)', () => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + GO_PIPELINE_NAME: 'my-pipeline', + GO_SERVER_URL: 'https://gocd.example.com', + GO_PIPELINE_COUNTER: '42', + GO_STAGE_COUNTER: '1' + }); + expect(env).toHaveProperty('ci', 'gocd'); + expect(env).toHaveProperty('commit', 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'); + }); +}); diff --git a/packages/env/test/teamcity.test.js b/packages/env/test/teamcity.test.js new file mode 100644 index 000000000..028afe718 --- /dev/null +++ b/packages/env/test/teamcity.test.js @@ -0,0 +1,47 @@ +import PercyEnv from '@percy/env'; + +describe('TeamCity', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + TEAMCITY_VERSION: '2024.07', + BUILD_VCS_NUMBER: 'teamcity-commit-sha', + BUILD_NUMBER: 'teamcity-build-number' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'teamcity'); + expect(env).toHaveProperty('commit', 'teamcity-commit-sha'); + // TeamCity does not expose branch/PR via standard env vars + expect(env).toHaveProperty('branch', null); + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'teamcity-build-number'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('returns null commit when only the multi-root suffixed var is set', () => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + TEAMCITY_VERSION: '2024.07', + BUILD_VCS_NUMBER_Repo1: 'teamcity-multi-root-sha', + BUILD_NUMBER: 'teamcity-build-number' + }); + expect(env).toHaveProperty('ci', 'teamcity'); + expect(env).toHaveProperty('commit', 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'); + }); +}); diff --git a/packages/env/test/tekton.test.js b/packages/env/test/tekton.test.js new file mode 100644 index 000000000..43d11800e --- /dev/null +++ b/packages/env/test/tekton.test.js @@ -0,0 +1,48 @@ +import PercyEnv from '@percy/env'; + +describe('Tekton Pipelines', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + TEKTON_PIPELINE_RUN: 'my-pipeline-run-42', + TEKTON_COMMIT_SHA: 'tekton-commit-sha', + TEKTON_BRANCH: 'tekton-branch' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'tekton'); + expect(env).toHaveProperty('commit', 'tekton-commit-sha'); + expect(env).toHaveProperty('branch', 'tekton-branch'); + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'my-pipeline-run-42'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR triggers', () => { + env = new PercyEnv({ ...env.vars, TEKTON_PULL_REQUEST: '42' }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + it('is not detected when TEKTON_PIPELINE_RUN is unset (opt-in)', () => { + env = new PercyEnv({ + TEKTON_COMMIT_SHA: 'tekton-commit-sha', + TEKTON_BRANCH: 'tekton-branch' + }); + 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'); + }); +}); diff --git a/packages/env/test/vercel.test.js b/packages/env/test/vercel.test.js new file mode 100644 index 000000000..269c9fce7 --- /dev/null +++ b/packages/env/test/vercel.test.js @@ -0,0 +1,48 @@ +import PercyEnv from '@percy/env'; + +describe('Vercel', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + VERCEL: '1', + VERCEL_GIT_COMMIT_SHA: 'vercel-commit-sha', + VERCEL_GIT_COMMIT_REF: 'vercel-branch', + VERCEL_GIT_PULL_REQUEST_ID: '', + VERCEL_DEPLOYMENT_ID: 'dpl_vercel-deployment-id' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'vercel'); + expect(env).toHaveProperty('commit', 'vercel-commit-sha'); + expect(env).toHaveProperty('branch', 'vercel-branch'); + // Empty string when branch has no PR yet or on production deploys + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'dpl_vercel-deployment-id'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR builds', () => { + env = new PercyEnv({ ...env.vars, VERCEL_GIT_PULL_REQUEST_ID: '42' }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + it('falls through to CI/unknown when system env vars are not exposed (checkbox off)', () => { + env = new PercyEnv({ CI: 'true' }); + expect(env).toHaveProperty('ci', 'CI/unknown'); + }); + + 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'); + }); +}); diff --git a/packages/env/test/woodpecker.test.js b/packages/env/test/woodpecker.test.js new file mode 100644 index 000000000..13b79d0e1 --- /dev/null +++ b/packages/env/test/woodpecker.test.js @@ -0,0 +1,67 @@ +import PercyEnv from '@percy/env'; + +describe('Woodpecker', () => { + let env; + + beforeEach(() => { + env = new PercyEnv({ + PERCY_PARALLEL_TOTAL: '-1', + CI_SYSTEM_NAME: 'woodpecker', + CI: 'woodpecker', + CI_COMMIT_SHA: 'woodpecker-commit-sha', + CI_COMMIT_BRANCH: 'woodpecker-branch', + CI_PIPELINE_NUMBER: 'woodpecker-pipeline-number' + }); + }); + + it('has the correct properties', () => { + expect(env).toHaveProperty('ci', 'woodpecker'); + expect(env).toHaveProperty('commit', 'woodpecker-commit-sha'); + expect(env).toHaveProperty('branch', 'woodpecker-branch'); + expect(env).toHaveProperty('target.commit', null); + expect(env).toHaveProperty('target.branch', null); + expect(env).toHaveProperty('pullRequest', null); + expect(env).toHaveProperty('parallel.nonce', 'woodpecker-pipeline-number'); + expect(env).toHaveProperty('parallel.total', -1); + }); + + it('has the correct properties for PR builds', () => { + env = new PercyEnv({ + ...env.vars, + CI_PIPELINE_EVENT: 'pull_request', + CI_COMMIT_PULL_REQUEST: '42' + }); + expect(env).toHaveProperty('pullRequest', '42'); + }); + + it('ignores CI_COMMIT_PULL_REQUEST on non-pull_request events', () => { + env = new PercyEnv({ + ...env.vars, + CI_PIPELINE_EVENT: 'push', + CI_COMMIT_PULL_REQUEST: '42' + }); + expect(env).toHaveProperty('pullRequest', null); + }); + + it('wins over Drone when Drone-compat vars are also set', () => { + env = new PercyEnv({ + ...env.vars, + DRONE: 'true', + DRONE_COMMIT: 'drone-commit' + }); + expect(env).toHaveProperty('ci', 'woodpecker'); + expect(env).toHaveProperty('commit', 'woodpecker-commit-sha'); + }); + + 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'); + }); +});