Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/sixty-mugs-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@varlock/ci-env-info": patch
"varlock": patch
---

new auto-inferred VARLOCK_ENV from ci info (uses new ci-env-info package)
35 changes: 35 additions & 0 deletions .cursor/rules/no-js-extension-imports.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: Do not use .js (or .ts) extensions in TypeScript import/export paths
globs: "*.ts"
alwaysApply: false
---
# No .js Extension in TypeScript Imports

<rule>
name: no_js_extension_imports
description: |
This project uses TypeScript with "moduleResolution": "bundler" (or "node16"/"nodenext" in a way that relies on the bundler to resolve modules). Do not add .js or .ts extensions to import or export paths in TypeScript files.

filters:
- type: file_extension
pattern: "\\.ts$"

actions:
- type: suggest
message: |
Do not use .js or .ts in TypeScript import/export paths. We use bundler module resolution, so write:

import { foo } from './bar';
export { foo } from './bar';

Not:

import { foo } from './bar.js';
export { foo } from './bar.js';

This keeps code consistent and avoids confusion between source and output paths.

metadata:
priority: high
version: 1.0
</rule>
13 changes: 13 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions packages/ci-env-info/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# @varlock/ci-env-info

Detect the current CI/deploy platform and expose normalized metadata: repo, branch, PR number, commit SHA, build URL, deployment environment, and more.

This package powers the `VARLOCK_*` builtin variables in [Varlock](https://varlock.dev). It can also be used standalone.

## Usage

```ts
import { getCiEnvFromProcess, getCiEnv } from '@varlock/ci-env-info';

// Use current process.env
const info = getCiEnvFromProcess();
if (info.isCI) {
console.log('Platform:', info.name);
console.log('Repo:', info.fullRepoName);
console.log('Branch:', info.branch);
console.log('Commit:', info.commitShaShort);
console.log('Environment:', info.environment);
}

// Or pass an env record (useful for testing)
const info2 = getCiEnv({ GITHUB_ACTIONS: 'true', GITHUB_REPOSITORY: 'owner/repo' });
```

## API

- **`getCiEnv(env: EnvRecord): CiEnvInfo`** – Returns CI environment info from the given env record.
- **`getCiEnvFromProcess(): CiEnvInfo`** – Convenience wrapper: calls `getCiEnv(process.env)`.
- **`EnvRecord`** – `Record<string, string | undefined>`.
- **`CiEnvInfo`** – `isCI`, `name`, `docsUrl`, `isPR`, `repo`, `fullRepoName`, `branch`, `prNumber`, `commitSha`, `commitShaShort`, `environment`, `runId`, `buildUrl`, `workflowName`, `actor`, `eventName`, `raw`.
- **`DeploymentEnvironment`** – `'development' | 'preview' | 'staging' | 'production' | 'test'`.

## Supported platforms

GitHub Actions, GitLab CI, Vercel, Netlify, Cloudflare Pages, Cloudflare Workers, AWS CodeBuild, Azure Pipelines, Bitbucket Pipelines, Buildkite, CircleCI, Jenkins, Render, Travis CI, and many more.

Platforms are defined in TypeScript with no external dependencies. Detection uses environment variables specific to each platform.

## Learn more

Check out the [Varlock docs](https://varlock.dev) for more about how this fits into env var management.
36 changes: 36 additions & 0 deletions packages/ci-env-info/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@varlock/ci-env-info",
"description": "Detect CI environment and normalize other data like environment, repo, branch, PR, commit, etc.",
"version": "0.0.0",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/dmno-dev/varlock.git",
"directory": "packages/ci-env-info"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"test:ci": "vitest --run"
},
"keywords": ["ci", "environment", "detection", "github-actions", "vercel", "netlify", "env vars"],
"author": "dmno-dev",
"license": "MIT",
"engines": { "node": ">=22" },
"devDependencies": {
"@types/node": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}
151 changes: 151 additions & 0 deletions packages/ci-env-info/src/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { CiEnvInfo, EnvRecord, PlatformDefinition } from './types';
import {
type DeploymentEnvironment, type RepoParts,
mapToDeploymentEnvironment,
parsePrNumber,
parseRepoSlug,
refToBranch,
shortSha,
} from './normalize';
import { PLATFORMS } from './platforms';

const VALID_ENVIRONMENTS: Set<string> = new Set(
['development', 'preview', 'staging', 'production', 'test'],
);

function runDetect(platform: PlatformDefinition, env: EnvRecord): boolean {
const d = platform.detect;
if (typeof d === 'string') return !!env[d];
return d(env);
}

function runExtractor<T>(
platform: PlatformDefinition,
key: keyof PlatformDefinition,
env: EnvRecord,
): T | undefined {
const ext = platform[key];
if (ext === undefined) return undefined;
if (typeof ext === 'function') return (ext as (env: EnvRecord) => T | undefined)(env);
if (key === 'environment' && typeof ext === 'object' && ext !== null && 'var' in ext && 'map' in ext) {
return mapToDeploymentEnvironment(env[ext.var], ext.map) as T;
}
const raw = env[ext as string];
if (raw === undefined || raw === '') return undefined;
switch (key) {
case 'repo':
return parseRepoSlug(raw) as T;
case 'branch': {
const branch = refToBranch(raw);
return (branch ?? raw) as T;
}
case 'prNumber':
return parsePrNumber(raw) as T;
case 'commitSha':
return raw as T;
case 'environment':
return VALID_ENVIRONMENTS.has(raw) ? raw as T : undefined;
default:
return raw as T;
}
}

function buildRaw(
platform: PlatformDefinition,
env: EnvRecord,
): Record<string, string> | undefined {
const keys = [
'repo',
'branch',
'prNumber',
'commitSha',
'environment',
'runId',
'buildUrl',
'workflowName',
'actor',
'eventName',
] as const;
const raw: Record<string, string> = {};
let hasAny = false;
for (const k of keys) {
const ext = platform[k];
if (typeof ext === 'string' && env[ext]) {
raw[ext] = env[ext]!;
hasAny = true;
}
if (k === 'environment' && typeof ext === 'object' && ext !== null && 'var' in ext && env[ext.var]) {
raw[ext.var] = env[ext.var]!;
hasAny = true;
}
}
return hasAny ? raw : undefined;
}

/**
* Compute CiEnvInfo from the given env record. Uses in-package platform definitions.
* If env.CI === 'false', returns isCI: false (escape hatch).
*/
export function getCiEnv(env: EnvRecord): CiEnvInfo {
const e = env;

if (e.CI === 'false') {
return { isCI: false };
}

for (const platform of PLATFORMS) {
if (!runDetect(platform, e)) continue;

const repo = runExtractor<RepoParts>(platform, 'repo', e);
const commitSha = runExtractor<string>(platform, 'commitSha', e);
let isPR: boolean | undefined;
if (platform.isPR !== undefined) {
isPR = typeof platform.isPR === 'string' ? !!e[platform.isPR] : platform.isPR(e);
} else {
isPR = runExtractor<number>(platform, 'prNumber', e) !== undefined;
}
const info: CiEnvInfo = {
isCI: true,
name: platform.name,
docsUrl: platform.docsUrl,
isPR,
repo,
fullRepoName: repo ? `${repo.owner}/${repo.name}` : undefined,
branch: runExtractor<string>(platform, 'branch', e),
prNumber: runExtractor<number>(platform, 'prNumber', e),
commitSha,
commitShaShort: shortSha(commitSha),
environment: runExtractor<DeploymentEnvironment>(platform, 'environment', e),
runId: runExtractor<string>(platform, 'runId', e),
buildUrl: runExtractor<string>(platform, 'buildUrl', e),
workflowName: runExtractor<string>(platform, 'workflowName', e),
actor: runExtractor<string>(platform, 'actor', e),
eventName: runExtractor<string>(platform, 'eventName', e),
raw: buildRaw(platform, e),
};
return info;
}

// Generic CI (e.g. CI=true but no vendor matched)
const isCI = e.CI === 'true'
|| !!e.BUILD_ID
|| !!e.BUILD_NUMBER
|| !!e.CI_APP_ID
|| !!e.CI_BUILD_ID
|| !!e.CI_BUILD_NUMBER
|| !!e.CI_NAME
|| !!e.CONTINUOUS_INTEGRATION
|| !!e.RUN_ID;

return {
isCI: !!isCI,
};
}

/**
* Convenience: compute CiEnvInfo from the current process.env.
* Equivalent to getCiEnv(process.env).
*/
export function getCiEnvFromProcess(): CiEnvInfo {
return getCiEnv(process.env);
}
3 changes: 3 additions & 0 deletions packages/ci-env-info/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { getCiEnv, getCiEnvFromProcess } from './detect';
export type { CiEnvInfo, EnvRecord } from './types';
export type { DeploymentEnvironment } from './normalize';
Loading
Loading