diff --git a/changelog.d/20230125_122634_nedbat_add_prs_to_projects_221.rst b/changelog.d/20230125_122634_nedbat_add_prs_to_projects_221.rst new file mode 100644 index 00000000..457dcd38 --- /dev/null +++ b/changelog.d/20230125_122634_nedbat_add_prs_to_projects_221.rst @@ -0,0 +1,5 @@ +.. A new scriv changelog fragment. + +- Added: contribution pull requests will be added to GitHub projects if the + base repo says to by adding an "openedx.org/add-to-projects" annotation in + its catalog-info.yaml file. diff --git a/docs/details.rst b/docs/details.rst index 9ef45f97..75648ed7 100644 --- a/docs/details.rst +++ b/docs/details.rst @@ -154,3 +154,31 @@ When a pull request is re-opened The bot deletes the "please complete a survey" comment that was added when the pull request was closed. The Jira issue is returned to the state it was in when the pull request was closed. + + +Adding pull requests to GitHub projects +--------------------------------------- + +The bot will add new pull requests to GitHub projects. Projects are specified +with a string like "openedx:23" meaning `project number 23`_ in the openedx +organization. + +.. _project number 23: https://github.com/orgs/openedx/projects/23 + +- Regular non-internal pull requests get added to the project specified in the + GITHUB_OSPR_PROJECT setting. + +- Blended pull requests get added to the project specified in the + GITHUB_BLENDED_PROJECT setting. + +- Individual repos can specify other projects that external non-draft pull + requests should be added to. The projects are listed in an annotation in + their catalog-info.yaml file: + + .. code-block:: yaml + + annotations: + # This can be multiple comma-separated projects. + openedx.org/add-to-projects: "openedx:23" + +The bot never removes pull requests from projects. diff --git a/openedx_webhooks/info.py b/openedx_webhooks/info.py index 441ba6f3..d21521ff 100644 --- a/openedx_webhooks/info.py +++ b/openedx_webhooks/info.py @@ -8,12 +8,13 @@ from typing import Dict, Iterable, Optional, Tuple, Union import yaml +from glom import glom from iso8601 import parse_date from openedx_webhooks import settings from openedx_webhooks.lib.github.models import PrId from openedx_webhooks.oauth import get_github_session -from openedx_webhooks.types import PrDict, PrCommentDict +from openedx_webhooks.types import GhProject, PrDict, PrCommentDict from openedx_webhooks.utils import ( memoize, memoize_timed, @@ -357,3 +358,38 @@ def jira_project_for_blended(pr: PrDict) -> Optional[str]: if settings.JIRA_SERVER is None: return None return "BLENDED" + + +def get_catalog_info(repo_fullname: str) -> Dict: + """Get the parsed catalog-info.yaml data from a repo, or {} if missing.""" + yml = _read_github_file(repo_fullname, "catalog-info.yaml", not_there="{}") + return yaml.safe_load(yml) + + +def projects_for_pr(pull_request: PrDict) -> Iterable[GhProject]: + """ + Get the projects a pull request should be added to. + + Draft pull requests don't get added. + + The projects are specified in an annotation in catalog-info.yaml:: + + metadata: + annotations: + openedx.org/add-to-projects: "openedx:23, openedx:456" + + Each entry is an org:num spec for an organization project. + + """ + if is_draft_pull_request(pull_request): + return set() + + catalog_info = get_catalog_info(pull_request["base"]["repo"]["full_name"]) + annotations = glom(catalog_info, "metadata.annotations", default={}) + projects = annotations.get("openedx.org/add-to-projects", "") + gh_projects = [] + if projects: + for spec in projects.split(","): + org, number = spec.strip().split(":") + gh_projects.append((org, int(number))) + return gh_projects diff --git a/openedx_webhooks/tasks/pr_tracking.py b/openedx_webhooks/tasks/pr_tracking.py index 93c33549..ca5e8a40 100644 --- a/openedx_webhooks/tasks/pr_tracking.py +++ b/openedx_webhooks/tasks/pr_tracking.py @@ -10,8 +10,6 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Set, Tuple, cast -from glom import glom - from openedx_webhooks import settings from openedx_webhooks.bot_comments import ( BOT_COMMENT_INDICATORS, @@ -49,6 +47,7 @@ is_private_repo_no_cla_pull_request, jira_project_for_blended, jira_project_for_ospr, + projects_for_pr, pull_request_has_cla, ) from openedx_webhooks.labels import ( @@ -299,6 +298,8 @@ def desired_support_state(pr: PrDict) -> Optional[PrDesiredInfo]: assert settings.GITHUB_OSPR_PROJECT, "You must set GITHUB_OSPR_PROJECT" desired.github_projects.add(settings.GITHUB_OSPR_PROJECT) + desired.github_projects.update(projects_for_pr(pr)) + has_signed_agreement = pull_request_has_cla(pr) if is_bot: desired.cla_check = CLA_STATUS_BOT diff --git a/tests/repo_data/anotherorg/multi-project/catalog-info.yaml b/tests/repo_data/anotherorg/multi-project/catalog-info.yaml new file mode 100644 index 00000000..b2a20a6b --- /dev/null +++ b/tests/repo_data/anotherorg/multi-project/catalog-info.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: 'Multi-project' + description: 'Some other thing' + annotations: + openedx.org/add-to-projects: "openedx:23, anotherorg:17" +spec: + type: 'service' + lifecycle: 'production' + owner: someone-else diff --git a/tests/repo_data/openedx/credentials/catalog-info.yaml b/tests/repo_data/openedx/credentials/catalog-info.yaml new file mode 100644 index 00000000..00df470c --- /dev/null +++ b/tests/repo_data/openedx/credentials/catalog-info.yaml @@ -0,0 +1,22 @@ +# This file records information about this repo. Its use is described in OEP-55: +# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: 'Credentials' + description: 'The Open edX Credentials Service, which supports course and program certificates.' + links: + - url: 'https://edx-credentials.readthedocs.io/' + title: 'Documentation' + icon: 'Article' + annotations: + # (Optional) Annotation keys and values can be whatever you want. + # We use it in Open edX repos to have a comma-separated list of GitHub user + # names that might be interested in changes to the architecture of this + # component. + openedx.org/arch-interest-groups: "" + openedx.org/add-to-projects: "openedx:23" +spec: + type: 'service' + lifecycle: 'production' + owner: 2U-aperture diff --git a/tests/repo_data/openedx/edx-platform/catalog-info.yaml b/tests/repo_data/openedx/edx-platform/catalog-info.yaml new file mode 100644 index 00000000..bb1f294c --- /dev/null +++ b/tests/repo_data/openedx/edx-platform/catalog-info.yaml @@ -0,0 +1,14 @@ +# This file records information about this repo. Its use is described in OEP-55: +# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html + +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: 'edx-platform' + description: "the original ball of mud" + annotations: + openedx.org/arch-interest-groups: "" +spec: + owner: group:arch-bom + type: 'service' + lifecycle: 'production' diff --git a/tests/test_pull_request_opened.py b/tests/test_pull_request_opened.py index 22fafdaf..cb1a2107 100644 --- a/tests/test_pull_request_opened.py +++ b/tests/test_pull_request_opened.py @@ -901,3 +901,29 @@ def test_extra_fields_are_ok(fake_github, fake_jira): assert len(pr.list_comments()) == 1 # The issue should still have the ad-hoc label. assert "my-label" in issue.labels + + +def test_dont_add_internal_prs_to_project(fake_github, fake_jira): + pr = fake_github.make_pull_request(owner="openedx", repo="credentials", user="nedbat") + pull_request_changed(pr.as_json()) + assert pull_request_projects(pr.as_json()) == set() + + +def test_add_external_prs_to_project(fake_github, fake_jira): + pr = fake_github.make_pull_request(owner="openedx", repo="credentials", user="tusbar") + pull_request_changed(pr.as_json()) + assert pull_request_projects(pr.as_json()) == {settings.GITHUB_OSPR_PROJECT, ("openedx", 23)} + + +def test_dont_add_draft_prs_to_project(fake_github, fake_jira): + pr = fake_github.make_pull_request(owner="openedx", repo="credentials", user="tusbar", draft=True) + pull_request_changed(pr.as_json()) + assert pull_request_projects(pr.as_json()) == {settings.GITHUB_OSPR_PROJECT} + + +def test_add_to_multiple_projects(fake_github, fake_jira): + pr = fake_github.make_pull_request(owner="anotherorg", repo="multi-project", user="tusbar") + pull_request_changed(pr.as_json()) + assert pull_request_projects(pr.as_json()) == { + settings.GITHUB_OSPR_PROJECT, ("openedx", 23), ("anotherorg", 17), + }