From 2dfaaecdfd3b112bc85d61c48bec10d75168e4cd Mon Sep 17 00:00:00 2001 From: Michael Buluma <1452922+buluma@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:09:51 +0300 Subject: [PATCH] Automate role index sync --- .github/workflows/update-roles.yml | 57 ++++++++++++++++++++++++++ README.md | 16 ++++++++ _data/ansible_roles.yml | 4 ++ scripts/update_roles.py | 66 ++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 .github/workflows/update-roles.yml diff --git a/.github/workflows/update-roles.yml b/.github/workflows/update-roles.yml new file mode 100644 index 0000000..2d0714a --- /dev/null +++ b/.github/workflows/update-roles.yml @@ -0,0 +1,57 @@ +name: Update Ansible roles + +on: + schedule: + - cron: "17 2 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: update-roles + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Install dependencies + run: python -m pip install PyYAML + + - name: Check pull request token + env: + ROLE_SYNC_TOKEN: ${{ secrets.ROLE_SYNC_TOKEN }} + run: | + if [ -z "$ROLE_SYNC_TOKEN" ]; then + echo "::error::Set the ROLE_SYNC_TOKEN repository secret before running this workflow." + exit 1 + fi + + - name: Sync roles from GitHub + run: python scripts/update_roles.py sync + env: + GITHUB_TOKEN: ${{ secrets.ROLE_SYNC_TOKEN }} + + - name: Open or update role list pull request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.ROLE_SYNC_TOKEN }} + branch: automation/update-ansible-roles + delete-branch: true + base: master + add-paths: _data/ansible_roles.yml + commit-message: Update Ansible role list + title: Update Ansible role list + body: | + Refresh `_data/ansible_roles.yml` from public `buluma/ansible-role-*` repositories. + Archived repositories are excluded. diff --git a/README.md b/README.md index 02416d7..a8d6a61 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,22 @@ python scripts/update_roles.py remove role_name python scripts/update_roles.py list ``` +### Syncing roles from GitHub + +```bash +python scripts/update_roles.py sync +``` + +This syncs public `buluma/ansible-role-*` repositories into the role list and +skips archived repositories. The **Update Ansible roles** workflow runs daily +or can be started manually from the Actions tab; it opens or updates a PR for +the generated change. + +The workflow needs a repository secret named `ROLE_SYNC_TOKEN`: a fine-grained +token with **Contents: read and write** and **Pull requests: read and write** +access to this repository. The token lets the generated PR trigger the normal +PR CI workflow. + The table is automatically regenerated when the site builds. No manual editing of `index.md` is needed for role changes. diff --git a/_data/ansible_roles.yml b/_data/ansible_roles.yml index b486437..1d2e865 100644 --- a/_data/ansible_roles.yml +++ b/_data/ansible_roles.yml @@ -54,6 +54,7 @@ - cron - crowd - crun +- cve_2024_3094 - daemonize - datadog - debug @@ -114,6 +115,7 @@ - kibana - kubectl - kubernetes +- lemp-stack - libvirt - local - locale @@ -171,6 +173,7 @@ - rclone - reboot - redis +- redmine-1 - remi - repo_epel - restore @@ -187,6 +190,7 @@ - sensu - service - setuptools +- skeleton - snmpd - solr - sonarqube diff --git a/scripts/update_roles.py b/scripts/update_roles.py index 254da48..18036b3 100755 --- a/scripts/update_roles.py +++ b/scripts/update_roles.py @@ -6,6 +6,7 @@ python scripts/update_roles.py add [ ...] python scripts/update_roles.py remove [ ...] python scripts/update_roles.py list + python scripts/update_roles.py sync python scripts/update_roles.py regenerate The role list is stored in _data/ansible_roles.yml and is used by index.md @@ -14,10 +15,19 @@ import sys import os +import json +import urllib.error +import urllib.request import yaml ROLES_FILE = "_data/ansible_roles.yml" REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +GITHUB_OWNER = "buluma" +ROLE_REPOSITORY_PREFIX = "ansible-role-" +ROLE_NAME_OVERRIDES = { + "digitalocean_agent": "digitalocean-agent", + "netiq_sentinel_syslog_event_source": "netiq-sentinel-syslog-event-source", +} def load_roles(): @@ -91,6 +101,59 @@ def cmd_list(): print(f" - {role}") +def cmd_sync(): + """Synchronize roles from non-archived public GitHub role repositories.""" + roles = set() + page = 1 + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + + while True: + url = ( + f"https://api.github.com/users/{GITHUB_OWNER}/repos" + f"?type=public&per_page=100&page={page}" + ) + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + if token: + request.add_header("Authorization", f"Bearer {token}") + + try: + with urllib.request.urlopen(request, timeout=30) as response: + repositories = json.load(response) + except urllib.error.HTTPError as error: + raise RuntimeError( + f"GitHub API request failed with HTTP {error.code} on page {page}" + ) from error + + if not isinstance(repositories, list): + raise RuntimeError("GitHub API returned an unexpected repository response") + if not repositories: + break + + for repository in repositories: + name = repository.get("name", "") + if repository.get("archived") or not name.startswith(ROLE_REPOSITORY_PREFIX): + continue + repository_role = name.removeprefix(ROLE_REPOSITORY_PREFIX) + roles.add(ROLE_NAME_OVERRIDES.get(repository_role, repository_role)) + + page += 1 + + if not roles: + raise RuntimeError( + f"No active {ROLE_REPOSITORY_PREFIX} repositories found for {GITHUB_OWNER}; " + "refusing to replace the current role list" + ) + + save_roles(roles) + print(f"Synchronized roles from {GITHUB_OWNER}'s active public repositories") + + def cmd_regenerate(): """ Regenerate ansible_roles.yml from index.md Liquid template output. @@ -169,6 +232,9 @@ def main(): elif command == "list": cmd_list() + elif command == "sync": + cmd_sync() + elif command == "regenerate": cmd_regenerate()