Public Access
DEVX-171: feat(ci): dep PRs carry target-repo task ID and generated spec
This commit was merged in pull request #337.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# DEVX-171: create_dependency_pr uses target-project task ID + generated spec
|
||||
|
||||
## Problem
|
||||
|
||||
Dep PRs fail target-repo validation: infra requires `OBL-INFRA-N` in the
|
||||
branch name and `docs/specs/<TASK>.md` in the diff. The tool created the
|
||||
Vikunja task in the *producer's* project and named the branch
|
||||
`deps/<pkg>-<ver>` with no task ID, and wrote no spec.
|
||||
|
||||
## Approach
|
||||
|
||||
REQ-1: Create the tracking task before branch creation, in the target
|
||||
repo's Vikunja project via `--task-project-id` (default:
|
||||
DEVX_VIKUNJA_PROJECT_ID). Embed the returned identifier in the branch
|
||||
name (`deps/<TASK>-<pkg>-<ver>`) and PR title, and commit a generated
|
||||
`docs/specs/<TASK>.md` with the required sections alongside the pin bump.
|
||||
|
||||
## Test Plan
|
||||
|
||||
- Branch name and PR title carry the target task ID; spec file added.
|
||||
- `create_vikunja_task(project_id=N)` passes N to VikunjaClient.
|
||||
- Existing no-task behavior preserved when Vikunja is unreachable.
|
||||
|
||||
## Deploy Plan
|
||||
|
||||
devx release tag; producer workflows pass `--task-project-id` for infra
|
||||
(project 3).
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Revert; dep PRs keep failing target validation (status quo).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] REQ-1: `--task-project-id` supported; branch/spec/PR title embed
|
||||
the target task ID; covered by unit tests at 100% coverage.
|
||||
@@ -192,7 +192,7 @@ def read_manifest_version(file_path: str, section: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def create_vikunja_task(title: str, description: str) -> str | None:
|
||||
def create_vikunja_task(title: str, description: str, project_id: int = 0) -> str | None:
|
||||
"""Create a Vikunja task and return its identifier (e.g., OBL-INFRA-531)."""
|
||||
try:
|
||||
token = get_vikunja_token()
|
||||
@@ -202,7 +202,7 @@ def create_vikunja_task(title: str, description: str) -> str | None:
|
||||
from devx.api_clients import VikunjaClient
|
||||
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
task = client.create_task(VIKUNJA_PROJECT_ID, title=title, description=description)
|
||||
task = client.create_task(project_id or VIKUNJA_PROJECT_ID, title=title, description=description)
|
||||
return str(task.get("identifier", ""))
|
||||
|
||||
|
||||
@@ -243,6 +243,15 @@ def create_vikunja_task(title: str, description: str) -> str | None:
|
||||
"__version__ tag vs a release git tag."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--task-project-id",
|
||||
type=int,
|
||||
default=0,
|
||||
help=_(
|
||||
"Vikunja project for the tracking task (default: DEVX_VIKUNJA_PROJECT_ID). "
|
||||
"Use the target repo's project so the generated branch/spec satisfy its validation."
|
||||
),
|
||||
)
|
||||
@click.option("--dry-run", is_flag=True, default=False, help=_("Show what would be done without creating PR"))
|
||||
def cli(
|
||||
repo: str,
|
||||
@@ -254,6 +263,7 @@ def cli(
|
||||
verify_container: str,
|
||||
source_ref: str,
|
||||
container_tag: str,
|
||||
task_project_id: int,
|
||||
dry_run: bool,
|
||||
) -> None:
|
||||
"""Create an infra PR to bump a pinned dependency version."""
|
||||
@@ -332,8 +342,20 @@ def cli(
|
||||
click.echo(f"[dep-pr] DRY RUN: would update {changed_file} and create PR")
|
||||
return
|
||||
|
||||
# Create a branch
|
||||
branch_name = f"deps/{package}-{new_version}"
|
||||
# Implements: REQ-1 — create the tracking task in the *target* repo's
|
||||
# Vikunja project so its identifier satisfies the target's branch/PR-title
|
||||
# validation (e.g., OBL-INFRA-N for oblachno/infra).
|
||||
task_title = f"Bump {package} to {new_version}"
|
||||
task_desc = (
|
||||
f"<p>Auto-created dependency bump PR.</p>"
|
||||
f"<p>Package: {package}</p>"
|
||||
f"<p>Version: {old_version} → {new_version}</p>"
|
||||
f"<p>Source: {source_repo} (run #{source_run_id})</p>"
|
||||
)
|
||||
task_id = create_vikunja_task(task_title, task_desc, task_project_id)
|
||||
|
||||
# Create a branch — embed the task ID so target-repo validation accepts it.
|
||||
branch_name = f"deps/{task_id}-{package}-{new_version}" if task_id else f"deps/{package}-{new_version}"
|
||||
base_branch = "master"
|
||||
|
||||
# Check for existing PR (reuse from tools.create_pr)
|
||||
@@ -390,7 +412,32 @@ def cli(
|
||||
raise click.ClickException(_("Failed to update {file}", file=changed_file))
|
||||
|
||||
assert changed_file is not None # nosec B101 — narrowed by the early exit above
|
||||
subprocess.run(["git", "add", changed_file], check=True, cwd=workdir) # nosec B603 B607
|
||||
add_files = [changed_file]
|
||||
if task_id:
|
||||
# Implements: REQ-1 — spec-driven validation requires a spec file.
|
||||
spec_rel = f"docs/specs/{task_id}.md"
|
||||
spec_file = workdir / spec_rel
|
||||
spec_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
spec_file.write_text(
|
||||
f"# {task_id}: {task_title}\n\n"
|
||||
f"## Problem\n\n"
|
||||
f"{source_repo} released {package} {new_version}; this repo pins {old_version}.\n\n"
|
||||
f"## Approach\n\n"
|
||||
f"REQ-1: Update `{changed_file}` to pin {package} {new_version} "
|
||||
f"(auto-generated dependency PR).\n\n"
|
||||
f"## Test Plan\n\n"
|
||||
f"- Producer release CI verified the artifact "
|
||||
f"({source_repo} run #{source_run_id or 'n/a'}).\n\n"
|
||||
f"## Deploy Plan\n\n"
|
||||
f"Merge updates the pin; the next deploy applies it.\n\n"
|
||||
f"## Rollback Plan\n\n"
|
||||
f"Revert the pin bump.\n\n"
|
||||
f"## Acceptance Criteria\n\n"
|
||||
f"- [x] REQ-1: `{changed_file}` pins {package} {new_version}.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
add_files.append(spec_rel)
|
||||
subprocess.run(["git", "add", *add_files], check=True, cwd=workdir) # nosec B603 B607
|
||||
commit_msg = f"deps: bump {package} from {old_version} to {new_version}"
|
||||
subprocess.run(["git", "commit", "-m", commit_msg], check=True, cwd=workdir) # nosec B603 B607
|
||||
subprocess.run( # nosec B603 B607
|
||||
@@ -399,16 +446,6 @@ def cli(
|
||||
cwd=workdir,
|
||||
)
|
||||
|
||||
# Create Vikunja task for tracking
|
||||
task_title = f"Bump {package} to {new_version}"
|
||||
task_desc = (
|
||||
f"<p>Auto-created dependency bump PR.</p>"
|
||||
f"<p>Package: {package}</p>"
|
||||
f"<p>Version: {old_version} → {new_version}</p>"
|
||||
f"<p>Source: {source_repo} (run #{source_run_id})</p>"
|
||||
)
|
||||
task_id = create_vikunja_task(task_title, task_desc)
|
||||
|
||||
# Create PR directly (dependency PRs have custom titles, not Vikunja-derived)
|
||||
pr_title = f"{task_id}: {task_title}" if task_id else task_title
|
||||
pr_body = (
|
||||
|
||||
@@ -3495,6 +3495,14 @@
|
||||
"ru": "Ошибка Vikunja API (HTTP {status}): {message}. Задача {task_id} НЕ была обновлена. Слияние прошло успешно, но задачу Vikunja нужно обновить вручную.",
|
||||
"zh": "Vikunja API 错误(HTTP {status}):{message}。任务 {task_id} 未更新。合并成功,但 Vikunja 任务需要手动更新。"
|
||||
},
|
||||
"Vikunja project for the tracking task (default: DEVX_VIKUNJA_PROJECT_ID). Use the target repo's project so the generated branch/spec satisfy its validation.": {
|
||||
"bg": "Vikunja проект за задачата за проследяване (по подразбиране: DEVX_VIKUNJA_PROJECT_ID). Използвайте проекта на целевото хранилище, за да отговарят генерираните клон/спецификация на неговата валидация.",
|
||||
"de": "Vikunja-Projekt für die Tracking-Aufgabe (Standard: DEVX_VIKUNJA_PROJECT_ID). Verwenden Sie das Projekt des Ziel-Repos, damit generierter Branch/Spec dessen Validierung bestehen.",
|
||||
"en": "Vikunja project for the tracking task (default: DEVX_VIKUNJA_PROJECT_ID). Use the target repo's project so the generated branch/spec satisfy its validation.",
|
||||
"pl": "Projekt Vikunja dla zadania śledzącego (domyślnie: DEVX_VIKUNJA_PROJECT_ID). Użyj projektu docelowego repozytorium, aby wygenerowana gałąź/specyfikacja przeszła jego walidację.",
|
||||
"ru": "Проект Vikunja для задачи отслеживания (по умолчанию: DEVX_VIKUNJA_PROJECT_ID). Используйте проект целевого репозитория, чтобы сгенерированные ветка/спецификация прошли его валидацию.",
|
||||
"zh": "用于跟踪任务的 Vikunja 项目(默认:DEVX_VIKUNJA_PROJECT_ID)。使用目标仓库的项目,以便生成的分支/规范通过其验证。"
|
||||
},
|
||||
"Vikunja task title '{title}' starts with '{prefix}:'. The task title should NOT include the '{prefix}' prefix — it is automatically added to the PR title. Update the Vikunja task title to remove the prefix.": {
|
||||
"bg": "Заглавието на задачата във Vikunja '{title}' започва с '{prefix}:'. Заглавието на задачата НЕ трябва да съдържа префикса '{prefix}' — той се добавя автоматично към заглавието на PR. Актуализирайте заглавието на задачата във Vikunja, за да премахнете префикса.",
|
||||
"de": "Der Vikunja-Aufgabentitel '{title}' beginnt mit '{prefix}:'. Der Aufgabentitel darf NICHT den Präfix '{prefix}' enthalten — er wird automatisch zum PR-Titel hinzugefügt. Aktualisieren Sie den Vikunja-Aufgabentitel, um den Präfix zu entfernen.",
|
||||
|
||||
@@ -189,6 +189,16 @@ class TestCreateVikunjaTask:
|
||||
result = create_vikunja_task("Test", "desc")
|
||||
assert result == "OBL-INFRA-999"
|
||||
|
||||
def test_explicit_project_id_used(self) -> None:
|
||||
with (
|
||||
patch("devx.ci.create_dependency_pr.get_vikunja_token", return_value="fake-token"),
|
||||
patch("devx.api_clients.VikunjaClient") as mock_client_cls,
|
||||
):
|
||||
mock_client = mock_client_cls.return_value
|
||||
mock_client.create_task.return_value = {"identifier": "OBL-INFRA-1"}
|
||||
create_vikunja_task("Test", "desc", project_id=3)
|
||||
assert mock_client.create_task.call_args.args[0] == 3
|
||||
|
||||
|
||||
class TestResolveContainerDigest:
|
||||
"""REQ-1: pre-PR artifact verification via the packages API."""
|
||||
@@ -581,3 +591,33 @@ class TestBranchCreation:
|
||||
calls = [c.args[0] for c in subprocess.run.call_args_list]
|
||||
assert ["git", "fetch", "origin", "deps/sso_bridge-0.9.1:refs/remotes/origin/deps/sso_bridge-0.9.1"] in calls
|
||||
assert ["git", "checkout", "-B", "deps/sso_bridge-0.9.1", "origin/deps/sso_bridge-0.9.1"] in calls
|
||||
|
||||
@patch("devx.ci.create_dependency_pr.create_vikunja_task", return_value="OBL-INFRA-581")
|
||||
@patch("devx.ci.create_dependency_pr.update_manifest", return_value=True)
|
||||
@patch("devx.ci.create_dependency_pr.read_manifest_version", return_value="0.9.0")
|
||||
@patch("devx.ci.create_dependency_pr.find_existing_pr", return_value=None)
|
||||
@patch("devx.ci.create_dependency_pr.GiteaClient")
|
||||
@patch("devx.ci.create_dependency_pr.get_ci_token", return_value="tok")
|
||||
def test_task_id_in_branch_and_spec_committed(
|
||||
self,
|
||||
_token: MagicMock,
|
||||
mock_client_cls: MagicMock,
|
||||
_find: MagicMock,
|
||||
_read: MagicMock,
|
||||
_update: MagicMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
import subprocess
|
||||
|
||||
client = self._client(mock_client_cls)
|
||||
result = self._invoke()
|
||||
assert result.exit_code == 0
|
||||
# Branch embeds the target-repo task ID
|
||||
post = client._request.call_args
|
||||
assert post.kwargs["json"]["new_branch_name"] == "deps/OBL-INFRA-581-sso_bridge-0.9.1"
|
||||
# Spec file added alongside the manifest change
|
||||
add = next(c for c in subprocess.run.call_args_list if c.args[0][:2] == ["git", "add"])
|
||||
assert "docs/specs/OBL-INFRA-581.md" in add.args[0]
|
||||
# PR title carries the task ID
|
||||
assert client.create_pr.call_args.kwargs["title"] == "OBL-INFRA-581: Bump sso_bridge to 0.9.1"
|
||||
mock_task.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user