diff --git a/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index 108ba7102..2bc9d9b5d 100644 --- a/docs/internals/requirements/requirements.rst +++ b/docs/internals/requirements/requirements.rst @@ -1085,19 +1085,21 @@ is described in the platform. .. tool_req:: Decision Record Type :id: tool_req__docs_dec_rec_type :tags: Process / Other - :version: 1 + :version: 2 :implemented: YES :satisfies: gd_req__process_management_build_blocks_attr[version==1], gd_req__process_management_build_blocks_link[version==1], - Docs-as-Code shall support the Decision Record (dec_rec) need type with the following options: + Docs-as-Code shall support the Decision Record (dec_rec) need type with the following mandatory options: + * ``id`` (mandatory, pattern: ``dec_rec____``; category and slug contain letters, numbers, underscores, or hyphens) * ``status`` (mandatory, one of: proposed, accepted, deprecated, rejected, superseded) * ``context`` (mandatory) * ``decision`` (mandatory) - * ``consequences`` (optional) - * ``tracking`` (optional, link to a GitHub issue) + * ``tracking`` (mandatory, link to a GitHub issue or pull request in the ``eclipse-score`` organization) + * ``version`` (mandatory) + * ``consequences`` (mandatory, non-empty) A Decision Record shall be able to link to other needs via the optional ``affects`` link. diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 88db2a044..c39b35f09 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -39,6 +39,10 @@ logger = logging.get_logger(__name__) env = Environment() +GITHUB_TRACKING_LINK_REGEX = ( + r"(?Phttps://github\.com/eclipse-score/" + r"(?P[^/]+)/(?:issues|pull)/(?P\d+))" +) local_check_function = Callable[[Sphinx, NeedItem, CheckLogger], None] graph_check_function = Callable[[Sphinx, NeedsView, CheckLogger], None] @@ -282,7 +286,7 @@ def setup(app: Sphinx) -> dict[str, str | bool]: app.config.needs_string_links.setdefault( "github_issue_linker", { - "regex": r"(?Phttps://github\.com/[^/]+/(?P[^/]+)/issues/(?P\d+))", + "regex": GITHUB_TRACKING_LINK_REGEX, "link_url": "{{url}}", "link_name": "{{repo}}#{{number}}", "options": github_issue_options, diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 214aa2e67..563b2f319 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -1051,14 +1051,15 @@ needs_types: dec_rec: title: Decision Record mandatory_options: + id: ^dec_rec__[0-9a-zA-Z_-]+__[0-9a-zA-Z_-]+$ status: ^(proposed|accepted|deprecated|rejected|superseded)$ - context: ^.+$ - decision: ^.+$ - optional_options: - consequences: ^.*$ - tracking: ^https://github\.com/[^/]+/[^/]+/issues/\d+$ + context: ^[\s\S]+$ + decision: ^[\s\S]+$ + tracking: ^https://github\.com/eclipse-score/[^/]+/(?:issues|pull)/\d+(?:#[A-Za-z0-9_-]+)?$ + consequences: ^[\s\S]+$ optional_links: affects: ANY + parts: 3 # Extra link types, which shall be available and allow need types to be linked to each other. # We use a dedicated linked type for each type of a connection, for instance from diff --git a/src/extensions/score_metamodel/tests/rst/options/test_dec_rec.rst b/src/extensions/score_metamodel/tests/rst/options/test_dec_rec.rst index f60175998..c2fbca1e5 100644 --- a/src/extensions/score_metamodel/tests/rst/options/test_dec_rec.rst +++ b/src/extensions/score_metamodel/tests/rst/options/test_dec_rec.rst @@ -14,24 +14,72 @@ .. test_metadata:: Test Decision Record Type :id: test_metadata__dec_rec - :partially_verifies_list: tool_req__docs_dec_rec_type + :fully_verifies_list: tool_req__docs_dec_rec_type[version==2] :test_type: requirements_based :derivation_technique: requirements_based - Tests that the dec_rec need type with its mandatory attributes and the - affects link is supported. + Tests that the dec_rec need type supports its mandatory attributes and affects + link, and that its tracking option accepts GitHub issue and pull request links. .. dec_rec:: Test Decision Record 1 - :id: dec_rec__test__decision_record_1 + :id: dec_rec__platform__feature_delivery :status: accepted :context: A decision record is needed for the test. :decision: Use a dedicated decision record need type. - :affects: dec_rec__test__decision_record_2 + :tracking: https://github.com/eclipse-score/docs-as-code/issues/1 + :version: 1 + :consequences: Provides traceability for decisions. + :affects: dec_rec__feature__follow_up .. dec_rec:: Test Decision Record 2 - :id: dec_rec__test__decision_record_2 + :id: dec_rec__feature__follow_up :status: proposed :context: This decision record is affected by another decision. :decision: Keep the relationship explicit. + :tracking: https://github.com/eclipse-score/score/pull/2#discussion_r3 + :expect_not: dec_rec__feature__follow_up.tracking (https://github.com/eclipse-score/score/pull/2#discussion_r3): does not follow pattern + :version: 1 + :consequences: Allows linking between related decisions. + + +.. dec_rec:: Decision Record with missing tracking + :id: dec_rec__logging__no_tracking + :status: accepted + :context: The tracking link is missing. + :decision: Require tracking for every decision record. + :version: 1 + :consequences: Decisions must refer to an issue or pull request. + :expect: dec_rec__logging__no_tracking: is missing required attribute: `tracking`. + + +.. dec_rec:: Decision Record with missing consequences + :id: dec_rec__logging__no_consequences + :status: accepted + :context: The consequences are missing. + :decision: Require consequences for every decision record. + :tracking: https://github.com/eclipse-score/score/pull/2 + :version: 1 + :expect: dec_rec__logging__no_consequences: is missing required attribute: `consequences`. + + +.. dec_rec:: Decision Record with invalid tracking organization + :id: dec_rec__dac__invalid_tracking_org + :status: accepted + :context: Tracking links must belong to the eclipse-score organization. + :decision: Reject links owned by other organizations. + :tracking: https://github.com/other-org/score/pull/3 + :version: 1 + :consequences: Tracking is restricted to the eclipse-score organization. + :expect: dec_rec__dac__invalid_tracking_org.tracking (https://github.com/other-org/score/pull/3): does not follow pattern + + +.. dec_rec:: Decision Record with missing status + :id: dec_rec__proc__missing_status + :context: The status is missing. + :decision: Required fields remain enforced. + :tracking: https://github.com/eclipse-score/score/issues/1 + :version: 1 + :consequences: Every decision record must declare its status. + :expect: dec_rec__proc__missing_status: is missing required attribute: `status`. diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index ff7e08b25..fe09ee84d 100644 --- a/src/extensions/score_metamodel/tests/test_metamodel_load.py +++ b/src/extensions/score_metamodel/tests/test_metamodel_load.py @@ -10,16 +10,67 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +import re from pathlib import Path from unittest.mock import mock_open, patch import pytest from attribute_plugin import add_test_properties # type: ignore[import-untyped] -from score_metamodel import ProhibitedWordCheck, load_metamodel_data +from score_metamodel import ( + GITHUB_TRACKING_LINK_REGEX, + ProhibitedWordCheck, + load_metamodel_data, +) MODEL_DIR = Path(__file__).absolute().parent / "model" +@pytest.mark.parametrize( + ("url", "expected_url", "expected_repo", "expected_number"), + [ + ( + "https://github.com/eclipse-score/docs-as-code/issues/1", + "https://github.com/eclipse-score/docs-as-code/issues/1", + "docs-as-code", + "1", + ), + ( + "https://github.com/eclipse-score/score/pull/2", + "https://github.com/eclipse-score/score/pull/2", + "score", + "2", + ), + ( + "https://github.com/eclipse-score/score/pull/2#discussion_r3", + "https://github.com/eclipse-score/score/pull/2", + "score", + "2", + ), + ], +) +def test_github_tracking_link_regex_matches_issue_pr_and_review_urls( + url: str, expected_url: str, expected_repo: str, expected_number: str +) -> None: + match = re.search(GITHUB_TRACKING_LINK_REGEX, url) + + assert match is not None + assert match.groupdict() == { + "url": expected_url, + "repo": expected_repo, + "number": expected_number, + } + + +def test_github_tracking_link_regex_rejects_other_organizations() -> None: + assert ( + re.search( + GITHUB_TRACKING_LINK_REGEX, + "https://github.com/other-org/score/pull/2", + ) + is None + ) + + def load_model_data(model_file: str) -> str: print(f"Loading model data from {model_file}") model_path = Path(MODEL_DIR) / model_file