From 698e1ca203df02267ee6cfa8b55793d8ba8e36f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Sj=C3=B6gren?= Date: Mon, 16 Mar 2026 20:14:08 +0100 Subject: [PATCH] github_secrets_info: new module (#11586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * github_secrets_info: new module Signed-off-by: Thomas Sjögren * clean tests Signed-off-by: Thomas Sjögren * remove pynacl dep Signed-off-by: Thomas Sjögren * fqcn Signed-off-by: Thomas Sjögren * remove excess output Signed-off-by: Thomas Sjögren * just return result as sample Signed-off-by: Thomas Sjögren * only print secrets, adapt tests Signed-off-by: Thomas Sjögren * Update plugins/modules/github_secrets_info.py Co-authored-by: Felix Fontein * Update plugins/modules/github_secrets_info.py Co-authored-by: Felix Fontein * Update plugins/modules/github_secrets_info.py Co-authored-by: Felix Fontein * t is for typing, and typing is what we did Signed-off-by: Thomas Sjögren * add info_module attributes Signed-off-by: Thomas Sjögren --------- Signed-off-by: Thomas Sjögren Co-authored-by: Felix Fontein (cherry picked from commit df9b30448a993c425b6ded0ee8bcf1bdabfb57ac) --- .github/BOTMETA.yml | 2 + plugins/modules/github_secrets_info.py | 158 ++++++++++++++++++ .../modules/test_github_secrets_info.py | 98 +++++++++++ 3 files changed, 258 insertions(+) create mode 100644 plugins/modules/github_secrets_info.py create mode 100644 tests/unit/plugins/modules/test_github_secrets_info.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index c4e8bf2e60..88c4168864 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -644,6 +644,8 @@ files: maintainers: atorrescogollo $modules/github_secrets.py: maintainers: konstruktoid + $modules/github_secrets_info.py: + maintainers: konstruktoid $modules/gitlab_: keywords: gitlab source_control maintainers: $team_gitlab diff --git a/plugins/modules/github_secrets_info.py b/plugins/modules/github_secrets_info.py new file mode 100644 index 0000000000..ace8c4d4c7 --- /dev/null +++ b/plugins/modules/github_secrets_info.py @@ -0,0 +1,158 @@ +#!/usr/bin/python + +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +DOCUMENTATION = r""" +module: github_secrets_info +short_description: List GitHub repository or organization secrets +description: + - List secrets in a GitHub repository or organization. +author: + - Thomas Sjögren (@konstruktoid) +version_added: '12.5.0' +extends_documentation_fragment: + - community.general.attributes + - community.general.attributes.info_module +options: + organization: + description: + - The GitHub username or organization name. + type: str + required: true + aliases: ["org", "username"] + repository: + description: + - The name of the repository. + - If not provided, the listing will be at organization level. + type: str + aliases: ["repo"] + api_url: + description: + - The base URL for the GitHub API. + type: str + default: "https://api.github.com" + token: + description: + - The GitHub token used for authentication. + type: str + required: true +""" + +EXAMPLES = r""" +- name: List Github secret + community.general.github_secrets_info: + token: "{{ lookup('ansible.builtin.env', 'GITHUB_TOKEN') }}" + repository: "ansible" + organization: "ansible" +""" + +RETURN = r""" +secrets: + description: The list of currently existing secrets. + type: list + elements: dict + returned: success + sample: [ + { + "created_at": "2026-01-11T23:19:00Z", + "name": "ANSIBLE", + "updated_at": "2026-02-15T22:18:16Z" + }, + ] + contains: + name: + description: The name of the secret. + type: str + created_at: + description: The date and time when the secret was created. + type: str + updated_at: + description: The date and time when the secret was last updated. + type: str +""" + +import json +import typing as t +from http import HTTPStatus + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + +from ansible_collections.community.general.plugins.module_utils import deps + + +def list_secrets( + module: AnsibleModule, + api_url: str, + headers: dict[str, str], + organization: str, + repository: str, +) -> dict[str, list]: + url = ( + f"{api_url}/repos/{organization}/{repository}/actions/secrets" + if repository + else f"{api_url}/orgs/{organization}/actions/secrets" + ) + + resp, info = fetch_url(module, url, headers=headers, method="GET") + + if info["status"] == HTTPStatus.OK: + body = resp.read() + return {"secrets": json.loads(body).get("secrets", [])} + elif info["status"] == HTTPStatus.NOT_FOUND: + return { + "secrets": [], + } + else: + module.fail_json(msg=f"Failed to list secrets: {info}") + + +def main() -> None: + """Ansible module entry point.""" + argument_spec = { + "organization": { + "type": "str", + "aliases": ["org", "username"], + "required": True, + }, + "repository": {"type": "str", "aliases": ["repo"]}, + "api_url": {"type": "str", "default": "https://api.github.com"}, + "token": {"type": "str", "required": True, "no_log": True}, + } + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + deps.validate(module) + + organization: str = module.params["organization"] + repository: str = module.params["repository"] + api_url: str = module.params["api_url"] + token: str = module.params["token"] + + result: dict[str, t.Any] = {} + + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + secrets = list_secrets(module, api_url, headers, organization, repository) + + result["changed"] = False + result.update( + secrets=secrets["secrets"], + ) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/test_github_secrets_info.py b/tests/unit/plugins/modules/test_github_secrets_info.py new file mode 100644 index 0000000000..ae8511494a --- /dev/null +++ b/tests/unit/plugins/modules/test_github_secrets_info.py @@ -0,0 +1,98 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + +import pytest +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( + AnsibleExitJson, + exit_json, + fail_json, + set_module_args, +) + +from ansible_collections.community.general.plugins.modules import github_secrets_info + +GITHUB_SECRETS_RESPONSE = { + "total_count": 2, + "secrets": [ + { + "name": "SECRET1", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-02T00:00:00Z", + }, + { + "name": "SECRET2", + "created_at": "2026-01-03T00:00:00Z", + "updated_at": "2026-01-04T00:00:00Z", + }, + ], +} + + +def make_fetch_url_response(body, status=200): + response = MagicMock() + response.read.return_value = json.dumps(body).encode("utf-8") + info = {"status": status, "msg": f"OK ({len(json.dumps(body))} bytes)"} + return (response, info) + + +@pytest.fixture(autouse=True) +def patch_module(): + with patch.multiple( + "ansible.module_utils.basic.AnsibleModule", + exit_json=exit_json, + fail_json=fail_json, + ): + yield + + +@pytest.fixture +def fetch_url_mock(): + with patch.object(github_secrets_info, "fetch_url") as mock: + yield mock + + +def test_list_repo_secrets(fetch_url_mock): + fetch_url_mock.side_effect = [ + make_fetch_url_response(GITHUB_SECRETS_RESPONSE), + make_fetch_url_response({}, status=200), + ] + + with set_module_args( + { + "organization": "myorg", + "repository": "myrepo", + "token": "ghp_test_token", + } + ): + with pytest.raises(AnsibleExitJson) as exc: + github_secrets_info.main() + + result = exc.value.args[0] + assert result["changed"] is False + assert result["secrets"] == GITHUB_SECRETS_RESPONSE["secrets"] + + +def test_fail_list_repo_secrets(fetch_url_mock): + fetch_url_mock.side_effect = [ + make_fetch_url_response({}, status=404), + ] + + with set_module_args( + { + "organization": "myorg", + "repository": "myrepo", + "token": "ghp_test_token", + } + ): + with pytest.raises(AnsibleExitJson) as exc: + github_secrets_info.main() + + result = exc.value.args[0] + assert result["changed"] is False + assert result["secrets"] == []