From 89f272de4cfa037914247399879a76f55dd5e253 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 12:50:51 -0300 Subject: [PATCH 01/17] Document PR Sync design and main baseline --- docs/repo/pr-sync.md | 180 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/repo/pr-sync.md diff --git a/docs/repo/pr-sync.md b/docs/repo/pr-sync.md new file mode 100644 index 0000000..3b98f20 --- /dev/null +++ b/docs/repo/pr-sync.md @@ -0,0 +1,180 @@ +# PR Sync + +## Status + +This document defines the design contract for the planned **PR Sync** automation. + +The `feat/pr-sync` branch was fast-forwarded to the current `main` baseline before this document was added. At this stage, the branch documents intended behavior only; PR Sync is not yet implemented. + +PR Sync is the generic GitHub Project Setup successor to the repository-specific workflow previously called **PR Hygiene** in the Take Your Pills reference repository. + +## Purpose + +PR Sync keeps an implementation pull request and its linked GitHub work items aligned after PR Guardrails has resolved and validated the pull request context. + +The intended execution order is: + +```text +Pull request event + | + v +PR Guardrails + - resolve context + - autofill resolvable metadata + - validate branch/body contract + | + v +PR Sync + - synchronize linked task/issue metadata + - synchronize parent/sub-issue relationship + - synchronize Project v2 membership and status +``` + +PR Sync must not replace PR Guardrails. Guardrails establishes trusted context; PR Sync consumes that context and applies synchronization. + +## Planned responsibilities + +PR Sync is expected to support the following operations when enabled by repository configuration: + +- resolve the implementation task from the PR closing reference (`Closes #N`, `Fixes #N`, or `Resolves #N`); +- copy configured label families from the linked task to the pull request; +- synchronize the pull request milestone with the linked task or resolved delivery context; +- synchronize assignees when repository policy enables it; +- ensure a task that declares a parent Story is linked as a GitHub sub-issue when possible; +- add the linked task to the configured GitHub Project v2 when it is not already present; +- synchronize the configured Project v2 status from the pull request lifecycle; +- maintain one marked/sticky PR Sync status comment rather than creating duplicate comments. + +## Default lifecycle mapping + +The generic default mapping should be configurable, but the initial contract is: + +| Pull request state | Project status target | +| --- | --- | +| Draft / converted to draft | `In progress` | +| Ready for review / open validated PR | `In review` | +| Closed without merge | `In progress` | +| Merged | `Done` | + +Repositories may use different option names. PR Sync must resolve configured aliases/options rather than hard-code a Take Your Pills-specific Project schema. + +## Promotion pull requests + +PR Sync is primarily intended for implementation pull requests. Promotion pull requests are infrastructure transitions: + +```text +develop -> Q.A -> main +``` + +They must not be treated as implementation tasks by default. Unless a target repository explicitly enables release/promotion synchronization, PR Sync should skip task-level synchronization for these promotion paths. + +This separation prevents a release promotion PR from accidentally inheriting labels, assignees, sub-issue relationships, or Project status intended for a single implementation task. + +## Configuration direction + +The exact configuration schema will be finalized during implementation. The intended shape is equivalent to: + +```json +{ + "prAutomation": { + "sync": { + "enabled": true, + "syncLabels": true, + "syncMilestone": true, + "syncAssignees": true, + "linkSubissues": true, + "syncProject": true, + "skipPromotionPullRequests": true, + "projectStatus": { + "draft": "In progress", + "review": "In review", + "closed": "In progress", + "merged": "Done" + } + } + } +} +``` + +This is a design example, not yet a committed configuration schema. + +## Authentication and permissions + +Repository-scoped PR/issue synchronization should prefer the GitHub Actions token with the minimum required permissions. Project v2 operations may require `PROJECT_SETUP_PAT` depending on the repository owner, Project location, and token capabilities. + +The workflow must never expose PAT values in logs or comments. + +A failure to synchronize an optional surface such as Project v2 should produce a clear diagnostic and must not silently corrupt or overwrite unrelated PR metadata. + +## Security model + +The planned workflow should use trusted automation definitions (`pull_request_target` or a trusted follow-up workflow) only when it does not execute untrusted pull-request code. + +PR Sync must: + +- check out trusted base-branch automation when code checkout is required; +- avoid executing files from an untrusted PR head under elevated write permissions; +- use minimum GitHub token permissions; +- be idempotent for labels, milestone, assignees, Project membership, status, and sticky comments; +- handle `403`/permission failures as explicit synchronization diagnostics rather than masking the original validation result with an unrelated traceback. + +## Relationship with existing GPA capabilities + +GitHub Project Setup already contains reusable lower-level operations that PR Sync can compose, including: + +- issue and PR label handling; +- milestone APIs; +- sub-issue linking support; +- Project v2 owner resolution; +- Project item creation and field synchronization; +- PR validation and marked validation comments. + +The PR Sync feature should reuse these primitives instead of duplicating GitHub API implementations. + +## Relationship with PR Guardrails + +PR Guardrails is responsible for determining what can be inferred safely before validation. PR Sync should run only after that stage has successfully established the PR context. + +A typical implementation PR should therefore become: + +```text +feat/US-12-example + | + v +PR Guardrails + Story / task / milestone resolution + body autofill + validation + | + v +PR Sync + task -> PR labels/milestone/assignee + Story <-> task relationship + task -> Project v2 + PR lifecycle -> Project Status +``` + +## Validation requirements before promotion + +Implementation of PR Sync must include automated coverage for at least: + +- missing linked task; +- linked item is a pull request instead of an issue/task; +- existing versus missing labels; +- milestone synchronization; +- assignee synchronization and disabled-assignee mode; +- parent/sub-issue already linked versus missing; +- Project item already present versus missing; +- Project status transitions for draft, review, closed, and merged states; +- missing Project PAT or insufficient Project permission; +- promotion PR skip behavior; +- repeated execution without duplicate side effects; +- same-repository and fork safety boundaries. + +Live Project v2 behavior should use the dedicated Q.A sandbox rather than the source repository. + +## Naming + +The generic feature and workflow name is **PR Sync**. + +Do not use `PR Hygiene` in new GPA code, workflows, CLI commands, configuration, or documentation except when referring historically to the Take Your Pills implementation from which behavior was evaluated. From 0cfd2a1c290ebadbf9a7a2db241c7349ffbc0007 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 12:51:23 -0300 Subject: [PATCH 02/17] Add Portuguese PR Sync design documentation --- docs/repo/pr-sync.pt-BR.md | 180 +++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/repo/pr-sync.pt-BR.md diff --git a/docs/repo/pr-sync.pt-BR.md b/docs/repo/pr-sync.pt-BR.md new file mode 100644 index 0000000..1c24cf6 --- /dev/null +++ b/docs/repo/pr-sync.pt-BR.md @@ -0,0 +1,180 @@ +# PR Sync + +## Status + +Este documento define o contrato de design da futura automação **PR Sync**. + +A branch `feat/pr-sync` foi atualizada por fast-forward para o baseline atual de `main` antes da inclusão desta documentação. Neste estágio, a branch documenta o comportamento pretendido; a PR Sync ainda não está implementada. + +PR Sync é o sucessor genérico, dentro do GitHub Project Setup, do workflow específico de repositório anteriormente chamado **PR Hygiene** no projeto de referência Take Your Pills. + +## Objetivo + +PR Sync mantém um pull request de implementação e seus itens de trabalho vinculados no GitHub sincronizados depois que o PR Guardrails resolve e valida o contexto do PR. + +A ordem pretendida é: + +```text +Evento de pull request + | + v +PR Guardrails + - resolve o contexto + - preenche metadata determinável + - valida branch/body + | + v +PR Sync + - sincroniza task/issue vinculada com o PR + - sincroniza relação pai/sub-issue + - sincroniza Project v2 e status +``` + +PR Sync não substitui PR Guardrails. Guardrails estabelece o contexto confiável; PR Sync consome esse contexto e aplica a sincronização. + +## Responsabilidades planejadas + +Quando habilitada pela configuração do repositório, PR Sync deverá poder: + +- resolver a task de implementação através de `Closes #N`, `Fixes #N` ou `Resolves #N`; +- copiar famílias configuradas de labels da task para o PR; +- sincronizar o milestone do PR com a task ou contexto de entrega resolvido; +- sincronizar assignees quando permitido pela política do repositório; +- garantir que uma task com Story pai seja vinculada como sub-issue quando possível; +- adicionar a task ao GitHub Project v2 configurado quando ainda não estiver presente; +- sincronizar o Status do Project v2 conforme o ciclo de vida do PR; +- manter um único comentário marcado/sticky de PR Sync, sem duplicações. + +## Mapeamento padrão do ciclo de vida + +O mapeamento deve ser configurável. O contrato inicial é: + +| Estado do PR | Status alvo no Project | +| --- | --- | +| Draft / convertido para draft | `In progress` | +| Ready for review / PR aberto e validado | `In review` | +| Fechado sem merge | `In progress` | +| Merged | `Done` | + +Repositórios podem utilizar nomes de opções diferentes. PR Sync deve resolver aliases/opções configuradas em vez de depender de um schema específico do Take Your Pills. + +## Pull requests de promoção + +PR Sync é voltado principalmente a pull requests de implementação. PRs de promoção são transições de infraestrutura: + +```text +develop -> Q.A -> main +``` + +Por padrão eles não devem ser tratados como tasks de implementação. A menos que o repositório habilite explicitamente sincronização de release/promoção, PR Sync deve ignorar sincronizações de task nesses caminhos. + +Isso impede que um PR de promoção herde labels, assignees, relações de sub-issue ou status de Project pertencentes a uma única task de implementação. + +## Direção de configuração + +O schema definitivo será fechado durante a implementação. O formato pretendido é equivalente a: + +```json +{ + "prAutomation": { + "sync": { + "enabled": true, + "syncLabels": true, + "syncMilestone": true, + "syncAssignees": true, + "linkSubissues": true, + "syncProject": true, + "skipPromotionPullRequests": true, + "projectStatus": { + "draft": "In progress", + "review": "In review", + "closed": "In progress", + "merged": "Done" + } + } + } +} +``` + +Este trecho é um exemplo de design e ainda não representa um schema de configuração consolidado. + +## Autenticação e permissões + +Sincronizações de PR/issue restritas ao repositório devem preferir o GitHub Actions token com as permissões mínimas necessárias. Operações em Project v2 podem exigir `PROJECT_SETUP_PAT`, dependendo do owner, localização do Project e capacidades do token. + +Valores de PAT nunca devem aparecer em logs ou comentários. + +Falhas em superfícies opcionais, como Project v2, devem produzir diagnóstico claro e não podem sobrescrever ou corromper metadata não relacionada do PR. + +## Modelo de segurança + +O workflow planejado deve usar automação confiável (`pull_request_target` ou workflow subsequente confiável) apenas quando não executar código não confiável do head do PR. + +PR Sync deve: + +- usar automação da base confiável quando checkout for necessário; +- não executar arquivos do head não confiável com permissões elevadas; +- usar o mínimo de permissões possível; +- ser idempotente para labels, milestone, assignees, Project, status e comentários sticky; +- tratar `403` e falhas de permissão como diagnóstico de sincronização, sem mascarar a causa original com traceback irrelevante. + +## Relação com capacidades já existentes no GPA + +O GPA já possui primitivas reutilizáveis que a PR Sync poderá compor, incluindo: + +- manipulação de labels em issues/PRs; +- APIs de milestone; +- suporte a sub-issues; +- resolução do owner do Project v2; +- criação de item e sincronização de campos no Project v2; +- validação de PR e comentários marcados de validação. + +A implementação deve reutilizar essas primitivas em vez de duplicar integrações com a API do GitHub. + +## Relação com PR Guardrails + +PR Guardrails é responsável por determinar o que pode ser inferido com segurança antes da validação. PR Sync deve rodar somente depois que esse estágio estabelecer com sucesso o contexto do PR. + +Um PR típico deve seguir: + +```text +feat/US-12-exemplo + | + v +PR Guardrails + resolução de Story / task / milestone + autofill do body + validação + | + v +PR Sync + task -> labels/milestone/assignee do PR + Story <-> task + task -> Project v2 + ciclo do PR -> Project Status +``` + +## Requisitos de validação antes da promoção + +A implementação da PR Sync deve conter cobertura automatizada para pelo menos: + +- task vinculada ausente; +- item vinculado é um PR em vez de issue/task; +- labels já existentes e ausentes; +- sincronização de milestone; +- sincronização de assignee e modo desabilitado; +- relação pai/sub-issue já existente e ausente; +- item de Project já existente e ausente; +- transições de status draft, review, closed e merged; +- PAT de Project ausente ou permissão insuficiente; +- comportamento de skip para PRs de promoção; +- execução repetida sem efeitos duplicados; +- limites de segurança para mesmo repositório e forks. + +Comportamentos live envolvendo Project v2 devem usar o sandbox dedicado de Q.A, nunca o repositório-fonte. + +## Nomenclatura + +O nome genérico da funcionalidade e do workflow é **PR Sync**. + +Não utilizar `PR Hygiene` em novos códigos, workflows, comandos CLI, configurações ou documentação do GPA, exceto em referências históricas ao comportamento avaliado no Take Your Pills. From c95a23fd93ffd76fee330d14968ad5aac6a256c7 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 12:51:52 -0300 Subject: [PATCH 03/17] Index PR Sync design documentation --- docs/DOCUMENTATION-GUIDE.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/DOCUMENTATION-GUIDE.md b/docs/DOCUMENTATION-GUIDE.md index b048fb4..966a20b 100644 --- a/docs/DOCUMENTATION-GUIDE.md +++ b/docs/DOCUMENTATION-GUIDE.md @@ -19,6 +19,7 @@ This guide maps the reusable configuration, workflows and operational documentat | Target repository instructions and recorded conventions (`AGENTS.md`, `CONTRIBUTING*`, README/docs, existing workflows/configuration) | Authoritative for repository-specific decisions and established project standards. | | `AI_SETUP_GUIDE.md` | Authoritative for the AI interaction sequence: inspect before asking, pause at checkpoints, re-verify user changes, protect credentials, and verify applied results. | | `docs/repo/qa-policy.md` / `docs/repo/qa-policy.pt-BR.md` | Authoritative for the `develop -> Q.A -> main` promotion gates, compatibility matrix, sandbox requirements, and live/manual Q.A behavior. | +| `docs/repo/pr-sync.md` / `docs/repo/pr-sync.pt-BR.md` | Design contract for the planned PR Sync feature; not an implementation-status claim until the feature lands. | | `docs/repo/project-setup-runbook.pt-BR.md` | Detailed human operational procedure and troubleshooting path. | | `README.md` / `README.pt-BR.md` | Concise user-facing overview, setup, authentication, commands, and safety model. | @@ -38,6 +39,16 @@ The AI guide does not override project-specific conventions. Its purpose is to m | `.github/workflows/main-source-branch.yml` | Allow promotion into `main` only from `Q.A` | `docs/repo/branching-policy.md`, `project_setup/pr_validation.py` | | `.github/workflows/repo-quality.yml` | Validate this tool repository | `Makefile`, `scripts/validation/repo_quality.py`, `tests/` | +## Feature design contracts + +Feature branches may carry design documentation before implementation begins. These documents must say explicitly when behavior is planned rather than implemented. + +Current design contracts: + +- `docs/repo/pr-sync.md` / `docs/repo/pr-sync.pt-BR.md`: planned PR Sync orchestration, lifecycle mapping, security model, relationship to PR Guardrails, promotion-PR exclusions, and required regression/live validation. + +When PR Sync is implemented, this guide must be updated in the same change to map the real workflow and source module rather than leaving it as a design-only contract. + ## Adding a milestone template 1. Add the milestone to `config/project/milestones.json`. @@ -65,7 +76,7 @@ project_setup.json File paths and execution defaults config/project/ Labels, milestones and Project v2 definition config/stories/ Backlog manifest .github/workflows/ Generic active workflows and Q.A gates -docs/repo/ Operational policies and runbooks +docs/repo/ Operational policies, runbooks, and feature design contracts tests/qa/ Q.A black-box, live sandbox, and guarded manual tests scripts/validation/ Cross-platform validation entrypoints tests/ Unit and installation tests From 30d8d810b1b766474b988ff18c9102f9ea1d2198 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:16:18 -0300 Subject: [PATCH 04/17] Implement reusable PR Sync engine --- project_setup/pr_sync.py | 591 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 project_setup/pr_sync.py diff --git a/project_setup/pr_sync.py b/project_setup/pr_sync.py new file mode 100644 index 0000000..8e8e167 --- /dev/null +++ b/project_setup/pr_sync.py @@ -0,0 +1,591 @@ +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import json +import os +from pathlib import Path +import re +from typing import Any + +from .github import ( + API_BASE, + GitHubClient, + GitHubRequestError, + get_project_pat, + require_client, + split_repo, +) +from .issues import add_sub_issue +from .project import ( + add_issue_to_project, + find_project, + issue_node_id, + list_project_fields, + list_project_items, + option_id, + update_single_select, +) + + +SYNC_MARKER = "" +LINKED_TASK_PATTERN = re.compile(r"\b(?:closes|fixes|resolves)\s*:?\s*#(\d+)\b", re.IGNORECASE) +PARENT_ISSUE_PATTERN = re.compile( + r"\bParent\s+(?:story|issue|task)\s*:\s*[^\n#]*#(\d+)\b", + re.IGNORECASE, +) + +DEFAULT_SYNC_CONFIG: dict[str, Any] = { + "enabled": True, + "syncLabels": True, + "labelPrefixes": ["type:", "priority:", "test:"], + "syncMilestone": True, + "syncAssignees": True, + "assignAuthorWhenTaskUnassigned": True, + "linkSubissues": True, + "syncProject": True, + "skipPromotionPullRequests": True, + "promotionPaths": [ + {"head": "develop", "base": "Q.A"}, + {"head": "Q.A", "base": "main"}, + ], + "projectStatusField": "Status", + "projectStatus": { + "draft": "In progress", + "review": "In review", + "closed": "In progress", + "merged": "Done", + }, +} + + +@dataclass(frozen=True) +class PullRequestContext: + number: int + action: str + body: str + base_ref: str + head_ref: str + head_repo: str + author: str + draft: bool + merged: bool + + +def load_event(path: str | os.PathLike[str]) -> dict[str, Any]: + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def load_sync_config(path: str | os.PathLike[str] = "project_setup.json") -> dict[str, Any]: + data = json.loads(Path(path).read_text(encoding="utf-8")) + configured = data.get("prAutomation", {}).get("sync", {}) + if configured is None: + configured = {} + if not isinstance(configured, dict): + raise ValueError("project_setup.json prAutomation.sync must be an object") + + result = dict(DEFAULT_SYNC_CONFIG) + result.update(configured) + + status = dict(DEFAULT_SYNC_CONFIG["projectStatus"]) + custom_status = configured.get("projectStatus", {}) + if custom_status is not None: + if not isinstance(custom_status, dict): + raise ValueError("prAutomation.sync.projectStatus must be an object") + status.update(custom_status) + result["projectStatus"] = status + + prefixes = result.get("labelPrefixes", []) + if not isinstance(prefixes, list) or not all(isinstance(value, str) and value for value in prefixes): + raise ValueError("prAutomation.sync.labelPrefixes must be a list of non-empty strings") + promotions = result.get("promotionPaths", []) + if not isinstance(promotions, list) or not all( + isinstance(item, dict) and item.get("head") and item.get("base") for item in promotions + ): + raise ValueError("prAutomation.sync.promotionPaths must contain {head, base} objects") + return result + + +def context_from_pull_request(pr: dict[str, Any], action: str = "") -> PullRequestContext: + head = pr.get("head") or {} + base = pr.get("base") or {} + head_repo = (head.get("repo") or {}).get("full_name") or "" + return PullRequestContext( + number=int(pr["number"]), + action=action, + body=pr.get("body") or "", + base_ref=base.get("ref") or "", + head_ref=head.get("ref") or "", + head_repo=head_repo, + author=(pr.get("user") or {}).get("login") or "", + draft=bool(pr.get("draft")), + merged=bool(pr.get("merged")), + ) + + +def context_from_event( + event: dict[str, Any], + *, + client: GitHubClient | None = None, + repo: str | None = None, +) -> PullRequestContext: + pr = event.get("pull_request") + if pr: + return context_from_pull_request(pr, event.get("action") or "") + + workflow_run = event.get("workflow_run") + if workflow_run: + if client is None or not repo: + raise RuntimeError("workflow_run PR Sync requires a GitHub client and repository") + related_prs = workflow_run.get("pull_requests") or [] + if not related_prs or related_prs[0].get("number") is None: + raise RuntimeError("Unsupported workflow_run payload: no associated pull request") + number = int(related_prs[0]["number"]) + live_pr = client.request_json("GET", f"{API_BASE}/repos/{repo}/pulls/{number}") + return context_from_pull_request(live_pr, "synchronize") + + raise RuntimeError("Unsupported event payload: expected pull_request or workflow_run") + + +def linked_task_number(body: str) -> int | None: + match = LINKED_TASK_PATTERN.search(body or "") + return int(match.group(1)) if match else None + + +def parent_issue_number(body: str) -> int | None: + match = PARENT_ISSUE_PATTERN.search(body or "") + return int(match.group(1)) if match else None + + +def is_same_repository(ctx: PullRequestContext, repo: str) -> bool: + return not ctx.head_repo or ctx.head_repo.casefold() == repo.casefold() + + +def is_promotion_pull_request(ctx: PullRequestContext, config: dict[str, Any]) -> bool: + if not config.get("skipPromotionPullRequests", True): + return False + return any( + ctx.head_ref == str(path.get("head")) and ctx.base_ref == str(path.get("base")) + for path in config.get("promotionPaths", []) + ) + + +def project_status_for_context(ctx: PullRequestContext, config: dict[str, Any]) -> str: + mapping = config["projectStatus"] + if ctx.action == "closed": + return str(mapping["merged"] if ctx.merged else mapping["closed"]) + if ctx.action == "converted_to_draft" or ctx.draft: + return str(mapping["draft"]) + return str(mapping["review"]) + + +def issue_label_names(item: dict[str, Any]) -> set[str]: + return { + str(label["name"]) + for label in item.get("labels", []) + if isinstance(label, dict) and label.get("name") + } + + +def issue_assignee_logins(item: dict[str, Any]) -> list[str]: + return [ + str(assignee["login"]) + for assignee in item.get("assignees", []) + if isinstance(assignee, dict) and assignee.get("login") + ] + + +def issue_milestone_number(item: dict[str, Any]) -> int | None: + milestone = item.get("milestone") + if isinstance(milestone, dict) and milestone.get("number") is not None: + return int(milestone["number"]) + return None + + +def add_labels(client: GitHubClient, repo: str, issue_number: int, labels: list[str]) -> None: + client.request_json( + "POST", + f"{API_BASE}/repos/{repo}/issues/{issue_number}/labels", + {"labels": labels}, + ) + + +def add_assignees(client: GitHubClient, repo: str, issue_number: int, assignees: list[str]) -> None: + client.request_json( + "POST", + f"{API_BASE}/repos/{repo}/issues/{issue_number}/assignees", + {"assignees": assignees}, + ) + + +def upsert_sync_comment( + client: GitHubClient, + repo: str, + pr_number: int, + body: str, + *, + dry_run: bool = False, +) -> None: + existing = next( + ( + comment + for comment in client.list_issue_comments(repo, pr_number) + if SYNC_MARKER in (comment.get("body") or "") + ), + None, + ) + if dry_run: + verb = "update" if existing else "create" + print(f"[DRY-RUN] Would {verb} PR Sync comment on PR #{pr_number}") + print(body) + return + if existing: + client.update_issue_comment(repo, int(existing["id"]), body) + else: + client.create_issue_comment(repo, pr_number, body) + + +def render_failure_comment(message: str) -> str: + return "\n".join( + [ + SYNC_MARKER, + "## PR Sync needs attention", + "", + message, + "", + "Use `Closes #123`, `Fixes #123`, or `Resolves #123` to identify the implementation task.", + ] + ) + + +def render_success_comment( + ctx: PullRequestContext, + task_number: int, + status: str, + project_note: str, + parent_note: str, +) -> str: + return "\n".join( + [ + SYNC_MARKER, + "## PR Sync", + "", + f"- Linked task: #{task_number}", + f"- PR lifecycle target: `{status}`", + f"- Project v2: {project_note}", + f"- Parent/sub-issue: {parent_note}", + f"- PR: #{ctx.number}", + ] + ) + + +def is_permission_error(exc: BaseException) -> bool: + if isinstance(exc, GitHubRequestError): + return exc.status in {401, 403} + text = str(exc).casefold() + return any( + marker in text + for marker in ( + "forbidden", + "resource not accessible", + "insufficient", + "permission", + "scope", + ) + ) + + +def sync_pr_metadata( + client: GitHubClient, + repo: str, + ctx: PullRequestContext, + task: dict[str, Any], + pr_issue: dict[str, Any], + config: dict[str, Any], + *, + dry_run: bool = False, +) -> None: + if config.get("syncLabels", True): + prefixes = tuple(str(value) for value in config.get("labelPrefixes", [])) + task_labels = sorted(name for name in issue_label_names(task) if name.startswith(prefixes)) + existing = issue_label_names(pr_issue) + missing = [name for name in task_labels if name not in existing] + if missing: + if dry_run: + print(f"[DRY-RUN] Would add labels to PR #{ctx.number}: {', '.join(missing)}") + else: + add_labels(client, repo, ctx.number, missing) + + if config.get("syncMilestone", True): + task_milestone = issue_milestone_number(task) + pr_milestone = issue_milestone_number(pr_issue) + if task_milestone is not None and task_milestone != pr_milestone: + if dry_run: + print(f"[DRY-RUN] Would set PR #{ctx.number} milestone to #{task_milestone}") + else: + client.update_issue(repo, ctx.number, {"milestone": task_milestone}) + + if config.get("syncAssignees", True): + task_assignees = issue_assignee_logins(task) + if not task_assignees and config.get("assignAuthorWhenTaskUnassigned", True) and ctx.author: + task_assignees = [ctx.author] + if dry_run: + print(f"[DRY-RUN] Would assign task #{task['number']} to {ctx.author}") + else: + add_assignees(client, repo, int(task["number"]), [ctx.author]) + + existing_pr_assignees = set(issue_assignee_logins(pr_issue)) + missing_assignees = [login for login in task_assignees if login not in existing_pr_assignees] + if missing_assignees: + if dry_run: + print(f"[DRY-RUN] Would assign PR #{ctx.number} to {', '.join(missing_assignees)}") + else: + add_assignees(client, repo, ctx.number, missing_assignees) + + +def sync_parent_relationship( + client: GitHubClient, + repo: str, + task: dict[str, Any], + config: dict[str, Any], + *, + dry_run: bool = False, +) -> str: + if not config.get("linkSubissues", True): + return "disabled by configuration." + parent_number = parent_issue_number(task.get("body") or "") + if parent_number is None: + return "no parent reference found." + if dry_run: + print(f"[DRY-RUN] Would link task #{task['number']} as sub-issue of #{parent_number}") + return f"would ensure #{task['number']} is a sub-issue of #{parent_number}." + try: + add_sub_issue(client, repo, parent_number, int(task["number"])) + return f"linked #{task['number']} under #{parent_number}." + except Exception as exc: + text = str(exc).casefold() + if any( + marker in text + for marker in ("already", "exists", "duplicate sub-issues", "may only have one parent") + ): + return f"already linked under a parent (requested #{parent_number})." + if is_permission_error(exc): + return f"not synchronized: token lacks permission ({exc})." + raise + + +def normalized_option(value: str) -> str: + return re.sub(r"[^a-z0-9]+", "", value.casefold()) + + +def status_option_id(field: dict[str, Any], desired: str) -> str | None: + desired_normalized = normalized_option(desired) + for option in field.get("options", []): + if normalized_option(str(option.get("name", ""))) == desired_normalized: + return str(option["id"]) + return option_id(field, desired) + + +def sync_project_status( + project_client: GitHubClient, + repo: str, + task: dict[str, Any], + project_number: int, + desired_status: str, + config: dict[str, Any], + *, + owner: str | None = None, + dry_run: bool = False, +) -> str: + if not config.get("syncProject", True): + return "disabled by configuration." + + project_owner = owner or split_repo(repo)[0] + project = find_project(project_client, project_owner, project_number) + fields = { + str(field["name"]): field + for field in list_project_fields(project_client, str(project["id"])) + if field and field.get("name") + } + field_name = str(config.get("projectStatusField") or "Status") + status_field = fields.get(field_name) + if not status_field: + raise RuntimeError(f"Project field `{field_name}` was not found") + + selected = status_option_id(status_field, desired_status) + if not selected: + available = ", ".join(str(item.get("name")) for item in status_field.get("options", [])) + raise RuntimeError( + f"Project status option `{desired_status}` was not found in `{field_name}`. " + f"Available options: {available or '(none)'}" + ) + + task_node = task.get("node_id") or issue_node_id(project_client, repo, int(task["number"])) + project_items = list_project_items(project_client, str(project["id"])) + item_id = project_items.get(str(task_node)) + if not item_id: + if dry_run: + print(f"[DRY-RUN] Would add task #{task['number']} to Project v2 #{project_number}") + item_id = f"dry-run-{task['number']}" + else: + item_id = add_issue_to_project(project_client, str(project["id"]), str(task_node)) + + if dry_run: + print( + f"[DRY-RUN] Would set task #{task['number']} `{field_name}` " + f"to `{desired_status}` in Project v2 #{project_number}" + ) + else: + update_single_select( + project_client, + str(project["id"]), + str(item_id), + str(status_field["id"]), + selected, + ) + return f"synced to `{desired_status}` in Project v2 #{project_number}." + + +def project_number_from_value(value: int | None) -> int | None: + if value is not None: + return value + raw = os.getenv("PROJECT_SETUP_PROJECT_NUMBER") + if raw is None or not raw.strip(): + return None + try: + return int(raw) + except ValueError as exc: + raise ValueError("PROJECT_SETUP_PROJECT_NUMBER must be an integer") from exc + + +def apply_pr_sync( + client: GitHubClient, + repo: str, + event: dict[str, Any], + config: dict[str, Any], + *, + project_client: GitHubClient | None = None, + project_number: int | None = None, + owner: str | None = None, + dry_run: bool = False, +) -> int: + ctx = context_from_event(event, client=client, repo=repo) + + if not config.get("enabled", True): + print("PR Sync is disabled by project_setup.json.") + return 0 + if not is_same_repository(ctx, repo): + print("Skipping PR Sync for a fork pull request.") + return 0 + if is_promotion_pull_request(ctx, config): + print(f"Skipping PR Sync for promotion PR {ctx.head_ref} -> {ctx.base_ref}.") + return 0 + + task_number = linked_task_number(ctx.body) + if task_number is None: + message = "No linked implementation task was found in the pull request body." + upsert_sync_comment(client, repo, ctx.number, render_failure_comment(message), dry_run=dry_run) + return 1 + + task = client.get_issue(repo, task_number) + if "pull_request" in task: + message = f"Linked item #{task_number} is a pull request, not an implementation issue/task." + upsert_sync_comment(client, repo, ctx.number, render_failure_comment(message), dry_run=dry_run) + return 1 + + pr_issue = client.get_issue(repo, ctx.number) + sync_pr_metadata(client, repo, ctx, task, pr_issue, config, dry_run=dry_run) + parent_note = sync_parent_relationship(client, repo, task, config, dry_run=dry_run) + + desired_status = project_status_for_context(ctx, config) + if not config.get("syncProject", True): + project_note = "disabled by configuration." + elif project_number is None: + project_note = "skipped because `PROJECT_SETUP_PROJECT_NUMBER` is not configured." + elif project_client is None: + project_note = "skipped because `PROJECT_SETUP_PAT` is not configured." + else: + try: + project_note = sync_project_status( + project_client, + repo, + task, + project_number, + desired_status, + config, + owner=owner, + dry_run=dry_run, + ) + except Exception as exc: + if not is_permission_error(exc): + raise + project_note = f"not synchronized: Project token lacks permission ({exc})." + + upsert_sync_comment( + client, + repo, + ctx.number, + render_success_comment(ctx, task_number, desired_status, project_note, parent_note), + dry_run=dry_run, + ) + return 0 + + +def apply_pr_sync_from_path( + client: GitHubClient, + repo: str, + event_path: str | os.PathLike[str], + config_path: str | os.PathLike[str] = "project_setup.json", + *, + project_client: GitHubClient | None = None, + project_number: int | None = None, + owner: str | None = None, + dry_run: bool = False, +) -> int: + return apply_pr_sync( + client, + repo, + load_event(event_path), + load_sync_config(config_path), + project_client=project_client, + project_number=project_number, + owner=owner, + dry_run=dry_run, + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Synchronize a pull request with its linked implementation task") + parser.add_argument("--repo", default=os.getenv("GITHUB_REPOSITORY")) + parser.add_argument("--event-path", default=os.getenv("GITHUB_EVENT_PATH")) + parser.add_argument("--config", default=os.getenv("PROJECT_SETUP_CONFIG", "project_setup.json")) + parser.add_argument("--project-number", type=int) + parser.add_argument("--owner") + parser.add_argument("--dry-run", action="store_true") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + if not args.repo: + raise SystemExit("Missing --repo or GITHUB_REPOSITORY") + if not args.event_path: + raise SystemExit("Missing --event-path or GITHUB_EVENT_PATH") + + client = require_client() + project_pat = get_project_pat() + project_client = GitHubClient(project_pat) if project_pat else None + return apply_pr_sync_from_path( + client, + args.repo, + args.event_path, + args.config, + project_client=project_client, + project_number=project_number_from_value(args.project_number), + owner=args.owner, + dry_run=args.dry_run, + ) + + +if __name__ == "__main__": + raise SystemExit(main()) From 107f88de24d2e167e3e9459e4fc9a9219225f279 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:16:33 -0300 Subject: [PATCH 05/17] Add trusted PR Sync workflow --- .github/workflows/pr-sync.yml | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/pr-sync.yml diff --git a/.github/workflows/pr-sync.yml b/.github/workflows/pr-sync.yml new file mode 100644 index 0000000..c089f47 --- /dev/null +++ b/.github/workflows/pr-sync.yml @@ -0,0 +1,67 @@ +--- +name: PR Sync + +"on": + pull_request_target: + types: + - converted_to_draft + - closed + workflow_run: + workflows: ["PR metadata validation", "PR guardrails"] + types: [completed] + +permissions: + contents: read + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + sync: + if: >- + ( + github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name == github.repository + ) || ( + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request_target' && + github.event.workflow_run.head_repository.full_name == github.repository && + github.event.workflow_run.pull_requests[0].number != null + ) + runs-on: ubuntu-latest + steps: + - name: Checkout trusted base commit for direct PR lifecycle events + if: github.event_name == 'pull_request_target' + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + + - name: Checkout trusted base branch after PR guardrails + if: github.event_name == 'workflow_run' + uses: actions/checkout@v6 + with: + ref: refs/heads/${{ github.event.workflow_run.pull_requests[0].base.ref }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Synchronize pull request context + env: + GITHUB_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ github.token }} + PROJECT_SETUP_PAT: ${{ secrets.PROJECT_SETUP_PAT }} + PROJECT_SETUP_PROJECT_NUMBER: ${{ vars.PROJECT_SETUP_PROJECT_NUMBER }} + PROJECT_SETUP_OWNER_TYPE: ${{ vars.PROJECT_SETUP_OWNER_TYPE }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + python -m project_setup.pr_sync \ + --repo "$GITHUB_REPOSITORY" \ + --event-path "$GITHUB_EVENT_PATH" From 4a8f20c837469824417b6181fecfbe14ea0b7cca Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:17:03 -0300 Subject: [PATCH 06/17] Add PR Sync unit and workflow contract tests --- tests/test_pr_sync.py | 299 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 tests/test_pr_sync.py diff --git a/tests/test_pr_sync.py b/tests/test_pr_sync.py new file mode 100644 index 0000000..d48fed3 --- /dev/null +++ b/tests/test_pr_sync.py @@ -0,0 +1,299 @@ +from __future__ import annotations + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import Mock, patch + +from project_setup.github import GitHubClient +from project_setup.pr_sync import ( + DEFAULT_SYNC_CONFIG, + PullRequestContext, + SYNC_MARKER, + apply_pr_sync, + linked_task_number, + load_sync_config, + parent_issue_number, + project_status_for_context, + sync_parent_relationship, + sync_pr_metadata, + upsert_sync_comment, +) + + +ROOT = Path(__file__).resolve().parents[1] + + +class PrSyncTests(unittest.TestCase): + def context(self, **overrides) -> PullRequestContext: + values = { + "number": 33, + "action": "opened", + "body": "Closes #12", + "base_ref": "develop", + "head_ref": "feat/example", + "head_repo": "owner/repo", + "author": "alice", + "draft": False, + "merged": False, + } + values.update(overrides) + return PullRequestContext(**values) + + def event(self, **overrides) -> dict: + pull_request = { + "number": 33, + "body": "Closes #12", + "base": {"ref": "develop"}, + "head": {"ref": "feat/example", "repo": {"full_name": "owner/repo"}}, + "user": {"login": "alice"}, + "draft": False, + "merged": False, + } + pull_request.update(overrides.pop("pull_request", {})) + event = {"action": "opened", "pull_request": pull_request} + event.update(overrides) + return event + + def test_linked_task_parser_accepts_closing_keywords(self): + for body in ("Closes #12", "fixes: #12", "Resolves #12"): + with self.subTest(body=body): + self.assertEqual(linked_task_number(body), 12) + + def test_parent_parser_accepts_generated_task_body(self): + self.assertEqual(parent_issue_number("Parent story: US-12 (#8)"), 8) + self.assertEqual(parent_issue_number("Parent issue: #9"), 9) + + def test_default_lifecycle_mapping(self): + self.assertEqual( + project_status_for_context(self.context(draft=True), DEFAULT_SYNC_CONFIG), + "In progress", + ) + self.assertEqual( + project_status_for_context(self.context(action="ready_for_review"), DEFAULT_SYNC_CONFIG), + "In review", + ) + self.assertEqual( + project_status_for_context(self.context(action="closed", merged=False), DEFAULT_SYNC_CONFIG), + "In progress", + ) + self.assertEqual( + project_status_for_context(self.context(action="closed", merged=True), DEFAULT_SYNC_CONFIG), + "Done", + ) + + def test_config_merges_custom_status_with_defaults(self): + with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as handle: + json.dump( + { + "prAutomation": { + "sync": { + "syncAssignees": False, + "projectStatus": {"review": "Review"}, + } + } + }, + handle, + ) + path = handle.name + + config = load_sync_config(path) + + self.assertFalse(config["syncAssignees"]) + self.assertEqual(config["projectStatus"]["review"], "Review") + self.assertEqual(config["projectStatus"]["merged"], "Done") + + def test_metadata_sync_adds_only_configured_labels_and_missing_assignees(self): + client = Mock(spec=GitHubClient) + task = { + "number": 12, + "labels": [ + {"name": "type:task"}, + {"name": "priority:high"}, + {"name": "status:backlog"}, + ], + "milestone": {"number": 4}, + "assignees": [{"login": "alice"}], + } + pr_issue = { + "labels": [{"name": "type:task"}], + "milestone": None, + "assignees": [], + } + + sync_pr_metadata( + client, + "owner/repo", + self.context(), + task, + pr_issue, + DEFAULT_SYNC_CONFIG, + ) + + client.request_json.assert_any_call( + "POST", + "https://api.github.com/repos/owner/repo/issues/33/labels", + {"labels": ["priority:high"]}, + ) + client.request_json.assert_any_call( + "POST", + "https://api.github.com/repos/owner/repo/issues/33/assignees", + {"assignees": ["alice"]}, + ) + client.update_issue.assert_called_once_with("owner/repo", 33, {"milestone": 4}) + + def test_metadata_sync_can_assign_author_when_task_is_unassigned(self): + client = Mock(spec=GitHubClient) + task = {"number": 12, "labels": [], "milestone": None, "assignees": []} + pr_issue = {"labels": [], "milestone": None, "assignees": []} + + sync_pr_metadata( + client, + "owner/repo", + self.context(author="alice"), + task, + pr_issue, + DEFAULT_SYNC_CONFIG, + ) + + client.request_json.assert_any_call( + "POST", + "https://api.github.com/repos/owner/repo/issues/12/assignees", + {"assignees": ["alice"]}, + ) + client.request_json.assert_any_call( + "POST", + "https://api.github.com/repos/owner/repo/issues/33/assignees", + {"assignees": ["alice"]}, + ) + + def test_parent_sync_treats_existing_relationship_as_idempotent(self): + client = Mock(spec=GitHubClient) + task = {"number": 12, "body": "Parent story: US-1 (#8)"} + + with patch( + "project_setup.pr_sync.add_sub_issue", + side_effect=RuntimeError("duplicate sub-issues"), + ): + note = sync_parent_relationship( + client, + "owner/repo", + task, + DEFAULT_SYNC_CONFIG, + ) + + self.assertIn("already", note) + + def test_fork_pull_request_is_skipped_before_reads(self): + client = Mock(spec=GitHubClient) + event = self.event( + pull_request={ + "head": { + "ref": "feat/example", + "repo": {"full_name": "fork-owner/repo"}, + } + } + ) + + result = apply_pr_sync(client, "owner/repo", event, DEFAULT_SYNC_CONFIG) + + self.assertEqual(result, 0) + client.get_issue.assert_not_called() + + def test_promotion_pull_request_is_skipped(self): + client = Mock(spec=GitHubClient) + event = self.event( + pull_request={ + "base": {"ref": "Q.A"}, + "head": {"ref": "develop", "repo": {"full_name": "owner/repo"}}, + } + ) + + result = apply_pr_sync(client, "owner/repo", event, DEFAULT_SYNC_CONFIG) + + self.assertEqual(result, 0) + client.get_issue.assert_not_called() + + def test_missing_linked_task_sets_sticky_failure_comment(self): + client = Mock(spec=GitHubClient) + client.list_issue_comments.return_value = [] + event = self.event(pull_request={"body": ""}) + + result = apply_pr_sync(client, "owner/repo", event, DEFAULT_SYNC_CONFIG) + + self.assertEqual(result, 1) + body = client.create_issue_comment.call_args.args[2] + self.assertIn(SYNC_MARKER, body) + self.assertIn("No linked implementation task", body) + + def test_linked_pull_request_is_rejected_as_task(self): + client = Mock(spec=GitHubClient) + client.get_issue.return_value = { + "number": 12, + "pull_request": {"url": "https://api.github.com/repos/owner/repo/pulls/12"}, + } + client.list_issue_comments.return_value = [] + + result = apply_pr_sync(client, "owner/repo", self.event(), DEFAULT_SYNC_CONFIG) + + self.assertEqual(result, 1) + self.assertIn( + "not an implementation issue/task", + client.create_issue_comment.call_args.args[2], + ) + + def test_success_comment_is_updated_instead_of_duplicated(self): + client = Mock(spec=GitHubClient) + client.list_issue_comments.return_value = [{"id": 99, "body": f"{SYNC_MARKER}\nold"}] + + upsert_sync_comment( + client, + "owner/repo", + 33, + f"{SYNC_MARKER}\nnew", + ) + + client.update_issue_comment.assert_called_once_with( + "owner/repo", + 99, + f"{SYNC_MARKER}\nnew", + ) + client.create_issue_comment.assert_not_called() + + +class PrSyncWorkflowContractTests(unittest.TestCase): + def test_workflow_uses_trusted_base_and_guardrail_completion(self): + text = (ROOT / ".github/workflows/pr-sync.yml").read_text(encoding="utf-8") + + for expected in ( + "name: PR Sync", + "pull_request_target:", + "- converted_to_draft", + "- closed", + "workflow_run:", + '"PR metadata validation"', + '"PR guardrails"', + "github.event.workflow_run.conclusion == 'success'", + "github.event.pull_request.head.repo.full_name == github.repository", + "github.event.workflow_run.head_repository.full_name == github.repository", + "ref: ${{ github.event.pull_request.base.sha }}", + "ref: refs/heads/${{ github.event.workflow_run.pull_requests[0].base.ref }}", + "persist-credentials: false", + "PROJECT_SETUP_PAT: ${{ secrets.PROJECT_SETUP_PAT }}", + "PROJECT_SETUP_PROJECT_NUMBER: ${{ vars.PROJECT_SETUP_PROJECT_NUMBER }}", + "python -m project_setup.pr_sync", + ): + with self.subTest(expected=expected): + self.assertIn(expected, text) + + self.assertNotIn("github.event.pull_request.head.sha", text) + self.assertNotIn("refs/heads/${{ github.event.pull_request.head.ref }}", text) + + def test_installer_distributes_pr_sync_workflow(self): + text = (ROOT / "project_setup/installer.py").read_text(encoding="utf-8") + self.assertIn('".github/workflows/pr-sync.yml"', text) + + +if __name__ == "__main__": + unittest.main() From cca0d6c279b127f5650cf1fbab9da78507e5e180 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:17:21 -0300 Subject: [PATCH 07/17] Distribute PR Sync with core installer --- project_setup/installer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project_setup/installer.py b/project_setup/installer.py index d323f8e..203ad05 100644 --- a/project_setup/installer.py +++ b/project_setup/installer.py @@ -18,6 +18,7 @@ ".github/workflows/qa-source-branch.yml", ".github/workflows/main-source-branch.yml", ".github/workflows/pr-metadata.yml", + ".github/workflows/pr-sync.yml", ".github/workflows/project-setup.yml", "config/project/labels.json", "config/project/milestones.json", From fbd2b2fde3f26aa7435b53a0354ccff4b80d6c87 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:17:32 -0300 Subject: [PATCH 08/17] Configure PR Sync defaults --- project_setup.json | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/project_setup.json b/project_setup.json index 6e42fe1..fd1f84a 100644 --- a/project_setup.json +++ b/project_setup.json @@ -5,6 +5,40 @@ "projectDefinitionFile": "config/project/project-definition.json", "backlogManifestFile": "config/stories/backlog-manifest.json", "secretName": "PROJECT_SETUP_PAT", + "prAutomation": { + "sync": { + "enabled": true, + "syncLabels": true, + "labelPrefixes": [ + "type:", + "priority:", + "test:" + ], + "syncMilestone": true, + "syncAssignees": true, + "assignAuthorWhenTaskUnassigned": true, + "linkSubissues": true, + "syncProject": true, + "skipPromotionPullRequests": true, + "promotionPaths": [ + { + "head": "develop", + "base": "Q.A" + }, + { + "head": "Q.A", + "base": "main" + } + ], + "projectStatusField": "Status", + "projectStatus": { + "draft": "In progress", + "review": "In review", + "closed": "In progress", + "merged": "Done" + } + } + }, "defaults": { "dryRun": true, "runLabels": true, From 5abd420fdf720416c05f8b130ed72240d67236a5 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:17:58 -0300 Subject: [PATCH 09/17] Document implemented PR Sync contract --- docs/repo/pr-sync.md | 268 +++++++++++++++++++++++++++++-------------- 1 file changed, 181 insertions(+), 87 deletions(-) diff --git a/docs/repo/pr-sync.md b/docs/repo/pr-sync.md index 3b98f20..e6c77ef 100644 --- a/docs/repo/pr-sync.md +++ b/docs/repo/pr-sync.md @@ -2,77 +2,158 @@ ## Status -This document defines the design contract for the planned **PR Sync** automation. +**Implemented.** -The `feat/pr-sync` branch was fast-forwarded to the current `main` baseline before this document was added. At this stage, the branch documents intended behavior only; PR Sync is not yet implemented. +PR Sync is the generic GitHub Project Setup automation that keeps an implementation pull request aligned with its linked issue/task and, when configured, GitHub Projects v2. -PR Sync is the generic GitHub Project Setup successor to the repository-specific workflow previously called **PR Hygiene** in the Take Your Pills reference repository. +The implementation is composed of: -## Purpose +- `.github/workflows/pr-sync.yml` — trusted GitHub Actions orchestration; +- `project_setup/pr_sync.py` — synchronization logic; +- `project_setup.json` → `prAutomation.sync` — repository policy/configuration; +- `tests/test_pr_sync.py` — unit and workflow-contract coverage. -PR Sync keeps an implementation pull request and its linked GitHub work items aligned after PR Guardrails has resolved and validated the pull request context. +The feature is the generic successor to the repository-specific workflow previously called **PR Hygiene** in the Take Your Pills reference repository. New GPA code and documentation use **PR Sync**. -The intended execution order is: +## Responsibility boundary + +PR Sync does not replace PR Guardrails. + +The intended pipeline is: ```text Pull request event | v -PR Guardrails - - resolve context - - autofill resolvable metadata +PR Guardrails / current PR metadata validation + - resolve or validate trusted PR context - validate branch/body contract | v PR Sync - - synchronize linked task/issue metadata + - resolve linked implementation task + - synchronize task -> PR metadata - synchronize parent/sub-issue relationship - - synchronize Project v2 membership and status + - synchronize task -> Project v2 + - synchronize PR lifecycle -> Project Status +``` + +The current GPA validation workflow is named `PR metadata validation`. PR Sync listens for successful completion of that workflow and also recognizes the reference-compatible `PR guardrails` workflow name so the orchestration remains valid when Guardrails is promoted to the generic name. + +## Event model + +Normal implementation updates run from `workflow_run` after the trusted PR validation/Guardrails workflow completes successfully. + +Lifecycle events that do not need another validation pass run directly from `pull_request_target`: + +- `converted_to_draft`; +- `closed`. + +Both paths use automation from the trusted base branch. The workflow does not execute code from an untrusted PR head with write permissions. + +Fork pull requests are skipped. + +## Linked implementation task + +PR Sync resolves the implementation issue/task from a closing reference in the PR body: + +```text +Closes #123 +Fixes #123 +Resolves #123 +``` + +If no closing reference exists, PR Sync writes or updates one marked status comment and returns a failure for the synchronization step. + +If the referenced item is itself a pull request, it is rejected as an implementation task. + +The closing reference is also the GitHub Development linkage between the PR and the issue/task; PR Sync builds its synchronization context from that canonical link rather than inventing another parallel association. + +## Metadata synchronization + +By default, the linked task is the source for these PR fields: + +### Labels + +Configured label families are copied to the PR when missing. + +Default prefixes: + +```text +type: +priority: +test: +``` + +PR Sync is additive: it does not remove unrelated or stale labels from the PR. + +### Milestone + +If the task has a milestone and the PR does not match it, the PR milestone is updated to the task milestone. + +### Assignees + +If task assignees exist, missing assignees are added to the PR. + +If the task is unassigned and `assignAuthorWhenTaskUnassigned` is enabled, the PR author is assigned to the task and then synchronized to the PR. + +These behaviors can be disabled independently in `project_setup.json`. + +## Parent Story / sub-issue synchronization + +Generated GPA task bodies already use a parent reference such as: + +```text +Parent story: US-12 (#45) ``` -PR Sync must not replace PR Guardrails. Guardrails establishes trusted context; PR Sync consumes that context and applies synchronization. +When `linkSubissues` is enabled, PR Sync resolves that parent issue number and ensures the implementation task is linked as a GitHub sub-issue. -## Planned responsibilities +Repeated execution is idempotent for an already-existing parent/sub-issue relationship. Permission failures are reported as synchronization diagnostics instead of causing duplicate mutations. -PR Sync is expected to support the following operations when enabled by repository configuration: +## Project v2 synchronization -- resolve the implementation task from the PR closing reference (`Closes #N`, `Fixes #N`, or `Resolves #N`); -- copy configured label families from the linked task to the pull request; -- synchronize the pull request milestone with the linked task or resolved delivery context; -- synchronize assignees when repository policy enables it; -- ensure a task that declares a parent Story is linked as a GitHub sub-issue when possible; -- add the linked task to the configured GitHub Project v2 when it is not already present; -- synchronize the configured Project v2 status from the pull request lifecycle; -- maintain one marked/sticky PR Sync status comment rather than creating duplicate comments. +When all of the following are configured: + +- `syncProject: true`; +- Actions variable `PROJECT_SETUP_PROJECT_NUMBER`; +- Actions secret `PROJECT_SETUP_PAT`; + +PR Sync ensures that the linked task belongs to the configured Project v2 and updates the configured single-select status field. + +The Project owner continues to use the existing GPA owner-resolution contract. `PROJECT_SETUP_OWNER_TYPE` may be provided as an Actions variable when explicit `user` or `organization` selection is required. + +If the Project number or PAT is missing, repository-level PR/task synchronization still runs and the sticky comment explains why Project synchronization was skipped. ## Default lifecycle mapping -The generic default mapping should be configurable, but the initial contract is: +The committed default mapping is: | Pull request state | Project status target | | --- | --- | | Draft / converted to draft | `In progress` | -| Ready for review / open validated PR | `In review` | +| Ready for review / validated open PR | `In review` | | Closed without merge | `In progress` | | Merged | `Done` | -Repositories may use different option names. PR Sync must resolve configured aliases/options rather than hard-code a Take Your Pills-specific Project schema. +The field name and option names are configurable. GPA resolves the configured option by normalized name rather than hard-coding one repository-specific Project schema. ## Promotion pull requests -PR Sync is primarily intended for implementation pull requests. Promotion pull requests are infrastructure transitions: +PR Sync is intended for implementation pull requests, not branch-promotion infrastructure. + +The default excluded paths are: ```text -develop -> Q.A -> main +develop -> Q.A +Q.A -> main ``` -They must not be treated as implementation tasks by default. Unless a target repository explicitly enables release/promotion synchronization, PR Sync should skip task-level synchronization for these promotion paths. - -This separation prevents a release promotion PR from accidentally inheriting labels, assignees, sub-issue relationships, or Project status intended for a single implementation task. +Promotion PRs are skipped before task lookup or metadata mutation. Repositories with a different promotion model can replace `promotionPaths` or disable `skipPromotionPullRequests`. -## Configuration direction +## Committed configuration schema -The exact configuration schema will be finalized during implementation. The intended shape is equivalent to: +`project_setup.json` contains the active PR Sync configuration: ```json { @@ -80,11 +161,18 @@ The exact configuration schema will be finalized during implementation. The inte "sync": { "enabled": true, "syncLabels": true, + "labelPrefixes": ["type:", "priority:", "test:"], "syncMilestone": true, "syncAssignees": true, + "assignAuthorWhenTaskUnassigned": true, "linkSubissues": true, "syncProject": true, "skipPromotionPullRequests": true, + "promotionPaths": [ + {"head": "develop", "base": "Q.A"}, + {"head": "Q.A", "base": "main"} + ], + "projectStatusField": "Status", "projectStatus": { "draft": "In progress", "review": "In review", @@ -96,85 +184,91 @@ The exact configuration schema will be finalized during implementation. The inte } ``` -This is a design example, not yet a committed configuration schema. +This is now an implementation contract, not a design placeholder. ## Authentication and permissions -Repository-scoped PR/issue synchronization should prefer the GitHub Actions token with the minimum required permissions. Project v2 operations may require `PROJECT_SETUP_PAT` depending on the repository owner, Project location, and token capabilities. +Repository-scoped synchronization uses `${{ github.token }}` with the workflow's minimum repository permissions. -The workflow must never expose PAT values in logs or comments. +Project v2 synchronization uses the existing explicit GPA boundary: -A failure to synchronize an optional surface such as Project v2 should produce a clear diagnostic and must not silently corrupt or overwrite unrelated PR metadata. +```text +PROJECT_SETUP_PAT +``` + +The PAT is never written to logs or comments. + +The workflow requests only: + +```yaml +permissions: + contents: read + issues: write +``` + +PR labels, assignees, milestone changes and PR comments are performed through GitHub's issue endpoints because pull requests are issue-backed resources. ## Security model -The planned workflow should use trusted automation definitions (`pull_request_target` or a trusted follow-up workflow) only when it does not execute untrusted pull-request code. +PR Sync follows the repository hardening introduced in the promotion-gate work: -PR Sync must: +- privileged PR automation is based on `pull_request_target` / trusted `workflow_run`; +- checkout points at the trusted base commit or trusted base branch; +- `persist-credentials` is disabled; +- fork PRs are rejected by both workflow conditions and implementation checks; +- no untrusted head code is executed with write permissions; +- GitHub mutation calls are not automatically replayed after transport failures; +- optional Project permission failures are surfaced clearly. -- check out trusted base-branch automation when code checkout is required; -- avoid executing files from an untrusted PR head under elevated write permissions; -- use minimum GitHub token permissions; -- be idempotent for labels, milestone, assignees, Project membership, status, and sticky comments; -- handle `403`/permission failures as explicit synchronization diagnostics rather than masking the original validation result with an unrelated traceback. +The same source/embedded-repository distinction documented in `project-setup-shared-tool.md` continues to apply when the workflow is distributed to target repositories. -## Relationship with existing GPA capabilities +## Idempotency -GitHub Project Setup already contains reusable lower-level operations that PR Sync can compose, including: +Repeated PR Sync execution is designed not to duplicate: -- issue and PR label handling; -- milestone APIs; -- sub-issue linking support; -- Project v2 owner resolution; -- Project item creation and field synchronization; -- PR validation and marked validation comments. +- labels already present on the PR; +- assignees already present on the PR; +- Project membership for an existing task; +- parent/sub-issue relationships already established; +- marked PR Sync comments. -The PR Sync feature should reuse these primitives instead of duplicating GitHub API implementations. +Milestone and Project status updates converge toward the configured task/lifecycle state. -## Relationship with PR Guardrails +## Installation -PR Guardrails is responsible for determining what can be inferred safely before validation. PR Sync should run only after that stage has successfully established the PR context. +`.github/workflows/pr-sync.yml` is part of the core installer manifest. Because package files under `project_setup/*.py` are distributed automatically, `project_setup/pr_sync.py` is installed with the workflow. -A typical implementation PR should therefore become: +Existing target files remain protected by the installer's normal preserve-by-default behavior. -```text -feat/US-12-example - | - v -PR Guardrails - Story / task / milestone resolution - body autofill - validation - | - v -PR Sync - task -> PR labels/milestone/assignee - Story <-> task relationship - task -> Project v2 - PR lifecycle -> Project Status -``` +## Validation + +`tests/test_pr_sync.py` covers the reusable contract, including: -## Validation requirements before promotion +- closing-reference parsing; +- generated parent-reference parsing; +- lifecycle status mapping; +- configuration overrides; +- label/milestone/assignee synchronization; +- author fallback for unassigned tasks; +- duplicate parent/sub-issue idempotency; +- fork safety; +- promotion-PR skip behavior; +- missing/invalid linked task behavior; +- sticky comment idempotency; +- trusted workflow checkout and dependency contract; +- installer distribution of the workflow. -Implementation of PR Sync must include automated coverage for at least: +Live Project v2 behavior remains a Q.A-sandbox concern. It must not use the source repository as the destructive integration-test target. -- missing linked task; -- linked item is a pull request instead of an issue/task; -- existing versus missing labels; -- milestone synchronization; -- assignee synchronization and disabled-assignee mode; -- parent/sub-issue already linked versus missing; -- Project item already present versus missing; -- Project status transitions for draft, review, closed, and merged states; -- missing Project PAT or insufficient Project permission; -- promotion PR skip behavior; -- repeated execution without duplicate side effects; -- same-repository and fork safety boundaries. +## Known limits -Live Project v2 behavior should use the dedicated Q.A sandbox rather than the source repository. +- PR Sync adds configured task labels but does not remove stale PR labels. +- It synchronizes assignees from task to PR; reviewer-request automation remains a Guardrails/review-policy responsibility rather than PR Sync. +- Project v2 synchronization is optional when its PAT/Project number are not configured. +- Promotion PR task synchronization is disabled by default. ## Naming The generic feature and workflow name is **PR Sync**. -Do not use `PR Hygiene` in new GPA code, workflows, CLI commands, configuration, or documentation except when referring historically to the Take Your Pills implementation from which behavior was evaluated. +Do not introduce `PR Hygiene` in new GPA code, workflows, CLI/configuration names, or documentation except when referring historically to the Take Your Pills reference implementation. From 51d88f07d40dd431576416e3a238924ebf66457b Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:18:25 -0300 Subject: [PATCH 10/17] Document implemented PR Sync contract in Portuguese --- docs/repo/pr-sync.pt-BR.md | 272 +++++++++++++++++++++++++------------ 1 file changed, 182 insertions(+), 90 deletions(-) diff --git a/docs/repo/pr-sync.pt-BR.md b/docs/repo/pr-sync.pt-BR.md index 1c24cf6..813e946 100644 --- a/docs/repo/pr-sync.pt-BR.md +++ b/docs/repo/pr-sync.pt-BR.md @@ -2,77 +2,156 @@ ## Status -Este documento define o contrato de design da futura automação **PR Sync**. +**Implementado.** -A branch `feat/pr-sync` foi atualizada por fast-forward para o baseline atual de `main` antes da inclusão desta documentação. Neste estágio, a branch documenta o comportamento pretendido; a PR Sync ainda não está implementada. +PR Sync é a automação genérica do GitHub Project Setup que mantém um pull request de implementação alinhado com sua issue/task vinculada e, quando configurado, com o GitHub Projects v2. -PR Sync é o sucessor genérico, dentro do GitHub Project Setup, do workflow específico de repositório anteriormente chamado **PR Hygiene** no projeto de referência Take Your Pills. +A implementação é composta por: -## Objetivo +- `.github/workflows/pr-sync.yml` — orquestração confiável no GitHub Actions; +- `project_setup/pr_sync.py` — lógica de sincronização; +- `project_setup.json` → `prAutomation.sync` — política/configuração do repositório; +- `tests/test_pr_sync.py` — testes unitários e de contrato do workflow. -PR Sync mantém um pull request de implementação e seus itens de trabalho vinculados no GitHub sincronizados depois que o PR Guardrails resolve e valida o contexto do PR. +A funcionalidade é o sucessor genérico do workflow específico anteriormente chamado **PR Hygiene** no repositório de referência Take Your Pills. Novos códigos e documentos do GPA usam **PR Sync**. -A ordem pretendida é: +## Limite de responsabilidade + +PR Sync não substitui PR Guardrails. + +O pipeline esperado é: ```text Evento de pull request | v -PR Guardrails - - resolve o contexto - - preenche metadata determinável - - valida branch/body +PR Guardrails / validação atual de metadata do PR + - resolve ou valida o contexto confiável + - valida contrato de branch/body | v PR Sync - - sincroniza task/issue vinculada com o PR + - resolve a task de implementação vinculada + - sincroniza metadata task -> PR - sincroniza relação pai/sub-issue - - sincroniza Project v2 e status + - sincroniza task -> Project v2 + - sincroniza ciclo do PR -> Status do Project ``` -PR Sync não substitui PR Guardrails. Guardrails estabelece o contexto confiável; PR Sync consome esse contexto e aplica a sincronização. +O workflow atual do GPA chama-se `PR metadata validation`. PR Sync escuta sua conclusão com sucesso e também reconhece o nome compatível `PR guardrails`, permitindo a futura promoção do Guardrails ao nome genérico sem quebrar a orquestração. -## Responsabilidades planejadas +## Modelo de eventos -Quando habilitada pela configuração do repositório, PR Sync deverá poder: +Atualizações normais de implementação rodam via `workflow_run` depois que a validação confiável/Guardrails termina com sucesso. -- resolver a task de implementação através de `Closes #N`, `Fixes #N` ou `Resolves #N`; -- copiar famílias configuradas de labels da task para o PR; -- sincronizar o milestone do PR com a task ou contexto de entrega resolvido; -- sincronizar assignees quando permitido pela política do repositório; -- garantir que uma task com Story pai seja vinculada como sub-issue quando possível; -- adicionar a task ao GitHub Project v2 configurado quando ainda não estiver presente; -- sincronizar o Status do Project v2 conforme o ciclo de vida do PR; -- manter um único comentário marcado/sticky de PR Sync, sem duplicações. +Eventos de ciclo de vida que não precisam de outra validação rodam diretamente por `pull_request_target`: -## Mapeamento padrão do ciclo de vida +- `converted_to_draft`; +- `closed`. + +Os dois caminhos usam automação da base confiável. O workflow não executa código do head não confiável com permissões de escrita. + +PRs vindos de forks são ignorados. + +## Task de implementação vinculada + +PR Sync resolve a issue/task por uma referência de fechamento no body do PR: + +```text +Closes #123 +Fixes #123 +Resolves #123 +``` + +Se não houver referência, PR Sync cria ou atualiza um único comentário marcado de status e retorna falha para a etapa de sincronização. + +Se o item referenciado for outro pull request, ele é rejeitado como task de implementação. + +A própria referência de fechamento estabelece o vínculo Development do GitHub entre PR e issue/task. PR Sync usa esse vínculo canônico como contexto, em vez de criar uma associação paralela. + +## Sincronização de metadata + +Por padrão, a task vinculada é a fonte para estes campos do PR. -O mapeamento deve ser configurável. O contrato inicial é: +### Labels + +Famílias configuradas de labels são copiadas quando estiverem ausentes. + +Prefixos padrão: + +```text +type: +priority: +test: +``` + +A operação é aditiva: PR Sync não remove labels não relacionados ou antigos do PR. + +### Milestone + +Se a task possuir milestone diferente do PR, o milestone do PR é atualizado para o da task. + +### Assignees + +Assignees existentes na task são adicionados ao PR quando estiverem ausentes. + +Se a task estiver sem assignee e `assignAuthorWhenTaskUnassigned` estiver habilitado, o autor do PR é atribuído à task e então sincronizado com o PR. + +Cada comportamento pode ser desabilitado na configuração. + +## Story pai / sub-issue + +Tasks geradas pelo GPA já utilizam referência de pai, por exemplo: + +```text +Parent story: US-12 (#45) +``` + +Com `linkSubissues` habilitado, PR Sync resolve o número da Story/issue pai e garante que a task seja vinculada como sub-issue. + +Execuções repetidas tratam relações já existentes como idempotentes. Falhas de permissão são apresentadas como diagnóstico, sem tentar duplicar a mutação. + +## Sincronização com Project v2 + +Quando estão configurados: + +- `syncProject: true`; +- variável de Actions `PROJECT_SETUP_PROJECT_NUMBER`; +- secret de Actions `PROJECT_SETUP_PAT`; + +PR Sync garante que a task vinculada pertença ao Project v2 configurado e atualiza o campo single-select de status. + +A resolução de owner reutiliza o contrato já existente do GPA. `PROJECT_SETUP_OWNER_TYPE` pode ser fornecido como variável de Actions quando for necessário fixar `user` ou `organization`. + +Sem Project number ou PAT, a sincronização de PR/task dentro do repositório continua funcionando e o comentário sticky informa por que o Project foi ignorado. + +## Mapeamento padrão do ciclo de vida | Estado do PR | Status alvo no Project | | --- | --- | | Draft / convertido para draft | `In progress` | -| Ready for review / PR aberto e validado | `In review` | +| Ready for review / PR aberto validado | `In review` | | Fechado sem merge | `In progress` | | Merged | `Done` | -Repositórios podem utilizar nomes de opções diferentes. PR Sync deve resolver aliases/opções configuradas em vez de depender de um schema específico do Take Your Pills. +O nome do campo e das opções são configuráveis. O GPA resolve a opção pelo nome normalizado, sem depender de um schema específico do Take Your Pills. -## Pull requests de promoção +## PRs de promoção -PR Sync é voltado principalmente a pull requests de implementação. PRs de promoção são transições de infraestrutura: +PR Sync é voltado a PRs de implementação, não à infraestrutura de promoção de branches. + +Os caminhos ignorados por padrão são: ```text -develop -> Q.A -> main +develop -> Q.A +Q.A -> main ``` -Por padrão eles não devem ser tratados como tasks de implementação. A menos que o repositório habilite explicitamente sincronização de release/promoção, PR Sync deve ignorar sincronizações de task nesses caminhos. - -Isso impede que um PR de promoção herde labels, assignees, relações de sub-issue ou status de Project pertencentes a uma única task de implementação. +Eles são ignorados antes da resolução de task ou qualquer mutação. Repositórios com outro modelo podem substituir `promotionPaths` ou desabilitar `skipPromotionPullRequests`. -## Direção de configuração +## Schema de configuração consolidado -O schema definitivo será fechado durante a implementação. O formato pretendido é equivalente a: +`project_setup.json` agora contém o contrato ativo: ```json { @@ -80,11 +159,18 @@ O schema definitivo será fechado durante a implementação. O formato pretendid "sync": { "enabled": true, "syncLabels": true, + "labelPrefixes": ["type:", "priority:", "test:"], "syncMilestone": true, "syncAssignees": true, + "assignAuthorWhenTaskUnassigned": true, "linkSubissues": true, "syncProject": true, "skipPromotionPullRequests": true, + "promotionPaths": [ + {"head": "develop", "base": "Q.A"}, + {"head": "Q.A", "base": "main"} + ], + "projectStatusField": "Status", "projectStatus": { "draft": "In progress", "review": "In review", @@ -96,85 +182,91 @@ O schema definitivo será fechado durante a implementação. O formato pretendid } ``` -Este trecho é um exemplo de design e ainda não representa um schema de configuração consolidado. +Esse bloco deixa de ser exemplo de design e passa a ser contrato de implementação. ## Autenticação e permissões -Sincronizações de PR/issue restritas ao repositório devem preferir o GitHub Actions token com as permissões mínimas necessárias. Operações em Project v2 podem exigir `PROJECT_SETUP_PAT`, dependendo do owner, localização do Project e capacidades do token. +Sincronizações restritas ao repositório usam `${{ github.token }}` com permissões mínimas. -Valores de PAT nunca devem aparecer em logs ou comentários. +Projects v2 preserva a fronteira explícita já usada pelo GPA: -Falhas em superfícies opcionais, como Project v2, devem produzir diagnóstico claro e não podem sobrescrever ou corromper metadata não relacionada do PR. +```text +PROJECT_SETUP_PAT +``` + +O PAT nunca deve aparecer em logs ou comentários. + +O workflow solicita somente: + +```yaml +permissions: + contents: read + issues: write +``` + +Labels, assignees, milestone e comentários de PR são tratados pelos endpoints de issues, pois PRs são recursos baseados em issues no GitHub. ## Modelo de segurança -O workflow planejado deve usar automação confiável (`pull_request_target` ou workflow subsequente confiável) apenas quando não executar código não confiável do head do PR. +PR Sync segue o hardening recente do repositório: -PR Sync deve: +- automação privilegiada usa `pull_request_target` / `workflow_run` confiável; +- checkout aponta para commit/branch da base confiável; +- `persist-credentials` fica desabilitado; +- forks são bloqueados no workflow e novamente na implementação; +- nenhum código do head não confiável roda com permissões de escrita; +- mutações GitHub não são repetidas automaticamente após falhas de transporte; +- falhas opcionais de permissão no Project são expostas claramente. -- usar automação da base confiável quando checkout for necessário; -- não executar arquivos do head não confiável com permissões elevadas; -- usar o mínimo de permissões possível; -- ser idempotente para labels, milestone, assignees, Project, status e comentários sticky; -- tratar `403` e falhas de permissão como diagnóstico de sincronização, sem mascarar a causa original com traceback irrelevante. +A distinção entre repositório-fonte e instalação embarcada descrita em `project-setup-shared-tool.md` continua válida ao distribuir o workflow. -## Relação com capacidades já existentes no GPA +## Idempotência -O GPA já possui primitivas reutilizáveis que a PR Sync poderá compor, incluindo: +Execuções repetidas não devem duplicar: -- manipulação de labels em issues/PRs; -- APIs de milestone; -- suporte a sub-issues; -- resolução do owner do Project v2; -- criação de item e sincronização de campos no Project v2; -- validação de PR e comentários marcados de validação. +- labels já presentes; +- assignees já presentes; +- membership do Project para task já adicionada; +- relação pai/sub-issue existente; +- comentário marcado da PR Sync. -A implementação deve reutilizar essas primitivas em vez de duplicar integrações com a API do GitHub. +Milestone e Status do Project convergem para o estado configurado. -## Relação com PR Guardrails +## Instalação -PR Guardrails é responsável por determinar o que pode ser inferido com segurança antes da validação. PR Sync deve rodar somente depois que esse estágio estabelecer com sucesso o contexto do PR. +`.github/workflows/pr-sync.yml` faz parte do manifest core do instalador. Como módulos `project_setup/*.py` são distribuídos automaticamente, `project_setup/pr_sync.py` acompanha o workflow. -Um PR típico deve seguir: +Arquivos já existentes no target continuam protegidos pelo comportamento preserve-by-default do instalador. -```text -feat/US-12-exemplo - | - v -PR Guardrails - resolução de Story / task / milestone - autofill do body - validação - | - v -PR Sync - task -> labels/milestone/assignee do PR - Story <-> task - task -> Project v2 - ciclo do PR -> Project Status -``` +## Validação + +`tests/test_pr_sync.py` cobre: -## Requisitos de validação antes da promoção +- parsing de closing reference; +- parsing da referência de pai gerada; +- mapeamento do ciclo de vida; +- overrides de configuração; +- sincronização de labels/milestone/assignees; +- fallback de autor para task sem assignee; +- idempotência de pai/sub-issue; +- segurança para forks; +- skip de PR de promoção; +- task ausente/inválida; +- idempotência do comentário sticky; +- checkout confiável e dependência do workflow; +- distribuição do workflow pelo instalador. -A implementação da PR Sync deve conter cobertura automatizada para pelo menos: +Validação live de Project v2 continua pertencendo ao sandbox de Q.A, nunca ao repositório-fonte. -- task vinculada ausente; -- item vinculado é um PR em vez de issue/task; -- labels já existentes e ausentes; -- sincronização de milestone; -- sincronização de assignee e modo desabilitado; -- relação pai/sub-issue já existente e ausente; -- item de Project já existente e ausente; -- transições de status draft, review, closed e merged; -- PAT de Project ausente ou permissão insuficiente; -- comportamento de skip para PRs de promoção; -- execução repetida sem efeitos duplicados; -- limites de segurança para mesmo repositório e forks. +## Limites conhecidos -Comportamentos live envolvendo Project v2 devem usar o sandbox dedicado de Q.A, nunca o repositório-fonte. +- PR Sync adiciona labels configuradas da task, mas não remove labels antigas do PR. +- Sincronização de assignee pertence ao PR Sync; solicitação de reviewers permanece responsabilidade de Guardrails/review-policy. +- Project v2 é opcional quando PAT/Project number não estiverem configurados. +- Sincronização de task em PRs de promoção fica desabilitada por padrão. ## Nomenclatura O nome genérico da funcionalidade e do workflow é **PR Sync**. -Não utilizar `PR Hygiene` em novos códigos, workflows, comandos CLI, configurações ou documentação do GPA, exceto em referências históricas ao comportamento avaliado no Take Your Pills. +Não introduzir `PR Hygiene` em novos códigos, workflows, nomes de configuração/CLI ou documentação do GPA, exceto em referências históricas à implementação avaliada no Take Your Pills. From 1ff083a7b0fb5a89dcc29bfb79a47e271cd0dbc3 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:18:45 -0300 Subject: [PATCH 11/17] Align documentation index with PR Sync and repository hardening --- docs/DOCUMENTATION-GUIDE.md | 67 +++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/docs/DOCUMENTATION-GUIDE.md b/docs/DOCUMENTATION-GUIDE.md index 966a20b..e0f8d02 100644 --- a/docs/DOCUMENTATION-GUIDE.md +++ b/docs/DOCUMENTATION-GUIDE.md @@ -6,11 +6,12 @@ This guide maps the reusable configuration, workflows and operational documentat | Configuration | Purpose | Documentation | | --- | --- | --- | -| `project_setup.json` | File paths and safe execution defaults | `docs/repo/project-setup-shared-tool.md`, `docs/repo/project-setup-runbook.pt-BR.md` | +| `project_setup.json` | File paths, safe execution defaults, and `prAutomation.sync` policy | `docs/repo/project-setup-shared-tool.md`, `docs/repo/pr-sync.md`, `docs/repo/project-setup-runbook.pt-BR.md` | | `config/project/labels.json` | Label names, colors and descriptions | `docs/repo/project-board-policy.md` | | `config/project/milestones.json` | Milestone definitions | `docs/milestones/MILESTONE-TEMPLATE.md` | | `config/project/project-definition.json` | Project v2 fields and options | `docs/repo/project-board-policy.md` | | `config/stories/backlog-manifest.json` | Phases, stories and tasks | milestone and story documentation in the target repository | +| `.project-setup-source` | Explicit identity marker for the GPA source repository; never installed into targets | `docs/repo/project-setup-shared-tool.md`, `docs/repo/branching-policy.md` | ## Operational guidance hierarchy @@ -19,7 +20,9 @@ This guide maps the reusable configuration, workflows and operational documentat | Target repository instructions and recorded conventions (`AGENTS.md`, `CONTRIBUTING*`, README/docs, existing workflows/configuration) | Authoritative for repository-specific decisions and established project standards. | | `AI_SETUP_GUIDE.md` | Authoritative for the AI interaction sequence: inspect before asking, pause at checkpoints, re-verify user changes, protect credentials, and verify applied results. | | `docs/repo/qa-policy.md` / `docs/repo/qa-policy.pt-BR.md` | Authoritative for the `develop -> Q.A -> main` promotion gates, compatibility matrix, sandbox requirements, and live/manual Q.A behavior. | -| `docs/repo/pr-sync.md` / `docs/repo/pr-sync.pt-BR.md` | Design contract for the planned PR Sync feature; not an implementation-status claim until the feature lands. | +| `docs/repo/pr-sync.md` / `docs/repo/pr-sync.pt-BR.md` | Authoritative implementation contract for PR Sync: inputs, synchronization behavior, security, Project mapping, configuration, and limits. | +| `docs/repo/branching-policy.md` / `docs/repo/branching-policy.pt-BR.md` | Branch topology plus trusted metadata-only promotion-gate security contract. | +| `docs/repo/project-setup-shared-tool.md` | Distribution model, source/embedded repository boundary, authentication boundary, and reusable automation internals. | | `docs/repo/project-setup-runbook.pt-BR.md` | Detailed human operational procedure and troubleshooting path. | | `README.md` / `README.pt-BR.md` | Concise user-facing overview, setup, authentication, commands, and safety model. | @@ -31,23 +34,52 @@ The AI guide does not override project-specific conventions. Its purpose is to m | --- | --- | --- | | `.github/workflows/project-setup.yml` | Manual dry-run or live setup | `project_setup/cli.py`, `project_setup/runner.py`, `project_setup.json` | | `.github/workflows/auto-label.yml` | Infer labels for issues and PRs | `project_setup/auto_label.py` | -| `.github/workflows/pr-metadata.yml` | Validate branch names and PR metadata | `project_setup/pr_validation.py` | -| `.github/workflows/qa-source-branch.yml` | Allow promotion into `Q.A` only from `develop` | `docs/repo/branching-policy.md`, `project_setup/pr_validation.py` | +| `.github/workflows/pr-metadata.yml` | Validate branch names and PR metadata; current Guardrails-stage workflow | `project_setup/pr_validation.py` | +| `.github/workflows/pr-sync.yml` | After successful Guardrails/metadata validation, synchronize linked task/PR/Project state; handle draft/closed lifecycle events | `project_setup/pr_sync.py`, `project_setup.json`, `tests/test_pr_sync.py`, `docs/repo/pr-sync.md` | +| `.github/workflows/qa-source-branch.yml` | Metadata-only trusted gate: allow promotion into `Q.A` only from `develop` | `docs/repo/branching-policy.md`, `tests/test_qa_workflows.py` | | `.github/workflows/qa-validation.yml` | Run deterministic cross-platform and package Q.A gates | `tests/qa/test_cli_e2e.py`, `tests/test_makefile_env_defaults.py`, `docs/repo/qa-policy.md` | | `.github/workflows/qa-live.yml` | Run self-cleaning live integration tests in the protected `qa` Environment | `tests/qa/live_sandbox.py`, `docs/repo/qa-policy.md` | | `.github/workflows/qa-issue-generation.yml` | Run guarded manual non-idempotent issue-generation validation | `tests/qa/live_issue_generation.py`, `docs/repo/qa-policy.md` | -| `.github/workflows/main-source-branch.yml` | Allow promotion into `main` only from `Q.A` | `docs/repo/branching-policy.md`, `project_setup/pr_validation.py` | -| `.github/workflows/repo-quality.yml` | Validate this tool repository | `Makefile`, `scripts/validation/repo_quality.py`, `tests/` | +| `.github/workflows/main-source-branch.yml` | Metadata-only trusted gate: allow promotion into `main` only from `Q.A` | `docs/repo/branching-policy.md`, `tests/test_qa_workflows.py` | +| `.github/workflows/repo-quality.yml` | Validate this tool repository and distinguish source vs embedded target mode | `Makefile`, `scripts/validation/repo_quality.py`, `.project-setup-source`, `tests/` | -## Feature design contracts +## PR automation pipeline -Feature branches may carry design documentation before implementation begins. These documents must say explicitly when behavior is planned rather than implemented. +The reusable implementation path is: -Current design contracts: +```text +implementation PR + | + v +PR metadata validation / PR Guardrails + | + | successful trusted context + v +PR Sync + | + +--> task labels / milestone / assignees -> PR + +--> parent Story <-> sub-issue + +--> task -> Project v2 + +--> PR lifecycle -> Project Status +``` + +Promotion PRs remain infrastructure transitions and are excluded from task-level PR Sync by default: + +```text +develop -> Q.A -> main +``` + +## Source and embedded repository validation + +Recent repository-mode hardening is part of the documented contract: -- `docs/repo/pr-sync.md` / `docs/repo/pr-sync.pt-BR.md`: planned PR Sync orchestration, lifecycle mapping, security model, relationship to PR Guardrails, promotion-PR exclusions, and required regression/live validation. +- `.project-setup-source` explicitly identifies the GPA source repository; +- promotion source gates use `pull_request_target` as metadata-only workflows and do not check out PR code; +- the source marker is not distributed into target repositories; +- embedded targets may keep their own Makefile/workflow callers without inheriting source-only caller contracts; +- managed target files are still validated for target-appropriate correctness. -When PR Sync is implemented, this guide must be updated in the same change to map the real workflow and source module rather than leaving it as a design-only contract. +This prevents a target project from being misdetected as GPA source merely because it contains similar paths or filenames. ## Adding a milestone template @@ -71,16 +103,17 @@ python -m project_setup apply --repo owner/repository --live ```text AI_SETUP_GUIDE.md Interaction contract for AI-guided configuration -project_setup/ Reusable Python package -project_setup.json File paths and execution defaults +.project-setup-source Source-only GPA repository identity marker +project_setup/ Reusable Python package, including PR Sync +project_setup.json File paths, execution defaults, PR automation policy config/project/ Labels, milestones and Project v2 definition config/stories/ Backlog manifest -.github/workflows/ Generic active workflows and Q.A gates -docs/repo/ Operational policies, runbooks, and feature design contracts +.github/workflows/ Generic active workflows, PR automation, Q.A gates +docs/repo/ Operational policies, runbooks, implementation contracts tests/qa/ Q.A black-box, live sandbox, and guarded manual tests scripts/validation/ Cross-platform validation entrypoints -tests/ Unit and installation tests +tests/ Unit, installation and workflow-contract tests Makefile Human and AI-oriented command interface ``` -When a configuration contract changes, update its loader, tests, README, AI guide when relevant, and the corresponding runbook or Q.A policy in the same pull request. +When a configuration or workflow contract changes, update its loader/implementation, tests, documentation guide, README or AI guide when user-facing behavior changes, and the corresponding policy/runbook in the same pull request. From b9b9082ccfb972925033f6cef82d65ede5f266f0 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:19:02 -0300 Subject: [PATCH 12/17] Document trusted promotion gates and PR Sync branching --- docs/repo/branching-policy.md | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/repo/branching-policy.md b/docs/repo/branching-policy.md index 651c24b..276907e 100644 --- a/docs/repo/branching-policy.md +++ b/docs/repo/branching-policy.md @@ -19,7 +19,49 @@ Direct `develop -> main`, implementation branch -> `Q.A`, and implementation bra Q.A gates and sandbox requirements are documented in [`qa-policy.md`](qa-policy.md). -Repositories that intentionally use a different promotion model should adapt `.github/workflows/qa-source-branch.yml`, `.github/workflows/main-source-branch.yml`, and the PR branch validator together instead of changing only one layer. +Repositories that intentionally use a different promotion model should adapt `.github/workflows/qa-source-branch.yml`, `.github/workflows/main-source-branch.yml`, the PR branch validator, and the PR Sync promotion exclusions together instead of changing only one layer. + +## Trusted promotion gates + +The `Q.A` and `main` source-branch gates are privileged metadata checks. + +They use `pull_request_target` deliberately and **do not check out pull-request code**: + +- `.github/workflows/qa-source-branch.yml` accepts only `develop -> Q.A`; +- `.github/workflows/main-source-branch.yml` accepts only `Q.A -> main`. + +These workflows must remain metadata-only. Adding `actions/checkout`, shell execution from the PR head, or another path that executes untrusted head content under the `pull_request_target` token would violate the security contract. + +The gate logic therefore reads the PR source/base refs from the event payload and exits without executing repository code from the proposed change. + +## Source repository vs embedded target + +The GPA source repository is identified explicitly by `.project-setup-source`. + +The marker contains the source identity contract and is intentionally **not** distributed by the installer. A target repository containing files with names similar to GPA internals must not be misclassified as the source repository. + +Embedded targets may preserve target-owned files such as an existing `Makefile` or workflow callers. Source-only caller/reference checks are skipped there while managed automation still remains subject to target-appropriate validation. + +See [`project-setup-shared-tool.md`](project-setup-shared-tool.md) for the distribution boundary. + +## PR automation and branch promotions + +Implementation PR automation follows the same branch model: + +```text +implementation branch -> develop -> Q.A -> main +``` + +PR Guardrails / PR metadata validation establishes the implementation PR context. PR Sync then synchronizes the linked task and Project metadata. + +PR Sync excludes branch-promotion PRs by default: + +- `develop -> Q.A`; +- `Q.A -> main`. + +This prevents a promotion PR from inheriting task-specific labels, assignees, milestones, sub-issue relationships, or Project status. + +See [`pr-sync.md`](pr-sync.md). ## Naming From f5e4872b87cc7a5198c703fbd2aa59657e3f4613 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:19:19 -0300 Subject: [PATCH 13/17] Document trusted promotion gates and PR Sync branching in Portuguese --- docs/repo/branching-policy.pt-BR.md | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/repo/branching-policy.pt-BR.md b/docs/repo/branching-policy.pt-BR.md index c7c646f..a21ac2a 100644 --- a/docs/repo/branching-policy.pt-BR.md +++ b/docs/repo/branching-policy.pt-BR.md @@ -19,7 +19,49 @@ Promoções diretas `develop -> main`, branch de implementação -> `Q.A` e bran Os gates, testes e requisitos do sandbox estão documentados em [`qa-policy.pt-BR.md`](qa-policy.pt-BR.md). -Repositórios que adotarem outro modelo de promoção devem adaptar juntos `.github/workflows/qa-source-branch.yml`, `.github/workflows/main-source-branch.yml` e o validador de branch de PR, em vez de alterar apenas uma camada. +Repositórios que adotarem outro modelo de promoção devem adaptar juntos `.github/workflows/qa-source-branch.yml`, `.github/workflows/main-source-branch.yml`, o validador de branch e as exclusões de promoção da PR Sync, em vez de alterar apenas uma camada. + +## Gates confiáveis de promoção + +Os gates de origem para `Q.A` e `main` são verificações privilegiadas apenas de metadata. + +Eles utilizam `pull_request_target` deliberadamente e **não fazem checkout do código do PR**: + +- `.github/workflows/qa-source-branch.yml` aceita somente `develop -> Q.A`; +- `.github/workflows/main-source-branch.yml` aceita somente `Q.A -> main`. + +Esses workflows devem permanecer metadata-only. Adicionar `actions/checkout`, executar shell vindo do head do PR ou qualquer caminho que execute conteúdo não confiável sob o token de `pull_request_target` quebra o contrato de segurança. + +A validação lê apenas os refs de origem/destino do payload do evento e termina sem executar código da mudança proposta. + +## Repositório-fonte vs target embarcado + +O repositório-fonte do GPA é identificado explicitamente por `.project-setup-source`. + +Esse marker contém o contrato de identidade do source e **não** é distribuído pelo instalador. Um target que possua arquivos com nomes semelhantes aos internos do GPA não deve ser confundido com o repositório-fonte. + +Targets embarcados podem preservar arquivos próprios, como `Makefile` ou workflows callers existentes. Validações de referência/caller exclusivas do source são ignoradas nesse modo, enquanto a automação gerenciada continua sujeita às validações apropriadas ao target. + +Veja [`project-setup-shared-tool.md`](project-setup-shared-tool.md). + +## Automação de PR e promoções + +A automação de implementação segue o mesmo fluxo: + +```text +branch de implementação -> develop -> Q.A -> main +``` + +PR Guardrails / validação de metadata estabelece o contexto do PR de implementação. PR Sync então sincroniza task e metadata do Project. + +Por padrão, PR Sync exclui PRs de promoção: + +- `develop -> Q.A`; +- `Q.A -> main`. + +Assim um PR de promoção não herda labels, assignees, milestone, sub-issue ou Status de Project pertencentes a uma task de implementação. + +Veja [`pr-sync.pt-BR.md`](pr-sync.pt-BR.md). ## Nomenclatura From 9db9fb04d4960d254c2ea2d8a845fbca900c58f8 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:19:39 -0300 Subject: [PATCH 14/17] Document source marker, embedded mode, and PR Sync internals --- docs/repo/project-setup-shared-tool.md | 69 ++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/repo/project-setup-shared-tool.md b/docs/repo/project-setup-shared-tool.md index 2a27b7c..05f155e 100644 --- a/docs/repo/project-setup-shared-tool.md +++ b/docs/repo/project-setup-shared-tool.md @@ -12,6 +12,12 @@ project_setup --help python -m project_setup --help ``` +Feature-specific trusted workflows may also call a package module directly. PR Sync uses: + +```bash +python -m project_setup.pr_sync --repo owner/repository --event-path "$GITHUB_EVENT_PATH" +``` + ## Distribution model The installer embeds the package and managed automation files directly in the target repository. It also installs `Makefile` and `.env.example` when they do not already exist. @@ -30,6 +36,33 @@ python -m project_setup init --target ../target-repository --live Existing files are preserved unless `--force` is explicitly selected. Existing Makefiles and environment templates should be reviewed and merged manually. Installation dry-runs do not create the target directory. +All `project_setup/*.py` package modules are distributed automatically. Managed core workflows are listed explicitly by the installer; this now includes `.github/workflows/pr-sync.yml`. + +## Source repository identity + +The GPA development repository and an embedded target intentionally have different validation contracts. + +The source repository is identified by the root marker: + +```text +.project-setup-source +``` + +The marker must contain the exact source identity expected by repository-quality validation. It is source-only and is **not** part of the installer manifest. + +This explicit marker replaces heuristic/path-based source detection. A target repository may legitimately contain a `Makefile`, tests, workflow files, or names that resemble GPA internals; those files alone must never switch the target into source-repository mode. + +### Embedded target behavior + +When the marker is absent, repository-quality validation treats the checkout as an embedded target: + +- target-owned `Makefile` and workflow callers may be preserved; +- source-only caller/reference contracts are skipped where they are not applicable; +- managed scripts and installed automation remain validated; +- target files do not acquire source-repository obligations merely because their names resemble GPA files. + +This boundary prevents the installer from making an external project accidentally responsible for the GPA source repository's internal wiring. + ## Local environment The CLI automatically loads `.env` from the current working directory without replacing variables that are already present in the process environment. @@ -43,14 +76,18 @@ make doctor ## Configuration -`project_setup.json` points to four manifests and selects which API modules participate: +`project_setup.json` points to the repository manifests and reusable automation policy. + +Core manifest references include: - labels; - milestones; - Project v2 definition; - backlog stories and tasks. -Every mutating CLI command remains dry-run by default. A live operation requires `--live`, the compatible `--no-dry-run` alias, or `LIVE=1` through Make. +`prAutomation.sync` configures PR Sync, including metadata families, assignee behavior, parent/sub-issue linking, Project status mapping, and promotion-PR exclusions. + +Every normal mutating CLI command remains dry-run by default. The PR Sync module is different because it is an event-driven Actions worker: the workflow invokes it to converge already-validated PR metadata, while local testing can use `--dry-run`. ## Discovery @@ -75,14 +112,36 @@ For the current GraphQL implementation: 1. create a personal access token classic; 2. select `repo` and `project` scopes; 3. save it as `PROJECT_SETUP_PAT` in the local `.env`; -4. save the same credential as the `PROJECT_SETUP_PAT` Actions secret when manual workflows must operate on Projects v2. +4. save the same credential as the `PROJECT_SETUP_PAT` Actions secret when workflows such as PR Sync must operate on Projects v2. + +PR Sync also reads `PROJECT_SETUP_PROJECT_NUMBER` from an Actions variable. `PROJECT_SETUP_OWNER_TYPE` may be supplied as an Actions variable when owner auto-detection should be replaced by explicit `user` or `organization` selection. -The CLI and workflow fail before a live Project v2 operation if the explicit PAT is missing. They do not silently use `github.token` for that operation. A Project sync dry-run without the PAT falls back to an offline preview and clearly states that remote data was not queried. +If Project credentials/configuration are absent, PR Sync still performs repository-scoped task/PR synchronization and reports that Project synchronization was skipped. ## Request safety GitHub API requests use a finite timeout. Automatic retries are limited to idempotent reads; mutation requests are not replayed after transport failures because a lost response could otherwise duplicate an issue or Project. +Privileged workflows that use `pull_request_target` must use trusted metadata and trusted base automation only. The promotion gates remain metadata-only and never check out PR code. PR Sync may check out automation, but only from the trusted base commit/branch with credentials persistence disabled. + +## PR Sync boundary + +PR Sync consumes a validated closing reference (`Closes/Fixes/Resolves #N`) and synchronizes: + +- configured task labels to the PR; +- task milestone to the PR; +- task assignees to the PR; +- optional author assignment when the task is unassigned; +- generated parent Story relationship to GitHub sub-issue; +- linked task membership/status in Project v2; +- one marked status comment. + +It skips forks and `develop -> Q.A` / `Q.A -> main` promotion PRs by default. + +See [`pr-sync.md`](pr-sync.md). + ## Automation boundaries -The tool automates repository files, labels, milestones, issues, sub-issues and Project v2 fields/items. Branch protection, rulesets and Project views remain outside the automated core. +The tool automates repository files, labels, milestones, issues, sub-issues, PR synchronization, and Project v2 fields/items. + +Branch protection, rulesets, Project views, and reviewer-selection policy remain outside PR Sync's automated core. Reviewer resolution belongs to PR Guardrails/review policy. From b76698802d79b6444848a1fe7d8a030a2c9e2bb6 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:21:04 -0300 Subject: [PATCH 15/17] Require PR Sync in repository quality gates --- scripts/validation/repo_quality.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/validation/repo_quality.py b/scripts/validation/repo_quality.py index 0b50e5a..eb57c13 100644 --- a/scripts/validation/repo_quality.py +++ b/scripts/validation/repo_quality.py @@ -26,6 +26,7 @@ ".github/workflows/project-setup.yml", ".github/workflows/auto-label.yml", ".github/workflows/pr-metadata.yml", + ".github/workflows/pr-sync.yml", ".github/workflows/qa-source-branch.yml", ".github/workflows/main-source-branch.yml", "config/project/labels.json", @@ -40,6 +41,7 @@ "project_setup/runner.py", "project_setup/installer.py", "project_setup/github.py", + "project_setup/pr_sync.py", "scripts/validation/repo_quality.py", "scripts/validation/validate_pr_body.py", ) @@ -62,6 +64,7 @@ "tests/test_script_references.py", "tests/test_branch_promotion.py", "tests/test_qa_workflows.py", + "tests/test_pr_sync.py", "tests/qa/test_cli_e2e.py", "tests/qa/live_sandbox.py", "tests/qa/live_issue_generation.py", From 7466c48e017cbbbd8fed0d910b70f78b1f320fc1 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:27:19 -0300 Subject: [PATCH 16/17] Document PR Sync onboarding and Actions configuration --- README.md | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 358c8d5..850d25a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ `project_setup` is a self-contained toolkit for installing and operating reusable GitHub repository automation. It combines a Makefile, a Python CLI, GitHub Actions workflows, manifests, and validation scripts so repositories can be configured manually, through automation, or with AI assistance without hiding what will be changed. -The project focuses on safe setup of labels, milestones, issues, sub-issues, pull-request guardrails, repository discovery, and GitHub Projects v2. Remote mutating commands default to dry-run and require an explicit live mode before writing to GitHub. +The project focuses on safe setup of labels, milestones, issues, sub-issues, pull-request guardrails, **PR Sync**, repository discovery, and GitHub Projects v2. Remote mutating commands default to dry-run and require an explicit live mode before writing to GitHub. If an AI assistant will perform or guide the setup, give it [`AI_SETUP_GUIDE.md`](AI_SETUP_GUIDE.md). That file tells the agent to inspect existing repository conventions before asking questions, pause at manual/credential/live checkpoints, re-verify user changes before continuing, and avoid duplicate resources. @@ -26,6 +26,7 @@ Typical uses include: - optionally link generated tasks as sub-issues; - create and synchronize GitHub Projects v2 owned by a personal account or GitHub Organization; - validate pull-request metadata and repository conventions; +- synchronize implementation PRs with their linked tasks and optional Project v2 lifecycle state through PR Sync; - inspect a repository and recommend a setup flow before applying it; - expose the same operations to humans, scripts, and AI agents through predictable Make/CLI commands. @@ -71,7 +72,7 @@ PROJECT_SETUP_CONFIG=project_setup.json PROJECT_SETUP_PROJECT_NUMBER= ``` -`PROJECT_SETUP_TARGET` is the local filesystem path. `GITHUB_REPOSITORY` is the GitHub `owner/repository` identifier. `PROJECT_SETUP_OWNER_TYPE` selects who owns GitHub Projects v2: use `user` for a personal account or `organization` for a company/team GitHub Organization. It may be left empty for authenticated auto-detection. Once a Project v2 exists, `PROJECT_SETUP_PROJECT_NUMBER` can store its number for `make project-sync`. +`PROJECT_SETUP_TARGET` is the local filesystem path. `GITHUB_REPOSITORY` is the GitHub `owner/repository` identifier. `PROJECT_SETUP_OWNER_TYPE` selects who owns GitHub Projects v2: use `user` for a personal account or `organization` for a company/team GitHub Organization. It may be left empty for authenticated auto-detection. Once a Project v2 exists, `PROJECT_SETUP_PROJECT_NUMBER` can store its number for `make project-sync` and is also the value PR Sync expects as an Actions repository variable when Project synchronization is enabled. If the tool is already embedded in and operated from the target repository itself, use: @@ -125,7 +126,7 @@ make setup TARGET=../other-project REPO=owner/other-project OWNER_TYPE=organizat | Repository operations inside GitHub Actions | `${{ github.token }}` exposed as `GITHUB_TOKEN` | [Automatic — no custom secret](#automatic-repository-token) | | Local labels, milestones, issues, comments, and similar repository operations | valid `gh auth`, `GITHUB_TOKEN`, `GH_TOKEN`, or `PROJECT_SETUP_PAT` | [Manual/configured](#local-authentication) | | Local GitHub Projects v2 | `PROJECT_SETUP_PAT` in `.env` | [Manual/configured PAT](#projects-v2-authentication) | -| GitHub Projects v2 from Actions | repository secret `PROJECT_SETUP_PAT` | [Manual/configured PAT + secret](#projects-v2-authentication) | +| GitHub Projects v2 from Actions / PR Sync | repository secret `PROJECT_SETUP_PAT` plus repository variable `PROJECT_SETUP_PROJECT_NUMBER` | [Manual/configured PAT + Actions configuration](#projects-v2-authentication) | ### Automatic repository token @@ -168,10 +169,26 @@ PROJECT_SETUP_PAT=ghp_your_token_here For Actions: repository **Settings** → **Secrets and variables** → **Actions** → **New repository secret** → `PROJECT_SETUP_PAT`. +When PR Sync should update Project v2, also create the Actions repository variable `PROJECT_SETUP_PROJECT_NUMBER` with the target Project number. `PROJECT_SETUP_OWNER_TYPE` may optionally be added as an Actions repository variable with `user` or `organization`; otherwise the existing authenticated owner auto-detection is used. + Project owner type is independent from authentication. Configure `PROJECT_SETUP_OWNER_TYPE=user` or `PROJECT_SETUP_OWNER_TYPE=organization`, or leave it empty for auto-detection. See [Project v2 owner type](docs/repo/project-owner-type.md). Never commit `.env`. See GitHub's documentation for [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) and [Projects automation](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/automating-projects-using-actions). +### PR Sync in installed repositories + +The core installer now includes `.github/workflows/pr-sync.yml` and the reusable `project_setup.pr_sync` module. PR Sync runs after successful trusted PR metadata/Guardrails validation and handles direct draft/closed lifecycle transitions without executing code from an untrusted PR head. + +For an implementation PR, use a closing reference in the PR body: + +```text +Closes #123 +``` + +The linked task can then drive configured PR labels, milestone and assignees, its parent/sub-issue relationship, and optional Project v2 lifecycle status. Repository-level synchronization uses `${{ github.token }}`. Project v2 synchronization is optional and requires the PAT/Project configuration described above. Promotion PRs `develop -> Q.A` and `Q.A -> main` are skipped by default. + +See [PR Sync](docs/repo/pr-sync.md) for the complete behavior and configuration contract. + ## 4. Common commands The target path and repository are normally read from `.env`. Remote mutating operations remain dry-run by default. Add `LIVE=1` only after reviewing the preview. @@ -231,10 +248,11 @@ The tool is intentionally conservative because repository setup mixes local file - **No credential logging:** diagnostics show credential source/status, never token values. - **Safe HTTP behavior:** GitHub requests have a finite timeout and are restricted to `https://api.github.com`. - **Mutation retry protection:** automatic transport retries are limited to idempotent reads. A lost response after a `POST`, `PATCH`, or `DELETE` is not automatically replayed. -- **Trusted privileged workflows:** workflows using `pull_request_target` execute automation from the trusted base branch. Read-only test workflows may validate proposed PR content. +- **Trusted privileged workflows:** workflows using `pull_request_target` execute only trusted metadata/base automation. The `Q.A` and `main` promotion gates are metadata-only and never check out PR-head code; PR Sync checks out only the trusted base with credential persistence disabled. +- **Explicit source identity:** `.project-setup-source` identifies this tool's source repository and is intentionally not installed into target repositories, preventing embedded targets from inheriting source-only validation contracts. - **Cross-platform entry points:** `.env` is parsed by Python rather than directly included by Make, keeping quoting and Windows behavior aligned with the CLI. -Current intentional limits: generated issues are not idempotent yet, Project v2 views remain manual, rulesets/branch protection are not created, and milestone synchronization inspects at most the first 100 existing milestones. +Current intentional limits: generated issues are not idempotent yet, Project v2 views remain manual, rulesets/branch protection are not created, milestone synchronization inspects at most the first 100 existing milestones, and PR Sync label synchronization is additive rather than destructive. PR Sync Project updates remain optional when their PAT/Project number are not configured. ## 6. Documentation @@ -242,10 +260,11 @@ Current intentional limits: generated issues are not idempotent yet, Project v2 | --- | --- | | [AI setup guide](AI_SETUP_GUIDE.md) | Operational contract for AI assistants: inspect existing patterns first, ask only for unresolved decisions, pause for manual/credential/live checkpoints, re-verify user changes, and verify results after application. | | [Portuguese README](README.pt-BR.md) | Complete Portuguese version of this overview, quick start, authentication, commands, safety model, environment defaults, and licensing information. | +| [PR Sync](docs/repo/pr-sync.md) | Implemented PR/task/Project synchronization contract, configuration, lifecycle mapping, promotion exclusions, security model, idempotency, and limits. | | [Project owner type](docs/repo/project-owner-type.md) | How to select `user` versus `organization`, auto-detection behavior, Make overrides, GraphQL namespace handling, and Q.A coverage. | | [Project Setup runbook (pt-BR)](docs/repo/project-setup-runbook.pt-BR.md) | Operational step-by-step procedure for configuring `.env`, diagnosing the environment, previewing, installing, and applying the tool in a target repository. | -| [Shared tool internals](docs/repo/project-setup-shared-tool.md) | Distribution model, package/CLI boundaries, authentication boundary, request-safety decisions, and what the reusable core automates. | -| [Branching policy](docs/repo/branching-policy.md) | Supported branch naming/source rules and the repository policy enforced around pull requests. | +| [Shared tool internals](docs/repo/project-setup-shared-tool.md) | Distribution model, source/embedded boundary, package/CLI responsibilities, authentication boundary, request-safety decisions, and reusable automation internals. | +| [Branching policy](docs/repo/branching-policy.md) | Supported branch naming/source rules, trusted metadata-only promotion gates, source marker behavior, and PR automation boundaries. | | [Project board policy](docs/repo/project-board-policy.md) | Expected Project v2 fields, statuses, item types, and conventions used by the generic manifests. | | [Script reference contract](docs/repo/script-reference-contract.md) | Contract that prevents validation scripts from becoming orphaned: each script must have an explicit caller and installer reference. | | [Documentation guide](docs/DOCUMENTATION-GUIDE.md) | Map connecting configuration files, workflows, implementation files, and their authoritative documentation. | From 8ea1970bd0944c414f942294d9845a899357dcd3 Mon Sep 17 00:00:00 2001 From: Vitor Guttler Date: Mon, 10 Aug 2026 13:28:05 -0300 Subject: [PATCH 17/17] Document PR Sync onboarding and Actions configuration in Portuguese --- README.pt-BR.md | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.pt-BR.md b/README.pt-BR.md index 70d4d4c..9ef79e3 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -10,7 +10,7 @@ O `project_setup` é uma ferramenta autocontida para instalar e operar automações reutilizáveis em repositórios GitHub. Ela combina Makefile, CLI Python, workflows do GitHub Actions, manifests e scripts de validação para que repositórios possam ser configurados manualmente, por automação ou com auxílio de IA sem esconder o que será alterado. -O foco é configurar com segurança labels, milestones, issues, sub-issues, guardrails de pull request, descoberta de repositório e GitHub Projects v2. Comandos remotos mutáveis usam dry-run por padrão e exigem modo live explícito antes de escrever no GitHub. +O foco é configurar com segurança labels, milestones, issues, sub-issues, guardrails de pull request, **PR Sync**, descoberta de repositório e GitHub Projects v2. Comandos remotos mutáveis usam dry-run por padrão e exigem modo live explícito antes de escrever no GitHub. Se uma IA for realizar ou conduzir o setup, forneça a ela o arquivo [`AI_SETUP_GUIDE.md`](AI_SETUP_GUIDE.md). Esse guia orienta o agente a inspecionar padrões existentes antes de perguntar, parar em checkpoints manuais/de credenciais/live, verificar novamente alterações feitas pelo usuário e evitar criação duplicada de recursos. @@ -26,6 +26,7 @@ Usos típicos: - opcionalmente vincular tasks como sub-issues; - criar e sincronizar GitHub Projects v2 pertencentes a uma conta pessoal ou GitHub Organization; - validar metadados de pull request e convenções do repositório; +- sincronizar PRs de implementação com suas tasks vinculadas e, opcionalmente, com o ciclo de vida no Project v2 através da PR Sync; - inspecionar um projeto e recomendar um fluxo de setup antes de aplicar alterações; - disponibilizar as mesmas operações para pessoas, scripts e agentes de IA através de comandos previsíveis em Make/CLI. @@ -71,7 +72,7 @@ PROJECT_SETUP_CONFIG=project_setup.json PROJECT_SETUP_PROJECT_NUMBER= ``` -`PROJECT_SETUP_TARGET` é o caminho local no sistema de arquivos. `GITHUB_REPOSITORY` é o identificador `owner/repository` no GitHub. `PROJECT_SETUP_OWNER_TYPE` seleciona quem é o proprietário do GitHub Projects v2: use `user` para uma conta pessoal ou `organization` para uma empresa/equipe representada por GitHub Organization. Ele pode ficar vazio para autodetecção durante operações autenticadas. Quando um Project v2 já existir, `PROJECT_SETUP_PROJECT_NUMBER` pode guardar o número utilizado por `make project-sync`. +`PROJECT_SETUP_TARGET` é o caminho local no sistema de arquivos. `GITHUB_REPOSITORY` é o identificador `owner/repository` no GitHub. `PROJECT_SETUP_OWNER_TYPE` seleciona quem é o proprietário do GitHub Projects v2: use `user` para uma conta pessoal ou `organization` para uma empresa/equipe representada por GitHub Organization. Ele pode ficar vazio para autodetecção durante operações autenticadas. Quando um Project v2 já existir, `PROJECT_SETUP_PROJECT_NUMBER` pode guardar o número utilizado por `make project-sync` e também é o valor que a PR Sync espera como variável de repositório do Actions quando a sincronização de Project estiver habilitada. Se a ferramenta já estiver incorporada no próprio repositório-alvo e for executada de dentro dele, use: @@ -125,7 +126,7 @@ make setup TARGET=../outro-projeto REPO=owner/outro-projeto OWNER_TYPE=organizat | Operações do próprio repositório dentro do GitHub Actions | `${{ github.token }}` exposto como `GITHUB_TOKEN` | [Automático — sem secret personalizado](#token-automático-do-repositório) | | Labels, milestones, issues, comentários e operações similares executadas localmente | `gh auth` válido, `GITHUB_TOKEN`, `GH_TOKEN` ou `PROJECT_SETUP_PAT` | [Manual/configurado](#autenticação-local) | | GitHub Projects v2 localmente | `PROJECT_SETUP_PAT` no `.env` | [PAT manual/configurada](#autenticação-do-projects-v2) | -| GitHub Projects v2 pelo Actions | secret de repositório `PROJECT_SETUP_PAT` | [PAT + secret manual/configurado](#autenticação-do-projects-v2) | +| GitHub Projects v2 pelo Actions / PR Sync | secret de repositório `PROJECT_SETUP_PAT` mais variável de repositório `PROJECT_SETUP_PROJECT_NUMBER` | [PAT + configuração do Actions](#autenticação-do-projects-v2) | ### Token automático do repositório @@ -168,10 +169,26 @@ PROJECT_SETUP_PAT=ghp_seu_token_aqui Para Actions: **Settings** do repositório → **Secrets and variables** → **Actions** → **New repository secret** → `PROJECT_SETUP_PAT`. +Quando a PR Sync também deve atualizar o Project v2, crie a variável de repositório do Actions `PROJECT_SETUP_PROJECT_NUMBER` com o número do Project. `PROJECT_SETUP_OWNER_TYPE` pode opcionalmente ser criada como variável de Actions com `user` ou `organization`; sem ela, permanece a autodetecção autenticada existente. + O tipo do owner é independente da autenticação. Configure `PROJECT_SETUP_OWNER_TYPE=user` ou `PROJECT_SETUP_OWNER_TYPE=organization`, ou deixe vazio para autodetecção. Consulte [Tipo de proprietário do Project v2](docs/repo/project-owner-type.pt-BR.md). Nunca versione o `.env`. Consulte a documentação do GitHub sobre [personal access tokens](https://docs.github.com/pt/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) e [automação de Projects](https://docs.github.com/pt/issues/planning-and-tracking-with-projects/automating-your-project/automating-projects-using-actions). +### PR Sync em repositórios instalados + +O instalador core agora inclui `.github/workflows/pr-sync.yml` e o módulo reutilizável `project_setup.pr_sync`. A PR Sync roda após a validação confiável de metadata/Guardrails e trata diretamente transições de draft/closed sem executar código do head não confiável do PR. + +Em um PR de implementação, use uma referência de fechamento no body: + +```text +Closes #123 +``` + +A task vinculada pode então fornecer as famílias configuradas de labels, milestone e assignees do PR, a relação pai/sub-issue e, opcionalmente, o Status no Project v2. A sincronização dentro do repositório usa `${{ github.token }}`. A parte de Project v2 é opcional e exige a configuração de PAT/Project descrita acima. PRs de promoção `develop -> Q.A` e `Q.A -> main` são ignorados por padrão. + +Consulte [PR Sync](docs/repo/pr-sync.pt-BR.md) para o contrato completo de comportamento e configuração. + ## 4. Comandos principais O caminho local e o repositório remoto são normalmente lidos do `.env`. Dry-run continua sendo o padrão para operações remotas mutáveis. Use `LIVE=1` somente depois de revisar a simulação. @@ -231,10 +248,11 @@ A ferramenta é deliberadamente conservadora porque o setup mistura arquivos loc - **Sem log de credenciais:** os diagnósticos mostram origem/estado, nunca o valor dos tokens. - **HTTP restrito:** as chamadas têm timeout finito e ficam restritas a `https://api.github.com`. - **Proteção contra repetição de mutações:** retentativas automáticas de transporte ficam limitadas a leituras idempotentes. Uma resposta perdida após `POST`, `PATCH` ou `DELETE` não provoca replay automático. -- **Workflows privilegiados confiáveis:** workflows com `pull_request_target` executam automação da branch-base confiável. Workflows de teste somente leitura podem validar o conteúdo proposto pelo PR. +- **Workflows privilegiados confiáveis:** workflows com `pull_request_target` executam somente metadata/automação da base confiável. Os gates de promoção para `Q.A` e `main` são metadata-only e nunca fazem checkout do head do PR; a PR Sync faz checkout somente da base confiável com persistência de credenciais desabilitada. +- **Identidade explícita do source:** `.project-setup-source` identifica o repositório-fonte desta ferramenta e não é instalado em targets, evitando que repositórios embarcados herdem contratos de validação exclusivos do source. - **Entrada multiplataforma:** o `.env` é interpretado pelo Python, em vez de ser incluído diretamente pelo Make, mantendo aspas e comportamento no Windows alinhados com a CLI. -Limitações intencionais atuais: geração de issues ainda não é idempotente, views do Project v2 continuam manuais, rulesets/branch protection não são criados e a sincronização de milestones consulta no máximo os primeiros 100 milestones existentes. +Limitações intencionais atuais: geração de issues ainda não é idempotente, views do Project v2 continuam manuais, rulesets/branch protection não são criados, a sincronização de milestones consulta no máximo os primeiros 100 milestones existentes e a sincronização de labels pela PR Sync é aditiva, não destrutiva. Atualizações de Project pela PR Sync continuam opcionais quando PAT/número do Project não estiverem configurados. ## 6. Documentação @@ -242,10 +260,11 @@ Limitações intencionais atuais: geração de issues ainda não é idempotente, | --- | --- | | [Guia de setup para IA](AI_SETUP_GUIDE.md) | Contrato operacional para agentes de IA: consultar padrões existentes primeiro, perguntar apenas decisões pendentes, parar em checkpoints manuais/de credenciais/live, verificar novamente mudanças do usuário e validar resultados após a aplicação. | | [README em inglês](README.md) | Versão principal internacional com visão geral, quick start, autenticação, comandos, segurança, defaults de ambiente e licença. | +| [PR Sync](docs/repo/pr-sync.pt-BR.md) | Contrato implementado de sincronização PR/task/Project, configuração, mapeamento de ciclo de vida, exclusões de promoção, segurança, idempotência e limites. | | [Tipo de proprietário do Project v2](docs/repo/project-owner-type.pt-BR.md) | Como selecionar `user` ou `organization`, autodetecção, overrides no Make, tratamento do namespace GraphQL e cobertura de Q.A. | | [Runbook do Project Setup](docs/repo/project-setup-runbook.pt-BR.md) | Procedimento operacional passo a passo para configurar `.env`, diagnosticar, simular, instalar e aplicar a ferramenta em outro repositório. | -| [Internos da ferramenta compartilhada](docs/repo/project-setup-shared-tool.md) | Modelo de distribuição, limites do pacote/CLI, fronteira de autenticação, decisões de segurança HTTP e escopo do core reutilizável. | -| [Política de branches](docs/repo/branching-policy.pt-BR.md) | Convenções de nomes/origens de branches e regras esperadas para pull requests. | +| [Internos da ferramenta compartilhada](docs/repo/project-setup-shared-tool.md) | Modelo de distribuição, fronteira source/embedded, responsabilidades do pacote/CLI, autenticação, segurança de requests e internos da automação reutilizável. | +| [Política de branches](docs/repo/branching-policy.pt-BR.md) | Convenções de nomes/origens, gates confiáveis metadata-only, comportamento do marker do source e limites da automação de PR. | | [Política do Project board](docs/repo/project-board-policy.pt-BR.md) | Fields, statuses, tipos de item e convenções esperadas pelo Project v2 e pelos manifests genéricos. | | [Contrato de referências de scripts](docs/repo/script-reference-contract.pt-BR.md) | Regra que evita scripts órfãos: todo script precisa ter chamador explícito e referência no instalador. | | [Guia da documentação](docs/DOCUMENTATION-GUIDE.md) | Mapa entre arquivos de configuração, workflows, implementações e documentação autoritativa. |