Compare commits

..

15 Commits

Author SHA1 Message Date
gustavderdrache 8d7ad706d3 Don't leak file handles
CI / Check the dist/ folder is up to date (push) Failing after 52s
CI / Test: ${{ matrix.runner }}${{ matrix.determinate && ' with determinate' || '' }} (true, ubuntu-latest) (push) Failing after 42s
2025-03-28 19:55:52 -04:00
gustavderdrache 871bc2c1eb Update dist 2025-03-28 19:43:29 -04:00
gustavderdrache 03441dfa7a Update messaging 2025-03-28 19:42:55 -04:00
gustavderdrache d58e92bfa1 Downgrade warning to info 2025-03-28 19:42:10 -04:00
gustavderdrache 1eafba6ccb Use NUL-terminated ls-files output 2025-03-28 19:41:14 -04:00
gustavderdrache 583b0fbb40 Cleanup child processes 2025-03-28 19:38:57 -04:00
gustavderdrache b433f89383 Escape all the metacharacters 2025-03-28 19:35:42 -04:00
gustavderdrache b09ec83579 Render hash mismatches as feedback 2025-03-28 18:37:29 -04:00
gustavderdrache 0e85837c7a prevent breakpoint activation 2025-03-28 18:37:15 -04:00
Graham Christensen 92da2ded77 ? 2025-03-28 16:47:54 -04:00
Graham Christensen 651153b0f5 ? 2025-03-28 16:42:33 -04:00
Graham Christensen f632d22519 bep 2025-03-28 16:33:50 -04:00
Graham Christensen 37394bd1c7 ? 2025-03-28 16:30:15 -04:00
Graham Christensen d9d0dababa ? 2025-03-28 16:29:37 -04:00
Graham Christensen e528e29ddf beep boop 2025-03-28 16:29:16 -04:00
28 changed files with 75430 additions and 94044 deletions
+2 -1
View File
@@ -7,8 +7,9 @@ EXPECTED_VERSION="${1}"
INSTALLED_NIX_VERSION_OUTPUT=$(nix --version)
INSTALLED_NIX_VERSION=$(echo "${INSTALLED_NIX_VERSION_OUTPUT}" | awk '{print $NF}')
EXPECTED_OUTPUT="nix (Nix) ${EXPECTED_VERSION}"
if [ "${INSTALLED_NIX_VERSION}" != "${EXPECTED_VERSION}" ]; then
if [ "${INSTALLED_NIX_VERSION_OUTPUT}" != "${EXPECTED_OUTPUT}" ]; then
echo "Nix version ${INSTALLED_NIX_VERSION} didn't match expected version ${EXPECTED_VERSION}"
exit 1
else
+11 -55
View File
@@ -3,30 +3,15 @@ name: CI
on:
pull_request:
push:
branches: [main]
branches: [main, curl-data]
workflow_dispatch:
jobs:
tests:
runs-on: ubuntu-latest
needs:
- check-dist-up-to-date
- install-nix
- install-with-non-default-source-inputs
- install-no-id-token
# NOTE(cole-h): GitHub treats "skipped" as "OK" for the purposes of required checks on branch
# protection, so we take advantage of this fact and fail if any of the dependent actions failed,
# or "skip" (which is a success for GHA's purposes) if none of them did.
if: failure()
steps:
- name: Dependent checks failed
run: exit 1
check-dist-up-to-date:
name: Check the dist/ folder is up to date
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/flakehub-cache-action@main
- name: pnpm install
@@ -51,18 +36,14 @@ jobs:
matrix:
runner:
- ubuntu-latest
- nscloud-ubuntu-22.04-amd64-4x16
- namespace-profile-default-arm64
- macos-latest # arm64
determinate:
- true
- false
runs-on: ${{ matrix.runner }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Install Nix
uses: ./
with:
@@ -71,11 +52,17 @@ jobs:
backtrace: full
_internal-strict-mode: true
determinate: ${{ matrix.determinate }}
# - name: Breakpoint if tests failed
# uses: namespacelabs/breakpoint-action@v0
# with:
# duration: 30m
# authorized-users: grahamc
- name: echo $PATH
run: echo $PATH
- name: Render the devshell
if: (success() || failure())
if: success() || failure()
run: |
nix develop --command date
@@ -140,34 +127,3 @@ jobs:
cat -n /etc/nix/nix.conf
nix config show | grep -E "^trusted-users = .*$USER"
nix config show | grep -E "^use-sqlite-wal = true"
install-with-non-default-source-inputs:
name: Install Nix using non-default source-${{ matrix.inputs.key }}
runs-on: ubuntu-latest
strategy:
matrix:
inputs:
# https://github.com/DeterminateSystems/nix-installer/blob/v3.11.3
- key: url
value: https://github.com/DeterminateSystems/nix-installer/releases/download/v3.11.3/nix-installer-x86_64-linux
nix-version: "2.31.2" # 3.11.3 is based on 2.31.2
steps:
- uses: actions/checkout@v6
- name: Install with alternative source-${{ matrix.inputs.key }}
uses: ./
with:
source-${{ matrix.inputs.key }}: ${{ matrix.inputs.value }}
_internal-strict-mode: true
- name: Ensure that the expected Nix version ${{ matrix.inputs.nix-version }} is installed via alternative source-${{ matrix.inputs.key }}
run: .github/verify-version.sh ${{ matrix.inputs.nix-version }}
install-no-id-token:
name: Install Nix without an ID token
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./
with:
_internal-strict-mode: true
determinate: true
-2
View File
@@ -1,5 +1,3 @@
# Submitting Pull Requests
Run `pnpm install` to install necessary JS tools.
This action is based off https://github.com/actions/javascript-action. As part of your contributing flow you **must** run `npm run all` before we can merge.
+15 -49
View File
@@ -1,14 +1,14 @@
# The Determinate Nix Installer Action
The fast, friendly, and reliable GitHub Action to install [Determinate Nix][det-nix] with [flakes].
The Determinate Nix Installer Action is based on [Determinate Nix Installer][installer], which is responsible for tens of thousands of installs daily.
Based on the [Determinate Nix Installer](https://github.com/DeterminateSystems/nix-installer), responsible for over tens of thousands of Nix installs daily.
The fast, friendly, and reliable GitHub Action to install Nix with Flakes.
## Supports
-**Accelerated KVM** on open source projects and larger runners. See [GitHub's announcement](https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/) for more info.
- ✅ Linux (x86_64 and aarch64)
- ✅ macOS (aarch64)
- ✅ Windows Subsystem for Linux (WSL) (x86_64 and aarch64)
- ✅ Linux, x86_64, aarch64, and i686
- ✅ macOS, x86_64 and aarch64
- ✅ WSL2, x86_64 and aarch64
- ✅ Containers
- ✅ Valve's SteamOS
- ✅ GitHub Enterprise Server
@@ -27,18 +27,14 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- run: nix build .
```
> [!NOTE]
> This Action installs [Determinate Nix][det-nix] by default.
> You can, however, use it to install [upstream Nix](#installing-upstream-nix) until **January 1, 2026**.
### With FlakeHub
To fetch private flakes from FlakeHub and Nix builds from FlakeHub Cache, update the `permissions` block and use [`determinate-nix-action`][determinate-nix-action] instead of this Action:
To fetch private flakes from FlakeHub and Nix builds from FlakeHub Cache, update the `permissions` block and pass `determinate: true`:
```yaml
on:
@@ -54,26 +50,15 @@ jobs:
id-token: "write"
contents: "read"
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: true
- run: nix build .
```
See [`.github/workflows/ci.yml`](.github/workflows/ci.yml) for a full example.
### Pinning the version
This GitHub Action uses the most recent version of Determinate Nix Installer, even when the Action itself is pinned.
If you wish to pin your CI workflows to a specific Determinate Nix version, use the [`determinate-nix-action`][determinate-nix-action].
That Action is updated and tagged for every Determinate release.
The `DeterminateSystems/determinate-nix-action@v3.5.2` reference, for example, always installs Determinate Nix v3.5.2.
Additionally, an extra tag on the major version is kept up to date with the current release.
The `DeterminateSystems/determinate-nix-action@v3` reference, for example, installs the most recent release in the `v3.x.y` series.
If you do tag to a specific version, please [use Dependabot to update your actions][dependabot-actions].
### Advanced Usage
- If KVM is available, the installer sets up KVM so that Nix can use it ,and exports the `DETERMINATE_NIX_KVM` environment variable set to 1.
@@ -88,8 +73,8 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
- the `nix-command` and `flakes` features are enabled
- `bash-prompt-prefix` is set
- `auto-optimise-store` is set to `true` (On Linux only)
- `extra-nix-path` is set to `nixpkgs=flake:nixpkgs`
- `max-jobs` is set to `auto`
* `extra-nix-path` is set to `nixpkgs=flake:nixpkgs`
* `max-jobs` is set to `auto`
- KVM is enabled by default.
- an installation receipt (for uninstalling) is stored at `/nix/receipt.json` as well as a copy of the install binary at `/nix/nix-installer`
- `nix-channel --update` is not run, `~/.nix-channels` is not provisioned
@@ -100,11 +85,11 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
| Parameter | Description | Type | Default |
| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------- |
| `backtrace` | The setting for [`RUST_BACKTRACE`][backtrace] | string | |
| `determinate` | Whether to install [Determinate Nix](https://determinate.systems/enterprise) and log in to FlakeHub for private Flakes and binary caches. | Boolean | `true` |
| `determinate` | Whether to install [Determinate Nix](https://determinate.systems/enterprise) and log in to FlakeHub for private Flakes and binary caches. | Boolean | `false` |
| `extra-args` | Extra arguments to pass to the planner (prefer using structured `with:` arguments unless using a custom [planner]!) | string | |
| `extra-conf` | Extra configuration lines for `/etc/nix/nix.conf` (includes `access-tokens` with `secrets.GITHUB_TOKEN` automatically if `github-token` is set) | string | |
| `flakehub` | Deprecated. Implies `determinate`. | Boolean | `false` |
| `force-no-systemd` | Force using other methods than systemd to launch the daemon. This setting is automatically enabled when necessary. | Boolean | `false` |
| `force-docker-shim` | Force the use of Docker as a process supervisor. This setting is automatically enabled when necessary. | Boolean | `false` |
| `github-token` | A [GitHub token] for making authenticated requests (which have a higher rate-limit quota than unauthenticated requests) | string | `${{ github.token }}` |
| `github-server-url` | The URL for the GitHub server, to use with the `github-token` token. Defaults to the current GitHub server, supporting GitHub Enterprise Server automatically. Only change this value if the provided `github-token` is for a different GitHub server than the current server. | string | `${{ github.server }}` |
| `init` | The init system to configure (requires `planner: linux-multi`) | enum (`none` or `systemd`) | |
@@ -132,32 +117,13 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
| `reinstall` | Force a reinstall if an existing installation is detected (consider backing up `/nix/store`) | Boolean | `false` |
| `start-daemon` | If the daemon should be started, requires `planner: linux-multi` | Boolean | `false` |
| `trust-runner-user` | Whether to make the runner user trusted by the Nix daemon | Boolean | `true` |
| `summarize` | Whether to add a build summary and timeline chart to the GitHub job summary | Boolean | `true` |
| `diagnostic-endpoint` | Diagnostic endpoint url where the installer sends install [diagnostic reports](https://github.com/DeterminateSystems/nix-installer#diagnostics) to, to disable set this to an empty string | string | `https://install.determinate.systems/nix-installer/diagnostic` |
| `proxy` | The proxy to use (if any), valid proxy bases are `https://$URL`, `http://$URL` and `socks5://$URL` | string | |
| `ssl-cert-file` | An SSL cert to use (if any), used for fetching Nix and sets `NIX_SSL_CERT_FILE` for Nix | string | |
## Installing upstream Nix
Although Determinate Nix is the default, you can also use this Action to install [upstream Nix][upstream].
Make sure to set `determinate: false` in the Action's configuration:
```yaml
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
```
This option will be available until **January 1, 2026**, at which point installing upstream Nix using this Action will no longer be possible.
[apfs]: https://en.wikipedia.org/wiki/Apple_File_System
[backtrace]: https://doc.rust-lang.org/std/backtrace/index.html#environment-variables
[dependabot-actions]: https://github.com/DeterminateSystems/determinate-nix-action?tab=readme-ov-file#-automate-updates-with-dependabot
[det-nix]: https://docs.determinate.systems/determinate-nix
[determinate-nix-action]: https://github.com/DeterminateSystems/determinate-nix-action
[github token]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication
[installer]: https://github.com/DeterminateSystems/nix-installer
[planner]: https://github.com/determinateSystems/nix-installer#usage
[profile]: https://nixos.org/manual/nix/stable/package-management/profiles
[tracing directives]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
[upstream]: https://github.com/NixOS/nix
+4 -7
View File
@@ -10,7 +10,7 @@ inputs:
determinate:
description: |
Whether to install [Determinate Nix](https://determinate.systems/enterprise) and log in to FlakeHub for private Flakes and binary caches.
default: true
default: false
extra-args:
description: Extra args to pass to the planner (prefer using structured `with:` arguments unless using a custom planner!)
required: false
@@ -21,8 +21,8 @@ inputs:
description: Deprecated. Implies `determinate`.
required: false
default: false
force-no-systemd:
description: Force using other methods than systemd to launch the daemon. This setting is automatically enabled when necessary.
force-docker-shim:
description: Force the use of Docker as a process supervisor. This setting is automatically enabled when necessary.
required: false
default: false
github-token:
@@ -126,9 +126,6 @@ inputs:
trust-runner-user:
description: Whether to make the runner user trusted by the Nix daemon
default: true
summarize:
description: Whether to add a build summary and timeline chart to the GitHub job summary
default: true
nix-installer-branch:
description: (deprecated) The branch of `nix-installer` to use (conflicts with `nix-installer-tag`, `nix-installer-revision`, `nix-installer-pr`)
required: false
@@ -150,6 +147,6 @@ inputs:
default: false
runs:
using: "node24"
using: "node20"
main: "dist/index.js"
post: "dist/index.js"
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
+70980 -88066
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
# Determinate Nix Installer: Docker Shim
#
# This empty image exists to lean on Docker as a process supervisor when
# systemd isn't available. Specifically intended for self-hosted GitHub
# Actions runners using Docker-in-Docker.
#
# See: https://github.com/DeterminateSystems/nix-installer-action
FROM scratch
ENTRYPOINT [ "/nix/var/nix/profiles/default/bin/nix-daemon"]
CMD []
HEALTHCHECK \
--interval=5m \
--timeout=3s \
CMD ["/nix/var/nix/profiles/default/bin/nix", "store", "ping", "--store", "daemon"]
COPY ./Dockerfile /README.md
+52
View File
@@ -0,0 +1,52 @@
# Determinate Nix Installer Action: Docker Shim
The image in this repository is a product of the contained Dockerfile.
It is an otherwise empty image with a configuration layer.
This image is to be used in GitHub Actions runners which don't have systemd available, like self-hosted ARC runners.
The image would have no layers / content at all, however Docker has a bug and refuses to export those images.
This isn't a technical limitation preventing us from creating and distributing that image, but an ease-of-use limitation.
Since some of Docker's inspection tools break on an empty image, the image contains a single layer containing a README.
To build:
```shell
docker build . --tag determinate-nix-shim:latest
docker image save determinate-nix-shim:latest | gzip --best > amd64.tar
```
Then, extract the tarball:
```
mkdir extract
cd extract
tar -xf ../amd64.tar
```
It'll look like this, though the hashes will be different.
```
.
├── 771204abb853cdde06bbbc680001a02642050a1db1a7b0a48cf5f20efa8bdc5d.json
├── c4088111818e553e834adfc81bda8fe6da281afa9a40012eaa82796fb5476e98
│   ├── VERSION
│   ├── json
│   └── layer.tar
├── manifest.json
└── repositories
```
Ignore `manifest.json`, and edit the other two JSON documents to replace `amd64` with `arm64`, both in a key named "architecture:
```
"architecture":"amd64"
```
Then re-create the tar, from within the `extract` directory:
```
tar --options gzip:compression-level=9 -zcf ../arm64.tar.gz .
```
Then `git add` the two .tar.gz's and you're done.
Binary file not shown.
Binary file not shown.
Generated
+10 -10
View File
@@ -2,12 +2,12 @@
"nodes": {
"flake-schemas": {
"locked": {
"lastModified": 1773853635,
"narHash": "sha256-4VHWij4EWNqy1iZr6OxNB6cuv5tDjMwAL/J1SBLFU2Q=",
"rev": "19686d6f3da8d4db959523f32eb1b50de9f18301",
"revCount": 130,
"lastModified": 1693491534,
"narHash": "sha256-ifw8Td8kD08J8DxFbYjeIx5naHcDLz7s2IFP3X42I/U=",
"rev": "c702cbb663d6d70bbb716584a2ee3aeb35017279",
"revCount": 21,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.4.1/019d01eb-47d3-710f-aade-0515bfcdaa1f/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.1/018a4c59-80e1-708a-bb4d-854930c20f72/source.tar.gz"
},
"original": {
"type": "tarball",
@@ -16,12 +16,12 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1774106199,
"narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
"rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
"revCount": 967235,
"lastModified": 1696879762,
"narHash": "sha256-Ud6bH4DMcYHUDKavNMxAhcIpDGgHMyL/yaDEAVSImQY=",
"rev": "f99e5f03cc0aa231ab5950a15ed02afec45ed51a",
"revCount": 534224,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.967235%2Brev-6c9a78c09ff4d6c21d0319114873508a6ec01655/019d198c-70dc-7753-b1d1-721451f578ae/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.534224%2Brev-f99e5f03cc0aa231ab5950a15ed02afec45ed51a/018b1d3c-12f0-76a5-b796-7668d7633f08/source.tar.gz"
},
"original": {
"type": "tarball",
+1 -1
View File
@@ -9,7 +9,7 @@
outputs = { self, flake-schemas, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-darwin" "aarch64-linux" ];
supportedSystems = [ "x86_64-linux" "aarch64-darwin" "aarch64-linux" "x86_64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
+14 -22
View File
@@ -11,8 +11,7 @@
"check-fmt": "prettier --check .",
"lint": "eslint src/**/*.ts",
"package": "ncc build",
"test": "vitest --watch false",
"all": "pnpm run test && pnpm run format && pnpm run lint && pnpm run build && pnpm run package"
"all": "pnpm run format && pnpm run lint && pnpm run build && pnpm run package"
},
"repository": {
"type": "git",
@@ -26,33 +25,26 @@
},
"homepage": "https://github.com/DeterminateSystems/nix-installer-action#readme",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"detsys-ts": "github:DeterminateSystems/detsys-ts",
"got": "^14.6.6",
"string-argv": "^0.3.2",
"vitest": "^3.2.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"unrs-resolver"
]
"got": "^14.4.6",
"string-argv": "^0.3.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^20.19.37",
"@types/node": "^20.17.28",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@vercel/ncc": "^0.38.4",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.10.1",
"eslint-import-resolver-typescript": "^3.10.0",
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5",
"prettier": "^3.8.1",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"tsup": "^8.4.0",
"typescript": "^5.8.2"
}
}
+3720 -4504
View File
File diff suppressed because it is too large Load Diff
-66
View File
@@ -1,66 +0,0 @@
import * as core from "@actions/core";
import type { Fix, FixHashesOutputV1, Mismatch } from "./fixHashes.js";
function prettyDerivation(derivation: string): string {
return derivation.replace(/\/nix\/store\/\w+-/, "").replace(/.drv$/, "");
}
function annotateSingle(
file: string,
line: number,
{ derivation, replacement }: Mismatch,
): void {
const pretty = prettyDerivation(derivation);
core.error(`To correct the hash mismatch for ${pretty}, use ${replacement}`, {
file,
startLine: line,
});
}
function annotateMultiple(
file: string,
{ line, found, mismatches }: Fix,
): void {
const matches = mismatches
.map(({ derivation, replacement }) => {
const pretty = prettyDerivation(derivation);
return `* For the derivation ${pretty}, use ${replacement}`;
})
.join("\n");
core.error(
`There are multiple replacements for the expression ${found}:\n${matches}`,
{
file,
startLine: line,
},
);
}
function annotate(file: string, fix: Fix): void {
if (fix.mismatches.length === 1) {
annotateSingle(file, fix.line, fix.mismatches[0]);
} else {
annotateMultiple(file, fix);
}
}
/**
* Annotates fixed-output derivation hash mismatches using GitHub Actions'
*
* @param output The output of `determinate-nixd fix hashes --json`
* @returns The number of annotations reported to the user
*/
export function annotateMismatches(output: FixHashesOutputV1): number {
let count = 0;
for (const { file, fixes } of output.files) {
for (const fix of fixes) {
annotate(file, fix);
count++;
}
}
return count;
}
-65
View File
@@ -1,65 +0,0 @@
import { parseEvents, getRecentEvents } from "./events.js";
import { expect, test } from "vitest";
// Handy test for locally making sure you can fetch recent events:
// eslint-disable-next-line no-constant-condition
if (false) {
test("Parsing existing events", async () => {
expect(await getRecentEvents(new Date(Date.now() - 1000000))).toStrictEqual(
[{}],
);
});
}
test("Parsing existing events", () => {
const { events } = parseEvents([
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/m96zgji4fhi70s2zs6pq5pric6ch7p4h-stdenv-darwin.drv",
outputs: ["/nix/store/dalhfz3l75w4b4q06sxzqgb2wfydvkbv-stdenv-darwin"],
timing: null,
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv",
outputs: ["/nix/store/qwlgz5da3pfb53gqpgdmazaj9jczrnly-dep-1"],
timing: {
startTime: "2025-04-11T14:38:02Z",
stopTime: "2025-04-11T14:38:05Z",
durationSeconds: 3,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: "/nix/store/ykvbksjqrza2zpj6nkbycrdfwgfdpr8g-hash-mismatch-md5-base16.drv",
timing: {
startTime: "2025-04-11T14:36:44Z",
stopTime: "2025-04-11T14:36:44Z",
durationSeconds: 0,
},
},
]);
expect(events).toStrictEqual([
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv",
timing: {
durationSeconds: 3,
startTime: new Date("2025-04-11T14:38:02Z"),
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: "/nix/store/ykvbksjqrza2zpj6nkbycrdfwgfdpr8g-hash-mismatch-md5-base16.drv",
timing: {
durationSeconds: 0,
startTime: new Date("2025-04-11T14:36:44Z"),
},
},
]);
});
-89
View File
@@ -1,89 +0,0 @@
import got from "got";
export interface DEvent {
v: string;
c: string;
drv: string;
timing: {
startTime: Date;
durationSeconds: number;
};
}
export interface ParsedEventsResult {
readonly events: DEvent[];
readonly hasMismatches: boolean;
}
export function parseEvents(data: unknown): ParsedEventsResult {
let hasMismatches = false;
if (!Array.isArray(data)) {
return { events: [], hasMismatches };
}
const events = data.flatMap((event) => {
// If this was a hash mismatch event, note it and move on
if (event.v === "1" && event.c === "HashMismatchResponseEventV1") {
hasMismatches = true;
return [];
}
// Otherwise, determine if it's an event we're interested in
if (
event.v === "1" &&
(event.c === "BuildFailureResponseEventV1" ||
event.c === "BuiltPathResponseEventV1") &&
Object.hasOwn(event, "drv") &&
typeof event.drv === "string" &&
Object.hasOwn(event, "timing") &&
typeof event.timing === "object" &&
event.timing !== null
) {
const timing = event.timing as { [key: string]: unknown };
if (
Object.hasOwn(timing, "startTime") &&
typeof timing.startTime === "string" &&
Object.hasOwn(timing, "durationSeconds") &&
typeof timing.durationSeconds === "number"
) {
const date = Date.parse(timing.startTime);
if (!Number.isNaN(date)) {
return [
{
v: event.v,
c: event.c,
drv: event.drv,
timing: {
startTime: new Date(date),
durationSeconds: timing.durationSeconds,
},
},
];
}
}
}
return [];
});
return { events, hasMismatches };
}
export async function getRecentEvents(
since: Date,
): Promise<ParsedEventsResult> {
const queryParam = encodeURIComponent(since.toISOString());
const resp = await got
.get(
`http://unix:/nix/var/determinate/determinate-nixd.socket:/events/recent?since=${queryParam}`,
{
enableUnixSockets: true,
},
)
.json();
return parseEvents(resp);
}
-275
View File
@@ -1,275 +0,0 @@
import { expect, test } from "vitest";
import {
FailureSummary,
getBuildFailures,
summarizeFailures,
} from "./failuresummary.js";
/* eslint-disable @typescript-eslint/no-non-null-assertion */
test("Select for failure events", () => {
const events = [
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv`,
timing: {
startTime: new Date(1 * 1000),
durationSeconds: 1,
},
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-2.drv`,
timing: {
startTime: new Date(2 * 1000),
durationSeconds: 2,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv`,
timing: {
startTime: new Date(3 * 1000),
durationSeconds: 3,
},
},
];
expect(getBuildFailures(events)).toStrictEqual([
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv`,
timing: {
startTime: new Date(1 * 1000),
durationSeconds: 1,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv`,
timing: {
startTime: new Date(3 * 1000),
durationSeconds: 3,
},
},
]);
});
test("Summarize Failures", async () => {
const events = [
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv`,
timing: {
startTime: new Date(1 * 1000),
durationSeconds: 1,
},
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-2.drv`,
timing: {
startTime: new Date(2 * 1000),
durationSeconds: 2,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv`,
timing: {
startTime: new Date(3 * 1000),
durationSeconds: 3,
},
},
];
const logMaker = async (drv: string): Promise<string | undefined> => {
if (drv.includes("dep-1")) {
return `${drv}\n`.repeat(9).trimEnd();
} else {
return `${drv}\n`.repeat(25).trimEnd();
}
};
const summary: FailureSummary = (await summarizeFailures(events, logMaker))!;
expect(summary.markdownLines.join("\n"))
.toStrictEqual(`### Build error review :boom:
> [!NOTE]
> 2 builds failed
<details><summary>Failure log: <code>/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-<strong>dep-1</strong>.drv</code></summary>
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
</details>
<details><summary>Failure log: <code>/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-<strong>dep-3</strong>.drv</code></summary>
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
</details>
`);
expect(summary.logLines.join("\n"))
.toStrictEqual(`\u001b[38;2;255;0;0mBuild logs from 2 failures
The following build logs are also available in the Markdown summary:
::group::Failed build: /nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
::endgroup::
::group::Failed build: /nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
::endgroup::`);
});
test("Omit some logs if there are too many", async () => {
const events = [
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv`,
timing: {
startTime: new Date(1 * 1000),
durationSeconds: 1,
},
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-2.drv`,
timing: {
startTime: new Date(2 * 1000),
durationSeconds: 2,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv`,
timing: {
startTime: new Date(3 * 1000),
durationSeconds: 3,
},
},
];
const logMaker = async (drv: string): Promise<string | undefined> => {
return `${drv}\n`.repeat(5).trimEnd();
};
const summary: FailureSummary = (await summarizeFailures(
events,
logMaker,
500,
))!;
expect(summary.markdownLines.join("\n"))
.toStrictEqual(`### Build error review :boom:
> [!NOTE]
> 2 builds failed
<details><summary>Failure log: <code>/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-<strong>dep-1</strong>.drv</code></summary>
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
</details>
> [!NOTE]
> The following failure has been omitted due to GitHub Actions' summary length limitations.
> The full logs are available in the post-run phase of the Nix Installer Action.
> * \`/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv\``);
expect(summary.logLines.join("\n"))
.toStrictEqual(`\u001b[38;2;255;0;0mBuild logs from 2 failures
The following build logs are also available in the Markdown summary:
::group::Failed build: /nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv
::endgroup::
The following build logs are NOT available in the Markdown summary:
::group::Failed build: /nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-3.drv
::endgroup::`);
});
-124
View File
@@ -1,124 +0,0 @@
import { getExecOutput } from "@actions/exec";
import { DEvent } from "./events.js";
import { stripVTControlCharacters } from "node:util";
// CI summaries have a max length of "1024k" which I assume to be 1048576 bytes.
// Generously, the mermaid doc is about 50,000 bytes.
// Rounding it all down a bit further for wiggle room, that leaves lots of log space.
const defaultMaxSummaryLength = 995_000;
export function getBuildFailures(events: DEvent[]): DEvent[] {
return events.filter((event: DEvent): Boolean => {
return event.c === "BuildFailureResponseEventV1";
});
}
export interface FailureSummary {
logLines: string[];
markdownLines: string[];
}
export async function summarizeFailures(
events: DEvent[],
getLog: (drv: string) => Promise<string | undefined> = getLogFromNix,
maxLength: number = defaultMaxSummaryLength,
): Promise<FailureSummary | undefined> {
const failures = getBuildFailures(events);
if (failures.length === 0) {
return undefined;
}
const logLines = [];
const markdownLines = [];
logLines.push(
`\u001b[38;2;255;0;0mBuild logs from ${failures.length} failure${failures.length === 1 ? "" : "s"}`,
);
logLines.push(
`The following build logs are also available in the Markdown summary:`,
);
markdownLines.push(`### Build error review :boom:`);
markdownLines.push("> [!NOTE]");
markdownLines.push(
`> ${failures.length} build${failures.length === 1 ? "" : "s"} failed`,
);
const markdownLogChunks: {
drv: string;
txtLines: string[];
mdLines: string[];
}[] = [];
for (const event of failures) {
const markdownLogChunk = [];
const txtLogChunk = [];
txtLogChunk.push(`::group::Failed build: ${event.drv}`);
const log =
(await getLog(event.drv)) ??
"(failure reading the log for this derivation.)";
const indented = log.split("\n").map((line) => ` ${line}`);
markdownLogChunk.push(
`<details><summary>Failure log: <code>${event.drv.replace(/^(\/nix[^-]*-)(.*)(\.drv)$/, "$1<strong>$2</strong>$3")}</code></summary>`,
);
markdownLogChunk.push("");
for (const line of indented) {
txtLogChunk.push(line);
markdownLogChunk.push(stripVTControlCharacters(line));
}
markdownLogChunk.push("");
markdownLogChunk.push("</details>");
markdownLogChunk.push("");
markdownLogChunks.push({
drv: event.drv,
mdLines: markdownLogChunk,
txtLines: txtLogChunk,
});
txtLogChunk.push(`::endgroup::`);
}
const skippedChunks = [];
// Add markdown log chunks until we exceed the max length
let markdownLength = markdownLines.join("\n").length;
for (const chunk of markdownLogChunks) {
const chunkLength = chunk.mdLines.join("\n").length;
if (markdownLength + chunkLength > maxLength) {
skippedChunks.push(chunk);
} else {
logLines.push(...chunk.txtLines);
markdownLines.push(...chunk.mdLines);
markdownLength += chunkLength;
}
}
if (skippedChunks.length > 0) {
markdownLines.push(
"",
"> [!NOTE]",
`> The following ${skippedChunks.length === 1 ? "failure has" : "failures have"} been omitted due to GitHub Actions' summary length limitations.`,
"> The full logs are available in the post-run phase of the Nix Installer Action.",
);
logLines.push(
"The following build logs are NOT available in the Markdown summary:",
);
for (const chunk of skippedChunks) {
markdownLines.push(`> * \`${chunk.drv}\``);
logLines.push(...chunk.txtLines);
}
}
return { logLines, markdownLines };
}
async function getLogFromNix(drv: string): Promise<string | undefined> {
const output = await getExecOutput("nix", ["log", drv], {
silent: true,
});
return output.stdout;
}
-38
View File
@@ -1,38 +0,0 @@
import { getExecOutput } from "@actions/exec";
export interface Mismatch {
readonly derivation: string;
readonly replacement: string;
}
export interface Fix {
readonly line: number;
readonly found: string;
readonly mismatches: readonly Mismatch[];
}
export interface FileFix {
readonly file: string;
readonly fixes: readonly Fix[];
}
export interface FixHashesOutputV1 {
readonly version: "v1";
readonly files: readonly FileFix[];
}
export async function getFixHashes(since: string): Promise<FixHashesOutputV1> {
const output = await getExecOutput(
"determinate-nixd",
["fix", "hashes", "--json", "--since", since],
{ silent: true },
);
if (output.exitCode !== 0) {
throw new Error(
`determinate-nixd fix hashes returned non-zero exit code ${output.exitCode} with the following error output:\n${output.stderr}`,
);
}
return JSON.parse(output.stdout);
}
+599 -361
View File
File diff suppressed because it is too large Load Diff
-207
View File
@@ -1,207 +0,0 @@
import { mermaidify, makeMermaidReport } from "./mermaid.js";
import { DEvent, parseEvents } from "./events.js";
import { expect, test } from "vitest";
/* eslint-disable @typescript-eslint/no-non-null-assertion */
function generateEvents(count: number): DEvent[] {
const events: DEvent[] = [];
for (let i = 0; i < count; i++) {
events.push({
v: "1",
c: "BuiltPathResponseEventV1",
drv: `/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-${i}.drv`,
timing: {
startTime: new Date(i * 1000),
durationSeconds: i,
},
});
}
return events;
}
test("Empty event list returns no report", () => {
const report = makeMermaidReport([]);
expect(report).toBeUndefined();
});
test("Create a very large report doc and make sure it is small enough", () => {
const report = makeMermaidReport(generateEvents(2500))!;
// Assert the `.drv` suffix was pruned (1 reference = the NOTE at the end)
expect(report.match(/\.drv/g)!.length).equals(1);
// Assert the `/nix/store` prefix was pruned (1 reference = the NOTE at the end)
expect(report.match(/\/nix\/store\//g)!.length).equals(1);
// Assert that some events were pruned
expect(report.match(/dep-/g)!.length).lessThan(2500);
expect(report.match(/dep-/g)!.length).greaterThan(1500);
expect(report).toContain("suffix, and builds that took less than ");
expect(report.length).lessThan(50200);
expect(report.length).greaterThan(49000);
});
test("Create a medium large report doc and make sure it is small enough", () => {
const eventCount = 675;
const report = makeMermaidReport(generateEvents(eventCount))!;
// Assert the `.drv` suffix was pruned (1 reference = the NOTE at the end)
expect(report.match(/\.drv/g)!.length).equals(1);
// Assert the `/nix/store` prefix was pruned (1 reference = the NOTE at the end)
expect(report.match(/\/nix\/store\//g)!.length).equals(1);
// Assert that no lines were pruned
expect(report.match(/dep-/g)!.length).toStrictEqual(eventCount);
expect(report).toContain(
"suffixes have been removed to make the graph small enough to render",
);
expect(report.length).lessThan(50200);
expect(report.length).greaterThan(18000);
});
test("Create a small report doc and make sure it isn't pruned", () => {
const report = makeMermaidReport(generateEvents(100))!;
// Assert 100 events have the `.drv` suffix, ie: were not pruned
expect(report.match(/\.drv/g)!.length).equals(100);
// Assert 100 events have the `.drv` suffix, ie: were not pruned
expect(report.match(/\/nix\/store\//g)!.length).equals(100);
expect(report.length).lessThan(50000);
});
test("Generate a really big report and shrink it", () => {
const events = generateEvents(1000);
const originalLength = mermaidify(events, -1)!.length;
const limitedLengthZero = mermaidify(events, 0)!.length;
const limitedLengthOne = mermaidify(events, 1)!.length;
const limitedLengthTwo = mermaidify(events, 2)!.length;
expect(originalLength).greaterThan(limitedLengthZero);
expect(limitedLengthZero).greaterThan(limitedLengthOne);
expect(limitedLengthOne).greaterThan(limitedLengthTwo);
});
test("Generate a rough report of various length", () => {
const { events } = parseEvents([
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-0.drv",
outputs: ["/nix/store/qwlgz5da3pfb53gqpgdmazaj9jczrnly-dep-0"],
timing: {
startTime: "2025-04-11T14:38:02Z",
stopTime: "2025-04-11T14:38:05Z",
durationSeconds: 0,
},
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv",
outputs: ["/nix/store/qwlgz5da3pfb53gqpgdmazaj9jczrnly-dep-1"],
timing: {
startTime: "2025-04-11T14:38:02Z",
stopTime: "2025-04-11T14:38:05Z",
durationSeconds: 1,
},
},
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-2.drv",
outputs: ["/nix/store/qwlgz5da3pfb53gqpgdmazaj9jczrnly-dep-2"],
timing: {
startTime: "2025-04-11T14:38:02Z",
stopTime: "2025-04-11T14:38:05Z",
durationSeconds: 2,
},
},
{
v: "1",
c: "BuildFailureResponseEventV1",
drv: "/nix/store/ykvbksjqrza2zpj6nkbycrdfwgfdpr8g-hash-mismatch-md5-base16.drv",
timing: {
startTime: "2025-04-11T14:38:05Z",
stopTime: "2025-04-11T14:38:09Z",
durationSeconds: 4,
},
},
]);
expect(mermaidify(events, -1)).toStrictEqual(`\`\`\`mermaid
gantt
dateFormat X
axisFormat %Mm%Ss
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-0.drv (0s):d, 0, 0s
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-1.drv (1s):d, 0, 1s
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-2.drv (2s):d, 0, 2s
/nix/store/ykvbksjqrza2zpj6nkbycrdfwgfdpr8g-hash-mismatch-md5-base16.drv (4s):crit, 3, 4s
\`\`\``);
expect(mermaidify(events, 0)).toStrictEqual(`\`\`\`mermaid
gantt
dateFormat X
axisFormat %Mm%Ss
dep-0 (0s):d, 0, 0s
dep-1 (1s):d, 0, 1s
dep-2 (2s):d, 0, 2s
hash-mismatch-md5-base16 (4s):crit, 3, 4s
\`\`\``);
expect(mermaidify(events, 1)).toStrictEqual(`\`\`\`mermaid
gantt
dateFormat X
axisFormat %Mm%Ss
dep-1 (1s):d, 0, 1s
dep-2 (2s):d, 0, 2s
hash-mismatch-md5-base16 (4s):crit, 3, 4s
\`\`\``);
});
test("Generate a really big report and shrink it", () => {
const events = generateEvents(1000);
const originalLength = mermaidify(events, -1)!.length;
const limitedLengthZero = mermaidify(events, 0)!.length;
const limitedLengthOne = mermaidify(events, 1)!.length;
const limitedLengthTwo = mermaidify(events, 2)!.length;
expect(originalLength).greaterThan(limitedLengthZero);
expect(limitedLengthZero).greaterThan(limitedLengthOne);
expect(limitedLengthOne).greaterThan(limitedLengthTwo);
});
test("Really long builds get multi-unit timestamps", () => {
const { events } = parseEvents([
{
v: "1",
c: "BuiltPathResponseEventV1",
drv: "/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-0.drv",
outputs: ["/nix/store/qwlgz5da3pfb53gqpgdmazaj9jczrnly-dep-0"],
timing: {
startTime: "2025-04-11T14:38:02Z",
stopTime: "2026-05-14T13:32:01Z",
durationSeconds: 34383239,
},
},
]);
expect(mermaidify(events, -1)).toStrictEqual(`\`\`\`mermaid
gantt
dateFormat X
axisFormat %Mm%Ss
/nix/store/rz9hrpay90sjrid5hx3x8v606ji679xa-dep-0.drv (573053m59s):d, 0, 34383239s
\`\`\``);
});
-96
View File
@@ -1,96 +0,0 @@
import { DEvent } from "./events.js";
import { truncateDerivation } from "./util.js";
export function makeMermaidReport(events: DEvent[]): string | undefined {
// # 50k is the max: https://github.com/mermaid-js/mermaid/blob/c269dc822c528e1afbde34e18a1cad03d972d4fe/src/defaultConfig.js#L55
const maxLength = 49900;
let mermaid = "";
let pruneLevel = -2;
do {
pruneLevel += 1;
mermaid = mermaidify(events, pruneLevel) ?? "";
} while (mermaid.length > maxLength);
if (!mermaid) {
return undefined;
}
const lines = [
"<details open><summary><strong>Build timeline</strong> :hourglass_flowing_sand:</summary>",
"", // load bearing whitespace, deleting it breaks the details expander / markdown
mermaid,
"", // load bearing whitespace, deleting it breaks the details expander / markdown
];
if (pruneLevel === 0) {
lines.push("> [!NOTE]");
lines.push(
"> `/nix/store/[hash]` and the `.drv` suffixes have been removed to make the graph small enough to render.",
);
} else if (pruneLevel > 0) {
lines.push("> [!NOTE]");
lines.push(
`> \`/nix/store/[hash]\`, the \`.drv\` suffix, and builds that took less than ${formatDuration(pruneLevel)} have been removed to make the graph small enough to render.`,
);
}
lines.push(""); // load bearing whitespace, deleting it breaks the details expander / markdown
lines.push("</details>");
return lines.join("\n");
}
export function mermaidify(
allEvents: DEvent[],
pruneLevel: number,
): string | undefined {
const events = allEvents
.filter(
(event) =>
event.c === "BuiltPathResponseEventV1" ||
event.c === "BuildFailureResponseEventV1",
)
.sort(
(a, b) => a.timing.startTime.getTime() - b.timing.startTime.getTime(),
);
const firstEvent = events.at(0);
if (firstEvent === undefined) {
return undefined;
}
const zeroMoment = firstEvent.timing.startTime.getTime();
const lines = [
"```mermaid",
"gantt",
" dateFormat X",
" axisFormat %Mm%Ss",
];
for (const event of events) {
const duration = event.timing.durationSeconds;
if (duration < pruneLevel) {
continue;
}
const label = pruneLevel >= 0 ? truncateDerivation(event.drv) : event.drv;
const tag = event.c === "BuildFailureResponseEventV1" ? "crit" : "d";
const relativeStartTime =
(event.timing.startTime.getTime() - zeroMoment) / 1000;
lines.push(
`${label} (${formatDuration(duration)}):${tag}, ${relativeStartTime}, ${duration}s`,
);
}
lines.push("```");
return lines.join("\n");
}
function formatDuration(duration: number): string {
const durSeconds = duration % 60;
const durMinutes = (duration - durSeconds) / 60;
return `${durMinutes > 0 ? `${durMinutes}m` : ""}${durSeconds}s`;
}
-3
View File
@@ -1,3 +0,0 @@
export function truncateDerivation(drv: string): string {
return drv.replace(/^\/nix\/store\/[a-z0-9]+-/, "").replace(/\.drv$/, "");
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "Node16",
"moduleResolution": "NodeNext",
"outDir": "./dist",
@@ -11,5 +11,5 @@
"resolveJsonModule": true,
"declaration": true
},
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "**/*.test.ts", "dist"]
}
+1 -1
View File
@@ -5,7 +5,7 @@ export default defineConfig({
name,
entry: ["src/index.ts"],
format: ["esm"],
target: "node24",
target: "node20",
bundle: true,
splitting: false,
clean: true,