From 3417acff8513cd16df58d8b431ca6f8afd15c516 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 14 Oct 2026 20:02:54 +0300 Subject: [PATCH] Record the state of linked merge requests and show it on the page (schema 7) Closes #30. --- README.md | 17 ++++++--- release_scope/_report.py | 6 ++-- release_scope/_static/index.html | 62 ++++++++++++++++++++++++++------ release_scope/_use_case.py | 43 ++++++++++++++++++++-- skills/release-scope/SKILL.md | 8 +++-- tests/test_cli.py | 2 +- tests/test_scope.py | 17 +++++++++ tests/test_use_case.py | 58 ++++++++++++++++++++++++++++++ 8 files changed, 190 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 783d6cb..9c7c65d 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ The report is versioned by `schema_version`; the models live in [`release_scope/_report.py`](https://github.com/modern-python/release-scope/blob/main/release_scope/_report.py). Top-level `jira` is `null` without a Jira token; otherwise it holds `issues` by key (summary, status, status category, issue type, linked GitLab changes), the `missing` keys Jira did not return, and an `error` if a Jira request failed. +A linked merge request in a collected service has its `state` (`opened`, `closed`, `locked`, or `merged`) and +`target_branch`. When a row holds it, it is merged into the default branch and costs no request; otherwise it is read +from GitLab, and a merged one is cached. The state stays `null` for a commit, for a project outside the report, and when +the merge request cannot be read. Warnings and errors, on services and in `jira`, are messages: a `code`, its `params`, and the English `text`. Each service lists its `candidates`: the tags a release could ship, newest first, each with its pipeline, the number of rows it ships, the in-scope Jira keys of those rows, and the compare link from production, or the tag's commit history @@ -151,12 +155,15 @@ reload starts from the release tags again. **Issues**, at the top of the Release column when the report has Jira issues, picks tags from issues instead. It lists every issue of the report, Done first and then the rest, by Jira's status category, and filters them by key or summary or shows only the chosen ones. Under each issue are the services it touches: the tag that ships it, `no tag -yet`, `in prod or not merged` when it links to a merge request or commit outside the range, `not in this report`, or -`failed to collect`. Ticking an issue picks, in every service it touches, the tag a `--jira` run would make its +yet`, `not in this report`, `failed to collect`, or, when it links only to changes outside the range, why: `not +merged` while a linked merge request is open, `in prod` when one was merged into the default branch (`older than the +range` when the range was cut), `merged into `, `closed`, or `in prod or not merged` for a commit or a +merge request that could not be read. Ticking an issue picks, in every service it touches, the tag a `--jira` run would make its release tag: the nearest tag at or above the newest row the issue links to or names. When several chosen issues touch a service, the newest of their tags wins; when one of them has no tag yet, the service gets no pick and its line links to create the next tag. Each touched service line names the chosen issues it is picked for and the other Jira -keys its picked tag also ships; in the list those issues are marked `ships anyway`. Services no chosen issue +keys its picked tag also ships; in the list those issues are marked `ships anyway`. It also says why a chosen issue +that links only to changes outside the range has nothing to ship there, such as `SHOP-150 not merged`. Services no chosen issue touches are dimmed, and projects the issues link to that are not in the report are listed under the services. Unticking an issue clears the picks only it needed. A pick made by hand stays until the chosen issues change, and the line says which chosen issues it does not ship. The address keeps the issues before the picks, as @@ -188,7 +195,7 @@ release-report: rules: - if: $CI_PIPELINE_SOURCE == "schedule" script: - - uvx --from 'release-scope>=0.8,<0.9' release-scope collect --group team/backend --output public || [ $? -eq 1 ] + - uvx --from 'release-scope>=0.9,<0.10' release-scope collect --group team/backend --output public || [ $? -eq 1 ] pages: true ``` @@ -227,4 +234,4 @@ npx skills add modern-python/release-scope ``` The agent reads the same environment variables as the CLI, so set them first as described under Configuration. -The skill runs `release-scope>=0.8,<0.9`, the range whose flags and report schema it describes. +The skill runs `release-scope>=0.9,<0.10`, the range whose flags and report schema it describes. diff --git a/release_scope/_report.py b/release_scope/_report.py index a637d4a..96b7820 100644 --- a/release_scope/_report.py +++ b/release_scope/_report.py @@ -5,7 +5,7 @@ import pydantic -SCHEMA_VERSION: typing.Final = 6 +SCHEMA_VERSION: typing.Final = 7 class MessageCode(enum.StrEnum): @@ -140,6 +140,8 @@ class LinkedChange(pydantic.BaseModel): url: str iid: int | None = None sha: str | None = None + state: str | None = None + target_branch: str | None = None class JiraIssue(pydantic.BaseModel): @@ -159,7 +161,7 @@ class JiraState(pydantic.BaseModel): class Report(pydantic.BaseModel): - schema_version: typing.Literal[6] = SCHEMA_VERSION + schema_version: typing.Literal[7] = SCHEMA_VERSION collected_at: dt.datetime production_environment: str services: list[Service] diff --git a/release_scope/_static/index.html b/release_scope/_static/index.html index ec7a86c..1beb4a3 100644 --- a/release_scope/_static/index.html +++ b/release_scope/_static/index.html @@ -105,7 +105,9 @@ .cell { padding: 0 6px; border-radius: 4px; font-size: 12px; white-space: nowrap; } .cell-tag { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); } .cell-untagged { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); } - .cell-elsewhere { background: color-mix(in srgb, var(--good) 14%, transparent); color: var(--good); } + .cell-production, .cell-older { background: color-mix(in srgb, var(--good) 14%, transparent); color: var(--good); } + .cell-open, .cell-branch { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); } + .cell-closed, .cell-elsewhere { background: var(--panel); color: var(--muted); } .cell-outside, .cell-failed { background: color-mix(in srgb, var(--bad) 12%, transparent); color: var(--bad); } .issue-tools { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0; } .issue-tools input { flex: 1; min-width: 140px; font: inherit; padding: 2px 8px; border: 1px solid var(--border); border-radius: 6px; @@ -205,8 +207,8 @@

x-text="service.untagged && service.untagged.next_tag ? t('createTag', { name: service.untagged.next_tag }) : t('createAnyTag')"> -