Skip to content

feat(notion): add markdown block payload conversion #3939

feat(notion): add markdown block payload conversion

feat(notion): add markdown block payload conversion #3939

Workflow file for this run

# Generated file - DO NOT EDIT
# Source: ci.yml.genie.ts
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
measurement_baseline_ref:
description: Optional ref/SHA to checkout before running CI measurement jobs. Used to backfill comparable baseline artifacts.
required: false
default: ''
type: string
measurement_baseline_label:
description: Optional human label for a measurement baseline backfill run, for example PR number.
required: false
default: ''
type: string
run_datasource_sync_demo:
description: Run the credentialed notion-datasource-sync demo showcase in the Notion integration lane. Requires NOTION_DATASOURCE_SYNC_DEMO_PAGE_ID.
required: false
default: false
type: boolean
debug_force_nix_diagnostics_failure:
description: 'Temporary debug switch (#272): force post-validation failure to verify diagnostics artifact + summary'
required: false
default: false
type: boolean
jobs:
default-ref-policy:
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
permissions:
contents: read
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Check first-party default refs
env:
FIRST_PARTY_OWNERS_JSON: '["schickling","overengineeringstudio"]'
DEFAULT_REF: main
DEFAULT_REFS_JSON: '{"livestorejs/livestore":"dev"}'
VERIFY_REACHABLE: '0'
NORMALIZE_GIT_BRANCH_REFS: '0'
run: |
nix shell nixpkgs#nodejs_24 -c node <<'NODE'
const fs = require('node:fs')
const path = require('node:path')
const os = require('node:os')
const cp = require('node:child_process')
const cwd = process.cwd()
const firstPartyOwners = new Set(JSON.parse(process.env.FIRST_PARTY_OWNERS_JSON).map((owner) => owner.toLowerCase()))
const defaultRef = process.env.DEFAULT_REF || 'main'
const defaultRefs = new Map(Object.entries(JSON.parse(process.env.DEFAULT_REFS_JSON || '{}')).map(([repo, ref]) => [repo.toLowerCase(), ref]))
const verifyReachable = process.env.VERIFY_REACHABLE === '1'
const violations = []
const repoKey = (repo) => repo.owner.toLowerCase() + '/' + repo.repo.toLowerCase()
const isFirstParty = (repo) => firstPartyOwners.has(repo.owner.toLowerCase())
const normalizeGitBranchRefs = process.env.NORMALIZE_GIT_BRANCH_REFS === '1'
const normalizeRef = (ref) => normalizeGitBranchRefs && ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
const expectedRefFor = (repo) => normalizeRef(defaultRefs.get(repoKey(repo)) || defaultRef)
const githubRepoFromUrl = (url) => {
const match = url.match(/github\.com[/:]([^/]+)\/([^/#?]+?)(?:\.git)?(?:[#?].*)?$/)
return match ? { owner: match[1], repo: match[2] } : undefined
}
const parseGithubLikeRef = (value) => {
const hashIndex = value.indexOf('#')
const baseWithQuery = hashIndex >= 0 ? value.slice(0, hashIndex) : value
const hashRef = hashIndex >= 0 ? value.slice(hashIndex + 1) : undefined
const queryIndex = baseWithQuery.indexOf('?')
const base = queryIndex >= 0 ? baseWithQuery.slice(0, queryIndex) : baseWithQuery
const params = new URLSearchParams(queryIndex >= 0 ? baseWithQuery.slice(queryIndex + 1) : '')
const queryRef = params.get('ref') || undefined
if (base.startsWith('github:')) {
const parts = base.slice('github:'.length).split('/')
if (parts.length < 2) return undefined
return {
repo: { owner: parts[0], repo: parts[1] },
ref: hashRef || queryRef || (parts.length > 2 ? parts.slice(2).join('/') : undefined),
}
}
if (base.startsWith('git+https://github.com/')) {
const repo = githubRepoFromUrl(base.slice('git+'.length))
return repo ? { repo, ref: hashRef || queryRef } : undefined
}
if (base.startsWith('git+ssh://git@github.com/')) {
const pathParts = base.slice('git+ssh://git@github.com/'.length).replace(/\.git$/, '').split('/')
return pathParts.length >= 2
? { repo: { owner: pathParts[0], repo: pathParts[1] }, ref: hashRef || queryRef }
: undefined
}
const urlRepo = githubRepoFromUrl(base)
if (urlRepo) return { repo: urlRepo, ref: hashRef || queryRef }
if (!value.includes('://') && !value.startsWith('./') && !value.startsWith('../') && !value.startsWith('/')) {
const parts = base.split('/')
if (parts.length === 2 && parts[0] && parts[1]) {
return { repo: { owner: parts[0], repo: parts[1] }, ref: hashRef }
}
}
return undefined
}
const addRefViolation = ({ file, repo, ref, field, inputName, memberName }) => {
if (!repo || !ref || !isFirstParty(repo)) return
const expectedRef = expectedRefFor(repo)
const normalizedRef = normalizeRef(ref)
if (normalizedRef === expectedRef) return
violations.push({
type: 'ref',
file,
repo: repoKey(repo),
ref: normalizedRef,
expectedRef,
field,
inputName,
memberName,
})
}
const readJson = (file) => {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'))
} catch {
return undefined
}
}
const displayPath = (file) => path.relative(cwd, file) || path.basename(file)
const authorityNames = new Set(['megarepo.kdl', 'megarepo.json', 'megarepo.lock', 'flake.nix', 'flake.lock', 'devenv.yaml', 'devenv.lock'])
const ignoredDirs = new Set(['.git', 'node_modules', 'dist', 'result', '.devenv', '.direnv', '.next', '.storybook-static'])
const collectAuthorityFiles = (root) => {
const files = []
const walk = (dir) => {
let entries = []
try {
entries = fs.readdirSync(dir)
} catch {
return
}
for (const entry of entries) {
const full = path.join(dir, entry)
let stat
try {
stat = fs.statSync(full)
} catch {
continue
}
if (stat.isDirectory()) {
if (ignoredDirs.has(entry)) continue
if (dir === root && entry === 'repos') continue
walk(full)
} else if (stat.isFile() && authorityNames.has(entry)) {
files.push(full)
}
}
}
walk(root)
return files
}
const extractFlakeNixInputs = (content) => {
const inputs = []
const directPattern = /(?:inputs\.)?([a-zA-Z0-9_-]+)\.url\s*=\s*"([^"]+)"/g
let match
while ((match = directPattern.exec(content)) !== null) {
inputs.push({ inputName: match[1], url: match[2] })
}
const lines = content.split('\n')
let inInputs = false
let inputsIndent = -1
let currentInputName
let currentInputIndent = -1
for (const line of lines) {
const trimmed = line.trimStart()
const indent = line.length - trimmed.length
if (trimmed.startsWith('inputs = {')) {
inInputs = true
inputsIndent = indent
currentInputName = undefined
currentInputIndent = -1
continue
}
if (!inInputs) continue
if (trimmed && indent <= inputsIndent && trimmed.startsWith('};')) {
inInputs = false
currentInputName = undefined
continue
}
if (currentInputName && trimmed === '};' && indent <= currentInputIndent) {
currentInputName = undefined
continue
}
const inputStart = trimmed.match(/^(?:inputs\.)?([a-zA-Z0-9_-]+)\s*=\s*\{\s*$/)
if (inputStart && indent > inputsIndent) {
currentInputName = inputStart[1]
currentInputIndent = indent
continue
}
const urlMatch = trimmed.match(/^url\s*=\s*"([^"]+)"/)
if (currentInputName && urlMatch) inputs.push({ inputName: currentInputName, url: urlMatch[1] })
}
return inputs
}
const extractDevenvYamlInputs = (content) => {
const inputs = []
const lines = content.split('\n')
let inInputs = false
let inputsIndent = -1
let currentInputName
for (const line of lines) {
const trimmed = line.trimStart()
const indent = line.length - trimmed.length
if (trimmed === 'inputs:' && !inInputs) {
inInputs = true
inputsIndent = indent
currentInputName = undefined
continue
}
if (!inInputs) continue
if (trimmed && indent <= inputsIndent && !trimmed.startsWith('#')) {
inInputs = false
currentInputName = undefined
continue
}
const inputNameMatch = trimmed.match(/^([a-zA-Z0-9_-]+):$/)
if (inputNameMatch && indent > inputsIndent) {
currentInputName = inputNameMatch[1]
continue
}
const urlMatch = trimmed.match(/^url:\s*(.+)$/)
if (currentInputName && urlMatch) {
const raw = urlMatch[1].trim()
const url = (raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))
? raw.slice(1, -1)
: raw
inputs.push({ inputName: currentInputName, url })
}
}
return inputs
}
const lockInputs = (content) => {
const parsed = readJsonContent(content)
const rootName = parsed && (parsed.root || 'root')
const root = parsed && parsed.nodes && parsed.nodes[rootName]
if (!root || !root.inputs || !parsed.nodes) return []
return Object.entries(root.inputs).flatMap(([inputName, nodeName]) => {
const node = parsed.nodes[nodeName]
return node ? [{ inputName, original: node.original, locked: node.locked }] : []
})
}
const readJsonContent = (content) => {
try {
return JSON.parse(content)
} catch {
return undefined
}
}
const repoFromLockSection = (section) => {
if (!section || typeof section !== 'object') return undefined
if (section.type === 'github' && typeof section.owner === 'string' && typeof section.repo === 'string') {
return { owner: section.owner, repo: section.repo }
}
if (section.type === 'git' && typeof section.url === 'string') return githubRepoFromUrl(section.url)
if (typeof section.url === 'string') return parseGithubLikeRef(section.url)?.repo || githubRepoFromUrl(section.url)
return undefined
}
const refFromLockSection = (section) => {
if (!section || typeof section !== 'object') return undefined
if (typeof section.ref === 'string') return section.ref
if (typeof section.url === 'string') return parseGithubLikeRef(section.url)?.ref
return undefined
}
const roots = [cwd]
const reposRoot = path.join(cwd, 'repos')
if (fs.existsSync(reposRoot)) {
for (const entry of fs.readdirSync(reposRoot)) {
roots.push(path.join(reposRoot, entry))
}
}
for (const root of roots) {
for (const file of collectAuthorityFiles(root)) {
const base = path.basename(file)
const rel = displayPath(file)
const content = fs.readFileSync(file, 'utf8')
if (base === 'megarepo.kdl') {
for (const line of content.split('\n')) {
const match = line.trim().match(/^([A-Za-z0-9_.-]+)\s+"([^"]+)"/)
if (!match) continue
const parsed = parseGithubLikeRef(match[2])
addRefViolation({ file: rel, repo: parsed && parsed.repo, ref: parsed && parsed.ref, field: 'members.source', memberName: match[1] })
}
} else if (base === 'megarepo.lock') {
const parsed = readJson(file)
for (const [memberName, member] of Object.entries((parsed && parsed.members) || {})) {
const repo = typeof member.url === 'string' ? githubRepoFromUrl(member.url) : undefined
addRefViolation({ file: rel, repo, ref: member.ref, field: 'members.ref', memberName })
}
} else if (base === 'flake.nix') {
for (const input of extractFlakeNixInputs(content)) {
const parsed = parseGithubLikeRef(input.url)
addRefViolation({ file: rel, repo: parsed && parsed.repo, ref: parsed && parsed.ref, field: 'url.ref', inputName: input.inputName })
}
} else if (base === 'devenv.yaml') {
for (const input of extractDevenvYamlInputs(content)) {
const parsed = parseGithubLikeRef(input.url)
addRefViolation({ file: rel, repo: parsed && parsed.repo, ref: parsed && parsed.ref, field: 'url.ref', inputName: input.inputName })
}
} else if (base === 'flake.lock' || base === 'devenv.lock') {
for (const input of lockInputs(content)) {
for (const [field, section] of [['original', input.original], ['locked', input.locked]]) {
addRefViolation({ file: rel, repo: repoFromLockSection(section), ref: refFromLockSection(section), field: field + '.ref', inputName: input.inputName })
}
}
}
}
}
if (verifyReachable) {
const lock = readJson(path.join(cwd, 'megarepo.lock'))
for (const [memberName, member] of Object.entries((lock && lock.members) || {})) {
const repo = typeof member.url === 'string' ? githubRepoFromUrl(member.url) : undefined
if (!repo || !isFirstParty(repo)) continue
const expectedRef = expectedRefFor(repo)
const remote = 'https://github.com/' + repo.owner + '/' + repo.repo + '.git'
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'default-ref-policy-'))
let ok = false
try {
cp.execFileSync('git', ['-C', tmp, 'init', '-q'], { stdio: 'ignore' })
cp.execFileSync('git', ['-C', tmp, 'remote', 'add', 'origin', remote], { stdio: 'ignore' })
cp.execFileSync('git', ['-C', tmp, 'fetch', '-q', '--filter=blob:none', 'origin', 'refs/heads/' + expectedRef + ':refs/remotes/origin/' + expectedRef], { stdio: 'ignore' })
cp.execFileSync('git', ['-C', tmp, 'cat-file', '-e', member.commit + '^{commit}'], { stdio: 'ignore' })
cp.execFileSync('git', ['-C', tmp, 'merge-base', '--is-ancestor', member.commit, 'refs/remotes/origin/' + expectedRef], { stdio: 'ignore' })
ok = true
} catch {
ok = false
} finally {
fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
}
if (!ok) {
violations.push({ type: 'reachability', file: 'megarepo.lock', repo: repoKey(repo), rev: member.commit, expectedRef, memberName })
}
}
}
if (violations.length === 0) {
console.log('Default ref policy OK')
process.exit(0)
}
console.error('Default ref policy failed:')
for (const violation of violations) {
if (violation.type === 'reachability') {
console.error(' - ' + violation.file + ': member ' + violation.memberName + ' ' + violation.repo + ' locks ' + violation.rev.slice(0, 12) + " outside '" + violation.expectedRef + "'")
} else {
const name = violation.memberName ? ' member ' + violation.memberName : violation.inputName ? ' input ' + violation.inputName : ''
console.error(' - ' + violation.file + ':' + name + ' ' + violation.repo + " uses ref '" + violation.ref + "' in " + violation.field + "; expected '" + violation.expectedRef + "'")
}
}
console.error('')
console.error('Fix: merge upstream PRs first, retarget first-party inputs back to their default refs, then refresh locks.')
process.exit(1)
NODE
shell: bash
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-default-ref-policy"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
typecheck:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Verify OTEL shell entry
shell: bash
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run otel:test --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run otel:test --mode before'
command -v script >/dev/null 2>&1
tmp_log="$(mktemp)"
printf 'printf "OTEL_MODE=%%s\n" "$OTEL_MODE"
printf "OTEL_GRAFANA_LINK_URL=%%s\n" "$OTEL_GRAFANA_LINK_URL"
exit
' | script -qefc '"${DEVENV_BIN:?DEVENV_BIN not set}" shell --no-reload' "$tmp_log"
grep -q '\[otel\] Using .* OTEL stack' "$tmp_log"
grep -q '\[otel\] Start with: devenv up' "$tmp_log"
grep -q '^OTEL_MODE=' "$tmp_log"
grep -q '^OTEL_GRAFANA_LINK_URL=http' "$tmp_log"
rm -f "$tmp_log"
- name: Type check
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run ts:check:strict --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run ts:check:strict --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-typecheck"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
lint:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Format + lint
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run lint:check --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run lint:check --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-lint"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
test:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
strategy:
fail-fast: false
matrix:
runner: [namespace-profile-linux-x86-64, namespace-profile-macos-arm64]
runs-on: ['${{ matrix.runner }}', 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Unit tests
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run test:run --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run test:run --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-test-${{ strategy.job-index }}"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
nix-check:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
strategy:
fail-fast: false
matrix:
runner: [namespace-profile-linux-x86-64, namespace-profile-macos-arm64]
runs-on: ['${{ matrix.runner }}', 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Nix hash check
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run nix:check --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run nix:check --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-nix-check-${{ strategy.job-index }}"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
nix-fod-check:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
strategy:
fail-fast: false
matrix:
runner: [namespace-profile-linux-x86-64, namespace-profile-macos-arm64]
runs-on: ['${{ matrix.runner }}', 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Cold pnpm deps validation
shell: bash
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'Cold pnpm deps validation' 'set -euo pipefail
for attr in '"'"'.#genie-pnpm-deps'"'"' '"'"'.#megarepo-pnpm-deps'"'"' '"'"'.#oxc-config-plugin-pnpm-deps'"'"'; do
echo "::group::rebuild-check $attr"
# Step 1: Realize once (may substitute) so rebuild has a trusted output to compare against.
nix build --no-link "$attr" --option substituters '"'"'https://cache.nixos.org'"'"'
# Step 2: Rebuild and compare locally. This fails on stale fixed-output hashes without
# relying on whether a shared daemon store made the prior out path disappear.
nix build --no-link --rebuild "$attr" --option substituters '"'"'https://cache.nixos.org'"'"'
echo "::endgroup::"
done'
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-nix-fod-check-${{ strategy.job-index }}"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
pnpm-builder-contract:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Audit native dependency policy
shell: bash
run: |
set -euo pipefail
audit=genie/ci-scripts/native-dep-policy-audit.ts
if command -v bun >/dev/null 2>&1; then
bun "$audit"
else
nix run nixpkgs#bun -- "$audit"
fi
- name: Guard pnpm builder contract
shell: bash
run: |
set -euo pipefail
builder='nix/workspace-tools/lib/mk-pnpm-deps.nix'
policy='nix/workspace-tools/lib/pnpm-install-policy.nix'
if [ ! -f "$builder" ]; then
echo "::error::missing pnpm deps builder: $builder"
exit 1
fi
if [ ! -f "$policy" ]; then
echo "::error::missing pnpm install policy: $policy"
exit 1
fi
for required in \
'store-dir=%s' \
'frozenLockfile ? true' \
'pnpm install --frozen-lockfile --ignore-scripts'; do
if ! grep -Fq -- "$required" "$builder"; then
echo "::error::missing required pnpm builder contract fragment: $required"
exit 1
fi
done
for required in \
'side-effects-cache=false' \
'verify-store-integrity=true' \
'package-import-method=${packageImportMethod}' \
'pm-on-fail=ignore' \
'strict-store-pkg-content-check=true' \
'child-concurrency=1' \
'network-concurrency=4'; do
if ! grep -Fq -- "$required" "$policy"; then
echo "::error::missing required pnpm policy contract fragment: $required"
exit 1
fi
done
for forbidden in \
'package-import-method=hardlink' \
'lockfile-only' \
'pnpm add pnpm@'; do
if grep -Fq -- "$forbidden" "$builder" "$policy"; then
echo "::error::forbidden pnpm builder contract fragment present: $forbidden"
exit 1
fi
done
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-pnpm-builder-contract"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
pnpm-regression:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: pnpm regression suite
run: |
bash genie/ci-scripts/nix-gc-race-retry.test.sh
bash genie/ci-scripts/ci-measurement-comparison.test.sh
bash genie/ci-scripts/native-dep-policy-audit.test.sh
bash nix/workspace-tools/lib/mk-pnpm-cli/tests/run.sh --skip-genie --skip-megarepo --skip-devenv-shell --skip-downstream-megarepo
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-pnpm-regression"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
bundle-smoke:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Bundle smoke tests
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run bundle:smoke --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run bundle:smoke --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-bundle-smoke"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
cargo:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide Rust toolchain
shell: bash
run: |
set -euo pipefail
for out in $(nix build --no-link --print-out-paths nixpkgs#cargo nixpkgs#rustc nixpkgs#clippy nixpkgs#rustfmt); do
echo "$out/bin" >> "$GITHUB_PATH"
done
- name: Cargo build + test + clippy + fmt
shell: bash
run: |
set -euo pipefail
cd packages/@overeng/otelite
cargo build --release
cargo test
cargo clippy -- -D warnings
cargo fmt --check
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-cargo"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
devenv-perf:
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
permissions:
actions: read
contents: write
issues: write
pull-requests: write
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
ARTIFACT_DIR: tmp/devenv-perf-ci
OTEL_SERVICE_NAME: devenv-perf-ci
RUNNER_CLASS: 'namespace-profile-linux-x86-64,namespace-features:github.run-id=${{ github.run_id }}'
CI_MEASUREMENT_SUBJECT_REF: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.ref || github.ref }}
CI_MEASUREMENT_SUBJECT_SHA: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.sha || github.sha }}
CI_MEASUREMENT_SUBJECT_LABEL: ${{ inputs.measurement_baseline_label }}
CI_MEASUREMENT_ALLOW_PROBE_FAILURES: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && '1' || '' }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Benchmark devenv surfaces
shell: bash
run: |
set -euo pipefail
ensure_ci_measurement_tool() {
tool_name="$1"
nix_attr="$2"
if command -v "$tool_name" >/dev/null 2>&1; then
return 0
fi
if ! command -v nix >/dev/null 2>&1; then
return 1
fi
if tool_out="$(nix build --no-link --print-out-paths "nixpkgs#$nix_attr" 2>/dev/null)"; then
while IFS= read -r tool_path; do
[ -n "$tool_path" ] || continue
[ -d "$tool_path/bin" ] || continue
export PATH="$tool_path/bin:$PATH"
if command -v "$tool_name" >/dev/null 2>&1; then
return 0
fi
done <<EOF
$tool_out
EOF
fi
command -v "$tool_name" >/dev/null 2>&1
}
require_ci_measurement_tool() {
tool_name="$1"
nix_attr="$2"
if ensure_ci_measurement_tool "$tool_name" "$nix_attr"; then
return 0
fi
echo "::error::$tool_name is not available; unable to produce CI measurement artifact"
exit 1
}
require_ci_measurement_tool awk gawk.out
require_ci_measurement_tool jq jq.bin
ARTIFACT_DIR="$(mkdir -p "$ARTIFACT_DIR" && cd "$ARTIFACT_DIR" && pwd -P)"
CI_MEASUREMENT_HEAD_DIR="${CI_MEASUREMENT_HEAD_DIR:-$PWD}"
CI_MEASUREMENT_BASE_DIR="${CI_MEASUREMENT_BASE_DIR:-${RUNNER_TEMP:-/tmp}/ci-measurement-base}"
CI_MEASUREMENT_PAIRED_ENABLED=0
CI_MEASUREMENT_ORDER_SEED="${CI_MEASUREMENT_ORDER_SEED:-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-${GITHUB_SHA:-unknown}}"
prepare_paired_base_worktree() {
if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then
return 0
fi
if [ -n "${CI_MEASUREMENT_ALLOW_PROBE_FAILURES:-}" ]; then
return 0
fi
if [ ! -f "${GITHUB_EVENT_PATH:-}" ]; then
return 0
fi
local base_sha
base_sha="$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH")"
if [ -z "$base_sha" ]; then
echo "::notice::paired wall-clock baseline unavailable: pull_request.base.sha missing"
return 0
fi
rm -rf "$CI_MEASUREMENT_BASE_DIR"
git worktree prune >/dev/null 2>&1 || true
if git fetch --no-tags --depth=1 origin "$base_sha" \
&& git worktree add --detach "$CI_MEASUREMENT_BASE_DIR" "$base_sha" >/dev/null; then
CI_MEASUREMENT_PAIRED_ENABLED=1
echo "::notice::paired wall-clock baseline prepared at $CI_MEASUREMENT_BASE_DIR ($base_sha)"
else
echo "::warning::paired wall-clock baseline unavailable: failed to prepare base worktree $base_sha"
CI_MEASUREMENT_PAIRED_ENABLED=0
fi
}
prepare_paired_base_worktree
mkdir -p "$ARTIFACT_DIR/traces"
{
printf 'timestamp_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'repository=%s\n' "${GITHUB_REPOSITORY:-unknown}"
printf 'ref=%s\n' "${GITHUB_REF:-unknown}"
printf 'sha=%s\n' "${GITHUB_SHA:-unknown}"
printf 'runner_name=%s\n' "${RUNNER_NAME:-unknown}"
printf 'runner_os=%s\n' "${RUNNER_OS:-unknown}"
printf 'runner_arch=%s\n' "${RUNNER_ARCH:-unknown}"
printf 'runner_class=%s\n' "${RUNNER_CLASS:-unknown}"
printf 'cpu_count=%s\n' "$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || printf unknown)"
printf 'load_average=%s\n' "$(cat /proc/loadavg 2>/dev/null || uptime 2>/dev/null || printf unknown)"
printf 'memory=%s\n' "$(free -h 2>/dev/null | tr '\n' ';' || printf unknown)"
printf 'cpu_model=%s\n' "$(awk -F ': ' '/model name|Hardware|Processor/ { print $2; exit }' /proc/cpuinfo 2>/dev/null || printf unknown)"
printf 'devenv_rev=%s\n' "${DEVENV_REV:-unknown}"
printf 'otel_service_name=%s\n' "${OTEL_SERVICE_NAME:-unknown}"
nix store ping 2>/dev/null || true
nix config show substituters trusted-substituters builders max-jobs cores 2>/dev/null || true
df -h / /nix 2>/dev/null || df -h /
ps -eo pid,ppid,stat,etime,pcpu,pmem,comm,args 2>/dev/null \
| grep -E 'devenv direnv-export|nix-daemon|nix build|nix flake|github-runner' \
| grep -v grep || true
} >"$ARTIFACT_DIR/host-context.txt"
printf '[' >"$ARTIFACT_DIR/timings.json"
first=1
json_append_timing() {
local id="$1"
local label="$2"
local group="$3"
local description="$4"
local status="$5"
local duration_ms="$6"
local stdout="$7"
local stderr="$8"
local trace="$9"
local gate_policy="${10}"
local metadata_json="${11}"
local samples_file="$ARTIFACT_DIR/$id.samples.json"
if [ "$first" -eq 0 ]; then
printf ',' >>"$ARTIFACT_DIR/timings.json"
fi
first=0
jq -cn \
--slurpfile samples "$samples_file" \
--arg id "$id" \
--arg label "$label" \
--arg group "$group" \
--arg description "$description" \
--argjson status "$status" \
--argjson durationMs "$duration_ms" \
--arg stdout "$stdout" \
--arg stderr "$stderr" \
--arg trace "$trace" \
--argjson gatePolicy "$gate_policy" \
--argjson metadata "$metadata_json" \
'def median:
sort as $sorted
| ($sorted | length) as $count
| if $count == 0 then null
elif ($count % 2) == 1 then $sorted[($count / 2 | floor)]
else (($sorted[($count / 2 - 1)] + $sorted[($count / 2)]) / 2)
end;
def percentile($p):
sort as $sorted
| ($sorted | length) as $count
| if $count == 0 then null
else $sorted[(($p * ($count - 1)) | floor)]
end;
($samples[0] // []) as $sampleList
| ($sampleList | map(select((.subject // "head") == "head" and .phase != "warmup" and .status == 0) | .durationMs)) as $successfulDurations
| ($sampleList | map(select((.subject // "head") == "head" and .phase == "warmup"))) as $warmupSamples
| ($sampleList | map(select((.subject // "head") == "head" and .phase == "measured" and .status == 0 and .pairIndex != null))) as $headSamples
| ($sampleList | map(select(.subject == "base" and .phase == "measured" and .status == 0 and .pairIndex != null))) as $baseSamples
| (
$headSamples
| map(. as $head | $baseSamples[]? | select(.pairIndex == $head.pairIndex) | {
pairIndex: $head.pairIndex,
currentDurationMs: $head.durationMs,
baselineDurationMs: .durationMs,
deltaMs: ($head.durationMs - .durationMs)
})
) as $pairedSamples
| ($pairedSamples | map(.currentDurationMs)) as $pairedCurrentDurations
| ($pairedSamples | map(.baselineDurationMs)) as $pairedBaselineDurations
| ($pairedSamples | map(.deltaMs)) as $pairedDeltaDurations
| ($pairedDeltaDurations | median) as $pairedDeltaMedian
| {
id:$id,
name:$id,
label:$label,
group:(if $group == "" then null else $group end),
description:(if $description == "" then null else $description end),
status:$status,
durationMs:$durationMs,
stdout:$stdout,
stderr:$stderr,
trace:(if $trace == "" then null else $trace end),
metadata:$metadata,
gatePolicy:$gatePolicy,
statistics: {
sampleCount: ($sampleList | length),
warmupCount: ($warmupSamples | length),
measuredSampleCount: (
$sampleList
| map(select((.subject // "head") == "head" and .phase != "warmup"))
| length
),
successfulSampleCount: ($successfulDurations | length),
minDurationMs: ($successfulDurations | min),
maxDurationMs: ($successfulDurations | max),
medianDurationMs: $durationMs,
pairedSampleCount: ($pairedSamples | length),
pairedCurrentMedianDurationMs: ($pairedCurrentDurations | median),
pairedBaselineMedianDurationMs: ($pairedBaselineDurations | median),
pairedDeltaMedianDurationMs: $pairedDeltaMedian,
pairedDeltaMinDurationMs: ($pairedDeltaDurations | min),
pairedDeltaMaxDurationMs: ($pairedDeltaDurations | max),
pairedDeltaP25DurationMs: ($pairedDeltaDurations | percentile(0.25)),
pairedDeltaP75DurationMs: ($pairedDeltaDurations | percentile(0.75)),
pairedDeltaMadDurationMs: (
if $pairedDeltaMedian == null then null
else ($pairedDeltaDurations | map(. - $pairedDeltaMedian | if . < 0 then -. else . end) | median)
end
),
pairedDeltaSampleDurationMs: $pairedDeltaDurations
},
samples:$sampleList
}' \
>>"$ARTIFACT_DIR/timings.json"
}
measure() {
local id="$1"
local label="$2"
local group="$3"
local description="$4"
local trace_file="$5"
local warmup_repetitions="$6"
local repetitions="$7"
local gate_policy="$8"
local metadata_json="$9"
shift 9
case "$trace_file" in
'$ARTIFACT_DIR'*) trace_file="${ARTIFACT_DIR}${trace_file#'$ARTIFACT_DIR'}" ;;
esac
local stdout="$ARTIFACT_DIR/$id.stdout"
local stderr="$ARTIFACT_DIR/$id.stderr"
local samples_file="$ARTIFACT_DIR/$id.samples.json"
local started ended status duration_ms
mkdir -p "$(dirname "$trace_file")"
if ! [[ "$repetitions" =~ ^[0-9]+$ ]] || [ "$repetitions" -lt 1 ]; then
repetitions=1
fi
if ! [[ "$warmup_repetitions" =~ ^[0-9]+$ ]] || [ "$warmup_repetitions" -lt 0 ]; then
warmup_repetitions=0
fi
printf '[' >"$samples_file"
local sample_first=1
local sample_index measured_index total_repetitions phase sample_stdout sample_stderr sample_trace expanded
local order_offset
order_offset="$(printf '%s' "$CI_MEASUREMENT_ORDER_SEED:$id" | cksum | awk '{ print $1 % 2 }')"
total_repetitions=$((warmup_repetitions + repetitions))
for sample_index in $(seq 1 "$total_repetitions"); do
if [ "$sample_index" -le "$warmup_repetitions" ]; then
phase="warmup"
measured_index=""
else
phase="measured"
measured_index=$((sample_index - warmup_repetitions))
fi
sample_stdout="$ARTIFACT_DIR/$id.$sample_index.stdout"
sample_stderr="$ARTIFACT_DIR/$id.$sample_index.stderr"
sample_trace=""
if [ -n "$trace_file" ]; then
sample_trace="$trace_file"
if [ "$repetitions" -gt 1 ]; then
sample_trace="${trace_file%.*}.$sample_index.${trace_file##*.}"
fi
fi
expanded=()
for arg in "$@"; do
case "$arg" in
'$DEVENV_BIN') expanded+=("${DEVENV_BIN:?DEVENV_BIN not set}") ;;
'$DEVENV_SHELL_TRACE_COMMAND')
if "${DEVENV_BIN:?DEVENV_BIN not set}" --help 2>&1 | grep -q -- '--trace-to'; then
expanded+=("${DEVENV_BIN:?DEVENV_BIN not set}" "--trace-to" "json:file:$sample_trace" "shell" "--no-reload" "--" "true")
elif "${DEVENV_BIN:?DEVENV_BIN not set}" --help 2>&1 | grep -q -- '--trace-format'; then
expanded+=("${DEVENV_BIN:?DEVENV_BIN not set}" "--trace-format" "json" "shell" "--no-reload" "--" "true")
sample_trace=""
else
expanded+=("${DEVENV_BIN:?DEVENV_BIN not set}" "shell" "--no-reload" "--" "true")
sample_trace=""
fi
;;
'$ARTIFACT_DIR'*) expanded+=("${ARTIFACT_DIR}${arg#'$ARTIFACT_DIR'}") ;;
'json:file:$trace_file') expanded+=("json:file:$sample_trace") ;;
'$trace_file') expanded+=("file:$sample_trace") ;;
*) expanded+=("$arg") ;;
esac
done
local base_ran_before_head=0 base_stdout base_stderr base_started base_ended base_status base_duration_ms
if [ "$phase" = "measured" ] && [ "$CI_MEASUREMENT_PAIRED_ENABLED" -eq 1 ] && [ $(((measured_index + order_offset) % 2)) -eq 0 ]; then
base_ran_before_head=1
base_stdout="$ARTIFACT_DIR/$id.$sample_index.base.stdout"
base_stderr="$ARTIFACT_DIR/$id.$sample_index.base.stderr"
base_started="$(date +%s%3N)"
set +e
(cd "$CI_MEASUREMENT_BASE_DIR" && "${expanded[@]}") >"$base_stdout" 2>"$base_stderr"
base_status=$?
set -e
base_ended="$(date +%s%3N)"
base_duration_ms=$((base_ended - base_started))
if [ "$sample_first" -eq 0 ]; then
printf ',' >>"$samples_file"
fi
sample_first=0
jq -cn \
--argjson index "$sample_index" \
--arg measuredIndex "$measured_index" \
--argjson status "$base_status" \
--argjson durationMs "$base_duration_ms" \
--arg stdout "$base_stdout" \
--arg stderr "$base_stderr" \
--arg orderSeed "$CI_MEASUREMENT_ORDER_SEED" \
'{index:$index,measuredIndex:($measuredIndex | tonumber),pairIndex:($measuredIndex | tonumber),subject:"base",phase:"measured",status:$status,durationMs:$durationMs,stdout:$stdout,stderr:$stderr,trace:null,order:"base-head",orderSeed:$orderSeed}' \
>>"$samples_file"
if [ "$base_status" -ne 0 ]; then
echo "::warning::$id paired baseline sample $measured_index failed after ${base_duration_ms}ms; this pair is excluded from wall-clock gating"
tail -40 "$base_stderr" || true
fi
fi
started="$(date +%s%3N)"
set +e
(cd "$CI_MEASUREMENT_HEAD_DIR" && "${expanded[@]}") >"$sample_stdout" 2>"$sample_stderr"
status=$?
set -e
ended="$(date +%s%3N)"
duration_ms=$((ended - started))
if [ "$sample_first" -eq 0 ]; then
printf ',' >>"$samples_file"
fi
sample_first=0
jq -cn \
--argjson index "$sample_index" \
--arg measuredIndex "$measured_index" \
--arg phase "$phase" \
--argjson status "$status" \
--argjson durationMs "$duration_ms" \
--arg stdout "$sample_stdout" \
--arg stderr "$sample_stderr" \
--arg trace "$sample_trace" \
--arg order "$(if [ "$phase" = "measured" ] && [ "$base_ran_before_head" -eq 1 ]; then printf base-head; else printf head-base; fi)" \
--arg orderSeed "$CI_MEASUREMENT_ORDER_SEED" \
'{index:$index,measuredIndex:(if $measuredIndex == "" then null else ($measuredIndex | tonumber) end),pairIndex:(if $measuredIndex == "" then null else ($measuredIndex | tonumber) end),subject:"head",phase:$phase,status:$status,durationMs:$durationMs,stdout:$stdout,stderr:$stderr,trace:(if $trace == "" then null else $trace end),order:(if $phase == "measured" then $order else null end),orderSeed:(if $phase == "measured" then $orderSeed else null end)}' \
>>"$samples_file"
if [ "$phase" = "measured" ] && [ "$status" -eq 0 ] && [ "$CI_MEASUREMENT_PAIRED_ENABLED" -eq 1 ] && [ "$base_ran_before_head" -eq 0 ]; then
base_stdout="$ARTIFACT_DIR/$id.$sample_index.base.stdout"
base_stderr="$ARTIFACT_DIR/$id.$sample_index.base.stderr"
base_started="$(date +%s%3N)"
set +e
(cd "$CI_MEASUREMENT_BASE_DIR" && "${expanded[@]}") >"$base_stdout" 2>"$base_stderr"
base_status=$?
set -e
base_ended="$(date +%s%3N)"
base_duration_ms=$((base_ended - base_started))
printf ',' >>"$samples_file"
jq -cn \
--argjson index "$sample_index" \
--arg measuredIndex "$measured_index" \
--argjson status "$base_status" \
--argjson durationMs "$base_duration_ms" \
--arg stdout "$base_stdout" \
--arg stderr "$base_stderr" \
--arg orderSeed "$CI_MEASUREMENT_ORDER_SEED" \
'{index:$index,measuredIndex:($measuredIndex | tonumber),pairIndex:($measuredIndex | tonumber),subject:"base",phase:"measured",status:$status,durationMs:$durationMs,stdout:$stdout,stderr:$stderr,trace:null,order:"head-base",orderSeed:$orderSeed}' \
>>"$samples_file"
if [ "$base_status" -ne 0 ]; then
echo "::warning::$id paired baseline sample $measured_index failed after ${base_duration_ms}ms; this pair is excluded from wall-clock gating"
tail -40 "$base_stderr" || true
fi
fi
stdout="$sample_stdout"
stderr="$sample_stderr"
trace_file="$sample_trace"
if [ "$status" -ne 0 ]; then
break
fi
done
printf ']\n' >>"$samples_file"
status="$(jq -r 'map(select((.subject // "head") == "head") | .status) | max // 0' "$samples_file")"
duration_ms="$(jq -r 'map(select((.subject // "head") == "head" and .phase != "warmup" and .status == 0) | .durationMs) as $values | if ($values | length) == 0 then (map(select((.subject // "head") == "head") | .durationMs) | max // 0) else ($values | sort | .[(length - 1) / 2 | floor]) end' "$samples_file")"
cp "$stdout" "$ARTIFACT_DIR/$id.stdout" 2>/dev/null || true
cp "$stderr" "$ARTIFACT_DIR/$id.stderr" 2>/dev/null || true
json_append_timing "$id" "$label" "$group" "$description" "$status" "$duration_ms" "$ARTIFACT_DIR/$id.stdout" "$ARTIFACT_DIR/$id.stderr" "$trace_file" "$gate_policy" "$metadata_json"
if [ "$status" -ne 0 ]; then
if [ "${CI_MEASUREMENT_ALLOW_PROBE_FAILURES:-}" = "1" ]; then
echo "::warning::$id failed after ${duration_ms}ms; keeping earlier successful baseline probes and excluding this failed probe from numeric observations"
else
echo "::error::$id failed after ${duration_ms}ms; stderr tail follows"
fi
tail -80 "$stderr" || true
if [ "${CI_MEASUREMENT_ALLOW_PROBE_FAILURES:-}" != "1" ]; then
return "$status"
fi
fi
}
measure 'shell_eval_traced' 'Shell eval with OTEL trace' 'devenv shell' 'Evaluates the dev shell with native devenv JSON tracing enabled.' '$ARTIFACT_DIR/traces/shell_eval_traced.json' '0' '1' '{"enabled":false,"minBaselineSources":10,"minCurrentSamples":3,"warnRatio":1.25,"failRatio":1.5,"warnAbs":1.5,"failAbs":3,"noiseFloor":0.5,"statisticalToleranceRatio":0.2,"statisticalToleranceAbs":1}' '{"path":[],"dimensions":{}}' '$DEVENV_SHELL_TRACE_COMMAND'
measure 'shell_eval_warm' 'Warm shell eval' 'devenv shell' 'Evaluates a warm dev shell without reloading direnv state.' '' '1' '5' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":5,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.1,"failRatio":1.2,"warnAbs":0.25,"failAbs":1,"noiseFloor":0.1,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.25}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'shell' '--no-reload' '--' 'true'
measure 'tasks_list' 'devenv tasks list' 'devenv cli' 'Lists devenv tasks to measure task graph loading overhead.' '' '1' '9' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":7,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.25,"failRatio":1.5,"warnAbs":0.05,"failAbs":0.15,"noiseFloor":0.03,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.03}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'tasks' 'list'
measure 'processes_help' 'devenv processes --help' 'devenv cli' 'Loads the devenv processes command help path.' '' '1' '9' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":7,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.25,"failRatio":1.5,"warnAbs":0.05,"failAbs":0.15,"noiseFloor":0.03,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.03}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'processes' '--help'
measure 'task_pnpm_install' 'pnpm install task' 'workspace setup' 'Runs the cached pnpm install devenv task.' '' '1' '5' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":5,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.1,"failRatio":1.2,"warnAbs":0.25,"failAbs":1,"noiseFloor":0.1,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.25}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'tasks' 'run' 'pnpm:install' '--mode' 'before' '--no-tui' '--show-output'
measure 'task_genie_run' 'Genie run task' 'genie' 'Runs the normal devenv genie:run task including its declared dependencies.' '' '1' '5' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":5,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.1,"failRatio":1.2,"warnAbs":0.25,"failAbs":1,"noiseFloor":0.1,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.25}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'tasks' 'run' 'genie:run' '--mode' 'before' '--no-tui' '--show-output'
measure 'task_check_quick_warm' 'Warm cached check:quick' 'quality gates' 'Runs the fast local quality gate through devenv after a warmup. This measures the cached no-op path and task/status orchestration overhead.' '' '1' '5' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":5,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.1,"failRatio":1.2,"warnAbs":0.25,"failAbs":1,"noiseFloor":0.1,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.25}' '{"path":["quality gates","check:quick"],"dimensions":{"workload":"cached-no-op","taskCacheMode":"warm"}}' '$DEVENV_BIN' 'tasks' 'run' 'check:quick' '--mode' 'before' '--no-tui' '--show-output'
measure 'task_check_quick_forced' 'Forced check:quick' 'quality gates' 'Runs the fast local quality gate through devenv with task-cache refresh. This measures the developer-facing quick-check workload rather than the cached no-op path.' '' '0' '3' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":3,"minBaselineSources":10,"minCurrentSamples":3,"warnRatio":1.15,"failRatio":1.3,"warnAbs":1.5,"failAbs":4,"noiseFloor":0.75,"statisticalToleranceRatio":0.15,"statisticalToleranceAbs":1}' '{"path":["quality gates","check:quick"],"dimensions":{"workload":"forced-task-cache","taskCacheMode":"refresh"}}' '$DEVENV_BIN' 'tasks' 'run' 'check:quick' '--mode' 'before' '--no-tui' '--show-output' '--refresh-task-cache'
measure 'genie_check_task' 'Genie check task' 'genie' 'Runs the supported Genie check task without shell-entry overhead.' '' '1' '5' '{"enabled":true,"comparisonMode":"paired","minPairedSamples":5,"minBaselineSources":10,"minCurrentSamples":5,"warnRatio":1.1,"failRatio":1.2,"warnAbs":0.25,"failAbs":1,"noiseFloor":0.1,"statisticalToleranceRatio":0.1,"statisticalToleranceAbs":0.25}' '{"path":[],"dimensions":{}}' '$DEVENV_BIN' 'tasks' 'run' 'genie:check' '--mode' 'before' '--no-tui'
printf ']\n' >>"$ARTIFACT_DIR/timings.json"
jq . "$ARTIFACT_DIR/timings.json" >"$ARTIFACT_DIR/timings.pretty.json"
jq -n \
--slurpfile timings "$ARTIFACT_DIR/timings.json" \
--arg schemaVersion "1" \
--arg generatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repository "${GITHUB_REPOSITORY:-unknown}" \
--arg ref "${GITHUB_REF:-unknown}" \
--arg sha "${GITHUB_SHA:-unknown}" \
--arg runnerName "${RUNNER_NAME:-unknown}" \
--arg runnerOs "${RUNNER_OS:-unknown}" \
--arg runnerArch "${RUNNER_ARCH:-unknown}" \
--arg devenvRev "${DEVENV_REV:-unknown}" \
--arg otelServiceName "${OTEL_SERVICE_NAME:-unknown}" \
'{
schemaVersion: $schemaVersion,
generatedAt: $generatedAt,
repository: $repository,
ref: $ref,
sha: $sha,
runner: { name: $runnerName, os: $runnerOs, arch: $runnerArch },
devenv: { rev: $devenvRev },
otel: { serviceName: $otelServiceName },
checks: ($timings[0] | map({ key: .id, value: . }) | from_entries)
}' >"$ARTIFACT_DIR/summary.json"
jq -n \
--slurpfile timings "$ARTIFACT_DIR/timings.json" \
--argjson schemaVersion 1 \
--arg generatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repository "${GITHUB_REPOSITORY:-unknown}" \
--arg branchKind "${GITHUB_EVENT_NAME:-unknown}" \
--arg ref "${CI_MEASUREMENT_SUBJECT_REF:-${GITHUB_REF:-unknown}}" \
--arg headSha "${CI_MEASUREMENT_SUBJECT_SHA:-${GITHUB_SHA:-unknown}}" \
--arg baseSha "${GITHUB_BASE_SHA:-}" \
--arg runnerName "${RUNNER_NAME:-unknown}" \
--arg runnerOs "${RUNNER_OS:-unknown}" \
--arg runnerArch "${RUNNER_ARCH:-unknown}" \
--arg runnerClass "${RUNNER_CLASS:-unknown}" \
--arg githubRunId "${GITHUB_RUN_ID:-unknown}" \
--arg githubRunAttempt "${GITHUB_RUN_ATTEMPT:-unknown}" \
--arg githubJob "${GITHUB_JOB:-unknown}" \
--arg taskId "${CROSSTASK_TASK_ID:-}" \
--arg taskAttemptId "${CROSSTASK_ATTEMPT_ID:-}" \
--arg traceId "${TRACE_ID:-}" \
--arg devenvRev "${DEVENV_REV:-unknown}" \
--arg otelServiceName "${OTEL_SERVICE_NAME:-unknown}" \
--arg orderSeed "$CI_MEASUREMENT_ORDER_SEED" \
--arg targetSystem "${DEVENV_SYSTEM:-${RUNNER_OS:-unknown}}" \
'{
schemaVersion: $schemaVersion,
generatedAt: $generatedAt,
producer: {
name: "effect-utils-ci-measurement",
version: 2,
measurementProtocol: "devenv-perf-warm-median-v2"
},
subject: {
repo: $repository,
branchKind: (if $branchKind == "" then "unknown" else $branchKind end),
ref: $ref,
headSha: $headSha,
baseSha: $baseSha
},
execution: {
provider: (if ($githubRunId != "" and $githubRunId != "unknown") then "github-actions" else "local" end),
workflow: "CI",
job: $githubJob,
runId: $githubRunId,
runAttempt: $githubRunAttempt,
taskId: $taskId,
attemptId: $taskAttemptId,
traceId: $traceId,
runner: { name: $runnerName, os: $runnerOs, arch: $runnerArch, class: $runnerClass }
},
target: { kind: "devenv", id: "dev-shell", name: "dev-shell", label: "Dev shell", group: "devenv", system: $targetSystem },
observations: (
$timings[0]
| map(select(.status == 0))
| map({
id: ("devenv." + .id + ".duration"),
label: .label,
group: .group,
path: (.metadata.path // []),
description: .description,
measurementKind: (if (.gatePolicy.enabled == false) then "diagnostic" else "wall-clock" end),
name: ("devenv." + .id + ".duration"),
unit: "seconds",
value: (.durationMs / 1000),
policy: .gatePolicy,
comparison: {
mode: (.gatePolicy.comparisonMode // "historical"),
pairedSampleCount: (.statistics.pairedSampleCount // 0),
baseline: (
if (.statistics.pairedBaselineMedianDurationMs // null) == null
then null
else (.statistics.pairedBaselineMedianDurationMs / 1000)
end
)
},
statistics: {
sampleCount: (.statistics.sampleCount // 1),
warmupCount: (.statistics.warmupCount // 0),
measuredSampleCount: (.statistics.measuredSampleCount // (.statistics.sampleCount // 1)),
successfulSampleCount: (.statistics.successfulSampleCount // (if .status == 0 then 1 else 0 end)),
min: ((.statistics.minDurationMs // .durationMs) / 1000),
max: ((.statistics.maxDurationMs // .durationMs) / 1000),
median: ((.statistics.medianDurationMs // .durationMs) / 1000),
pairedSampleCount: (.statistics.pairedSampleCount // 0),
pairedCurrentMedian: (
if (.statistics.pairedCurrentMedianDurationMs // null) == null
then null
else (.statistics.pairedCurrentMedianDurationMs / 1000)
end
),
pairedBaselineMedian: (
if (.statistics.pairedBaselineMedianDurationMs // null) == null
then null
else (.statistics.pairedBaselineMedianDurationMs / 1000)
end
),
pairedDeltaMedian: (
if (.statistics.pairedDeltaMedianDurationMs // null) == null
then null
else (.statistics.pairedDeltaMedianDurationMs / 1000)
end
),
pairedDeltaMin: (
if (.statistics.pairedDeltaMinDurationMs // null) == null
then null
else (.statistics.pairedDeltaMinDurationMs / 1000)
end
),
pairedDeltaMax: (
if (.statistics.pairedDeltaMaxDurationMs // null) == null
then null
else (.statistics.pairedDeltaMaxDurationMs / 1000)
end
),
pairedDeltaP25: (
if (.statistics.pairedDeltaP25DurationMs // null) == null
then null
else (.statistics.pairedDeltaP25DurationMs / 1000)
end
),
pairedDeltaP75: (
if (.statistics.pairedDeltaP75DurationMs // null) == null
then null
else (.statistics.pairedDeltaP75DurationMs / 1000)
end
),
pairedDeltaMad: (
if (.statistics.pairedDeltaMadDurationMs // null) == null
then null
else (.statistics.pairedDeltaMadDurationMs / 1000)
end
),
pairedDeltaSamples: ((.statistics.pairedDeltaSampleDurationMs // []) | map(. / 1000))
},
dimensions: ((.metadata.dimensions // {}) + {
probe: .id,
probeLabel: .label,
status: .status,
sampleCount: (.statistics.sampleCount // 1),
warmupCount: (.statistics.warmupCount // 0),
measuredSampleCount: (.statistics.measuredSampleCount // (.statistics.sampleCount // 1)),
pairedSampleCount: (.statistics.pairedSampleCount // 0),
pairedOrderProtocol: (
if (.statistics.pairedSampleCount // 0) > 0
then "balanced-seeded-alternating-v1"
else null
end
),
pairedOrderSeed: (
if (.statistics.pairedSampleCount // 0) > 0
then $orderSeed
else null
end
),
measurementProtocol: "devenv-perf-warm-median-v2",
aggregation: "median",
phase: "warm",
devenvRev: $devenvRev,
otelServiceName: $otelServiceName
})
})
),
artifacts: [
{ name: "host-context", path: "host-context.txt", contentType: "text/plain" },
{ name: "timings", path: "timings.json", contentType: "application/json" },
{ name: "summary", path: "summary.json", contentType: "application/json" },
{ name: "shell-eval-trace", path: "traces/shell_eval_traced.json", contentType: "application/json" }
],
details: {
stdoutStderrByProbe: (
$timings[0]
| map({ key: .id, value: { label: .label, group: .group, description: .description, stdout: .stdout, stderr: .stderr, trace: .trace } })
| from_entries
)
}
}' >"$ARTIFACT_DIR/measurements.json"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### Devenv perf"
echo ""
echo "| Probe | Status | Duration |"
echo "| --- | ---: | ---: |"
jq -r '.[] | "| \(.label // .id) | \(.status) | \(.durationMs) ms |"' "$ARTIFACT_DIR/timings.json"
echo ""
echo "- Artifact directory: \`$ARTIFACT_DIR\`"
echo "- OTEL service: \`${OTEL_SERVICE_NAME:-unknown}\`"
} >>"$GITHUB_STEP_SUMMARY"
fi
cat "$ARTIFACT_DIR/timings.pretty.json"
- name: Upload devenv perf artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: devenv-perf
path: |
tmp/devenv-perf-ci
!tmp/devenv-perf-ci/baseline/**
if-no-files-found: error
retention-days: 7
timeout-minutes: 30
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-devenv-perf"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
nix-closure-sizes:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
permissions:
actions: read
contents: write
issues: write
pull-requests: write
env:
CI_MEASUREMENT_SUBJECT_REF: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.ref || github.ref }}
CI_MEASUREMENT_SUBJECT_SHA: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.sha || github.sha }}
CI_MEASUREMENT_SUBJECT_LABEL: ${{ inputs.measurement_baseline_label }}
CI_MEASUREMENT_ALLOW_PROBE_FAILURES: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && '1' || '' }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: 'Measure Nix closure: genie'
shell: bash
env:
ARTIFACT_DIR: tmp/nix-closure-ci/current/genie_package
RUNNER_CLASS: '${{ runner.os }}-${{ runner.arch }}'
run: |
set -euo pipefail
mkdir -p "$ARTIFACT_DIR"
installable='.#genie'
target_id='genie_package'
target_name='genie'
target_label='Genie package'
target_group='packages'
target_description='the packaged Genie CLI closure'
artifact_file="$ARTIFACT_DIR/measurements.json"
target_system='x86_64-linux'
out_path="$(nix build --no-update-lock-file --no-link --print-out-paths "$installable")"
path_info="$ARTIFACT_DIR/nix-closure-path-info.json"
paths_file="$ARTIFACT_DIR/nix-closure-paths.json"
nix path-info --recursive --closure-size --json "$out_path" >"$path_info"
jq 'to_entries | map({ path: .key, narSize: (.value.narSize // 0), closureSize: (.value.closureSize // 0) })' "$path_info" >"$paths_file"
jq -n \
--slurpfile paths "$paths_file" \
--argjson schemaVersion 1 \
--arg generatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repository "${GITHUB_REPOSITORY:-unknown}" \
--arg branchKind "${GITHUB_EVENT_NAME:-unknown}" \
--arg ref "${CI_MEASUREMENT_SUBJECT_REF:-${GITHUB_REF:-unknown}}" \
--arg headSha "${CI_MEASUREMENT_SUBJECT_SHA:-${GITHUB_SHA:-unknown}}" \
--arg baseSha "${GITHUB_BASE_SHA:-}" \
--arg runnerName "${RUNNER_NAME:-unknown}" \
--arg runnerOs "${RUNNER_OS:-unknown}" \
--arg runnerArch "${RUNNER_ARCH:-unknown}" \
--arg runnerClass "${RUNNER_CLASS:-unknown}" \
--arg githubRunId "${GITHUB_RUN_ID:-unknown}" \
--arg githubRunAttempt "${GITHUB_RUN_ATTEMPT:-unknown}" \
--arg githubJob "${GITHUB_JOB:-unknown}" \
--arg taskId "${CROSSTASK_TASK_ID:-}" \
--arg taskAttemptId "${CROSSTASK_ATTEMPT_ID:-}" \
--arg traceId "${TRACE_ID:-}" \
--arg targetName "$target_name" \
--arg targetId "$target_id" \
--arg targetLabel "$target_label" \
--arg targetGroup "$target_group" \
--arg targetDescription "$target_description" \
--arg targetSystem "$target_system" \
--arg outPath "$out_path" \
--argjson buckets '[{"name":"node","label":"Node / pnpm","pathRegex":"node_modules|npm-deps|pnpm"},{"name":"nix-sources","label":"Nix sources","pathRegex":"-source$"},{"name":"rust","label":"Rust","pathRegex":"cargo|rust|rustc"}]' \
--argjson targetPath '["nix","closures","packages","genie"]' \
--argjson gatePolicy '{}' \
'
($paths[0] // []) as $closurePaths
| ($closurePaths | map(select(.path == $outPath) | .closureSize) | first // ($closurePaths | map(.narSize) | add // 0)) as $totalClosureSize
| ($closurePaths | map(.narSize) | add // 0) as $totalNarSize
| ($closurePaths | length) as $pathCount
| ($buckets | map(
. as $bucket
| {
name: "nix.closure.bucket.nar_size",
id: "nix.closure.bucket.nar_size",
label: (($bucket.label // $bucket.name) + " closure size"),
group: "nix closure buckets",
path: ($targetPath + ["buckets", $bucket.name]),
description: ("Store size contributed by closure paths matching " + $bucket.pathRegex),
measurementKind: "deterministic",
unit: "bytes",
value: (
$closurePaths
| map(select(.path | test($bucket.pathRegex)) | .narSize)
| add // 0
),
policy: $gatePolicy,
dimensions: { bucket: $bucket.name }
}
)) as $bucketObservations
| {
schemaVersion: $schemaVersion,
generatedAt: $generatedAt,
producer: { name: "effect-utils-ci-measurement", version: 1 },
subject: {
repo: $repository,
branchKind: (if $branchKind == "" then "unknown" else $branchKind end),
ref: $ref,
headSha: $headSha,
baseSha: $baseSha
},
execution: {
provider: (if ($githubRunId != "" and $githubRunId != "unknown") then "github-actions" else "local" end),
workflow: "CI",
job: $githubJob,
runId: $githubRunId,
runAttempt: $githubRunAttempt,
taskId: $taskId,
attemptId: $taskAttemptId,
traceId: $traceId,
runner: { name: $runnerName, os: $runnerOs, arch: $runnerArch, class: $runnerClass }
},
target: { kind: "nix-closure", id: $targetId, name: $targetName, label: $targetLabel, group: $targetGroup, path: $targetPath, system: $targetSystem },
observations: ([
{
id: "nix.closure.nar_size",
label: "Total closure size",
group: "nix closure",
path: ($targetPath + ["total", "closure-size"]),
description: ("Total recursive store closure size for " + $targetDescription),
name: "nix.closure.nar_size",
measurementKind: "deterministic",
unit: "bytes",
value: $totalClosureSize,
policy: $gatePolicy,
dimensions: { bucket: "total" }
},
{
id: "nix.closure.serialized_nar_size",
label: "Total serialized NAR size",
group: "nix closure diagnostics",
path: ($targetPath + ["total", "serialized-nar-size"]),
description: ("Diagnostic sum of serialized NAR sizes for all paths in " + $targetDescription),
name: "nix.closure.serialized_nar_size",
measurementKind: "diagnostic",
unit: "bytes",
value: $totalNarSize,
policy: { comparisonMode: "diagnostic", gate: "off" },
dimensions: { bucket: "total", sizeKind: "nar" }
},
{
id: "nix.closure.path_count",
label: "Total closure path count",
group: "nix closure",
path: ($targetPath + ["total", "path-count"]),
description: ("Number of store paths in " + $targetDescription),
name: "nix.closure.path_count",
measurementKind: "deterministic",
unit: "count",
value: $pathCount,
policy: $gatePolicy,
dimensions: { bucket: "total" }
}
] + $bucketObservations),
artifacts: [
{ name: "nix-closure-path-info", path: "nix-closure-path-info.json", contentType: "application/json" },
{ name: "nix-closure-paths", path: "nix-closure-paths.json", contentType: "application/json" }
],
details: {
outPath: $outPath,
topPaths: ($closurePaths | sort_by(.narSize) | reverse | .[:30])
}
}
' >"$artifact_file"
cat "$artifact_file"
- name: 'Measure Nix closure: megarepo'
shell: bash
env:
ARTIFACT_DIR: tmp/nix-closure-ci/current/megarepo_package
RUNNER_CLASS: '${{ runner.os }}-${{ runner.arch }}'
run: |
set -euo pipefail
mkdir -p "$ARTIFACT_DIR"
installable='.#megarepo'
target_id='megarepo_package'
target_name='megarepo'
target_label='Megarepo package'
target_group='packages'
target_description='the packaged megarepo CLI closure'
artifact_file="$ARTIFACT_DIR/measurements.json"
target_system='x86_64-linux'
out_path="$(nix build --no-update-lock-file --no-link --print-out-paths "$installable")"
path_info="$ARTIFACT_DIR/nix-closure-path-info.json"
paths_file="$ARTIFACT_DIR/nix-closure-paths.json"
nix path-info --recursive --closure-size --json "$out_path" >"$path_info"
jq 'to_entries | map({ path: .key, narSize: (.value.narSize // 0), closureSize: (.value.closureSize // 0) })' "$path_info" >"$paths_file"
jq -n \
--slurpfile paths "$paths_file" \
--argjson schemaVersion 1 \
--arg generatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repository "${GITHUB_REPOSITORY:-unknown}" \
--arg branchKind "${GITHUB_EVENT_NAME:-unknown}" \
--arg ref "${CI_MEASUREMENT_SUBJECT_REF:-${GITHUB_REF:-unknown}}" \
--arg headSha "${CI_MEASUREMENT_SUBJECT_SHA:-${GITHUB_SHA:-unknown}}" \
--arg baseSha "${GITHUB_BASE_SHA:-}" \
--arg runnerName "${RUNNER_NAME:-unknown}" \
--arg runnerOs "${RUNNER_OS:-unknown}" \
--arg runnerArch "${RUNNER_ARCH:-unknown}" \
--arg runnerClass "${RUNNER_CLASS:-unknown}" \
--arg githubRunId "${GITHUB_RUN_ID:-unknown}" \
--arg githubRunAttempt "${GITHUB_RUN_ATTEMPT:-unknown}" \
--arg githubJob "${GITHUB_JOB:-unknown}" \
--arg taskId "${CROSSTASK_TASK_ID:-}" \
--arg taskAttemptId "${CROSSTASK_ATTEMPT_ID:-}" \
--arg traceId "${TRACE_ID:-}" \
--arg targetName "$target_name" \
--arg targetId "$target_id" \
--arg targetLabel "$target_label" \
--arg targetGroup "$target_group" \
--arg targetDescription "$target_description" \
--arg targetSystem "$target_system" \
--arg outPath "$out_path" \
--argjson buckets '[{"name":"node","label":"Node / pnpm","pathRegex":"node_modules|npm-deps|pnpm"},{"name":"nix-sources","label":"Nix sources","pathRegex":"-source$"},{"name":"rust","label":"Rust","pathRegex":"cargo|rust|rustc"}]' \
--argjson targetPath '["nix","closures","packages","megarepo"]' \
--argjson gatePolicy '{}' \
'
($paths[0] // []) as $closurePaths
| ($closurePaths | map(select(.path == $outPath) | .closureSize) | first // ($closurePaths | map(.narSize) | add // 0)) as $totalClosureSize
| ($closurePaths | map(.narSize) | add // 0) as $totalNarSize
| ($closurePaths | length) as $pathCount
| ($buckets | map(
. as $bucket
| {
name: "nix.closure.bucket.nar_size",
id: "nix.closure.bucket.nar_size",
label: (($bucket.label // $bucket.name) + " closure size"),
group: "nix closure buckets",
path: ($targetPath + ["buckets", $bucket.name]),
description: ("Store size contributed by closure paths matching " + $bucket.pathRegex),
measurementKind: "deterministic",
unit: "bytes",
value: (
$closurePaths
| map(select(.path | test($bucket.pathRegex)) | .narSize)
| add // 0
),
policy: $gatePolicy,
dimensions: { bucket: $bucket.name }
}
)) as $bucketObservations
| {
schemaVersion: $schemaVersion,
generatedAt: $generatedAt,
producer: { name: "effect-utils-ci-measurement", version: 1 },
subject: {
repo: $repository,
branchKind: (if $branchKind == "" then "unknown" else $branchKind end),
ref: $ref,
headSha: $headSha,
baseSha: $baseSha
},
execution: {
provider: (if ($githubRunId != "" and $githubRunId != "unknown") then "github-actions" else "local" end),
workflow: "CI",
job: $githubJob,
runId: $githubRunId,
runAttempt: $githubRunAttempt,
taskId: $taskId,
attemptId: $taskAttemptId,
traceId: $traceId,
runner: { name: $runnerName, os: $runnerOs, arch: $runnerArch, class: $runnerClass }
},
target: { kind: "nix-closure", id: $targetId, name: $targetName, label: $targetLabel, group: $targetGroup, path: $targetPath, system: $targetSystem },
observations: ([
{
id: "nix.closure.nar_size",
label: "Total closure size",
group: "nix closure",
path: ($targetPath + ["total", "closure-size"]),
description: ("Total recursive store closure size for " + $targetDescription),
name: "nix.closure.nar_size",
measurementKind: "deterministic",
unit: "bytes",
value: $totalClosureSize,
policy: $gatePolicy,
dimensions: { bucket: "total" }
},
{
id: "nix.closure.serialized_nar_size",
label: "Total serialized NAR size",
group: "nix closure diagnostics",
path: ($targetPath + ["total", "serialized-nar-size"]),
description: ("Diagnostic sum of serialized NAR sizes for all paths in " + $targetDescription),
name: "nix.closure.serialized_nar_size",
measurementKind: "diagnostic",
unit: "bytes",
value: $totalNarSize,
policy: { comparisonMode: "diagnostic", gate: "off" },
dimensions: { bucket: "total", sizeKind: "nar" }
},
{
id: "nix.closure.path_count",
label: "Total closure path count",
group: "nix closure",
path: ($targetPath + ["total", "path-count"]),
description: ("Number of store paths in " + $targetDescription),
name: "nix.closure.path_count",
measurementKind: "deterministic",
unit: "count",
value: $pathCount,
policy: $gatePolicy,
dimensions: { bucket: "total" }
}
] + $bucketObservations),
artifacts: [
{ name: "nix-closure-path-info", path: "nix-closure-path-info.json", contentType: "application/json" },
{ name: "nix-closure-paths", path: "nix-closure-paths.json", contentType: "application/json" }
],
details: {
outPath: $outPath,
topPaths: ($closurePaths | sort_by(.narSize) | reverse | .[:30])
}
}
' >"$artifact_file"
cat "$artifact_file"
- name: 'Measure Nix closure: oxlint-npm'
shell: bash
env:
ARTIFACT_DIR: tmp/nix-closure-ci/current/oxlint_npm_package
RUNNER_CLASS: '${{ runner.os }}-${{ runner.arch }}'
run: |
set -euo pipefail
mkdir -p "$ARTIFACT_DIR"
installable='.#oxlint-npm'
target_id='oxlint_npm_package'
target_name='oxlint-npm'
target_label='oxlint npm package'
target_group='packages'
target_description='the packaged oxlint npm compatibility wrapper closure'
artifact_file="$ARTIFACT_DIR/measurements.json"
target_system='x86_64-linux'
out_path="$(nix build --no-update-lock-file --no-link --print-out-paths "$installable")"
path_info="$ARTIFACT_DIR/nix-closure-path-info.json"
paths_file="$ARTIFACT_DIR/nix-closure-paths.json"
nix path-info --recursive --closure-size --json "$out_path" >"$path_info"
jq 'to_entries | map({ path: .key, narSize: (.value.narSize // 0), closureSize: (.value.closureSize // 0) })' "$path_info" >"$paths_file"
jq -n \
--slurpfile paths "$paths_file" \
--argjson schemaVersion 1 \
--arg generatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repository "${GITHUB_REPOSITORY:-unknown}" \
--arg branchKind "${GITHUB_EVENT_NAME:-unknown}" \
--arg ref "${CI_MEASUREMENT_SUBJECT_REF:-${GITHUB_REF:-unknown}}" \
--arg headSha "${CI_MEASUREMENT_SUBJECT_SHA:-${GITHUB_SHA:-unknown}}" \
--arg baseSha "${GITHUB_BASE_SHA:-}" \
--arg runnerName "${RUNNER_NAME:-unknown}" \
--arg runnerOs "${RUNNER_OS:-unknown}" \
--arg runnerArch "${RUNNER_ARCH:-unknown}" \
--arg runnerClass "${RUNNER_CLASS:-unknown}" \
--arg githubRunId "${GITHUB_RUN_ID:-unknown}" \
--arg githubRunAttempt "${GITHUB_RUN_ATTEMPT:-unknown}" \
--arg githubJob "${GITHUB_JOB:-unknown}" \
--arg taskId "${CROSSTASK_TASK_ID:-}" \
--arg taskAttemptId "${CROSSTASK_ATTEMPT_ID:-}" \
--arg traceId "${TRACE_ID:-}" \
--arg targetName "$target_name" \
--arg targetId "$target_id" \
--arg targetLabel "$target_label" \
--arg targetGroup "$target_group" \
--arg targetDescription "$target_description" \
--arg targetSystem "$target_system" \
--arg outPath "$out_path" \
--argjson buckets '[{"name":"node","label":"Node / pnpm","pathRegex":"node_modules|npm-deps|pnpm"},{"name":"nix-sources","label":"Nix sources","pathRegex":"-source$"},{"name":"rust","label":"Rust","pathRegex":"cargo|rust|rustc"}]' \
--argjson targetPath '["nix","closures","packages","oxlint-npm"]' \
--argjson gatePolicy '{}' \
'
($paths[0] // []) as $closurePaths
| ($closurePaths | map(select(.path == $outPath) | .closureSize) | first // ($closurePaths | map(.narSize) | add // 0)) as $totalClosureSize
| ($closurePaths | map(.narSize) | add // 0) as $totalNarSize
| ($closurePaths | length) as $pathCount
| ($buckets | map(
. as $bucket
| {
name: "nix.closure.bucket.nar_size",
id: "nix.closure.bucket.nar_size",
label: (($bucket.label // $bucket.name) + " closure size"),
group: "nix closure buckets",
path: ($targetPath + ["buckets", $bucket.name]),
description: ("Store size contributed by closure paths matching " + $bucket.pathRegex),
measurementKind: "deterministic",
unit: "bytes",
value: (
$closurePaths
| map(select(.path | test($bucket.pathRegex)) | .narSize)
| add // 0
),
policy: $gatePolicy,
dimensions: { bucket: $bucket.name }
}
)) as $bucketObservations
| {
schemaVersion: $schemaVersion,
generatedAt: $generatedAt,
producer: { name: "effect-utils-ci-measurement", version: 1 },
subject: {
repo: $repository,
branchKind: (if $branchKind == "" then "unknown" else $branchKind end),
ref: $ref,
headSha: $headSha,
baseSha: $baseSha
},
execution: {
provider: (if ($githubRunId != "" and $githubRunId != "unknown") then "github-actions" else "local" end),
workflow: "CI",
job: $githubJob,
runId: $githubRunId,
runAttempt: $githubRunAttempt,
taskId: $taskId,
attemptId: $taskAttemptId,
traceId: $traceId,
runner: { name: $runnerName, os: $runnerOs, arch: $runnerArch, class: $runnerClass }
},
target: { kind: "nix-closure", id: $targetId, name: $targetName, label: $targetLabel, group: $targetGroup, path: $targetPath, system: $targetSystem },
observations: ([
{
id: "nix.closure.nar_size",
label: "Total closure size",
group: "nix closure",
path: ($targetPath + ["total", "closure-size"]),
description: ("Total recursive store closure size for " + $targetDescription),
name: "nix.closure.nar_size",
measurementKind: "deterministic",
unit: "bytes",
value: $totalClosureSize,
policy: $gatePolicy,
dimensions: { bucket: "total" }
},
{
id: "nix.closure.serialized_nar_size",
label: "Total serialized NAR size",
group: "nix closure diagnostics",
path: ($targetPath + ["total", "serialized-nar-size"]),
description: ("Diagnostic sum of serialized NAR sizes for all paths in " + $targetDescription),
name: "nix.closure.serialized_nar_size",
measurementKind: "diagnostic",
unit: "bytes",
value: $totalNarSize,
policy: { comparisonMode: "diagnostic", gate: "off" },
dimensions: { bucket: "total", sizeKind: "nar" }
},
{
id: "nix.closure.path_count",
label: "Total closure path count",
group: "nix closure",
path: ($targetPath + ["total", "path-count"]),
description: ("Number of store paths in " + $targetDescription),
name: "nix.closure.path_count",
measurementKind: "deterministic",
unit: "count",
value: $pathCount,
policy: $gatePolicy,
dimensions: { bucket: "total" }
}
] + $bucketObservations),
artifacts: [
{ name: "nix-closure-path-info", path: "nix-closure-path-info.json", contentType: "application/json" },
{ name: "nix-closure-paths", path: "nix-closure-paths.json", contentType: "application/json" }
],
details: {
outPath: $outPath,
topPaths: ($closurePaths | sort_by(.narSize) | reverse | .[:30])
}
}
' >"$artifact_file"
cat "$artifact_file"
- name: 'Upload CI measurements: nix-closure-measurements'
if: always()
uses: actions/upload-artifact@v4
with:
name: nix-closure-measurements
path: |
tmp/nix-closure-ci
!tmp/nix-closure-ci/baseline/**
if-no-files-found: error
retention-days: 14
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-nix-closure-sizes"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
source-shape:
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
permissions:
actions: read
contents: write
issues: write
pull-requests: write
env:
CI_MEASUREMENT_SUBJECT_REF: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.ref || github.ref }}
CI_MEASUREMENT_SUBJECT_SHA: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.sha || github.sha }}
CI_MEASUREMENT_SUBJECT_LABEL: ${{ inputs.measurement_baseline_label }}
CI_MEASUREMENT_ALLOW_PROBE_FAILURES: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && '1' || '' }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: 'Measure source shape: effect-utils'
shell: bash
env:
ARTIFACT_DIR: tmp/source-shape-ci/current/effect-utils
RUNNER_CLASS: '${{ runner.os }}-${{ runner.arch }}'
run: |
set -euo pipefail
ensure_ci_measurement_tool() {
tool_name="$1"
nix_attr="$2"
if command -v "$tool_name" >/dev/null 2>&1; then
return 0
fi
if ! command -v nix >/dev/null 2>&1; then
return 1
fi
if tool_out="$(nix build --no-link --print-out-paths "nixpkgs#$nix_attr" 2>/dev/null)"; then
while IFS= read -r tool_path; do
[ -n "$tool_path" ] || continue
[ -d "$tool_path/bin" ] || continue
export PATH="$tool_path/bin:$PATH"
if command -v "$tool_name" >/dev/null 2>&1; then
return 0
fi
done <<EOF
$tool_out
EOF
fi
command -v "$tool_name" >/dev/null 2>&1
}
require_ci_measurement_tool() {
tool_name="$1"
nix_attr="$2"
if ensure_ci_measurement_tool "$tool_name" "$nix_attr"; then
return 0
fi
echo "::error::$tool_name is not available; unable to produce CI measurement artifact"
exit 1
}
require_ci_measurement_tool node nodejs
mkdir -p "$ARTIFACT_DIR"
target_id='effect_utils'
target_name='effect-utils'
target_label='effect-utils repository'
target_group='source'
artifact_file="$ARTIFACT_DIR/measurements.json"
target_system="${DEVENV_SYSTEM:-${RUNNER_OS:-unknown}}"
SCOPES_JSON='[{"id":"genie_ci_workflow","label":"Genie CI workflow helpers","group":"source / ci","path":["source","effect-utils","genie","ci-workflow"],"includePaths":["genie/ci-workflow",".github/workflows/ci.yml.genie.ts"],"includeExtensions":[".ts"]},{"id":"genie_runtime","label":"Genie runtime","group":"source / genie","path":["source","effect-utils","packages","genie"],"includePaths":["packages/@overeng/genie/src"],"includeExtensions":[".ts",".tsx"]},{"id":"nix_workspace_tools","label":"Nix workspace tools","group":"source / nix","path":["source","effect-utils","nix","workspace-tools"],"includePaths":["nix/workspace-tools"],"includeExtensions":[".nix"]}]' \
TARGET_PATH_JSON='["source","effect-utils"]' \
TARGET_ID="$target_id" \
TARGET_NAME="$target_name" \
TARGET_LABEL="$target_label" \
TARGET_GROUP="$target_group" \
TARGET_SYSTEM="$target_system" \
node <<'NODE' >"$artifact_file"
const cp = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const normalize = (value) => {
const normalized = value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
return normalized === '.' ? '' : normalized
}
const scopes = JSON.parse(process.env.SCOPES_JSON || '[]')
const targetPath = JSON.parse(process.env.TARGET_PATH_JSON || '["source"]')
const gitFiles = cp
.execFileSync('git', ['ls-files', '-z'], { encoding: 'buffer' })
.toString('utf8')
.split('\0')
.filter(Boolean)
.map(normalize)
const includesPath = (file, candidates) => {
if (!Array.isArray(candidates) || candidates.length === 0) return true
return candidates.map(normalize).some((candidate) => candidate === '' || file === candidate || file.startsWith(candidate + '/'))
}
const excludesPath = (file, candidates) =>
Array.isArray(candidates) &&
candidates.map(normalize).some((candidate) => candidate !== '' && (file === candidate || file.startsWith(candidate + '/')))
const matchesExtension = (file, extensions) => {
if (!Array.isArray(extensions) || extensions.length === 0) return true
const ext = path.extname(file).toLowerCase()
return extensions.map((extension) => extension.toLowerCase()).some((extension) => ext === extension)
}
const countLines = (file) => {
const buffer = fs.readFileSync(file)
if (buffer.includes(0)) return undefined
if (buffer.length === 0) return 0
let lines = 0
for (const byte of buffer) {
if (byte === 10) lines += 1
}
return buffer[buffer.length - 1] === 10 ? lines : lines + 1
}
const observations = []
const scopeSummaries = []
for (const scope of scopes) {
const root = normalize(scope.root || '.')
const includePaths = Array.isArray(scope.includePaths) && scope.includePaths.length > 0 ? scope.includePaths : [root]
const files = gitFiles
.filter((file) => includesPath(file, includePaths))
.filter((file) => !excludesPath(file, scope.excludePaths))
.filter((file) => matchesExtension(file, scope.includeExtensions))
let lineCount = 0
let measuredFileCount = 0
for (const file of files) {
const lines = countLines(file)
if (lines === undefined) continue
lineCount += lines
measuredFileCount += 1
}
const group = scope.group || 'source shape'
const scopePath = Array.isArray(scope.path) ? scope.path : ['source', scope.id]
const policy = scope.gate || { enabled: false, minBaselineSources: 3, minCurrentSamples: 1 }
observations.push(
{
id: 'source.lines',
label: scope.label + ' lines',
group,
path: scopePath,
description: 'Tracked non-binary source lines in the configured scope.',
measurementKind: 'deterministic',
name: 'source.lines',
unit: 'lines',
value: lineCount,
dimensions: { scope: scope.id },
policy,
statistics: { sampleCount: 1, measuredSampleCount: measuredFileCount },
},
{
id: 'source.files',
label: scope.label + ' files',
group,
path: scopePath,
description: 'Tracked non-binary source files in the configured scope.',
measurementKind: 'deterministic',
name: 'source.files',
unit: 'count',
value: measuredFileCount,
dimensions: { scope: scope.id },
policy,
statistics: { sampleCount: 1, measuredSampleCount: measuredFileCount },
},
)
scopeSummaries.push({
id: scope.id,
label: scope.label,
root,
includePaths,
excludePaths: scope.excludePaths || [],
includeExtensions: scope.includeExtensions || [],
fileCount: measuredFileCount,
lineCount,
})
}
const artifact = {
schemaVersion: 1,
generatedAt: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
producer: {
name: 'effect-utils-ci-measurement',
version: 1,
measurementProtocol: 'source-shape-v1',
},
subject: {
repo: process.env.GITHUB_REPOSITORY || 'unknown',
branchKind: process.env.GITHUB_EVENT_NAME || 'unknown',
ref: process.env.CI_MEASUREMENT_SUBJECT_REF || process.env.GITHUB_REF || 'unknown',
headSha: process.env.CI_MEASUREMENT_SUBJECT_SHA || process.env.GITHUB_SHA || 'unknown',
baseSha: process.env.GITHUB_BASE_SHA || '',
},
execution: {
provider: process.env.GITHUB_RUN_ID && process.env.GITHUB_RUN_ID !== 'unknown' ? 'github-actions' : 'local',
workflow: 'CI',
job: process.env.GITHUB_JOB || 'unknown',
runId: process.env.GITHUB_RUN_ID || 'unknown',
runAttempt: process.env.GITHUB_RUN_ATTEMPT || 'unknown',
taskId: process.env.CROSSTASK_TASK_ID || '',
attemptId: process.env.CROSSTASK_ATTEMPT_ID || '',
traceId: process.env.TRACE_ID || '',
runner: {
name: process.env.RUNNER_NAME || 'unknown',
os: process.env.RUNNER_OS || 'unknown',
arch: process.env.RUNNER_ARCH || 'unknown',
class: process.env.RUNNER_CLASS || 'unknown',
},
},
target: {
kind: 'source-shape',
id: process.env.TARGET_ID,
name: process.env.TARGET_NAME,
label: process.env.TARGET_LABEL,
group: process.env.TARGET_GROUP,
path: targetPath,
system: process.env.TARGET_SYSTEM,
},
observations,
details: { scopes: scopeSummaries },
}
process.stdout.write(JSON.stringify(artifact, null, 2) + '\n')
NODE
cat "$artifact_file"
- name: 'Upload CI measurements: source-shape'
if: always()
uses: actions/upload-artifact@v4
with:
name: source-shape
path: |
tmp/source-shape-ci
!tmp/source-shape-ci/baseline/**
if-no-files-found: error
retention-days: 14
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-source-shape"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
ci-measurements-report:
name: ci/measurements-report
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
needs: [devenv-perf, nix-closure-sizes, source-shape]
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
defaults:
run:
shell: bash
permissions:
actions: read
contents: write
issues: write
pull-requests: write
env:
CI_MEASUREMENT_SUBJECT_REF: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.ref || github.ref }}
CI_MEASUREMENT_SUBJECT_SHA: ${{ inputs.measurement_baseline_ref || github.event.pull_request.head.sha || github.sha }}
CI_MEASUREMENT_SUBJECT_LABEL: ${{ inputs.measurement_baseline_label }}
CI_MEASUREMENT_ALLOW_PROBE_FAILURES: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && '1' || '' }}
steps:
- uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide CI measurement report tools
shell: bash
run: |
set -euo pipefail
for out in $(nix build --no-link --print-out-paths nixpkgs#jq nixpkgs#nodejs nixpkgs#gh nixpkgs#resvg); do
echo "$out/bin" >> "$GITHUB_PATH"
done
- name: 'Download current measurement artifact: devenv-perf'
uses: actions/download-artifact@v4
with:
name: devenv-perf
path: tmp/ci-measurement-report/current/devenv-perf
- name: 'Download current measurement artifact: nix-closure-measurements'
uses: actions/download-artifact@v4
with:
name: nix-closure-measurements
path: tmp/ci-measurement-report/current/nix-closure-measurements
- name: 'Download current measurement artifact: source-shape'
uses: actions/download-artifact@v4
with:
name: source-shape
path: tmp/ci-measurement-report/current/source-shape
- name: 'Download previous artifact: devenv-perf'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BASELINE_ARTIFACT_NAME: devenv-perf
BASELINE_OUTPUT_DIR: tmp/ci-measurement-report/baseline/devenv-perf
BASELINE_WORKFLOW_NAME: ${{ github.workflow }}
BASELINE_BRANCH: ${{ github.base_ref || github.ref_name }}
BASELINE_SEED_RUNS_JSON: '[]'
BASELINE_MAX_RUNS: '20'
BASELINE_MAX_CANDIDATE_RUNS: '60'
BASELINE_REQUIRED_OBSERVATIONS_JSON: '[]'
BASELINE_DOWNLOAD_TIMEOUT_SECONDS: '120'
run: |
set -euo pipefail
mkdir -p "$BASELINE_OUTPUT_DIR"
if command -v gh >/dev/null 2>&1; then
GH_BIN="$(command -v gh)"
else
echo "::notice::gh is not on PATH; resolving GitHub CLI through Nix"
if ! GH_BIN="$(nix build --no-link --print-out-paths nixpkgs#gh 2>/dev/null)/bin/gh"; then
echo "::notice::unable to resolve GitHub CLI through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using GitHub CLI: $GH_BIN"
CURL_BIN="$(command -v curl || true)"
if [ -z "$CURL_BIN" ]; then
echo "::notice::curl is not on PATH; resolving curl through Nix"
if ! CURL_BIN="$(nix build --no-link --print-out-paths nixpkgs#curl 2>/dev/null)/bin/curl"; then
echo "::notice::unable to resolve curl through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using curl: $CURL_BIN"
UNZIP_BIN="$(command -v unzip || true)"
if [ -z "$UNZIP_BIN" ]; then
echo "::notice::unzip is not on PATH; resolving unzip through Nix"
if ! UNZIP_BIN="$(nix build --no-link --print-out-paths nixpkgs#unzip 2>/dev/null)/bin/unzip"; then
echo "::notice::unable to resolve unzip through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using unzip: $UNZIP_BIN"
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}"
workflow="${BASELINE_WORKFLOW_NAME:-CI}"
branch="${BASELINE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF_NAME:-main}}}"
seed_runs_file="$BASELINE_OUTPUT_DIR/baseline-seed-runs.json"
required_observations_file="$BASELINE_OUTPUT_DIR/baseline-required-observations.json"
printf '%s' "${BASELINE_SEED_RUNS_JSON:-[]}" >"$seed_runs_file"
printf '%s' "${BASELINE_REQUIRED_OBSERVATIONS_JSON:-[]}" >"$required_observations_file"
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.runId | type == "string")) else false end' \
"$seed_runs_file" >/dev/null; then
echo "::error::BASELINE_SEED_RUNS_JSON must be an array of objects with string runId fields"
exit 1
fi
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.id | type == "string") and (.minSources | type == "number")) else false end' \
"$required_observations_file" >/dev/null; then
echo "::error::BASELINE_REQUIRED_OBSERVATIONS_JSON must be an array of objects with string id and numeric minSources fields"
exit 1
fi
seed_run_ids="$(jq -r '.[].runId' "$seed_runs_file")"
required_observation_count="$(jq 'length' "$required_observations_file")"
max_candidate_runs="${BASELINE_MAX_CANDIDATE_RUNS:-${BASELINE_MAX_RUNS:-5}}"
if ! [[ "$max_candidate_runs" =~ ^[0-9]+$ ]] || [ "$max_candidate_runs" -lt 1 ]; then
max_candidate_runs=1
fi
candidate_runs="$(
"$GH_BIN" run list \
--repo "$repo" \
--workflow "$workflow" \
--branch "$branch" \
--event push \
--status success \
--json databaseId,headSha \
--limit "$max_candidate_runs" \
--jq '[.[] | select(.headSha != env.GITHUB_SHA) | .databaseId] | .[]'
)"
candidate_runs="$seed_run_ids
$candidate_runs"
max_runs="${BASELINE_MAX_RUNS:-5}"
if ! [[ "$max_runs" =~ ^[0-9]+$ ]] || [ "$max_runs" -lt 1 ]; then
max_runs=1
fi
download_timeout_seconds="${BASELINE_DOWNLOAD_TIMEOUT_SECONDS:-120}"
if ! [[ "$download_timeout_seconds" =~ ^[0-9]+$ ]] || [ "$download_timeout_seconds" -lt 1 ]; then
download_timeout_seconds=120
fi
write_baseline_observation_counts() {
local measurement_index="$BASELINE_OUTPUT_DIR/baseline-measurement-files.txt"
local counts_file="$BASELINE_OUTPUT_DIR/baseline-observation-counts.json"
find "$BASELINE_OUTPUT_DIR" \
-mindepth 2 \
-maxdepth 2 \
-name measurements.json \
-type f \
-print \
| sort >"$measurement_index" || true
if [ -s "$measurement_index" ]; then
xargs -r jq -s \
--slurpfile required "$required_observations_file" \
'
([.[] | (.observations // [])[]? | select(.value | type == "number") | .id] | sort | group_by(.) | map({id: .[0], sources: length})) as $counts
| ($required[0] // []) as $requiredRows
| {
counts: $counts,
required: (
$requiredRows
| map(. as $requiredRow | ($counts | map(select(.id == $requiredRow.id)) | .[0].sources // 0) as $actual | $requiredRow + {sources:$actual, satisfied:($actual >= $requiredRow.minSources)})
)
}
' <"$measurement_index" >"$counts_file"
else
jq -n --slurpfile required "$required_observations_file" \
'{counts: [], required: (($required[0] // []) | map(. + {sources:0, satisfied:false}))}' >"$counts_file"
fi
}
baseline_requirements_satisfied() {
if [ "$required_observation_count" -eq 0 ]; then
return 1
fi
write_baseline_observation_counts
jq -e '.required | all(.satisfied == true)' "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" >/dev/null
}
download_artifact_archive() {
local artifact_id="$1"
local output_dir="$2"
local archive_file="$output_dir/artifact.zip"
local artifact_url="https://api.github.com/repos/$repo/actions/artifacts/$artifact_id/zip"
local curl_args=(
--fail
--location
--silent
--show-error
--connect-timeout 15
--max-time "$download_timeout_seconds"
--retry 2
--retry-delay 2
--retry-max-time "$download_timeout_seconds"
--header "Accept: application/vnd.github+json"
--header "X-GitHub-Api-Version: 2022-11-28"
--output "$archive_file"
)
if [ -n "${GH_TOKEN:-}" ]; then
curl_args+=(--header "Authorization: Bearer ${GH_TOKEN}")
fi
"$CURL_BIN" "${curl_args[@]}" "$artifact_url"
"$UNZIP_BIN" -q -o "$archive_file" -d "$output_dir"
rm -f "$archive_file"
}
run_id=""
artifact_name=""
artifact_id=""
downloaded_runs_file="$BASELINE_OUTPUT_DIR/baseline-runs.jsonl"
seen_runs_file="$BASELINE_OUTPUT_DIR/baseline-seen-runs.txt"
: >"$downloaded_runs_file"
: >"$seen_runs_file"
for candidate_run in $candidate_runs; do
if [ -z "$candidate_run" ]; then
continue
fi
if grep -qxF "$candidate_run" "$seen_runs_file"; then
continue
fi
downloaded_count="$(wc -l <"$downloaded_runs_file" | tr -d ' ')"
if [ "$downloaded_count" -ge "$max_runs" ]; then
if baseline_requirements_satisfied; then
break
fi
echo "::notice::downloaded $downloaded_count baseline artifact(s), but required observation counts are not satisfied yet; continuing through bounded candidate history"
fi
if [ "$(wc -l <"$seen_runs_file" | tr -d ' ')" -ge "$max_candidate_runs" ]; then
break
fi
printf '%s\n' "$candidate_run" >>"$seen_runs_file"
artifact_json="$(
"$GH_BIN" api "repos/$repo/actions/runs/$candidate_run/artifacts" \
| jq --arg artifactName "$BASELINE_ARTIFACT_NAME" '.artifacts
| map(select(.expired == false))
| map(select(.name == $artifactName or (.name | startswith($artifactName + "-"))))
| sort_by(.created_at // "")
| reverse
| .[0] // empty'
)"
if [ -n "$artifact_json" ]; then
current_artifact_name="$(printf '%s' "$artifact_json" | jq -r '.name')"
current_artifact_id="$(printf '%s' "$artifact_json" | jq -r '.id')"
current_output_dir="$BASELINE_OUTPUT_DIR/run-$candidate_run"
mkdir -p "$current_output_dir"
if download_artifact_archive "$current_artifact_id" "$current_output_dir"; then
if [ -z "$run_id" ]; then
run_id="$candidate_run"
artifact_name="$current_artifact_name"
artifact_id="$current_artifact_id"
fi
jq -cn \
--arg runId "$candidate_run" \
--arg artifactName "$current_artifact_name" \
--arg artifactId "$current_artifact_id" \
--arg path "run-$candidate_run" \
'{runId:$runId, artifactName:$artifactName, artifactId:$artifactId, path:$path}' \
>>"$downloaded_runs_file"
else
status="$?"
rm -rf "$current_output_dir"
echo "::notice::failed or timed out after ${download_timeout_seconds}s downloading baseline artifact $current_artifact_name from run $candidate_run (exit $status); skipping candidate"
fi
fi
done
write_baseline_observation_counts
if [ -z "$run_id" ] || [ -z "$artifact_name" ]; then
echo "::notice::no successful baseline run found for $repo workflow=$workflow branch=$branch"
exit 0
fi
jq -n \
--slurpfile runs "$downloaded_runs_file" \
--slurpfile seedRuns "$seed_runs_file" \
--slurpfile observationCounts "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" \
--argjson schemaVersion 1 \
--arg repository "$repo" \
--arg workflow "$workflow" \
--arg branch "$branch" \
--arg runId "$run_id" \
--arg artifactName "$artifact_name" \
--arg artifactId "$artifact_id" \
'{
schemaVersion: $schemaVersion,
source: "github-actions-artifact",
repository: $repository,
workflow: $workflow,
branch: $branch,
runId: $runId,
artifactName: $artifactName,
artifactId: $artifactId,
seedRuns: ($seedRuns[0] // []),
runs: $runs,
observationCounts: ($observationCounts[0] // null)
}' >"$BASELINE_OUTPUT_DIR/baseline-provenance.json"
echo "Downloaded $(wc -l <"$downloaded_runs_file" | tr -d ' ') baseline artifact(s), latest $artifact_name from run $run_id into $BASELINE_OUTPUT_DIR"
- name: 'Download previous artifact: nix-closure-measurements'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BASELINE_ARTIFACT_NAME: nix-closure-measurements
BASELINE_OUTPUT_DIR: tmp/ci-measurement-report/baseline/nix-closure-measurements
BASELINE_WORKFLOW_NAME: ${{ github.workflow }}
BASELINE_BRANCH: ${{ github.base_ref || github.ref_name }}
BASELINE_SEED_RUNS_JSON: '[]'
BASELINE_MAX_RUNS: '20'
BASELINE_MAX_CANDIDATE_RUNS: '60'
BASELINE_REQUIRED_OBSERVATIONS_JSON: '[]'
BASELINE_DOWNLOAD_TIMEOUT_SECONDS: '120'
run: |
set -euo pipefail
mkdir -p "$BASELINE_OUTPUT_DIR"
if command -v gh >/dev/null 2>&1; then
GH_BIN="$(command -v gh)"
else
echo "::notice::gh is not on PATH; resolving GitHub CLI through Nix"
if ! GH_BIN="$(nix build --no-link --print-out-paths nixpkgs#gh 2>/dev/null)/bin/gh"; then
echo "::notice::unable to resolve GitHub CLI through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using GitHub CLI: $GH_BIN"
CURL_BIN="$(command -v curl || true)"
if [ -z "$CURL_BIN" ]; then
echo "::notice::curl is not on PATH; resolving curl through Nix"
if ! CURL_BIN="$(nix build --no-link --print-out-paths nixpkgs#curl 2>/dev/null)/bin/curl"; then
echo "::notice::unable to resolve curl through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using curl: $CURL_BIN"
UNZIP_BIN="$(command -v unzip || true)"
if [ -z "$UNZIP_BIN" ]; then
echo "::notice::unzip is not on PATH; resolving unzip through Nix"
if ! UNZIP_BIN="$(nix build --no-link --print-out-paths nixpkgs#unzip 2>/dev/null)/bin/unzip"; then
echo "::notice::unable to resolve unzip through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using unzip: $UNZIP_BIN"
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}"
workflow="${BASELINE_WORKFLOW_NAME:-CI}"
branch="${BASELINE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF_NAME:-main}}}"
seed_runs_file="$BASELINE_OUTPUT_DIR/baseline-seed-runs.json"
required_observations_file="$BASELINE_OUTPUT_DIR/baseline-required-observations.json"
printf '%s' "${BASELINE_SEED_RUNS_JSON:-[]}" >"$seed_runs_file"
printf '%s' "${BASELINE_REQUIRED_OBSERVATIONS_JSON:-[]}" >"$required_observations_file"
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.runId | type == "string")) else false end' \
"$seed_runs_file" >/dev/null; then
echo "::error::BASELINE_SEED_RUNS_JSON must be an array of objects with string runId fields"
exit 1
fi
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.id | type == "string") and (.minSources | type == "number")) else false end' \
"$required_observations_file" >/dev/null; then
echo "::error::BASELINE_REQUIRED_OBSERVATIONS_JSON must be an array of objects with string id and numeric minSources fields"
exit 1
fi
seed_run_ids="$(jq -r '.[].runId' "$seed_runs_file")"
required_observation_count="$(jq 'length' "$required_observations_file")"
max_candidate_runs="${BASELINE_MAX_CANDIDATE_RUNS:-${BASELINE_MAX_RUNS:-5}}"
if ! [[ "$max_candidate_runs" =~ ^[0-9]+$ ]] || [ "$max_candidate_runs" -lt 1 ]; then
max_candidate_runs=1
fi
candidate_runs="$(
"$GH_BIN" run list \
--repo "$repo" \
--workflow "$workflow" \
--branch "$branch" \
--event push \
--status success \
--json databaseId,headSha \
--limit "$max_candidate_runs" \
--jq '[.[] | select(.headSha != env.GITHUB_SHA) | .databaseId] | .[]'
)"
candidate_runs="$seed_run_ids
$candidate_runs"
max_runs="${BASELINE_MAX_RUNS:-5}"
if ! [[ "$max_runs" =~ ^[0-9]+$ ]] || [ "$max_runs" -lt 1 ]; then
max_runs=1
fi
download_timeout_seconds="${BASELINE_DOWNLOAD_TIMEOUT_SECONDS:-120}"
if ! [[ "$download_timeout_seconds" =~ ^[0-9]+$ ]] || [ "$download_timeout_seconds" -lt 1 ]; then
download_timeout_seconds=120
fi
write_baseline_observation_counts() {
local measurement_index="$BASELINE_OUTPUT_DIR/baseline-measurement-files.txt"
local counts_file="$BASELINE_OUTPUT_DIR/baseline-observation-counts.json"
find "$BASELINE_OUTPUT_DIR" \
-mindepth 2 \
-maxdepth 2 \
-name measurements.json \
-type f \
-print \
| sort >"$measurement_index" || true
if [ -s "$measurement_index" ]; then
xargs -r jq -s \
--slurpfile required "$required_observations_file" \
'
([.[] | (.observations // [])[]? | select(.value | type == "number") | .id] | sort | group_by(.) | map({id: .[0], sources: length})) as $counts
| ($required[0] // []) as $requiredRows
| {
counts: $counts,
required: (
$requiredRows
| map(. as $requiredRow | ($counts | map(select(.id == $requiredRow.id)) | .[0].sources // 0) as $actual | $requiredRow + {sources:$actual, satisfied:($actual >= $requiredRow.minSources)})
)
}
' <"$measurement_index" >"$counts_file"
else
jq -n --slurpfile required "$required_observations_file" \
'{counts: [], required: (($required[0] // []) | map(. + {sources:0, satisfied:false}))}' >"$counts_file"
fi
}
baseline_requirements_satisfied() {
if [ "$required_observation_count" -eq 0 ]; then
return 1
fi
write_baseline_observation_counts
jq -e '.required | all(.satisfied == true)' "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" >/dev/null
}
download_artifact_archive() {
local artifact_id="$1"
local output_dir="$2"
local archive_file="$output_dir/artifact.zip"
local artifact_url="https://api.github.com/repos/$repo/actions/artifacts/$artifact_id/zip"
local curl_args=(
--fail
--location
--silent
--show-error
--connect-timeout 15
--max-time "$download_timeout_seconds"
--retry 2
--retry-delay 2
--retry-max-time "$download_timeout_seconds"
--header "Accept: application/vnd.github+json"
--header "X-GitHub-Api-Version: 2022-11-28"
--output "$archive_file"
)
if [ -n "${GH_TOKEN:-}" ]; then
curl_args+=(--header "Authorization: Bearer ${GH_TOKEN}")
fi
"$CURL_BIN" "${curl_args[@]}" "$artifact_url"
"$UNZIP_BIN" -q -o "$archive_file" -d "$output_dir"
rm -f "$archive_file"
}
run_id=""
artifact_name=""
artifact_id=""
downloaded_runs_file="$BASELINE_OUTPUT_DIR/baseline-runs.jsonl"
seen_runs_file="$BASELINE_OUTPUT_DIR/baseline-seen-runs.txt"
: >"$downloaded_runs_file"
: >"$seen_runs_file"
for candidate_run in $candidate_runs; do
if [ -z "$candidate_run" ]; then
continue
fi
if grep -qxF "$candidate_run" "$seen_runs_file"; then
continue
fi
downloaded_count="$(wc -l <"$downloaded_runs_file" | tr -d ' ')"
if [ "$downloaded_count" -ge "$max_runs" ]; then
if baseline_requirements_satisfied; then
break
fi
echo "::notice::downloaded $downloaded_count baseline artifact(s), but required observation counts are not satisfied yet; continuing through bounded candidate history"
fi
if [ "$(wc -l <"$seen_runs_file" | tr -d ' ')" -ge "$max_candidate_runs" ]; then
break
fi
printf '%s\n' "$candidate_run" >>"$seen_runs_file"
artifact_json="$(
"$GH_BIN" api "repos/$repo/actions/runs/$candidate_run/artifacts" \
| jq --arg artifactName "$BASELINE_ARTIFACT_NAME" '.artifacts
| map(select(.expired == false))
| map(select(.name == $artifactName or (.name | startswith($artifactName + "-"))))
| sort_by(.created_at // "")
| reverse
| .[0] // empty'
)"
if [ -n "$artifact_json" ]; then
current_artifact_name="$(printf '%s' "$artifact_json" | jq -r '.name')"
current_artifact_id="$(printf '%s' "$artifact_json" | jq -r '.id')"
current_output_dir="$BASELINE_OUTPUT_DIR/run-$candidate_run"
mkdir -p "$current_output_dir"
if download_artifact_archive "$current_artifact_id" "$current_output_dir"; then
if [ -z "$run_id" ]; then
run_id="$candidate_run"
artifact_name="$current_artifact_name"
artifact_id="$current_artifact_id"
fi
jq -cn \
--arg runId "$candidate_run" \
--arg artifactName "$current_artifact_name" \
--arg artifactId "$current_artifact_id" \
--arg path "run-$candidate_run" \
'{runId:$runId, artifactName:$artifactName, artifactId:$artifactId, path:$path}' \
>>"$downloaded_runs_file"
else
status="$?"
rm -rf "$current_output_dir"
echo "::notice::failed or timed out after ${download_timeout_seconds}s downloading baseline artifact $current_artifact_name from run $candidate_run (exit $status); skipping candidate"
fi
fi
done
write_baseline_observation_counts
if [ -z "$run_id" ] || [ -z "$artifact_name" ]; then
echo "::notice::no successful baseline run found for $repo workflow=$workflow branch=$branch"
exit 0
fi
jq -n \
--slurpfile runs "$downloaded_runs_file" \
--slurpfile seedRuns "$seed_runs_file" \
--slurpfile observationCounts "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" \
--argjson schemaVersion 1 \
--arg repository "$repo" \
--arg workflow "$workflow" \
--arg branch "$branch" \
--arg runId "$run_id" \
--arg artifactName "$artifact_name" \
--arg artifactId "$artifact_id" \
'{
schemaVersion: $schemaVersion,
source: "github-actions-artifact",
repository: $repository,
workflow: $workflow,
branch: $branch,
runId: $runId,
artifactName: $artifactName,
artifactId: $artifactId,
seedRuns: ($seedRuns[0] // []),
runs: $runs,
observationCounts: ($observationCounts[0] // null)
}' >"$BASELINE_OUTPUT_DIR/baseline-provenance.json"
echo "Downloaded $(wc -l <"$downloaded_runs_file" | tr -d ' ') baseline artifact(s), latest $artifact_name from run $run_id into $BASELINE_OUTPUT_DIR"
- name: 'Download previous artifact: source-shape'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BASELINE_ARTIFACT_NAME: source-shape
BASELINE_OUTPUT_DIR: tmp/ci-measurement-report/baseline/source-shape
BASELINE_WORKFLOW_NAME: ${{ github.workflow }}
BASELINE_BRANCH: ${{ github.base_ref || github.ref_name }}
BASELINE_SEED_RUNS_JSON: '[{"runId":"26085158592","label":"main baseline","sha":"ce7cf8f8ebfaa1da6c7e9122cd195a5f95ce2fca","source":"manual-backfill","artifacts":["source-shape"],"notes":"Backfilled with the current measurement workflow for the effect-utils #658 rollout."}]'
BASELINE_MAX_RUNS: '20'
BASELINE_MAX_CANDIDATE_RUNS: '60'
BASELINE_REQUIRED_OBSERVATIONS_JSON: '[]'
BASELINE_DOWNLOAD_TIMEOUT_SECONDS: '120'
run: |
set -euo pipefail
mkdir -p "$BASELINE_OUTPUT_DIR"
if command -v gh >/dev/null 2>&1; then
GH_BIN="$(command -v gh)"
else
echo "::notice::gh is not on PATH; resolving GitHub CLI through Nix"
if ! GH_BIN="$(nix build --no-link --print-out-paths nixpkgs#gh 2>/dev/null)/bin/gh"; then
echo "::notice::unable to resolve GitHub CLI through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using GitHub CLI: $GH_BIN"
CURL_BIN="$(command -v curl || true)"
if [ -z "$CURL_BIN" ]; then
echo "::notice::curl is not on PATH; resolving curl through Nix"
if ! CURL_BIN="$(nix build --no-link --print-out-paths nixpkgs#curl 2>/dev/null)/bin/curl"; then
echo "::notice::unable to resolve curl through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using curl: $CURL_BIN"
UNZIP_BIN="$(command -v unzip || true)"
if [ -z "$UNZIP_BIN" ]; then
echo "::notice::unzip is not on PATH; resolving unzip through Nix"
if ! UNZIP_BIN="$(nix build --no-link --print-out-paths nixpkgs#unzip 2>/dev/null)/bin/unzip"; then
echo "::notice::unable to resolve unzip through Nix; skipping previous artifact download"
exit 0
fi
fi
echo "Using unzip: $UNZIP_BIN"
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}"
workflow="${BASELINE_WORKFLOW_NAME:-CI}"
branch="${BASELINE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF_NAME:-main}}}"
seed_runs_file="$BASELINE_OUTPUT_DIR/baseline-seed-runs.json"
required_observations_file="$BASELINE_OUTPUT_DIR/baseline-required-observations.json"
printf '%s' "${BASELINE_SEED_RUNS_JSON:-[]}" >"$seed_runs_file"
printf '%s' "${BASELINE_REQUIRED_OBSERVATIONS_JSON:-[]}" >"$required_observations_file"
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.runId | type == "string")) else false end' \
"$seed_runs_file" >/dev/null; then
echo "::error::BASELINE_SEED_RUNS_JSON must be an array of objects with string runId fields"
exit 1
fi
if ! jq -e 'if type == "array" then all(.[]; type == "object" and (.id | type == "string") and (.minSources | type == "number")) else false end' \
"$required_observations_file" >/dev/null; then
echo "::error::BASELINE_REQUIRED_OBSERVATIONS_JSON must be an array of objects with string id and numeric minSources fields"
exit 1
fi
seed_run_ids="$(jq -r '.[].runId' "$seed_runs_file")"
required_observation_count="$(jq 'length' "$required_observations_file")"
max_candidate_runs="${BASELINE_MAX_CANDIDATE_RUNS:-${BASELINE_MAX_RUNS:-5}}"
if ! [[ "$max_candidate_runs" =~ ^[0-9]+$ ]] || [ "$max_candidate_runs" -lt 1 ]; then
max_candidate_runs=1
fi
candidate_runs="$(
"$GH_BIN" run list \
--repo "$repo" \
--workflow "$workflow" \
--branch "$branch" \
--event push \
--status success \
--json databaseId,headSha \
--limit "$max_candidate_runs" \
--jq '[.[] | select(.headSha != env.GITHUB_SHA) | .databaseId] | .[]'
)"
candidate_runs="$seed_run_ids
$candidate_runs"
max_runs="${BASELINE_MAX_RUNS:-5}"
if ! [[ "$max_runs" =~ ^[0-9]+$ ]] || [ "$max_runs" -lt 1 ]; then
max_runs=1
fi
download_timeout_seconds="${BASELINE_DOWNLOAD_TIMEOUT_SECONDS:-120}"
if ! [[ "$download_timeout_seconds" =~ ^[0-9]+$ ]] || [ "$download_timeout_seconds" -lt 1 ]; then
download_timeout_seconds=120
fi
write_baseline_observation_counts() {
local measurement_index="$BASELINE_OUTPUT_DIR/baseline-measurement-files.txt"
local counts_file="$BASELINE_OUTPUT_DIR/baseline-observation-counts.json"
find "$BASELINE_OUTPUT_DIR" \
-mindepth 2 \
-maxdepth 2 \
-name measurements.json \
-type f \
-print \
| sort >"$measurement_index" || true
if [ -s "$measurement_index" ]; then
xargs -r jq -s \
--slurpfile required "$required_observations_file" \
'
([.[] | (.observations // [])[]? | select(.value | type == "number") | .id] | sort | group_by(.) | map({id: .[0], sources: length})) as $counts
| ($required[0] // []) as $requiredRows
| {
counts: $counts,
required: (
$requiredRows
| map(. as $requiredRow | ($counts | map(select(.id == $requiredRow.id)) | .[0].sources // 0) as $actual | $requiredRow + {sources:$actual, satisfied:($actual >= $requiredRow.minSources)})
)
}
' <"$measurement_index" >"$counts_file"
else
jq -n --slurpfile required "$required_observations_file" \
'{counts: [], required: (($required[0] // []) | map(. + {sources:0, satisfied:false}))}' >"$counts_file"
fi
}
baseline_requirements_satisfied() {
if [ "$required_observation_count" -eq 0 ]; then
return 1
fi
write_baseline_observation_counts
jq -e '.required | all(.satisfied == true)' "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" >/dev/null
}
download_artifact_archive() {
local artifact_id="$1"
local output_dir="$2"
local archive_file="$output_dir/artifact.zip"
local artifact_url="https://api.github.com/repos/$repo/actions/artifacts/$artifact_id/zip"
local curl_args=(
--fail
--location
--silent
--show-error
--connect-timeout 15
--max-time "$download_timeout_seconds"
--retry 2
--retry-delay 2
--retry-max-time "$download_timeout_seconds"
--header "Accept: application/vnd.github+json"
--header "X-GitHub-Api-Version: 2022-11-28"
--output "$archive_file"
)
if [ -n "${GH_TOKEN:-}" ]; then
curl_args+=(--header "Authorization: Bearer ${GH_TOKEN}")
fi
"$CURL_BIN" "${curl_args[@]}" "$artifact_url"
"$UNZIP_BIN" -q -o "$archive_file" -d "$output_dir"
rm -f "$archive_file"
}
run_id=""
artifact_name=""
artifact_id=""
downloaded_runs_file="$BASELINE_OUTPUT_DIR/baseline-runs.jsonl"
seen_runs_file="$BASELINE_OUTPUT_DIR/baseline-seen-runs.txt"
: >"$downloaded_runs_file"
: >"$seen_runs_file"
for candidate_run in $candidate_runs; do
if [ -z "$candidate_run" ]; then
continue
fi
if grep -qxF "$candidate_run" "$seen_runs_file"; then
continue
fi
downloaded_count="$(wc -l <"$downloaded_runs_file" | tr -d ' ')"
if [ "$downloaded_count" -ge "$max_runs" ]; then
if baseline_requirements_satisfied; then
break
fi
echo "::notice::downloaded $downloaded_count baseline artifact(s), but required observation counts are not satisfied yet; continuing through bounded candidate history"
fi
if [ "$(wc -l <"$seen_runs_file" | tr -d ' ')" -ge "$max_candidate_runs" ]; then
break
fi
printf '%s\n' "$candidate_run" >>"$seen_runs_file"
artifact_json="$(
"$GH_BIN" api "repos/$repo/actions/runs/$candidate_run/artifacts" \
| jq --arg artifactName "$BASELINE_ARTIFACT_NAME" '.artifacts
| map(select(.expired == false))
| map(select(.name == $artifactName or (.name | startswith($artifactName + "-"))))
| sort_by(.created_at // "")
| reverse
| .[0] // empty'
)"
if [ -n "$artifact_json" ]; then
current_artifact_name="$(printf '%s' "$artifact_json" | jq -r '.name')"
current_artifact_id="$(printf '%s' "$artifact_json" | jq -r '.id')"
current_output_dir="$BASELINE_OUTPUT_DIR/run-$candidate_run"
mkdir -p "$current_output_dir"
if download_artifact_archive "$current_artifact_id" "$current_output_dir"; then
if [ -z "$run_id" ]; then
run_id="$candidate_run"
artifact_name="$current_artifact_name"
artifact_id="$current_artifact_id"
fi
jq -cn \
--arg runId "$candidate_run" \
--arg artifactName "$current_artifact_name" \
--arg artifactId "$current_artifact_id" \
--arg path "run-$candidate_run" \
'{runId:$runId, artifactName:$artifactName, artifactId:$artifactId, path:$path}' \
>>"$downloaded_runs_file"
else
status="$?"
rm -rf "$current_output_dir"
echo "::notice::failed or timed out after ${download_timeout_seconds}s downloading baseline artifact $current_artifact_name from run $candidate_run (exit $status); skipping candidate"
fi
fi
done
write_baseline_observation_counts
if [ -z "$run_id" ] || [ -z "$artifact_name" ]; then
echo "::notice::no successful baseline run found for $repo workflow=$workflow branch=$branch"
exit 0
fi
jq -n \
--slurpfile runs "$downloaded_runs_file" \
--slurpfile seedRuns "$seed_runs_file" \
--slurpfile observationCounts "$BASELINE_OUTPUT_DIR/baseline-observation-counts.json" \
--argjson schemaVersion 1 \
--arg repository "$repo" \
--arg workflow "$workflow" \
--arg branch "$branch" \
--arg runId "$run_id" \
--arg artifactName "$artifact_name" \
--arg artifactId "$artifact_id" \
'{
schemaVersion: $schemaVersion,
source: "github-actions-artifact",
repository: $repository,
workflow: $workflow,
branch: $branch,
runId: $runId,
artifactName: $artifactName,
artifactId: $artifactId,
seedRuns: ($seedRuns[0] // []),
runs: $runs,
observationCounts: ($observationCounts[0] // null)
}' >"$BASELINE_OUTPUT_DIR/baseline-provenance.json"
echo "Downloaded $(wc -l <"$downloaded_runs_file" | tr -d ' ') baseline artifact(s), latest $artifact_name from run $run_id into $BASELINE_OUTPUT_DIR"
- name: Compare CI measurements with baseline
shell: bash
env:
CI_MEASUREMENT_CURRENT_DIR: tmp/ci-measurement-report/current
CI_MEASUREMENT_BASELINE_DIR: tmp/ci-measurement-report/baseline
CI_MEASUREMENT_COMPARISON_FILE: tmp/ci-measurement-report/measurement-comparison.json
CI_MEASUREMENT_REGRESSION_MODE: warn
CI_MEASUREMENT_PR_COMMENT_ENABLED: 'true'
CI_MEASUREMENT_PR_COMMENT_TITLE: CI Measurements
CI_MEASUREMENT_PR_COMMENT_MAX_ROWS: '16'
CI_MEASUREMENT_PR_COMMENT_MAX_HISTORY: '20'
CI_MEASUREMENT_PR_COMMENT_ASSET_BRANCH: ci-measurement-assets
CI_MEASUREMENT_IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && '1' || '' }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
export PATH="/run/current-system/sw/bin:/usr/bin:/bin:$PATH"
current_dir="${CI_MEASUREMENT_CURRENT_DIR:?CI_MEASUREMENT_CURRENT_DIR not set}"
baseline_dir="${CI_MEASUREMENT_BASELINE_DIR:?CI_MEASUREMENT_BASELINE_DIR not set}"
comparison_file="${CI_MEASUREMENT_COMPARISON_FILE:?CI_MEASUREMENT_COMPARISON_FILE not set}"
mode="${CI_MEASUREMENT_REGRESSION_MODE:-warn}"
mkdir -p "$(dirname "$comparison_file")"
if [ "$mode" = "off" ]; then
jq -n --argjson schemaVersion 1 --arg status skipped --arg mode "$mode" \
'{schemaVersion:$schemaVersion,status:$status,mode:$mode,comparisons:{}}' \
>"$comparison_file"
exit 0
fi
current_index="$(mktemp)"
baseline_index="$(mktemp)"
find "$current_dir" -name baseline -type d -prune -o -name measurements.json -type f -print | sort >"$current_index" || true
{
find "$baseline_dir" -name baseline -type d ! -path "$baseline_dir" -prune -o -name measurements.json -type f -print
} | sort -u >"$baseline_index" || true
if [ ! -s "$current_index" ]; then
echo "::error::no current measurements.json files found under $current_dir"
exit 1
fi
current_json="$comparison_file.current.json"
baseline_json="$comparison_file.baseline.json"
xargs -r jq -s '.' <"$current_index" >"$current_json"
if [ -s "$baseline_index" ]; then
xargs -r jq -s '.' <"$baseline_index" >"$baseline_json"
else
printf '[]\n' >"$baseline_json"
fi
jq -n \
--slurpfile current "$current_json" \
--slurpfile baseline "$baseline_json" \
--argjson schemaVersion 1 \
--arg mode "$mode" \
--arg currentDir "$current_dir" \
--arg baselineDir "$baseline_dir" \
'
def identity_dimensions:
(.dimensions // {})
| to_entries
| map(select(.key as $key | ["devenvRev", "otelServiceName", "status", "probeLabel", "sampleCount", "measuredSampleCount"] | index($key) | not))
| sort_by(.key)
| map("\(.key)=\(.value|tostring)")
| join(",");
def observation_key($doc):
[
($doc.target.kind // "unknown"),
($doc.target.id // $doc.target.name // "unknown"),
($doc.target.system // "unknown"),
(.id // .name // "unknown"),
(.unit // "unknown"),
identity_dimensions
] | join("|");
def median:
sort as $sorted
| ($sorted | length) as $count
| if $count == 0 then null
elif ($count % 2) == 1 then $sorted[($count / 2 | floor)]
else (($sorted[($count / 2 - 1)] + $sorted[($count / 2)]) / 2)
end;
def percentile($p):
sort as $sorted
| ($sorted | length) as $count
| if $count == 0 then null
else $sorted[(($p * ($count - 1)) | floor)]
end;
def abs_value: if . < 0 then -. else . end;
def observations_by_key($docs):
reduce $docs[]? as $doc
({};
reduce (($doc.observations // [])[]? | select(.value | type == "number")) as $obs
(.;
($obs | observation_key($doc)) as $key
| .[$key] = ((.[$key] // []) + [{
target: $doc.target,
observation: $obs,
generatedAt: $doc.generatedAt
}])
)
);
def observation_stats($items):
($items | map(.observation.value)) as $values
| ($items | map(.observation.comparison.baseline // empty)) as $pairedBaselineValues
| ($items | map(.observation.statistics.pairedDeltaMedian // empty)) as $pairedDeltaMedianValues
| ($items | map(.observation.statistics.pairedDeltaP25 // empty)) as $pairedDeltaP25Values
| ($items | map(.observation.statistics.pairedDeltaP75 // empty)) as $pairedDeltaP75Values
| ($items | map(.observation.statistics.pairedDeltaMad // empty)) as $pairedDeltaMadValues
| ($items | map(.observation.statistics.pairedDeltaSamples // []) | add // []) as $pairedDeltaSampleValues
| ($items | map(.observation.statistics.measuredSampleCount // .observation.statistics.sampleCount // 1) | add // ($items | length)) as $sampleCount
| ($values | median) as $median
| {
target: ($items[0].target // {}),
observation: ($items[-1].observation // {}),
measurementKind: ($items[-1].observation.measurementKind // null),
value: $median,
min: ($values | min),
max: ($values | max),
p25: ($values | percentile(0.25)),
p75: ($values | percentile(0.75)),
p95: ($values | percentile(0.95)),
mad: ($values | map(. - $median | if . < 0 then -. else . end) | median),
sourceCount: ($items | length),
sampleCount: $sampleCount,
pairedSampleCount: ($items | map(.observation.statistics.pairedSampleCount // .observation.comparison.pairedSampleCount // 0) | add // 0),
pairedBaselineValue: (if ($pairedBaselineValues | length) == 0 then null else ($pairedBaselineValues | median) end),
pairedDeltaMedianValue: (if ($pairedDeltaMedianValues | length) == 0 then null else ($pairedDeltaMedianValues | median) end),
pairedDeltaP25Value: (if ($pairedDeltaP25Values | length) == 0 then null else ($pairedDeltaP25Values | median) end),
pairedDeltaP75Value: (if ($pairedDeltaP75Values | length) == 0 then null else ($pairedDeltaP75Values | median) end),
pairedDeltaMadValue: (if ($pairedDeltaMadValues | length) == 0 then null else ($pairedDeltaMadValues | median) end),
pairedDeltaSampleValues: $pairedDeltaSampleValues,
generatedAt: ($items[-1].generatedAt // null)
};
def budget($metric; $unit):
if $metric == "nix.closure.nar_size" then
{warnRatio:1.05, failRatio:1.10, warnAbs:52428800, failAbs:209715200, statisticalToleranceRatio:0.02, statisticalToleranceAbs:10485760}
elif $metric == "nix.closure.bucket.nar_size" then
{warnRatio:1.10, failRatio:1.20, warnAbs:52428800, failAbs:209715200, statisticalToleranceRatio:0.05, statisticalToleranceAbs:10485760}
elif $metric == "nix.closure.path_count" then
{warnRatio:1.05, failRatio:1.10, warnAbs:100, failAbs:500, statisticalToleranceRatio:0.02, statisticalToleranceAbs:10}
elif $unit == "seconds" then
{warnRatio:1.10, failRatio:1.20, warnAbs:0.25, failAbs:1, statisticalToleranceRatio:0.10, statisticalToleranceAbs:0.25}
else
{warnRatio:1.25, failRatio:1.50, warnAbs:1, failAbs:3, statisticalToleranceRatio:0.10, statisticalToleranceAbs:1}
end;
def noise_floor($metric; $unit):
if $metric == "nix.closure.nar_size" or $metric == "nix.closure.bucket.nar_size" then 10485760
elif $metric == "nix.closure.path_count" then 10
elif $unit == "seconds" then 0.1
else 0
end;
def default_policy($metric; $unit):
budget($metric; $unit) as $b
| noise_floor($metric; $unit) as $noise
| $b + {
enabled:true,
comparisonMode:(if $metric == "nix.closure.nar_size" or $metric == "nix.closure.bucket.nar_size" or $metric == "nix.closure.path_count" or $unit != "seconds" then "budget" else "historical" end),
minBaselineSources:(if $metric == "nix.closure.nar_size" or $metric == "nix.closure.bucket.nar_size" or $metric == "nix.closure.path_count" or $unit != "seconds" then 1 else 10 end),
minCurrentSamples:(if $unit == "seconds" then 3 else 1 end),
minPairedSamples:(if $unit == "seconds" then 5 else 0 end),
noiseFloor:$noise
};
def observation_policy($obs):
default_policy($obs.name // "unknown"; $obs.unit // "unknown") + ($obs.policy // {});
def policy_enabled($policy):
if ($policy | has("enabled")) then $policy.enabled else true end;
def classify($metric; $unit; $measurementKind; $policy; $current; $currentP25; $currentP75; $currentMad; $baseline; $baselineMin; $baselineMax; $baselineP25; $baselineP75; $baselineP95; $baselineMad; $currentSamples; $baselineSources; $pairedSamples; $pairedDeltaMedian; $pairedDeltaP25; $pairedDeltaP75; $pairedDeltaMad; $pairedDeltaValues):
$policy as $b
| ($policy.comparisonMode // (if $measurementKind == "deterministic" or $unit != "seconds" then "budget" elif $measurementKind == "diagnostic" then "diagnostic" else "historical" end)) as $comparisonMode
| ($policy.noiseFloor // noise_floor($metric; $unit)) as $noise
| ($current - $baseline) as $delta
| (if $comparisonMode == "paired" and $pairedDeltaMedian != null then $pairedDeltaMedian else $delta end) as $evidenceDelta
| (($policy.pairedEvidenceQuantile // 0.25) | tonumber) as $pairedEvidenceQuantile
| (if $baseline > 0 then ($current / $baseline) else null end) as $ratio
| (($baselineP75 // $baseline) - ($baselineP25 // $baseline)) as $iqr
| (($currentP75 // $current) - ($currentP25 // $current)) as $currentIqr
| (($pairedDeltaP75 // $evidenceDelta) - ($pairedDeltaP25 // $evidenceDelta)) as $pairedDeltaIqr
| ([
$noise,
(($policy.statisticalToleranceAbs // 0) | tonumber),
(if $baseline > 0 then ($baseline * (($policy.statisticalToleranceRatio // 0) | tonumber)) else 0 end),
(($baselineMad // 0) * 3),
(($iqr // 0) * 1.5)
] | max) as $robustTolerance
| (if $currentSamples > 1 then ([
$noise,
(($policy.statisticalToleranceAbs // 0) | tonumber),
(if $current > 0 then ($current * (($policy.statisticalToleranceRatio // 0) | tonumber)) else 0 end),
(($currentMad // 0) * 3),
(($currentIqr // 0) * 1.5)
] | max) else 0 end) as $currentRobustTolerance
| ([
$noise,
(($policy.statisticalToleranceAbs // 0) | tonumber),
(if $baseline > 0 then ($baseline * (($policy.statisticalToleranceRatio // 0) | tonumber)) else 0 end),
(($pairedDeltaMad // 0) * 3),
(($pairedDeltaIqr // 0) * 1.5)
] | max) as $pairedDeltaTolerance
| ($baseline + $robustTolerance) as $robustUpper
| ($baseline - $robustTolerance) as $robustLower
| ($current + $currentRobustTolerance) as $currentRobustUpper
| ($current - $currentRobustTolerance) as $currentRobustLower
| (if $comparisonMode == "paired" and ($pairedDeltaValues | length) > 0 then ($pairedDeltaValues | percentile($pairedEvidenceQuantile)) else ($evidenceDelta - $pairedDeltaTolerance) end) as $evidenceDeltaLower
| (if $comparisonMode == "paired" and ($pairedDeltaValues | length) > 0 then ($pairedDeltaValues | percentile(1 - $pairedEvidenceQuantile)) else ($evidenceDelta + $pairedDeltaTolerance) end) as $evidenceDeltaUpper
| ([($b.warnAbs // 0), (if $baseline > 0 then ($baseline * (($b.warnRatio // 1) - 1)) else 0 end), $noise, 0.000000001] | max) as $warnBudget
| ([($b.failAbs // 0), (if $baseline > 0 then ($baseline * (($b.failRatio // 1) - 1)) else 0 end), $noise, 0.000000001] | max) as $failBudget
| ($comparisonMode != "paired") as $needsHistoricalBaselineCount
| (
($current >= $robustLower and $current <= $robustUpper)
or ($currentRobustTolerance > 0 and $currentRobustLower <= $robustUpper and $currentRobustUpper >= $robustLower)
) as $withinRobustBand
| ($comparisonMode == "historical" and $measurementKind != "deterministic") as $canUseRobustBandSuppression
| (
$baselineMin != null
and $baselineMax != null
and $current >= $baselineMin
and $current <= $baselineMax
) as $withinBaselineRange
| (
if $baseline <= 0 then "unknown"
elif $comparisonMode == "paired" and $evidenceDeltaLower > $failBudget then "fail"
elif $comparisonMode == "paired" and $evidenceDeltaLower > $warnBudget then "warn"
elif $comparisonMode == "paired" then "pass"
elif ($delta > $b.failAbs and $current > ($baseline * $b.failRatio)) then "fail"
elif ($delta > $b.warnAbs and $current > ($baseline * $b.warnRatio)) then "warn"
else "pass"
end
) as $thresholdStatus
| (
policy_enabled($policy) == true
and $baseline > 0
and (if $needsHistoricalBaselineCount then $baselineSources >= ($policy.minBaselineSources // 1) else true end)
and $currentSamples >= ($policy.minCurrentSamples // 1)
and (if $comparisonMode == "paired" then $pairedSamples >= ($policy.minPairedSamples // 1) else true end)
and (if $comparisonMode == "paired" then $pairedDeltaMedian != null else true end)
) as $gateable
| (
if (policy_enabled($policy) != true) then "disabled"
elif $baseline <= 0 then "missing_baseline"
elif $needsHistoricalBaselineCount and $baselineSources < ($policy.minBaselineSources // 1) then "low_baseline_count"
elif $currentSamples < ($policy.minCurrentSamples // 1) then "low_current_sample_count"
elif $comparisonMode == "paired" and $pairedSamples < ($policy.minPairedSamples // 1) then "low_paired_sample_count"
elif $comparisonMode == "paired" and $pairedDeltaMedian == null then "missing_paired_delta"
else "eligible"
end
) as $gateReason
| (
if $baseline <= 0 then "unknown"
elif (policy_enabled($policy) != true) then "diagnostic"
elif ($delta | abs_value) <= $noise then "noise_floor"
elif $needsHistoricalBaselineCount and $baselineSources < ($policy.minBaselineSources // 1) then "low_baseline_count"
elif $currentSamples < ($policy.minCurrentSamples // 1) then "low_current_sample_count"
elif $comparisonMode == "paired" and $pairedSamples < ($policy.minPairedSamples // 1) then "low_paired_sample_count"
elif $comparisonMode == "paired" and $pairedDeltaMedian == null then "missing_paired_delta"
elif $comparisonMode == "paired" and $thresholdStatus == "pass" and $evidenceDelta > $warnBudget then "paired_uncertain"
elif ($canUseRobustBandSuppression and $thresholdStatus != "pass" and $withinRobustBand) then "within_robust_band"
elif $thresholdStatus == "pass" then "within_budget"
else "threshold_exceeded"
end
) as $confidence
| (
if ($gateable and $confidence == "threshold_exceeded") then $thresholdStatus
elif $thresholdStatus == "unknown" then "unknown"
else "pass"
end
) as $status
| (
if $baseline <= 0 then "unknown"
elif $comparisonMode == "paired" and ($evidenceDelta | abs_value) <= $noise then "unchanged"
elif $comparisonMode == "paired" and $evidenceDeltaLower <= 0 and $evidenceDeltaUpper >= 0 then "unchanged"
elif $comparisonMode == "paired" and $evidenceDelta < 0 then "improved"
elif $comparisonMode == "paired" then "regressed"
elif ($delta | abs_value) <= $noise then "unchanged"
elif $canUseRobustBandSuppression and $withinRobustBand then "unchanged"
elif $delta < 0 then "improved"
else "regressed"
end
) as $direction
| (
if $baseline <= 0 then null
elif (policy_enabled($policy) != true) then null
elif $comparisonMode == "paired" and ($evidenceDeltaLower <= 0 and $evidenceDeltaUpper >= 0) then 0
elif $comparisonMode == "paired" and ($evidenceDelta | abs_value) <= $noise then 0
elif $comparisonMode == "paired" and $evidenceDelta > 0 then ([0, $evidenceDeltaLower] | max) / $warnBudget
elif $comparisonMode == "paired" then -(([0, (-$evidenceDeltaUpper)] | max) / $warnBudget)
elif $canUseRobustBandSuppression and $withinRobustBand then 0
elif ($delta | abs_value) <= $noise then 0
elif ($confidence == "threshold_exceeded" and $delta > 0) then ([0, ($currentRobustLower - $robustUpper), $delta] | max) / $warnBudget
elif ($confidence == "threshold_exceeded" and $delta < 0) then -(([0, ($robustLower - $currentRobustUpper), (-$delta)] | max) / $warnBudget)
elif $delta > 0 then ([0, ($currentRobustLower - $robustUpper)] | max) / $warnBudget
else -(([0, ($robustLower - $currentRobustUpper)] | max) / $warnBudget)
end
) as $semanticImpactScore
| (
if (policy_enabled($policy) != true) then "diagnostic"
elif $semanticImpactScore == null then "unknown"
elif $semanticImpactScore == 0 then "neutral"
elif $semanticImpactScore >= ($failBudget / $warnBudget) then "fail_boundary"
elif $semanticImpactScore >= 1 then "warn_boundary"
elif $semanticImpactScore > 0 then "below_warn_boundary"
else "improvement"
end
) as $semanticImpactKind
| {status:$status,current:$current,baseline:$baseline,delta:$delta,ratio:$ratio,budget:$b,gatePolicy:$policy,comparisonMode:$comparisonMode,gateable:$gateable,gateReason:$gateReason,confidence:$confidence,direction:$direction,semanticImpactScore:$semanticImpactScore,semanticImpactKind:$semanticImpactKind,semanticWarnBudget:$warnBudget,semanticFailBudget:$failBudget,baselineRobustLower:$robustLower,baselineRobustUpper:$robustUpper,baselineRobustTolerance:$robustTolerance,currentRobustLower:$currentRobustLower,currentRobustUpper:$currentRobustUpper,currentRobustTolerance:$currentRobustTolerance,withinBaselineRange:$withinBaselineRange,pairedSamples:$pairedSamples,evidenceDelta:$evidenceDelta,evidenceDeltaLower:$evidenceDeltaLower,evidenceDeltaUpper:$evidenceDeltaUpper,evidenceDeltaTolerance:$pairedDeltaTolerance,pairedEvidenceQuantile:$pairedEvidenceQuantile,pairedEvidenceProtocol:(if $comparisonMode == "paired" and ($pairedDeltaValues | length) > 0 then "paired-delta-quantile-v1" elif $comparisonMode == "paired" then "paired-summary-robust-band-v1" else null end)};
(observations_by_key($current[0]) | with_entries(.value = observation_stats(.value))) as $currentObs
| (observations_by_key($baseline[0]) | with_entries(.value = observation_stats(.value))) as $baselineObs
| (
$currentObs
| to_entries
| map(
.key as $key
| .value as $currentValue
| ($baselineObs[$key] // null) as $baselineValue
| ($currentValue.observation | observation_policy(.)) as $policy
| ($policy.comparisonMode // (if ($currentValue.observation.measurementKind // $currentValue.measurementKind) == "deterministic" or ($currentValue.observation.unit // "") != "seconds" then "budget" elif ($currentValue.observation.measurementKind // $currentValue.measurementKind) == "diagnostic" then "diagnostic" else "historical" end)) as $comparisonMode
| ($currentValue.pairedBaselineValue // null) as $pairedBaselineValue
| (if $comparisonMode == "paired" and $pairedBaselineValue != null then {
value: $pairedBaselineValue,
min: $pairedBaselineValue,
max: $pairedBaselineValue,
p25: $pairedBaselineValue,
p75: $pairedBaselineValue,
p95: $pairedBaselineValue,
mad: 0,
sourceCount: $currentValue.pairedSampleCount
} else $baselineValue end) as $effectiveBaselineValue
| {
key: $key,
value: (
if $effectiveBaselineValue == null then
{
status: "missing_baseline",
target: $currentValue.target,
observation: $currentValue.observation,
current: $currentValue.value,
currentSamples: $currentValue.sampleCount,
baselineSources: 0,
gatePolicy: $policy,
comparisonMode: $comparisonMode,
gateable: false,
gateReason: "missing_baseline",
confidence: "missing_baseline",
direction: "unknown"
}
else
classify(
$currentValue.observation.name;
$currentValue.observation.unit;
($currentValue.observation.measurementKind // $currentValue.measurementKind);
$policy;
$currentValue.value;
$currentValue.p25;
$currentValue.p75;
$currentValue.mad;
$effectiveBaselineValue.value;
$effectiveBaselineValue.min;
$effectiveBaselineValue.max;
$effectiveBaselineValue.p25;
$effectiveBaselineValue.p75;
$effectiveBaselineValue.p95;
$effectiveBaselineValue.mad;
$currentValue.sampleCount;
$effectiveBaselineValue.sourceCount;
$currentValue.pairedSampleCount;
$currentValue.pairedDeltaMedianValue;
$currentValue.pairedDeltaP25Value;
$currentValue.pairedDeltaP75Value;
$currentValue.pairedDeltaMadValue;
($currentValue.pairedDeltaSampleValues // [])
) + {
target: $currentValue.target,
observation: $currentValue.observation,
currentSamples: $currentValue.sampleCount,
baselineSources: $effectiveBaselineValue.sourceCount,
baselineMin: $effectiveBaselineValue.min,
baselineMax: $effectiveBaselineValue.max,
baselineP25: $effectiveBaselineValue.p25,
baselineP75: $effectiveBaselineValue.p75,
baselineP95: $effectiveBaselineValue.p95
,baselineMad: $effectiveBaselineValue.mad
}
end
)
}
)
| from_entries
) as $comparisons
| (
if any($comparisons[]?; .status == "fail") then "fail"
elif any($comparisons[]?; .status == "warn") then "warn"
elif any($comparisons[]?;
(if (.gatePolicy | has("enabled")) then .gatePolicy.enabled else true end)
and (.gateReason == "missing_baseline"
or .gateReason == "low_baseline_count"
or .gateReason == "low_current_sample_count"
or .gateReason == "low_paired_sample_count"
or .gateReason == "missing_paired_delta")
) then "partial"
else "pass"
end
) as $status
| (
[$comparisons[]?]
| {
enabledCount: (map(select((if (.gatePolicy | has("enabled")) then .gatePolicy.enabled else true end))) | length),
gateableCount: (map(select(.gateable == true)) | length),
missingBaselineCount: (map(select(.gateReason == "missing_baseline")) | length),
lowBaselineCount: (map(select(.gateReason == "low_baseline_count")) | length),
lowCurrentSampleCount: (map(select(.gateReason == "low_current_sample_count")) | length),
lowPairedSampleCount: (map(select(.gateReason == "low_paired_sample_count")) | length),
missingPairedDeltaCount: (map(select(.gateReason == "missing_paired_delta")) | length)
}
| . + {
nonGateableCount: (.enabledCount - .gateableCount),
enforceable: (.enabledCount == .gateableCount)
}
) as $readiness
| {
schemaVersion:$schemaVersion,
status:$status,
mode:$mode,
readiness:$readiness,
currentDir:$currentDir,
baselineDir:$baselineDir,
comparisons:$comparisons
}
' >"$comparison_file"
baseline_provenance_file="$baseline_dir/baseline-provenance.json"
if [ -f "$baseline_provenance_file" ]; then
comparison_with_provenance="$(mktemp)"
jq --slurpfile baselineProvenance "$baseline_provenance_file" \
'. + {baselineProvenance: ($baselineProvenance[0] // null)}' \
"$comparison_file" >"$comparison_with_provenance"
mv "$comparison_with_provenance" "$comparison_file"
fi
status="$(jq -r '.status' "$comparison_file")"
exit_code=0
case "$status:$mode" in
fail:fail)
echo "::error::CI measurement regression detected"
exit_code=1
;;
fail:*|warn:*)
echo "::warning::CI measurement regression threshold exceeded"
;;
partial:*)
echo "::notice::CI measurement comparison is partial because one or more enabled observations are not gateable"
;;
esac
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### ${CI_MEASUREMENT_PR_COMMENT_TITLE:-CI Measurements}"
echo ""
jq -r '"- Status: " + .status + "\n- Gate: " + (if .mode == "fail" then "enforced" elif .mode == "warn" then "advisory" elif .mode == "off" then "off" else (.mode // "unknown") end) + "\n- Baseline: " + .baselineDir' "$comparison_file"
echo ""
echo "| Status | Gate | Target | Observation | Current | Baseline | Delta | Ratio |"
echo "| --- | --- | --- | --- | ---: | ---: | ---: | ---: |"
jq -r '
.comparisons
| to_entries
| sort_by(
if .value.status == "fail" then 0
elif .value.status == "warn" then 1
elif .value.status == "missing_baseline" then 2
else 3
end
)
| .[:20]
| .[]
| .value as $v
| [
$v.status,
(if ($v.gateable // false) then "yes" else ($v.gateReason // "no") end),
(($v.target.kind // "unknown") + "/" + ($v.target.name // "unknown") + "/" + ($v.target.system // "unknown")),
($v.observation.name // "unknown"),
(($v.current // $v.observation.value // 0) | tostring),
(($v.baseline // "") | tostring),
(($v.delta // "") | tostring),
(if $v.ratio == null or $v.ratio == "" then "" else (($v.ratio * 100 | round / 100) | tostring) end)
]
| "| " + (map(gsub("\\|"; "\\\\|")) | join(" | ")) + " |"
' "$comparison_file"
} >>"$GITHUB_STEP_SUMMARY"
fi
if [ "${CI_MEASUREMENT_PR_COMMENT_ENABLED:-false}" = "true" ]; then
if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then
echo "::notice::CI measurement PR comments are produced only by pull_request workflows; skipping comment for event ${GITHUB_EVENT_NAME:-unknown}"
exit 0
fi
can_render_pr_comment=true
is_fork_pr=false
if [ "${CI_MEASUREMENT_IS_FORK_PR:-}" = "1" ]; then
echo "::notice::CI measurement PR comment skipped for fork pull request; summary and artifacts remain available"
can_render_pr_comment=false
is_fork_pr=true
fi
ensure_ci_measurement_tool() {
tool_name="$1"
nix_attr="$2"
if command -v "$tool_name" >/dev/null 2>&1; then
return 0
fi
if ! command -v nix >/dev/null 2>&1; then
return 1
fi
if tool_out="$(nix build --no-link --print-out-paths "nixpkgs#$nix_attr" 2>/dev/null)"; then
export PATH="$tool_out/bin:$PATH"
fi
command -v "$tool_name" >/dev/null 2>&1
}
if ! ensure_ci_measurement_tool gh gh; then
echo "::error::gh is not available; unable to publish required CI measurement PR comment"
can_render_pr_comment=false
fi
if ! ensure_ci_measurement_tool node nodejs; then
echo "::error::node is not available; unable to publish required CI measurement PR comment"
can_render_pr_comment=false
fi
if ! command -v jq >/dev/null 2>&1; then
if ensure_ci_measurement_tool jq jq; then
:
else
echo "::error::jq is not available; unable to publish required CI measurement PR comment"
can_render_pr_comment=false
fi
fi
if [ -z "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]; then
echo "::error::GH_TOKEN/GITHUB_TOKEN is not set; unable to publish required CI measurement PR comment"
can_render_pr_comment=false
fi
event_path="${GITHUB_EVENT_PATH:-}"
pr_number=""
if [ "$can_render_pr_comment" = "true" ] && [ -n "$event_path" ] && [ -f "$event_path" ]; then
pr_number="$(jq -r '.pull_request.number // empty' "$event_path")"
fi
if [ "$can_render_pr_comment" = "true" ] && [ -z "$pr_number" ]; then
echo "::error::pull request number is unavailable; unable to publish required CI measurement PR comment"
can_render_pr_comment=false
fi
if [ "$can_render_pr_comment" != "true" ]; then
if [ "$is_fork_pr" != "true" ]; then
exit 1
fi
fi
if [ "$can_render_pr_comment" = "true" ]; then
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}"
comment_tmp_dir="$(mktemp -d)"
comments_json="$comment_tmp_dir/comments.json"
comment_body="$comment_tmp_dir/comment.md"
comment_id_file="$comment_tmp_dir/comment-id.txt"
chart_file="$comment_tmp_dir/perf-change-vs-baseline.svg"
chart_dark_file="$comment_tmp_dir/perf-change-vs-baseline-dark.svg"
chart_png_file="$comment_tmp_dir/perf-change-vs-baseline.png"
chart_dark_png_file="$comment_tmp_dir/perf-change-vs-baseline-dark.png"
renderer_script="$comment_tmp_dir/render-ci-measurement-comment.mjs"
if ! gh api "repos/$repo/issues/$pr_number/comments" --paginate >"$comments_json"; then
echo "::notice::unable to list PR comments; skipping CI measurement PR comment"
can_render_pr_comment=false
fi
if [ "$can_render_pr_comment" = "true" ]; then
asset_branch="${CI_MEASUREMENT_PR_COMMENT_ASSET_BRANCH:-ci-measurement-assets}"
asset_title="$(printf '%s' "${CI_MEASUREMENT_PR_COMMENT_TITLE:-ci-measurements}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
if [ -z "$asset_title" ]; then
asset_title="ci-measurements"
fi
asset_head_sha="${CI_MEASUREMENT_SUBJECT_SHA:-${GITHUB_HEAD_SHA:-${GITHUB_SHA:-unknown}}}"
asset_run_id="${GITHUB_RUN_ID:-local}"
asset_run_attempt="${GITHUB_RUN_ATTEMPT:-0}"
asset_svg_path="ci-measurements/pr-$pr_number/${asset_head_sha}/run-${asset_run_id}-attempt-${asset_run_attempt}/${asset_title}.svg"
asset_png_path="ci-measurements/pr-$pr_number/${asset_head_sha}/run-${asset_run_id}-attempt-${asset_run_attempt}/${asset_title}.png"
asset_dark_png_path="ci-measurements/pr-$pr_number/${asset_head_sha}/run-${asset_run_id}-attempt-${asset_run_attempt}/${asset_title}-dark.png"
public_asset_command="${CI_MEASUREMENT_PR_COMMENT_PUBLIC_ASSET_COMMAND:-}"
repo_private="$(gh api "repos/$repo" --jq '.private // false' 2>/dev/null || printf 'true')"
require_public_asset=false
if [ "$repo_private" = "true" ]; then
require_public_asset=true
fi
if [ "${GITHUB_SERVER_URL:-https://github.com}" = "https://github.com" ]; then
github_raw_chart_url="https://raw.githubusercontent.com/$repo/$asset_branch/$asset_png_path"
github_raw_chart_dark_url="https://raw.githubusercontent.com/$repo/$asset_branch/$asset_dark_png_path"
github_raw_chart_source_url="https://raw.githubusercontent.com/$repo/$asset_branch/$asset_svg_path"
else
github_raw_chart_url="${GITHUB_SERVER_URL:-https://github.com}/$repo/raw/$asset_branch/$asset_png_path"
github_raw_chart_dark_url="${GITHUB_SERVER_URL:-https://github.com}/$repo/raw/$asset_branch/$asset_dark_png_path"
github_raw_chart_source_url="${GITHUB_SERVER_URL:-https://github.com}/$repo/raw/$asset_branch/$asset_svg_path"
fi
if [ "$repo_private" = "true" ]; then
chart_url=""
chart_dark_url=""
chart_source_url=""
else
chart_url="$github_raw_chart_url"
chart_dark_url="$github_raw_chart_dark_url"
chart_source_url="$github_raw_chart_source_url"
fi
export CI_MEASUREMENT_PR_COMMENT_CHART_URL="$chart_url"
export CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL="$chart_dark_url"
export CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL="$chart_source_url"
cat > "$renderer_script" <<'EOF'
import { readFileSync, writeFileSync } from 'node:fs'
const [comparisonPath, commentsPath, bodyPath, commentIdPath, chartPath, chartDarkPath] = process.argv.slice(2)
const title = process.env.CI_MEASUREMENT_PR_COMMENT_TITLE || 'CI Measurements'
const maxRows = Number.parseInt(process.env.CI_MEASUREMENT_PR_COMMENT_MAX_ROWS || '10', 10)
const maxHistory = Number.parseInt(process.env.CI_MEASUREMENT_PR_COMMENT_MAX_HISTORY || '20', 10)
const repo = process.env.GITHUB_REPOSITORY || 'unknown'
const runId = process.env.GITHUB_RUN_ID || ''
const runAttempt = process.env.GITHUB_RUN_ATTEMPT || ''
const sha = process.env.GITHUB_SHA || ''
const headSha = process.env.CI_MEASUREMENT_SUBJECT_SHA || process.env.GITHUB_HEAD_SHA || sha
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'
const workflow = process.env.GITHUB_WORKFLOW || 'CI'
const job = process.env.GITHUB_JOB || ''
const chartUrl = process.env.CI_MEASUREMENT_PR_COMMENT_CHART_URL || ''
const chartDarkUrl = process.env.CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL || ''
const chartSourceUrl = process.env.CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL || ''
const markerScope = (process.env.CI_MEASUREMENT_PR_COMMENT_MARKER || title)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'default'
const marker = '<!-- ci-measurement-comment:managed:' + markerScope + ' -->'
const legacyMarker = '<!-- ci-measurement-comment:managed -->'
const statePrefix = '<!-- ci-measurement-comment:state\n'
const stateSuffix = '\n-->'
const stateTag = 'ci-measurement-comment-state'
const schemaVersion = 1
const comparison = JSON.parse(readFileSync(comparisonPath, 'utf8'))
const comments = JSON.parse(readFileSync(commentsPath, 'utf8'))
if (!Array.isArray(comments)) throw new Error('comments response must be an array')
const existing = comments.find((comment) => {
if (typeof comment?.body !== 'string') return false
return comment.body.includes(marker) ||
(comment.body.includes(legacyMarker) && comment.body.includes('## ' + title))
})
const extractState = (body) => {
if (typeof body !== 'string') return undefined
const start = body.indexOf(statePrefix)
if (start === -1) return undefined
const end = body.indexOf(stateSuffix, start + statePrefix.length)
if (end === -1) return undefined
try {
const parsed = JSON.parse(body.slice(start + statePrefix.length, end))
if (parsed && parsed._tag === stateTag && Array.isArray(parsed.runs)) return parsed
} catch {
return undefined
}
return undefined
}
const formatNumber = (value) => {
if (value === null || value === undefined || Number.isNaN(value)) return 'n/a'
if (Number.isInteger(value)) return String(value)
return String(Math.round(value * 1000) / 1000)
}
const formatValue = (value, unit) => {
if (value === null || value === undefined) return 'n/a'
if (unit === 'bytes') {
if (value >= 1073741824) return formatNumber(Math.round((value / 1073741824) * 10) / 10) + ' GiB'
if (value >= 1048576) return formatNumber(Math.round((value / 1048576) * 10) / 10) + ' MiB'
if (value >= 1024) return formatNumber(Math.round((value / 1024) * 10) / 10) + ' KiB'
return formatNumber(value) + ' B'
}
if (unit === 'seconds') return formatNumber(value) + ' s'
return formatNumber(value) + (unit ? ' ' + unit : '')
}
const formatDelta = (value, unit) => {
if (value === null || value === undefined) return 'n/a'
const sign = value >= 0 ? '+' : '-'
return sign + formatValue(Math.abs(value), unit)
}
const formatRatio = (value) => {
if (value === null || value === undefined) return 'n/a'
return formatNumber(Math.round((value - 1) * 1000) / 10) + '%'
}
const formatSemanticImpact = (value) => {
if (value === null || value === undefined || Number.isNaN(value)) return 'n/a'
if (Math.abs(value) < 0.005) return '0.00x'
const sign = value > 0 ? '+' : ''
return sign + formatNumber(Math.round(value * 100) / 100) + 'x'
}
const formatRowImpact = (row) => {
if (row.confidence === 'diagnostic' || row.gateReason === 'disabled' || row.semanticImpactKind === 'diagnostic') {
return 'diagnostic'
}
return formatSemanticImpact(row.semanticImpactScore)
}
const formatEvidence = (row) => {
const unit = row.observation?.unit
if (row.comparisonMode === 'paired' && typeof row.evidenceDeltaLower === 'number' && typeof row.evidenceDeltaUpper === 'number') {
const quantile = typeof row.pairedEvidenceQuantile === 'number'
? Math.round(row.pairedEvidenceQuantile * 100)
: 25
return (row.confidence || 'unknown')
+ '<br><sub>paired n=' + (row.pairedSamples ?? 0)
+ ', ' + quantile + '-' + (100 - quantile) + '% delta '
+ formatValue(row.evidenceDeltaLower, unit)
+ ' - ' + formatValue(row.evidenceDeltaUpper, unit)
+ '</sub>'
}
return (row.confidence || 'unknown') + '<br><sub>baseline n=' + (row.baselineSources ?? 0) + ', current samples=' + (row.currentSamples ?? 1) + '</sub>'
}
const interpretation = (row) => {
if (row.confidence === 'low_baseline_count') return {
label: 'Needs more baseline',
detail: 'Not enough compatible baseline runs to make this gate trustworthy.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'low_current_sample_count') return {
label: 'Needs repeat',
detail: 'Current run has too few successful measured samples.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'low_paired_sample_count') return {
label: 'Needs paired evidence',
detail: 'Wall-clock gates require same-run base/head samples before they can block merges.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'missing_paired_delta') return {
label: 'Needs paired delta stats',
detail: 'Wall-clock gates require per-pair delta statistics, not only paired medians.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'paired_uncertain') return {
label: 'Uncertain wall-clock movement',
detail: 'The paired median moved, but the paired delta band still crosses the configured budget.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'diagnostic') return {
label: 'Diagnostic only',
detail: 'Shown for investigation, but intentionally excluded from gating.',
tone: 'diagnostic',
color: '#a78bfa',
}
if (row.status === 'fail') return {
label: 'Regression - blocks merge',
detail: 'Worse than the configured fail threshold with enough samples.',
tone: 'bad',
color: '#ef4444',
}
if (row.status === 'warn') return {
label: 'Regression - review',
detail: 'Worse than the configured warning threshold.',
tone: 'warn',
color: '#f59e0b',
}
if (row.status === 'missing_baseline') return {
label: 'No baseline yet',
detail: 'Current value is measured, but no comparable baseline exists.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'noise_floor') return {
label: 'Too small to matter',
detail: 'The absolute change is below the noise floor for this metric.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'within_baseline_range') return {
label: 'Historical range only',
detail: 'Inside the full historical min/max range, but this range is not used to pass a gate.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.confidence === 'within_robust_band' || row.confidence === 'within_baseline_distribution') return {
label: 'Within noise band',
detail: 'Current and baseline robust noise bands overlap.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.direction === 'improved' && typeof row.semanticImpactScore === 'number' && row.semanticImpactScore <= -1) return {
label: 'Meaningfully lower',
detail: 'Lower than baseline by enough to cross the configured review threshold.',
tone: 'good',
color: '#10b981',
}
if (row.direction === 'improved') return {
label: 'Slightly lower, ok',
detail: 'Lower than baseline, but still inside the configured review budget.',
tone: 'neutral',
color: '#94a3b8',
}
if (row.direction === 'regressed') return {
label: 'Slightly higher, ok',
detail: 'Higher than baseline but still inside the configured budget.',
tone: 'neutral',
color: '#94a3b8',
}
return {
label: 'Unchanged',
detail: 'No meaningful movement from baseline.',
tone: 'neutral',
color: '#94a3b8',
}
}
const formatGate = (row) => {
if (row.gateable) return 'yes'
const reason = row.gateReason || row.confidence || 'unknown'
return 'no<br><sub>' + reason + '</sub>'
}
const escapeCell = (value) => String(value ?? '-').replaceAll('|', '\\|').replaceAll('\n', '<br>')
const escapeXml = (value) => String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
const humanProbe = (row) => {
if (row.observation?.label) return row.observation.label
const probe = row.observation?.dimensions?.probe
const name = row.observation?.name || 'unknown'
const labels = {
shell_eval_traced: 'Shell eval with OTEL trace',
shell_eval_warm: 'Warm shell eval',
tasks_list: 'devenv tasks list',
processes_help: 'devenv processes --help',
task_pnpm_install: 'pnpm:install',
task_genie_run: 'genie:run',
task_check_quick: 'check:quick',
task_check_quick_warm: 'Warm cached check:quick',
task_check_quick_forced: 'Forced check:quick',
}
if (probe && labels[probe]) return labels[probe]
if (name.startsWith('devenv.') && name.endsWith('.duration')) {
return name.slice('devenv.'.length, -'.duration'.length).replaceAll('_', ' ')
}
return name
}
const semanticPath = (row) => {
const parts = semanticSegments(row)
return parts.length > 0 ? parts.join(' / ') : '-'
}
const semanticSegments = (row) => {
const parts = [
...(Array.isArray(row.target?.path) ? row.target.path : []),
row.target?.group,
...(Array.isArray(row.observation?.path) ? row.observation.path : []),
row.observation?.group,
].filter((value) => typeof value === 'string' && value.length > 0)
const seen = new Set()
const unique = parts.filter((part) => {
if (seen.has(part)) return false
seen.add(part)
return true
})
if (unique.length > 0) return unique
const kind = row.target?.kind || row.observation?.measurementKind || 'measurements'
if (kind === 'devenv') return ['performance', 'devenv']
if (kind === 'nix-closure') return ['nix', 'closures']
return [String(kind)]
}
const semanticGroupSegments = (row) => {
const segments = semanticSegments(row)
if (segments.length <= 1) return segments
const targetKind = row.target?.kind
if (targetKind === 'devenv') return segments.slice(0, Math.min(2, segments.length))
if (targetKind === 'nix-closure') return segments.slice(0, Math.min(3, segments.length))
if (targetKind === 'source-shape') return segments.slice(0, Math.min(2, segments.length))
return segments.slice(0, Math.min(2, segments.length))
}
const semanticGroupLabel = (row) => {
const segments = semanticGroupSegments(row)
return segments.length > 0 ? segments.join(' / ') : 'measurements'
}
const groupRows = (rows) => {
const groups = new Map()
for (const row of rows) {
const label = semanticGroupLabel(row)
const existing = groups.get(label)
if (existing) existing.rows.push(row)
else groups.set(label, { label, rows: [row] })
}
return Array.from(groups.values()).sort((left, right) => {
const leftRank = Math.min(...left.rows.map(rank))
const rightRank = Math.min(...right.rows.map(rank))
if (leftRank !== rightRank) return leftRank - rightRank
const leftImpact = Math.max(...left.rows.map((row) => Math.abs(row.semanticImpactScore || 0)))
const rightImpact = Math.max(...right.rows.map((row) => Math.abs(row.semanticImpactScore || 0)))
if (rightImpact !== leftImpact) return rightImpact - leftImpact
return left.label.localeCompare(right.label)
})
}
const chartProbe = (row) => {
if (row.observation?.label) return row.observation.label
const probe = row.observation?.dimensions?.probe
const labels = {
shell_eval_traced: 'Shell eval with OTEL trace',
shell_eval_warm: 'Warm shell eval',
tasks_list: 'devenv tasks list',
processes_help: 'processes --help',
task_pnpm_install: 'pnpm:install',
task_genie_run: 'genie:run',
task_check_quick: 'check:quick',
task_check_quick_warm: 'Warm cached check:quick',
task_check_quick_forced: 'Forced check:quick',
}
if (probe && labels[probe]) return labels[probe]
return humanProbe(row)
}
const dimensions = (row) => {
const entries = Object.entries(row.observation?.dimensions || {})
if (entries.length === 0) return '-'
return entries
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => key + '=' + String(value))
.join('<br>')
}
const rank = (row) => {
if (row.status === 'fail') return 0
if (row.status === 'warn') return 1
if (row.status === 'missing_baseline') return 3
return 2
}
const allRows = Object.values(comparison.comparisons || {}).sort((left, right) => {
const byRank = rank(left) - rank(right)
if (byRank !== 0) return byRank
const leftImpact = typeof left.semanticImpactScore === 'number' ? Math.abs(left.semanticImpactScore) : 0
const rightImpact = typeof right.semanticImpactScore === 'number' ? Math.abs(right.semanticImpactScore) : 0
if (rightImpact !== leftImpact) return rightImpact - leftImpact
const leftDelta = typeof left.delta === 'number' ? Math.abs(left.delta) : 0
const rightDelta = typeof right.delta === 'number' ? Math.abs(right.delta) : 0
if (rightDelta !== leftDelta) return rightDelta - leftDelta
return humanProbe(left).localeCompare(humanProbe(right))
})
const protocolLabel = (() => {
const protocols = new Set(
allRows
.map((row) => row.observation?.dimensions?.measurementProtocol)
.filter((value) => typeof value === 'string' && value.length > 0),
)
return protocols.size > 0 ? Array.from(protocols).join(', ') : 'legacy'
})()
const visibleLimit = Number.isFinite(maxRows) && maxRows > 0 ? maxRows : 10
const comparableRows = allRows.filter((row) => typeof row.baseline === 'number')
const hasComparableBaseline = comparableRows.length > 0
const isDiagnosticRow = (row) =>
row.status === 'missing_baseline' ||
row.confidence === 'diagnostic' ||
row.gateReason === 'disabled' ||
row.semanticImpactKind === 'diagnostic' ||
(!row.gateable && typeof row.baseline !== 'number')
const isZeroImpactRow = (row) =>
typeof row.semanticImpactScore === 'number' &&
!Number.isNaN(row.semanticImpactScore) &&
Math.abs(row.semanticImpactScore) < 0.005
const actionableComparableRows = comparableRows.filter((row) => !isDiagnosticRow(row))
const visibleRows = (hasComparableBaseline
? actionableComparableRows
: allRows.filter((row) => !isDiagnosticRow(row)).sort((left, right) => (right.current || 0) - (left.current || 0))
).slice(0, visibleLimit)
const nonZeroImpactRows = actionableComparableRows.filter((row) => !isZeroImpactRow(row))
const zeroImpactRows = actionableComparableRows.filter(isZeroImpactRow)
const visibleNonZeroImpactRows = nonZeroImpactRows.slice(0, visibleLimit)
const diagnosticRows = allRows.filter(isDiagnosticRow)
const baselineToCurrent = (row) => {
const unit = row.observation?.unit
return formatValue(row.baseline, unit) + ' -> ' + formatValue(row.current, unit)
}
const rawChange = (row) => {
const unit = row.observation?.unit
return formatDelta(row.delta, unit) + ' / ' + formatRatio(row.ratio)
}
const confidenceSummary = (row) => {
const unit = row.observation?.unit
if (row.comparisonMode === 'paired' && typeof row.evidenceDeltaLower === 'number' && typeof row.evidenceDeltaUpper === 'number') {
const quantile = typeof row.pairedEvidenceQuantile === 'number'
? Math.round(row.pairedEvidenceQuantile * 100)
: 25
return 'paired n=' + (row.pairedSamples ?? 0)
+ ', ' + quantile + '-' + (100 - quantile) + '% delta '
+ formatValue(row.evidenceDeltaLower, unit)
+ '..' + formatValue(row.evidenceDeltaUpper, unit)
}
return (row.confidence || 'unknown') + ', baseline n=' + (row.baselineSources ?? 0) + ', current n=' + (row.currentSamples ?? 1)
}
const scanDecision = (row) => {
if (row.status === 'fail') return 'regression blocks'
if (row.status === 'warn') return 'regression review'
if (row.status === 'missing_baseline') return 'needs baseline'
if (row.direction === 'improved') return 'faster'
if (row.direction === 'regressed') return 'no material impact'
return 'unchanged'
}
const scanTable = (rows) => {
if (rows.length === 0) return 'No non-zero actionable measurement impact detected.'
return [
'| What changed? | Group | Probe | Baseline -> current | Raw change | Impact | Confidence |',
'| --- | --- | --- | --- | ---: | ---: | --- |',
...rows.map((row) => {
return '| ' + [
scanDecision(row),
semanticGroupLabel(row),
humanProbe(row),
baselineToCurrent(row),
rawChange(row),
formatRowImpact(row),
confidenceSummary(row),
].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const groupedScanTables = (rows) => {
if (rows.length === 0) return 'No non-zero actionable measurement impact detected.'
return groupRows(rows).map((group) => [
'### ' + group.label,
'',
scanTable(group.rows),
].join('\n')).join('\n\n')
}
const zeroImpactTable = (rows) => {
if (rows.length === 0) return 'No zero-impact measurements.'
return [
'| Group | Probe | Baseline -> current | Raw change | Impact | Gate | Evidence | Why hidden |',
'| --- | --- | --- | ---: | ---: | --- | --- | --- |',
...rows.map((row) => {
const meaning = interpretation(row)
return '| ' + [
semanticGroupLabel(row),
humanProbe(row),
baselineToCurrent(row),
rawChange(row),
formatRowImpact(row),
row.gateable ? 'yes' : (row.gateReason || 'no'),
confidenceSummary(row),
meaning.label,
].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const groupedZeroImpactTables = (rows) => {
if (rows.length === 0) return 'No zero-impact measurements.'
return groupRows(rows).map((group) => [
'### ' + group.label,
'',
zeroImpactTable(group.rows),
].join('\n')).join('\n\n')
}
const diagnosticTable = (rows) => {
if (rows.length === 0) return 'No diagnostic or ungated measurements.'
return [
'| Group | Probe | Current | Baseline | Impact | Gate | Reason | Evidence |',
'| --- | --- | ---: | ---: | ---: | --- | --- | --- |',
...rows.map((row) => {
return '| ' + [
semanticGroupLabel(row),
humanProbe(row),
formatValue(row.current, row.observation?.unit),
formatValue(row.baseline, row.observation?.unit),
formatRowImpact(row),
row.gateable ? 'yes' : (row.gateReason || row.status || 'no'),
interpretation(row).label,
confidenceSummary(row),
].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const groupedDiagnosticTables = (rows) => {
if (rows.length === 0) return 'No diagnostic or ungated measurements.'
return groupRows(rows).map((group) => [
'### ' + group.label,
'',
diagnosticTable(group.rows),
].join('\n')).join('\n\n')
}
const comparisonTable = (rows) => {
if (rows.length === 0) return 'No measurement regressions detected.'
return [
'| Group | Measurement | Baseline | Current | Raw change | Impact | Meaning | Gate | Evidence |',
'| --- | --- | ---: | ---: | ---: | ---: | --- | --- | --- |',
...rows.map((row) => {
const unit = row.observation?.unit
const baselineRange = typeof row.baselineRobustLower === 'number' && typeof row.baselineRobustUpper === 'number' && row.baselineRobustLower !== row.baselineRobustUpper
? '<br><sub>noise band ' + formatValue(row.baselineRobustLower, unit) + ' - ' + formatValue(row.baselineRobustUpper, unit) + '</sub>'
: typeof row.baselineMin === 'number' && typeof row.baselineMax === 'number' && row.baselineMin !== row.baselineMax
? '<br><sub>range ' + formatValue(row.baselineMin, unit) + ' - ' + formatValue(row.baselineMax, unit) + '</sub>'
: ''
const meaning = interpretation(row)
return '| ' + [
semanticPath(row),
humanProbe(row),
formatValue(row.baseline, unit) + baselineRange,
formatValue(row.current, unit),
formatDelta(row.delta, unit) + ' / ' + formatRatio(row.ratio),
formatRowImpact(row),
meaning.label + '<br><sub>' + meaning.detail + '</sub>',
formatGate(row),
formatEvidence(row),
].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const currentOnlyTable = (rows) => {
if (rows.length === 0) return 'No current measurements found.'
return [
'| Group | Measurement | Current |',
'| --- | --- | ---: |',
...rows.map((row) => {
return '| ' + [semanticPath(row), humanProbe(row), formatValue(row.current, row.observation?.unit)].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const allMeasurementsTable = (rows) => {
if (rows.length === 0) return 'No measurement regressions detected.'
return [
'| Status | Gate | Target | Observation | Dimensions | Baseline | Current | Delta | Ratio | Impact |',
'| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: |',
...rows.map((row) => {
const unit = row.observation?.unit
return '| ' + [
row.status,
row.gateable ? 'yes' : (row.gateReason || 'no'),
row.target?.label || row.target?.name || 'unknown',
row.observation?.label || row.observation?.name || 'unknown',
dimensions(row),
formatValue(row.baseline, unit),
formatValue(row.current, unit),
formatDelta(row.delta, unit),
formatRatio(row.ratio),
formatRowImpact(row),
].map(escapeCell).join(' | ') + ' |'
}),
].join('\n')
}
const sourceMeasurement = (row) => ({
id: row.observation?.dimensions?.probe || row.observation?.name || humanProbe(row),
label: humanProbe(row),
group: semanticGroupLabel(row),
path: semanticSegments(row),
groupPath: semanticGroupSegments(row),
status: row.status,
direction: row.direction,
gateable: row.gateable,
gateReason: row.gateReason,
confidence: row.confidence,
comparisonMode: row.comparisonMode,
unit: row.observation?.unit,
baseline: row.baseline ?? null,
current: row.current ?? null,
delta: row.delta ?? null,
ratio: row.ratio ?? null,
semanticImpactScore: row.semanticImpactScore ?? null,
semanticImpactKind: row.semanticImpactKind ?? null,
baselineSources: row.baselineSources ?? null,
currentSamples: row.currentSamples ?? null,
pairedSamples: row.pairedSamples ?? null,
evidenceDeltaLower: row.evidenceDeltaLower ?? null,
evidenceDeltaUpper: row.evidenceDeltaUpper ?? null,
pairedEvidenceQuantile: row.pairedEvidenceQuantile ?? null,
dimensions: row.observation?.dimensions || {},
})
const truncate = (value, maxLength) => {
const text = String(value)
if (text.length <= maxLength) return text
if (maxLength <= 1) return text.slice(0, maxLength)
return text.slice(0, Math.max(0, maxLength - 3)) + '...'
}
const renderPerfChangeSvg = (rows, theme = 'adaptive') => {
const chartRows = rows
.filter((row) => typeof row.current === 'number' && typeof row.baseline === 'number')
.filter((row) => row.gateable === true)
.filter((row) => typeof row.semanticImpactScore === 'number')
.sort((left, right) => (left.semanticImpactScore || 0) - (right.semanticImpactScore || 0))
.slice(0, visibleLimit)
if (chartRows.length === 0) return ''
const impactScores = chartRows.map((row) => row.semanticImpactScore || 0)
const minImpact = Math.min(-1, ...impactScores)
const maxImpact = Math.max(1, ...impactScores)
const lower = Math.floor(minImpact)
const upper = Math.ceil(maxImpact)
const span = upper - lower || 1
const width = 1040
const rowHeight = 46
const height = 112 + chartRows.length * rowHeight + 34
const labelX = 230
const plotX = 252
const plotWidth = 320
const impactX = 596
const nominalX = 672
const meaningX = 804
const topY = 92
const barHeight = 18
const zeroX = plotX + ((0 - lower) / span) * plotWidth
const themeCss = theme === 'dark'
? [
' .chart-bg { fill: #0d1117; }',
' .chart-border { fill: none; stroke: #30363d; }',
' .chart-title { fill: #f0f6fc; }',
' .chart-muted { fill: #8b949e; }',
' .chart-axis { stroke: #8b949e; }',
' .chart-label { fill: #c9d1d9; }',
' .chart-value { fill: #8b949e; }',
' .chart-track { fill: #21262d; }',
]
: [
' .chart-bg { fill: #ffffff; }',
' .chart-border { fill: none; stroke: #d0d7de; }',
' .chart-title { fill: #24292f; }',
' .chart-muted { fill: #57606a; }',
' .chart-axis { stroke: #8c959f; }',
' .chart-label { fill: #24292f; }',
' .chart-value { fill: #57606a; }',
' .chart-track { fill: #f6f8fa; }',
...(theme === 'adaptive'
? [
' @media (prefers-color-scheme: dark) {',
' .chart-bg { fill: #0d1117; }',
' .chart-border { stroke: #30363d; }',
' .chart-title { fill: #f0f6fc; }',
' .chart-muted { fill: #8b949e; }',
' .chart-axis { stroke: #8b949e; }',
' .chart-label { fill: #c9d1d9; }',
' .chart-value { fill: #8b949e; }',
' .chart-track { fill: #21262d; }',
' }',
]
: []),
]
const svg = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + ' ' + height + '">',
'<style>',
...themeCss,
'</style>',
'<rect class="chart-bg" width="' + width + '" height="' + height + '" rx="8"/>',
'<rect class="chart-border" x="0.5" y="0.5" width="' + (width - 1) + '" height="' + (height - 1) + '" rx="7.5"/>',
'<text class="chart-title" x="' + width / 2 + '" y="28" text-anchor="middle" font-family="DejaVu Sans" font-size="16" font-weight="700">Actionable measurement impact</text>',
'<text class="chart-muted" x="' + width / 2 + '" y="48" text-anchor="middle" font-family="DejaVu Sans" font-size="11">0 means no actionable PR impact; 1x reaches the warning budget.</text>',
'<text x="' + plotX + '" y="72" font-family="DejaVu Sans" font-size="11" fill="#059669">improved</text>',
'<text x="' + (plotX + plotWidth) + '" y="72" text-anchor="end" font-family="DejaVu Sans" font-size="11" fill="#dc2626">regressed</text>',
'<text class="chart-muted" x="' + impactX + '" y="72" font-family="DejaVu Sans" font-size="11">impact</text>',
'<text class="chart-muted" x="' + nominalX + '" y="72" font-family="DejaVu Sans" font-size="11">baseline -> current</text>',
'<text class="chart-muted" x="' + meaningX + '" y="72" font-family="DejaVu Sans" font-size="11">meaning</text>',
'<line class="chart-axis" x1="' + zeroX.toFixed(1) + '" y1="82" x2="' + zeroX.toFixed(1) + '" y2="' + (height - 34) + '" stroke-width="1.1" opacity="0.9"/>',
]
for (const [index, row] of chartRows.entries()) {
const impact = row.semanticImpactScore || 0
const y = topY + index * rowHeight
const valueWidth = Math.max(2, Math.abs(impact) / span * plotWidth)
const x = impact < 0 ? zeroX - valueWidth : zeroX
const meaning = interpretation(row)
const color = meaning.color
const formattedImpact = formatSemanticImpact(impact)
const label = chartProbe(row)
const nominal = formatValue(row.baseline, row.observation?.unit).replaceAll(' ', '') + ' -> ' + formatValue(row.current, row.observation?.unit).replaceAll(' ', '')
const barOpacity = meaning.tone === 'neutral' ? '0.65' : '1'
const dash = meaning.tone === 'diagnostic' ? ' stroke-dasharray="3 3"' : ''
svg.push(
'<text class="chart-label" x="' + labelX + '" y="' + (y + 13) + '" text-anchor="end" font-family="DejaVu Sans" font-size="12"><title>' + escapeXml(label) + '</title>' + escapeXml(truncate(label, 28)) + '</text>',
'<rect class="chart-track" x="' + plotX + '" y="' + y + '" width="' + plotWidth + '" height="' + barHeight + '" rx="5"/>',
'<rect x="' + x.toFixed(1) + '" y="' + y + '" width="' + valueWidth.toFixed(1) + '" height="' + barHeight + '" rx="5" fill="' + color + '" opacity="' + barOpacity + '"' + dash + '/>',
'<text x="' + impactX + '" y="' + (y + 13) + '" font-family="DejaVu Sans" font-size="12" font-weight="700" fill="' + color + '">' + escapeXml(formattedImpact) + '</text>',
'<text class="chart-value" x="' + nominalX + '" y="' + (y + 13) + '" font-family="DejaVu Sans" font-size="11"><title>' + escapeXml(nominal) + '</title>' + escapeXml(truncate(nominal, 21)) + '</text>',
'<text x="' + meaningX + '" y="' + (y + 13) + '" font-family="DejaVu Sans" font-size="11" font-weight="600" fill="' + color + '"><title>' + escapeXml(meaning.detail) + '</title>' + escapeXml(truncate(meaning.label, 30)) + '</text>',
)
}
svg.push(
'<text class="chart-muted" x="' + zeroX.toFixed(1) + '" y="' + (height - 16) + '" text-anchor="middle" font-family="DejaVu Sans" font-size="10">0</text>',
'</svg>',
)
return svg.join('\n')
}
const statusWord = comparison.status || 'unknown'
const readiness = comparison.readiness || {}
const readinessLabel = readiness.enforceable
? 'enforceable'
: 'partial (' + (readiness.gateableCount ?? 0) + '/' + (readiness.enabledCount ?? 0) + ' enabled observations gateable)'
const runUrl = runId ? serverUrl + '/' + repo + '/actions/runs/' + runId : undefined
const shortSha = (headSha || sha || 'unknown').slice(0, 7)
const existingState = extractState(existing?.body)
const currentRun = {
commitSha: headSha || sha || 'unknown',
shortSha,
generatedAt: new Date().toISOString(),
status: statusWord,
mode: comparison.mode || 'unknown',
runUrl,
runAttempt,
workflow,
job,
visibleRows: visibleRows.map((row) => ({
status: row.status,
target: row.target?.label || row.target?.name || 'unknown',
observation: row.observation?.label || row.observation?.name || 'unknown',
meaning: interpretation(row).label,
dimensions: dimensions(row).replaceAll('<br>', ', '),
baseline: formatValue(row.baseline, row.observation?.unit),
current: formatValue(row.current, row.observation?.unit),
delta: formatDelta(row.delta, row.observation?.unit),
ratio: formatRatio(row.ratio),
impact: formatSemanticImpact(row.semanticImpactScore),
})),
}
const hasComparableHistory = (run) => Array.isArray(run.visibleRows) && run.visibleRows.some((row) =>
row.status !== 'missing_baseline' &&
row.baseline !== 'n/a' &&
row.ratio !== 'n/a'
)
const previousRuns = (existingState?.runs || []).filter((run) => run.commitSha !== currentRun.commitSha && hasComparableHistory(run))
const historyLimit = Number.isFinite(maxHistory) && maxHistory > 0 ? maxHistory : 20
const state = { _tag: stateTag, schemaVersion, title, runs: [currentRun, ...previousRuns].slice(0, historyLimit) }
const gateModeLabel = (mode) => {
if (mode === 'fail') return 'enforced'
if (mode === 'warn') return 'advisory'
if (mode === 'off') return 'off'
return mode || 'unknown'
}
const historyRows = state.runs.slice(1).map((run) => {
const link = run.runUrl ? '[' + run.shortSha + '](' + run.runUrl + ')' : run.shortSha
const top = Array.isArray(run.visibleRows) && run.visibleRows.length > 0
? run.visibleRows.slice(0, 3).map((row) => (row.meaning || row.status) + ' ' + row.target + ' ' + row.observation + ' ' + row.delta + ' / ' + row.ratio).join('<br>')
: 'No regressions'
return '| ' + [link, run.status, gateModeLabel(run.mode), top].map(escapeCell).join(' | ') + ' |'
})
const runLink = runUrl ? '[workflow run](' + runUrl + ')' : 'workflow run unavailable'
const baselineProvenance = comparison.baselineProvenance
const baselineLabel = baselineProvenance?.runId
? '[main run ' + baselineProvenance.runId + '](' + serverUrl + '/' + repo + '/actions/runs/' + baselineProvenance.runId + ')' +
(Array.isArray(baselineProvenance.runs) && baselineProvenance.runs.length > 1 ? ' + ' + (baselineProvenance.runs.length - 1) + ' older baseline runs' : '')
: 'not available'
const sourceOfTruth = {
schemaVersion,
title,
status: statusWord,
gate: gateModeLabel(comparison.mode),
readiness: readinessLabel,
commit: {
shortSha,
sha: headSha || sha || 'unknown',
},
run: {
id: runId || null,
attempt: runAttempt || null,
url: runUrl || null,
},
baseline: baselineProvenance || null,
protocol: protocolLabel,
chart: {
meaning: 'semantic-impact',
zeroImpactMeaning: 'no actionable PR impact after budgets, noise floor, and robust evidence checks',
svg: chartSourceUrl || null,
lightPng: chartUrl || null,
darkPng: chartDarkUrl || null,
},
measurements: allRows.map(sourceMeasurement),
}
const chartSvg = hasComparableBaseline && visibleRows.length > 0 ? renderPerfChangeSvg(visibleRows) : ''
const chartDarkSvg = hasComparableBaseline && visibleRows.length > 0 ? renderPerfChangeSvg(visibleRows, 'dark') : ''
if (chartPath && chartSvg) writeFileSync(chartPath, chartSvg)
if (chartDarkPath && chartDarkSvg) writeFileSync(chartDarkPath, chartDarkSvg)
const chartImageMarkdown = chartUrl && chartSvg
? (chartDarkUrl
? '<picture>\n' +
' <source media="(prefers-color-scheme: dark)" srcset="' + chartDarkUrl + '">\n' +
' <source media="(prefers-color-scheme: light)" srcset="' + chartUrl + '">\n' +
' <img alt="Measurement change vs baseline chart" src="' + chartUrl + '">\n' +
'</picture>'
: '![Measurement change vs baseline chart](' + chartUrl + ')')
: ''
const chartMarkdown = chartImageMarkdown
? chartImageMarkdown +
(chartSourceUrl ? '\n\n[SVG source](' + chartSourceUrl + ')' : '')
: ''
const regressionCount = allRows.filter((row) => row.status === 'fail' || row.status === 'warn').length
const improvementCount = comparableRows.filter((row) => row.direction === 'improved' && !isZeroImpactRow(row)).length
const neutralCount = zeroImpactRows.length + diagnosticRows.length
const humanSummary = hasComparableBaseline
? regressionCount > 0
? String(regressionCount) + ' regression' + (regressionCount === 1 ? '' : 's') + ' need review.'
: improvementCount > 0
? 'No regressions. ' + String(improvementCount) + ' probe' + (improvementCount === 1 ? '' : 's') + ' got faster; ' + String(neutralCount) + ' neutral or ungated row' + (neutralCount === 1 ? '' : 's') + ' are collapsed below.'
: 'No regressions. Comparable movement is below the semantic impact threshold; neutral rows are collapsed below.'
: 'No compatible baseline was available, so this run shows current measurements only.'
const summaryLines = [
'## ' + title,
'',
'**' + statusWord + '** - ' + gateModeLabel(comparison.mode) + ' gate - readiness <code>' + readinessLabel + '</code> - commit <code>' + shortSha + '</code> - protocol <code>' + protocolLabel + '</code>',
'',
'> ' + humanSummary,
'',
chartMarkdown,
'',
hasComparableBaseline
? groupedScanTables(visibleNonZeroImpactRows)
: currentOnlyTable(visibleRows),
]
if (hasComparableBaseline && zeroImpactRows.length > 0) {
summaryLines.push(
'',
'<details>',
'<summary>Unchanged / 0-impact measurements (' + zeroImpactRows.length + ')</summary>',
'',
'These rows had compatible baseline data, but their semantic impact rounded to 0.00x because the movement was below the configured budget, below the noise floor, or inside the robust noise band.',
'',
groupedZeroImpactTables(zeroImpactRows),
'',
'</details>',
)
}
if (diagnosticRows.length > 0) {
summaryLines.push(
'',
'<details>',
'<summary>Diagnostic / ungated measurements (' + diagnosticRows.length + ')</summary>',
'',
groupedDiagnosticTables(diagnosticRows),
'',
'</details>',
)
}
summaryLines.push(
'',
'<details>',
'<summary>All measurements</summary>',
'',
allMeasurementsTable(allRows),
'',
'</details>',
)
if (historyRows.length > 0) {
summaryLines.push(
'',
'<details>',
'<summary>Previous runs</summary>',
'',
'| Commit | Status | Gate | Top changes |',
'| --- | --- | --- | --- |',
...historyRows,
'',
'</details>',
)
}
summaryLines.push(
'',
'<details>',
'<summary>Source-of-truth JSON</summary>',
'',
'~~~json',
JSON.stringify(sourceOfTruth, null, 2),
'~~~',
'',
'</details>',
)
summaryLines.push('', marker, statePrefix + JSON.stringify(state, null, 2) + stateSuffix)
writeFileSync(bodyPath, summaryLines.join('\n') + '\n')
writeFileSync(commentIdPath, existing?.id ? String(existing.id) : '')
EOF
node "$renderer_script" "$comparison_file" "$comments_json" "$comment_body" "$comment_id_file" "$chart_file" "$chart_dark_file"
rerender_ci_measurement_comment() {
node "$renderer_script" "$comparison_file" "$comments_json" "$comment_body" "$comment_id_file" "$chart_file" "$chart_dark_file"
}
drop_ci_measurement_chart_source() {
export CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL=""
rerender_ci_measurement_comment
}
drop_ci_measurement_chart_images() {
export CI_MEASUREMENT_PR_COMMENT_CHART_URL=""
export CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL=""
export CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL=""
rerender_ci_measurement_comment
}
if [ -s "$chart_file" ]; then
if [ "$require_public_asset" = "true" ] && [ -z "$public_asset_command" ]; then
echo "::error::CI measurement chart was rendered for a private repository, but CI_MEASUREMENT_PR_COMMENT_PUBLIC_ASSET_COMMAND is not configured. Private raw GitHub URLs cannot be embedded in PR comments."
exit 1
fi
if ensure_ci_measurement_tool resvg resvg; then
resvg_font_args=()
if command -v nix >/dev/null 2>&1; then
if font_out="$(nix build --no-link --print-out-paths nixpkgs#dejavu_fonts 2>/dev/null)"; then
resvg_font_args+=(--use-fonts-dir "$font_out/share/fonts/truetype")
fi
fi
if ! resvg --background '#ffffff' "${resvg_font_args[@]}" "$chart_file" "$chart_png_file"; then
echo "::notice::unable to render CI measurement chart PNG"
rm -f "$chart_png_file"
fi
if [ -s "$chart_dark_file" ] && ! resvg --background '#0d1117' "${resvg_font_args[@]}" "$chart_dark_file" "$chart_dark_png_file"; then
echo "::notice::unable to render dark CI measurement chart PNG"
rm -f "$chart_dark_png_file"
fi
else
echo "::notice::resvg is not available; skipping embedded CI measurement chart PNG"
fi
if ! gh api "repos/$repo/git/ref/heads/$asset_branch" >/dev/null 2>&1; then
default_branch_sha="$(gh api "repos/$repo/git/ref/heads/${GITHUB_BASE_REF:-main}" --jq '.object.sha' 2>/dev/null || true)"
if [ -z "$default_branch_sha" ]; then
default_branch_sha="${GITHUB_SHA:-}"
fi
if [ -n "$default_branch_sha" ]; then
gh api "repos/$repo/git/refs" --method POST --field ref="refs/heads/$asset_branch" --field sha="$default_branch_sha" >/dev/null || true
fi
fi
chart_content="$(base64 <"$chart_file" | tr -d '\n')"
if ! gh api "repos/$repo/contents/$asset_svg_path" --method PUT --field message="Update CI measurement chart SVG for PR #$pr_number" --field content="$chart_content" --field branch="$asset_branch" >/dev/null; then
echo "::notice::unable to upload CI measurement chart SVG asset"
if [ -z "$public_asset_command" ]; then
drop_ci_measurement_chart_source
fi
fi
if [ -s "$chart_png_file" ]; then
chart_png_content="$(base64 <"$chart_png_file" | tr -d '\n')"
if ! gh api "repos/$repo/contents/$asset_png_path" --method PUT --field message="Update CI measurement chart PNG for PR #$pr_number" --field content="$chart_png_content" --field branch="$asset_branch" >/dev/null; then
echo "::notice::unable to upload CI measurement chart PNG asset"
if [ -z "$public_asset_command" ]; then
drop_ci_measurement_chart_images
fi
fi
else
drop_ci_measurement_chart_images
fi
if [ -s "$chart_dark_png_file" ]; then
chart_dark_png_content="$(base64 <"$chart_dark_png_file" | tr -d '\n')"
if ! gh api "repos/$repo/contents/$asset_dark_png_path" --method PUT --field message="Update dark CI measurement chart PNG for PR #$pr_number" --field content="$chart_dark_png_content" --field branch="$asset_branch" >/dev/null; then
echo "::notice::unable to upload dark CI measurement chart PNG asset"
if [ -z "$public_asset_command" ]; then
export CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL=""
rerender_ci_measurement_comment
fi
fi
fi
if [ -n "$public_asset_command" ] && [ -s "$chart_png_file" ]; then
if public_chart_url="$(bash -c "$public_asset_command" _ "$chart_png_file" png)" && [ -n "$public_chart_url" ]; then
chart_url="$public_chart_url"
export CI_MEASUREMENT_PR_COMMENT_CHART_URL="$chart_url"
else
echo "::notice::unable to publish CI measurement chart PNG to public asset host"
export CI_MEASUREMENT_PR_COMMENT_CHART_URL=""
fi
if [ -s "$chart_dark_png_file" ] && public_chart_dark_url="$(bash -c "$public_asset_command" _ "$chart_dark_png_file" png)" && [ -n "$public_chart_dark_url" ]; then
chart_dark_url="$public_chart_dark_url"
export CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL="$chart_dark_url"
else
echo "::notice::unable to publish dark CI measurement chart PNG to public asset host"
export CI_MEASUREMENT_PR_COMMENT_CHART_DARK_URL=""
fi
if public_chart_source_url="$(bash -c "$public_asset_command" _ "$chart_file" svg)" && [ -n "$public_chart_source_url" ]; then
chart_source_url="$public_chart_source_url"
export CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL="$chart_source_url"
else
echo "::notice::unable to publish CI measurement chart SVG to public asset host"
export CI_MEASUREMENT_PR_COMMENT_CHART_SOURCE_URL=""
fi
if [ "$require_public_asset" = "true" ] && [ -z "$chart_url" ]; then
echo "::error::unable to publish CI measurement chart PNG to a public asset host for private repository $repo"
exit 1
fi
if [ "$require_public_asset" = "true" ] && [ -s "$chart_dark_png_file" ] && [ -z "$chart_dark_url" ]; then
echo "::error::unable to publish dark CI measurement chart PNG to a public asset host for private repository $repo"
exit 1
fi
node "$renderer_script" "$comparison_file" "$comments_json" "$comment_body" "$comment_id_file" "$chart_file" "$chart_dark_file"
fi
fi
comment_id="$(cat "$comment_id_file")"
comment_payload_file="$comment_body.payload.json"
node -e "const fs=require('node:fs'); fs.writeFileSync(process.argv[2], JSON.stringify({ body: fs.readFileSync(process.argv[1], 'utf8') }))" "$comment_body" "$comment_payload_file"
if [ -n "$comment_id" ]; then
if ! gh api "repos/$repo/issues/comments/$comment_id" --method PATCH --input "$comment_payload_file" >/dev/null; then
echo "::notice::unable to update CI measurement PR comment"
fi
else
if ! gh api "repos/$repo/issues/$pr_number/comments" --method POST --input "$comment_payload_file" >/dev/null; then
echo "::notice::unable to create CI measurement PR comment"
fi
fi
fi
fi
fi
if [ "$exit_code" -ne 0 ]; then
exit "$exit_code"
fi
- name: 'Upload CI measurements: ci-measurements-report'
if: always()
uses: actions/upload-artifact@v4
with:
name: ci-measurements-report
path: |
tmp/ci-measurement-report
!tmp/ci-measurement-report/baseline/**
if-no-files-found: error
retention-days: 14
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-ci-measurements-report"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
test-integration-notion:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 90
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
NOTION_API_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
NOTION_TEST_PARENT_PAGE_ID: ${{ secrets.NOTION_TEST_PARENT_PAGE_ID }}
NOTION_DATASOURCE_SYNC_PARENT_PAGE_ID: ${{ secrets.NOTION_DATASOURCE_SYNC_PARENT_PAGE_ID || secrets.NOTION_TEST_PARENT_PAGE_ID }}
NOTION_DATASOURCE_SYNC_E2E_LEDGER_PAGE_ID: ${{ secrets.NOTION_DATASOURCE_SYNC_E2E_LEDGER_PAGE_ID }}
NOTION_DATASOURCE_SYNC_DEMO_PAGE_ID: ${{ github.event_name == 'workflow_dispatch' && (inputs.run_datasource_sync_demo == true || inputs.run_datasource_sync_demo == 'true') && secrets.NOTION_DATASOURCE_SYNC_DEMO_PAGE_ID || '' }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Notion integration tests
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run test:notion-integration --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run test:notion-integration --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-test-integration-notion"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
test-integration-restate:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
concurrency:
group: 'test-integration-restate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
cancel-in-progress: true
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 60
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- name: Restate integration tests
run: |
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run test:restate-integration --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run test:restate-integration --mode before'
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
deploy-storybooks:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') }}
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
defaults:
run:
shell: bash
env:
FORCE_SETUP: '1'
CI: 'true'
GITHUB_TOKEN: ${{ github.token }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
steps:
- uses: actions/checkout@v6
- name: Checkout CI measurement baseline ref
if: ${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.measurement_baseline_ref }}
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
extra-substituters = https://devenv.cachix.org
extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=
access-tokens = github.com=${{ github.token }}
summarize: true
- name: Provide cachix CLI from nixpkgs
shell: bash
run: |
set -euo pipefail
out=$(nix build --no-link --print-out-paths nixpkgs#cachix)
echo "$out/bin" >> "$GITHUB_PATH"
- name: Enable Cachix cache
uses: cachix/cachix-action@v17
with:
name: overeng-effect-utils
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Use pinned devenv from lock
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
echo "DEVENV_REV=$DEVENV_REV" >> "$GITHUB_ENV"
echo "Pinned devenv rev: $DEVENV_REV"
shell: bash
- name: Isolate pnpm state
shell: bash
run: |
echo "PNPM_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_CONFIG_STORE_DIR=${{ runner.temp }}/pnpm-store/${{ github.job }}" >> "$GITHUB_ENV"
echo "PNPM_HOME=${{ github.workspace }}/.pnpm-home" >> "$GITHUB_ENV"
- id: restore-pnpm-state
name: Restore pnpm state
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Resolve devenv
run: |
DEVENV_REV=$(jq -r .nodes.devenv.locked.rev devenv.lock)
if [ -z "$DEVENV_REV" ] || [ "$DEVENV_REV" = "null" ]; then
echo '::error::devenv.lock missing .nodes.devenv.locked.rev'
exit 1
fi
resolve_devenv() {
nix build \
--accept-flake-config \
--option extra-substituters https://devenv.cachix.org \
--option extra-trusted-public-keys devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= \
--no-link \
--print-out-paths \
"github:cachix/devenv/$DEVENV_REV#devenv"
}
# Temporary: capture diagnostics dir for #272 root-cause analysis.
DIAG_ROOT="${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-${GITHUB_JOB:-job}-${RUNNER_OS:-unknown}-${GITHUB_RUN_ATTEMPT:-0}"
mkdir -p "$DIAG_ROOT"
echo "NIX_STORE_DIAGNOSTICS_DIR=$DIAG_ROOT" >> "$GITHUB_ENV"
{
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "runner_name=${RUNNER_NAME:-unknown}"
echo "runner_os=${RUNNER_OS:-unknown}"
echo "runner_arch=${RUNNER_ARCH:-unknown}"
echo "github_job=${GITHUB_JOB:-unknown}"
echo "github_run_id=${GITHUB_RUN_ID:-unknown}"
echo "nix_user_conf_files=${NIX_USER_CONF_FILES:-}"
nix --version || true
} > "$DIAG_ROOT/environment.txt" 2>&1
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv.log" >&2)); then
echo "::error::resolve_devenv failed. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
# Fast validity check on the devenv store path (~1-2s vs ~25s for devenv info).
if ! nix-store --check-validity "$DEVENV_OUT" 2>/dev/null; then
echo "::warning::devenv store path invalid, repairing targeted path..."
nix-store --repair-path "$DEVENV_OUT" > "$DIAG_ROOT/nix-store-verify-repair.log" 2>&1 || true
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/nix/eval-cache-* ~/.cache/nix/eval-cache-*
if ! DEVENV_OUT=$(resolve_devenv 2> >(tee "$DIAG_ROOT/resolve-devenv-post-repair.log" >&2)); then
echo "::error::resolve_devenv failed after repair. Last 30 lines of log:"
tail -30 "$DIAG_ROOT/resolve-devenv-post-repair.log" || true
exit 1
fi
DEVENV_BIN="$DEVENV_OUT/bin/devenv"
fi
echo "DEVENV_BIN=$DEVENV_BIN" >> "$GITHUB_ENV"
"$DEVENV_BIN" version | tee "$DIAG_ROOT/devenv-version.txt"
shell: bash
- name: Evict cached pnpm deps for oxlint-npm
shell: bash
run: |
targetRef='.#oxlint-npm'
entriesJson=$(mktemp)
if nix eval --json "$targetRef.passthru.depsBuildEntries" >"$entriesJson" 2>/dev/null; then
while IFS=$'\t' read -r attrName drv; do
[ -n "$drv" ] || continue
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(jq -r '.[] | [.attrName, (.drvPath // "")] | @tsv' "$entriesJson")
else
topDrv=$(nix path-info --derivation "$targetRef" 2>/dev/null || true)
if [ -n "$topDrv" ]; then
while IFS= read -r drv; do
[ -n "$drv" ] || continue
attrName=""
while IFS= read -r outPath; do
[ -n "$outPath" ] || continue
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "evicting cached: $(basename "$outPath")"
if ! nix store delete --ignore-liveness "$outPath" >/dev/null 2>&1; then
echo "::error::failed to evict cached pnpm-deps output: $outPath"
exit 1
fi
if nix path-info "$outPath" >/dev/null 2>&1; then
echo "::error::cached pnpm-deps output still present after eviction: $outPath"
exit 1
fi
fi
done < <(nix-store -q --outputs "$drv" 2>/dev/null || true)
done < <(nix-store -qR "$topDrv" 2>/dev/null | grep "pnpm-deps-[a-z0-9-]*-v[0-9].*\.drv$" || true)
fi
fi
rm -f "$entriesJson"
- name: Force diagnostics failure (debug)
if: ${{ github.event_name == 'workflow_dispatch' && (inputs.debug_force_nix_diagnostics_failure == true || inputs.debug_force_nix_diagnostics_failure == 'true') }}
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-${RUNNER_TEMP:-/tmp}/nix-store-diagnostics-missing}"
mkdir -p "$diag_dir"
cat > "$diag_dir/synthetic-signature.log" <<'EOF'
Failed to convert config.cachix to JSON
... while evaluating the option `cachix.package`
error: path '/nix/store/synthetic-invalid-path' is not valid
EOF
echo "::warning::Intentional failure for diagnostics validation (#272)"
exit 1
- id: deploy
name: Deploy storybooks to Netlify
shell: bash
run: |
tmp_log="/tmp/netlify-storybook-deploy.log"
rm -f "$tmp_log"
workflow_report_dir="${RUNNER_TEMP:-/tmp}/workflow-reports"
mkdir -p "$workflow_report_dir"
workflow_report_path="$(mktemp "$workflow_report_dir/netlify-storybooks.XXXXXX.jsonl")"
export WORKFLOW_REPORT_OUTPUT_FILE="$workflow_report_path"
if [ -z "${NETLIFY_AUTH_TOKEN:-}" ]; then
deployed_at_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
workflow_report_json="$(jq -cn \
--argjson schemaVersion 1 \
--arg tag WorkflowReportRecord \
--arg id deploy-netlify-storybooks \
--arg kind "deploy-preview" \
--arg status skipped \
--arg provider netlify \
--arg target storybooks \
--arg displayName "Storybooks" \
--arg deployedAtUtc "$deployed_at_utc" \
--arg message "Skipping Netlify deploy because NETLIFY_AUTH_TOKEN is not available." \
'{_tag: $tag, schemaVersion: $schemaVersion, id: $id, kind: $kind, subject: {id: $target, label: $displayName}, status: $status, title: ($displayName + " preview not deployed"), summary: $message, createdAtUtc: $deployedAtUtc, data: {provider: $provider, target: $target, displayName: $displayName, reason: $message}}')"
printf "%s\n" "$workflow_report_json" > "$workflow_report_path"
echo "WORKFLOW_REPORT_V1: $workflow_report_json"
echo "workflow_report_path=$workflow_report_path" >> "$GITHUB_OUTPUT"
printf 'workflow_report=%s\n' "$workflow_report_json" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping Netlify deploy (NETLIFY_AUTH_TOKEN not available)"
exit 0
fi
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run netlify:deploy --show-output --input type=prod --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run netlify:deploy --show-output --input type=prod --mode before' 2>&1 | tee "$tmp_log"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
__nix_gc_retry_helper=$(mktemp)
cat > "$__nix_gc_retry_helper" <<'EOF'
#!/usr/bin/env bash
run_nix_gc_race_retry() {
local task="$1"
local command="$2"
local max="${NIX_GC_RACE_MAX_RETRIES:-10}"
local heartbeat="${CI_PROGRESS_HEARTBEAT_SECONDS:-60}"
local attempt=1
local log rc path start now elapsed hb_pid flattened saw_invalid_path saw_cachix_signature saw_fetch_signature saw_daemon_socket_failure had_errexit
start="$(date +%s)"
write_summary() {
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
{
echo "### CI Task"
echo "- Task: $task"
echo "- Status: $1"
echo "- Duration: $elapsed s"
echo "- Attempts: $attempt/$max"
[ -z "${2:-}" ] || echo "- Note: $2"
} >> "$GITHUB_STEP_SUMMARY"
}
repair_nix_daemon() {
echo "::warning::Nix daemon socket is unavailable; attempting daemon restart before retry"
if command -v launchctl >/dev/null 2>&1; then
sudo launchctl kickstart -k system/org.nixos.nix-daemon >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl restart nix-daemon.socket >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon.service >/dev/null 2>&1 || true
sudo systemctl restart nix-daemon >/dev/null 2>&1 || true
fi
if [ ! -S /nix/var/nix/daemon-socket/socket ] && [ -x /nix/var/nix/profiles/default/bin/nix-daemon ]; then
sudo /nix/var/nix/profiles/default/bin/nix-daemon --daemon >/tmp/nix-daemon-restart.log 2>&1 || true
fi
for _ in 1 2 3 4 5; do
[ -S /nix/var/nix/daemon-socket/socket ] && return 0
sleep 1
done
return 0
}
while [ "$attempt" -le "$max" ]; do
echo "::notice::[ci] starting $task (attempt $attempt/$max)"
(
while sleep "$heartbeat"; do
now=$(date +%s)
elapsed=$((now - start))
echo "::notice::[ci] $task still running after $elapsed s (attempt $attempt/$max)"
done
) &
hb_pid=$!
log=$(mktemp)
had_errexit=false
case $- in
*e*) had_errexit=true ;;
esac
set +e
eval "$command" > >(tee -a "$log") 2> >(tee -a "$log" >&2)
rc=$?
if [ "$had_errexit" = true ]; then
set -e
fi
kill "$hb_pid" 2>/dev/null || true
wait "$hb_pid" 2>/dev/null || true
now=$(date +%s)
elapsed=$((now - start))
if [ "$rc" -eq 0 ]; then
echo "::notice::[ci] completed $task in $elapsed s"
if [ "$attempt" -gt 1 ]; then
write_summary success "Recovered from transient Nix failure after retry"
else
write_summary success
fi
rm -f "$log"
return 0
fi
flattened=$(tr '\r\n' ' ' < "$log" | sed -E $'s/\x1B\[[0-9;]*m//g')
path=$(printf '%s' "$flattened" |
grep -o "error:[[:space:]]*path '/nix/store/[^']*'[[:space:]]*is not valid" |
head -1 |
grep -o "/nix/store/[^']*" |
tr -d '[:space:]' || true)
saw_invalid_path=false
saw_cachix_signature=false
saw_fetch_signature=false
saw_daemon_socket_failure=false
[ -n "$path" ] && saw_invalid_path=true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*Failed to convert config\.cachix to JSON' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*.*while evaluating the option.*cachix\.package' && saw_cachix_signature=true || true
printf '%s' "$flattened" | grep -Eq 'error:[[:space:]]*cannot read file from tarball:[[:space:]]*Truncated tar archive detected while reading data' && saw_fetch_signature=true || true
printf '%s' "$flattened" | grep -Eq "error:[[:space:]]*cannot connect to socket at '/nix/var/nix/daemon-socket/socket'" && saw_daemon_socket_failure=true || true
rm -f "$log"
if [ "$saw_invalid_path" != true ] && [ "$saw_cachix_signature" != true ] && [ "$saw_fetch_signature" != true ] && [ "$saw_daemon_socket_failure" != true ]; then
echo "::warning::[ci] $task failed after $elapsed s without a detected transient Nix failure"
write_summary failure "No transient Nix failure signature detected"
return "$rc"
fi
if [ "$saw_daemon_socket_failure" = true ]; then
repair_nix_daemon
echo "::warning::Nix daemon socket failure detected for $task (attempt $attempt/$max); retrying after daemon repair"
elif [ "$saw_fetch_signature" = true ]; then
echo "::warning::Nix source fetch corruption detected for $task (attempt $attempt/$max); retrying with a refreshed eval cache"
elif [ "$saw_cachix_signature" = true ] && [ -n "$path" ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper (attempt $attempt/$max): $path"
elif [ "$saw_cachix_signature" = true ]; then
echo "::warning::Nix store validity race detected for $task via cachix eval wrapper without extracted store path (attempt $attempt/$max)"
else
echo "::warning::Nix store validity race detected for $task (attempt $attempt/$max): $path"
fi
[ -z "$path" ] || nix-store --realise "$path" 2>/dev/null || true
rm -rf ~/.cache/nix/eval-cache-*
attempt=$((attempt + 1))
done
now=$(date +%s)
elapsed=$((now - start))
echo "::error::Transient Nix retry exhausted for $task ($max attempts)"
write_summary failure "Transient Nix retry exhausted"
return 1
}
EOF
. "$__nix_gc_retry_helper"
rm -f "$__nix_gc_retry_helper"
run_nix_gc_race_retry 'devenv tasks run netlify:deploy --show-output --input type=pr --input pr=${{ github.event.pull_request.number }} --mode before' 'if [ -n "${NIX_CONFIG:-}" ]; then NIX_CONFIG_WITH_APPEND=$(printf '"'"'%s\n%s'"'"' "$NIX_CONFIG" '"'"'restrict-eval = false'"'"'); else NIX_CONFIG_WITH_APPEND='"'"'restrict-eval = false'"'"'; fi; NIX_CONFIG="$NIX_CONFIG_WITH_APPEND" PNPM_HOME="${PNPM_HOME:-${{ github.workspace }}/.pnpm-home}" PNPM_STORE_DIR="${PNPM_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" PNPM_CONFIG_STORE_DIR="${PNPM_CONFIG_STORE_DIR:-${{ runner.temp }}/pnpm-store/${{ github.job }}}" DT_PASSTHROUGH=1 "${DEVENV_BIN:?DEVENV_BIN not set}" tasks run netlify:deploy --show-output --input type=pr --input pr=${{ github.event.pull_request.number }} --mode before' 2>&1 | tee "$tmp_log"
fi
deploy_exit=${PIPESTATUS[0]}
if [ "$deploy_exit" -ne 0 ]; then exit "$deploy_exit"; fi
workflow_report_json=""
if [ -s "$workflow_report_path" ]; then
workflow_report_json="$(tail -n 1 "$workflow_report_path")"
fi
if [ -z "$workflow_report_json" ]; then
workflow_report_json=$(grep -F "WORKFLOW_REPORT_V1: " "$tmp_log" | sed 's/^.*WORKFLOW_REPORT_V1: //' | tail -n 1 || true)
fi
if [ -n "$workflow_report_json" ]; then
workflow_report_json="$(printf "%s" "$workflow_report_json" | jq -c '.')"
printf "%s\n" "$workflow_report_json" > "$workflow_report_path"
echo "WORKFLOW_REPORT_V1: $workflow_report_json"
printf 'workflow_report=%s\n' "$workflow_report_json" >> "$GITHUB_OUTPUT"
fi
echo "workflow_report_path=$workflow_report_path" >> "$GITHUB_OUTPUT"
- name: Collect workflow report bundle
shell: bash
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_REPORT_FLAKE_REF: "github:${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}/${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}#workflow-report"
WORKFLOW_REPORT_BUNDLE_ID: storybook-preview
WORKFLOW_REPORT_INPUT_PATHS_JSON: '["${{ steps.deploy.outputs.workflow_report_path }}"]'
WORKFLOW_REPORT_OUTPUT_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-bundle.json'
WORKFLOW_REPORT_RECORD_MARKER: 'WORKFLOW_REPORT_V1: '
WORKFLOW_REPORT_ALLOW_MISSING_INPUT: '1'
run: |
if [ -n "${GH_TOKEN:-}" ]; then
export NIX_CONFIG="${NIX_CONFIG:+$NIX_CONFIG$'\n'}access-tokens = github.com=${GH_TOKEN}"
fi
workflow_report_flake_ref="${WORKFLOW_REPORT_FLAKE_REF:-github:overengineeringstudio/effect-utils/main#workflow-report}"
nix run "$workflow_report_flake_ref" -- collect-bundle --bundle-id "$WORKFLOW_REPORT_BUNDLE_ID" --input-paths-json "$WORKFLOW_REPORT_INPUT_PATHS_JSON" --output-path "$WORKFLOW_REPORT_OUTPUT_PATH" --record-marker "$WORKFLOW_REPORT_RECORD_MARKER" --allow-missing-input
- name: Render workflow report comment
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
WORKFLOW_REPORT_FLAKE_REF: "github:${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}/${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}#workflow-report"
WORKFLOW_REPORT_BUNDLE_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-bundle.json'
WORKFLOW_REPORT_COMMENT_BODY_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-comment.md'
WORKFLOW_REPORT_SUMMARY_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-summary.md'
WORKFLOW_REPORT_TITLE: Storybook Previews
WORKFLOW_REPORT_NO_RECORDS_MESSAGE: No storybooks were deployed.
WORKFLOW_REPORT_STATE_ID: storybook-preview
WORKFLOW_REPORT_ENTRY_ID: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
WORKFLOW_REPORT_ENTRY_LABEL: ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || 'prod' }}
WORKFLOW_REPORT_CREATED_AT_UTC: ''
WORKFLOW_REPORT_TIME_ZONE: UTC
WORKFLOW_REPORT_MANAGED_MARKER: '<!-- workflow-report:managed -->'
run: |
if [ -n "${GH_TOKEN:-}" ]; then
export NIX_CONFIG="${NIX_CONFIG:+$NIX_CONFIG$'\n'}access-tokens = github.com=${GH_TOKEN}"
fi
comments_json="$(mktemp)"
if [ "${{ github.event_name }}" = "pull_request" ]; then
nix run nixpkgs#gh -- api "repos/$GH_REPO/issues/${{ github.event.pull_request.number }}/comments" --paginate > "$comments_json"
else
printf '[]' > "$comments_json"
fi
workflow_report_flake_ref="${WORKFLOW_REPORT_FLAKE_REF:-github:overengineeringstudio/effect-utils/main#workflow-report}"
nix run "$workflow_report_flake_ref" -- render-comment-body --bundle-path "$WORKFLOW_REPORT_BUNDLE_PATH" --comments-path "$comments_json" --comment-body-path "$WORKFLOW_REPORT_COMMENT_BODY_PATH" --summary-path "$WORKFLOW_REPORT_SUMMARY_PATH" --title "$WORKFLOW_REPORT_TITLE" --no-records-message "$WORKFLOW_REPORT_NO_RECORDS_MESSAGE" --state-id "$WORKFLOW_REPORT_STATE_ID" --entry-id "$WORKFLOW_REPORT_ENTRY_ID" --entry-label "$WORKFLOW_REPORT_ENTRY_LABEL" --created-at-utc "$WORKFLOW_REPORT_CREATED_AT_UTC" --time-zone "$WORKFLOW_REPORT_TIME_ZONE" --managed-marker "$WORKFLOW_REPORT_MANAGED_MARKER"
- name: Publish workflow report
if: always() && !cancelled()
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
WORKFLOW_REPORT_FLAKE_REF: "github:${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}/${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}#workflow-report"
WORKFLOW_REPORT_STATE_ID: storybook-preview
WORKFLOW_REPORT_COMMENT_BODY_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-comment.md'
WORKFLOW_REPORT_SUMMARY_PATH: '${{ runner.temp }}/workflow-reports/storybook-preview-summary.md'
WORKFLOW_REPORT_MANAGED_MARKER: '<!-- workflow-report:managed -->'
run: |
if [ -n "${GH_TOKEN:-}" ]; then
export NIX_CONFIG="${NIX_CONFIG:+$NIX_CONFIG$'\n'}access-tokens = github.com=${GH_TOKEN}"
fi
if [ -s "$WORKFLOW_REPORT_SUMMARY_PATH" ]; then
cat "$WORKFLOW_REPORT_SUMMARY_PATH" >> "$GITHUB_STEP_SUMMARY"
fi
if [ "${{ github.event_name }}" != "pull_request" ]; then
exit 0
fi
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "::notice::workflow report PR comment skipped for fork pull request; summary remains available in the job summary"
exit 0
fi
if [ ! -s "$WORKFLOW_REPORT_COMMENT_BODY_PATH" ]; then
echo "::notice::workflow report comment body is empty; skipping PR comment"
exit 0
fi
comments_json="$(mktemp)"
comment_id_file="$(mktemp)"
nix run nixpkgs#gh -- api "repos/$GH_REPO/issues/${{ github.event.pull_request.number }}/comments" --paginate > "$comments_json"
workflow_report_flake_ref="${WORKFLOW_REPORT_FLAKE_REF:-github:overengineeringstudio/effect-utils/main#workflow-report}"
nix run "$workflow_report_flake_ref" -- find-comment --comments-path "$comments_json" --comment-body-path "$WORKFLOW_REPORT_COMMENT_BODY_PATH" --comment-id-path "$comment_id_file" --state-id "$WORKFLOW_REPORT_STATE_ID" --managed-marker "$WORKFLOW_REPORT_MANAGED_MARKER"
comment_id="$(cat "$comment_id_file")"
if [ -n "$comment_id" ]; then
nix run nixpkgs#gh -- api \
--method PATCH \
"repos/$GH_REPO/issues/comments/$comment_id" \
--field body=@"$WORKFLOW_REPORT_COMMENT_BODY_PATH" >/dev/null
else
nix run nixpkgs#gh -- pr comment "${{ github.event.pull_request.number }}" --body-file "$WORKFLOW_REPORT_COMMENT_BODY_PATH"
fi
- name: Save pnpm state
if: ${{ success() && steps.restore-pnpm-state.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/.pnpm-home
${{ runner.temp }}/pnpm-store/${{ github.job }}
key: "pnpm-state-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}"
- name: Nix diagnostics summary
if: failure()
shell: bash
run: |
diag_dir="${NIX_STORE_DIAGNOSTICS_DIR:-}"
if [ -z "$diag_dir" ] || [ ! -d "$diag_dir" ]; then
echo "## Nix Store Diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No diagnostics directory found (validation may have failed before capture)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
{
echo "## Nix Store Diagnostics"
echo ""
echo "Temporary instrumentation for #272; remove after root cause is confirmed and CI is stable."
echo ""
echo "- Diagnostics directory: \`$diag_dir\`"
echo "- Tracking issue: https://github.com/overengineeringstudio/effect-utils/issues/272"
} >> "$GITHUB_STEP_SUMMARY"
markers_file="${RUNNER_TEMP:-/tmp}/nix-store-signature-markers.txt"
grep -R -n -E "config\\.cachix|cachix\\.package|error: path '/nix/store/.+ is not valid" --exclude="$(basename "$markers_file")" "$diag_dir" > "$markers_file" || true
if [ -s "$markers_file" ]; then
{
echo ""
echo "### Signature markers"
echo '```text'
head -n 120 "$markers_file"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
else
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- No signature markers found in captured diagnostics." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Nix diagnostics artifact
if: failure() && env.NIX_STORE_DIAGNOSTICS_DIR != ''
uses: actions/upload-artifact@v4
with:
name: 'nix-store-diagnostics-${{ github.job }}-${{ runner.os }}-run-${{ github.run_id }}-attempt-${{ github.run_attempt }}'
path: ${{ env.NIX_STORE_DIAGNOSTICS_DIR }}
if-no-files-found: ignore
retention-days: 14
- name: Failure note
if: failure()
shell: bash
run: |
echo "If this looks like Namespace runner Nix store corruption (e.g. \"... is not valid\", \"config.cachix\", \"cachix.package\"), add the run link + full nix-store output to:"
echo " https://github.com/overengineeringstudio/effect-utils/issues/201"
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-deploy-storybooks"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}
notify-alignment:
runs-on:
[namespace-profile-linux-x86-64, 'namespace-features:github.run-id=${{ github.run_id }}']
timeout-minutes: 30
needs:
- typecheck
- lint
- test
- nix-check
- nix-fod-check
- pnpm-builder-contract
- pnpm-regression
- bundle-smoke
- cargo
- deploy-storybooks
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
steps:
- name: Dispatch alignment to coordinator
env:
GH_TOKEN: ${{ secrets.MEGAREPO_ALIGNMENT_TOKEN }}
run: |
payload=$(printf '{"event_type":"upstream-changed","client_payload":{"source_repo":"%s","source_sha":"%s"}}' "${{ github.repository }}" "${{ github.sha }}")
curl --fail-with-body --silent --show-error --request POST \
--url "https://api.github.com/repos/schickling/megarepo-all/dispatches" \
--header "Accept: application/vnd.github+json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${GH_TOKEN}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
--data "$payload"
shell: bash
concurrency:
group: "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '' && format('measurement-baseline-{0}', inputs.measurement_baseline_ref) || (github.event_name == 'workflow_dispatch' && format('manual-run-{0}', github.run_id) || (github.event_name == 'pull_request' && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && format('label-{0}', github.event.label.name) || 'code')) }}-notify-alignment"
cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.measurement_baseline_ref != '') && (github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled')) }}