@determinate-systems/detsys-ts 1.0.0 → 2.0.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/dist/index.mjs CHANGED
@@ -1,20 +1,29 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import * as fs$1 from "node:fs";
3
- import { constants, createReadStream, createWriteStream, readFileSync } from "node:fs";
4
- import * as os$1 from "node:os";
5
- import { tmpdir } from "node:os";
2
+ import * as nodeFs from "node:fs";
3
+ import fs, { createReadStream } from "node:fs";
4
+ import os, { tmpdir } from "node:os";
6
5
  import { promisify } from "node:util";
7
6
  import * as actionsCore from "@actions/core";
8
7
  import * as exec$1 from "@actions/exec";
9
- import os from "os";
10
- import fs, { chmod, copyFile, mkdir, readFile, readdir, stat } from "node:fs/promises";
11
- import { gzip } from "node:zlib";
12
- import { createHash, randomUUID } from "node:crypto";
8
+ import os$1 from "os";
9
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
10
+ import * as otelApi from "@opentelemetry/api";
11
+ import { SeverityNumber, logs } from "@opentelemetry/api-logs";
12
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
13
+ import * as otelCore from "@opentelemetry/core";
14
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
15
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
16
+ import * as otelResources from "@opentelemetry/resources";
17
+ import * as sdkLogs from "@opentelemetry/sdk-logs";
18
+ import * as sdkTrace from "@opentelemetry/sdk-trace-base";
19
+ import * as semconv from "@opentelemetry/semantic-conventions";
13
20
  import got, { TimeoutError } from "got";
14
21
  import { resolveSrv } from "node:dns/promises";
15
22
  import * as actionsCache from "@actions/cache";
23
+ import * as semconvIncubating from "@opentelemetry/semantic-conventions/incubating";
16
24
  import { exec } from "node:child_process";
17
- import * as path from "node:path";
25
+ import fs$1, { chmod, copyFile, mkdir, readFile } from "node:fs/promises";
26
+ import path from "node:path";
18
27
  //#region src/linux-release-info.ts
19
28
  /*!
20
29
  * linux-release-info
@@ -26,7 +35,7 @@ import * as path from "node:path";
26
35
  * Licensed under MIT
27
36
  * Copyright (c) 2018-2020 [Samuel Carreira]
28
37
  */
29
- const readFileAsync = promisify(fs$1.readFile);
38
+ const readFileAsync = promisify(fs.readFile);
30
39
  const linuxReleaseInfoOptionsDefaults = {
31
40
  mode: "async",
32
41
  customFile: null,
@@ -44,7 +53,7 @@ function releaseInfo(infoOptions) {
44
53
  ...infoOptions
45
54
  };
46
55
  const searchOsReleaseFileList = osReleaseFileList(options.customFile);
47
- if (os$1.type() !== "Linux") {
56
+ if (os.type() !== "Linux") {
48
57
  if (options.mode === "sync") return getOsInfo();
49
58
  else return Promise.resolve(getOsInfo());
50
59
  }
@@ -93,11 +102,11 @@ function osReleaseFileList(customFile) {
93
102
  */
94
103
  function getOsInfo() {
95
104
  return {
96
- type: os$1.type(),
97
- platform: os$1.platform(),
98
- hostname: os$1.hostname(),
99
- arch: os$1.arch(),
100
- release: os$1.release()
105
+ type: os.type(),
106
+ platform: os.platform(),
107
+ hostname: os.hostname(),
108
+ arch: os.arch(),
109
+ release: os.release()
101
110
  };
102
111
  }
103
112
  async function readAsyncOsReleaseFile(fileList, options) {
@@ -117,7 +126,7 @@ function readSyncOsreleaseFile(releaseFileList, options) {
117
126
  let fileData = null;
118
127
  for (const osReleaseFile of releaseFileList) try {
119
128
  if (options.debug) console.log(`Trying to read '${osReleaseFile}'...`);
120
- fileData = fs$1.readFileSync(osReleaseFile, "binary");
129
+ fileData = fs.readFileSync(osReleaseFile, "binary");
121
130
  if (options.debug) console.log(`Read data:\n${fileData}`);
122
131
  break;
123
132
  } catch (error) {
@@ -191,11 +200,11 @@ function getPropertyWithDefault(data, name, defaultValue) {
191
200
  /**
192
201
  * The Action runner's platform.
193
202
  */
194
- const platform = os.platform();
203
+ const platform = os$1.platform();
195
204
  /**
196
205
  * The Action runner's architecture.
197
206
  */
198
- const arch = os.arch();
207
+ const arch = os$1.arch();
199
208
  /**
200
209
  * Whether the Action runner is a Windows system.
201
210
  */
@@ -222,112 +231,6 @@ async function getDetails() {
222
231
  };
223
232
  }
224
233
  //#endregion
225
- //#region src/errors.ts
226
- /**
227
- * Coerce a value of type `unknown` into a string.
228
- */
229
- function stringifyError(e) {
230
- if (e instanceof Error) return e.message;
231
- else if (typeof e === "string") return e;
232
- else return JSON.stringify(e);
233
- }
234
- //#endregion
235
- //#region src/backtrace.ts
236
- /**
237
- * @packageDocumentation
238
- * Collects backtraces for executables for diagnostics
239
- */
240
- const START_SLOP_SECONDS = 5;
241
- async function collectBacktraces(prefixes, programNameDenyList, startTimestampMs) {
242
- if (isMacOS) return await collectBacktracesMacOS(prefixes, programNameDenyList, startTimestampMs);
243
- if (isLinux) return await collectBacktracesSystemd(prefixes, programNameDenyList, startTimestampMs);
244
- return /* @__PURE__ */ new Map();
245
- }
246
- async function collectBacktracesMacOS(prefixes, programNameDenyList, startTimestampMs) {
247
- const backtraces = /* @__PURE__ */ new Map();
248
- try {
249
- const { stdout: logJson } = await exec$1.getExecOutput("log", [
250
- "show",
251
- "--style",
252
- "json",
253
- "--last",
254
- "1m",
255
- "--no-info",
256
- "--predicate",
257
- "sender = 'ReportCrash'"
258
- ], { silent: true });
259
- const sussyArray = JSON.parse(logJson);
260
- if (!Array.isArray(sussyArray)) throw new Error(`Log json isn't an array: ${logJson}`);
261
- if (sussyArray.length > 0) {
262
- actionsCore.info(`Collecting crash data...`);
263
- const delay = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
264
- await delay(5e3);
265
- }
266
- } catch {
267
- actionsCore.debug("Failed to check logs for in-progress crash dumps; now proceeding with the assumption that all crash dumps completed.");
268
- }
269
- const dirs = [["system", "/Library/Logs/DiagnosticReports/"], ["user", `${process.env["HOME"]}/Library/Logs/DiagnosticReports/`]];
270
- for (const [source, dir] of dirs) {
271
- const fileNames = (await readdir(dir)).filter((fileName) => {
272
- return prefixes.some((prefix) => fileName.startsWith(prefix));
273
- }).filter((fileName) => {
274
- return !programNameDenyList.some((programName) => fileName.startsWith(programName));
275
- }).filter((fileName) => {
276
- return !fileName.endsWith(".diag");
277
- });
278
- const doGzip = promisify(gzip);
279
- for (const fileName of fileNames) try {
280
- if ((await stat(`${dir}/${fileName}`)).ctimeMs >= startTimestampMs) {
281
- const buf = await doGzip(await readFile(`${dir}/${fileName}`));
282
- backtraces.set(`backtrace_value_${source}_${fileName}`, buf.toString("base64"));
283
- }
284
- } catch (innerError) {
285
- backtraces.set(`backtrace_failure_${source}_${fileName}`, stringifyError(innerError));
286
- }
287
- }
288
- return backtraces;
289
- }
290
- async function collectBacktracesSystemd(prefixes, programNameDenyList, startTimestampMs) {
291
- const sinceSeconds = Math.ceil((Date.now() - startTimestampMs) / 1e3) + START_SLOP_SECONDS;
292
- const backtraces = /* @__PURE__ */ new Map();
293
- const coredumps = [];
294
- try {
295
- const { stdout: coredumpjson } = await exec$1.getExecOutput("coredumpctl", [
296
- "--json=pretty",
297
- "list",
298
- "--since",
299
- `${sinceSeconds} seconds ago`
300
- ], { silent: true });
301
- const sussyArray = JSON.parse(coredumpjson);
302
- if (!Array.isArray(sussyArray)) throw new Error(`Coredump isn't an array: ${coredumpjson}`);
303
- for (const sussyObject of sussyArray) {
304
- const keys = Object.keys(sussyObject);
305
- if (keys.includes("exe") && keys.includes("pid")) {
306
- if (typeof sussyObject.exe == "string" && typeof sussyObject.pid == "number") {
307
- const execParts = sussyObject.exe.split("/");
308
- const binaryName = execParts[execParts.length - 1];
309
- if (prefixes.some((prefix) => binaryName.startsWith(prefix)) && !programNameDenyList.includes(binaryName)) coredumps.push({
310
- exe: sussyObject.exe,
311
- pid: sussyObject.pid
312
- });
313
- } else actionsCore.debug(`Mysterious coredump entry missing exe string and/or pid number: ${JSON.stringify(sussyObject)}`);
314
- } else actionsCore.debug(`Mysterious coredump entry missing exe value and/or pid value: ${JSON.stringify(sussyObject)}`);
315
- }
316
- } catch (innerError) {
317
- actionsCore.debug(`Cannot collect backtraces: ${stringifyError(innerError)}`);
318
- return backtraces;
319
- }
320
- const doGzip = promisify(gzip);
321
- for (const coredump of coredumps) try {
322
- const { stdout: logText } = await exec$1.getExecOutput("coredumpctl", ["info", `${coredump.pid}`], { silent: true });
323
- const buf = await doGzip(logText);
324
- backtraces.set(`backtrace_value_${coredump.pid}`, buf.toString("base64"));
325
- } catch (innerError) {
326
- backtraces.set(`backtrace_failure_${coredump.pid}`, stringifyError(innerError));
327
- }
328
- return backtraces;
329
- }
330
- //#endregion
331
234
  //#region src/checksums.ts
332
235
  /**
333
236
  * @packageDocumentation
@@ -463,6 +366,418 @@ function hashEnvironmentVariables(prefix, variables) {
463
366
  return `${prefix}-${hash.digest("hex")}`;
464
367
  }
465
368
  //#endregion
369
+ //#region src/errors.ts
370
+ /**
371
+ * Coerce a value of type `unknown` into a string.
372
+ */
373
+ function stringifyError(e) {
374
+ if (e instanceof Error) return e.message;
375
+ else if (typeof e === "string") return e;
376
+ else return JSON.stringify(e);
377
+ }
378
+ //#endregion
379
+ //#region src/telemetry.ts
380
+ /**
381
+ * @packageDocumentation
382
+ * OpenTelemetry traces and logs for Determinate Systems' GitHub Actions.
383
+ *
384
+ * The OpenTelemetry API is a no-op until a provider is registered globally.
385
+ * That means instrumentation call sites -- spans, log records -- can be
386
+ * written unconditionally: when export is disabled they cost nothing and no
387
+ * branching is needed at the call site.
388
+ *
389
+ * The SDK configures itself from the standard `OTEL_*` environment variables.
390
+ * This module only supplies defaults for the variables the user has not set,
391
+ * so every documented OpenTelemetry knob works here as it does anywhere else.
392
+ */
393
+ /** The instrumentation scope name for everything this library emits. */
394
+ const SCOPE_NAME = "detsys-ts";
395
+ /**
396
+ * The OTLP/HTTP collector for all Actions.
397
+ * The exporters add `/v1/traces` and `/v1/logs` to this URL.
398
+ *
399
+ * This collector is a fixed service.
400
+ * It is not one of the install.determinate.systems backends.
401
+ * Thus it does not use their SRV failover.
402
+ */
403
+ const DEFAULT_OTLP_ENDPOINT = "https://otel.determinate.systems";
404
+ /**
405
+ * The token for {@link DEFAULT_OTLP_ENDPOINT}.
406
+ * The exporters send it as `Authorization: Bearer <token>`.
407
+ * That is the default scheme of the collector's `bearertokenauth` extension.
408
+ *
409
+ * This token is public.
410
+ * It ships in `dist/`, on npm, and in each workflow that uses this library.
411
+ * It permits telemetry writes and no other operation.
412
+ * Change it in the collector configuration and in this file at the same time.
413
+ */
414
+ const OTLP_INGEST_TOKEN = "8bfa2d8b689352981286f0149c4e55cc0dff30a4f7a735b560e31479904a74e1";
415
+ /**
416
+ * How long to wait for buffered spans and logs to reach the collector before
417
+ * giving up. The Action's process exits immediately afterward, so this is a
418
+ * hard ceiling on how much a slow collector can delay a workflow.
419
+ */
420
+ const SHUTDOWN_TIMEOUT_MS = 5e3;
421
+ /**
422
+ * The default for `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`.
423
+ *
424
+ * The SDK's own default is unlimited. Attributes here can carry pasted
425
+ * command output and other unbounded text, which the collector should not
426
+ * have to absorb, so cap them. File-sized payloads go out as log records
427
+ * instead: a log record's body is not an attribute and is not truncated.
428
+ */
429
+ const DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT = 8192;
430
+ /** The OTLP environment variables a child process inherits from this run. */
431
+ const OTLP_EXPORT_VARIABLES = [
432
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
433
+ "OTEL_EXPORTER_OTLP_HEADERS",
434
+ "OTEL_EXPORTER_OTLP_COMPRESSION"
435
+ ];
436
+ /**
437
+ * Our own propagator instance, rather than the global one.
438
+ *
439
+ * The global propagator only exists once {@link Telemetry.start} has
440
+ * registered it, which would make traceparent handling silently depend on
441
+ * start-up ordering. Owning an instance keeps {@link traceparentOf} and {@link
442
+ * contextFromTraceparent} correct no matter when they're called.
443
+ */
444
+ const PROPAGATOR = new otelCore.W3CTraceContextPropagator();
445
+ const SEVERITY = {
446
+ debug: SeverityNumber.DEBUG,
447
+ info: SeverityNumber.INFO,
448
+ notice: SeverityNumber.INFO2,
449
+ warning: SeverityNumber.WARN,
450
+ error: SeverityNumber.ERROR
451
+ };
452
+ /**
453
+ * Whether this run exports telemetry at all.
454
+ *
455
+ * `OTEL_SDK_DISABLED=true` is the standard way to turn the export off. An
456
+ * empty `OTEL_EXPORTER_OTLP_ENDPOINT` does the same, which is what this
457
+ * library documented before `OTEL_SDK_DISABLED` was in the specification.
458
+ */
459
+ function exportEnabled() {
460
+ if (otelCore.getBooleanFromEnv("OTEL_SDK_DISABLED")) return false;
461
+ const endpoint = process.env["OTEL_EXPORTER_OTLP_ENDPOINT"];
462
+ if (endpoint !== void 0 && endpoint.trim() === "") return false;
463
+ return true;
464
+ }
465
+ /**
466
+ * Fill in the `OTEL_*` variables this run needs and the user has not set.
467
+ *
468
+ * From here on the exporters read their whole configuration from the
469
+ * environment, exactly as they would in any other OpenTelemetry program.
470
+ * Child processes inherit the same variables, so their telemetry reaches the
471
+ * same collector without any further arrangement.
472
+ */
473
+ function applyOtlpEnvironmentDefaults() {
474
+ if (otelCore.getStringFromEnv("OTEL_EXPORTER_OTLP_ENDPOINT") === void 0) process.env["OTEL_EXPORTER_OTLP_ENDPOINT"] = DEFAULT_OTLP_ENDPOINT;
475
+ if (exportsToDefaultCollector()) {
476
+ const headers = otelCore.parseKeyPairsIntoRecord(otelCore.getStringFromEnv("OTEL_EXPORTER_OTLP_HEADERS"));
477
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "authorization")) {
478
+ headers["Authorization"] = `Bearer ${OTLP_INGEST_TOKEN}`;
479
+ process.env["OTEL_EXPORTER_OTLP_HEADERS"] = encodeOtlpHeaders(headers);
480
+ }
481
+ }
482
+ if (otelCore.getStringFromEnv("OTEL_EXPORTER_OTLP_COMPRESSION") === void 0) process.env["OTEL_EXPORTER_OTLP_COMPRESSION"] = "gzip";
483
+ if (otelCore.getNumberFromEnv("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT") === void 0) process.env["OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = `${DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT}`;
484
+ }
485
+ /**
486
+ * Whether this run sends its data to {@link DEFAULT_OTLP_ENDPOINT}.
487
+ *
488
+ * Only that collector gets {@link OTLP_INGEST_TOKEN}. A collector the user
489
+ * chose must not receive our credentials.
490
+ */
491
+ function exportsToDefaultCollector() {
492
+ const endpoint = otelCore.getStringFromEnv("OTEL_EXPORTER_OTLP_ENDPOINT");
493
+ if (endpoint === void 0) return false;
494
+ try {
495
+ return new URL(endpoint).toString() === new URL(DEFAULT_OTLP_ENDPOINT).toString();
496
+ } catch {
497
+ return false;
498
+ }
499
+ }
500
+ /**
501
+ * The OTLP variables in the environment, for a child process that does not
502
+ * inherit ours.
503
+ */
504
+ function otlpExportEnvironment() {
505
+ const environment = {};
506
+ for (const name of OTLP_EXPORT_VARIABLES) {
507
+ const value = otelCore.getStringFromEnv(name);
508
+ if (value !== void 0) environment[name] = value;
509
+ }
510
+ return environment;
511
+ }
512
+ /**
513
+ * Make the value of `OTEL_EXPORTER_OTLP_HEADERS`.
514
+ *
515
+ * The variable uses the W3C baggage format.
516
+ * The reader decodes each percent-encoded value.
517
+ * Thus you must encode the space in `Bearer <token>`.
518
+ * If you do not encode it, the scheme and the token become two entries.
519
+ */
520
+ function encodeOtlpHeaders(headers) {
521
+ return Object.entries(headers).map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join(",");
522
+ }
523
+ /**
524
+ * The generator of the trace and span IDs of this run.
525
+ *
526
+ * It makes random IDs, as the default generator does.
527
+ * It can also give one span an identity that you supply.
528
+ * That is how a span that one process announces starts in a different process.
529
+ * See {@link Telemetry.startAnnouncedSpan}.
530
+ */
531
+ var PinnedIdGenerator = class {
532
+ /** Give the next span this identity. */
533
+ pin(traceId, spanId) {
534
+ this.traceId = traceId;
535
+ this.spanId = spanId;
536
+ }
537
+ /** Give each subsequent span a random identity again. */
538
+ unpin() {
539
+ this.traceId = void 0;
540
+ this.spanId = void 0;
541
+ }
542
+ generateTraceId() {
543
+ return this.traceId ?? randomHex(16);
544
+ }
545
+ generateSpanId() {
546
+ return this.spanId ?? randomHex(8);
547
+ }
548
+ };
549
+ /**
550
+ * Owns the OpenTelemetry SDK's lifecycle. Constructing this does nothing on
551
+ * its own; `start()` registers the global providers and `shutdown()` flushes
552
+ * whatever is buffered.
553
+ */
554
+ var Telemetry = class {
555
+ /** Whether OTLP export is actually running. */
556
+ get enabled() {
557
+ return this.tracerProvider !== void 0;
558
+ }
559
+ /**
560
+ * Register the global tracer and logger providers.
561
+ *
562
+ * Safe to call at most once. If it throws, telemetry stays disabled and the
563
+ * Action carries on: instrumentation degrades to the API's no-ops rather
564
+ * than failing the workflow.
565
+ */
566
+ start(options) {
567
+ if (this.enabled || !exportEnabled()) return;
568
+ try {
569
+ applyOtlpEnvironmentDefaults();
570
+ const resource = otelResources.defaultResource().merge(otelResources.resourceFromAttributes({
571
+ [semconv.ATTR_SERVICE_NAME]: options.serviceName,
572
+ ...options.serviceVersion === void 0 ? {} : { [semconv.ATTR_SERVICE_VERSION]: options.serviceVersion },
573
+ ...options.resourceAttributes
574
+ })).merge(otelResources.detectResources({ detectors: [otelResources.envDetector] }));
575
+ this.idGenerator = new PinnedIdGenerator();
576
+ this.tracerProvider = new sdkTrace.BasicTracerProvider({
577
+ resource,
578
+ idGenerator: this.idGenerator,
579
+ spanProcessors: [new sdkTrace.BatchSpanProcessor(new OTLPTraceExporter())]
580
+ });
581
+ this.loggerProvider = new sdkLogs.LoggerProvider({
582
+ resource,
583
+ logRecordLimits: { attributeValueLengthLimit: otelCore.getNumberFromEnv("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT") },
584
+ processors: [new sdkLogs.BatchLogRecordProcessor({ exporter: new OTLPLogExporter() })]
585
+ });
586
+ otelApi.context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
587
+ otelApi.propagation.setGlobalPropagator(PROPAGATOR);
588
+ otelApi.trace.setGlobalTracerProvider(this.tracerProvider);
589
+ logs.setGlobalLoggerProvider(this.loggerProvider);
590
+ actionsCore.debug(`OpenTelemetry export enabled to ${otelCore.getStringFromEnv("OTEL_EXPORTER_OTLP_ENDPOINT")}`);
591
+ } catch (e) {
592
+ this.tracerProvider = void 0;
593
+ this.loggerProvider = void 0;
594
+ this.idGenerator = void 0;
595
+ actionsCore.debug(`Failed to start OpenTelemetry export, continuing without it: ${stringifyError(e)}`);
596
+ }
597
+ }
598
+ /**
599
+ * Start the span that {@link newTraceparent} announced.
600
+ *
601
+ * A workflow job runs each Action as a process of its own.
602
+ * Thus a span that covers more than one Action can only start in one of them.
603
+ * The Action that announces such a span makes its identity known first, and
604
+ * starts the span itself last, in the process that runs at the end.
605
+ * The spans that already point at that identity then find their parent.
606
+ *
607
+ * The span starts at `startTime`, which is the moment of the announcement.
608
+ * It is a child of the span in `parentContext`, and a root span if that
609
+ * context holds no span.
610
+ *
611
+ * Returns undefined if the export is off, or if `traceparent` does not name a
612
+ * usable span.
613
+ */
614
+ startAnnouncedSpan(name, traceparent, startTime, parentContext = otelApi.ROOT_CONTEXT) {
615
+ const generator = this.idGenerator;
616
+ const spanContext = otelApi.trace.getSpanContext(contextFromTraceparent(traceparent));
617
+ if (generator === void 0 || this.tracerProvider === void 0 || spanContext === void 0 || !otelApi.isSpanContextValid(spanContext)) return;
618
+ const tracer = this.tracerProvider.getTracer(SCOPE_NAME, "1.0");
619
+ try {
620
+ generator.pin(spanContext.traceId, spanContext.spanId);
621
+ return tracer.startSpan(name, { startTime }, parentContext);
622
+ } finally {
623
+ generator.unpin();
624
+ }
625
+ }
626
+ /**
627
+ * Flush buffered spans and logs and tear the SDK down.
628
+ *
629
+ * Never throws and never hangs: the Action calls this on its way out, so a
630
+ * broken or slow collector must not be able to fail or stall the workflow.
631
+ */
632
+ async shutdown() {
633
+ const providers = [this.tracerProvider, this.loggerProvider].flatMap((p) => p ?? []);
634
+ if (providers.length === 0) return;
635
+ try {
636
+ await withTimeout(Promise.all(providers.map(async (p) => p.shutdown())), SHUTDOWN_TIMEOUT_MS);
637
+ } catch (e) {
638
+ actionsCore.debug(`Error flushing OpenTelemetry data: ${stringifyError(e)}`);
639
+ } finally {
640
+ this.tracerProvider = void 0;
641
+ this.loggerProvider = void 0;
642
+ this.idGenerator = void 0;
643
+ }
644
+ }
645
+ };
646
+ /**
647
+ * The tracer for this library. Returns a no-op tracer until {@link
648
+ * Telemetry.start} has run, so this is always safe to call.
649
+ */
650
+ function getTracer() {
651
+ return otelApi.trace.getTracer(SCOPE_NAME, "1.0");
652
+ }
653
+ /**
654
+ * The logger for this library. Returns a no-op logger until {@link
655
+ * Telemetry.start} has run, so this is always safe to call.
656
+ */
657
+ function getLogger() {
658
+ return logs.getLogger(SCOPE_NAME, "1.0");
659
+ }
660
+ /**
661
+ * Emit a log record at `level`, correlated to whatever span is currently
662
+ * active.
663
+ */
664
+ function emitLogRecord(level, message, attributes) {
665
+ getLogger().emit({
666
+ severityNumber: SEVERITY[level],
667
+ severityText: level.toUpperCase(),
668
+ body: message,
669
+ attributes,
670
+ context: otelApi.context.active()
671
+ });
672
+ }
673
+ /**
674
+ * Serialize a span as a W3C `traceparent` header value, suitable for stashing
675
+ * in the Action's state or handing to a child process.
676
+ *
677
+ * Returns undefined when telemetry is disabled, since the no-op span's context
678
+ * is all zeroes and would not be a valid parent.
679
+ */
680
+ function traceparentOf(span) {
681
+ if (span === void 0 || !otelApi.isSpanContextValid(span.spanContext())) return;
682
+ const carrier = {};
683
+ PROPAGATOR.inject(otelApi.trace.setSpan(otelApi.ROOT_CONTEXT, span), carrier, otelApi.defaultTextMapSetter);
684
+ return carrier["traceparent"];
685
+ }
686
+ /**
687
+ * Make the identity of a span, but do not start the span.
688
+ *
689
+ * Announce the result to whatever must point at the span before it starts:
690
+ * a different process, or a request this process makes too early to record.
691
+ * Start the span itself with {@link Telemetry.startAnnouncedSpan}.
692
+ *
693
+ * The span is in the trace of `parent`, or in a new trace of its own if there
694
+ * is no usable parent.
695
+ * A new trace is sampled, because a process that only forwards an identity
696
+ * cannot ask the sampler, and an unsampled parent would discard the work of
697
+ * each process that joins.
698
+ */
699
+ function newTraceparent(parent) {
700
+ const parentContext = otelApi.trace.getSpanContext(contextFromTraceparent(parent));
701
+ if (parentContext !== void 0 && otelApi.isSpanContextValid(parentContext)) {
702
+ const flags = parentContext.traceFlags.toString(16).padStart(2, "0");
703
+ return `00-${parentContext.traceId}-${randomHex(8)}-${flags}`;
704
+ }
705
+ return `00-${randomHex(16)}-${randomHex(8)}-01`;
706
+ }
707
+ /**
708
+ * The W3C trace context headers of the operation in progress, for an outgoing
709
+ * HTTP request.
710
+ *
711
+ * Put these headers on the request.
712
+ * The service that answers it can then put its own work in this trace.
713
+ *
714
+ * The headers describe the span that is active now.
715
+ * When no span is active yet -- a request the Action makes before it starts a
716
+ * span of its own -- they describe the span that `$TRACEPARENT` names, which is
717
+ * the span the Action announced, or the span of the workflow job.
718
+ *
719
+ * The result is empty when the export is off.
720
+ * A no-op span's context is all zeroes, and is not a valid parent.
721
+ */
722
+ function traceContextHeaders() {
723
+ const active = otelApi.context.active();
724
+ const context = otelApi.trace.getSpanContext(active) === void 0 ? contextFromTraceparent(process.env["TRACEPARENT"]) : active;
725
+ const carrier = {};
726
+ PROPAGATOR.inject(context, carrier, otelApi.defaultTextMapSetter);
727
+ return carrier;
728
+ }
729
+ /**
730
+ * Rebuild a Context from a W3C `traceparent` value, so a span started in one
731
+ * process can parent spans started in another. Falls back to the root context
732
+ * when `traceparent` is absent or unparseable.
733
+ */
734
+ function contextFromTraceparent(traceparent) {
735
+ if (traceparent === void 0 || traceparent === "") return otelApi.ROOT_CONTEXT;
736
+ return PROPAGATOR.extract(otelApi.ROOT_CONTEXT, { traceparent }, otelApi.defaultTextMapGetter);
737
+ }
738
+ /**
739
+ * Mark `span` as failed and attach the exception to it.
740
+ */
741
+ function recordSpanError(span, error) {
742
+ span.recordException(error instanceof Error ? error : new Error(stringifyError(error)));
743
+ span.setStatus({
744
+ code: otelApi.SpanStatusCode.ERROR,
745
+ message: stringifyError(error)
746
+ });
747
+ }
748
+ /**
749
+ * Run `fn` inside a new active span, ending the span when it settles and
750
+ * marking it failed if it throws. The error is always re-thrown: this records,
751
+ * it does not swallow.
752
+ */
753
+ async function withSpan(name, fn, attributes) {
754
+ return await getTracer().startActiveSpan(name, { attributes }, async (span) => {
755
+ try {
756
+ return await fn(span);
757
+ } catch (e) {
758
+ recordSpanError(span, e);
759
+ throw e;
760
+ } finally {
761
+ span.end();
762
+ }
763
+ });
764
+ }
765
+ /** A random ID of `bytes` bytes, in the lowercase hex the W3C format uses. */
766
+ function randomHex(bytes) {
767
+ return randomBytes(bytes).toString("hex");
768
+ }
769
+ /** Reject if `promise` has not settled within `timeoutMs`. */
770
+ async function withTimeout(promise, timeoutMs) {
771
+ let timer;
772
+ try {
773
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
774
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
775
+ })]);
776
+ } finally {
777
+ if (timer !== void 0) clearTimeout(timer);
778
+ }
779
+ }
780
+ //#endregion
466
781
  //#region src/ids-host.ts
467
782
  /**
468
783
  * @packageDocumentation
@@ -500,6 +815,7 @@ var IdsHost = class {
500
815
  actionsCore.info(`Retrying after error ${error.code}, retry #: ${retryCount}`);
501
816
  }],
502
817
  beforeRequest: [async (options) => {
818
+ for (const [name, value] of Object.entries(traceContextHeaders())) options.headers[name] = value;
503
819
  const currentUrl = options.url;
504
820
  if (this.isUrlSubjectToDynamicUrls(currentUrl)) {
505
821
  const newUrl = new URL(currentUrl);
@@ -544,6 +860,13 @@ var IdsHost = class {
544
860
  if (url === void 0) return new URL(DEFAULT_IDS_HOST);
545
861
  return url;
546
862
  }
863
+ /**
864
+ * The diagnostics endpoint of the current backend.
865
+ *
866
+ * This library reports nothing there: its telemetry is OpenTelemetry. The
867
+ * URL is for the programs an Action runs, which have diagnostics of their
868
+ * own.
869
+ */
547
870
  async getDiagnosticsUrl() {
548
871
  if (this.runtimeDiagnosticsUrl === "") return;
549
872
  if (this.runtimeDiagnosticsUrl !== "-" && this.runtimeDiagnosticsUrl !== void 0) try {
@@ -728,6 +1051,85 @@ const getStringOrUndefined = (name) => {
728
1051
  else return value;
729
1052
  };
730
1053
  //#endregion
1054
+ //#region src/log.ts
1055
+ /**
1056
+ * @packageDocumentation
1057
+ * Logging that tees to both the GitHub Actions console and OpenTelemetry.
1058
+ *
1059
+ * These are drop-in replacements for the `@actions/core` logging functions.
1060
+ * Every call still writes to the workflow log exactly as it did before -- the
1061
+ * user-visible output is unchanged -- and additionally emits an OpenTelemetry
1062
+ * LogRecord correlated to the currently active span.
1063
+ *
1064
+ * When telemetry is disabled the OpenTelemetry half is a no-op, so these
1065
+ * behave identically to calling `@actions/core` directly.
1066
+ */
1067
+ var log_exports = /* @__PURE__ */ __exportAll({
1068
+ debug: () => debug,
1069
+ error: () => error,
1070
+ group: () => group,
1071
+ info: () => info,
1072
+ notice: () => notice,
1073
+ setFailed: () => setFailed,
1074
+ warning: () => warning
1075
+ });
1076
+ function tee(level, message, attributes) {
1077
+ const text = typeof message === "string" ? message : stringifyError(message);
1078
+ emitLogRecord(level, text, attributes);
1079
+ return text;
1080
+ }
1081
+ /**
1082
+ * Write a debug message. Only visible in the workflow log when the user has
1083
+ * enabled step debug logging, but always exported to OpenTelemetry.
1084
+ */
1085
+ function debug(message, attributes) {
1086
+ actionsCore.debug(tee("debug", message, attributes));
1087
+ }
1088
+ /** Write an informational message to the workflow log. */
1089
+ function info(message, attributes) {
1090
+ actionsCore.info(tee("info", message, attributes));
1091
+ }
1092
+ /** Write a notice annotation to the workflow log. */
1093
+ function notice(message, properties, attributes) {
1094
+ tee("notice", message, attributes);
1095
+ actionsCore.notice(message, properties);
1096
+ }
1097
+ /** Write a warning annotation to the workflow log. */
1098
+ function warning(message, properties, attributes) {
1099
+ tee("warning", message, attributes);
1100
+ actionsCore.warning(message, properties);
1101
+ }
1102
+ /** Write an error annotation to the workflow log. */
1103
+ function error(message, properties, attributes) {
1104
+ tee("error", message, attributes);
1105
+ actionsCore.error(message, properties);
1106
+ }
1107
+ /**
1108
+ * Fail the workflow step, recording the reason as an OpenTelemetry error log.
1109
+ */
1110
+ function setFailed(message, attributes) {
1111
+ tee("error", message, attributes);
1112
+ actionsCore.setFailed(message);
1113
+ }
1114
+ /**
1115
+ * Run `fn` inside both a collapsible group in the workflow log and an active
1116
+ * OpenTelemetry span of the same name.
1117
+ *
1118
+ * This is the replacement for a `startGroup`/`endGroup` pair: the group closes
1119
+ * and the span ends even if `fn` throws, and a throwing `fn` marks the span
1120
+ * failed before re-throwing.
1121
+ */
1122
+ async function group(name, fn, attributes) {
1123
+ return await withSpan(name, async () => {
1124
+ actionsCore.startGroup(name);
1125
+ try {
1126
+ return await fn();
1127
+ } finally {
1128
+ actionsCore.endGroup();
1129
+ }
1130
+ }, attributes);
1131
+ }
1132
+ //#endregion
731
1133
  //#region src/platform.ts
732
1134
  /**
733
1135
  * @packageDocumentation
@@ -805,40 +1207,52 @@ function noisilyGetInput(suffix, legacyPrefix) {
805
1207
  * @packageDocumentation
806
1208
  * Determinate Systems' TypeScript library for creating GitHub Actions logic.
807
1209
  */
808
- const pkgVersion = "1.0";
809
- const EVENT_BACKTRACES = "backtrace";
810
- const EVENT_EXCEPTION = "exception";
811
- const EVENT_ARTIFACT_CACHE_HIT = "artifact_cache_hit";
812
- const EVENT_ARTIFACT_CACHE_MISS = "artifact_cache_miss";
813
- const EVENT_ARTIFACT_CACHE_PERSIST = "artifact_cache_persist";
814
- const EVENT_PREFLIGHT_REQUIRE_NIX_DENIED = "preflight-require-nix-denied";
815
- const EVENT_STORE_IDENTITY_FAILED = "store_identity_failed";
816
- const FACT_ARTIFACT_FETCHED_FROM_CACHE = "artifact_fetched_from_cache";
817
- const FACT_ENDED_WITH_EXCEPTION = "ended_with_exception";
818
- const FACT_FINAL_EXCEPTION = "final_exception";
819
- const FACT_OS = "$os";
820
- const FACT_OS_VERSION = "$os_version";
821
- const FACT_SOURCE_URL = "source_url";
822
- const FACT_SOURCE_URL_ETAG = "source_url_etag";
823
- const FACT_SOURCE_CHECKSUMS_SHA256 = "source_checksums_sha256";
824
- const FACT_NIX_VERSION = "nix_version";
825
- const FACT_NIX_LOCATION = "nix_location";
826
- const FACT_NIX_STORE_TRUST = "nix_store_trusted";
827
- const FACT_NIX_STORE_VERSION = "nix_store_version";
828
- const FACT_NIX_STORE_CHECK_METHOD = "nix_store_check_method";
829
- const FACT_NIX_STORE_CHECK_ERROR = "nix_store_check_error";
1210
+ const EVENT_IDS_FAILOVER = "detsys.ids_failover";
1211
+ const EVENT_PREFLIGHT_REQUIRE_NIX_DENIED = "detsys.preflight_require_nix_denied";
1212
+ const EVENT_REQUEST_TIMEOUT = "detsys.request_timeout";
1213
+ const EVENT_STORE_IDENTITY_FAILED = "detsys.store_identity_failed";
1214
+ const ATTR_PROJECT = "detsys.project";
1215
+ const ATTR_IDS_PROJECT = "detsys.ids_project";
1216
+ const ATTR_EXECUTION_PHASE = "detsys.execution_phase";
1217
+ const ATTR_CROSS_PHASE_ID = "detsys.cross_phase_id";
1218
+ const ATTR_ANONYMOUS_ID = "detsys.anonymous_id";
1219
+ const ATTR_CORRELATION_SOURCE = "detsys.correlation_source";
1220
+ const ATTR_ARCH_OS = "detsys.arch_os";
1221
+ const ATTR_NIX_SYSTEM = "detsys.nix_system";
1222
+ const ATTR_FEATURE_PREFIX = "detsys.feature.";
1223
+ const ATTR_GITHUB_EVENT_NAME = "detsys.github.event_name";
1224
+ const ATTR_GITHUB_ACTION_REPOSITORY = "detsys.github.action_repository";
1225
+ const ATTR_GITHUB_REPOSITORY_HASH = "detsys.github.repository_hash";
1226
+ const ATTR_GITHUB_ORGANIZATION_HASH = "detsys.github.organization_hash";
1227
+ const ATTR_GITHUB_WORKFLOW_HASH = "detsys.github.workflow_hash";
1228
+ const ATTR_GITHUB_WORKFLOW_JOB_HASH = "detsys.github.workflow_job_hash";
1229
+ const ATTR_GITHUB_WORKFLOW_RUN_HASH = "detsys.github.workflow_run_hash";
1230
+ const ATTR_GITHUB_WORKFLOW_RUN_DIFFERENTIATOR_HASH = "detsys.github.workflow_run_differentiator_hash";
1231
+ const ATTR_ARTIFACT_NAME = "detsys.artifact.name";
1232
+ const ATTR_ARTIFACT_FETCH_SUFFIX = "detsys.artifact.fetch_suffix";
1233
+ const ATTR_ARTIFACT_CACHE_HIT = "detsys.artifact.cache_hit";
1234
+ const ATTR_SOURCE_URL = "detsys.source.url";
1235
+ const ATTR_SOURCE_ETAG = "detsys.source.etag";
1236
+ const ATTR_SOURCE_CHECKSUMS_SHA256 = "detsys.source.checksums_sha256";
1237
+ const ATTR_NIX_LOCATION = "detsys.nix.location";
1238
+ const ATTR_NIX_VERSION = "detsys.nix.version";
1239
+ const ATTR_NIX_STORE_TRUST = "detsys.nix.store_trust";
1240
+ const ATTR_NIX_STORE_VERSION = "detsys.nix.store_version";
1241
+ const ATTR_NIX_STORE_CHECK_METHOD = "detsys.nix.store_check_method";
1242
+ const ATTR_NIX_STORE_CHECK_ERROR = "detsys.nix.store_check_error";
1243
+ const ATTR_ATTACHMENT_NAME = "detsys.attachment.name";
1244
+ const ATTR_ATTACHMENT_PATH = "detsys.attachment.path";
830
1245
  const STATE_KEY_EXECUTION_PHASE = "detsys_action_execution_phase";
831
1246
  const STATE_KEY_NIX_NOT_FOUND = "detsys_action_nix_not_found";
832
1247
  const STATE_NOT_FOUND = "not-found";
833
1248
  const STATE_KEY_CROSS_PHASE_ID = "detsys_cross_phase_id";
834
- const STATE_BACKTRACE_START_TIMESTAMP = "detsys_backtrace_start_timestamp";
835
- const DIAGNOSTIC_ENDPOINT_TIMEOUT_MS = 1e4;
1249
+ const STATE_KEY_TRACEPARENT = "detsys_otel_traceparent";
1250
+ const STATE_KEY_JOB_TRACEPARENT = "detsys_otel_job_traceparent";
1251
+ const STATE_KEY_JOB_SPAN_START = "detsys_otel_job_span_start";
1252
+ const ENV_TRACEPARENT = "TRACEPARENT";
1253
+ const SPAN_JOB = "github_actions_job";
1254
+ const SPAN_CHECK_IN = "check_in";
836
1255
  const CHECK_IN_ENDPOINT_TIMEOUT_MS = 1e3;
837
- const PROGRAM_NAME_CRASH_DENY_LIST = [
838
- "nix-expr-tests",
839
- "nix-store-tests",
840
- "nix-util-tests"
841
- ];
842
1256
  const determinateStateDir = "/var/lib/determinate";
843
1257
  const determinateIdentityFile = path.join(determinateStateDir, "identity.json");
844
1258
  const isRoot = typeof process.geteuid === "function" && process.geteuid() === 0;
@@ -861,14 +1275,14 @@ async function sudoWriteCorrelationHashes(hashes) {
861
1275
  const buffer = Buffer.from(hashes);
862
1276
  const code = await exec$1.exec("sudo", ["tee", determinateIdentityFile], {
863
1277
  input: buffer,
864
- outStream: createWriteStream("/dev/null")
1278
+ outStream: nodeFs.createWriteStream("/dev/null")
865
1279
  });
866
1280
  if (code !== 0) throw new Error(`sudo tee exit: ${code}`);
867
1281
  }
868
1282
  /** Writes correlation hashes to the Determinate state directory, escalating if necessary */
869
1283
  async function writeCorrelationHashes(hashes) {
870
1284
  await ensureDeterminateStateDir();
871
- if (isRoot) await fs.writeFile(determinateIdentityFile, hashes, "utf-8");
1285
+ if (isRoot) await fs$1.writeFile(determinateIdentityFile, hashes, "utf-8");
872
1286
  else return sudoWriteCorrelationHashes(hashes);
873
1287
  }
874
1288
  var DetSysAction = class {
@@ -881,6 +1295,7 @@ var DetSysAction = class {
881
1295
  constructor(actionOptions) {
882
1296
  this.actionOptions = makeOptionsConfident(actionOptions);
883
1297
  this.idsHost = new IdsHost(this.actionOptions.idsProjectName, actionOptions.diagnosticsSuffix, process.env["INPUT_DIAGNOSTIC-ENDPOINT"], getNumberOrUndefined("timeout-request"));
1298
+ this.telemetry = new Telemetry();
884
1299
  this.exceptionAttachments = /* @__PURE__ */ new Map();
885
1300
  this.nixStoreTrust = "unknown";
886
1301
  this.strictMode = getBool("_internal-strict-mode");
@@ -889,54 +1304,33 @@ var DetSysAction = class {
889
1304
  process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = void 0;
890
1305
  }
891
1306
  this.features = {};
892
- this.featureEventMetadata = {};
893
- this.events = [];
1307
+ this.pendingAttributes = {};
894
1308
  this.getCrossPhaseId();
895
- this.collectBacktraceSetup();
896
- this.facts = {
897
- $lib: "idslib",
898
- $lib_version: pkgVersion,
899
- project: this.actionOptions.name,
900
- ids_project: this.actionOptions.idsProjectName
901
- };
902
- for (const [target, env] of [
903
- ["github_action_ref", "GITHUB_ACTION_REF"],
904
- ["github_action_repository", "GITHUB_ACTION_REPOSITORY"],
905
- ["github_event_name", "GITHUB_EVENT_NAME"],
906
- ["$os", "RUNNER_OS"],
907
- ["arch", "RUNNER_ARCH"]
908
- ]) {
909
- const value = process.env[env];
910
- if (value) this.facts[target] = value;
911
- }
912
1309
  this.identity = identify();
913
1310
  this.archOs = getArchOs();
914
1311
  this.nixSystem = getNixPlatform(this.archOs);
915
- this.facts.$app_name = `${this.actionOptions.name}/action`;
916
- this.facts.arch_os = this.archOs;
917
- this.facts.nix_system = this.nixSystem;
918
- getDetails().then((details) => {
919
- if (details.name !== "unknown") this.addFact(FACT_OS, details.name);
920
- if (details.version !== "unknown") this.addFact(FACT_OS_VERSION, details.version);
921
- }).catch((e) => {
1312
+ this.systemDetails = getDetails().then((details) => ({
1313
+ name: details.name,
1314
+ version: details.version
1315
+ })).catch((e) => {
922
1316
  actionsCore.debug(`Failure getting platform details: ${stringifyError$1(e)}`);
923
1317
  });
924
1318
  this.executionPhase = this.determineExecutionPhase();
925
- this.facts.execution_phase = this.executionPhase;
926
1319
  if (this.actionOptions.fetchStyle === "gh-env-style") this.architectureFetchSuffix = this.archOs;
927
1320
  else if (this.actionOptions.fetchStyle === "nix-style") this.architectureFetchSuffix = this.nixSystem;
928
1321
  else if (this.actionOptions.fetchStyle === "universal") this.architectureFetchSuffix = "universal";
929
1322
  else throw new Error(`fetchStyle ${this.actionOptions.fetchStyle} is not a valid style`);
930
1323
  this.sourceParameters = constructSourceParameters(this.actionOptions.legacySourcePrefix);
931
- this.recordEvent(`begin_${this.executionPhase}`);
932
1324
  }
933
1325
  /**
934
- * Attach a file to the diagnostics data in error conditions.
1326
+ * Attach a file to the telemetry for this run, to be emitted if the Action
1327
+ * fails.
935
1328
  *
936
1329
  * The file at `location` doesn't need to exist when stapleFile is called.
937
1330
  *
938
- * If the file doesn't exist or is unreadable when trying to staple the attachments, the JS error will be stored in a context value at `staple_failure_{name}`.
939
- * If the file is readable, the file's contents will be stored in a context value at `staple_value_{name}`.
1331
+ * Each attachment becomes one OpenTelemetry log record, correlated to the
1332
+ * phase's span: the file's contents as the body if it can be read, the
1333
+ * reason it could not be read otherwise.
940
1334
  */
941
1335
  stapleFile(name, location) {
942
1336
  this.exceptionAttachments.set(name, location);
@@ -954,9 +1348,29 @@ var DetSysAction = class {
954
1348
  const tmpDir = process.env["RUNNER_TEMP"] || tmpdir();
955
1349
  return path.join(tmpDir, `${this.actionOptions.name}-${randomUUID()}`);
956
1350
  }
957
- addFact(key, value) {
958
- this.facts[key] = value;
1351
+ /**
1352
+ * Describe this run with an attribute.
1353
+ *
1354
+ * The attribute lands on the phase's root span, not on whichever span
1355
+ * happens to be active, because it describes the run as a whole. Set it
1356
+ * whenever the value becomes known: attributes set before the span opens
1357
+ * are replayed onto it.
1358
+ *
1359
+ * Namespace your keys, as OpenTelemetry expects: `detsys.nix.version`, not
1360
+ * `nix_version`.
1361
+ */
1362
+ setAttribute(key, value) {
1363
+ if (this.phaseSpan === void 0) this.pendingAttributes[key] = value;
1364
+ else this.phaseSpan.setAttribute(key, value);
959
1365
  }
1366
+ /**
1367
+ * The diagnostics endpoint for the programs this Action runs, such as
1368
+ * `nix-installer` and `magic-nix-cache`.
1369
+ *
1370
+ * This library reports nothing there. Its own telemetry is OpenTelemetry;
1371
+ * see {@link getTelemetryEnvironment} for putting a child process's
1372
+ * telemetry in this run's trace.
1373
+ */
960
1374
  async getDiagnosticsUrl() {
961
1375
  return await this.idsHost.getDiagnosticsUrl();
962
1376
  }
@@ -974,20 +1388,17 @@ var DetSysAction = class {
974
1388
  getCorrelationHashes() {
975
1389
  return this.identity;
976
1390
  }
977
- recordEvent(eventName, context = {}) {
978
- const prefixedName = eventName === "$feature_flag_called" || eventName === "$groupidentify" ? eventName : `${this.actionOptions.eventPrefix}${eventName}`;
979
- this.events.push({
980
- name: prefixedName,
981
- distinct_id: this.identity.$anon_distinct_id,
982
- uuid: randomUUID(),
983
- timestamp: /* @__PURE__ */ new Date(),
984
- properties: {
985
- ...context,
986
- ...this.identity,
987
- ...this.facts,
988
- ...Object.fromEntries(Object.entries(this.featureEventMetadata).map(([name, variant]) => [`$feature/${name}`, variant]))
989
- }
990
- });
1391
+ /**
1392
+ * Record that something happened, as a span event.
1393
+ *
1394
+ * The event lands on whichever span is active, so that it sits on the
1395
+ * operation that produced it, and on the phase's root span when there is no
1396
+ * nested span in progress.
1397
+ *
1398
+ * Namespace your attribute keys, as OpenTelemetry expects.
1399
+ */
1400
+ addEvent(name, attributes) {
1401
+ (otelApi.trace.getActiveSpan() ?? this.phaseSpan)?.addEvent(name, attributes);
991
1402
  }
992
1403
  /**
993
1404
  * Unpacks the closure returned by `fetchArtifact()`, imports the
@@ -997,7 +1408,7 @@ var DetSysAction = class {
997
1408
  async unpackClosure(bin) {
998
1409
  const artifact = await this.fetchArtifact();
999
1410
  const { stdout } = await promisify(exec)(`cat "${artifact}" | xz -d | nix-store --import`);
1000
- return `${stdout.split(os$1.EOL).at(-2)}/bin/${bin}`;
1411
+ return `${stdout.split(os.EOL).at(-2)}/bin/${bin}`;
1001
1412
  }
1002
1413
  /**
1003
1414
  * Fetches the executable at the URL determined by the `source-*` inputs and
@@ -1005,7 +1416,7 @@ var DetSysAction = class {
1005
1416
  */
1006
1417
  async fetchExecutable() {
1007
1418
  const binaryPath = await this.fetchArtifact();
1008
- await chmod(binaryPath, constants.S_IXUSR | constants.S_IXGRP);
1419
+ await chmod(binaryPath, nodeFs.constants.S_IXUSR | nodeFs.constants.S_IXGRP);
1009
1420
  return binaryPath;
1010
1421
  }
1011
1422
  get isMain() {
@@ -1015,63 +1426,219 @@ var DetSysAction = class {
1015
1426
  return this.executionPhase === "post";
1016
1427
  }
1017
1428
  async executeAsync() {
1429
+ const phaseStartTime = /* @__PURE__ */ new Date();
1018
1430
  try {
1019
- await this.checkIn();
1020
- const correlationHashes = JSON.stringify(this.getCorrelationHashes());
1021
- process.env.DETSYS_CORRELATION = correlationHashes;
1022
- try {
1023
- await writeCorrelationHashes(correlationHashes);
1024
- } catch (error) {
1025
- this.recordEvent(EVENT_STORE_IDENTITY_FAILED, { error: String(error) });
1026
- }
1027
- if (!await this.preflightRequireNix()) {
1028
- this.recordEvent(EVENT_PREFLIGHT_REQUIRE_NIX_DENIED);
1029
- return;
1030
- } else {
1031
- await this.preflightNixStoreInfo();
1032
- await this.preflightNixVersion();
1033
- this.addFact(FACT_NIX_STORE_TRUST, this.nixStoreTrust);
1034
- }
1035
- if (this.isMain) {
1036
- this.recordGroup();
1037
- await this.main();
1038
- await this.preflightNixVersion();
1039
- } else if (this.isPost) await this.post();
1040
- this.addFact(FACT_ENDED_WITH_EXCEPTION, false);
1431
+ this.announceJobTrace(phaseStartTime);
1432
+ await this.startTelemetry();
1433
+ this.startPhaseSpan(phaseStartTime);
1434
+ await this.withPhaseSpanActive(async () => {
1435
+ await withSpan(SPAN_CHECK_IN, async () => {
1436
+ await this.checkIn();
1437
+ });
1438
+ const correlationHashes = JSON.stringify(this.getCorrelationHashes());
1439
+ process.env.DETSYS_CORRELATION = correlationHashes;
1440
+ try {
1441
+ await writeCorrelationHashes(correlationHashes);
1442
+ } catch (error) {
1443
+ this.addEvent(EVENT_STORE_IDENTITY_FAILED, { [semconv.ATTR_EXCEPTION_MESSAGE]: stringifyError$1(error) });
1444
+ }
1445
+ if (!await this.preflightRequireNix()) {
1446
+ this.addEvent(EVENT_PREFLIGHT_REQUIRE_NIX_DENIED);
1447
+ return;
1448
+ } else {
1449
+ await this.preflightNixStoreInfo();
1450
+ await this.preflightNixVersion();
1451
+ this.setAttribute(ATTR_NIX_STORE_TRUST, this.nixStoreTrust);
1452
+ }
1453
+ if (this.isMain) {
1454
+ await this.main();
1455
+ await this.preflightNixVersion();
1456
+ } else if (this.isPost) await this.post();
1457
+ });
1041
1458
  } catch (e) {
1042
- this.addFact(FACT_ENDED_WITH_EXCEPTION, true);
1043
1459
  const reportable = stringifyError$1(e);
1044
- this.addFact(FACT_FINAL_EXCEPTION, reportable);
1045
- if (this.isPost) actionsCore.warning(reportable);
1046
- else actionsCore.setFailed(reportable);
1047
- const doGzip = promisify(gzip);
1048
- const exceptionContext = /* @__PURE__ */ new Map();
1049
- for (const [attachmentLabel, filePath] of this.exceptionAttachments) try {
1050
- const buf = await doGzip(readFileSync(filePath));
1051
- exceptionContext.set(`staple_value_${attachmentLabel}`, buf.toString("base64"));
1052
- } catch (innerError) {
1053
- exceptionContext.set(`staple_failure_${attachmentLabel}`, stringifyError$1(innerError));
1054
- }
1055
- this.recordEvent(EVENT_EXCEPTION, Object.fromEntries(exceptionContext));
1460
+ if (this.phaseSpan !== void 0) recordSpanError(this.phaseSpan, e);
1461
+ if (this.isPost) warning(reportable);
1462
+ else setFailed(reportable);
1463
+ await this.withPhaseSpanActive(async () => {
1464
+ await this.emitAttachments();
1465
+ });
1056
1466
  } finally {
1057
- if (this.isPost) await this.collectBacktraces();
1058
1467
  await this.complete();
1059
1468
  }
1060
1469
  }
1470
+ /**
1471
+ * Run `fn` with the phase's root span as the active span, so anything it
1472
+ * starts is parented into this phase's trace.
1473
+ */
1474
+ async withPhaseSpanActive(fn) {
1475
+ const span = this.phaseSpan;
1476
+ if (span === void 0) return await fn();
1477
+ return await otelApi.context.with(otelApi.trace.setSpan(otelApi.context.active(), span), fn);
1478
+ }
1479
+ /**
1480
+ * Start the OpenTelemetry export.
1481
+ *
1482
+ * All runs export their data.
1483
+ * To stop the export, set `OTEL_SDK_DISABLED` to `true`, or set
1484
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` to an empty value.
1485
+ * The SDK then does not start.
1486
+ * The OpenTelemetry API stays in its no-op state.
1487
+ * Each span and log record then does nothing.
1488
+ * Thus the call sites do not test if the export is on.
1489
+ */
1490
+ async startTelemetry() {
1491
+ this.telemetry.start({
1492
+ serviceName: `${this.actionOptions.name}-action`,
1493
+ serviceVersion: process.env["GITHUB_ACTION_REF"],
1494
+ resourceAttributes: await this.telemetryResourceAttributes()
1495
+ });
1496
+ }
1497
+ /**
1498
+ * Put every Action of this workflow job in one trace.
1499
+ *
1500
+ * A job runs each Action as a process of its own.
1501
+ * Thus the Actions can only agree on a trace through the job's environment.
1502
+ * The first Action to run makes the identity of the job's span and exports it
1503
+ * as `$TRACEPARENT`.
1504
+ * Each later step finds it there: the other Actions, and the programs the
1505
+ * workflow runs, such as Nix.
1506
+ *
1507
+ * The span itself starts and ends in the post phase of the Action that
1508
+ * announced it.
1509
+ * GitHub Actions runs the post phases in the reverse of the order of the main
1510
+ * phases, thus that phase is the last one of the job.
1511
+ * The span then covers the whole job.
1512
+ * See {@link endJobSpan}.
1513
+ *
1514
+ * A `$TRACEPARENT` that is already set belongs to an earlier Action, or to the
1515
+ * system that started the workflow.
1516
+ * Do not change it, and join that trace.
1517
+ */
1518
+ announceJobTrace(startTime) {
1519
+ if (!this.isMain || !exportEnabled()) return;
1520
+ if (process.env[ENV_TRACEPARENT]) return;
1521
+ const traceparent = newTraceparent();
1522
+ actionsCore.exportVariable(ENV_TRACEPARENT, traceparent);
1523
+ actionsCore.saveState(STATE_KEY_JOB_TRACEPARENT, traceparent);
1524
+ actionsCore.saveState(STATE_KEY_JOB_SPAN_START, `${startTime.getTime()}`);
1525
+ }
1526
+ /**
1527
+ * End the job's span, if this Action is the one that announced it.
1528
+ *
1529
+ * The span also starts here.
1530
+ * A span belongs to the process that ends it, and the process that made the
1531
+ * announcement stopped long ago.
1532
+ * See {@link announceJobTrace}.
1533
+ */
1534
+ endJobSpan() {
1535
+ if (!this.isPost) return;
1536
+ const traceparent = actionsCore.getState(STATE_KEY_JOB_TRACEPARENT);
1537
+ if (traceparent === "") return;
1538
+ const startTime = parseInt(actionsCore.getState(STATE_KEY_JOB_SPAN_START), 10);
1539
+ this.telemetry.startAnnouncedSpan(SPAN_JOB, traceparent, new Date(Number.isFinite(startTime) ? startTime : Date.now()))?.end();
1540
+ }
1541
+ /**
1542
+ * Start the root span of this execution phase.
1543
+ *
1544
+ * The span starts at the moment the phase did, and thus covers the start of
1545
+ * the SDK, which comes before it.
1546
+ *
1547
+ * `main` and `post` are separate processes.
1548
+ * Thus the main phase saves the identity of its span in the Action's state,
1549
+ * and the post phase makes its span a child of it.
1550
+ * A `$TRACEPARENT` in the environment is the span of the workflow job, or of
1551
+ * the system that started the workflow.
1552
+ */
1553
+ startPhaseSpan(startTime) {
1554
+ if (!this.telemetry.enabled) return;
1555
+ const parent = actionsCore.getState(STATE_KEY_TRACEPARENT) || process.env[ENV_TRACEPARENT] || void 0;
1556
+ const span = getTracer().startSpan(`${this.actionOptions.name}:${this.executionPhase}`, { startTime }, contextFromTraceparent(parent));
1557
+ span.setAttributes(this.pendingAttributes);
1558
+ this.pendingAttributes = {};
1559
+ const traceparent = traceparentOf(span);
1560
+ if (traceparent !== void 0) {
1561
+ process.env[ENV_TRACEPARENT] = traceparent;
1562
+ if (this.isMain) actionsCore.saveState(STATE_KEY_TRACEPARENT, traceparent);
1563
+ }
1564
+ this.phaseSpan = span;
1565
+ }
1566
+ /**
1567
+ * The stable, run-scoped attributes attached to every span and log record.
1568
+ *
1569
+ * The correlation data here is hashed and does not identify a repository,
1570
+ * an organization, or a person.
1571
+ */
1572
+ async telemetryResourceAttributes() {
1573
+ const details = await this.systemDetails;
1574
+ return {
1575
+ [semconvIncubating.ATTR_OS_TYPE]: osType(),
1576
+ [semconvIncubating.ATTR_HOST_ARCH]: hostArch(),
1577
+ ...details?.name === void 0 || details.name === "unknown" ? {} : { [semconvIncubating.ATTR_OS_NAME]: details.name },
1578
+ ...details?.version === void 0 || details.version === "unknown" ? {} : { [semconvIncubating.ATTR_OS_VERSION]: details.version },
1579
+ [ATTR_PROJECT]: this.actionOptions.name,
1580
+ [ATTR_IDS_PROJECT]: this.actionOptions.idsProjectName,
1581
+ [ATTR_EXECUTION_PHASE]: this.executionPhase,
1582
+ [ATTR_CROSS_PHASE_ID]: this.getCrossPhaseId(),
1583
+ [ATTR_ANONYMOUS_ID]: this.identity.$anon_distinct_id,
1584
+ [ATTR_CORRELATION_SOURCE]: this.identity.correlation_source,
1585
+ [ATTR_ARCH_OS]: this.archOs,
1586
+ [ATTR_NIX_SYSTEM]: this.nixSystem,
1587
+ [ATTR_GITHUB_EVENT_NAME]: process.env["GITHUB_EVENT_NAME"],
1588
+ [ATTR_GITHUB_ACTION_REPOSITORY]: process.env["GITHUB_ACTION_REPOSITORY"],
1589
+ [ATTR_GITHUB_REPOSITORY_HASH]: this.identity.github_repository_hash,
1590
+ [ATTR_GITHUB_ORGANIZATION_HASH]: this.identity.$groups["github_organization"],
1591
+ [ATTR_GITHUB_WORKFLOW_HASH]: this.identity.github_workflow_hash,
1592
+ [ATTR_GITHUB_WORKFLOW_JOB_HASH]: this.identity.github_workflow_job_hash,
1593
+ [ATTR_GITHUB_WORKFLOW_RUN_HASH]: this.identity.github_workflow_run_hash,
1594
+ [ATTR_GITHUB_WORKFLOW_RUN_DIFFERENTIATOR_HASH]: this.identity.github_workflow_run_differentiator_hash
1595
+ };
1596
+ }
1597
+ /**
1598
+ * The W3C `traceparent` identifying the span currently in progress.
1599
+ *
1600
+ * Hand this to a child process -- as `$TRACEPARENT` -- so that its own
1601
+ * OpenTelemetry data joins this Action's trace. Returns undefined when
1602
+ * OpenTelemetry export is disabled for this run.
1603
+ */
1604
+ getTraceparent() {
1605
+ return traceparentOf(otelApi.trace.getActiveSpan() ?? this.phaseSpan);
1606
+ }
1607
+ /**
1608
+ * The environment variables that let a child process add data to this
1609
+ * Action's trace: the current `$TRACEPARENT` and the OTLP export settings.
1610
+ *
1611
+ * Add these variables to the environment of each child process to trace.
1612
+ * A child that inherits this process's environment already has the OTLP
1613
+ * settings; only `$TRACEPARENT` changes as the run proceeds.
1614
+ *
1615
+ * The result is empty if the OpenTelemetry export is off.
1616
+ * Thus it is always safe to add them.
1617
+ */
1618
+ async getTelemetryEnvironment() {
1619
+ if (!this.telemetry.enabled) return {};
1620
+ const environment = otlpExportEnvironment();
1621
+ const traceparent = this.getTraceparent();
1622
+ if (traceparent !== void 0) environment[ENV_TRACEPARENT] = traceparent;
1623
+ return environment;
1624
+ }
1061
1625
  async getClient() {
1062
1626
  return await this.idsHost.getGot((incitingError, prevUrl, nextUrl) => {
1063
1627
  this.recordPlausibleTimeout(incitingError);
1064
- this.recordEvent("ids-failover", {
1065
- previousUrl: prevUrl.toString(),
1066
- nextUrl: nextUrl.toString()
1628
+ this.addEvent(EVENT_IDS_FAILOVER, {
1629
+ "detsys.ids.previous_url": prevUrl.toString(),
1630
+ "detsys.ids.next_url": nextUrl.toString()
1067
1631
  });
1068
1632
  });
1069
1633
  }
1634
+ /**
1635
+ * Check in, and tell the user about the incidents and the maintenance the
1636
+ * check-in reports.
1637
+ */
1070
1638
  async checkIn() {
1071
1639
  const checkin = await this.requestCheckIn();
1072
1640
  if (checkin === void 0) return;
1073
1641
  this.features = checkin.options;
1074
- for (const [key, feature] of Object.entries(this.features)) this.featureEventMetadata[key] = feature.variant;
1075
1642
  const impactSymbol = /* @__PURE__ */ new Map([
1076
1643
  ["none", "⚪"],
1077
1644
  ["maintenance", "🛠️"],
@@ -1092,24 +1659,60 @@ var DetSysAction = class {
1092
1659
  }
1093
1660
  }
1094
1661
  }
1662
+ /**
1663
+ * The variant of a feature flag this run resolved, if the check-in returned
1664
+ * one.
1665
+ *
1666
+ * Each variant this Action asks for becomes an attribute of the run, under
1667
+ * `detsys.feature.`, so the telemetry can be sliced by the flags that
1668
+ * changed what the run did.
1669
+ */
1095
1670
  getFeature(name) {
1096
1671
  if (!this.features.hasOwnProperty(name)) return;
1097
- const result = this.features[name];
1098
- if (result === void 0) return;
1099
- this.recordEvent("$feature_flag_called", {
1100
- $feature_flag: name,
1101
- $feature_flag_response: result.variant
1102
- });
1103
- return result;
1104
- }
1105
- recordGroup() {
1106
- const ghorg_hash = this.identity.$groups["github_organization"];
1107
- const ghorg_name = process.env["GITHUB_REPOSITORY_OWNER"];
1108
- if (ghorg_hash !== void 0 && ghorg_name !== void 0) this.recordEvent("$groupidentify", {
1109
- $group_type: "github_organization",
1110
- $group_key: ghorg_hash,
1111
- $group_set: { name: ghorg_name }
1112
- });
1672
+ const feature = this.features[name];
1673
+ this.setAttribute(`${ATTR_FEATURE_PREFIX}${name}`, feature.variant);
1674
+ return feature;
1675
+ }
1676
+ /**
1677
+ * The person properties the check-in evaluates feature flags against.
1678
+ *
1679
+ * These names are the flag-targeting contract with the feature flag
1680
+ * service, which is why they keep their `$`-prefixed spelling. They are not
1681
+ * telemetry: nothing here is reported anywhere. The telemetry for this run
1682
+ * is OpenTelemetry, and it names the same values the way OpenTelemetry
1683
+ * does.
1684
+ */
1685
+ async checkInPersonProperties() {
1686
+ const properties = {
1687
+ ci: "github",
1688
+ $lib: "idslib",
1689
+ $lib_version: "1.0",
1690
+ $app_name: `${this.actionOptions.name}/action`,
1691
+ project: this.actionOptions.name,
1692
+ ids_project: this.actionOptions.idsProjectName,
1693
+ arch_os: this.archOs,
1694
+ nix_system: this.nixSystem,
1695
+ execution_phase: this.executionPhase
1696
+ };
1697
+ for (const [target, variable] of [
1698
+ ["github_action_ref", "GITHUB_ACTION_REF"],
1699
+ ["github_action_repository", "GITHUB_ACTION_REPOSITORY"],
1700
+ ["github_event_name", "GITHUB_EVENT_NAME"],
1701
+ ["$os", "RUNNER_OS"],
1702
+ ["arch", "RUNNER_ARCH"]
1703
+ ]) {
1704
+ const value = process.env[variable];
1705
+ if (value) properties[target] = value;
1706
+ }
1707
+ const details = await this.systemDetails;
1708
+ if (details !== void 0) {
1709
+ if (details.name !== "unknown") properties.$os = details.name;
1710
+ if (details.version !== "unknown") properties.$os_version = details.version;
1711
+ }
1712
+ return {
1713
+ ...properties,
1714
+ ...this.identity
1715
+ };
1113
1716
  }
1114
1717
  /**
1115
1718
  * Check in to install.determinate.systems, to accomplish three things:
@@ -1128,11 +1731,7 @@ var DetSysAction = class {
1128
1731
  distinct_id: this.identity.$anon_distinct_id,
1129
1732
  anon_distinct_id: this.identity.$anon_distinct_id,
1130
1733
  groups: this.identity.$groups,
1131
- person_properties: {
1132
- ci: "github",
1133
- ...this.identity,
1134
- ...this.facts
1135
- }
1734
+ person_properties: await this.checkInPersonProperties()
1136
1735
  };
1137
1736
  return await (await this.getClient()).post(checkInUrl, {
1138
1737
  json: props,
@@ -1147,12 +1746,12 @@ var DetSysAction = class {
1147
1746
  }
1148
1747
  recordPlausibleTimeout(e) {
1149
1748
  if (e instanceof TimeoutError && "timings" in e && "request" in e) {
1150
- const reportContext = {
1151
- url: e.request.requestUrl?.toString(),
1152
- retry_count: e.request.retryCount
1749
+ const attributes = {
1750
+ [semconv.ATTR_URL_FULL]: e.request.requestUrl?.toString(),
1751
+ [semconv.ATTR_HTTP_REQUEST_RESEND_COUNT]: e.request.retryCount
1153
1752
  };
1154
- for (const [key, value] of Object.entries(e.timings.phases)) if (Number.isFinite(value)) reportContext[`timing_phase_${key}`] = value;
1155
- this.recordEvent("timeout", reportContext);
1753
+ for (const [key, value] of Object.entries(e.timings.phases)) if (Number.isFinite(value)) attributes[`detsys.http.timing.${key}`] = value;
1754
+ this.addEvent(EVENT_REQUEST_TIMEOUT, attributes);
1156
1755
  }
1157
1756
  }
1158
1757
  /**
@@ -1170,49 +1769,54 @@ var DetSysAction = class {
1170
1769
  async fetchArtifact() {
1171
1770
  const sourceBinary = getStringOrNull("source-binary");
1172
1771
  if (sourceBinary !== null && sourceBinary !== "") {
1173
- actionsCore.debug(`Using the provided source binary at ${sourceBinary}`);
1772
+ debug(`Using the provided source binary at ${sourceBinary}`);
1174
1773
  return sourceBinary;
1175
1774
  }
1176
- const expectedArtifactHash = await this.resolveExpectedArtifactHash();
1177
- actionsCore.startGroup(`Downloading ${this.actionOptions.name} for ${this.architectureFetchSuffix}`);
1178
- try {
1179
- actionsCore.info(`Fetching from ${await this.getSourceUrl()}`);
1180
- const correlatedUrl = await this.getSourceUrl();
1181
- correlatedUrl.searchParams.set("ci", "github");
1182
- correlatedUrl.searchParams.set("correlation", JSON.stringify(this.identity));
1183
- const versionCheckup = await (await this.getClient()).head(correlatedUrl);
1184
- if (versionCheckup.headers.etag) {
1185
- const v = versionCheckup.headers.etag;
1186
- this.addFact(FACT_SOURCE_URL_ETAG, v);
1187
- actionsCore.debug(`Checking the tool cache for ${await this.getSourceUrl()} at ${v}`);
1188
- const cached = await this.getCachedVersion(v, expectedArtifactHash);
1189
- if (cached) {
1190
- this.facts[FACT_ARTIFACT_FETCHED_FROM_CACHE] = true;
1191
- actionsCore.debug(`Tool cache hit.`);
1192
- await this.verifyArtifactHash(cached, expectedArtifactHash);
1193
- return cached;
1775
+ return await withSpan("fetch_artifact", async (span) => {
1776
+ const expectedArtifactHash = await this.resolveExpectedArtifactHash();
1777
+ actionsCore.startGroup(`Downloading ${this.actionOptions.name} for ${this.architectureFetchSuffix}`);
1778
+ try {
1779
+ info(`Fetching from ${await this.getSourceUrl()}`);
1780
+ const correlatedUrl = await this.getSourceUrl();
1781
+ correlatedUrl.searchParams.set("ci", "github");
1782
+ correlatedUrl.searchParams.set("correlation", JSON.stringify(this.identity));
1783
+ const versionCheckup = await (await this.getClient()).head(correlatedUrl);
1784
+ if (versionCheckup.headers.etag) {
1785
+ const v = versionCheckup.headers.etag;
1786
+ this.setAttribute(ATTR_SOURCE_ETAG, v);
1787
+ debug(`Checking the tool cache for ${await this.getSourceUrl()} at ${v}`);
1788
+ const cached = await this.getCachedVersion(v, expectedArtifactHash);
1789
+ if (cached) {
1790
+ span.setAttribute(ATTR_ARTIFACT_CACHE_HIT, true);
1791
+ debug(`Tool cache hit.`);
1792
+ await this.verifyArtifactHash(cached, expectedArtifactHash);
1793
+ return cached;
1794
+ }
1194
1795
  }
1195
- }
1196
- this.facts[FACT_ARTIFACT_FETCHED_FROM_CACHE] = false;
1197
- actionsCore.debug(`No match from the cache, re-fetching from the redirect: ${versionCheckup.url}`);
1198
- const destFile = this.getTemporaryName();
1199
- const fetchStream = await this.downloadFile(new URL(versionCheckup.url), destFile);
1200
- await this.verifyArtifactHash(destFile, expectedArtifactHash);
1201
- if (fetchStream.response?.headers.etag) {
1202
- const v = fetchStream.response.headers.etag;
1203
- try {
1204
- await this.saveCachedVersion(v, destFile, expectedArtifactHash);
1205
- } catch (e) {
1206
- actionsCore.debug(`Error caching the artifact: ${stringifyError$1(e)}`);
1796
+ span.setAttribute(ATTR_ARTIFACT_CACHE_HIT, false);
1797
+ debug(`No match from the cache, re-fetching from the redirect: ${versionCheckup.url}`);
1798
+ const destFile = this.getTemporaryName();
1799
+ const fetchStream = await this.downloadFile(new URL(versionCheckup.url), destFile);
1800
+ await this.verifyArtifactHash(destFile, expectedArtifactHash);
1801
+ if (fetchStream.response?.headers.etag) {
1802
+ const v = fetchStream.response.headers.etag;
1803
+ try {
1804
+ await this.saveCachedVersion(v, destFile, expectedArtifactHash);
1805
+ } catch (e) {
1806
+ debug(`Error caching the artifact: ${stringifyError$1(e)}`);
1807
+ }
1207
1808
  }
1809
+ return destFile;
1810
+ } catch (e) {
1811
+ this.recordPlausibleTimeout(e);
1812
+ throw e;
1813
+ } finally {
1814
+ actionsCore.endGroup();
1208
1815
  }
1209
- return destFile;
1210
- } catch (e) {
1211
- this.recordPlausibleTimeout(e);
1212
- throw e;
1213
- } finally {
1214
- actionsCore.endGroup();
1215
- }
1816
+ }, {
1817
+ [ATTR_ARTIFACT_NAME]: this.actionOptions.name,
1818
+ [ATTR_ARTIFACT_FETCH_SUFFIX]: this.architectureFetchSuffix
1819
+ });
1216
1820
  }
1217
1821
  /**
1218
1822
  * Read the `source-checksums-url` and `source-checksums-sha256` inputs and,
@@ -1228,7 +1832,7 @@ var DetSysAction = class {
1228
1832
  if (checksumsUrl === null || checksumsSha256 === null) throw new Error("`source-checksums-url` and `source-checksums-sha256` must be set together");
1229
1833
  assertChecksumSourceIsPinned(this.sourceParameters);
1230
1834
  const expectedFileHash = checksumsSha256.toLowerCase();
1231
- this.addFact(FACT_SOURCE_CHECKSUMS_SHA256, expectedFileHash);
1835
+ this.setAttribute(ATTR_SOURCE_CHECKSUMS_SHA256, expectedFileHash);
1232
1836
  const parsedUrl = new URL(checksumsUrl);
1233
1837
  const safeUrl = parsedUrl.origin + parsedUrl.pathname;
1234
1838
  actionsCore.info(`Fetching checksums file from ${safeUrl}`);
@@ -1257,13 +1861,16 @@ var DetSysAction = class {
1257
1861
  if (this.strictMode) actionsCore.setFailed(`strict mode failure: ${msg}`);
1258
1862
  }
1259
1863
  async downloadFile(url, destination) {
1864
+ return await withSpan("download_file", async () => this.download(url, destination));
1865
+ }
1866
+ async download(url, destination) {
1260
1867
  const client = await this.getClient();
1261
1868
  return new Promise((resolve, reject) => {
1262
1869
  let writeStream;
1263
1870
  let failed = false;
1264
1871
  const retry = (stream) => {
1265
1872
  if (writeStream) writeStream.destroy();
1266
- writeStream = createWriteStream(destination, {
1873
+ writeStream = nodeFs.createWriteStream(destination, {
1267
1874
  encoding: "binary",
1268
1875
  mode: 493
1269
1876
  });
@@ -1283,8 +1890,10 @@ var DetSysAction = class {
1283
1890
  });
1284
1891
  }
1285
1892
  async complete() {
1286
- this.recordEvent(`complete_${this.executionPhase}`);
1287
- await this.submitEvents();
1893
+ this.phaseSpan?.end();
1894
+ this.phaseSpan = void 0;
1895
+ this.endJobSpan();
1896
+ await this.telemetry.shutdown();
1288
1897
  }
1289
1898
  async getCheckInUrl() {
1290
1899
  const checkInUrl = await this.idsHost.getDynamicRootUrl();
@@ -1295,7 +1904,7 @@ var DetSysAction = class {
1295
1904
  async getSourceUrl() {
1296
1905
  const p = this.sourceParameters;
1297
1906
  if (p.url) {
1298
- this.addFact(FACT_SOURCE_URL, p.url);
1907
+ this.setAttribute(ATTR_SOURCE_URL, p.url);
1299
1908
  return new URL(p.url);
1300
1909
  }
1301
1910
  const fetchUrl = await this.idsHost.getRootUrl();
@@ -1306,7 +1915,7 @@ var DetSysAction = class {
1306
1915
  else if (p.revision) fetchUrl.pathname += `/rev/${p.revision}`;
1307
1916
  else fetchUrl.pathname += `/stable`;
1308
1917
  fetchUrl.pathname += `/${this.architectureFetchSuffix}`;
1309
- this.addFact(FACT_SOURCE_URL, fetchUrl.toString());
1918
+ this.setAttribute(ATTR_SOURCE_URL, fetchUrl.toString());
1310
1919
  return fetchUrl;
1311
1920
  }
1312
1921
  cacheKey(version, expectedHash) {
@@ -1315,179 +1924,188 @@ var DetSysAction = class {
1315
1924
  return `determinatesystem-${this.actionOptions.name}-${this.architectureFetchSuffix}-${cleanedVersion}${hashSuffix}`;
1316
1925
  }
1317
1926
  async getCachedVersion(version, expectedHash) {
1318
- const startCwd = process.cwd();
1319
- try {
1320
- const tempDir = this.getTemporaryName();
1321
- await mkdir(tempDir);
1322
- process.chdir(tempDir);
1323
- process.env.GITHUB_WORKSPACE_BACKUP = process.env.GITHUB_WORKSPACE;
1324
- delete process.env.GITHUB_WORKSPACE;
1325
- if (await actionsCache.restoreCache([this.actionOptions.name], this.cacheKey(version, expectedHash), [], void 0, true)) {
1326
- this.recordEvent(EVENT_ARTIFACT_CACHE_HIT);
1327
- return `${tempDir}/${this.actionOptions.name}`;
1927
+ return await withSpan("artifact_cache_restore", async (span) => {
1928
+ const startCwd = process.cwd();
1929
+ try {
1930
+ const tempDir = this.getTemporaryName();
1931
+ await mkdir(tempDir);
1932
+ process.chdir(tempDir);
1933
+ process.env.GITHUB_WORKSPACE_BACKUP = process.env.GITHUB_WORKSPACE;
1934
+ delete process.env.GITHUB_WORKSPACE;
1935
+ if (await actionsCache.restoreCache([this.actionOptions.name], this.cacheKey(version, expectedHash), [], void 0, true)) {
1936
+ span.setAttribute(ATTR_ARTIFACT_CACHE_HIT, true);
1937
+ return `${tempDir}/${this.actionOptions.name}`;
1938
+ }
1939
+ span.setAttribute(ATTR_ARTIFACT_CACHE_HIT, false);
1940
+ return;
1941
+ } finally {
1942
+ process.env.GITHUB_WORKSPACE = process.env.GITHUB_WORKSPACE_BACKUP;
1943
+ delete process.env.GITHUB_WORKSPACE_BACKUP;
1944
+ process.chdir(startCwd);
1328
1945
  }
1329
- this.recordEvent(EVENT_ARTIFACT_CACHE_MISS);
1330
- return;
1331
- } finally {
1332
- process.env.GITHUB_WORKSPACE = process.env.GITHUB_WORKSPACE_BACKUP;
1333
- delete process.env.GITHUB_WORKSPACE_BACKUP;
1334
- process.chdir(startCwd);
1335
- }
1946
+ });
1336
1947
  }
1337
1948
  async saveCachedVersion(version, toolPath, expectedHash) {
1338
- const startCwd = process.cwd();
1339
- try {
1340
- const tempDir = this.getTemporaryName();
1341
- await mkdir(tempDir);
1342
- process.chdir(tempDir);
1343
- await copyFile(toolPath, `${tempDir}/${this.actionOptions.name}`);
1344
- process.env.GITHUB_WORKSPACE_BACKUP = process.env.GITHUB_WORKSPACE;
1345
- delete process.env.GITHUB_WORKSPACE;
1346
- await actionsCache.saveCache([this.actionOptions.name], this.cacheKey(version, expectedHash), void 0, true);
1347
- this.recordEvent(EVENT_ARTIFACT_CACHE_PERSIST);
1348
- } finally {
1349
- process.env.GITHUB_WORKSPACE = process.env.GITHUB_WORKSPACE_BACKUP;
1350
- delete process.env.GITHUB_WORKSPACE_BACKUP;
1351
- process.chdir(startCwd);
1352
- }
1353
- }
1354
- collectBacktraceSetup() {
1355
- if (!process.env.DETSYS_BACKTRACE_COLLECTOR) {
1356
- actionsCore.exportVariable("DETSYS_BACKTRACE_COLLECTOR", this.getCrossPhaseId());
1357
- actionsCore.saveState(STATE_BACKTRACE_START_TIMESTAMP, Date.now());
1358
- }
1949
+ return await withSpan("artifact_cache_persist", async () => {
1950
+ const startCwd = process.cwd();
1951
+ try {
1952
+ const tempDir = this.getTemporaryName();
1953
+ await mkdir(tempDir);
1954
+ process.chdir(tempDir);
1955
+ await copyFile(toolPath, `${tempDir}/${this.actionOptions.name}`);
1956
+ process.env.GITHUB_WORKSPACE_BACKUP = process.env.GITHUB_WORKSPACE;
1957
+ delete process.env.GITHUB_WORKSPACE;
1958
+ await actionsCache.saveCache([this.actionOptions.name], this.cacheKey(version, expectedHash), void 0, true);
1959
+ } finally {
1960
+ process.env.GITHUB_WORKSPACE = process.env.GITHUB_WORKSPACE_BACKUP;
1961
+ delete process.env.GITHUB_WORKSPACE_BACKUP;
1962
+ process.chdir(startCwd);
1963
+ }
1964
+ });
1359
1965
  }
1360
- async collectBacktraces() {
1361
- try {
1362
- if (process.env.DETSYS_BACKTRACE_COLLECTOR !== this.getCrossPhaseId()) return;
1363
- const backtraces = await collectBacktraces(this.actionOptions.binaryNamePrefixes, this.actionOptions.binaryNamesDenyList, parseInt(actionsCore.getState(STATE_BACKTRACE_START_TIMESTAMP)));
1364
- actionsCore.debug(`Backtraces identified: ${backtraces.size}`);
1365
- if (backtraces.size > 0) this.recordEvent(EVENT_BACKTRACES, Object.fromEntries(backtraces));
1366
- } catch (innerError) {
1367
- actionsCore.debug(`Error collecting backtraces: ${stringifyError$1(innerError)}`);
1966
+ /**
1967
+ * Emit the files `stapleFile` collected, as log records correlated to this
1968
+ * phase's span. The Action has already failed by the time this runs.
1969
+ */
1970
+ async emitAttachments() {
1971
+ for (const [name, location] of this.exceptionAttachments) {
1972
+ const attributes = {
1973
+ [ATTR_ATTACHMENT_NAME]: name,
1974
+ [ATTR_ATTACHMENT_PATH]: location.toString()
1975
+ };
1976
+ try {
1977
+ emitLogRecord("error", await readFile(location, "utf-8"), attributes);
1978
+ } catch (innerError) {
1979
+ emitLogRecord("error", `Attachment unavailable`, {
1980
+ ...attributes,
1981
+ [semconv.ATTR_EXCEPTION_MESSAGE]: stringifyError$1(innerError)
1982
+ });
1983
+ }
1368
1984
  }
1369
1985
  }
1370
1986
  async preflightRequireNix() {
1371
- let nixLocation;
1372
- const pathParts = (process.env["PATH"] || "").split(":");
1373
- for (const location of pathParts) {
1374
- const candidateNix = path.join(location, "nix");
1375
- try {
1376
- await fs.access(candidateNix, fs.constants.X_OK);
1377
- actionsCore.debug(`Found Nix at ${candidateNix}`);
1378
- nixLocation = candidateNix;
1379
- break;
1380
- } catch {
1381
- actionsCore.debug(`Nix not at ${candidateNix}`);
1987
+ return await withSpan("preflight_require_nix", async () => {
1988
+ let nixLocation;
1989
+ const pathParts = (process.env["PATH"] || "").split(":");
1990
+ for (const location of pathParts) {
1991
+ const candidateNix = path.join(location, "nix");
1992
+ try {
1993
+ await fs$1.access(candidateNix, fs$1.constants.X_OK);
1994
+ debug(`Found Nix at ${candidateNix}`);
1995
+ nixLocation = candidateNix;
1996
+ break;
1997
+ } catch {
1998
+ actionsCore.debug(`Nix not at ${candidateNix}`);
1999
+ }
1382
2000
  }
1383
- }
1384
- this.addFact(FACT_NIX_LOCATION, nixLocation || "");
1385
- if (this.actionOptions.requireNix === "ignore") return true;
1386
- if (actionsCore.getState(STATE_KEY_NIX_NOT_FOUND) === STATE_NOT_FOUND) return false;
1387
- if (nixLocation !== void 0) return true;
1388
- actionsCore.saveState(STATE_KEY_NIX_NOT_FOUND, STATE_NOT_FOUND);
1389
- switch (this.actionOptions.requireNix) {
1390
- case "fail":
1391
- actionsCore.setFailed(["This action can only be used when Nix is installed.", "Add `- uses: DeterminateSystems/determinate-nix-action@v3` earlier in your workflow."].join(" "));
1392
- break;
1393
- case "warn": actionsCore.warning(["This action is in no-op mode because Nix is not installed.", "Add `- uses: DeterminateSystems/determinate-nix-action@v3` earlier in your workflow."].join(" "));
1394
- }
1395
- return false;
2001
+ this.setAttribute(ATTR_NIX_LOCATION, nixLocation || "");
2002
+ if (this.actionOptions.requireNix === "ignore") return true;
2003
+ if (actionsCore.getState(STATE_KEY_NIX_NOT_FOUND) === STATE_NOT_FOUND) return false;
2004
+ if (nixLocation !== void 0) return true;
2005
+ actionsCore.saveState(STATE_KEY_NIX_NOT_FOUND, STATE_NOT_FOUND);
2006
+ switch (this.actionOptions.requireNix) {
2007
+ case "fail":
2008
+ setFailed(["This action can only be used when Nix is installed.", "Add `- uses: DeterminateSystems/determinate-nix-action@v3` earlier in your workflow."].join(" "));
2009
+ break;
2010
+ case "warn": warning(["This action is in no-op mode because Nix is not installed.", "Add `- uses: DeterminateSystems/determinate-nix-action@v3` earlier in your workflow."].join(" "));
2011
+ }
2012
+ return false;
2013
+ });
1396
2014
  }
1397
2015
  async preflightNixStoreInfo() {
1398
- let output = "";
1399
- const options = {};
1400
- options.silent = true;
1401
- options.listeners = { stdout: (data) => {
1402
- output += data.toString();
1403
- } };
1404
- try {
1405
- output = "";
1406
- await exec$1.exec("nix", [
1407
- "store",
1408
- "info",
1409
- "--json"
1410
- ], options);
1411
- this.addFact(FACT_NIX_STORE_CHECK_METHOD, "info");
1412
- } catch {
2016
+ return await withSpan("preflight_nix_store_info", async (span) => {
2017
+ let output = "";
2018
+ const options = {};
2019
+ options.silent = true;
2020
+ options.listeners = { stdout: (data) => {
2021
+ output += data.toString();
2022
+ } };
1413
2023
  try {
1414
2024
  output = "";
1415
2025
  await exec$1.exec("nix", [
1416
2026
  "store",
1417
- "ping",
2027
+ "info",
1418
2028
  "--json"
1419
2029
  ], options);
1420
- this.addFact(FACT_NIX_STORE_CHECK_METHOD, "ping");
2030
+ this.setAttribute(ATTR_NIX_STORE_CHECK_METHOD, "info");
1421
2031
  } catch {
1422
- this.addFact(FACT_NIX_STORE_CHECK_METHOD, "none");
1423
- return;
2032
+ try {
2033
+ output = "";
2034
+ await exec$1.exec("nix", [
2035
+ "store",
2036
+ "ping",
2037
+ "--json"
2038
+ ], options);
2039
+ this.setAttribute(ATTR_NIX_STORE_CHECK_METHOD, "ping");
2040
+ } catch {
2041
+ this.setAttribute(ATTR_NIX_STORE_CHECK_METHOD, "none");
2042
+ return;
2043
+ }
1424
2044
  }
1425
- }
1426
- try {
1427
- const parsed = JSON.parse(output);
1428
- if (parsed.trusted === true || parsed.trusted === 1) this.nixStoreTrust = "trusted";
1429
- else if (parsed.trusted === false || parsed.trusted === 0) this.nixStoreTrust = "untrusted";
1430
- else if (parsed.trusted !== void 0) this.addFact(FACT_NIX_STORE_CHECK_ERROR, `Mysterious trusted value: ${JSON.stringify(parsed.trusted)}`);
1431
- this.addFact(FACT_NIX_STORE_VERSION, JSON.stringify(parsed.version));
1432
- } catch (e) {
1433
- this.addFact(FACT_NIX_STORE_CHECK_ERROR, stringifyError$1(e));
1434
- }
2045
+ try {
2046
+ const parsed = JSON.parse(output);
2047
+ if (parsed.trusted === true || parsed.trusted === 1) this.nixStoreTrust = "trusted";
2048
+ else if (parsed.trusted === false || parsed.trusted === 0) this.nixStoreTrust = "untrusted";
2049
+ else if (parsed.trusted !== void 0) this.setAttribute(ATTR_NIX_STORE_CHECK_ERROR, `Mysterious trusted value: ${JSON.stringify(parsed.trusted)}`);
2050
+ this.setAttribute(ATTR_NIX_STORE_VERSION, JSON.stringify(parsed.version));
2051
+ } catch (e) {
2052
+ this.setAttribute(ATTR_NIX_STORE_CHECK_ERROR, stringifyError$1(e));
2053
+ }
2054
+ span.setAttribute(ATTR_NIX_STORE_TRUST, this.nixStoreTrust);
2055
+ });
1435
2056
  }
1436
2057
  async preflightNixVersion() {
1437
- let output = "unknown";
1438
- try {
1439
- ({stdout: output} = await exec$1.getExecOutput("nix", ["--version"], { silent: true }));
1440
- output = output.trim() || "unknown";
1441
- } catch {}
1442
- this.addFact(FACT_NIX_VERSION, output);
1443
- }
1444
- async submitEvents() {
1445
- const diagnosticsUrl = await this.idsHost.getDiagnosticsUrl();
1446
- if (diagnosticsUrl === void 0) {
1447
- actionsCore.debug("Diagnostics are disabled. Not sending the following events:");
1448
- actionsCore.debug(JSON.stringify(this.events, void 0, 2));
1449
- return;
1450
- }
1451
- const batch = {
1452
- sent_at: /* @__PURE__ */ new Date(),
1453
- batch: this.events
1454
- };
1455
- try {
1456
- await (await this.getClient()).post(diagnosticsUrl, {
1457
- json: batch,
1458
- timeout: { request: DIAGNOSTIC_ENDPOINT_TIMEOUT_MS }
1459
- });
1460
- } catch (err) {
1461
- this.recordPlausibleTimeout(err);
1462
- actionsCore.debug(`Error submitting diagnostics event to ${diagnosticsUrl}: ${stringifyError$1(err)}`);
1463
- }
1464
- this.events = [];
2058
+ return await withSpan("preflight_nix_version", async (span) => {
2059
+ let output = "unknown";
2060
+ try {
2061
+ ({stdout: output} = await exec$1.getExecOutput("nix", ["--version"], { silent: true }));
2062
+ output = output.trim() || "unknown";
2063
+ } catch {}
2064
+ this.setAttribute(ATTR_NIX_VERSION, output);
2065
+ span.setAttribute(ATTR_NIX_VERSION, output);
2066
+ });
1465
2067
  }
1466
2068
  };
1467
2069
  function stringifyError$1(error) {
1468
2070
  return error instanceof Error || typeof error == "string" ? error.toString() : JSON.stringify(error);
1469
2071
  }
2072
+ /**
2073
+ * The runner's operating system, as `os.type` spells it.
2074
+ */
2075
+ function osType() {
2076
+ switch (platform) {
2077
+ case "win32": return semconvIncubating.OS_TYPE_VALUE_WINDOWS;
2078
+ case "darwin": return semconvIncubating.OS_TYPE_VALUE_DARWIN;
2079
+ case "linux": return semconvIncubating.OS_TYPE_VALUE_LINUX;
2080
+ default: return platform;
2081
+ }
2082
+ }
2083
+ /**
2084
+ * The runner's architecture, as `host.arch` spells it.
2085
+ */
2086
+ function hostArch() {
2087
+ switch (arch) {
2088
+ case "x64": return semconvIncubating.HOST_ARCH_VALUE_AMD64;
2089
+ case "arm64": return semconvIncubating.HOST_ARCH_VALUE_ARM64;
2090
+ case "ia32": return semconvIncubating.HOST_ARCH_VALUE_X86;
2091
+ case "arm": return semconvIncubating.HOST_ARCH_VALUE_ARM32;
2092
+ default: return arch;
2093
+ }
2094
+ }
1470
2095
  function makeOptionsConfident(actionOptions) {
1471
2096
  const idsProjectName = actionOptions.idsProjectName ?? actionOptions.name;
1472
2097
  const finalOpts = {
1473
2098
  name: actionOptions.name,
1474
2099
  idsProjectName,
1475
- eventPrefix: actionOptions.eventPrefix || "action:",
1476
2100
  fetchStyle: actionOptions.fetchStyle,
1477
2101
  legacySourcePrefix: actionOptions.legacySourcePrefix,
1478
- requireNix: actionOptions.requireNix,
1479
- binaryNamePrefixes: actionOptions.binaryNamePrefixes ?? [
1480
- "nix",
1481
- "determinate-nixd",
1482
- actionOptions.name
1483
- ],
1484
- binaryNamesDenyList: actionOptions.binaryNamesDenyList ?? PROGRAM_NAME_CRASH_DENY_LIST
2102
+ requireNix: actionOptions.requireNix
1485
2103
  };
1486
2104
  actionsCore.debug("idslib options:");
1487
2105
  actionsCore.debug(JSON.stringify(finalOpts, void 0, 2));
1488
2106
  return finalOpts;
1489
2107
  }
1490
2108
  //#endregion
1491
- export { DetSysAction, IdsHost, inputs_exports as inputs, platform_exports as platform, stringifyError };
2109
+ export { DetSysAction, IdsHost, SCOPE_NAME, contextFromTraceparent, getLogger, getTracer, inputs_exports as inputs, log_exports as log, platform_exports as platform, recordSpanError, stringifyError, traceContextHeaders, traceparentOf, withSpan };
1492
2110
 
1493
2111
  //# sourceMappingURL=index.mjs.map