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
24 changed files with 4901 additions and 12755 deletions
+7 -58
View File
@@ -3,25 +3,10 @@ name: CI
on:
pull_request:
push:
branches: [main]
branches: [main, curl-data]
workflow_dispatch:
jobs:
tests:
runs-on: ubuntu-22.04
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-22.04
@@ -51,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
@@ -74,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
@@ -143,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-22.04
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-22.04
steps:
- uses: actions/checkout@v4
- 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.
+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:
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
BIN
View File
Binary file not shown.
Generated Vendored
+3645 -10276
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.
+11 -13
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",
@@ -28,25 +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.4"
"got": "^14.4.6",
"string-argv": "^0.3.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^20.19.4",
"@types/node": "^20.17.28",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@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.1",
"prettier": "^3.6.2",
"tsup": "^8.5.0",
"typescript": "^5.8.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"
}
}
+562 -1144
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);
}
+596 -277
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"]
}