From 6f995fbd4db2ad9f9e73904b15af3442dfd050b8 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 20 Apr 2026 11:08:12 +0200 Subject: [PATCH 01/15] test: prefer github app auth in integration fixtures --- github-runner-manager/tests/conftest.py | 18 +++++++ .../tests/integration/conftest.py | 17 ++++++- .../tests/integration/factories.py | 22 ++++++++- .../tests/unit/test_integration_factories.py | 47 +++++++++++++++++++ tests/integration/conftest.py | 15 ++++++ tests/integration/test_e2e.py | 28 +---------- 6 files changed, 118 insertions(+), 29 deletions(-) create mode 100644 github-runner-manager/tests/unit/test_integration_factories.py diff --git a/github-runner-manager/tests/conftest.py b/github-runner-manager/tests/conftest.py index d2d6095acf..4557f0e90f 100644 --- a/github-runner-manager/tests/conftest.py +++ b/github-runner-manager/tests/conftest.py @@ -29,6 +29,24 @@ def pytest_addoption(parser): help="Alternate GitHub token from a different user for fork testing.", default=os.getenv("INTEGRATION_TOKEN_ALT"), ) + parser.addoption( + "--github-app-client-id", + action="store", + help="GitHub App Client ID for integration tests.", + default=os.getenv("GITHUB_APP_CLIENT_ID"), + ) + parser.addoption( + "--github-app-installation-id", + action="store", + help="GitHub App installation ID for integration tests.", + default=os.getenv("GITHUB_APP_INSTALLATION_ID"), + ) + parser.addoption( + "--github-app-private-key", + action="store", + help="GitHub App PEM-encoded private key for integration tests.", + default=os.getenv("GITHUB_APP_PRIVATE_KEY"), + ) parser.addoption( "--openstack-auth-url", action="store", diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index 541e2dd0e8..3caecdc0cb 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -6,7 +6,7 @@ import logging import time from pathlib import Path -from typing import Generator +from typing import Generator, cast import openstack import pytest @@ -50,6 +50,9 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: token = pytestconfig.getoption("--github-token") alt_token = pytestconfig.getoption("--github-token-alt", None) path = pytestconfig.getoption("--github-repository") + app_client_id = pytestconfig.getoption("--github-app-client-id") or None + installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None + private_key = pytestconfig.getoption("--github-app-private-key") or None if not token or not alt_token or not path: pytest.fail( @@ -58,6 +61,18 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: "GITHUB_REPOSITORY environment variables." ) + if all((app_client_id, installation_id_raw, private_key)): + logger.info("Using GitHub App authentication for integration tests") + return GitHubConfig( + token=token, + alt_token=alt_token, + path=path, + app_client_id=app_client_id, + installation_id=int(cast(str, installation_id_raw)), + private_key=private_key, + ) + + logger.info("Using PAT authentication for integration tests") return GitHubConfig(token=token, alt_token=alt_token, path=path) diff --git a/github-runner-manager/tests/integration/factories.py b/github-runner-manager/tests/integration/factories.py index 3ccac24fe9..908cdb6907 100644 --- a/github-runner-manager/tests/integration/factories.py +++ b/github-runner-manager/tests/integration/factories.py @@ -27,11 +27,23 @@ class GitHubConfig: token: GitHub personal access token. alt_token: Alternate GitHub personal access token for external contributor. path: GitHub path in / or format. + app_client_id: GitHub App Client ID. + installation_id: GitHub App installation ID. + private_key: GitHub App PEM-encoded private key. + has_app_auth: Whether GitHub App authentication credentials are configured. """ token: str alt_token: str path: str + app_client_id: str | None = None + installation_id: int | None = None + private_key: str | None = None + + @property + def has_app_auth(self) -> bool: + """Whether GitHub App authentication credentials are configured.""" + return all((self.app_client_id, self.installation_id, self.private_key)) @dataclass @@ -214,7 +226,15 @@ def create_default_config( "allow_external_contributor": allow_external_contributor, "extra_labels": test_config.labels, "github_config": { - "auth": {"token": github_config.token}, + "auth": ( + { + "app_client_id": github_config.app_client_id, + "installation_id": github_config.installation_id, + "private_key": github_config.private_key, + } + if github_config.has_app_auth + else {"token": github_config.token} + ), "path": path_config, }, "service_config": { diff --git a/github-runner-manager/tests/unit/test_integration_factories.py b/github-runner-manager/tests/unit/test_integration_factories.py new file mode 100644 index 0000000000..1e32a8a721 --- /dev/null +++ b/github-runner-manager/tests/unit/test_integration_factories.py @@ -0,0 +1,47 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Unit tests for integration test factories.""" + +from tests.integration.factories import GitHubConfig, create_default_config + + +def test_create_default_config_uses_token_auth_by_default(): + """ + arrange: A GitHub test config without GitHub App credentials. + act: Build the integration test application config. + assert: The GitHub auth block uses token auth. + """ + config = create_default_config( + github_config=GitHubConfig( + token="ghp_test_token_1234567890abcdef", + alt_token="ghp_test_alt_token_1234567890abcdef", + path="canonical/example", + ) + ) + + assert config["github_config"]["auth"] == {"token": "ghp_test_token_1234567890abcdef"} + + +def test_create_default_config_uses_github_app_auth_when_available(): + """ + arrange: A GitHub test config with complete GitHub App credentials. + act: Build the integration test application config. + assert: The GitHub auth block uses GitHub App auth. + """ + config = create_default_config( + github_config=GitHubConfig( + token="ghp_test_token_1234567890abcdef", + alt_token="ghp_test_alt_token_1234567890abcdef", + path="canonical/example", + app_client_id="Iv23liExample", + installation_id=456, + private_key="-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", + ) + ) + + assert config["github_config"]["auth"] == { + "app_client_id": "Iv23liExample", + "installation_id": 456, + "private_key": "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", + } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ab902812a7..984c13c726 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -319,6 +319,21 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: "path of / or / format." ) + app_client_id = pytestconfig.getoption("--github-app-client-id") or None + installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None + private_key = pytestconfig.getoption("--github-app-private-key") or None + + if all((app_client_id, installation_id_raw, private_key)): + logging.info("Using GitHub App authentication for integration tests") + return GitHubConfig( + token=random_token, + path=path, + app_client_id=app_client_id, + installation_id=int(cast(str, installation_id_raw)), + private_key=private_key, + ) + + logging.info("Using PAT authentication for integration tests") return GitHubConfig(token=random_token, path=path) diff --git a/tests/integration/test_e2e.py b/tests/integration/test_e2e.py index 5fb3f81c35..86619b66e1 100644 --- a/tests/integration/test_e2e.py +++ b/tests/integration/test_e2e.py @@ -1,19 +1,14 @@ # Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. -"""End-to-end integration test. +"""End-to-end integration test.""" -Uses GitHub App authentication when credentials are provided, falling back to PAT. -""" - -import logging from typing import Iterator import pytest from github.Branch import Branch from github.Repository import Repository -from tests.integration.conftest import GitHubConfig from tests.integration.helpers.common import ( DISPATCH_E2E_TEST_RUN_WORKFLOW_FILENAME, dispatch_workflow, @@ -21,27 +16,6 @@ from tests.integration.helpers.openstack import OpenStackInstanceHelper -@pytest.fixture(scope="module") -def github_config(pytestconfig: pytest.Config, github_config: GitHubConfig) -> GitHubConfig: - """Override github_config to prefer GitHub App auth when credentials are available.""" - app_client_id = pytestconfig.getoption("--github-app-client-id") or None - installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None - private_key = pytestconfig.getoption("--github-app-private-key") or None - - if not all((app_client_id, installation_id_raw, private_key)): - logging.info("Using PAT authentication for e2e test") - return github_config - - logging.info("Using GitHub App authentication for e2e test") - return GitHubConfig( - token=github_config.token, - path=github_config.path, - app_client_id=app_client_id, - installation_id=int(installation_id_raw), # type: ignore[arg-type] - private_key=private_key, - ) - - @pytest.fixture(scope="function", name="app") def app_fixture( basic_app: str, From e204e3dedc3bae0d70fa72aa19066e600f4c0e62 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 20 Apr 2026 12:47:02 +0200 Subject: [PATCH 02/15] style: revert unrelated conftest formatting churn --- tests/integration/conftest.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 984c13c726..983730654b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -594,7 +594,8 @@ def image_builder_fixture( series = dep_ctx.series any_charm_src_overwrite = { - "any_charm.py": textwrap.dedent(f"""\ + "any_charm.py": textwrap.dedent( + f"""\ from any_charm_base import AnyCharmBase class AnyCharm(AnyCharmBase): @@ -607,7 +608,8 @@ def _image_relation_changed(self, event): # Provide mock image relation data event.relation.data[self.unit]['id'] = '{openstack_config.test_image_id}' event.relation.data[self.unit]['tags'] = '{series}, amd64' - """), + """ + ), } logging.info( "Deploying fake image builder via any-charm for image ID %s", @@ -933,7 +935,8 @@ def mock_planner_app(juju: jubilant.Juju, planner_token_secret: str) -> Iterator planner_name = "planner" any_charm_src_overwrite = { - "any_charm.py": textwrap.dedent(f"""\ + "any_charm.py": textwrap.dedent( + f"""\ from any_charm_base import AnyCharmBase class AnyCharm(AnyCharmBase): @@ -947,7 +950,8 @@ def __init__(self, *args, **kwargs): def _on_planner_relation_changed(self, event): event.relation.data[self.app]["endpoint"] = "http://mock:8080" event.relation.data[self.app]["token"] = "{planner_token_secret}" - """), + """ + ), } juju.deploy( From c8046910f165c9afaadcf515b05ec7305bacd422 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 20 Apr 2026 12:58:16 +0200 Subject: [PATCH 03/15] test: read github app private keys from env only --- github-runner-manager/tests/conftest.py | 6 ------ github-runner-manager/tests/integration/conftest.py | 3 ++- tests/conftest.py | 6 ------ tests/integration/conftest.py | 3 ++- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/github-runner-manager/tests/conftest.py b/github-runner-manager/tests/conftest.py index 4557f0e90f..61d9280212 100644 --- a/github-runner-manager/tests/conftest.py +++ b/github-runner-manager/tests/conftest.py @@ -41,12 +41,6 @@ def pytest_addoption(parser): help="GitHub App installation ID for integration tests.", default=os.getenv("GITHUB_APP_INSTALLATION_ID"), ) - parser.addoption( - "--github-app-private-key", - action="store", - help="GitHub App PEM-encoded private key for integration tests.", - default=os.getenv("GITHUB_APP_PRIVATE_KEY"), - ) parser.addoption( "--openstack-auth-url", action="store", diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index 3caecdc0cb..b09ef504d1 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -4,6 +4,7 @@ """Fixtures for github-runner-manager integration tests.""" import logging +import os import time from pathlib import Path from typing import Generator, cast @@ -52,7 +53,7 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: path = pytestconfig.getoption("--github-repository") app_client_id = pytestconfig.getoption("--github-app-client-id") or None installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None - private_key = pytestconfig.getoption("--github-app-private-key") or None + private_key = os.getenv("GITHUB_APP_PRIVATE_KEY") or None if not token or not alt_token or not path: pytest.fail( diff --git a/tests/conftest.py b/tests/conftest.py index cdfa69112e..dc265ae22e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -179,12 +179,6 @@ def pytest_addoption(parser: Parser): help="The GitHub App installation ID for GitHub App authentication testing.", default=os.environ.get("GITHUB_APP_INSTALLATION_ID"), ) - parser.addoption( - "--github-app-private-key", - action="store", - help="The GitHub App PEM-encoded private key for GitHub App authentication testing.", - default=os.environ.get("GITHUB_APP_PRIVATE_KEY"), - ) parser.addoption( "--keep-models", action="store_true", diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 983730654b..00ab4ec965 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -5,6 +5,7 @@ import json import logging +import os import random import re import secrets @@ -321,7 +322,7 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: app_client_id = pytestconfig.getoption("--github-app-client-id") or None installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None - private_key = pytestconfig.getoption("--github-app-private-key") or None + private_key = os.getenv("GITHUB_APP_PRIVATE_KEY") or None if all((app_client_id, installation_id_raw, private_key)): logging.info("Using GitHub App authentication for integration tests") From 05fa8d9ccfb3f93f6b7617a98979f8ebca27d837 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 20 Apr 2026 14:05:16 +0200 Subject: [PATCH 04/15] test: remove fork path change integration coverage --- tests/integration/conftest.py | 72 ------------------- .../test_charm_fork_path_change.py | 71 ------------------ 2 files changed, 143 deletions(-) delete mode 100644 tests/integration/test_charm_fork_path_change.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 00ab4ec965..88fa669937 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -13,7 +13,6 @@ import textwrap from dataclasses import dataclass from pathlib import Path -from time import sleep from typing import Any, Generator, Iterator, Optional, cast import jubilant @@ -34,7 +33,6 @@ LABELS_CONFIG_NAME, OPENSTACK_FLAVOR_CONFIG_NAME, OPENSTACK_NETWORK_CONFIG_NAME, - PATH_CONFIG_NAME, USE_APROXY_CONFIG_NAME, ) from tests.integration.helpers.common import ( @@ -794,76 +792,6 @@ def github_repository(github_client: Github, github_config: GitHubConfig) -> Rep return github_client.get_repo(github_config.path) -@pytest.fixture(scope="module") -def forked_github_repository( - github_repository: Repository, -) -> Repository: - """Create a fork for a GitHub repository.""" - # After fork creation, the repository workflow run must be enabled manually. Otherwise, a 404 - # on the workflow get API will be returned. - forked_repository = github_repository.create_fork(name=f"test-{github_repository.name}") - - # Wait for repo to be ready - for _ in range(10): - try: - sleep(10) - forked_repository.get_branches() - break - except GithubException: - pass - else: - assert False, "timed out whilst waiting for repository creation" - - return forked_repository - - # Parallel runs of this test module is allowed. Therefore, the forked repo is not removed. - - -@pytest.fixture(scope="module") -def forked_github_branch( - github_repository: Repository, forked_github_repository: Repository -) -> Iterator[Branch]: - """Create a new forked branch for testing.""" - branch_name = f"test/{secrets.token_hex(4)}" - - main_branch = forked_github_repository.get_branch(github_repository.default_branch) - branch_ref = forked_github_repository.create_git_ref( - ref=f"refs/heads/{branch_name}", sha=main_branch.commit.sha - ) - - for _ in range(10): - try: - branch = forked_github_repository.get_branch(branch_name) - break - except GithubException as err: - if err.status == 404: - sleep(5) - continue - raise - else: - assert ( - False - ), "Failed to get created branch in fork repo, the issue with GitHub or network." - - yield branch - - branch_ref.delete() - - -@pytest.fixture(scope="module") -def app_with_forked_repo( - juju: jubilant.Juju, basic_app: str, forked_github_repository: Repository -) -> str: - """Application with no runner on a forked repo. - - Test should ensure it returns with the application in a good state and has - one runner. - """ - juju.config(basic_app, values={PATH_CONFIG_NAME: forked_github_repository.full_name}) - - return basic_app - - @pytest.fixture(scope="module", name="test_github_branch") def test_github_branch_fixture(github_repository: Repository) -> Iterator[Branch]: """Create a new branch for testing, from latest commit in current branch.""" diff --git a/tests/integration/test_charm_fork_path_change.py b/tests/integration/test_charm_fork_path_change.py deleted file mode 100644 index 566dd1cfe6..0000000000 --- a/tests/integration/test_charm_fork_path_change.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2026 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Integration tests for github-runner charm with a fork repo. - -Tests a path change in the repo. -""" - -import logging - -import jubilant -import pytest -from github.Repository import Repository - -from charm_state import PATH_CONFIG_NAME -from tests.integration.conftest import GitHubConfig -from tests.integration.helpers.common import wait_for_runner_ready -from tests.integration.helpers.openstack import OpenStackInstanceHelper - -logger = logging.getLogger(__name__) - - -@pytest.mark.skip( - reason=( - "Fork creation fails under the current fine-grained PAT (scoped to canonical org, " - "fork lands in a personal namespace). Will be resolved in an upcoming PR." - ) -) -@pytest.mark.openstack -@pytest.mark.abort_on_fail -def test_path_config_change( - juju: jubilant.Juju, - app_with_forked_repo: str, - github_repository: Repository, - github_config: GitHubConfig, - instance_helper: OpenStackInstanceHelper, -) -> None: - """ - arrange: A working application with one runner in a forked repository. - act: Change the path configuration to the main repository and reconcile runners. - assert: No runners connected to the forked repository and one runner in the main repository. - """ - logger.info("test_path_config_change") - juju.wait( - lambda status: jubilant.all_active(status, app_with_forked_repo), - delay=10, - timeout=10 * 60, - ) - - logger.info("Ensure there is a runner (this calls reconcile)") - instance_helper.ensure_charm_has_runner(app_with_forked_repo) - - juju.config(app_with_forked_repo, values={PATH_CONFIG_NAME: github_config.path}) - - logger.info("Reconciling (again)") - wait_for_runner_ready(juju, app_with_forked_repo) - - unit_name = f"{app_with_forked_repo}/0" - runner_names = instance_helper.get_runner_names(unit_name) - logger.info("runners: %s", runner_names) - assert len(runner_names) == 1 - runner_name = runner_names[0] - - runners_in_repo = github_repository.get_self_hosted_runners() - logger.info("runners in github repo: %s", list(runners_in_repo)) - - runner_in_repo_with_same_name = tuple( - filter(lambda runner: runner.name == runner_name, runners_in_repo) - ) - - assert len(runner_in_repo_with_same_name) == 1 From 0aea0b49f47c94d1cad983d344b72d8eb2177437 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 20 Apr 2026 15:24:34 +0200 Subject: [PATCH 05/15] ci: drop removed fork path change test from workflow --- .github/workflows/integration_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 1c6ad3eb2c..6c5884aae6 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -20,7 +20,7 @@ jobs: juju-channel: 3.6/stable provider: lxd test-tox-env: integration - modules: '["test_multi_unit_same_machine", "test_charm_fork_path_change", "test_charm_no_runner", "test_charm_upgrade"]' + modules: '["test_multi_unit_same_machine", "test_charm_no_runner", "test_charm_upgrade"]' # INTEGRATION_TOKEN, INTEGRATION_TOKEN_ALT, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ # mapping. See CONTRIBUTING.md for more details. extra-arguments: | From f39ac7a57c00422ccfcf3b09bfa5a88b21462be1 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 12:36:26 +0200 Subject: [PATCH 06/15] test: remove unused alt token from integration fixtures The second GitHub token was only needed for the fork-path-change tests that were removed in recent commits. Drop the now-dead --github-token-alt option, INTEGRATION_TOKEN_ALT env plumbing, and alt_token field. --- .github/workflows/integration_test.yaml | 4 ++-- .github/workflows/test_github_runner_manager.yaml | 2 +- github-runner-manager/tests/conftest.py | 6 ------ github-runner-manager/tests/integration/conftest.py | 11 ++++------- github-runner-manager/tests/integration/factories.py | 3 --- .../tests/unit/test_integration_factories.py | 2 -- github-runner-manager/tox.ini | 1 - tox.ini | 1 - 8 files changed, 7 insertions(+), 23 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 6c5884aae6..6cae6be80e 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -21,7 +21,7 @@ jobs: provider: lxd test-tox-env: integration modules: '["test_multi_unit_same_machine", "test_charm_no_runner", "test_charm_upgrade"]' - # INTEGRATION_TOKEN, INTEGRATION_TOKEN_ALT, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ + # INTEGRATION_TOKEN, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ # mapping. See CONTRIBUTING.md for more details. extra-arguments: | -m=openstack \ @@ -49,7 +49,7 @@ jobs: provider: lxd test-tox-env: integration modules: '["test_prometheus_metrics"]' - # INTEGRATION_TOKEN, INTEGRATION_TOKEN_ALT, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ + # INTEGRATION_TOKEN, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ # mapping. See CONTRIBUTING.md for more details. extra-arguments: | -m=openstack \ diff --git a/.github/workflows/test_github_runner_manager.yaml b/.github/workflows/test_github_runner_manager.yaml index cdbecc563f..dd4ccdc566 100644 --- a/.github/workflows/test_github_runner_manager.yaml +++ b/.github/workflows/test_github_runner_manager.yaml @@ -37,7 +37,7 @@ jobs: working-directory: ./github-runner-manager/ env: # GitHub configuration - # INTEGRATION_TOKEN, INTEGRATION_TOKEN_ALT + # INTEGRATION_TOKEN ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_1 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_1 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_2 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_2 }} # OpenStack configuration diff --git a/github-runner-manager/tests/conftest.py b/github-runner-manager/tests/conftest.py index 61d9280212..1c44625efc 100644 --- a/github-runner-manager/tests/conftest.py +++ b/github-runner-manager/tests/conftest.py @@ -23,12 +23,6 @@ def pytest_addoption(parser): action="store", help="The GitHub repository in / format for integration tests.", ) - parser.addoption( - "--github-token-alt", - action="store", - help="Alternate GitHub token from a different user for fork testing.", - default=os.getenv("INTEGRATION_TOKEN_ALT"), - ) parser.addoption( "--github-app-client-id", action="store", diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index b09ef504d1..450fb327bd 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -49,24 +49,21 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: environment variable is set. """ token = pytestconfig.getoption("--github-token") - alt_token = pytestconfig.getoption("--github-token-alt", None) path = pytestconfig.getoption("--github-repository") app_client_id = pytestconfig.getoption("--github-app-client-id") or None installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None private_key = os.getenv("GITHUB_APP_PRIVATE_KEY") or None - if not token or not alt_token or not path: + if not token or not path: pytest.fail( - "GitHub configuration not provided. Use --github-token, --github-token-alt, and " - "--github-repository options or set INTEGRATION_TOKEN, INTEGRATION_TOKEN_ALT, and " - "GITHUB_REPOSITORY environment variables." + "GitHub configuration not provided. Use --github-token and --github-repository " + "options or set INTEGRATION_TOKEN and GITHUB_REPOSITORY environment variables." ) if all((app_client_id, installation_id_raw, private_key)): logger.info("Using GitHub App authentication for integration tests") return GitHubConfig( token=token, - alt_token=alt_token, path=path, app_client_id=app_client_id, installation_id=int(cast(str, installation_id_raw)), @@ -74,7 +71,7 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: ) logger.info("Using PAT authentication for integration tests") - return GitHubConfig(token=token, alt_token=alt_token, path=path) + return GitHubConfig(token=token, path=path) @pytest.fixture(scope="module") diff --git a/github-runner-manager/tests/integration/factories.py b/github-runner-manager/tests/integration/factories.py index 908cdb6907..923c404e1b 100644 --- a/github-runner-manager/tests/integration/factories.py +++ b/github-runner-manager/tests/integration/factories.py @@ -25,7 +25,6 @@ class GitHubConfig: Attributes: token: GitHub personal access token. - alt_token: Alternate GitHub personal access token for external contributor. path: GitHub path in / or format. app_client_id: GitHub App Client ID. installation_id: GitHub App installation ID. @@ -34,7 +33,6 @@ class GitHubConfig: """ token: str - alt_token: str path: str app_client_id: str | None = None installation_id: int | None = None @@ -178,7 +176,6 @@ def create_default_config( if github_config is None: github_config = GitHubConfig( token="ghp_test_token_1234567890abcdef", - alt_token="ghp_test_alt_token_1234567890abcdef", path="test-org", ) diff --git a/github-runner-manager/tests/unit/test_integration_factories.py b/github-runner-manager/tests/unit/test_integration_factories.py index 1e32a8a721..a33d35b442 100644 --- a/github-runner-manager/tests/unit/test_integration_factories.py +++ b/github-runner-manager/tests/unit/test_integration_factories.py @@ -15,7 +15,6 @@ def test_create_default_config_uses_token_auth_by_default(): config = create_default_config( github_config=GitHubConfig( token="ghp_test_token_1234567890abcdef", - alt_token="ghp_test_alt_token_1234567890abcdef", path="canonical/example", ) ) @@ -32,7 +31,6 @@ def test_create_default_config_uses_github_app_auth_when_available(): config = create_default_config( github_config=GitHubConfig( token="ghp_test_token_1234567890abcdef", - alt_token="ghp_test_alt_token_1234567890abcdef", path="canonical/example", app_client_id="Iv23liExample", installation_id=456, diff --git a/github-runner-manager/tox.ini b/github-runner-manager/tox.ini index 195eb39a70..a7b7accb8a 100644 --- a/github-runner-manager/tox.ini +++ b/github-runner-manager/tox.ini @@ -114,7 +114,6 @@ passenv = PYTHONPATH GITHUB_REPOSITORY INTEGRATION_TOKEN - INTEGRATION_TOKEN_ALT OS_AUTH_URL OS_PROJECT_DOMAIN_NAME OS_PROJECT_NAME diff --git a/tox.ini b/tox.ini index 572b6f811e..6e9b06c8fa 100644 --- a/tox.ini +++ b/tox.ini @@ -112,7 +112,6 @@ description = Run integration tests pass_env = PYTEST_ADDOPTS INTEGRATION_TOKEN - INTEGRATION_TOKEN_ALT OS_AUTH_URL OS_PROJECT_DOMAIN_NAME OS_PROJECT_NAME From 3f0138556b3343418337f879bac759888efc2dff Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 12:41:27 +0200 Subject: [PATCH 07/15] ci: drop rot-prone slot-to-var comments from workflows The inline comments naming which env var fills each INTEGRATION_TEST_SECRET_ENV_VALUE_ slot could silently desync when the mapping is reshuffled in repo settings. Point to CONTRIBUTING.md instead, which is the authoritative source. --- .github/workflows/integration_test.yaml | 8 ++++---- .github/workflows/test_github_runner_manager.yaml | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 6cae6be80e..0495842af6 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -21,8 +21,8 @@ jobs: provider: lxd test-tox-env: integration modules: '["test_multi_unit_same_machine", "test_charm_no_runner", "test_charm_upgrade"]' - # INTEGRATION_TOKEN, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ - # mapping. See CONTRIBUTING.md for more details. + # Sensitive env vars are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_; + # see CONTRIBUTING.md for the slot-to-var mapping. extra-arguments: | -m=openstack \ --log-format="%(asctime)s %(levelname)s %(message)s" \ @@ -49,8 +49,8 @@ jobs: provider: lxd test-tox-env: integration modules: '["test_prometheus_metrics"]' - # INTEGRATION_TOKEN, OS_* are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ - # mapping. See CONTRIBUTING.md for more details. + # Sensitive env vars are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_; + # see CONTRIBUTING.md for the slot-to-var mapping. extra-arguments: | -m=openstack \ --log-format="%(asctime)s %(levelname)s %(message)s" \ diff --git a/.github/workflows/test_github_runner_manager.yaml b/.github/workflows/test_github_runner_manager.yaml index dd4ccdc566..28628cdab4 100644 --- a/.github/workflows/test_github_runner_manager.yaml +++ b/.github/workflows/test_github_runner_manager.yaml @@ -36,12 +36,10 @@ jobs: - name: Run integration tests - ${{ matrix.test-module }} working-directory: ./github-runner-manager/ env: - # GitHub configuration - # INTEGRATION_TOKEN + # Sensitive env vars are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_; + # see CONTRIBUTING.md for the slot-to-var mapping. ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_1 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_1 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_2 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_2 }} - # OpenStack configuration - # OS_AUTH_URL, OS_PROJECT_DOMAIN_NAME, OS_PROJECT_NAME, OS_USER_DOMAIN_NAME, OS_USERNAME, OS_PASSWORD, OS_NETWORK, OS_REGION_NAME ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_3 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_3 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_4 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_4 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_5 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_5 }} From 485da79cd911066b5a4345c5dae26bb1ca7e7d1a Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 12:44:10 +0200 Subject: [PATCH 08/15] test: read github token from env only Drop the --github-token pytest option so the token can only flow through the INTEGRATION_TOKEN environment variable. Matches how GITHUB_APP_PRIVATE_KEY is already handled and keeps secrets out of the process command line. --- github-runner-manager/tests/conftest.py | 6 ------ github-runner-manager/tests/integration/conftest.py | 9 ++++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/github-runner-manager/tests/conftest.py b/github-runner-manager/tests/conftest.py index 1c44625efc..1f949bf7e4 100644 --- a/github-runner-manager/tests/conftest.py +++ b/github-runner-manager/tests/conftest.py @@ -12,12 +12,6 @@ def pytest_addoption(parser): Args: parser: Pytest parser. """ - parser.addoption( - "--github-token", - action="store", - help="GitHub personal access token for integration tests.", - default=os.getenv("INTEGRATION_TOKEN"), - ) parser.addoption( "--github-repository", action="store", diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index 450fb327bd..26cbc7bb52 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -45,10 +45,9 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: GitHub configuration object. Raises: - pytest.fail: If neither --github-token option nor INTEGRATION_TOKEN - environment variable is set. + pytest.fail: If INTEGRATION_TOKEN or the GitHub repository is not set. """ - token = pytestconfig.getoption("--github-token") + token = os.getenv("INTEGRATION_TOKEN") path = pytestconfig.getoption("--github-repository") app_client_id = pytestconfig.getoption("--github-app-client-id") or None installation_id_raw = pytestconfig.getoption("--github-app-installation-id") or None @@ -56,8 +55,8 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: if not token or not path: pytest.fail( - "GitHub configuration not provided. Use --github-token and --github-repository " - "options or set INTEGRATION_TOKEN and GITHUB_REPOSITORY environment variables." + "GitHub configuration not provided. Set INTEGRATION_TOKEN and use " + "--github-repository." ) if all((app_client_id, installation_id_raw, private_key)): From ada84b199f343dbd9205e9420c06c3697aef1fca Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 14:12:42 +0200 Subject: [PATCH 09/15] ci: wire additional secret slots for app auth credentials Add the unsuffixed INTEGRATION_TEST_SECRET_ENV_NAME slot plus _11 and _12 so the GitHub App client id, installation id, and private key can be delivered to integration tests. --- .github/workflows/test_github_runner_manager.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_github_runner_manager.yaml b/.github/workflows/test_github_runner_manager.yaml index 28628cdab4..449f8c06a1 100644 --- a/.github/workflows/test_github_runner_manager.yaml +++ b/.github/workflows/test_github_runner_manager.yaml @@ -38,6 +38,7 @@ jobs: env: # Sensitive env vars are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_; # see CONTRIBUTING.md for the slot-to-var mapping. + ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_1 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_1 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_2 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_2 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_3 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_3 }} @@ -48,6 +49,8 @@ jobs: ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_8 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_8 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_9 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_9 }} ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_10 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_10 }} + ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_11 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_11 }} + ${{ vars.INTEGRATION_TEST_SECRET_ENV_NAME_12 }}: ${{ secrets.INTEGRATION_TEST_SECRET_ENV_VALUE_12 }} run: | tox -e integration -- -v --tb=native -s \ tests/integration/${{ matrix.test-module }}.py \ From 635b29143cca6b39f2bfda0f03c8ead85da277cb Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 14:19:57 +0200 Subject: [PATCH 10/15] ci: pass github app client id to charm integration tests Mirror the e2e workflow and forward INTEGRATION_TEST_GITHUB_APP_CLIENT_ID into the charm integration test jobs so they can opt into GitHub App authentication when the other app credentials are available. --- .github/workflows/integration_test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 0495842af6..3a19d92745 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -35,6 +35,7 @@ jobs: --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" \ --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ --openstack-image-id="${{ vars.INTEGRATION_TEST_IMAGE_ID }}" \ + --github-app-client-id="${{ vars.INTEGRATION_TEST_GITHUB_APP_CLIENT_ID }}" \ --dockerhub-mirror="${{ vars.INTEGRATION_TEST_DOCKERHUB_MIRROR }}" self-hosted-runner: true self-hosted-runner-label: pfe-ci @@ -63,6 +64,7 @@ jobs: --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" \ --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ --openstack-image-id="${{ vars.INTEGRATION_TEST_IMAGE_ID }}" \ + --github-app-client-id="${{ vars.INTEGRATION_TEST_GITHUB_APP_CLIENT_ID }}" \ --dockerhub-mirror="${{ vars.INTEGRATION_TEST_DOCKERHUB_MIRROR }}" self-hosted-runner: true self-hosted-runner-label: pfe-ci @@ -91,6 +93,7 @@ jobs: --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" \ --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ --openstack-image-id="${{ vars.INTEGRATION_TEST_IMAGE_ID }}" \ + --github-app-client-id="${{ vars.INTEGRATION_TEST_GITHUB_APP_CLIENT_ID }}" \ --dockerhub-mirror="${{ vars.INTEGRATION_TEST_DOCKERHUB_MIRROR }}" \ --base="${{ matrix.base }}" self-hosted-runner: true From cbe4ed698b056e1620605441fca464c0270deafe Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 15:04:19 +0200 Subject: [PATCH 11/15] chore: remove outdated skip of test_charm_upgrade --- tests/integration/test_charm_upgrade.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/integration/test_charm_upgrade.py b/tests/integration/test_charm_upgrade.py index 4960c56619..8fd92aac01 100644 --- a/tests/integration/test_charm_upgrade.py +++ b/tests/integration/test_charm_upgrade.py @@ -31,15 +31,6 @@ pytestmark = pytest.mark.openstack -@pytest.mark.skip( - reason=( - "latest/edge charm predates token-secret-id and openstack-clouds-yaml-secret-id " - "config options. Falling back to the plaintext token/openstack-clouds-yaml options " - "is not acceptable because jubilant logs deploy config, which leaks the token. " - "Re-enable once a release containing the secret-id options has been promoted to " - "latest/edge." - ) -) def test_charm_upgrade( juju: jubilant.Juju, deployment_context: DeploymentContext, From 441350b61112b04f328897742fcf61600572b6e5 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 15:17:33 +0200 Subject: [PATCH 12/15] test: hide secrets from integration config reprs Mark the token, private key, and OpenStack password fields with repr=False so assertion failures, traceback locals, or stray logs of these dataclasses cannot leak credentials in transformed (non-masked) forms. --- github-runner-manager/tests/integration/factories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github-runner-manager/tests/integration/factories.py b/github-runner-manager/tests/integration/factories.py index 923c404e1b..d060b63844 100644 --- a/github-runner-manager/tests/integration/factories.py +++ b/github-runner-manager/tests/integration/factories.py @@ -32,11 +32,11 @@ class GitHubConfig: has_app_auth: Whether GitHub App authentication credentials are configured. """ - token: str + token: str = field(repr=False) path: str app_client_id: str | None = None installation_id: int | None = None - private_key: str | None = None + private_key: str | None = field(default=None, repr=False) @property def has_app_auth(self) -> bool: @@ -64,7 +64,7 @@ class OpenStackConfig: auth_url: str project: str username: str - password: str + password: str = field(repr=False) network: str region_name: str = "RegionOne" user_domain_name: str = "Default" From df614ed2c17b2fa3d28ee7b7d18829834f0337fb Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 15:34:37 +0200 Subject: [PATCH 13/15] docs: clarify github_config fixture input sources Token and private key are env-only; path and app IDs come from pytest options. Prior docstring said "pytest options or environment" without distinguishing which inputs come from where. --- github-runner-manager/tests/integration/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index 26cbc7bb52..9f9ac06252 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -36,7 +36,12 @@ def test_config(pytestconfig: pytest.Config) -> TestConfig: @pytest.fixture(scope="module") def github_config(pytestconfig: pytest.Config) -> GitHubConfig: - """Get GitHub configuration from pytest options or environment. + """Get GitHub configuration. + + The token and private key are read from the environment only + (``INTEGRATION_TOKEN`` and ``GITHUB_APP_PRIVATE_KEY``). The repository path + and GitHub App client/installation IDs come from pytest options (which + themselves fall back to environment variables; see ``pytest_addoption``). Args: pytestconfig: Pytest configuration object. From 0407945bc63cc8b64b259e83b11b6f32c63fd601 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 15:49:49 +0200 Subject: [PATCH 14/15] docs: narrow env-fallback claim to app id options Only --github-app-client-id and --github-app-installation-id fall back to env vars; --github-repository does not. Previous wording implied all three did. --- github-runner-manager/tests/integration/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github-runner-manager/tests/integration/conftest.py b/github-runner-manager/tests/integration/conftest.py index 9f9ac06252..81257d7b22 100644 --- a/github-runner-manager/tests/integration/conftest.py +++ b/github-runner-manager/tests/integration/conftest.py @@ -40,8 +40,8 @@ def github_config(pytestconfig: pytest.Config) -> GitHubConfig: The token and private key are read from the environment only (``INTEGRATION_TOKEN`` and ``GITHUB_APP_PRIVATE_KEY``). The repository path - and GitHub App client/installation IDs come from pytest options (which - themselves fall back to environment variables; see ``pytest_addoption``). + and GitHub App client/installation IDs come from pytest options; the app + ID options fall back to environment variables (see ``pytest_addoption``). Args: pytestconfig: Pytest configuration object. From eb29b2094d32fe4ecc85b22e3232aefa9deb9105 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Tue, 21 Apr 2026 15:52:51 +0200 Subject: [PATCH 15/15] style: restore inline textwrap.dedent formatting Earlier revert commit moved two any_charm blocks to a multi-line form that the current black config rejects. Match main's inline style so lint passes. --- tests/integration/conftest.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 88fa669937..4ced2a443c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -593,8 +593,7 @@ def image_builder_fixture( series = dep_ctx.series any_charm_src_overwrite = { - "any_charm.py": textwrap.dedent( - f"""\ + "any_charm.py": textwrap.dedent(f"""\ from any_charm_base import AnyCharmBase class AnyCharm(AnyCharmBase): @@ -607,8 +606,7 @@ def _image_relation_changed(self, event): # Provide mock image relation data event.relation.data[self.unit]['id'] = '{openstack_config.test_image_id}' event.relation.data[self.unit]['tags'] = '{series}, amd64' - """ - ), + """), } logging.info( "Deploying fake image builder via any-charm for image ID %s", @@ -864,8 +862,7 @@ def mock_planner_app(juju: jubilant.Juju, planner_token_secret: str) -> Iterator planner_name = "planner" any_charm_src_overwrite = { - "any_charm.py": textwrap.dedent( - f"""\ + "any_charm.py": textwrap.dedent(f"""\ from any_charm_base import AnyCharmBase class AnyCharm(AnyCharmBase): @@ -879,8 +876,7 @@ def __init__(self, *args, **kwargs): def _on_planner_relation_changed(self, event): event.relation.data[self.app]["endpoint"] = "http://mock:8080" event.relation.data[self.app]["token"] = "{planner_token_secret}" - """ - ), + """), } juju.deploy(