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
31 changed files with 9726 additions and 13644 deletions
+74
View File
@@ -0,0 +1,74 @@
{
"plugins": ["@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"settings": {
"import/resolver": {
"typescript": {}
}
},
"rules": {
"i18n-text/no-en": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "no-public"
}
],
"@typescript-eslint/no-base-to-string": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true
}
}
+10 -63
View File
@@ -3,40 +3,23 @@ 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@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/flakehub-cache-action@main
- name: pnpm install
run: nix develop --command pnpm install
- name: Check formatting
- name: prettier format
run: nix develop --command pnpm run check-fmt
- name: Link
- name: ESLint
run: nix develop --command pnpm run lint
- name: Test
run: nix develop --command pnpm run test
- name: tsup build
run: nix develop --command pnpm run build
- name: ncc package
@@ -53,15 +36,8 @@ jobs:
matrix:
runner:
- ubuntu-latest
- nscloud-ubuntu-22.04-amd64-4x16
- namespace-profile-default-arm64
# - macos-12-large # determinate-nixd is broken on macos-12
- macos-13-large
- macos-14-large
- macos-14-xlarge # arm64
determinate:
- true
- false
runs-on: ${{ matrix.runner }}
permissions:
contents: read
@@ -76,6 +52,12 @@ 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
@@ -145,38 +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/v0.18.0
- key: url
value: https://github.com/DeterminateSystems/nix-installer/releases/download/v0.18.0/nix-installer-x86_64-linux
nix-version: "2.21.2"
# https://github.com/DeterminateSystems/nix-installer/tree/7011c077ec491da410fbc39f68676b0908b9ce7e
- key: revision
value: 7011c077ec491da410fbc39f68676b0908b9ce7e
nix-version: "2.19.2"
steps:
- uses: actions/checkout@v4
- 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@v4
- uses: ./
with:
_internal-strict-mode: true
determinate: true
+3
View File
@@ -53,6 +53,9 @@ typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
+3
View File
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
+1
View File
@@ -0,0 +1 @@
{}
-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.
+5 -18
View File
@@ -34,7 +34,7 @@ jobs:
### 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]:
To fetch private flakes from FlakeHub and Nix builds from FlakeHub Cache, update the `permissions` block and pass `determinate: true`:
```yaml
on:
@@ -51,25 +51,14 @@ jobs:
contents: "read"
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/determinate-nix-action@v3
- 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 the Determinate Nix Installer, even when the Action itself is pinned.
If you wish to pin your CI workflows to a specific 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.
@@ -100,7 +89,7 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
| `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`) | |
@@ -134,8 +123,6 @@ Differing from the upstream [Nix](https://github.com/NixOS/nix) installer script
[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
[determinate-nix-action]: https://github.com/DeterminateSystems/determinate-nix-action
[github token]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication
[planner]: https://github.com/determinateSystems/nix-installer#usage
[profile]: https://nixos.org/manual/nix/stable/package-management/profiles
+2 -2
View File
@@ -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:
-148
View File
@@ -1,148 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"ignoreUnknown": false,
"ignore": ["dist"]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"complexity": {
"noExtraBooleanCast": "error",
"noMultipleSpacesInRegularExpressionLiterals": "error",
"noStaticOnlyClass": "error",
"noUselessConstructor": "error",
"noUselessStringConcat": "error",
"noUselessTypeConstraint": "error"
},
"correctness": {
"noConstAssign": "error",
"noConstantCondition": "error",
"noEmptyCharacterClassInRegex": "error",
"noEmptyPattern": "error",
"noGlobalObjectCalls": "error",
"noInnerDeclarations": "error",
"noInvalidConstructorSuper": "error",
"noNewSymbol": "error",
"noSelfAssign": "error",
"noSwitchDeclarations": "error",
"noUndeclaredVariables": "error",
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnusedLabels": "error",
"noUnusedVariables": "error",
"useArrayLiterals": "off",
"useIsNan": "error",
"useYield": "error"
},
"style": {
"noArguments": "error",
"noCommaOperator": "error",
"noInferrableTypes": "error",
"noNamespace": "error",
"noNonNullAssertion": "warn",
"noVar": "error",
"useAsConstAssertion": "error",
"useConsistentArrayType": "error",
"useConst": "error",
"useForOf": "warn",
"useShorthandFunctionType": "warn",
"useSingleVarDeclarator": "error",
"useTemplate": "error"
},
"suspicious": {
"noCatchAssign": "error",
"noClassAssign": "error",
"noCompareNegZero": "error",
"noConsole": "error",
"noControlCharactersInRegex": "error",
"noDebugger": "error",
"noDoubleEquals": "error",
"noDuplicateCase": "error",
"noDuplicateClassMembers": "error",
"noDuplicateObjectKeys": "error",
"noDuplicateParameters": "error",
"noEmptyBlockStatements": "error",
"noExplicitAny": "error",
"noExtraNonNullAssertion": "error",
"noFallthroughSwitchClause": "error",
"noFunctionAssign": "error",
"noGlobalAssign": "error",
"noMisleadingInstantiator": "error",
"noRedeclare": "error",
"noSparseArray": "error",
"noUnsafeDeclarationMerging": "error",
"noUnsafeNegation": "error",
"useNamespaceKeyword": "error",
"useValidTypeof": "error"
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "double",
"attributePosition": "auto",
"bracketSpacing": true
},
"globals": ["exports"]
},
"overrides": [
{
"include": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
"linter": {
"rules": {
"complexity": { "noWith": "off" },
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidBuiltinInstantiation": "off",
"noInvalidConstructorSuper": "off",
"noNewSymbol": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": {
"noArguments": "error",
"noVar": "error",
"useConst": "error",
"useNamingConvention": "error"
},
"suspicious": {
"noClassAssign": "off",
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"useGetterReturn": "off"
}
}
}
}
]
}
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
+3663 -10119
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
+12 -12
View File
@@ -2,30 +2,30 @@
"nodes": {
"flake-schemas": {
"locked": {
"lastModified": 1721999734,
"narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=",
"rev": "0a5c42297d870156d9c57d8f99e476b738dcd982",
"revCount": 75,
"lastModified": 1693491534,
"narHash": "sha256-ifw8Td8kD08J8DxFbYjeIx5naHcDLz7s2IFP3X42I/U=",
"rev": "c702cbb663d6d70bbb716584a2ee3aeb35017279",
"revCount": 21,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/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",
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/0.1"
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%2A.tar.gz"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1748460289,
"narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=",
"rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102",
"revCount": 807377,
"lastModified": 1696879762,
"narHash": "sha256-Ud6bH4DMcYHUDKavNMxAhcIpDGgHMyL/yaDEAVSImQY=",
"rev": "f99e5f03cc0aa231ab5950a15ed02afec45ed51a",
"revCount": 534224,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.807377%2Brev-96ec055edbe5ee227f28cdbc3f1ddf1df5965102/01972133-94d2-786e-bfeb-34136c7f0b07/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",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz"
}
},
"root": {
+4 -4
View File
@@ -3,8 +3,8 @@
description = "Development environment for the Nix Installer action for GitHub.";
inputs = {
flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/0.1";
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1";
flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*.tar.gz";
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz";
};
outputs = { self, flake-schemas, nixpkgs }:
@@ -21,9 +21,9 @@
default = pkgs.mkShell {
packages = with pkgs; [
nodejs_latest
nodePackages_latest.pnpm
biome
nixpkgs-fmt
nodePackages_latest.pnpm
nodePackages_latest.typescript-language-server
];
};
});
+18 -12
View File
@@ -7,12 +7,11 @@
"type": "module",
"scripts": {
"build": "tsup",
"format": "biome format --write",
"check-fmt": "biome format",
"lint": "biome lint",
"format": "prettier --write .",
"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 test && pnpm run build && pnpm run package"
"all": "pnpm run format && pnpm run lint && pnpm run build && pnpm run package"
},
"repository": {
"type": "git",
@@ -28,17 +27,24 @@
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.1",
"@actions/github": "^6.0.0",
"detsys-ts": "github:DeterminateSystems/detsys-ts",
"got": "^14.4.7",
"string-argv": "^0.3.2",
"vitest": "^3.2.0"
"got": "^14.4.6",
"string-argv": "^0.3.2"
},
"devDependencies": {
"@types/node": "^20.17.57",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^20.17.28",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@vercel/ncc": "^0.38.3",
"tsup": "^8.5.0",
"typescript": "^5.8.3"
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.10.0",
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"tsup": "^8.4.0",
"typescript": "^5.8.2"
}
}
+5240 -1951
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 { expect, test } from "vitest";
import { getRecentEvents, parseEvents } from "./events.js";
// Handy test for locally making sure you can fetch recent events:
// biome-ignore lint/correctness/noConstantCondition: testing
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 Event {
v: string;
c: string;
drv: string;
timing: {
startTime: Date;
durationSeconds: number;
};
}
export interface ParsedEventsResult {
readonly events: Event[];
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);
}
-279
View File
@@ -1,279 +0,0 @@
import { expect, test } from "vitest";
import {
FailureSummary,
getBuildFailures,
summarizeFailures,
} from "./failuresummary.js";
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();
}
};
// biome-ignore lint/style/noNonNullAssertion: testing
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();
};
// biome-ignore lint/style/noNonNullAssertion: testing
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 { stripVTControlCharacters } from "node:util";
import { getExecOutput } from "@actions/exec";
import { Event } from "./events.js";
// 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: Event[]): Event[] {
return events.filter((event: Event): Boolean => {
return event.c === "BuildFailureResponseEventV1";
});
}
export interface FailureSummary {
logLines: string[];
markdownLines: string[];
}
export async function summarizeFailures(
events: Event[],
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);
}
+618 -326
View File
File diff suppressed because it is too large Load Diff
-225
View File
@@ -1,225 +0,0 @@
import { expect, test } from "vitest";
import { Event, parseEvents } from "./events.js";
import { makeMermaidReport, mermaidify } from "./mermaid.js";
function generateEvents(count: number): Event[] {
const events: Event[] = [];
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", () => {
// biome-ignore lint/style/noNonNullAssertion: testing
const report = makeMermaidReport(generateEvents(2500))!;
// Assert the `.drv` suffix was pruned (1 reference = the NOTE at the end)
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/\.drv/g)!.length).equals(1);
// Assert the `/nix/store` prefix was pruned (1 reference = the NOTE at the end)
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/\/nix\/store\//g)!.length).equals(1);
// Assert that some events were pruned
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/dep-/g)!.length).lessThan(2500);
// biome-ignore lint/style/noNonNullAssertion: testing
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;
// biome-ignore lint/style/noNonNullAssertion: testing
const report = makeMermaidReport(generateEvents(eventCount))!;
// Assert the `.drv` suffix was pruned (1 reference = the NOTE at the end)
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/\.drv/g)!.length).equals(1);
// Assert the `/nix/store` prefix was pruned (1 reference = the NOTE at the end)
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/\/nix\/store\//g)!.length).equals(1);
// Assert that no lines were pruned
// biome-ignore lint/style/noNonNullAssertion: testing
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", () => {
// biome-ignore lint/style/noNonNullAssertion: testing
const report = makeMermaidReport(generateEvents(100))!;
// Assert 100 events have the `.drv` suffix, ie: were not pruned
// biome-ignore lint/style/noNonNullAssertion: testing
expect(report.match(/\.drv/g)!.length).equals(100);
// Assert 100 events have the `.drv` suffix, ie: were not pruned
// biome-ignore lint/style/noNonNullAssertion: testing
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);
// biome-ignore lint/style/noNonNullAssertion: testing
const originalLength = mermaidify(events, -1)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
const limitedLengthZero = mermaidify(events, 0)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
const limitedLengthOne = mermaidify(events, 1)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
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);
// biome-ignore lint/style/noNonNullAssertion: testing
const originalLength = mermaidify(events, -1)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
const limitedLengthZero = mermaidify(events, 0)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
const limitedLengthOne = mermaidify(events, 1)!.length;
// biome-ignore lint/style/noNonNullAssertion: testing
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 { Event } from "./events.js";
import { truncateDerivation } from "./util.js";
export function makeMermaidReport(events: Event[]): 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: Event[],
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"]
}