Separate user identity (data) from the reusable modules, and let a host declare any number of users instead of exactly one. - users/registry.nix: per-user identity (name, email, groups, authorized and signing keys) as the single source of identity; no user data is hardcoded in the modules. - mkHost takes a `users` set keyed by username; per-user identity is injected into each home config via the `identity` module arg (extraSpecialArgs is per-host, so it cannot carry per-user data). - modules/users.nix builds accounts from the registry; modules/ssh.nix no longer defines authorized keys (the registry owns them); home/git.nix and home/desktop.nix read `identity`; users/emmathorpe/work.nix drops its now-redundant git identity override. - Restructure the tree: users/, home/, modules/, hosts/, lib/ replace the former lyrathorpe/ and system/ layout. - Add standalone homeConfigurations (the portable subset: shell, git, editor, claude) and an exported homeModules output for use on machines not managed by this flake, or as an input to other flakes. Behaviour-preserving for existing hosts: lyrathorpe-mbp and emmathorpe-edaas evaluate to identical derivations; lyrathorpe-t400, lyrathorpe-macpro31 and lyrathorpe-rpi5 differ only by de-duplicating a repeated authorized_keys entry. Fixes the SSH authorized-key leak (one user's key was applied to every account), the hardcoded default git identity, and the hardcoded EDaaS linger setting.
165 lines
9.6 KiB
Nix
165 lines
9.6 KiB
Nix
# Daily automated review and triage of Renovate dependency PRs awaiting Emma's
|
|
# review.
|
|
#
|
|
# Host-scoped: imported only from work.nix (the EDaaS/WSL host), so the timer
|
|
# exists on this machine alone. A systemd *user* timer runs Claude Code headless
|
|
# once a day; it queries GitHub via the project-scoped github MCP server and
|
|
# writes a risk-graded summary to the journal (read with
|
|
# `journalctl --user -u renovate-review`).
|
|
#
|
|
# Triage policy:
|
|
# * PRs that are clearly low risk (patch/minor bumps to tooling, infra, test
|
|
# or framework libs; symmetric diff; CI green; no application logic) AND not
|
|
# already approved are AUTO-APPROVED headlessly. These repos enable Renovate
|
|
# automerge, so an approval lets the PR merge itself with no human in the
|
|
# loop. This is intentional and was explicitly requested.
|
|
# * Everything else (medium/high risk, failing/pending CI, stale branches,
|
|
# anything touching application logic or needing judgement) is left
|
|
# untouched and surfaced to Emma.
|
|
#
|
|
# The run records two state files under $XDG_STATE_HOME/renovate-review for the
|
|
# once-a-day interactive-shell reminder defined below (programs.zsh.initContent):
|
|
# `last-run` (date of the last successful run) and `needs-review.txt` (the PRs
|
|
# that need Emma's eyes).
|
|
#
|
|
# Caveats (the foundation this stands on, none of it owned by this flake):
|
|
# * Auth is Vertex AI via gcloud Application Default Credentials
|
|
# (~/.config/gcloud/application_default_credentials.json). When that token
|
|
# can no longer refresh the run fails; re-auth with `gcloud auth login`.
|
|
# * The Vertex project, region and model are hardcoded below, copied from the
|
|
# interactive environment (the corporate launcher injects them; they live in
|
|
# no config file). If IT changes them, update them here. Claude Code handles
|
|
# its own network egress, so no proxy is set.
|
|
# * The github MCP server is defined in ~/code/.mcp.json, so the job runs with
|
|
# that directory as its working directory.
|
|
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
# The review instructions handed to headless Claude: queue -> drop archived
|
|
# repos -> grade risk -> auto-approve the clearly-safe ones, surface the rest.
|
|
reviewPrompt = ''
|
|
Daily Renovate PR review and triage for Emma-Thorpe_citrix.
|
|
|
|
1. github MCP search_pull_requests, query: `is:open is:pr review-requested:Emma-Thorpe_citrix author:app/jenkins-stf-jm` (jenkins-stf-jm[bot] is this org's Renovate bot), perPage 50.
|
|
2. Build the archived-repo exclusion set: github MCP search_repositories with query `org:csg-citrix-storefront archived:true`, perPage 100, paginate all pages (~128). Collect each archived repo full_name. Do NOT use the `archived:false` qualifier on the PR search itself; it is mis-indexed and returns zero. Filter by the repo set instead.
|
|
3. Drop any PR whose repository is in the archived set (e.g. csg-citrix-storefront/traefik-fips is archived; a PR to an archived repo cannot merge and is noise).
|
|
4. For each remaining PR: pull_request_read method=get (diff size, mergeable_state, labels, age), method=get_status (CI), and method=get_reviews (existing approvals). Read the body's dependency table for what is bumped.
|
|
5. Grade risk Low / Medium / High. LOW means ALL of: only patch or minor version bumps; the packages are tooling, observability, infrastructure, test, or framework/runtime libraries (not business logic); the diff is small and symmetric (version strings / lockfiles only); CI is passing; nothing security-policy-loosening. Anything that is a major bump, touches application logic, has failing or pending CI, is a stale branch needing rebase, or that you are not confident about is NOT Low.
|
|
6. AUTO-APPROVE the safe ones: for every PR that is Low risk AND has passing CI AND is not already approved by Emma-Thorpe_citrix, submit an approving review with pull_request_review_write (method=create, event=APPROVE, body: a one-line note that this is an automated approval of a low-risk dependency update). Approve only these. NEVER call merge. NEVER approve a Medium/High PR or one you are unsure about. (Note: these repos automerge on approval, so approval effectively merges it.)
|
|
7. Leave for Emma, without approving: every Medium/High risk PR, anything with failing or pending CI, stale branches, and anything needing human judgement.
|
|
8. Print a markdown table (PR linked, repo, change summary, size, CI, risk, action: Auto-approved / Needs review / Held) and terse notes. State how many PRs were excluded as archived.
|
|
9. As the FINAL lines of your output, emit machine-readable triage lines, one per PR, with these EXACT prefixes and nothing else on the line:
|
|
- For each PR you auto-approved: APPROVED> owner/repo#NUMBER short title
|
|
- For each PR that needs Emma's review: NEEDS> owner/repo#NUMBER (Risk) one-line reason — https://github.com/owner/repo/pull/NUMBER
|
|
If no PR needs Emma's review, emit no NEEDS> lines at all.
|
|
If the post-filter search returns zero PRs, say so in one line and emit no NEEDS> lines.
|
|
'';
|
|
|
|
# Hold the prompt in its own store file rather than inline, so its literal
|
|
# backticks and `$` don't trip shellcheck (SC2016) in the wrapper below.
|
|
promptFile = pkgs.writeText "renovate-review-prompt.md" reviewPrompt;
|
|
|
|
# Tools the headless run is permitted to use without interactive prompts.
|
|
# Read-only github MCP calls, plus review_write so it can submit APPROVE
|
|
# reviews on low-risk PRs. Deliberately NOT included: any merge tool.
|
|
allowedTools = lib.concatStringsSep "," [
|
|
"mcp__github-mcp__search_pull_requests"
|
|
"mcp__github-mcp__search_repositories"
|
|
"mcp__github-mcp__pull_request_read"
|
|
"mcp__github-mcp__pull_request_review_write"
|
|
];
|
|
|
|
# Where the run records state for the interactive-shell reminder.
|
|
stateDir = "$HOME/.local/state/renovate-review";
|
|
|
|
renovate-review = pkgs.writeShellApplication {
|
|
name = "renovate-review";
|
|
runtimeInputs = [ config.programs.claude-code.package ];
|
|
text = ''
|
|
# The github MCP server is project-scoped to ~/code; run from there.
|
|
cd "$HOME/code"
|
|
|
|
# Claude Code auth + endpoint: Vertex AI. These are injected into the
|
|
# interactive shell by the corporate launcher (not present in any config
|
|
# file), so a systemd-spawned process must set them explicitly. Do NOT set
|
|
# HTTP(S)_PROXY: Claude Code self-provisions its own network egress to
|
|
# Vertex; forcing a proxy here points it at a per-session socket that does
|
|
# not exist outside an interactive launch and breaks connectivity.
|
|
export CLAUDE_CODE_USE_VERTEX=1
|
|
export ANTHROPIC_VERTEX_PROJECT_ID=claude-code-citrix
|
|
export CLOUD_ML_REGION=global
|
|
export ANTHROPIC_MODEL='claude-opus-4-8[1m]'
|
|
|
|
# Capture the run so we can both log it (journal) and persist the triage
|
|
# for the shell reminder. If claude exits non-zero, errexit aborts here and
|
|
# the state files are left stale, so the reminder will flag a missed run.
|
|
out="$(claude -p "$(cat ${promptFile})" \
|
|
--allowedTools ${lib.escapeShellArg allowedTools} \
|
|
--output-format text)"
|
|
|
|
printf '%s\n' "$out"
|
|
|
|
# Persist state for programs.zsh.initContent's daily reminder. needs-review
|
|
# gets the PRs Claude flagged for Emma (the NEEDS> lines, prefix stripped);
|
|
# it is empty when nothing needs her attention. grep || true: no matches is
|
|
# the all-clear case, not an error.
|
|
mkdir -p "${stateDir}"
|
|
printf '%s\n' "$out" | grep '^NEEDS> ' | sed 's/^NEEDS> //' > "${stateDir}/needs-review.txt" || true
|
|
date +%F > "${stateDir}/last-run"
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
systemd.user.services.renovate-review = {
|
|
Unit.Description = "Daily Renovate PR review (headless Claude Code)";
|
|
Service = {
|
|
Type = "oneshot";
|
|
ExecStart = lib.getExe renovate-review;
|
|
};
|
|
};
|
|
|
|
systemd.user.timers.renovate-review = {
|
|
Unit.Description = "Schedule the daily Renovate PR review";
|
|
Timer = {
|
|
OnCalendar = "*-*-* 08:47:00";
|
|
# Run on next boot if the machine was off at the scheduled time.
|
|
Persistent = true;
|
|
# Avoid firing exactly on the minute boundary.
|
|
RandomizedDelaySec = "5m";
|
|
};
|
|
Install.WantedBy = [ "timers.target" ];
|
|
};
|
|
|
|
# Interactive-shell reminder: nudge once per calendar day about the daily
|
|
# Renovate timer -- whether it actually ran, and any PRs that need Emma's eyes
|
|
# (the auto-approved ones need no nudge). Throttled via a `reminded-on` marker
|
|
# so it prints in the first shell/tmux pane of the day, not every pane. mkOrder
|
|
# 1600 runs after shell.nix's tmux re-exec (order 200), so it fires inside the
|
|
# tmux pane where Emma actually reads it.
|
|
programs.zsh.initContent = lib.mkOrder 1600 ''
|
|
if [[ $- == *i* ]]; then
|
|
__rr_dir="$HOME/.local/state/renovate-review"
|
|
__rr_today=$(date +%F)
|
|
if [[ "$(cat "$__rr_dir/reminded-on" 2>/dev/null)" != "$__rr_today" ]]; then
|
|
__rr_last=$(cat "$__rr_dir/last-run" 2>/dev/null)
|
|
if [[ "$__rr_last" != "$__rr_today" ]]; then
|
|
print -P "%F{yellow}renovate:%f last review ''${__rr_last:-never} (not today) -- check: systemctl --user status renovate-review"
|
|
fi
|
|
if [[ -s "$__rr_dir/needs-review.txt" ]]; then
|
|
print -P "%F{red}renovate:%f $(grep -c . "$__rr_dir/needs-review.txt") PR(s) need your review:"
|
|
sed 's/^/ - /' "$__rr_dir/needs-review.txt"
|
|
print -P " -> journalctl --user -u renovate-review -e"
|
|
elif [[ "$__rr_last" == "$__rr_today" ]]; then
|
|
print -P "%F{green}renovate:%f reviewed today -- low-risk auto-approved, nothing for you."
|
|
fi
|
|
mkdir -p "$__rr_dir" && print -r -- "$__rr_today" > "$__rr_dir/reminded-on"
|
|
fi
|
|
unset __rr_dir __rr_today __rr_last
|
|
fi
|
|
'';
|
|
}
|