@netlify/build 29.36.6 → 29.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/core/build.js CHANGED
@@ -350,7 +350,13 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, configO
350
350
  // the build is finished. The exception is when running in the dev timeline
351
351
  // since those are long-running events by nature.
352
352
  if (timeline !== 'dev') {
353
- stopPlugins(childProcesses);
353
+ await stopPlugins({
354
+ childProcesses,
355
+ pluginOptions: pluginsOptionsA,
356
+ netlifyConfig,
357
+ logs,
358
+ verbose,
359
+ });
354
360
  }
355
361
  }
356
362
  };
@@ -1,6 +1,6 @@
1
1
  import { setInspectColors } from '../../log/colors.js';
2
- import { sendEventToParent, getEventsFromParent } from '../ipc.js';
3
- import { handleProcessErrors, handleError } from './error.js';
2
+ import { getEventsFromParent, sendEventToParent } from '../ipc.js';
3
+ import { handleError, handleProcessErrors } from './error.js';
4
4
  import { load } from './load.js';
5
5
  import { run } from './run.js';
6
6
  // Boot plugin child process.
@@ -33,5 +33,20 @@ const handleEvent = async function ({ callId, eventName, payload, state, state:
33
33
  await handleError(error, verbose);
34
34
  }
35
35
  };
36
- const EVENTS = { load, run };
36
+ const EVENTS = {
37
+ load,
38
+ run,
39
+ // async shutdown hook to stop tracing reliably
40
+ shutdown: async () => {
41
+ try {
42
+ const { stopTracing } = await import('@netlify/opentelemetry-sdk-setup');
43
+ await stopTracing();
44
+ }
45
+ catch {
46
+ // noop as the opentelemetry-sdk-setup is an optional dependency
47
+ // and might not be present in the CLI
48
+ }
49
+ return { context: {} };
50
+ },
51
+ };
37
52
  bootPlugin();
@@ -1,10 +1,11 @@
1
- export function run({ event, error, constants, envChanges, featureFlags, netlifyConfig }: {
1
+ export function run({ event, error, constants, envChanges, featureFlags, netlifyConfig, otelCarrier }: {
2
2
  event: any;
3
3
  error: any;
4
4
  constants: any;
5
5
  envChanges: any;
6
6
  featureFlags: any;
7
7
  netlifyConfig: any;
8
+ otelCarrier: any;
8
9
  }, { methods, inputs, packageJson, verbose }: {
9
10
  methods: any;
10
11
  inputs: any;
@@ -1,30 +1,36 @@
1
+ import { getGlobalContext, setGlobalContext } from '@netlify/opentelemetry-utils';
2
+ import { context, propagation } from '@opentelemetry/api';
1
3
  import { getNewEnvChanges, setEnvChanges } from '../../env/changes.js';
2
- import { logPluginMethodStart, logPluginMethodEnd } from '../../log/messages/ipc.js';
4
+ import { logPluginMethodEnd, logPluginMethodStart } from '../../log/messages/ipc.js';
3
5
  import { cloneNetlifyConfig, getConfigMutations } from './diff.js';
4
6
  import { getSystemLog } from './systemLog.js';
5
7
  import { getUtils } from './utils.js';
6
- // Run a specific plugin event handler
7
- export const run = async function ({ event, error, constants, envChanges, featureFlags, netlifyConfig }, { methods, inputs, packageJson, verbose }) {
8
- const method = methods[event];
9
- const runState = {};
10
- const systemLog = getSystemLog();
11
- const utils = getUtils({ event, constants, runState });
12
- const netlifyConfigCopy = cloneNetlifyConfig(netlifyConfig);
13
- const runOptions = {
14
- utils,
15
- constants,
16
- inputs,
17
- netlifyConfig: netlifyConfigCopy,
18
- packageJson,
19
- error,
20
- featureFlags,
21
- systemLog,
22
- };
23
- const envBefore = setEnvChanges(envChanges);
24
- logPluginMethodStart(verbose);
25
- await method(runOptions);
26
- logPluginMethodEnd(verbose);
27
- const newEnvChanges = getNewEnvChanges(envBefore, netlifyConfig, netlifyConfigCopy);
28
- const configMutations = getConfigMutations(netlifyConfig, netlifyConfigCopy, event);
29
- return { ...runState, newEnvChanges, configMutations };
8
+ /** Run a specific plugin event handler */
9
+ export const run = async function ({ event, error, constants, envChanges, featureFlags, netlifyConfig, otelCarrier }, { methods, inputs, packageJson, verbose }) {
10
+ setGlobalContext(propagation.extract(context.active(), otelCarrier));
11
+ // set the global context for the plugin run
12
+ return context.with(getGlobalContext(), async () => {
13
+ const method = methods[event];
14
+ const runState = {};
15
+ const systemLog = getSystemLog();
16
+ const utils = getUtils({ event, constants, runState });
17
+ const netlifyConfigCopy = cloneNetlifyConfig(netlifyConfig);
18
+ const runOptions = {
19
+ utils,
20
+ constants,
21
+ inputs,
22
+ netlifyConfig: netlifyConfigCopy,
23
+ packageJson,
24
+ error,
25
+ featureFlags,
26
+ systemLog,
27
+ };
28
+ const envBefore = setEnvChanges(envChanges);
29
+ logPluginMethodStart(verbose);
30
+ await method(runOptions);
31
+ logPluginMethodEnd(verbose);
32
+ const newEnvChanges = getNewEnvChanges(envBefore, netlifyConfig, netlifyConfigCopy);
33
+ const configMutations = getConfigMutations(netlifyConfig, netlifyConfigCopy, event);
34
+ return { ...runState, newEnvChanges, configMutations };
35
+ });
30
36
  };
@@ -6,6 +6,7 @@ export type PluginsOptions = {
6
6
  loadedFrom: PluginsLoadedFrom;
7
7
  origin: 'config' | string;
8
8
  inputs: Record<string, any>;
9
+ pluginPackageJson?: Record<string, any>;
9
10
  };
10
11
  /**
11
12
  * Local plugins and `package.json`-installed plugins use user's preferred Node.js version if higher than our minimum
@@ -41,9 +41,9 @@ const addPluginNodeVersion = async function ({ featureFlags, pluginOptions, plug
41
41
  !semver.satisfies(userNodeVersion, UPCOMING_MINIMUM_REQUIRED_NODE_VERSION)) {
42
42
  logWarningSubHeader(logs, `Warning: Starting January 30, 2024 plugin "${packageName}" will be executed with Node.js version 20.`);
43
43
  logWarning(logs, ` We're upgrading our system node version on that day, which means the plugin cannot be executed with your defined Node.js version ${userNodeVersion}.
44
-
44
+
45
45
  Please make sure your plugin supports being run on Node.js 20.
46
-
46
+
47
47
  Read more about our minimum required version in our ${link('forums announcement', 'https://answers.netlify.com/t/build-plugin-update-system-node-js-version-upgrade-to-20/108633')}`);
48
48
  if (pluginPath) {
49
49
  const pluginDir = dirname(pluginPath);
@@ -70,7 +70,7 @@ const addPluginNodeVersion = async function ({ featureFlags, pluginOptions, plug
70
70
  }
71
71
  logWarningSubHeader(logs, `Warning: ${packageName} will be executed with Node.js version ${currentNodeVersion}`);
72
72
  logWarning(logs, ` The plugin cannot be executed with your defined Node.js version ${userNodeVersion}
73
-
73
+
74
74
  Read more about our minimum required version in our ${link('forums announcement', 'https://answers.netlify.com/t/build-plugins-dropping-support-for-node-js-12/79421')}`);
75
75
  return systemNode;
76
76
  };
@@ -1,2 +1,14 @@
1
+ import { ExecaChildProcess } from 'execa';
2
+ import { NetlifyConfig } from '../index.js';
3
+ import { BufferedLogs } from '../log/logger.js';
4
+ import { PluginsOptions } from './node_version.js';
1
5
  export declare const startPlugins: any;
2
- export declare const stopPlugins: (childProcesses: any) => void;
6
+ export declare const stopPlugins: ({ childProcesses, logs, verbose, pluginOptions, netlifyConfig, }: {
7
+ logs: BufferedLogs;
8
+ verbose: boolean;
9
+ childProcesses: {
10
+ childProcess: ExecaChildProcess;
11
+ }[];
12
+ pluginOptions: PluginsOptions[];
13
+ netlifyConfig: NetlifyConfig;
14
+ }) => Promise<void>;
@@ -1,12 +1,16 @@
1
- import { fileURLToPath } from 'url';
1
+ import { createRequire } from 'module';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import { trace } from '@opentelemetry/api';
2
4
  import { execaNode } from 'execa';
5
+ import { gte } from 'semver';
3
6
  import { addErrorInfo } from '../error/info.js';
4
- import { logRuntime, logLoadingPlugins, logOutdatedPlugins, logIncompatiblePlugins, logLoadingIntegration, } from '../log/messages/compatibility.js';
7
+ import { logIncompatiblePlugins, logLoadingIntegration, logLoadingPlugins, logOutdatedPlugins, logRuntime, } from '../log/messages/compatibility.js';
5
8
  import { isTrustedPlugin } from '../steps/plugin.js';
6
9
  import { measureDuration } from '../time/main.js';
7
- import { getEventFromChild } from './ipc.js';
10
+ import { callChild, getEventFromChild } from './ipc.js';
8
11
  import { getSpawnInfo } from './options.js';
9
12
  const CHILD_MAIN_FILE = fileURLToPath(new URL('child/main.js', import.meta.url));
13
+ const require = createRequire(import.meta.url);
10
14
  // Start child processes used by all plugins
11
15
  // We fire plugins through child processes so that:
12
16
  // - each plugin is sandboxed, e.g. cannot access/modify its parent `process`
@@ -21,20 +25,47 @@ const tStartPlugins = async function ({ pluginsOptions, buildDir, childEnv, logs
21
25
  }
22
26
  logOutdatedPlugins(logs, pluginsOptions);
23
27
  logIncompatiblePlugins(logs, pluginsOptions);
24
- const childProcesses = await Promise.all(pluginsOptions.map(({ pluginDir, nodePath, pluginPackageJson }) => startPlugin({ pluginDir, nodePath, buildDir, childEnv, systemLogFile, pluginPackageJson })));
28
+ const childProcesses = await Promise.all(pluginsOptions.map(({ pluginDir, nodePath, nodeVersion, pluginPackageJson }) => startPlugin({ pluginDir, nodePath, nodeVersion, buildDir, childEnv, systemLogFile, pluginPackageJson })));
25
29
  return { childProcesses };
26
30
  };
27
31
  export const startPlugins = measureDuration(tStartPlugins, 'start_plugins');
28
- const startPlugin = async function ({ pluginDir, nodePath, buildDir, childEnv, systemLogFile, pluginPackageJson }) {
29
- const childProcess = execaNode(CHILD_MAIN_FILE, [], {
32
+ const startPlugin = async function ({ pluginDir, nodeVersion, nodePath, buildDir, childEnv, systemLogFile, pluginPackageJson, }) {
33
+ const ctx = trace.getActiveSpan()?.spanContext();
34
+ // the baggage will be passed to the child process when sending the run event
35
+ const args = [
36
+ ...process.argv.filter((arg) => arg.startsWith('--tracing')),
37
+ `--tracing.traceId=${ctx?.traceId}`,
38
+ `--tracing.parentSpanId=${ctx?.spanId}`,
39
+ `--tracing.traceFlags=${ctx?.traceFlags}`,
40
+ `--tracing.enabled=${!!isTrustedPlugin(pluginPackageJson?.name)}`,
41
+ ];
42
+ const nodeOptions = [];
43
+ // the sdk setup is a optional dependency that might not exist
44
+ // only use it if it exists
45
+ try {
46
+ // the --import preloading is only available in node 18.18.0 and above
47
+ // plugins that run on a lower node version will not be able to be instrumented with opentelemetry
48
+ if (gte(nodeVersion, '18.18.0')) {
49
+ const entry = require.resolve('@netlify/opentelemetry-sdk-setup/bin.js');
50
+ // on windows only file:// urls are allowed
51
+ nodeOptions.push('--import', pathToFileURL(entry).toString());
52
+ }
53
+ }
54
+ catch {
55
+ // noop
56
+ }
57
+ const childProcess = execaNode(CHILD_MAIN_FILE, args, {
30
58
  cwd: buildDir,
31
59
  preferLocal: true,
32
60
  localDir: pluginDir,
33
61
  nodePath,
34
- // make sure we don't pass build's node cli properties for now (e.g. --import)
35
- nodeOptions: [],
62
+ nodeOptions,
36
63
  execPath: nodePath,
37
- env: childEnv,
64
+ env: {
65
+ ...childEnv,
66
+ OTEL_SERVICE_NAME: pluginPackageJson?.name,
67
+ OTEL_SERVICE_VERSION: pluginPackageJson?.version,
68
+ },
38
69
  extendEnv: false,
39
70
  stdio: isTrustedPlugin(pluginPackageJson?.name) && systemLogFile
40
71
  ? ['pipe', 'pipe', 'pipe', 'ipc', systemLogFile]
@@ -51,11 +82,28 @@ const startPlugin = async function ({ pluginDir, nodePath, buildDir, childEnv, s
51
82
  }
52
83
  };
53
84
  // Stop all plugins child processes
54
- export const stopPlugins = function (childProcesses) {
55
- childProcesses.forEach(stopPlugin);
85
+ export const stopPlugins = async function ({ childProcesses, logs, verbose, pluginOptions, netlifyConfig, }) {
86
+ await Promise.all(childProcesses.map(({ childProcess }, index) => {
87
+ return stopPlugin({ childProcess, verbose, logs, pluginOptions: pluginOptions[index], netlifyConfig });
88
+ }));
56
89
  };
57
- const stopPlugin = function ({ childProcess }) {
90
+ const stopPlugin = async function ({ childProcess, logs, pluginOptions: { packageName, inputs, pluginPath, pluginPackageJson: packageJson = {} }, netlifyConfig, verbose, }) {
58
91
  if (childProcess.connected) {
92
+ // reliable stop tracing inside child processes
93
+ await callChild({
94
+ childProcess,
95
+ eventName: 'shutdown',
96
+ payload: {
97
+ packageName,
98
+ pluginPath,
99
+ inputs,
100
+ packageJson,
101
+ verbose,
102
+ netlifyConfig,
103
+ },
104
+ logs,
105
+ verbose,
106
+ });
59
107
  childProcess.disconnect();
60
108
  }
61
109
  childProcess.kill();
@@ -1,36 +1,45 @@
1
1
  import { platform } from 'process';
2
+ import { trace } from '@opentelemetry/api';
3
+ import { wrapTracer } from '@opentelemetry/api/experimental';
2
4
  import { execa } from 'execa';
3
5
  import { addErrorInfo } from '../error/info.js';
4
6
  import { getBuildCommandDescription } from '../log/description.js';
5
7
  import { logBuildCommandStart } from '../log/messages/steps.js';
6
8
  import { getBuildCommandStdio, handleBuildCommandOutput } from '../log/stream.js';
9
+ const tracer = wrapTracer(trace.getTracer('build-command'));
7
10
  // Fire `build.command`
8
11
  const coreStep = async function ({ configPath, buildDir, nodePath, childEnv, logs, netlifyConfig: { build: { command: buildCommand, commandOrigin: buildCommandOrigin }, }, }) {
9
- logBuildCommandStart(logs, buildCommand);
10
- const stdio = getBuildCommandStdio(logs);
11
- const childProcess = execa(buildCommand, {
12
- shell: SHELL,
13
- cwd: buildDir,
14
- preferLocal: true,
15
- execPath: nodePath,
16
- env: childEnv,
17
- extendEnv: false,
18
- stdio,
12
+ return tracer.withActiveSpan('build.command', async (span) => {
13
+ span.setAttributes({
14
+ 'build.command.origin': buildCommandOrigin,
15
+ 'build.cwd': buildDir,
16
+ });
17
+ logBuildCommandStart(logs, buildCommand);
18
+ const stdio = getBuildCommandStdio(logs);
19
+ const childProcess = execa(buildCommand, {
20
+ shell: SHELL,
21
+ cwd: buildDir,
22
+ preferLocal: true,
23
+ execPath: nodePath,
24
+ env: childEnv,
25
+ extendEnv: false,
26
+ stdio,
27
+ });
28
+ try {
29
+ const buildCommandOutput = await childProcess;
30
+ handleBuildCommandOutput(buildCommandOutput, logs);
31
+ return {};
32
+ }
33
+ catch (error) {
34
+ // In our test environment we use `stdio: 'pipe'` on the build command, meaning our `stdout/stderr` output are
35
+ // buffered and consequently added to `error.message`. To avoid this and end up with duplicated output in our
36
+ // logs/snapshots we need to rely on `error.shortMessage`.
37
+ error.message = error.shortMessage;
38
+ handleBuildCommandOutput(error, logs);
39
+ addErrorInfo(error, { type: 'buildCommand', location: { buildCommand, buildCommandOrigin, configPath } });
40
+ throw error;
41
+ }
19
42
  });
20
- try {
21
- const buildCommandOutput = await childProcess;
22
- handleBuildCommandOutput(buildCommandOutput, logs);
23
- return {};
24
- }
25
- catch (error) {
26
- // In our test environment we use `stdio: 'pipe'` on the build command, meaning our `stdout/stderr` output are
27
- // buffered and consequently added to `error.message`. To avoid this and end up with duplicated output in our
28
- // logs/snapshots we need to rely on `error.shortMessage`.
29
- error.message = error.shortMessage;
30
- handleBuildCommandOutput(error, logs);
31
- addErrorInfo(error, { type: 'buildCommand', location: { buildCommand, buildCommandOrigin, configPath } });
32
- throw error;
33
- }
34
43
  };
35
44
  // We use Bash on Unix and `cmd.exe` on Windows
36
45
  const SHELL = platform === 'win32' ? true : 'bash';
@@ -1,3 +1,4 @@
1
+ import { context, propagation } from '@opentelemetry/api';
1
2
  import { addErrorInfo } from '../error/info.js';
2
3
  import { logStepCompleted } from '../log/messages/ipc.js';
3
4
  import { pipePluginOutput, unpipePluginOutput } from '../log/stream.js';
@@ -9,6 +10,8 @@ export const isTrustedPlugin = (packageName) => packageName?.startsWith('@netlif
9
10
  // Fire a plugin step
10
11
  export const firePluginStep = async function ({ event, childProcess, packageName, packagePath, pluginPackageJson, loadedFrom, origin, envChanges, errorParams, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, constants, steps, error, logs, systemLog, featureFlags, debug, verbose, }) {
11
12
  const listeners = pipePluginOutput(childProcess, logs);
13
+ const otelCarrier = {};
14
+ propagation.inject(context.active(), otelCarrier);
12
15
  try {
13
16
  const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath]);
14
17
  const { newEnvChanges, configMutations: newConfigMutations, status, } = await callChild({
@@ -21,6 +24,7 @@ export const firePluginStep = async function ({ event, childProcess, packageName
21
24
  featureFlags: isTrustedPlugin(pluginPackageJson?.name) ? featureFlags : undefined,
22
25
  netlifyConfig,
23
26
  constants,
27
+ otelCarrier,
24
28
  },
25
29
  logs,
26
30
  verbose,
@@ -22,9 +22,13 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
22
22
  'build.execution.step.origin': origin,
23
23
  'build.execution.step.event': event,
24
24
  };
25
+ if (pluginPackageJson?.name && pluginPackageJson?.version) {
26
+ attributes['build.execution.step.plugin_name'] = pluginPackageJson.name;
27
+ attributes['build.execution.step.plugin_version'] = pluginPackageJson.version;
28
+ }
25
29
  const spanCtx = setMultiSpanAttributes(attributes);
26
30
  // If there's no `coreStepId` then this is a plugin execution
27
- const spanName = `run-step-${coreStepId || 'plugin'}`;
31
+ const spanName = `run-step-${coreStepId || `plugin-${event}`}`;
28
32
  return tracer.startActiveSpan(spanName, {}, spanCtx, async (span) => {
29
33
  const constantsA = await addMutableConstants({ constants, buildDir, netlifyConfig });
30
34
  const shouldRun = await shouldRunStep({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "29.36.6",
3
+ "version": "29.37.0",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -75,7 +75,7 @@
75
75
  "@netlify/framework-info": "^9.8.11",
76
76
  "@netlify/functions-utils": "^5.2.51",
77
77
  "@netlify/git-utils": "^5.1.1",
78
- "@netlify/opentelemetry-utils": "^1.0.3",
78
+ "@netlify/opentelemetry-utils": "^1.1.0",
79
79
  "@netlify/plugins-list": "^6.75.0",
80
80
  "@netlify/run-utils": "^5.1.1",
81
81
  "@netlify/zip-it-and-ship-it": "9.29.2",
@@ -127,8 +127,8 @@
127
127
  },
128
128
  "devDependencies": {
129
129
  "@netlify/nock-udp": "^3.1.2",
130
- "@opentelemetry/api": "^1.7.0",
131
- "@opentelemetry/sdk-trace-base": "^1.18.1",
130
+ "@opentelemetry/api": "~1.8.0",
131
+ "@opentelemetry/sdk-trace-base": "~1.22.0",
132
132
  "@types/node": "^14.18.53",
133
133
  "@vitest/coverage-c8": "^0.33.0",
134
134
  "atob": "^2.1.2",
@@ -153,8 +153,8 @@
153
153
  "yarn": "^1.22.4"
154
154
  },
155
155
  "peerDependencies": {
156
- "@netlify/opentelemetry-sdk-setup": "^1.0.5",
157
- "@opentelemetry/api": "^1.7.0"
156
+ "@netlify/opentelemetry-sdk-setup": "^1.1.0",
157
+ "@opentelemetry/api": "~1.8.0"
158
158
  },
159
159
  "peerDependenciesMeta": {
160
160
  "@netlify/opentelemetry-sdk-setup": {
@@ -164,5 +164,5 @@
164
164
  "engines": {
165
165
  "node": "^14.16.0 || >=16.0.0"
166
166
  },
167
- "gitHead": "1cee56d47237e07daa078e8149c411aa346bc8e1"
167
+ "gitHead": "058943be167177f38fc6a02f7da00a18d72f2d89"
168
168
  }