Skip to content

Maintenance

Maintenance #39

Workflow file for this run

# Canonical Maintenance workflow — Nix Packaging Standard.
# Source of truth: github:Daaboulex/nix-packaging-standard. Synced into each
# packaging repo by sync.sh; byte-identity enforced by the `std-conformance`
# flake check. flake.lock refresh: rebuild, push only if green, else open a
# labeled issue; plus stale-branch cleanup. Cadence is per-repo via the
# MAINTENANCE_CADENCE repo variable (daily | biweekly | weekly); unset = weekly.
name: Maintenance
on:
schedule:
- cron: '0 4 * * *' # Daily 4 AM UTC; the gate job applies the per-repo cadence
workflow_dispatch:
permissions:
contents: write
issues: write
jobs:
# Per-repo cadence gate. Set the MAINTENANCE_CADENCE repo variable to
# `daily` or `biweekly` (Wed+Sun) for fast-moving / closure-heavy repos;
# unset or any other value means `weekly` (Sun) — identical to the old
# Sunday-only schedule. workflow_dispatch always runs.
gate:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.c.outputs.run }}
steps:
- id: c
env:
CADENCE: ${{ vars.MAINTENANCE_CADENCE || 'weekly' }}
EVENT: ${{ github.event_name }}
run: |
dow=$(date -u +%u) # 1=Mon .. 7=Sun
run=no
case "$CADENCE" in
daily) run=yes ;;
biweekly) if [ "$dow" = 3 ] || [ "$dow" = 7 ]; then run=yes; fi ;;
*) if [ "$dow" = 7 ]; then run=yes; fi ;; # weekly (default)
esac
if [ "$EVENT" = workflow_dispatch ]; then run=yes; fi
echo "cadence=$CADENCE dow=$dow event=$EVENT -> run=$run"
echo "run=$run" >> "$GITHUB_OUTPUT"
update-lock:
needs: gate
if: needs.gate.outputs.run == 'yes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 (node24)
- name: Update flake.lock
run: nix flake update
- name: Verify build
id: build
run: |
set +e
SYS=$(nix eval --impure --raw --expr 'builtins.currentSystem')
# Build every output the flake declares for this system — the SAME
# target ci.yml uses (.#checks aliases each package plus the std
# checks). Bare `nix build` assumes .#default, which module-only repos
# (a NixOS/HM module, no package) do not have — that mismatch filed
# spurious "broke build" maintenance issues on every run.
if ! nix eval ".#checks.$SYS" --apply 'x: builtins.attrNames x != [ ]' 2>/dev/null | grep -qx true; then
echo "Flake declares no checks for $SYS — lock bump verified by eval only."
echo "exit_code=0" >> "$GITHUB_OUTPUT"
exit 0
fi
build() {
nix run nixpkgs#nix-fast-build -- --skip-cached --no-nom --flake ".#checks.$SYS" 2>&1 | tee /tmp/lock-build.log
return "${PIPESTATUS[0]}" # nix-fast-build's real exit, NOT tee's (always 0).
}
build
code=$?
# Retry once on a transient external-fetch failure (crates.io rate-limit,
# registry/CDN blip) so maintenance doesn't open a spurious "broke build"
# issue for an infra hiccup that has nothing to do with the lock bump.
if [ "$code" -ne 0 ] && grep -qiE 'crates\.io|error: cannot download|status code: (403|429)|curl: \(|couldn.t resolve host|connection reset by peer|temporary failure in name resolution|operation timed out' /tmp/lock-build.log; then
echo "::warning::Transient external-fetch failure during maintenance build; retrying once in 60s."
sleep 60
build
code=$?
fi
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
- name: Push if build passes
if: steps.build.outputs.exit_code == '0'
run: |
if git diff --quiet flake.lock; then
echo "No changes to flake.lock"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "chore: update flake.lock"
git push
- name: Issue if build fails
if: steps.build.outputs.exit_code != '0'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 (node24)
with:
script: |
const fs = require('fs');
const log = fs.readFileSync('/tmp/lock-build.log', 'utf8');
const lastLines = log.split('\n').slice(-80).join('\n');
const title = 'Maintenance: flake.lock update broke build';
// Close any prior open report of the same failure before filing a
// fresh one, so a recurring lock-build break does not pile up
// duplicate issues (mirrors update.yml's update-failed dedup).
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner, repo: context.repo.repo,
labels: 'maintenance', state: 'open'
});
for (const issue of existing.data) {
if (issue.title !== title) continue;
await github.rest.issues.update({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issue.number, state: 'closed',
state_reason: 'not_planned'
});
}
await github.rest.issues.create({
owner: context.repo.owner, repo: context.repo.repo,
title,
labels: ['maintenance'],
body: [
'## flake.lock update failed to build',
'',
'The scheduled `nix flake update` produced a flake.lock that does not build.',
'**flake.lock was NOT pushed.**',
'',
'<details><summary>Build log (last 80 lines)</summary>',
'',
'```',
lastLines,
'```',
'</details>',
'',
'### Recovery',
'```bash',
'nix flake update',
'nix build --no-link # investigate failure',
'```',
'',
'---',
'*Created by [maintenance workflow](.github/workflows/maintenance.yml)*'
].join('\n')
});
cleanup:
needs: gate
if: needs.gate.outputs.run == 'yes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Cleanup stale branches
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 (node24)
with:
script: |
// Delete stale maintenance branches older than 30 days.
// Matches update/* and upstream-update/* (both _ and / separators).
const { data: allRefs } = await github.rest.git.listMatchingRefs({
owner: context.repo.owner, repo: context.repo.repo,
ref: 'heads/'
});
const stalePatterns = [
/^refs\/heads\/update[\/_]/,
/^refs\/heads\/upstream-update[\/_]/,
];
const candidates = allRefs.filter(r => stalePatterns.some(p => p.test(r.ref)));
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
for (const ref of candidates) {
try {
const { data: commit } = await github.rest.git.getCommit({
owner: context.repo.owner, repo: context.repo.repo,
commit_sha: ref.object.sha
});
if (new Date(commit.author.date).getTime() < thirtyDaysAgo) {
console.log(`Deleting stale branch: ${ref.ref}`);
await github.rest.git.deleteRef({
owner: context.repo.owner, repo: context.repo.repo,
ref: ref.ref.replace('refs/', '')
});
}
} catch (e) {
console.log(`Skip ${ref.ref}: ${e.message}`);
}
}