From b87ac4a7812fc8851be0601e7fca0c16c8ecb656 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 10 Nov 2025 19:58:02 -0800 Subject: [PATCH] Update ci_info with more CI detection (#23708) Fixes ENG-21481 Updates ci_info to include more CIs. It makes it codegen the ci detection based on the json from the ci-info package. Also it supports setting CI=true to force ci detected. --------- Co-authored-by: pfg Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- build.zig | 7 + cmake/targets/BuildBun.cmake | 20 ++ src/bun.js/test/ScopeFunctions.zig | 2 +- src/bun.js/test/expect.zig | 2 +- src/bun.js/test/jest.zig | 2 +- src/bun.js/test/snapshot.zig | 2 +- src/bun.zig | 2 +- src/ci_info.zig | 431 ++------------------------- src/cli/publish_command.zig | 20 +- src/codegen/ci_info.ts | 454 +++++++++++++++++++++++++++++ src/env_var.zig | 1 - src/install/npm.zig | 11 +- test/cli/env/ci-info.fixture.ts | 5 + test/cli/env/ci-info.test.ts | 48 +++ 14 files changed, 562 insertions(+), 445 deletions(-) create mode 100644 src/codegen/ci_info.ts create mode 100644 test/cli/env/ci-info.fixture.ts create mode 100644 test/cli/env/ci-info.test.ts diff --git a/build.zig b/build.zig index e9c5fe8db1..ba5eb4a1c2 100644 --- a/build.zig +++ b/build.zig @@ -782,6 +782,13 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { mod.addImport("cpp", cppImport); cppImport.addImport("bun", mod); } + { + const ciInfoImport = b.createModule(.{ + .root_source_file = (std.Build.LazyPath{ .cwd_relative = opts.codegen_path }).path(b, "ci_info.zig"), + }); + mod.addImport("ci_info", ciInfoImport); + ciInfoImport.addImport("bun", mod); + } inline for (.{ .{ .import = "completions-bash", .file = b.path("completions/bun.bash") }, .{ .import = "completions-zsh", .file = b.path("completions/bun.zsh") }, diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 6155f10c7b..43b061846b 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -317,6 +317,10 @@ set(BUN_CPP_OUTPUTS ${CODEGEN_PATH}/cpp.zig ) +set(BUN_CI_INFO_OUTPUTS + ${CODEGEN_PATH}/ci_info.zig +) + register_command( TARGET bun-cppbind @@ -334,6 +338,21 @@ register_command( ${BUN_CPP_OUTPUTS} ) +register_command( + TARGET + bun-ci-info + COMMENT + "Generating CI info" + COMMAND + ${BUN_EXECUTABLE} + ${CWD}/src/codegen/ci_info.ts + ${CODEGEN_PATH}/ci_info.zig + SOURCES + ${BUN_JAVASCRIPT_CODEGEN_SOURCES} + OUTPUTS + ${BUN_CI_INFO_OUTPUTS} +) + register_command( TARGET bun-js-modules @@ -612,6 +631,7 @@ set(BUN_ZIG_GENERATED_SOURCES ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_CPP_OUTPUTS} + ${BUN_CI_INFO_OUTPUTS} ${BUN_BINDGENV2_ZIG_OUTPUTS} ) diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig index d5f4fa5e29..ba08f8b1c9 100644 --- a/src/bun.js/test/ScopeFunctions.zig +++ b/src/bun.js/test/ScopeFunctions.zig @@ -278,7 +278,7 @@ fn genericExtend(this: *ScopeFunctions, globalThis: *JSGlobalObject, cfg: bun_te } fn errorInCI(globalThis: *jsc.JSGlobalObject, signature: []const u8) bun.JSError!void { - if (bun.detectCI()) |_| { + if (bun.ci.isCI()) { return globalThis.throwPretty("{s} is disabled in CI environments to prevent accidentally skipping tests. To override, set the environment variable CI=false.", .{signature}); } } diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index fd18757855..ae4bb1f3f6 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -744,7 +744,7 @@ pub const Expect = struct { } if (needs_write) { - if (bun.detectCI()) |_| { + if (bun.ci.isCI()) { if (!update) { const signature = comptime getSignature(fn_name, "", false); // Only creating new snapshots can reach here (updating with mismatches errors earlier with diff) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 182bf6ed1e..07144dbf5c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -501,7 +501,7 @@ pub fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObj } pub fn errorInCI(globalObject: *jsc.JSGlobalObject, message: []const u8) bun.JSError!void { - if (bun.detectCI()) |_| { + if (bun.ci.isCI()) { return globalObject.throwPretty("{s}\nTo override, set the environment variable CI=false.", .{message}); } } diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 3762c72943..11277e710e 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -86,7 +86,7 @@ pub const Snapshots = struct { // doesn't exist. append to file bytes and add to hashmap. // Prevent snapshot creation in CI environments unless --update-snapshots is used - if (bun.detectCI()) |_| { + if (bun.ci.isCI()) { if (!this.update_snapshots) { // Store the snapshot name for error reporting if (this.last_error_snapshot_name) |old_name| { diff --git a/src/bun.zig b/src/bun.zig index 05bd5746bc..8697e07a8c 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -174,7 +174,7 @@ pub const JSTerminated = error{ pub const JSOOM = OOM || JSError; -pub const detectCI = @import("./ci_info.zig").detectCI; +pub const ci = @import("./ci_info.zig"); /// Cross-platform system APIs pub const sys = @import("./sys.zig"); diff --git a/src/ci_info.zig b/src/ci_info.zig index 00a1f7099a..76a02ad894 100644 --- a/src/ci_info.zig +++ b/src/ci_info.zig @@ -1,422 +1,27 @@ // A modified port of ci-info@4.0.0 (https://github.com/watson/ci-info) // Only gets the CI name, `isPR` is not implemented. +// Main implementation is in src/codegen/ci_info.ts -// Names are changed to match what `npm publish` uses -// https://github.com/npm/cli/blob/63d6a732c3c0e9c19fd4d147eaa5cc27c29b168d/workspaces/config/lib/definitions/definitions.js#L2129 -// `name.toLowerCase().split(' ').join('-')` +var detectCIOnce = bun.once(detectUncached); +var isCIOnce = bun.once(isCIUncached); -var ci_name: ?[]const u8 = null; - -pub fn detectCI() ?[]const u8 { - const ci = ci_name orelse ci_name: { - CI.once.call(); - break :ci_name ci_name.?; - }; - - return if (ci.len == 0) null else ci; +/// returns true if the current process is running in a CI environment +pub fn isCI() bool { + return isCIOnce.call(.{}); } -const CI = enum { - @"agola-ci", - appcircle, - appveyor, - @"aws-codebuild", - @"azure-pipelines", - bamboo, - @"bitbucket-pipelines", - bitrise, - buddy, - buildkite, - circleci, - @"cirrus-ci", - codefresh, - codemagic, - codeship, - drone, - dsari, - earthly, - @"expo-application-services", - gerrit, - @"gitea-actions", - @"github-actions", - @"gitlab-ci", - gocd, - @"google-cloud-build", - @"harness-ci", - // heroku, - hudson, - jenkins, - layerci, - @"magnum-ci", - @"netlify-ci", - nevercode, - prow, - releasehub, - render, - @"sail-ci", - screwdriver, - semaphore, - sourcehut, - @"strider-cd", - taskcluster, - teamcity, - @"travis-ci", - vela, - vercel, - @"visual-studio-app-center", - woodpecker, - @"xcode-cloud", - @"xcode-server", +/// returns the CI name, or null if the CI name could not be determined. note that this can be null even if `isCI` is true. +pub fn detectCIName() ?[]const u8 { + return detectCIOnce.call(.{}); +} - pub var once = std.once(struct { - pub fn once() void { - var name: []const u8 = ""; - defer ci_name = name; - - if (bun.env_var.CI.get()) |ci| { - if (!ci) { - return; - } - } - - // Special case Heroku - if (bun.env_var.NODE.get()) |node| { - if (strings.containsComptime(node, "/app/.heroku/node/bin/node")) { - name = "heroku"; - return; - } - } - - ci: for (CI.array.values, 0..) |item, i| { - const any, const pairs = item; - - pairs: for (pairs) |pair| { - const key, const value = pair; - - if (bun.getenvZ(key)) |env| { - if (value.len == 0 or bun.strings.eqlLong(env, value, true)) { - if (!any) continue :pairs; - - name = @tagName(Array.Indexer.keyForIndex(i)); - return; - } - } - - if (!any) continue :ci; - } - - if (!any) { - name = @tagName(Array.Indexer.keyForIndex(i)); - return; - } - } - } - }.once); - - pub const Array = std.EnumArray(CI, struct { bool, []const [2][:0]const u8 }); - - pub const array = Array.init(.{ - .@"agola-ci" = .{ - false, - &.{ - .{ "AGOLA_GIT_REF", "" }, - }, - }, - .appcircle = .{ - false, - &.{ - .{ "AC_APPCIRCLE", "" }, - }, - }, - .appveyor = .{ - false, - &.{ - .{ "APPVEYOR", "" }, - }, - }, - .@"aws-codebuild" = .{ - false, - &.{ - .{ "CODEBUILD_BUILD_ARN", "" }, - }, - }, - .@"azure-pipelines" = .{ - false, - &.{ - .{ "TF_BUILD", "" }, - }, - }, - .bamboo = .{ - false, - &.{ - .{ "bamboo_planKey", "" }, - }, - }, - .@"bitbucket-pipelines" = .{ - false, - &.{ - .{ "BITBUCKET_COMMIT", "" }, - }, - }, - .bitrise = .{ - false, - &.{ - .{ "BITRISE_IO", "" }, - }, - }, - .buddy = .{ - false, - &.{ - .{ "BUDDY_WORKSPACE_ID", "" }, - }, - }, - .buildkite = .{ - false, - &.{ - .{ "BUILDKITE", "" }, - }, - }, - .circleci = .{ - false, - &.{ - .{ "CIRCLECI", "" }, - }, - }, - .@"cirrus-ci" = .{ - false, - &.{ - .{ "CIRRUS_CI", "" }, - }, - }, - .codefresh = .{ - false, - &.{ - .{ "CF_BUILD_ID", "" }, - }, - }, - .codemagic = .{ - false, - &.{ - .{ "CM_BUILD_ID", "" }, - }, - }, - .codeship = .{ - false, - &.{ - .{ "CI_NAME", "codeship" }, - }, - }, - .drone = .{ - false, - &.{ - .{ "DRONE", "" }, - }, - }, - .dsari = .{ - false, - &.{ - .{ "DSARI", "" }, - }, - }, - .earthly = .{ - false, - &.{ - .{ "EARTHLY_CI", "" }, - }, - }, - .@"expo-application-services" = .{ - false, - &.{ - .{ "EAS_BUILD", "" }, - }, - }, - .gerrit = .{ - false, - &.{ - .{ "GERRIT_PROJECT", "" }, - }, - }, - .@"gitea-actions" = .{ - false, - &.{ - .{ "GITEA_ACTIONS", "" }, - }, - }, - .@"github-actions" = .{ - false, - &.{ - .{ "GITHUB_ACTIONS", "" }, - }, - }, - .@"gitlab-ci" = .{ - false, - &.{ - .{ "GITLAB_CI", "" }, - }, - }, - .gocd = .{ - false, - &.{ - .{ "GO_PIPELINE_LABEL", "" }, - }, - }, - .@"google-cloud-build" = .{ - false, - &.{ - .{ "BUILDER_OUTPUT", "" }, - }, - }, - .@"harness-ci" = .{ - false, - &.{ - .{ "HARNESS_BUILD_ID", "" }, - }, - }, - .hudson = .{ - false, - &.{ - .{ "HUDSON_URL", "" }, - }, - }, - .jenkins = .{ - false, - &.{ - .{ "JENKINS_URL", "" }, - .{ "BUILD_ID", "" }, - }, - }, - .layerci = .{ - false, - &.{ - .{ "LAYERCI", "" }, - }, - }, - .@"magnum-ci" = .{ - false, - &.{ - .{ "MAGNUM", "" }, - }, - }, - .@"netlify-ci" = .{ - false, - &.{ - .{ "NETLIFY", "" }, - }, - }, - .nevercode = .{ - false, - &.{ - .{ "NEVERCODE", "" }, - }, - }, - .prow = .{ - false, - &.{ - .{ "PROW_JOB_ID", "" }, - }, - }, - .releasehub = .{ - false, - &.{ - .{ "RELEASE_BUILD_ID", "" }, - }, - }, - .render = .{ - false, - &.{ - .{ "RENDER", "" }, - }, - }, - .@"sail-ci" = .{ - false, - &.{ - .{ "SAILCI", "" }, - }, - }, - .screwdriver = .{ - false, - &.{ - .{ "SCREWDRIVER", "" }, - }, - }, - .semaphore = .{ - false, - &.{ - .{ "SEMAPHORE", "" }, - }, - }, - .sourcehut = .{ - false, - &.{ - .{ "CI_NAME", "sourcehut" }, - }, - }, - .@"strider-cd" = .{ - false, - &.{ - .{ "STRIDER", "" }, - }, - }, - .taskcluster = .{ - false, - &.{ - .{ "TASK_ID", "" }, - .{ "RUN_ID", "" }, - }, - }, - .teamcity = .{ - false, - &.{ - .{ "TEAMCITY_VERSION", "" }, - }, - }, - .@"travis-ci" = .{ - false, - &.{ - .{ "TRAVIS", "" }, - }, - }, - .vela = .{ - false, - &.{ - .{ "VELA", "" }, - }, - }, - .vercel = .{ - true, - &.{ - .{ "NOW_BUILDER", "" }, - .{ "VERCEL", "" }, - }, - }, - .@"visual-studio-app-center" = .{ - false, - &.{ - .{ "APPCENTER_BUILD_ID", "" }, - }, - }, - .woodpecker = .{ - false, - &.{ - .{ "CI", "woodpecker" }, - }, - }, - .@"xcode-cloud" = .{ - false, - &.{ - .{ "CI_XCODE_PROJECT", "" }, - }, - }, - .@"xcode-server" = .{ - false, - &.{ - .{ "XCS", "" }, - }, - }, - }); -}; - -const std = @import("std"); +fn isCIUncached() bool { + return bun.env_var.CI.get() orelse generated.isCIUncachedGenerated() or detectCIName() != null; +} +fn detectUncached() ?[]const u8 { + if (bun.env_var.CI.get() == false) return null; + return generated.detectUncachedGenerated(); +} const bun = @import("bun"); -const strings = bun.strings; +const generated = @import("ci_info"); diff --git a/src/cli/publish_command.zig b/src/cli/publish_command.zig index 222444ef80..ad3b092b4e 100644 --- a/src/cli/publish_command.zig +++ b/src/cli/publish_command.zig @@ -1272,7 +1272,7 @@ pub const PublishCommand = struct { if (auth_type) |auth| @tagName(auth) else "web" else "legacy"; - const ci_name = bun.detectCI(); + const ci_name = bun.ci.detectCIName(); { headers.count("accept", "*/*"); @@ -1299,14 +1299,7 @@ pub const PublishCommand = struct { } headers.count("npm-command", "publish"); - try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ - Global.user_agent, - Global.os_name, - Global.arch_name, - uses_workspaces, - if (ci_name != null) " ci/" else "", - ci_name orelse "", - }); + try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ Global.user_agent, Global.os_name, Global.arch_name, uses_workspaces, if (ci_name != null) " ci/" else "", ci_name orelse "" }); // headers.count("user-agent", "npm/10.8.3 node/v24.3.0 darwin arm64 workspaces/false"); headers.count("user-agent", print_buf.items); print_buf.clearRetainingCapacity(); @@ -1348,14 +1341,7 @@ pub const PublishCommand = struct { } headers.append("npm-command", "publish"); - try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ - Global.user_agent, - Global.os_name, - Global.arch_name, - uses_workspaces, - if (ci_name != null) " ci/" else "", - ci_name orelse "", - }); + try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ Global.user_agent, Global.os_name, Global.arch_name, uses_workspaces, if (ci_name != null) " ci/" else "", ci_name orelse "" }); // headers.append("user-agent", "npm/10.8.3 node/v24.3.0 darwin arm64 workspaces/false"); headers.append("user-agent", print_buf.items); print_buf.clearRetainingCapacity(); diff --git a/src/codegen/ci_info.ts b/src/codegen/ci_info.ts new file mode 100644 index 0000000000..4797ecd1a5 --- /dev/null +++ b/src/codegen/ci_info.ts @@ -0,0 +1,454 @@ +type Vendor = { + name: string; + constant: string; + env: EnvMatch; + pr?: unknown; +}; +type EnvMatch = + | string + | EnvMatch[] + | { + env: string; + includes: string; + } + | { + any: string[]; + } + | Record; + +// The vendors list is copied from https://github.com/watson/ci-info/blob/master/vendors.json +// The extras list is copied and edited from https://github.com/watson/ci-info/blob/master/index.js line `exports.isCI = !!(...)` +// To update, copy the JSON again. +/* +The MIT License (MIT) + +Copyright (c) 2016 Thomas Watson Steen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +const extras: string[] = [ + "BUILD_ID", // Jenkins, Cloudbees + "BUILD_NUMBER", // Jenkins, TeamCity + "CI", // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari, Cloudflare Pages/Workers + "CI_APP_ID", // Appflow + "CI_BUILD_ID", // Appflow + "CI_BUILD_NUMBER", // Appflow + "CI_NAME", // Codeship and others + "CONTINUOUS_INTEGRATION", // Travis CI, Cirrus CI + "RUN_ID", // TaskCluster, dsari +]; +const vendors: Vendor[] = [ + { + "name": "Agola CI", + "constant": "AGOLA", + "env": "AGOLA_GIT_REF", + "pr": "AGOLA_PULL_REQUEST_ID", + }, + { + "name": "Appcircle", + "constant": "APPCIRCLE", + "env": "AC_APPCIRCLE", + "pr": { + "env": "AC_GIT_PR", + "ne": "false", + }, + }, + { + "name": "AppVeyor", + "constant": "APPVEYOR", + "env": "APPVEYOR", + "pr": "APPVEYOR_PULL_REQUEST_NUMBER", + }, + { + "name": "AWS CodeBuild", + "constant": "CODEBUILD", + "env": "CODEBUILD_BUILD_ARN", + "pr": { + "env": "CODEBUILD_WEBHOOK_EVENT", + "any": ["PULL_REQUEST_CREATED", "PULL_REQUEST_UPDATED", "PULL_REQUEST_REOPENED"], + }, + }, + { + "name": "Azure Pipelines", + "constant": "AZURE_PIPELINES", + "env": "TF_BUILD", + "pr": { + "BUILD_REASON": "PullRequest", + }, + }, + { + "name": "Bamboo", + "constant": "BAMBOO", + "env": "bamboo_planKey", + }, + { + "name": "Bitbucket Pipelines", + "constant": "BITBUCKET", + "env": "BITBUCKET_COMMIT", + "pr": "BITBUCKET_PR_ID", + }, + { + "name": "Bitrise", + "constant": "BITRISE", + "env": "BITRISE_IO", + "pr": "BITRISE_PULL_REQUEST", + }, + { + "name": "Buddy", + "constant": "BUDDY", + "env": "BUDDY_WORKSPACE_ID", + "pr": "BUDDY_EXECUTION_PULL_REQUEST_ID", + }, + { + "name": "Buildkite", + "constant": "BUILDKITE", + "env": "BUILDKITE", + "pr": { + "env": "BUILDKITE_PULL_REQUEST", + "ne": "false", + }, + }, + { + "name": "CircleCI", + "constant": "CIRCLE", + "env": "CIRCLECI", + "pr": "CIRCLE_PULL_REQUEST", + }, + { + "name": "Cirrus CI", + "constant": "CIRRUS", + "env": "CIRRUS_CI", + "pr": "CIRRUS_PR", + }, + { + "name": "Cloudflare Pages", + "constant": "CLOUDFLARE_PAGES", + "env": "CF_PAGES", + }, + { + "name": "Cloudflare Workers", + "constant": "CLOUDFLARE_WORKERS", + "env": "WORKERS_CI", + }, + { + "name": "Codefresh", + "constant": "CODEFRESH", + "env": "CF_BUILD_ID", + "pr": { + "any": ["CF_PULL_REQUEST_NUMBER", "CF_PULL_REQUEST_ID"], + }, + }, + { + "name": "Codemagic", + "constant": "CODEMAGIC", + "env": "CM_BUILD_ID", + "pr": "CM_PULL_REQUEST", + }, + { + "name": "Codeship", + "constant": "CODESHIP", + "env": { + "CI_NAME": "codeship", + }, + }, + { + "name": "Drone", + "constant": "DRONE", + "env": "DRONE", + "pr": { + "DRONE_BUILD_EVENT": "pull_request", + }, + }, + { + "name": "dsari", + "constant": "DSARI", + "env": "DSARI", + }, + { + "name": "Earthly", + "constant": "EARTHLY", + "env": "EARTHLY_CI", + }, + { + "name": "Expo Application Services", + "constant": "EAS", + "env": "EAS_BUILD", + }, + { + "name": "Gerrit", + "constant": "GERRIT", + "env": "GERRIT_PROJECT", + }, + { + "name": "Gitea Actions", + "constant": "GITEA_ACTIONS", + "env": "GITEA_ACTIONS", + }, + { + "name": "GitHub Actions", + "constant": "GITHUB_ACTIONS", + "env": "GITHUB_ACTIONS", + "pr": { + "GITHUB_EVENT_NAME": "pull_request", + }, + }, + { + "name": "GitLab CI", + "constant": "GITLAB", + "env": "GITLAB_CI", + "pr": "CI_MERGE_REQUEST_ID", + }, + { + "name": "GoCD", + "constant": "GOCD", + "env": "GO_PIPELINE_LABEL", + }, + { + "name": "Google Cloud Build", + "constant": "GOOGLE_CLOUD_BUILD", + "env": "BUILDER_OUTPUT", + }, + { + "name": "Harness CI", + "constant": "HARNESS", + "env": "HARNESS_BUILD_ID", + }, + { + "name": "Heroku", + "constant": "HEROKU", + "env": { + "env": "NODE", + "includes": "/app/.heroku/node/bin/node", + }, + }, + { + "name": "Hudson", + "constant": "HUDSON", + "env": "HUDSON_URL", + }, + { + "name": "Jenkins", + "constant": "JENKINS", + "env": ["JENKINS_URL", "BUILD_ID"], + "pr": { + "any": ["ghprbPullId", "CHANGE_ID"], + }, + }, + { + "name": "LayerCI", + "constant": "LAYERCI", + "env": "LAYERCI", + "pr": "LAYERCI_PULL_REQUEST", + }, + { + "name": "Magnum CI", + "constant": "MAGNUM", + "env": "MAGNUM", + }, + { + "name": "Netlify CI", + "constant": "NETLIFY", + "env": "NETLIFY", + "pr": { + "env": "PULL_REQUEST", + "ne": "false", + }, + }, + { + "name": "Nevercode", + "constant": "NEVERCODE", + "env": "NEVERCODE", + "pr": { + "env": "NEVERCODE_PULL_REQUEST", + "ne": "false", + }, + }, + { + "name": "Prow", + "constant": "PROW", + "env": "PROW_JOB_ID", + }, + { + "name": "ReleaseHub", + "constant": "RELEASEHUB", + "env": "RELEASE_BUILD_ID", + }, + { + "name": "Render", + "constant": "RENDER", + "env": "RENDER", + "pr": { + "IS_PULL_REQUEST": "true", + }, + }, + { + "name": "Sail CI", + "constant": "SAIL", + "env": "SAILCI", + "pr": "SAIL_PULL_REQUEST_NUMBER", + }, + { + "name": "Screwdriver", + "constant": "SCREWDRIVER", + "env": "SCREWDRIVER", + "pr": { + "env": "SD_PULL_REQUEST", + "ne": "false", + }, + }, + { + "name": "Semaphore", + "constant": "SEMAPHORE", + "env": "SEMAPHORE", + "pr": "PULL_REQUEST_NUMBER", + }, + { + "name": "Sourcehut", + "constant": "SOURCEHUT", + "env": { + "CI_NAME": "sourcehut", + }, + }, + { + "name": "Strider CD", + "constant": "STRIDER", + "env": "STRIDER", + }, + { + "name": "TaskCluster", + "constant": "TASKCLUSTER", + "env": ["TASK_ID", "RUN_ID"], + }, + { + "name": "TeamCity", + "constant": "TEAMCITY", + "env": "TEAMCITY_VERSION", + }, + { + "name": "Travis CI", + "constant": "TRAVIS", + "env": "TRAVIS", + "pr": { + "env": "TRAVIS_PULL_REQUEST", + "ne": "false", + }, + }, + { + "name": "Vela", + "constant": "VELA", + "env": "VELA", + "pr": { + "VELA_PULL_REQUEST": "1", + }, + }, + { + "name": "Vercel", + "constant": "VERCEL", + "env": { + "any": ["NOW_BUILDER", "VERCEL"], + }, + "pr": "VERCEL_GIT_PULL_REQUEST_ID", + }, + { + "name": "Visual Studio App Center", + "constant": "APPCENTER", + "env": "APPCENTER_BUILD_ID", + }, + { + "name": "Woodpecker", + "constant": "WOODPECKER", + "env": { + "CI": "woodpecker", + }, + "pr": { + "CI_BUILD_EVENT": "pull_request", + }, + }, + { + "name": "Xcode Cloud", + "constant": "XCODE_CLOUD", + "env": "CI_XCODE_PROJECT", + "pr": "CI_PULL_REQUEST_NUMBER", + }, + { + "name": "Xcode Server", + "constant": "XCODE_SERVER", + "env": "XCS", + }, +]; + +function genEnvCondition(env: EnvMatch): string { + if (typeof env === "string") { + return `bun.getenvZ(${JSON.stringify(env)}) != null`; + } else if (Array.isArray(env)) { + return env + .map(itm => { + const res = genEnvCondition(itm); + if (res.includes(" or ")) return `(${res})`; + return res; + }) + .join(" and "); + } else if (typeof env === "object") { + if ("env" in env) { + return `bun.strings.containsComptime(bun.getenvZ(${JSON.stringify(env.env)}) orelse "", ${JSON.stringify(env.includes)})`; + } else if ("any" in env) { + return (env.any as string[]).map(genEnvCondition).join(" or "); + } else { + return Object.entries(env) + .map( + ([key, value]) => + `bun.strings.eqlComptime(bun.getenvZ(${JSON.stringify(key)}) orelse "", ${JSON.stringify(value)})`, + ) + .join(" and "); + } + } else throw new Error("Not implemented"); +} + +let codegen: string[] = []; +codegen.push(`/// Generated by src/codegen/ci_info.ts\n`); +codegen.push(`pub fn isCIUncachedGenerated() bool {\n`); +for (const extra of extras) { + codegen.push(` if (${genEnvCondition(extra)}) return true;\n`); +} +codegen.push(` return false;\n`); +codegen.push(`}\n`); +codegen.push(`\n`); +codegen.push(`/// Generated by src/codegen/ci_info.ts\n`); +codegen.push(`pub fn detectUncachedGenerated() ?[]const u8 {\n`); +for (const vendor of vendors) { + // Names are changed to match what `npm publish` uses + // https://github.com/npm/cli/blob/63d6a732c3c0e9c19fd4d147eaa5cc27c29b168d/workspaces/config/lib/definitions/definitions.js#L2129 + const npm_style_name = vendor.name.toLowerCase().replaceAll(" ", "-"); + codegen.push(` if (${genEnvCondition(vendor.env)}) return ${JSON.stringify(npm_style_name)};\n`); +} +codegen.push(` return null;\n`); +codegen.push(`}\n`); +codegen.push(`\n`); +codegen.push(`const bun = @import("bun");\n`); +const result = codegen.join(""); + +if (import.meta.main) { + const args = process.argv.slice(2); + const out = args[0]; + if (out) { + await Bun.write(out, result); + } else { + console.log(result); + } +} diff --git a/src/env_var.zig b/src/env_var.zig index f99f7f7252..13fea31056 100644 --- a/src/env_var.zig +++ b/src/env_var.zig @@ -97,7 +97,6 @@ pub const JENKINS_URL = New(kind.string, "JENKINS_URL", .{}); /// `MIMALLOC_VERBOSE`, documented here: https://microsoft.github.io/mimalloc/environment.html pub const MI_VERBOSE = New(kind.boolean, "MI_VERBOSE", .{ .default = false }); pub const NO_COLOR = New(kind.boolean, "NO_COLOR", .{ .default = false }); -pub const NODE = New(kind.string, "NODE", .{}); pub const NODE_CHANNEL_FD = New(kind.string, "NODE_CHANNEL_FD", .{}); pub const NODE_PRESERVE_SYMLINKS_MAIN = New(kind.boolean, "NODE_PRESERVE_SYMLINKS_MAIN", .{ .default = false }); pub const NODE_USE_SYSTEM_CA = New(kind.boolean, "NODE_USE_SYSTEM_CA", .{ .default = false }); diff --git a/src/install/npm.zig b/src/install/npm.zig index 1c903e5797..e2a26f8040 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -20,7 +20,7 @@ pub fn whoami(allocator: std.mem.Allocator, manager: *PackageManager) WhoamiErro } const auth_type = if (manager.options.publish_config.auth_type) |auth_type| @tagName(auth_type) else "web"; - const ci_name = bun.detectCI(); + const ci_name = bun.ci.detectCIName(); var print_buf = std.array_list.Managed(u8).init(allocator); defer print_buf.deinit(); @@ -69,14 +69,7 @@ pub fn whoami(allocator: std.mem.Allocator, manager: *PackageManager) WhoamiErro headers.append("npm-auth-type", auth_type); headers.append("npm-command", "whoami"); - try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ - Global.user_agent, - Global.os_name, - Global.arch_name, - false, - if (ci_name != null) " ci/" else "", - ci_name orelse "", - }); + try print_writer.print("{s} {s} {s} workspaces/{}{s}{s}", .{ Global.user_agent, Global.os_name, Global.arch_name, false, if (ci_name != null) " ci/" else "", ci_name orelse "" }); headers.append("user-agent", print_buf.items); print_buf.clearRetainingCapacity(); diff --git a/test/cli/env/ci-info.fixture.ts b/test/cli/env/ci-info.fixture.ts new file mode 100644 index 0000000000..b3e2dcf24a --- /dev/null +++ b/test/cli/env/ci-info.fixture.ts @@ -0,0 +1,5 @@ +import { test, expect } from "bun:test"; + +test.only("only", () => { + expect(1 + 1).toBe(2); +}); diff --git a/test/cli/env/ci-info.test.ts b/test/cli/env/ci-info.test.ts new file mode 100644 index 0000000000..2f9c2681ec --- /dev/null +++ b/test/cli/env/ci-info.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "../../harness"; + +const cleanEnv = { ...bunEnv }; +delete cleanEnv.GITHUB_ACTIONS; +delete cleanEnv.GITLAB_CI; +delete cleanEnv.CIRCLECI; +delete cleanEnv.TRAVIS; +delete cleanEnv.BUILDKITE; +delete cleanEnv.JENKINS_URL; +delete cleanEnv.BUILD_ID; +delete cleanEnv.CI; + +async function performTest(env: Record, result: "deny-only" | "allow-only") { + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "./ci-info.fixture.ts"], + env, + cwd: import.meta.dir, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // test.only should work (not throw) when CI=false + if (result === "deny-only") { + expect(stderr).toContain(".only is disabled in CI environments"); + expect(exitCode).toBe(1); + } else { + expect(stderr).toContain("1 pass"); + expect(exitCode).toBe(0); + } +} + +describe("CI detection", () => { + test("Without CI env vars, test.only should work", async () => { + await performTest(cleanEnv, "allow-only"); + }); + test("CI=false disables CI detection even with GITHUB_ACTIONS=true", async () => { + await performTest({ ...cleanEnv, CI: "false", GITHUB_ACTIONS: "true" }, "allow-only"); + }); + test("CI=true enables CI detection even with no CI env vars", async () => { + await performTest({ ...cleanEnv, CI: "true" }, "deny-only"); + }); + test("CI=true enables CI detection with GITHUB_ACTIONS=true", async () => { + await performTest({ ...cleanEnv, CI: "true", GITHUB_ACTIONS: "true" }, "deny-only"); + }); +});