Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions .github/workflows/update-maintainers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
name: Update maintainers list

on:
push:
branches:
- main
paths:
- lib/maintainers.nix
schedule:
# Update every Monday at 9 AM UTC
- cron: "0 9 * * 1"
workflow_dispatch:
inputs:
create_pr:
description: "Create PR even if no changes"
required: false
default: false
type: boolean

jobs:
update-maintainers:
runs-on: ubuntu-latest
if: github.repository_owner == 'nix-community' || github.event_name == 'workflow_dispatch'
# Permissions required for workflow
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this makes it sound like this is the canonical place where the permissions are defined. However we actually have them in two places, and this is the less important of the two since it is only used on forks that don't have an APP_ID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this would be less important? These are required for a token to get generated with permissions required, the others are just to limit the scope of permissions granted to an app.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dictate the permissions of github.token and are entirely unrelated to the app token.

I said they're less important because github.token is only used when there's no app token.

Copy link
Copy Markdown
Contributor Author

@khaneliman khaneliman Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dictate the permissions of github.token and are entirely unrelated to the app token.

the others are just to limit the scope of permissions granted to an app.

Understood, as part of previous response.

Guess its differing opinion on what's important. I think declaring the app token permissions as less because they are optional for functionality. But, the app token permissions are just a couple lines lower with the exact same naming convention and didn't think it warranted copy/paste for same explanation.

# `contents`: to update maintainers file
# `pull-requests`: to create pr
# `issues`: to label pr
permissions:
contents: write
pull-requests: write
issues: write
env:
pr_branch: update/maintainers-${{ github.ref_name }}
steps:
- name: Create GitHub App token
uses: actions/create-github-app-token@v2
if: vars.CI_APP_ID
id: app-token
with:
app-id: ${{ vars.CI_APP_ID }}
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
permission-issues: write

- name: Get GitHub App user info
id: user-info
if: vars.CI_APP_ID
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
slug: ${{ steps.app-token.outputs.app-slug }}
run: |
name="$slug[bot]"
id=$(gh api "/users/$name" --jq .id)
{
echo "id=$id"
echo "name=$name"
echo "email=$id+$name@users.noreply.github.com"
} >> "$GITHUB_OUTPUT"

- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token || github.token }}

- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ steps.app-token.outputs.token || github.token }}
extra_nix_config: |
allow-import-from-derivation = false

- name: Setup Git
env:
name: ${{ steps.user-info.outputs.name || 'github-actions[bot]' }}
email: ${{ steps.user-info.outputs.email || '41898282+github-actions[bot]@users.noreply.github.com' }}
run: |
git config user.name "$name"
git config user.email "$email"

- name: Generate updated maintainers list
run: |
echo "::group::📋 Generating updated generated/all-maintainers.nix..."
nix run .#generate-all-maintainers -- --root . --output generated/all-maintainers.nix
echo "::endgroup::"
echo "::group::🎨 Formatting with nixfmt..."
nix fmt generated/all-maintainers.nix
echo "::endgroup::"

- name: Check for changes
id: check-changes
run: |
if git diff --quiet generated/all-maintainers.nix; then
echo "No changes to generated/all-maintainers.nix"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected in generated/all-maintainers.nix"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
added=$(git diff --numstat generated/all-maintainers.nix | cut -f1)
removed=$(git diff --numstat generated/all-maintainers.nix | cut -f2)
echo "changes_summary=+$added -$removed lines" >> "$GITHUB_OUTPUT"
fi

- name: Validate generated file
if: steps.check-changes.outputs.has_changes == 'true'
run: |
echo "🔍 Validating generated generated/all-maintainers.nix..."
if nix-instantiate --eval generated/all-maintainers.nix --strict > /dev/null; then
echo "✅ Generated file has valid Nix syntax"
else
echo "❌ Generated file has invalid Nix syntax"
exit 1
fi

- name: Create update branch
run: |
git branch -D "$pr_branch" || echo "Nothing to delete"
git switch -c "$pr_branch"

- name: Get info on the current PR
id: open_pr_info
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
run: |
# Query for info about the already open update PR
info=$(
gh api graphql -F owner='{owner}' -F repo='{repo}' -F branch="$pr_branch" -f query='
query($owner:String!, $repo:String!, $branch:String!) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 1, states: OPEN, headRefName: $branch) {
nodes {
number
url
}
}
}
}
' | jq --raw-output '
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI gh api also has a --jq flag:

Suggested change
' | jq --raw-output '
' --jq '

Sometimes it's still worth it to pipe to jq though, e.g. if you want to use --exit-status to trigger a failure if the jq query doesn't match the input JSON.


We use a similar graphql query elsewhere, but one issue I'm now spotting is that headRefName could also match PRs on forks that use the same branch name... I don't see an obvious quick fix in the graphql explorer, and the issue applies to both places, so this is fine for now.

.data.repository.pullRequests.nodes[]
| to_entries[]
| "\(.key)=\(.value)"
'
)
if [[ -n "$info" ]]; then
echo "PR info:"
echo "$info"
echo "$info" >> $GITHUB_OUTPUT
else
echo "No PR is currently open"
fi

- name: Fetch current PR's branch
if: steps.open_pr_info.outputs.number
run: |
git fetch origin "$pr_branch"
git branch --set-upstream-to "origin/$pr_branch"

- name: Create Pull Request
id: create-pr
if: steps.check-changes.outputs.has_changes == 'true' || github.event.inputs.create_pr == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
title: "maintainers: update generated/all-maintainers.nix"
commit_body: |
Automated update of the master maintainers list combining:
- Nixvim specific maintainers from lib/maintainers.nix
- Nixpkgs maintainers referenced in Nixvim modules

Changes: ${{ steps.check-changes.outputs.changes_summary || 'No content changes' }}

Generated by: flake/dev/generate-all-maintainers/generate-all-maintainers.py
pr_url: ${{ steps.open_pr_info.outputs.url }}
pr_num: ${{ steps.open_pr_info.outputs.number }}
pr_body: |
## 📋 Summary

This PR updates the master maintainers list (`generated/all-maintainers.nix`) which combines:
- **Nixvim specific maintainers** from `lib/maintainers.nix`
- **Nixpkgs maintainers** referenced in Nixvim modules

## 🔄 Changes

**Statistics:** ${{ steps.check-changes.outputs.changes_summary || 'No content changes (format/comment updates only)' }}

The updated list includes all maintainers needed for review assignments across the Nixvim project.

## 🤖 Automation

- **Generated by:** `flake/dev/generate-all-maintainers/generate-all-maintainers.py`
- **Trigger:** ${{ github.event_name == 'schedule' && 'Scheduled weekly update' || 'Manual workflow dispatch' }}
- **Validation:** File syntax verified with `nix-instantiate --eval`

## 📚 Usage

This file can be imported and used for maintainer lookups:
```nix
let allMaintainers = import ./generated/all-maintainers.nix; in
# Access any maintainer by name: allMaintainers.username
```

---
🤖 *This PR was automatically created by the [update-maintainers workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*
run: |
git add generated/all-maintainers.nix
git commit -m "$title" -m "$commit_body"

echo "Pushing to remote branch $pr_branch"
git push --force --set-upstream origin "$pr_branch"

if [ -z "$pr_num" ]; then
echo "Creating new pull request."
PR_URL=$(
gh pr create \
--title "$title" \
--body "$pr_body"
)
else
PR_URL=$pr_url
echo "Pull request already exists: $PR_URL"
gh pr edit "$pr_num" --body "$pr_body"
fi

echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"

- name: Summary
env:
has_changes: ${{ steps.check-changes.outputs.has_changes }}
changes: ${{ steps.check-changes.outputs.changes_summary }}
pr_url: ${{ steps.create-pr.outputs.pr_url}}
pr_num: ${{ steps.open_pr_info.outputs.number }}
run: |
if [[ "$has_changes" == "true" ]]; then
if [[ -n "$pr_num" ]]; then
echo "✅ Successfully updated PR with new changes."
echo "$changes"
echo "🔗 PR URL: $pr_url"
echo "### ✅ PR Updated" >> $GITHUB_STEP_SUMMARY
echo "[$pr_url]($pr_url)" >> $GITHUB_STEP_SUMMARY
elif [[ -n "$pr_url" ]]; then
echo "✅ Successfully created PR with maintainer updates."
echo "$changes"
echo "🔗 PR URL: $pr_url"
echo "### ✅ PR Created" >> $GITHUB_STEP_SUMMARY
echo "[$pr_url]($pr_url)" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Failed to create or update pull request."
echo "### ❌ PR Operation Failed" >> $GITHUB_STEP_SUMMARY
echo "A pull request was intended but the URL was not captured. Please check the logs." >> $GITHUB_STEP_SUMMARY
fi
else
echo "ℹ️ No changes detected - maintainers list is up to date."
echo "### ℹ️ No Changes" >> $GITHUB_STEP_SUMMARY
echo "The maintainers list is up-to-date. No PR was created." >> $GITHUB_STEP_SUMMARY
fi
1 change: 1 addition & 0 deletions flake/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
packages = lib.optionalAttrs (partitionStack == [ ]) {
# Propagate `packages` from the `dev` partition:
inherit (config.partitions.dev.module.flake.packages.${system})
generate-all-maintainers
list-plugins
;
};
Expand Down
1 change: 1 addition & 0 deletions flake/dev/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
imports = [
./devshell.nix
./generate-all-maintainers
./list-plugins
./package-tests.nix
./template-tests.nix
Expand Down
42 changes: 42 additions & 0 deletions flake/dev/generate-all-maintainers/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{ self, ... }:
{
perSystem =
{
lib,
pkgs,
...
}:
let
package = pkgs.writers.writePython3Bin "generate-all-maintainers" {
# Disable flake8 checks that are incompatible with the ruff ones
flakeIgnore = [
# Thinks shebang is a block comment
"E265"
# line too long
"E501"
# line break before binary operator
"W503"
];
} (builtins.readFile ./generate-all-maintainers.py);
in
{
packages.generate-all-maintainers = package;

checks.generate-all-maintainers-test =
pkgs.runCommand "generate-all-maintainers-test"
{
nativeBuildInputs = [ package ];
}
''
generate-all-maintainers --root ${self} --output $out
'';

devshells.default.commands = [
{
name = "generate-all-maintainers";
command = ''${lib.getExe package} "$@"'';
help = "Generate a single nix file with all `nixvim` and `nixpkgs` maintainer entries";
}
];
};
}
74 changes: 74 additions & 0 deletions flake/dev/generate-all-maintainers/extract-maintainers-meta.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Extract maintainers from nixvim configuration using meta.maintainers
# This script evaluates an empty nixvimConfiguration and extracts the merged maintainer information
let
nixvim = import ../../..;
lib = nixvim.inputs.nixpkgs.lib.extend nixvim.lib.overlay;

emptyConfig = lib.nixvim.evalNixvim {
modules = [ { _module.check = false; } ];
extraSpecialArgs = {
pkgs = null;
};
};

inherit (emptyConfig.config.meta) maintainers;

extractMaintainerObjects =
maintainerData:
lib.pipe maintainerData [
lib.attrValues
lib.concatLists
lib.unique
];

allMaintainerObjects = extractMaintainerObjects maintainers;

allMaintainerNames = lib.filter (name: name != null) (
map (maintainer: maintainer.github) allMaintainerObjects
);

nixvimMaintainers = import ../../../lib/maintainers.nix;
nixvimMaintainerNames = lib.attrNames nixvimMaintainers;
partitionedMaintainers = lib.partition (nameValue: lib.elem nameValue.name nixvimMaintainerNames) (
lib.attrsToList maintainerDetails
);

maintainerDetails = lib.pipe allMaintainerObjects [
(map (obj: {
name = obj.github;
value = obj // {
source =
if categorizedMaintainers.nixvim ? ${obj.github} then
"nixvim"
else if categorizedMaintainers.nixpkgs ? ${obj.github} then
"nixpkgs"
else
throw "${obj.github} is neither a nixvim or nixpkgs maintainer";
};
}))
lib.listToAttrs
];

categorizedMaintainers = {
nixvim = lib.listToAttrs partitionedMaintainers.right;
nixpkgs = lib.listToAttrs partitionedMaintainers.wrong;
};

formattedMaintainers = lib.generators.toPretty {
multiline = true;
indent = "";
} maintainerDetails;
in
{
raw = maintainers;
names = allMaintainerNames;
details = maintainerDetails;
categorized = categorizedMaintainers;
formatted = formattedMaintainers;

stats = {
totalMaintainers = lib.length allMaintainerNames;
nixvimMaintainers = lib.length (lib.attrNames categorizedMaintainers.nixvim);
nixpkgsMaintainers = lib.length (lib.attrNames categorizedMaintainers.nixpkgs);
};
}
Loading