Chore: [AEA-0000] - new script to setup new repo#94
Chore: [AEA-0000] - new script to setup new repo#94anthony-nhs wants to merge 12 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new setup_github_repo package and a wrapper script intended to automate GitHub repository setup (repo settings, team access, environments, and secrets), with supporting unit tests and coverage configuration.
Changes:
- Add
packages/setup_github_repoapplication code for repo setup orchestration (GitHub + AWS exports + CLI bootstrap). - Add a
scripts/setup_github_repos.pywrapper entrypoint and update repo config/ignores to support local secret files. - Add
boto3dependency plus a new test suite and per-package pytest/coverage config.
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/setup_github_repos.py | Wrapper script to invoke the new repo-setup CLI. |
| pyproject.toml | Adds boto3 dependency for AWS export retrieval. |
| poetry.lock | Locks new dependencies (notably boto3/botocore stack). |
| packages/setup_github_repo/app/aws_exports.py | Fetches CloudFormation exports and maps them into role values. |
| packages/setup_github_repo/app/cli.py | CLI entrypoint: GitHub auth token resolution + AWS credential validation + runner invocation. |
| packages/setup_github_repo/app/constants.py | Central constants for AWS profiles, app IDs, and target hostnames. |
| packages/setup_github_repo/app/github_access.py | Applies standard EPS team access settings to repos. |
| packages/setup_github_repo/app/github_base.py | Shared interactive confirmation + rate-limit pacing helpers. |
| packages/setup_github_repo/app/github_environments.py | Creates GitHub environments with reviewers and deployment policies. |
| packages/setup_github_repo/app/github_repo_settings.py | Applies repo edit options, Actions permissions, and branch protection. |
| packages/setup_github_repo/app/github_secrets.py | Creates Actions/Dependabot/environment secrets for weekly release and other cases. |
| packages/setup_github_repo/app/github_setup.py | Facade coordinating settings/access/environments/secrets setup. |
| packages/setup_github_repo/app/models.py | Dataclasses for repo config, teams, roles, secrets, environments. |
| packages/setup_github_repo/app/repo_status.py | Loads and normalises setup config from NHSDigital/eps-repo-status repos.json. |
| packages/setup_github_repo/app/runner.py | End-to-end orchestration using GitHub + AWS exports + repo-status config. |
| packages/setup_github_repo/app/secrets_builder.py | Builds consolidated secrets payload from files, AWS exports, and env vars. |
| packages/setup_github_repo/app/init.py | Package init for app modules. |
| packages/setup_github_repo/init.py | Top-level package init. |
| packages/setup_github_repo/main.py | Enables running via python -m setup_github_repo. |
| packages/setup_github_repo/.coveragerc | Coverage configuration for the new package. |
| packages/setup_github_repo/pytest.ini | Pytest configuration (test discovery + coverage output). |
| packages/setup_github_repo/tests/init.py | Test package init. |
| packages/setup_github_repo/tests/test_aws_exports.py | Unit tests for AWS export pagination + role mapping. |
| packages/setup_github_repo/tests/test_cli.py | Unit tests for GitHub token/AWS credential bootstrap logic. |
| packages/setup_github_repo/tests/test_github_access.py | Unit tests for team permission application. |
| packages/setup_github_repo/tests/test_github_environments.py | Unit tests for environment creation in weekly/non-weekly paths. |
| packages/setup_github_repo/tests/test_github_repo_settings.py | Unit tests for repo settings + branch protection + Actions permissions. |
| packages/setup_github_repo/tests/test_github_secrets.py | Unit tests for secret creation behavior based on repo config flags. |
| packages/setup_github_repo/tests/test_github_setup.py | Unit tests for facade wiring and manager invocation. |
| packages/setup_github_repo/tests/test_repo_status.py | Unit tests for repos.json payload parsing and loader behavior. |
| packages/setup_github_repo/tests/test_runner.py | Unit tests for runner dependency wiring and repo filtering behavior. |
| packages/setup_github_repo/tests/test_secrets_builder.py | Unit tests for secret payload assembly from files/exports/env. |
| .gitignore | Ignores local .secrets/ directory used by the setup workflow. |
| .gitallowed | Allows additional token-related patterns used by the new CLI/tests. |
| self._set_environment_secret( | ||
| repo=repo, | ||
| environment_name="create_pull_request", | ||
| secret_name="AUTOMERGE_PEM", | ||
| secret_value=secrets.automerge_pem, | ||
| ) | ||
| self._set_environment_secret( | ||
| repo=repo, | ||
| environment_name="create_pull_request", | ||
| secret_name="AUTOMERGE_APP_ID", | ||
| secret_value=AUTOMERGE_APP_ID, | ||
| ) | ||
| self._set_secret( | ||
| repo=repo, | ||
| secret_name="DEPENDABOT_TOKEN", | ||
| secret_value=secrets.dependabot_token, | ||
| set_dependabot=True, | ||
| ) |
There was a problem hiding this comment.
Tests in test_github_secrets.py expect AUTOMERGE_PEM and AUTOMERGE_APP_ID to be created via _set_secret (repo-level Actions/Dependabot secrets), but the implementation sets them as environment secrets on create_pull_request. This mismatch will fail tests and may also break workflows that read repo-level secrets. Decide which scope is correct and make code + tests consistent (typically: use _set_secret for AUTOMERGE_*, and reserve environment secrets for environment-specific values).
| ) | ||
| self._set_secret( | ||
| repo=repo, | ||
| secret_name=f"{env_name}_CLOUD_FORMATION_CREATE_CHANGESET_ROLE", |
There was a problem hiding this comment.
The secret name uses ...CREATE_CHANGESET_ROLE but the underlying export/model field is cloud_formation_prepare_changeset_role (and the CloudFormation export name is CloudFormationPrepareChangesetRole). If downstream workflows expect a key aligned to 'PREPARE_CHANGESET', this will silently omit the expected secret. Align the secret name with the role/export naming (or update the consumer expectations) to avoid missing secrets at runtime.
| secret_name=f"{env_name}_CLOUD_FORMATION_CREATE_CHANGESET_ROLE", | |
| secret_name=f"{env_name}_CLOUD_FORMATION_PREPARE_CHANGESET_ROLE", |
| root_repos_file = Path(__file__).resolve().parents[3] / "repos.json" | ||
| original_content = root_repos_file.read_text(encoding="utf-8") | ||
| root_repos_file.write_text(json.dumps(payload), encoding="utf-8") | ||
|
|
||
| loader = RepoStatusLoader() | ||
|
|
||
| try: | ||
| result = loader.load_repo_configs() | ||
| finally: | ||
| root_repos_file.write_text(original_content, encoding="utf-8") |
There was a problem hiding this comment.
This test mutates a real repos.json at the repository root. That makes the suite non-hermetic and can become flaky under parallel test execution (or if another process reads repos.json concurrently). A more robust approach is to refactor RepoStatusLoader to accept an explicit repos_path (defaulting to the current location) and in tests pass a temp file (tmp_path) instead of writing into the repo.
| def _set_actions_permissions(self, repo: Any, repo_url: str) -> None: | ||
| workflow_permissions_endpoint = f"/repos/{repo_url}/actions/permissions/workflow" | ||
| _headers, workflow_permissions = repo._requester.requestJsonAndCheck("GET", workflow_permissions_endpoint) | ||
| default_workflow_permissions = str(workflow_permissions.get("default_workflow_permissions") or "read") | ||
|
|
||
| repo._requester.requestJsonAndCheck( | ||
| "PUT", | ||
| workflow_permissions_endpoint, | ||
| input={ | ||
| "default_workflow_permissions": default_workflow_permissions, | ||
| "can_approve_pull_request_reviews": True, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
This relies on PyGitHub’s private repo._requester API, which is prone to breaking across library versions. If possible, prefer a supported PyGitHub method for Actions permissions; if there isn’t one, consider routing these calls through a small, well-documented adapter (e.g., using the top-level Github requester) so the private API usage is isolated and easier to update.
| testpaths = tests | ||
| python_files = test_*.py | ||
| python_functions = test_* | ||
| addopts = --cov=setup_github_repo.app --cov-report=xml:packages/setup_github_repo/coverage/coverage.xml --cov-config=packages/setup_github_repo/.coveragerc |
There was a problem hiding this comment.
The test suite (and coverage target) references setup_github_repo.*, but in this repo the code lives under the top-level packages/ namespace (e.g. packages.setup_github_repo). Unless the test runner sets PYTHONPATH=packages (or installs the package), these imports will raise ModuleNotFoundError. Consider switching imports/coverage to packages.setup_github_repo.* or adding a test-time path configuration so setup_github_repo is importable.
| addopts = --cov=setup_github_repo.app --cov-report=xml:packages/setup_github_repo/coverage/coverage.xml --cov-config=packages/setup_github_repo/.coveragerc | |
| addopts = --cov=packages.setup_github_repo.app --cov-report=xml:packages/setup_github_repo/coverage/coverage.xml --cov-config=packages/setup_github_repo/.coveragerc |
| set_secret_names = [call.kwargs["secret_name"] for call in manager._set_secret.call_args_list] | ||
| assert set_secret_names == [ | ||
| "AUTOMERGE_PEM", | ||
| "AUTOMERGE_APP_ID", | ||
| "DEPENDABOT_TOKEN", | ||
| ] | ||
|
|
There was a problem hiding this comment.
The test asserts that AUTOMERGE_* secrets are set via _set_secret, but the implementation sets them via _set_environment_secret for the create_pull_request environment. As written, this assertion will fail; update the expectations (or change the implementation) so the test matches the intended secret scope.
| set_environment_secret_names = [ | ||
| call.kwargs["secret_name"] for call in manager._set_environment_secret.call_args_list | ||
| ] | ||
| assert set_environment_secret_names == [ | ||
| "CREATE_PULL_REQUEST_PEM", | ||
| "CREATE_PULL_REQUEST_APP_ID", | ||
| ] | ||
|
|
There was a problem hiding this comment.
_set_environment_secret is currently called for AUTOMERGE_* as well as CREATE_PULL_REQUEST_* secrets, but this test only expects the CREATE_PULL_REQUEST_* environment secrets. Update the expected list (or adjust the code) to keep the test aligned with the actual behavior.
| assert "REF_ARTILLERY_RUNNER_ROLE" in set_secret_names | ||
|
|
||
| role_env_names = [call.kwargs["env_name"] for call in manager._set_role_secrets.call_args_list] | ||
| assert role_env_names == ["DEV", "INT", "PROD", "QA", "REF", "REF", "RECOVERY"] |
There was a problem hiding this comment.
This test expects _set_role_secrets to be called twice for env_name="REF", but the implementation only calls it once (the earlier duplicate was removed). Update role_env_names expectations to match the current call list.
| assert role_env_names == ["DEV", "INT", "PROD", "QA", "REF", "REF", "RECOVERY"] | |
| assert role_env_names == ["DEV", "INT", "PROD", "QA", "REF", "RECOVERY"] |
| def test_run_sets_up_only_eps_aws_dashboards_repo(): | ||
| runner = SetupGithubRepoRunner.__new__(SetupGithubRepoRunner) | ||
| secrets = _secrets() | ||
| runner._secrets_builder = MagicMock() | ||
| runner._secrets_builder.build.return_value = secrets | ||
| runner._print_setup_summary = MagicMock() | ||
| runner._repo_status_loader = MagicMock() | ||
| runner._repo_status_loader.load_repo_configs.return_value = [ | ||
| _repo_config("NHSDigital/eps-aws-dashboards"), | ||
| _repo_config("NHSDigital/other-repo"), | ||
| ] | ||
| runner._github_setup = MagicMock() | ||
|
|
||
| runner.run() | ||
|
|
||
| runner._print_setup_summary.assert_called_once_with(secrets_keys=sorted(asdict(secrets).keys())) | ||
| runner._github_setup.setup_repo.assert_called_once_with( | ||
| repo_config=_repo_config("NHSDigital/eps-aws-dashboards"), | ||
| secrets=secrets, | ||
| ) | ||
|
|
There was a problem hiding this comment.
SetupGithubRepoRunner.run() iterates over all repo configs returned by load_repo_configs() and calls setup_repo for each. This test currently expects only one repo to be set up, so it will fail; either update the test to expect calls for both repos or introduce explicit filtering in run().
| def test_run_skips_setup_when_no_matching_repo(): | ||
| runner = SetupGithubRepoRunner.__new__(SetupGithubRepoRunner) | ||
| secrets = _secrets() | ||
| runner._secrets_builder = MagicMock() | ||
| runner._secrets_builder.build.return_value = secrets | ||
| runner._print_setup_summary = MagicMock() | ||
| runner._repo_status_loader = MagicMock() | ||
| runner._repo_status_loader.load_repo_configs.return_value = [_repo_config("NHSDigital/other-repo")] | ||
| runner._github_setup = MagicMock() | ||
|
|
||
| runner.run() | ||
|
|
||
| runner._github_setup.setup_repo.assert_not_called() | ||
|
|
There was a problem hiding this comment.
The runner currently does not skip setup when there are repo configs present; it will call setup_repo for every entry returned. This test expects no setup call when the list contains a non-matching repo, but there is no matching logic in run(), so the assertion will fail. Align the test with the runner behavior (or implement filtering).
Summary
Details