From 4442aae41a2e8bb4a2d43580e52e7770471c7630 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 9 Sep 2026 16:57:52 +0200 Subject: [PATCH 1/9] develop: add SDD standard and release orchestration spec --- .agents/skills/product-specification/SKILL.md | 61 + AGENTS.md | 15 + specs/README.md | 222 +++ specs/_template.md | 337 ++++ specs/configurable-release-orchestration.md | 1560 +++++++++++++++++ 5 files changed, 2195 insertions(+) create mode 100644 .agents/skills/product-specification/SKILL.md create mode 100644 specs/README.md create mode 100644 specs/_template.md create mode 100644 specs/configurable-release-orchestration.md diff --git a/.agents/skills/product-specification/SKILL.md b/.agents/skills/product-specification/SKILL.md new file mode 100644 index 000000000..9df4c55bf --- /dev/null +++ b/.agents/skills/product-specification/SKILL.md @@ -0,0 +1,61 @@ +--- +name: product-specification +description: Create, revise, or review repository product and engineering specifications, SDDs, and implementation RFCs with explicit architecture, test budgets, documentation, bounded configuration, and user-facing GitHub UX. Use when a document will guide implementation or acceptance; do not use for implementation-only tasks or informal answers. +--- + +# Product Specification + +Produce a specification that lets product, engineering, reviewers, and +operators understand the same intended product and verify when it is complete. + +Before drafting or reviewing, read these repository sources in full: + +- [Product Specification Standard](../../../specs/README.md) +- [Specification template](../../../specs/_template.md) + +Use existing code, workflows, documentation, configuration, and observed +failures as evidence. Follow the repository's Graphify rules before broad source +exploration. Research external behavior when it is unstable, provider-specific, +safety-critical, or central to the decision, and link primary sources. + +## Working method + +1. Establish current behavior and evidence before proposing changes. +2. Separate product requirements, implementation choices, recommended defaults, + safety invariants, non-goals, and open decisions. +3. Describe the complete user/operator journey, including pending, successful, + partial, blocked, retried, and canceled states. +4. Apply Clean Architecture to the project's real boundaries: identify pure + decisions, use cases, semantic ports, adapters, composition, presentation, + state ownership, trust boundaries, and executable dependency constraints. +5. Define configuration as a bounded product contract: defaults, values/ranges, + invalid combinations, precedence, persistence, migration, and intentionally + non-configurable safety rules. +6. Treat issues, PRs, comments, labels, checks, summaries, CLI output, and docs as + UI when people rely on them. Show representative content and navigation, not + only the data or API behind it. +7. Make complex relationships visual with a small diagram, state table, + timeline, or Markdown wireframe. Always provide an adjacent textual + equivalent and never rely on color or emoji alone. +8. Define a numeric, risk-derived test budget with distribution by behavior + area, coverage expectations, integration/replay/race/security cases, and any + required human UX evidence. A count alone is never sufficient. +9. Specify documentation deliverables for users, setup, configuration, + operations/recovery, migration, and architecture as applicable. +10. Finish with observable acceptance scenarios, requirement traceability, an + implementation sequence, and a Definition of Done containing every quality + gate. + +## Quality boundary + +Be proportional: simple changes can mark sections not applicable with a reason. +Never omit architecture, testing, documentation, configuration, UX, security, +or operations merely because the implementation has not been designed yet. + +Prefer realistic states, messages, and links in examples while clearly +distinguishing examples from fixed configuration. Keep the primary product view +plain and close to the user's language; place internal identifiers, provider +DTOs, stack traces, and low-level diagnostics in technical detail. + +Do not declare a spec ready while a decision that can materially change +architecture, public behavior, data safety, or acceptance remains unresolved. diff --git a/AGENTS.md b/AGENTS.md index 6511cd1dd..de93f774b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,18 @@ Rules: - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). + +## Product specifications + +When creating, revising, or reviewing a product/engineering specification, SDD, +or implementation RFC: + +1. Read `.agents/skills/product-specification/SKILL.md` in full before drafting. +2. Apply the canonical quality standard in `specs/README.md`. +3. Start new specifications from `specs/_template.md`, adapting sections to the + risk and scope instead of deleting a concern silently. +4. Treat GitHub issues, pull requests, comments, checks, and Job Summaries as + product UI whenever users or maintainers interact with them. +5. Include concrete flows, diagrams, representative UI/content examples, a + numeric test budget, documentation work, configuration boundaries, and + executable acceptance criteria whenever applicable. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 000000000..2572deb15 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,222 @@ +# Product Specification Standard + +This directory contains implementation-driving product and engineering +specifications. A spec is a shared product contract, not only a technical design. +It must make the intended behavior understandable to someone who did not take +part in the original discussion and testable by someone who did not implement it. + +New specifications start from [`_template.md`](./_template.md). Existing specs +should converge on this standard when they are materially revised. + +## 1. Core criteria + +Every spec must be: + +- **Product-near:** use the names, states, actions, and surfaces that users and + operators will actually see. +- **Visual where it helps:** show multi-step flows, state changes, ownership, or + several relationships with a compact diagram, table, timeline, or Markdown + wireframe. +- **Textually complete:** every visual has an adjacent textual equivalent; color, + emoji, screenshots, and Mermaid are never the only source of meaning. +- **Explicit:** distinguish current facts, proposed behavior, recommended + defaults, configurable choices, fixed safety limits, non-goals, and open + questions. +- **End-to-end:** include happy paths, alternatives, partial success, failure, + retry, cancellation, migration, and cleanup where relevant. +- **Verifiable:** important statements use normative language and map to an + automated test, contract check, or explicit human acceptance artifact. +- **Proportional:** a concern may be marked not applicable with a reason; it must + not disappear silently. + +Use `MUST`, `SHOULD`, and `MAY` deliberately. A `MUST` requires an acceptance +scenario. Avoid prescribing implementation details unless they protect product +behavior, architecture, interoperability, or safety. + +## 2. Required specification contract + +A specification must cover the following concerns or mark them not applicable: + +| Concern | Required outcome | +|---|---| +| Decision metadata | Status, date, owners, scope, review gates, and unresolved decisions | +| Problem and evidence | Current behavior, concrete pain, affected actors, and evidence/source | +| Goals and limits | Measurable outcomes, non-goals, fixed safety/product invariants | +| Product journey | Current versus proposed flow, actors, entrypoints, visible surfaces, next actions | +| Domain model | Shared terminology, invariants, states, transitions, and ownership of facts | +| Configuration | Recommended defaults, bounded alternatives, validation, precedence, persistence, migration | +| Architecture | Dependency direction, pure policy, use cases, ports, adapters, presentation, state and trust boundaries | +| UI/UX and content | Information hierarchy, examples for every primary state, navigation, accessibility, localization | +| Failure and recovery | Impact, partial-success facts, retryability, idempotency, cleanup, operator action | +| Security and privacy | Permissions, untrusted input, secrets, execution boundaries, auditability | +| Observability | User status, operator evidence, logs/metrics, correlation, noise budget | +| Compatibility and rollout | Existing in-flight state, migration, deprecation, rollout and rollback | +| Testing | Numeric risk-derived budget, distribution, coverage, fixtures, integration and human evidence | +| Documentation | User, setup, configuration, recovery, migration, architecture and discoverability | +| Acceptance and DoD | Observable scenarios, traceability, implementation order, all quality gates | + +## 3. Product and GitHub UI/UX standard + +GitHub issues, pull requests, reviews, comments, checks, labels, releases, and Job +Summaries are product surfaces when a person uses them to understand or operate a +feature. CLI output and generated documentation are product surfaces for the same +reason. + +For each primary state, the first visible content must answer in this order: + +1. What is happening now? +2. What completed already? +3. What happens next? +4. Is human action required? +5. What is the impact, especially for partial success or failure? +6. Where can the result or transition be inspected? +7. Where are technical details available? + +A spec affecting these surfaces must include representative rendered Markdown +or a wireframe for at least pending, action-required, blocked/failed, partially +complete, and complete states. Use actual product terminology and plausible data. +Do not specify only abstract fields such as `status`, `message`, and `url`. + +The UX contract must define, when applicable: + +- one primary status and one primary action; +- progress/state representation and the facts behind it; +- deterministic titles, headings, labels, and durable markers; +- descriptive links to the issue, PR, comparison, commit, run, artifact, release, + package, documentation, and recovery action; +- progressive disclosure: user meaning first, technical diagnostics later; +- error content in `impact -> cause -> action -> retained state` order; +- idempotent update behavior and a bounded comment/notification budget; +- locale source, supported locales, and fallback behavior; +- narrow/mobile readability, light/dark themes, and logical heading/table use; +- sanitization of mentions, commands, Markdown, HTML markers, URLs, and diagrams; + and +- a textual fallback for Mermaid, images, icons, and other visual elements. + +For a flow with three or more dependent transitions, include one overview visual. +For a user-facing issue or PR change, include at least one concrete content +mockup. Screenshots can supplement these artifacts but cannot replace the +semantic written contract. + +## 4. Clean Architecture standard + +Apply Clean Architecture to the repository's real boundaries rather than +inventing a parallel folder taxonomy. A design must identify: + +- domain entities, value objects, invariants, and pure policies; +- application use cases and narrow immutable input/output contracts; +- semantic ports that express capabilities without provider DTOs; +- data/provider adapters and their error mappings; +- infrastructure and composition ownership; +- entrypoint responsibility and trusted/untrusted event boundaries; +- presentation view models and renderers, separate from mutation policy; +- durable state, concurrency, idempotency, and migration ownership; and +- forbidden dependency directions that can be enforced automatically. + +Use a component/dependency diagram when three or more layers or external systems +interact. Use a sequence or state diagram when ordering, retries, asynchronous +events, or partial completion affect correctness. + +Architecture requirements are incomplete without executable constraints. The +spec must state which dependency, contract, cycle, schema, or workflow checks +will prevent boundary erosion. + +## 5. Configuration standard + +Configuration exists to support legitimate product variation, not to make every +implementation detail optional. Every input must define: + +| Field | Required information | +|---|---| +| Name and type | Stable public name, type, and owning surface | +| Recommended default | Safe default and why it is recommended | +| Allowed values/range | Bounded choices with observable semantics | +| Validation | Invalid values and invalid cross-field combinations | +| Scope and precedence | Project/repository/operation scope and override order | +| Persistence | Whether an in-flight operation snapshots or rereads the value | +| Migration | Behavior for absent, legacy, renamed, or future values | +| Security | Values that must never accept arbitrary code, refs, URLs, or secrets | + +Show at least one recommended configuration and one meaningful alternative. +State which correctness and safety invariants are intentionally not configurable. + +## 6. Test budget standard + +Every implementation-driving spec must contain a numeric test budget. Derive it +from the behavior and risk inventory; do not reuse one universal count for small +and large changes. + +The budget table must distribute a minimum number of distinct cases across the +applicable areas: + +- domain/configuration/pure planning; +- state transitions, idempotency, replay, cancellation, and races; +- application use cases; +- adapters and provider error mapping; +- workflows, permissions, schemas, and setup contracts; +- presentation states, links, localization, accessibility, and sanitization; +- integration/end-to-end and compatibility/migration; and +- security/abuse cases. + +Each `it`/`test` case counts once. Parameterized rows count separately only when +they represent distinct behavior. The number is a floor, not a substitute for +covering every requirement and failure mode. + +The spec must also define: + +- repository-wide thresholds that remain in force; +- higher branch coverage for new pure policy where justified; +- changed-module expectations; +- deterministic fakes instead of real waits or live services; +- fixtures/golden output without relying only on snapshots; +- workflow/contract checks that parse structure rather than grep prose; +- required manual UX evidence that automation cannot establish; and +- a traceability mapping from each normative requirement to tests or evidence. + +## 7. Documentation standard + +Documentation is an implementation deliverable. Identify the applicable +audiences and artifacts: + +- user journey and recommended default; +- setup, permissions, environments, credentials, and external prerequisites; +- complete configuration reference and examples; +- operator state model, troubleshooting, recovery, replay, and cleanup; +- upgrade, migration, compatibility, deprecation, and rollback; +- architecture, contracts, trust boundaries, and contributor guidance; and +- route/navigation registration, links, assets, examples, and validation jobs. + +Lead documentation with the normal path and one helpful visual, then disclose +configuration, failures, and internals progressively. Examples must be checked +against implementation fixtures or contract tests whenever practical. + +## 8. Acceptance, traceability, and review + +Acceptance scenarios describe observable behavior, not implementation activity. +Cover normal, alternative, boundary, invalid, duplicate, out-of-order, partial, +failure, recovery, migration, security, UX, and accessibility behavior as +applicable. + +Maintain a traceability table for substantial specs: + +```text +Requirement -> domain/policy/use case/adapter/presentation -> test or evidence -> documentation +``` + +Before changing a spec to `Ready for implementation`, reviewers must be able to +answer yes to the following: + +- Is the origin/current behavior supported by evidence? +- Can a user understand the proposed product without reading implementation code? +- Are product defaults and non-configurable safety limits explicit? +- Are architecture boundaries and executable constraints clear? +- Is the numeric test budget proportional and complete? +- Are documentation and migration deliverables named? +- Do UI examples cover pending, action, error/partial, and completed states? +- Can status and required action be understood quickly without logs? +- Are security, idempotency, retries, concurrency, and cleanup addressed? +- Does every `MUST` map to acceptance and verification? +- Are material decisions resolved or explicitly blocking readiness? + +The Definition of Done must repeat these gates. A concern mentioned only in an +introductory paragraph is not an enforceable delivery requirement. diff --git a/specs/_template.md b/specs/_template.md new file mode 100644 index 000000000..67babe076 --- /dev/null +++ b/specs/_template.md @@ -0,0 +1,337 @@ +# + +- Status: Draft +- Date: YYYY-MM-DD +- Owners: +- Scope: +- Related issues/PRs: +- Required review gates: product UX, architecture, testing, documentation, + security/operations +- Open decisions blocking readiness: + +> Start with the repository standard in [`README.md`](./README.md). Remove this +> note when the spec is ready. Mark a non-applicable section explicitly and give +> a reason instead of silently deleting the concern. + +## 1. Executive summary + +Describe the user-visible outcome, the recommended default, and the principal +safety/correctness rule in plain language. + +```text + +``` + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Who is affected, what they cannot understand/do safely, and why it matters. + +### 2.2 Current behavior + +Number the actual current sequence. Separate verified facts from assumptions. + +### 2.3 Evidence + +- Code/workflow/documentation references: +- Incidents or concrete examples: +- External primary sources: +- Unknowns: + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| | | | | + +Define product terms, branch/data roles, ownership of facts, and language that +must remain consistent across UI, code, tests, and documentation. + +## 4. Goals, non-goals, and fixed invariants + +### 4.1 Goals + +1. + +### 4.2 Non-goals + +1. + +### 4.3 Fixed product/safety invariants + +1. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| | | | | + +For three or more dependent transitions, add a visual and a textual equivalent: + +```mermaid +flowchart LR + A[User entry point] --> B[System transition] + B --> C[Visible result] +``` + +Text equivalent: ` -> -> `. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. + +### 6.2 Alternative paths + +- + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| | | | | | + +Define duplicate, stale, out-of-order, canceled, and partially completed event +behavior where applicable. + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| `` | | `` — | | | + +Document: + +- validation and invalid cross-field combinations; +- precedence and operation-time snapshot/reread behavior; +- migration, legacy, unknown, and deprecated values; +- one recommended example and one meaningful alternative; and +- behavior intentionally not configurable. + +## 8. Clean Architecture design + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | | | +| Application | | | +| Adapters/data | | | +| Infrastructure/composition | | | +| Entrypoints | | | +| Presentation | | | + +```mermaid +flowchart LR + E[Entrypoint] --> A[Application use case] + A --> D[Domain policy] + A --> P[Semantic port] + I[Provider adapter] --> P + A --> V[Presentation model] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: +- Application contracts: +- Semantic ports: +- Durable state/schema ownership: +- Concurrency/idempotency strategy: +- Trusted and untrusted inputs: +- Provider error mapping: + +### 8.3 Executable architecture constraints + +- + +## 9. UI/UX and content contract + +Treat issues, PRs, comments, labels, checks, summaries, CLI output, and docs as UI +when applicable. + +### 9.1 Information hierarchy + +1. Current status +2. Completed facts +3. Next transition +4. Human action, or explicit statement that none is needed +5. Impact and inspectable links +6. Collapsed technical evidence + +### 9.2 Representative primary view + +```markdown + + +# + +> **Current status:** +> +> **Action required:** + +## Progress + +- [x] +- [ ] + +## What happens next + + + +## Links + +[Descriptive destination](...) · [Descriptive destination](...) + +
+Technical details + +
+``` + +Add concrete variants for: + +- pending/no action; +- action required; +- blocked or failed before irreversible effects; +- partially successful after an irreversible effect; and +- completed. + +### 9.3 Issue, PR, and comment behavior + +- Deterministic titles and purpose: +- What merge/close/retry will do: +- Durable marker and idempotent update policy: +- Comment/notification budget: +- Labels and checks as supplemental state: +- Direct links and navigation: + +### 9.4 Accessibility, localization, and responsive behavior + +- Supported locales and fallback: +- Textual equivalent for visuals: +- Narrow/mobile and light/dark expectations: +- Heading/table/link rules: +- Sanitization of untrusted Markdown, mentions, commands, markers, and URLs: + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| | | | | | | + +User-facing errors follow `impact -> cause -> action -> retained state` and must +not imply that an already successful irreversible action failed. + +## 11. Security, permissions, and privacy + +1. +2. +3. +4. + +## 12. Observability and operational UX + +- User-facing state: +- Job Summary/operator evidence: +- Logs, metrics, and correlation: +- Pending external dependency versus workflow failure: +- Rate-limit/retry behavior: +- Noise/comment budget: + +## 13. Compatibility, migration, rollout, and rollback + +- Existing in-flight operations/data: +- Schema/configuration migration: +- Deprecated behavior and compatibility window: +- Rollout stages/feature flag if needed: +- Rollback and irreversible effects: + +## 14. Testing strategy and numeric budget + +The count is derived from the behavior/risk inventory and is a minimum, not a +substitute for requirement coverage. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Domain/configuration/pure planning | | | +| State/application/idempotency/races | | | +| Adapters/provider contracts | | | +| Workflows/setup/schema | | | +| UI/UX/localization/sanitization | | | +| Integration/security/migration | | | +| **Total** | **** | No double counting | + +Define: + +- repository and changed-module coverage thresholds; +- pure-policy branch coverage; +- table-driven, replay, race, adapter, workflow-contract, and security tests; +- deterministic clocks/IDs/fakes with no real waits or live services; +- golden UI fixtures plus semantic assertions; and +- manual UX evidence for readability that automation cannot establish. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User | | | | +| Setup owner | | | | +| Operator | | | | +| Contributor | | | | + +Include migration and troubleshooting documentation where applicable. Keep +examples synchronized with implementation fixtures or contract tests. + +## 16. Acceptance scenarios + +1. Given ``, when ``, then ``. +2. +3. +4. +5. +6. +7. +8. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| `
` | | | | + +## 18. Implementation sequence + +1. Contracts, decisions, configuration, and architecture checks. +2. Pure policies and tests. +3. State/application orchestration and tests. +4. Adapters/workflows and contract tests. +5. Presentation, fixtures, accessibility, and localization. +6. Migration, recovery, security, documentation, and generated artifacts. +7. Integration, coverage, human UX evidence, and final validation. + +Adjust ordering to risk and dependencies; do not postpone documentation or UX +until after behavior is considered complete. + +## 19. Definition of Done + +- [ ] Every normative requirement has acceptance and traceability. +- [ ] Architecture boundaries are implemented and automatically enforced. +- [ ] Numeric test budget, distribution, and coverage requirements pass. +- [ ] Configuration defaults, limits, validation, persistence, and migration agree. +- [ ] Primary UI states and content examples are implemented and reviewed. +- [ ] Accessibility, localization, responsive behavior, sanitization, and noise + budgets pass. +- [ ] User/setup/operator/contributor documentation is complete and discoverable. +- [ ] Failure, partial success, retry, idempotency, security, and cleanup pass. +- [ ] Required generated artifacts and repository validation commands pass. +- [ ] No readiness-blocking decision remains unresolved. + +## 20. References and decisions + +- Primary sources: +- Related specs/ADRs/issues/PRs: +- Decisions and rejected alternatives: +- Follow-up work explicitly outside this spec: diff --git a/specs/configurable-release-orchestration.md b/specs/configurable-release-orchestration.md new file mode 100644 index 000000000..d035fa1aa --- /dev/null +++ b/specs/configurable-release-orchestration.md @@ -0,0 +1,1560 @@ +# Configurable Production-First Release Orchestration + +- Status: Ready for implementation +- Date: 2026-09-09 +- Owners: Copilot maintainers +- Scope: release and hotfix promotion, publication, reconciliation, and cleanup +- Required review gates: architecture, product UX, security, and operations +- Standard: [Product Specification Standard](./README.md) + +## 1. Summary + +Copilot shall replace its post-publication, runner-blocking branch merge flow with +an event-driven and resumable release orchestration. + +A release branch remains a frozen cut of the configured development branch. It +is prepared and validated on that branch, promoted to the configured production +branch through a pull request, and only then tagged and published. After +publication, the accepted production state is reconciled into the current +development line. + +The recommended default is: + +```text +development branch + -> cut release/ + -> prepare, build, validate and smoke-test release/ + -> PR release/ -> production branch + -> GitHub-managed merge after required checks + -> tag the accepted production commit + -> publish npm through OIDC + -> publish the GitHub Release and update the major action tag + -> reconcile production -> development + -> close the launcher issue and clean temporary branches +``` + +Hotfixes use the same production-first promotion rule, but their source is the +latest production tag rather than the development branch. + +The behavior is configurable through a bounded set of validated strategies. +Arbitrary source/target graphs and publication before production acceptance are +intentionally not configurable. + +## 2. Problem statement + +The current project workflow: + +1. prepares and commits version/build files on the release branch; +2. creates the version tag from the release branch; +3. publishes npm and the GitHub Release; +4. invokes `deployed_action`; and +5. creates `release -> main` and `release -> development` PRs concurrently. + +This has four correctness and reliability problems: + +1. The package can be publicly available before the production branch accepts + the release. +2. Both reconciliation PRs use the same release head SHA. GitHub's Checks API + lists checks for a Git ref/SHA, so checks associated with different PR bases + can be observed together or satisfy a same-named required context. +3. The Action waits and polls inside one runner with a finite timeout, despite + GitHub already owning branch protection, required checks, auto-merge, and + merge queues. +4. The operation is not resumable. A partial success can leave the package + published, PRs open or merged, the source branch retained, and the launcher + issue in an inconsistent state. Re-running may fail because the `deployed` + label, tag, release, package, or PR already exists. + +The v3.3.1 audit demonstrated the shared-SHA and timeout problem: publication +succeeded, both release PRs were created from the same head, the custom waiters +saw checks belonging to both PR contexts, and the workflow timed out before a +third-party check completed. + +## 3. Terminology and branch roles + +The design uses semantic branch roles. Concrete names continue to come from the +existing configuration. + +| Role | Existing input | Default | Meaning | +|---|---|---|---| +| Production | `main-branch` | `master` | The accepted, production-ready source line | +| Development | `development-branch` | `develop` | Integration line for the next release | +| Release source tree | `release-tree` | `release` | Prefix for versioned release branches | +| Hotfix source tree | `hotfix-tree` | `hotfix` | Prefix for emergency production fixes | +| Reconciliation tree | new `reconciliation-tree` | `sync` | Prefix for ephemeral back-merge branches | + +### 3.1 Release origin + +A release branch MUST be created from the exact commit referenced by +`development-branch` at cut time: + +```text +release origin branch = configured development branch +release origin SHA = development HEAD when the branch is created +release branch = / +``` + +Commits added later to development are outside that release. Only commits made +or merged into the release branch after the cut are additional release content. + +The release origin branch and SHA MUST be persisted with the launcher issue. +Changing repository Variables after a release starts MUST NOT change its stored +origin, branch roles, version, or orchestration strategy. + +### 3.2 Hotfix origin + +A hotfix branch MUST continue to be created from the latest accepted production +version tag: + +```text +hotfix origin ref = tags/v +hotfix branch = / +``` + +### 3.3 Promotion and reconciliation + +- Promotion moves a prepared release or hotfix into production. +- Publication creates external artifacts from the accepted production commit. +- Reconciliation carries the accepted production state, or the original source + branch in canonical Gitflow mode, into the current development line. +- A reconciliation branch is an ephemeral branch created from the current + reconciliation target and updated with the selected reconciliation source. + +## 4. Goals + +1. Never publish a new version before its promotion PR is merged into the + configured production branch. +2. Tag the exact accepted production commit, not the pre-merge release head. +3. Delegate required-check and review enforcement to GitHub. +4. Do not keep a runner alive while waiting for PR checks or human reviews. +5. Resume safely after cancellation, timeout, duplicate events, or manual + workflow re-runs. +6. Support both production-lineage and canonical Gitflow reconciliation through + safe, documented strategies. +7. Support direct PRs, ephemeral reconciliation branches, merge queues, and + create-only/manual merging through bounded configuration. +8. Preserve development commits made after the release cut. +9. Keep release and hotfix behavior consistent where their semantics overlap. +10. Make setup templates, project workflows, Action inputs, persisted issue + configuration, documentation, and tests agree. +11. Make the launcher issue and managed PRs understandable as a coherent product + without requiring maintainers to inspect workflow logs. +12. Preserve Clean Architecture boundaries and make them executable through + dependency and contract tests. + +## 5. Non-goals and fixed safety limits + +The following behavior is intentionally not configurable: + +1. A release cannot use the current development branch as the source of the + production PR. The source is always the frozen release branch. +2. A release cannot be defined as `release -> current development -> + production`; that can promote post-cut development work. +3. A new npm version cannot be published before its production PR is merged. +4. A version tag cannot point at a commit other than the accepted production + commit. +5. Managed promotion cannot bypass branch protection or force-push a protected + branch. +6. User-provided arbitrary DAGs, arbitrary refs, shell fragments, or JSON merge + plans are not accepted. +7. Production-lineage reconciliation cannot use squash or rebase merge because + it must retain production ancestry. Managed promotion and reconciliation use + merge commits unless a future specification defines equivalent ancestry + guarantees. +8. The workflow never merges the current development branch into production to + make a reconciliation PR "up to date". + +## 6. User-facing configuration + +All values MUST be Action inputs, accepted by local/setup configuration, exposed +as repository Variables by setup, validated at the boundary, documented, and +snapshotted into the launcher issue when orchestration starts. + +### 6.1 Strategy inputs + +#### `release-reconciliation-strategy` + +Default: `production-lineage` + +Allowed values: + +- `production-lineage`: promote `release -> production`, then reconcile the + accepted production commit into development. +- `canonical-gitflow`: promote `release -> production`, then reconcile the + release head into development. +- `manual`: stop after successful publication; do not create a reconciliation + PR and do not close the issue automatically. + +#### `hotfix-reconciliation-strategy` + +Default: `production-lineage` + +Allowed values: + +- `production-lineage`: promote `hotfix -> production`, then reconcile the + accepted production commit into the selected hotfix back-merge target. +- `canonical-gitflow`: promote `hotfix -> production`, then reconcile the + hotfix head into the selected hotfix back-merge target. +- `manual`: stop after successful publication. + +#### `reconciliation-pr-mode` + +Default: `auto` + +Allowed values: + +- `auto`: use merge queue when the target requires it; otherwise enable native + auto-merge when the PR is not immediately mergeable; merge immediately only + when GitHub reports all target requirements satisfied. +- `auto-merge`: use GitHub native auto-merge. Fail with an actionable message if + repository settings do not support it. +- `merge-queue`: enqueue the PR. Fail if the target or required integrations do + not support merge queues. +- `create-only`: create/reuse the PR and wait for an authorized person or another + system to merge it. +- `legacy-wait`: compatibility mode using the existing bounded poll-and-merge + adapter. It is deprecated and MUST NOT be the default. + +`merge-timeout` applies only to `legacy-wait`. Native auto-merge, merge queue, +and create-only modes are event-driven and have no runner wait timeout. + +#### `reconciliation-backmerge-mode` + +Default: `auto` + +Allowed values: + +- `auto`: inspect the target rules and branch relationship. Use `sync-branch` + when a direct PR cannot safely satisfy an up-to-date requirement; otherwise + use `direct`. +- `direct`: open the second PR directly from the selected reconciliation source + to its target. +- `sync-branch`: create a branch from the current target, merge the selected + reconciliation source into it, and open the PR from that unique head. + +The `direct` mode MUST fail with guidance rather than updating production from +development when strict target rules make the direct source out of date. + +### 6.2 Hotfix target selection + +#### `hotfix-active-release-policy` + +Default: `prefer-release` + +Allowed values: + +- `prefer-release`: if exactly one active release exists, reconcile the hotfix + into that release; otherwise reconcile into development. +- `development`: always reconcile into the development branch. +- `both`: reconcile sequentially into the active release, when present, and + development. Each target MUST use a unique reconciliation head when their + required checks can be target-dependent. + +If more than one active release is detected and the policy needs an active +release, orchestration MUST enter `blocked` and request an explicit maintainer +decision. It MUST NOT guess. + +### 6.3 Lifecycle and cleanup inputs + +#### `reconciliation-tree` + +Default: `sync` + +The branch prefix used for managed ephemeral branches. The generated name MUST +be deterministic and collision-resistant, for example: + +```text +sync/release-3.4.0-to-develop- +``` + +#### `reconciliation-cleanup` + +Default: `all` + +Allowed values: + +- `all`: delete the release/hotfix source and all reconciliation branches after + full completion. +- `source-only`: delete the release/hotfix source and retain reconciliation + branches. +- `sync-only`: retain the release/hotfix source and delete reconciliation + branches. +- `none`: retain all branches. + +No source branch is deleted before publication and every required reconciliation +target completes. + +#### `reconciliation-issue-completion` + +Default: `close` + +Allowed values: + +- `close`: close the launcher issue after the complete configured plan. +- `keep-open`: leave it open and publish a completion comment. + +`manual` reconciliation always keeps the issue open until a maintainer explicitly +marks the operation complete. + +#### `orchestration-presentation-mode` + +Default: `guided` + +Allowed values: + +- `guided`: show the current phase, completed/pending steps, one concise diagram, + artifact links, the expected next transition, and any required human action. +- `compact`: show status, current phase, next action, and primary links without a + diagram or expanded technical context. +- `quiet`: publish only the initial control-center comment, actionable failures, + requests for human input, and final completion. + +Operational facts, failure impact, and required user action are never hidden by +presentation mode. + +#### `orchestration-diagrams` + +Default: `true` + +When enabled in `guided` mode, render a small GitHub-supported Mermaid flow in +the issue control center and managed PR descriptions. Every diagram MUST have an +equivalent textual phase table. Diagrams are never the sole source of status or +instructions. + +#### `orchestration-comment-mode` + +Default: `update` + +Allowed values: + +- `update`: maintain one durable issue control-center comment and update it as + the operation advances. +- `milestones`: update the control center and additionally publish a bounded + notification for promotion merged, publication completed, reconciliation + blocked, and orchestration completed. + +`milestones` MUST publish at most four additional lifecycle comments for one +operation. Duplicate event delivery cannot create duplicate comments. + +### 6.4 Existing configuration retained + +The following inputs retain their current meaning: + +- `main-branch` +- `development-branch` +- `release-tree` +- `hotfix-tree` +- `release-workflow` +- `hotfix-workflow` +- `merge-timeout`, only for `legacy-wait` +- `issues-locale`, used for the issue control center and issue comments +- `pull-requests-locale`, used for managed PR titles and descriptions + +### 6.5 Project publication timing + +The active Copilot repository workflow additionally exposes bounded repository +Variables for npm visibility checks: + +| Variable | Default | Allowed range | +|---|---:|---:| +| `NPM_VISIBILITY_POLL_INTERVAL_SECONDS` | `20` | 10-60 seconds | +| `NPM_VISIBILITY_TIMEOUT_SECONDS` | `120` | 60-900 seconds | + +These are project workflow settings, not generic Action inputs, because the +reusable setup release template does not require npm. Invalid, missing, or +out-of-range values fail validation before publication rather than being passed +to shell arithmetic unchecked. + +### 6.6 Invalid combinations + +Validation MUST reject at least: + +- identical production and development branch names; +- any tree value equal to a protected long-lived branch name; +- empty or unsafe branch prefixes; +- `production-lineage` with a non-ancestry-preserving merge method; +- `merge-queue` when required workflow contracts do not include + `merge_group` support; +- automatic issue closure with `manual` reconciliation; +- `direct` back-merge when GitHub reports that satisfying strict rules would + require merging development into production; +- unknown enum values, including case variants not explicitly normalized. + +## 7. Recommended default flows + +### 7.1 Release + +```text +develop@D3 + -> create release/3.4.0@D3 + +develop: D3---D4---D5 + \ +release: R1---R2---R3 + | + +-- PR release/3.4.0 -> master + | + +-- required checks/reviews + +-- GitHub merge + +-- accepted production SHA P + | + +-- tag v3.4.0 -> P + +-- npm publish from v3.4.0 + +-- GitHub Release + +-- update v3 + +-- reconcile P -> current develop +``` + +When `sync-branch` is selected: + +```text +sync branch = current develop + merge accepted production SHA P +PR sync branch -> develop +``` + +This preserves D4 and D5 while adding the release changes and production +lineage. D4 and D5 never enter production. + +### 7.2 Hotfix + +```text +latest production tag + -> create hotfix/ + -> prepare and validate + -> PR hotfix -> production + -> accepted production SHA P + -> tag and publish P + -> reconcile P according to hotfix-active-release-policy +``` + +## 8. Durable orchestration state + +The issue configuration schema MUST be incremented and gain a typed +`deploymentOrchestration` object. Unknown fields and future schema versions MUST +continue to round-trip safely. + +Minimum persisted shape: + +```json +{ + "schemaVersion": 3, + "deploymentOrchestration": { + "operationId": "uuid", + "kind": "release", + "version": "3.4.0", + "phase": "promotion_pr_pending", + "strategy": "production-lineage", + "prMode": "auto", + "backmergeMode": "auto", + "cleanup": "all", + "issueCompletion": "close", + "sourceBranch": "release/3.4.0", + "sourceSha": "release-head-after-build", + "originBranch": "develop", + "originSha": "develop-head-at-cut", + "productionBranch": "master", + "developmentBranch": "develop", + "promotionPullRequest": 123, + "productionSha": "accepted-merge-commit", + "tag": "v3.4.0", + "publicationVerified": false, + "reconciliationTargets": [ + { + "targetBranch": "develop", + "sourceBranch": "master", + "syncBranch": "sync/release-3.4.0-to-develop-abcd1234", + "pullRequest": 124, + "status": "pending" + } + ], + "lastFailure": null + } +} +``` + +SHAs, PR numbers, and phase transitions are runtime state. Strategy, branch +roles, and cleanup policy are immutable configuration snapshots for one +operation. + +### 8.1 State machine + +```text +preparing + -> promotion_pr_pending + -> promoted + -> publishing + -> published + -> reconciliation_pending + -> completed + +Any non-terminal phase + -> blocked + +blocked + -> previous safe phase after explicit correction/retry +``` + +Each transition MUST be monotonic and compare the stored operation ID and +expected phase before writing. Duplicate or out-of-order events MUST be no-ops +with an observable result. + +## 9. Event-driven orchestration + +The system MUST NOT wait for PR checks in a long-running release job. + +### 9.1 Initial release workflow + +The workflow dispatched on the release branch shall: + +1. enter the mutation queue; +2. verify the stored release origin and source branch; +3. update version files; +4. build; +5. run release validation and smoke tests; +6. commit generated/version files to the release branch; +7. persist the final release source SHA; +8. create or reuse the promotion PR to production; +9. request the configured GitHub merge behavior; and +10. exit successfully in `promotion_pr_pending` without creating a tag or + publishing npm. + +### 9.2 Promotion PR completion + +A dedicated PR completion path shall listen for merged managed PRs. It cannot be +silently excluded by the generic `COPILOT_BOT_LOGIN` guard used for ordinary PR +automation. + +Before advancing, it MUST verify: + +- the event repository is the expected repository; +- the PR marker and operation ID match durable state; +- head and base branches match the stored plan; +- the PR is closed and merged, not merely closed; +- the reported production merge SHA exists and is reachable from the production + branch; and +- the release source SHA included by the promotion is the stored prepared SHA. + +It then persists `productionSha` and dispatches the publication continuation on +the production ref. The active `release_workflow.yml` and `hotfix_workflow.yml` +gain an internal execution-mode input with a default initial mode and a +continuation mode. A continuation dispatch includes the operation ID and issue +number, but all version, ref, strategy, and SHA facts are reloaded from durable +state instead of trusting dispatch input. + +Keeping publication in the same workflow file preserves the npm trusted +publisher identity already bound to `release_workflow.yml`. The continuation +mode skips branch preparation and enters only the verified tag/publication path. + +### 9.3 Publication continuation + +The publication job shall: + +1. create or verify `v` at exactly `productionSha`; +2. check out the immutable tag; +3. install locked dependencies; +4. validate release identity and package contents; +5. run the npm package smoke test; +6. publish through npm trusted publishing/OIDC; +7. poll npm visibility using the bounded project timing configuration; +8. create or verify the GitHub Release; +9. create/update and verify the major action tag; +10. replace `deploy` with `deployed` idempotently after npm and GitHub + publication are verified; and +11. create or reuse the configured reconciliation PRs. + +The npm environment and `id-token: write` permission remain limited to the npm +publication job. + +### 9.4 Reconciliation completion + +For every configured target, a merged managed PR event advances that target to +`completed`. After every target completes, the orchestrator: + +1. marks the operation `completed`; +2. applies the configured cleanup policy; +3. closes or comments on the launcher issue according to configuration; and +4. publishes a summary containing production SHA, version tag, package, release, + promotion PR, reconciliation PRs, and retained/deleted branches. + +### 9.5 Workflow event delivery + +Managed PRs MUST be created with the configured PAT or another credential whose +events can start the repository's PR completion workflow. They MUST NOT rely on +events suppressed by GitHub's recursive `GITHUB_TOKEN` protection. + +The completion workflow uses the `pull_request: closed` payload only as a wake-up +signal. It checks out a trusted base/production ref and invokes the orchestrator, +which reloads and verifies authoritative PR and issue state. It MUST NOT use +`pull_request_target` to execute code from the PR head. + +## 10. Managed PR identity + +Every managed PR MUST contain a machine-readable marker that is not used as the +sole authorization mechanism, for example: + +```html + +``` + +The operation ID, issue configuration, exact source/base pair, and live GitHub +PR data MUST all agree. + +PR creation is idempotent: + +1. reuse an open PR with the same valid operation/phase marker; +2. accept an already merged matching PR and advance; +3. block when a matching PR was closed without merge; +4. block when multiple matching PRs are found; and +5. never reuse a PR based only on a title or branch-name substring. + +## 11. Native GitHub merge behavior + +The application layer shall expose semantic ports for: + +- reading target rules/merge capabilities; +- creating/finding managed PRs; +- enabling auto-merge; +- enqueueing a PR; +- reading authoritative PR merge state; +- creating/deleting refs; +- merging a trusted source commit into an ephemeral reconciliation ref; and +- verifying commit reachability. + +GitHub GraphQL/REST details remain in provider adapters. + +The existing check-run polling adapter remains reachable only through +`legacy-wait`. New modes MUST use GitHub's mergeability and protection decisions +instead of reproducing them from Checks API responses. + +Merge queue support MUST be accompanied by `merge_group` triggers in every +required GitHub Actions workflow. Setup validation MUST warn or fail when a +required third-party check cannot be proven to support merge groups. + +## 12. Idempotency and recovery rules + +### 12.1 Tags + +- Missing tag: create it at `productionSha`. +- Existing tag at `productionSha`: treat as complete. +- Existing tag at another SHA: block; never move the immutable version tag. + +### 12.2 npm + +- Version absent: publish through OIDC. +- Exact version already visible: do not republish; verify package identity and + continue. +- Registry response ambiguous: retry bounded reads before deciding. +- Published package identity inconsistent with the stored operation: block. + +### 12.3 GitHub Release and major action tag + +- Reuse an existing release only when its tag matches the operation. +- Updating the moving major tag is allowed only after the immutable version tag + and published package are verified. +- Verify the final major tag target after update. + +### 12.4 Labels and issue closure + +- An existing `deployed` label is a fact, not an error. +- A closed launcher issue may be updated with the final summary without failing + the operation. +- Issue closure is not used as proof that reconciliation completed. + +### 12.5 Branches + +- Existing expected source/sync branches are reused after verifying their SHAs. +- A name collision with a different operation blocks the run. +- Deleted source branches do not block a retry after all facts needed from them + have been persisted and verified. + +## 13. Failure semantics + +Failures before production merge do not create a tag or publish external +artifacts. + +Failures after production merge but before publication leave the operation +resumable from `promoted`. + +Failures after publication leave the issue open unless configured otherwise, +retain enough state and branches to resume reconciliation, and report that the +artifact is already public. + +Closing a promotion PR without merging sets `blocked` and does not publish. +Closing a reconciliation PR without merging sets `blocked` and does not delete +source branches or close the issue. + +Every failure comment and Job Summary MUST distinguish: + +- promotion failure; +- publication failure; +- reconciliation failure; and +- cleanup/reporting failure after functional completion. + +Cleanup/reporting failures MUST NOT falsely mark publication or merges as +failed; they remain retryable post-completion tasks. + +## 14. Security requirements + +1. npm publication uses OIDC trusted publishing; no npm automation token is + introduced. +2. The PAT may create PRs, comments, refs, tags, and workflow dispatches only + through existing scoped repository operations. +3. A privileged continuation MUST NOT check out or execute an arbitrary PR head. + It checks out the verified production SHA or immutable version tag. +4. Managed events MUST be same-repository events and must pass the identity + checks in section 10. +5. Auto-merge and merge queue MUST respect branch protection; no admin bypass is + part of this design. +6. Source branch names, target names, prefixes, operation IDs, and version values + are validated before reaching GitHub API calls. +7. Secrets and OIDC claims are never persisted in issue configuration, PR + markers, summaries, logs, or setup Variables. + +## 15. Observability + +Every invocation shall log and summarize: + +- operation ID and kind; +- previous and resulting phase; +- immutable strategy snapshot; +- source/origin/production/development branches and relevant SHAs; +- created or reused PRs; +- selected PR and back-merge modes, including why `auto` selected them; +- tag/package/release verification status; +- pending external action, if any; and +- explicit recovery instructions when blocked. + +No invocation should emit repeated 20-second PR check polling logs outside +`legacy-wait`. + +## 16. GitHub product experience and UI/UX + +The GitHub issue is the product's release control center. Managed PRs are +focused execution surfaces for one transition. Workflow logs and Job Summaries +are technical evidence, not the primary place where a maintainer must discover +what is happening. + +### 16.1 Presentation principles + +Every issue, PR description, comment, and Job Summary MUST answer, in this +order: + +1. What is the current state? +2. What has already completed? +3. What is happening or expected next? +4. Does a person need to do anything? +5. What changed, and where can it be inspected? +6. Where are the technical details if they are needed? + +The primary view uses plain product language. Internal use-case names, enum +values, operation IDs, full SHAs, GraphQL terminology, and stack traces belong +in a collapsed technical section or Job Summary. + +Success, pending, warning, blocked, and failure states MUST use both an icon and +a textual label. Color or emoji alone never communicates status. + +### 16.2 Overall visual model + +In guided mode, the issue control center contains a compact diagram equivalent +to the following: + +```mermaid +flowchart LR + D[Development snapshot] --> R[Release prepared] + R --> P[Production PR] + P --> A[Accepted in production] + A --> N[Package and GitHub Release] + N --> B[Back-merge PR] + B --> C[Complete] +``` + +The diagram MUST be generated from fixed, localized node labels. Branch names, +titles, errors, and other untrusted values MUST NOT be interpolated into Mermaid +syntax. The adjacent textual phase table remains the accessible and canonical +status representation. + +### 16.3 Issue control center + +One durable bot-owned comment is created per orchestration operation and found +through an operation marker. In `update` mode it is edited in place. It is never +recreated because of a retry or duplicate event. + +The guided presentation follows this information hierarchy: + +```markdown + + +# 🚀 Release 3.4.0 + +> **Current status: waiting for production approval** +> +> No action is required while GitHub checks are running. + +## Progress + +- [x] Release cut from `develop` at [`abc1234`](...) +- [x] Version files, build, validation, and smoke test +- [ ] Promotion PR [#401](...) into `master` +- [ ] npm package and GitHub Release +- [ ] Reconciliation into `develop` +- [ ] Cleanup and issue completion + +## Current transition + +| From | To | State | +|---|---|---| +| `release/3.4.0` | `master` | ⏳ Checks and review | + +## What happens next + +After [PR #401](...) is merged, Copilot will tag the accepted `master` commit, +publish `@vypdev/copilot@3.4.0`, and start development reconciliation. + +## Links + +[Promotion PR](...) · [Compare changes](...) · [Workflow run](...) + +
+Technical details + +Strategy, operation ID, full SHAs, selected merge mode, and verification facts. +
+``` + +The actual renderer MAY use a table instead of a checklist in compact mode, but +MUST preserve the same semantic facts and ordering. + +The top status statement MUST distinguish at least: + +- preparing release; +- waiting for production checks; +- waiting for production review/manual merge; +- publishing artifacts; +- waiting for registry visibility; +- reconciling development; +- waiting for reconciliation review/manual merge; +- blocked before publication; +- published but reconciliation blocked; and +- completed. + +When no action is needed, say so explicitly. When action is needed, the first +visible section after status is `Action required` and contains one primary +instruction with a direct link or copyable command. + +### 16.4 Blocked and failure presentation + +A blocked presentation uses the following order: + +```markdown +# ❌ Release 3.4.0 needs attention + +> **Published package:** No +> **Production updated:** No +> **Development synchronized:** No + +## What happened + +The production PR was closed without merging, so publication was stopped. + +## Action required + +Reopen [PR #401](...) or run `/copilot retry-release` after correcting the +problem. + +## What Copilot protected + +No npm version, GitHub Release, or version tag was created. + +
+Technical details +Sanitized diagnostic and correlation data. +
+``` + +For failures after publication, the first block MUST state that the package is +already public and MUST NOT be republished. Errors use the structure `impact -> +cause -> action -> retained state`, not a raw API message. + +### 16.5 Promotion PR UX + +Promotion PR titles are deterministic and human-readable: + +```text +release(3.4.0): promote to master +hotfix(3.4.1): promote to master +``` + +Its first screen follows this compact visual contract: + +```markdown +# 🚀 Promote release 3.4.0 to `master` + +> **Purpose:** accept the prepared release in production. +> **After merge:** Copilot will publish npm and the GitHub Release from the +> accepted `master` commit. This PR does not publish before merge. + +| Origin | Prepared source | Destination | Publication | +|---|---|---|---| +| `develop@abc1234` | `release/3.4.0@def5678` | `master` | After merge | + +## Ready before review + +- ✅ Build and release validation +- ✅ Package smoke test +- ⏳ Protected-branch checks and reviews + +[Compare release](...) · [Release control center](...) +``` + +The PR body contains, in this order: + +1. a one-sentence purpose and publication consequence; +2. the route from origin snapshot to source branch to production; +3. a release-scope table with version, origin SHA, prepared SHA, target, and + launcher issue; +4. the validation already completed before PR creation; +5. required GitHub checks/reviews still owned by the target branch; +6. an explicit `After merge` list explaining tag, package, release, major tag, + and back-merge actions; +7. a compare link and launcher issue link; and +8. collapsed technical metadata and the managed marker. + +The promotion PR references the launcher issue but MUST NOT close it. Only final +orchestration completion may close the issue. + +### 16.6 Reconciliation PR UX + +Reconciliation PR titles are deterministic: + +```text +release(3.4.0): reconcile master into develop +hotfix(3.4.1): reconcile master into develop +``` + +For canonical Gitflow mode, the title and body name the release/hotfix source +instead of production. For an ephemeral branch, the title describes the +semantic source and target, not the generated sync branch name. + +Its first screen follows this compact visual contract: + +```markdown +# 🔄 Reconcile release 3.4.0 into `develop` + +> **Package status: already published.** Merging or closing this PR cannot +> publish `@vypdev/copilot@3.4.0` again. + +| Production fact | Development preserved | Completion effect | +|---|---|---| +| `v3.4.0` at `fed9012` | Commits made after the release cut | Close issue and clean branches | + +Copilot selected a sync branch because `develop` requires an up-to-date head. + +[Compare reconciliation](...) · [Release control center](...) +``` + +The PR body MUST prominently say: + +- whether the package is already public; +- that the PR cannot publish or republish anything; +- which production tag/SHA it reconciles; +- which post-cut development commits are preserved; +- why a sync branch was selected, when applicable; +- what completion and cleanup will happen after merge; and +- what happens if the PR is closed without merge. + +### 16.7 Comments and notification budget + +- Routine transitions update the issue control center rather than adding new + comments. +- No comment is produced for each check poll, check-run update, or unchanged + state observation. +- PRs do not receive duplicate status comments already visible in the issue or + GitHub merge box. +- `milestones` mode emits only the bounded events defined in section 6.3. +- A human-action request and a recovery confirmation are separate comments only + when editing the control center would be unlikely to notify the maintainer. +- Every separately published comment has its own stable idempotency marker. + +### 16.8 Links and actionable controls + +Every visible entity name links to its GitHub or registry page when a URL exists: + +- launcher issue; +- source, production, development, and sync branches; +- short commit SHA; +- compare view; +- promotion and reconciliation PRs; +- immutable version tag; +- npm package version; +- GitHub Release; +- major action tag; and +- relevant workflow run. + +Link labels describe their destination. Generic labels such as `click here` are +not allowed. Destructive actions are never represented as one-click links in a +comment; they require an authorized command or GitHub confirmation surface. + +### 16.9 Labels and lifecycle projection + +The orchestration reuses the existing lifecycle, activity, and waiting label +dimensions rather than creating a label for every internal phase: + +| Orchestration state | Durable lifecycle | Waiting label | +|---|---|---| +| Preparing/building/publishing | `state:in-progress` | none | +| Promotion or reconciliation PR open | `state:reviewing` | none | +| PR checks complete but manual merge required | `state:ready` | `state:awaiting-maintainer` | +| Blocked | `state:blocked` | `state:awaiting-maintainer` when human action is required | +| Completed | `state:verified` | none | + +The `deploy` label means publication was requested. It is replaced idempotently +by `deployed` immediately after npm and GitHub publication are verified. The +issue remains open until reconciliation completes. `deployed` therefore reports +the external deployment fact, while lifecycle labels report orchestration state. + +Managed PRs inherit the release or hotfix type label and the relevant durable +lifecycle label when doing so does not conflict with existing PR lifecycle +automation. Labels supplement, but never replace, the PR body status. + +### 16.10 Localization + +Issue-facing presentation uses `issues-locale`; PR-facing presentation uses +`pull-requests-locale`. + +All orchestration-owned headings, status sentences, instructions, table labels, +and failure guidance MUST come from a typed message catalog rather than scattered +string literals. The first implementation MUST provide complete `en-US` and +`es-ES` catalogs. An unsupported locale falls back to `en-US` and emits one +visible, non-blocking warning in technical details. + +Branch names, package names, tag names, GitHub check names, and copied provider +facts are not translated. + +### 16.11 Accessibility and responsive readability + +- Status is never encoded only through color, emoji, a checkbox, or a Mermaid + node style. +- Images and diagrams have concise alternative text or an adjacent textual + equivalent. +- Decorative GIFs are omitted from the durable control center by default; they + may appear only outside the operational status block when existing image + configuration enables them. +- Primary status and required action are never hidden inside `
`. +- Tables use at most four columns and avoid long unbroken full SHAs. +- Full SHAs and verbose provider facts remain available in technical details. +- Headings follow a logical hierarchy without skipped levels. +- The presentation remains readable in GitHub desktop/mobile layouts and light + and dark themes. +- Generated Markdown is bounded and sanitized through the existing publication + boundary. Untrusted values cannot create mentions, workflow commands, hidden + markers, injected Mermaid syntax, or misleading headings. + +### 16.12 Job Summary + +The Job Summary mirrors the issue's current facts but is optimized for operators: + +- result and phase at the top; +- transition performed by this invocation; +- verified input/output SHAs; +- API operations created/reused/skipped; +- publication evidence; +- pending external dependency; +- sanitized failure classification and retryability; and +- a final artifact/PR link table. + +It MUST distinguish `waiting externally` from `workflow failure`. A workflow that +successfully creates a pending auto-merge PR finishes green and reports the +pending PR as the next external transition. + +## 17. Clean Architecture design + +### 17.1 Layer ownership + +| Layer | Owns | Must not own | +|---|---|---| +| Domain and pure policies | phases, strategies, invariants, transition decisions, branch-role plans, configuration validation | Octokit types, Actions contexts, Markdown, npm commands, timers | +| Application use cases | orchestration, semantic ports, idempotent transition coordination, typed results | GitHub SDK calls, filesystem/process details, workflow YAML assumptions | +| Data/repository adapters | GitHub/npm data mapping, managed PR/tag/ref persistence, provider error classification | release policy or presentation decisions | +| Infrastructure/composition | Octokit/process implementations, clocks, logging, dependency wiring | business branching rules | +| Action/workflow entrypoints | input adaptation, trusted event facts, workflow outputs, composition selection | duplicated orchestration policy | +| Presentation policies | localized issue/PR/summary view models and safe Markdown rendering | provider mutations or phase transitions | + +Dependency direction remains inward. Domain and application production code +cannot import GitHub SDKs, Actions packages, concrete repositories, CLI/process +libraries, or workflow-specific payload types. + +### 17.2 Narrow contracts + +New orchestration policies MUST receive narrow immutable contracts rather than +the complete legacy `Execution` aggregate. `Execution` is adapted once at the +route/composition boundary. + +Recommended application contracts include: + +- `DeploymentOperationSnapshot` +- `DeploymentTransitionFacts` +- `DeploymentPlan` +- `DeploymentTransitionDecision` +- `ManagedPullRequestIdentity` +- `PublicationEvidence` +- `ReconciliationTargetState` +- `DeploymentPresentationModel` + +Provider DTOs are translated into these contracts at adapter boundaries. + +### 17.3 Pure state and presentation policies + +The following decisions MUST be pure and exhaustively unit-testable: + +- configuration normalization and invalid-combination detection; +- release/hotfix origin and target planning; +- next valid state transition; +- duplicate/out-of-order event handling; +- direct versus sync-branch selection; +- hotfix active-release target selection; +- cleanup eligibility; +- lifecycle/waiting label projection; +- issue control-center view-model construction; +- PR title/body view-model construction; and +- localized Markdown rendering and link selection. + +The orchestrator produces structured facts. It does not embed large Markdown +strings in generic `Result.steps`. Presentation policies render structured +deployment results at the publication boundary. + +### 17.4 Ports and failures + +Ports describe semantic capabilities such as `enableAutoMerge`, +`findManagedPullRequest`, `verifyTagTarget`, or `mergeIntoReconciliationRef`. +They do not expose raw REST/GraphQL request shapes. + +Expected operational failures use typed `ApplicationError` categories with +retryability and sanitized diagnostics. Provider exceptions and stack traces do +not cross into domain decisions or user-facing models. + +### 17.5 Persistence and concurrency + +Durable transitions use compare-before-write semantics on operation ID and +expected phase. Concurrent duplicate invocations may repeat safe reads but only +one can persist a successful phase transition. Side effects are protected by +deterministic identities and postcondition verification. + +Schema migration, future-field preservation, and legacy delegation remain +separate policies from orchestration. + +### 17.6 Executable architecture constraints + +Architecture tests MUST enforce: + +- no domain/application import of `@octokit`, `@actions`, concrete adapters, or + Node process/filesystem APIs; +- no adapter-to-use-case dependency cycle; +- presentation policies cannot call mutation ports; +- provider DTOs cannot appear in public application port signatures; +- workflow/action entrypoints depend on composition roots rather than construct + provider clients ad hoc; and +- every new production import resolves and the production dependency graph + remains acyclic. + +## 18. Testing strategy and required quantity + +Test quantity is a floor, not a substitute for requirement coverage. This +feature MUST add or materially update at least **72 distinct test cases**. Each +`it`/`test` case counts once; parameterized rows count only when they represent a +distinct input/output behavior. + +Minimum distribution: + +| Test area | Minimum cases | Required focus | +|---|---:|---| +| Domain/configuration/planning policies | 18 | enum normalization, invalid combinations, release origin, strategy plans, strict-safe selection | +| State machine/idempotent application orchestration | 18 | every transition, duplicate and stale events, cancellation/retry, partial publication | +| GitHub/repository adapters and port contracts | 12 | PR identity, auto-merge, queue, refs, tag target, reachability, provider error mapping | +| Workflow/setup contracts | 8 | phase dispatch, permissions, OIDC environment, PAT event delivery, variables, templates | +| UI/UX, rendering, localization, and sanitization | 10 | all primary states, links, diagrams/fallbacks, en-US/es-ES, bounded comments | +| Integration/replay/security scenarios | 6 | multi-run lifecycle, concurrent delivery, forged events, strict develop, legacy resume | +| **Total** | **72** | Cases cannot be double-counted between rows | + +### 18.1 Coverage requirements + +The existing global Jest thresholds remain mandatory: + +- lines: 90%; +- statements: 90%; +- functions: 88%; and +- branches: 82%. + +In addition: + +- new pure planning, state-transition, configuration, and presentation policies + require 100% branch coverage; +- new orchestration use cases require at least 95% line/statement coverage and + 90% branch/function coverage; +- changed modules cannot reduce repository-wide coverage; and +- every MUST requirement has at least one test or an explicit workflow/manual + verification entry in the traceability matrix. + +### 18.2 Required test styles + +- Table-driven unit tests cover every enum value and invalid combination. +- Deterministic fakes own clocks, UUIDs, GitHub facts, registry facts, and delays; + tests do not sleep or call live services. +- Contract tests parse active and setup workflow YAML rather than searching only + for unstructured text. +- Replay tests invoke the same event multiple times and in reordered sequences. +- Race tests simulate two invocations reading the same phase and verify one + durable transition/side effect identity. +- Adapter tests cover success, already-exists, not-found, conflict, protection, + permission, rate-limit, and transient failure mappings. +- Presentation tests assert semantic sections, ordering, stable markers, URLs, + localization, and sanitization. Snapshot tests are used only for reviewed + complete Markdown fixtures and are not the sole assertions. + +### 18.3 UX fixture matrix + +Golden Markdown fixtures MUST include at least: + +- release preparing; +- promotion pending with no human action; +- promotion ready for manual merge; +- publishing/registry pending; +- reconciliation pending; +- blocked before publication; +- blocked after publication; +- completed with cleanup; +- promotion PR body; and +- reconciliation PR body. + +At least the critical pending, blocked, and completed states are rendered in +both `en-US` and `es-ES`, in guided and compact modes. Quiet-mode suppression and +milestone comment limits receive separate policy tests. + +### 18.4 Human GitHub UX acceptance + +Before completion, the implementation PR MUST include evidence from a test +repository showing: + +- issue control center on desktop and narrow/mobile width; +- promotion and reconciliation PR bodies; +- pending, blocked, published-but-not-reconciled, and completed states; +- light and dark GitHub themes; +- Mermaid enabled and disabled/fallback presentations; and +- en-US and es-ES output. + +Reviewers verify that the current state and required next action can be +identified without opening workflow logs and that no primary instruction is +hidden below technical details. + +## 19. Documentation and discoverability + +Documentation is part of the implementation, not a follow-up. + +### 19.1 Required user documentation + +Update or add: + +- release lifecycle: origin snapshot, preparation, promotion, publication, + reconciliation, and cleanup; +- hotfix lifecycle and active-release behavior; +- a complete configuration reference with defaults, allowed values, safe limits, + invalid combinations, and examples; +- setup/upgrade instructions and the exact repository Variables/workflow changes; +- OIDC/trusted publishing prerequisites; +- protected branch, strict check, auto-merge, and merge-queue guidance; +- operator recovery for every blocked/partial-publication phase; +- migration behavior for legacy in-progress releases; +- issue/PR UI examples for guided, compact, and quiet modes; and +- a troubleshooting decision tree beginning with the visible orchestration + phase rather than raw error text. + +### 19.2 Documentation information design + +Each user-facing page follows progressive disclosure: + +1. recommended default and one diagram; +2. normal happy-path steps; +3. configuration choices and when to use them; +4. failure/recovery guidance; and +5. technical/security detail. + +Examples use real semantic branch names but clearly state that concrete names +come from configuration. Every diagram has an adjacent textual explanation. +Documentation never describe `release` as originating from production. + +### 19.3 Developer documentation + +Update the architecture guide with the state machine, dependency boundaries, +ports, persistence schema, event trust model, and presentation boundary. Add a +requirements traceability table mapping: + +```text +SDD requirement -> policy/use case/adapter -> automated tests -> user docs +``` + +The implementation PR description links this SDD and summarizes any accepted +deviation. Deviations require an explicit update to this specification. + +### 19.4 Documentation validation + +- Register every new MDX route in `docs.json`. +- `validate:documentation`, `validate:docs-page`, link/asset validation, and + workflow-contract validation MUST pass. +- Examples are copied from or checked against workflow/configuration fixtures so + documented input names and defaults cannot silently diverge. + +## 20. Required code and workflow changes + +### 20.1 Domain and configuration + +- Add provider-neutral enums and validation policies for every new input. +- Extend `SetupRepositoryConfiguration`, defaults, validation, plan generation, + and override merging. +- Add Action inputs and `INPUT_KEYS` entries. +- Read the values in GitHub and local configuration builders. +- Add repository Variables to active and setup workflow templates. +- Snapshot effective values into durable issue orchestration state. +- Increment the persisted configuration schema and provide migration from schema + v2. + +### 20.2 Application + +- Replace the current two-element merge plan with a typed phase/state policy. +- Add a narrow orchestration context rather than expanding the full `Execution` + dependency across new policies. +- Add an idempotent `advance deployment orchestration` use case that performs at + most the safe transitions enabled by verified current facts. +- Separate deployment-label facts, publication facts, reconciliation, issue + completion, and cleanup so each can be retried independently. + +### 20.3 GitHub adapters + +- Add managed PR lookup and marker parsing. +- Add native auto-merge and merge-queue capabilities. +- Add target-rule/capability inspection. +- Add tag-at-SHA and reachability verification. +- Add reconciliation ref creation and trusted server-side merge support. +- Keep provider response types outside the application layer. + +### 20.4 Workflows + +- Change the active project release workflow and setup template to prepare and + promote before tagging/publishing. +- Make the hotfix workflow follow the same production-first phase model. +- Add a dedicated managed-PR completion path that can process bot-authored PRs + without enabling generic bot recursion. +- Add `merge_group` support only when merge-queue mode is supported by all + required workflows. +- Keep project-specific npm publication in the active project workflow; the + reusable setup template retains generic deployment extension points. + +### 20.5 Presentation + +- Add typed deployment view models separate from orchestration results and + provider DTOs. +- Add a stable control-center marker and an idempotent find-or-update comment + port; never identify the dashboard from mutable prose. +- Add safe localized renderers for the issue control center, promotion PR, + reconciliation PR, milestone comment, failure guidance, and Job Summary. +- Add fixed-label Mermaid renderers with an adjacent textual phase table and a + configuration-controlled fallback. +- Build every URL from verified repository entities and sanitize all untrusted + values before Markdown publication. +- Keep generic `Result.steps` for short generic feedback only; deployment + orchestration uses its dedicated structured presentation boundary. + +### 20.6 Documentation + +- Correct the existing statement that release branches are created from main. +- Document release origin as a development snapshot and hotfix origin as a + production tag. +- Document every strategy, default, invalid combination, state, recovery path, + and strict-branch caveat. +- Update release, hotfix, deployment, setup, configuration, and troubleshooting + pages. + +### 20.7 Generated artifacts + +- Do not edit `build/` manually. +- Rebuild the Action and CLI bundles after source and workflow-contract tests + pass. +- Run `graphify update .` after implementation. + +## 21. Backward compatibility and migration + +1. Existing issues without orchestration state are migrated lazily. +2. A legacy release that is already tagged or published MUST be detected and + resumed from the first unverified phase; it MUST NOT be forced through a new + production-first publication attempt. +3. `merge-timeout` remains accepted for `legacy-wait` and is ignored with an + informational message in event-driven modes. +4. The current `deployed_action` input remains available during one compatibility + period. It delegates to the new state-aware orchestration when state exists + and uses clearly marked legacy behavior otherwise. +5. Setup dry-run shows new Variables and workflow changes before provisioning. +6. Unknown configuration fields and future schema versions remain preserved. + +## 22. Acceptance scenarios + +The implementation is not complete until deterministic automated tests, or the +explicitly identified human UX evidence where automation cannot establish +readability, cover at least the following scenarios. + +1. A release is cut from the exact current development SHA and that SHA is + persisted. +2. Development advances after the cut; those commits never appear in the + production promotion diff. +3. Release-only version, build, changelog, and fix commits reach production. +4. No tag, npm publication, GitHub Release, or major tag update occurs before the + promotion PR merges. +5. The immutable tag points at the accepted production merge SHA. +6. A required check remains pending longer than ten minutes; the original + workflow has already exited and no timeout failure occurs. +7. Two PRs would otherwise share a release SHA; sync-branch mode generates a + unique reconciliation SHA and observes only its own target validation. +8. Development is strict and has advanced; auto back-merge selects a sync branch + without merging development into production. +9. Development is loose and compatible; auto back-merge may select a direct PR. +10. A promotion PR is manually merged; the event resumes publication. +11. A promotion PR is closed unmerged; publication does not occur and the issue + reports `blocked`. +12. A reconciliation PR is closed unmerged; published artifacts remain valid, + source branches are retained, and retry is possible. +13. The same merged event is delivered twice; the second delivery is a no-op. +14. A workflow is canceled between tag creation and npm publish; retry verifies + the tag and resumes. +15. npm already contains the exact version; retry does not attempt another + publish. +16. An existing version tag points to another SHA; orchestration blocks. +17. The GitHub Release already exists for the correct tag; it is verified/reused. +18. The `deployed` label already exists; retry continues rather than failing. +19. Repository Variables change mid-release; the stored release strategy and + branch roles remain unchanged. +20. `canonical-gitflow` reconciles the prepared release head, not current + development, into development. +21. `production-lineage` reconciles the accepted production commit and preserves + its ancestry. +22. An active release exists during a hotfix; each active-release policy produces + its documented target plan. +23. Multiple active releases with `prefer-release` block for a decision. +24. `manual` publishes successfully, creates no reconciliation PR, and leaves the + issue open. +25. Every cleanup and issue-completion mode follows its configuration. +26. Bot-authored managed PR events advance the operation while ordinary + bot-authored PR automation remains suppressed. +27. A forged marker, wrong operation ID, wrong head/base, fork PR, or stale SHA + cannot advance or publish. +28. Setup validation rejects all invalid combinations from section 6.6. +29. Workflow contract tests verify OIDC permissions and npm environment scoping. +30. Architecture tests confirm that GitHub SDK types do not enter application or + domain policies. +31. One operation creates exactly one marked issue control center; retries, + duplicate events, and reordered events update it without duplicating it. +32. Guided, compact, and quiet presentation modes expose their documented + information and never suppress an actionable failure. +33. Guided mode renders a fixed-label Mermaid diagram and an equivalent textual + phase representation; untrusted content cannot alter either structure. +34. All primary pending, blocked, partially complete, and complete states render + correctly in `en-US` and `es-ES`; an unsupported locale safely falls back. +35. Promotion and reconciliation PRs use deterministic titles and ordered bodies + that explain origin, destination, publication consequence, and next action. +36. A blocked-before-publication view and a published-but-not-reconciled view + state the production, package, and development facts without ambiguity. +37. Every available issue, PR, compare, commit, tag, package, release, branch, + and workflow entity has a descriptive verified link. +38. `deploy` changes to `deployed` immediately after publication verification, + while lifecycle labels continue to show reconciliation as pending. +39. Check updates and unchanged external state create no comment spam; milestone + mode never exceeds its configured four additional lifecycle comments. +40. Manual UX evidence verifies desktop, narrow/mobile, light-theme, dark-theme, + Mermaid, and textual-fallback readability. +41. Malicious branch names, titles, errors, provider facts, or configuration + cannot inject mentions, workflow commands, headings, markers, or Mermaid. +42. Job Summary distinguishes a successful wait on an external PR from a failed + workflow and links to the pending transition. +43. Documentation routes, links, examples, defaults, and configuration names are + checked against implementation fixtures and pass all documentation validators. +44. Architecture tests enforce the dependency direction, provider-neutral ports, + acyclic production imports, and mutation-free presentation policies. +45. The feature adds or materially updates at least 72 distinct cases with the + distribution and coverage floors from section 18 and a complete traceability + matrix. +46. In a ten-second comprehension review, a maintainer unfamiliar with the run + can identify the operation kind, current phase, published/not-published fact, + branch transition, and whether human action is required without opening logs. + +## 23. Implementation sequence + +1. Add configuration enums, defaults, validation, Action inputs, setup Variables, + typed localization catalogs, and documentation stubs. +2. Define narrow domain/application/presentation contracts and add executable + dependency-boundary tests before provider implementation. +3. Add schema-v3 durable orchestration state and migration tests. +4. Add pure planning, transition, lifecycle-label, and presentation-view-model + policies with exhaustive table-driven tests. +5. Add managed PR identity and idempotent lookup. +6. Add native auto-merge, merge queue, target-rule inspection, and tag-at-SHA + ports/adapters. +7. Implement the idempotent issue control center, promotion/reconciliation PR + renderers, safe links, diagrams/fallbacks, and Job Summary. +8. Implement promotion creation without publication and convert release/hotfix + workflows to the new first phase. +9. Implement verified merged-PR continuation and production-SHA persistence. +10. Move tag/npm/GitHub Release/major-tag publication behind production merge + and project `deploy`/`deployed` labels from verified facts. +11. Implement direct and sync-branch reconciliation. +12. Implement hotfix active-release target policies. +13. Implement final cleanup, issue completion, bounded notifications, and + recovery commands. +14. Add legacy migration/delegation behavior. +15. Complete the 72-case minimum, UX fixture matrix, race/replay tests, and + requirement traceability matrix. +16. Update active workflows, setup templates, user/developer docs, workflow + contracts, and all generated bundles. +17. Capture the human GitHub UX evidence required by section 18.4. +18. Run unit, integration, architecture, workflow-contract, documentation, lint, + build, coverage, and Graphify update checks. + +## 24. Definition of done + +- Every functional and safety requirement in this specification is implemented. +- All automatable acceptance scenarios have deterministic coverage; human-only + readability scenarios have the reviewed evidence required by section 18.4. +- The active project release path publishes only from a tag on the accepted + production commit. +- No default release/hotfix path polls PR checks inside a runner. +- A canceled or duplicated run can resume without duplicate tags, packages, + releases, PRs, comments, labels, or merges. +- Release origin and strategy are visible and immutable for the duration of an + operation. +- Default setup produces the recommended production-lineage, auto PR, automatic + strict-safe back-merge behavior. +- Documentation and runtime behavior agree about release and hotfix origins. +- Clean Architecture boundaries are enforced by automated dependency and port + contract tests, not only described in documentation. +- At least 72 distinct feature cases and every section 18 coverage floor pass; + every MUST requirement is represented in the traceability matrix. +- The issue control center and both managed PR types satisfy the semantic order, + direct-link, accessibility, responsive, localization, sanitization, and + comment-budget requirements from section 16. +- Reviewed fixtures cover every primary state in guided and compact modes, plus + quiet-mode suppression and both `en-US` and `es-ES`. +- The implementation PR contains desktop/mobile and light/dark UX evidence, and + reviewers can identify status, publication fact, next transition, and required + action without inspecting workflow logs. +- User, setup, recovery, architecture, and troubleshooting documentation passes + route/link/example validation and is linked from the implementation PR. + +## 25. External references + +- [A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/) + defines release branches from development, production-first release finishing, + and the active-release exception for hotfix back-merges. +- [Automatically merging a pull request](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/automatically-merging-a-pull-request) + defines GitHub-native auto-merge behavior after required reviews and checks. +- [About protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) + defines strict versus loose required status checks. +- [Managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue) + defines temporary merge groups and the required `merge_group` workflow event. +- [REST API endpoints for check runs](https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28) + documents that check runs are listed for a Git reference rather than a unique + PR/base pair. From df9724909f7ec33f4666eacac4d19e5c2098c6c2 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 9 Sep 2026 19:36:45 +0200 Subject: [PATCH 2/9] develop: add configurable deployment orchestration --- .github/workflows/ci_check.yml | 2 + .../copilot_deployment_orchestration.yml | 48 + .github/workflows/copilot_pull_request.yml | 14 + .github/workflows/hotfix_workflow.yml | 417 ++-- .github/workflows/release_workflow.yml | 412 ++-- .github/workflows/repowise.yml | 2 + action.yml | 36 + build/api/index.js | 299 ++- build/api/src/data/model/config.d.ts | 7 +- build/api/src/data/model/execution.d.ts | 2 + .../src/data/model/execution_components.d.ts | 2 + build/api/src/data/model/single_action.d.ts | 8 +- .../src/domain/deployment_configuration.d.ts | 43 + .../api/src/domain/deployment_operation.d.ts | 70 + build/cli/index.js | 2111 +++++++++++++++- build/github_action/index.js | 2121 ++++++++++++++++- docs.json | 5 + docs/configuration.mdx | 18 + docs/development/architecture.mdx | 40 + docs/development/release-process.mdx | 13 +- docs/issues/deployment-orchestration.mdx | 257 ++ docs/issues/type/hotfix.mdx | 83 +- docs/issues/type/release.mdx | 105 +- scripts/validate-workflow-contract.cjs | 210 +- .../copilot_deployment_orchestration.yml | 48 + setup/workflows/copilot_pull_request.yml | 14 + setup/workflows/hotfix_workflow.yml | 270 +-- setup/workflows/release_workflow.yml | 270 +-- ...able-release-orchestration-traceability.md | 90 + .../deployment_configuration_builder.test.ts | 24 + .../deployment_configuration_builder.ts | 110 + src/actions/github_action_completion.ts | 26 +- src/actions/github_action_execution.ts | 9 + .../local_action_configuration_sections.ts | 20 +- src/actions/local_action_execution.ts | 5 +- .../__tests__/architecture_boundaries.test.ts | 22 + src/application/contracts/input_keys.ts | 12 + .../configuration_persistence_policy.test.ts | 4 + .../deployment_continuation_guard.test.ts | 50 + .../deployment_lifecycle_policy.test.ts | 44 + .../__tests__/deployment_plan_policy.test.ts | 186 ++ .../deployment_presentation_policy.test.ts | 214 ++ .../setup_configuration_policy.test.ts | 4 +- .../policies/deployment_continuation_guard.ts | 30 + .../policies/deployment_lifecycle_policy.ts | 34 + .../policies/deployment_plan_policy.ts | 193 ++ .../deployment_presentation_policy.ts | 349 +++ .../policies/setup_configuration_defaults.ts | 2 + .../policies/setup_configuration_plan.ts | 25 + .../setup_configuration_validation.ts | 20 + .../ports/deployment_orchestration_ports.ts | 156 ++ .../ports/repository_release_ports.ts | 1 + .../deployment_orchestration_use_case.test.ts | 577 +++++ .../actions/create_release_workflow.ts | 10 +- .../usecases/actions/create_tag_workflow.ts | 34 +- .../deployment_orchestration_use_case.ts | 686 ++++++ .../actions/publish_github_action_workflow.ts | 13 +- .../execution/setup_execution_workflow.ts | 4 + .../usecases/single_action_use_case.ts | 2 + .../usecases/single_action_workflow.ts | 6 + .../steps/issue/prepare_hotfix_branch.ts | 1 + .../steps/issue/prepare_release_branch.ts | 14 +- src/cli/setup_config_file.ts | 12 +- src/cli/setup_prompt_adapter.ts | 47 + .../model/__tests__/single_action.test.ts | 8 + src/data/model/action_types.ts | 4 + src/data/model/config.ts | 13 +- src/data/model/execution.ts | 3 + src/data/model/execution_components.ts | 2 + src/data/model/single_action.ts | 34 + .../linked_branch_repository.test.ts | 1 + .../branch/linked_branch_result_policy.ts | 3 +- .../branch/linked_branch_workflow.ts | 2 +- .../deployment_state_repository.test.ts | 74 + .../github_deployment_repository.test.ts | 179 ++ ...legacy_deployment_merge_repository.test.ts | 25 + .../deployment_continuation_repository.ts | 24 + .../deployment_presentation_repository.ts | 28 + .../deployment/deployment_state_repository.ts | 36 + .../github_deployment_repository.ts | 220 ++ .../legacy_deployment_merge_repository.ts | 37 + .../repository_release_repository.test.ts | 12 +- ...pository_release_publication_repository.ts | 35 +- .../release/repository_tag_repository.ts | 31 +- .../deployment_configuration.test.ts | 141 ++ .../__tests__/deployment_operation.test.ts | 144 ++ .../__tests__/managed_pull_request.test.ts | 41 + src/domain/deployment_configuration.ts | 157 ++ src/domain/deployment_operation.ts | 250 ++ src/domain/managed_pull_request.ts | 28 + src/domain/setup.ts | 21 + src/domain/setup_workflow_catalog.ts | 12 +- .../main_run_route_composition_root.ts | 25 + .../github/octokit_deployment_adapter.ts | 9 + .../ports/github_deployment_provider_port.ts | 37 + .../ports/github_release_provider_ports.ts | 2 +- .../configuration_payload_policy.ts | 8 + .../validate_workflow_contract.test.ts | 119 +- 98 files changed, 10654 insertions(+), 1074 deletions(-) create mode 100644 .github/workflows/copilot_deployment_orchestration.yml create mode 100644 build/api/src/domain/deployment_configuration.d.ts create mode 100644 build/api/src/domain/deployment_operation.d.ts create mode 100644 docs/issues/deployment-orchestration.mdx create mode 100644 setup/workflows/copilot_deployment_orchestration.yml create mode 100644 specs/configurable-release-orchestration-traceability.md create mode 100644 src/actions/__tests__/deployment_configuration_builder.test.ts create mode 100644 src/actions/deployment_configuration_builder.ts create mode 100644 src/application/policies/__tests__/deployment_continuation_guard.test.ts create mode 100644 src/application/policies/__tests__/deployment_lifecycle_policy.test.ts create mode 100644 src/application/policies/__tests__/deployment_plan_policy.test.ts create mode 100644 src/application/policies/__tests__/deployment_presentation_policy.test.ts create mode 100644 src/application/policies/deployment_continuation_guard.ts create mode 100644 src/application/policies/deployment_lifecycle_policy.ts create mode 100644 src/application/policies/deployment_plan_policy.ts create mode 100644 src/application/policies/deployment_presentation_policy.ts create mode 100644 src/application/ports/deployment_orchestration_ports.ts create mode 100644 src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts create mode 100644 src/application/usecases/actions/deployment_orchestration_use_case.ts create mode 100644 src/data/repository/deployment/__tests__/deployment_state_repository.test.ts create mode 100644 src/data/repository/deployment/__tests__/github_deployment_repository.test.ts create mode 100644 src/data/repository/deployment/__tests__/legacy_deployment_merge_repository.test.ts create mode 100644 src/data/repository/deployment/deployment_continuation_repository.ts create mode 100644 src/data/repository/deployment/deployment_presentation_repository.ts create mode 100644 src/data/repository/deployment/deployment_state_repository.ts create mode 100644 src/data/repository/deployment/github_deployment_repository.ts create mode 100644 src/data/repository/deployment/legacy_deployment_merge_repository.ts create mode 100644 src/domain/__tests__/deployment_configuration.test.ts create mode 100644 src/domain/__tests__/deployment_operation.test.ts create mode 100644 src/domain/__tests__/managed_pull_request.test.ts create mode 100644 src/domain/deployment_configuration.ts create mode 100644 src/domain/deployment_operation.ts create mode 100644 src/domain/managed_pull_request.ts create mode 100644 src/infrastructure/github/octokit_deployment_adapter.ts create mode 100644 src/infrastructure/github/ports/github_deployment_provider_port.ts diff --git a/.github/workflows/ci_check.yml b/.github/workflows/ci_check.yml index 6cf32f923..abb5aca84 100644 --- a/.github/workflows/ci_check.yml +++ b/.github/workflows/ci_check.yml @@ -5,6 +5,8 @@ on: branches: [master] pull_request: types: [opened, synchronize] + merge_group: + types: [checks_requested] workflow_dispatch: concurrency: diff --git a/.github/workflows/copilot_deployment_orchestration.yml b/.github/workflows/copilot_deployment_orchestration.yml new file mode 100644 index 000000000..5ba0d3244 --- /dev/null +++ b/.github/workflows/copilot_deployment_orchestration.yml @@ -0,0 +1,48 @@ +name: Copilot - Deployment Orchestration + +on: + pull_request: + types: [closed] + +concurrency: + group: copilot-deployment-${{ github.event.pull_request.base.repo.full_name }}-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + continue: + name: Verify managed PR and continue + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && contains(github.event.pull_request.body || '', '/); + if (!match) core.setFailed('The managed deployment marker is malformed.'); + else { + core.setOutput('operation-id', match[1]); + core.setOutput('issue', match[3]); + } + - name: Checkout trusted base branch + uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 + - name: Advance durable deployment operation + uses: ./ + with: + single-action: continue_deployment_action + single-action-issue: ${{ steps.identity.outputs.issue }} + single-action-operation-id: ${{ steps.identity.outputs.operation-id }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/copilot_pull_request.yml b/.github/workflows/copilot_pull_request.yml index 97eb2112b..f38080d80 100644 --- a/.github/workflows/copilot_pull_request.yml +++ b/.github/workflows/copilot_pull_request.yml @@ -5,8 +5,22 @@ on: types: [opened, reopened, edited, closed, synchronize] pull_request_review: types: [submitted, edited, dismissed] + merge_group: + types: [checks_requested] jobs: + copilot-merge-group: + if: ${{ github.event_name == 'merge_group' }} + name: Copilot - Pull Request + runs-on: [self-hosted, codex] + timeout-minutes: 10 + permissions: + checks: write + contents: read + steps: + - name: Confirm merge-group compatibility + run: echo "Copilot PR analysis already ran on each constituent pull request." + copilot-pull-requests: if: ${{ (vars.COPILOT_BOT_LOGIN == '' || github.actor != vars.COPILOT_BOT_LOGIN) && github.event.pull_request.head.repo.full_name == github.repository }} name: Copilot - Pull Request diff --git a/.github/workflows/hotfix_workflow.yml b/.github/workflows/hotfix_workflow.yml index 77c3317dd..b1e66d705 100644 --- a/.github/workflows/hotfix_workflow.yml +++ b/.github/workflows/hotfix_workflow.yml @@ -3,18 +3,28 @@ name: Task - Hotfix on: workflow_dispatch: inputs: - version: - description: 'Hotfix version' + mode: + description: 'Internal orchestration phase' required: true - default: '1.0.0' + default: prepare + type: choice + options: [prepare, publish] + operation-id: + description: 'Internal durable operation identifier' + required: false + default: '' + version: + description: 'Hotfix version (MAJOR.MINOR.PATCH)' + required: false + default: '' title: - description: 'Title' - required: true - default: 'New Version' + description: 'Hotfix title' + required: false + default: '' changelog: - description: 'Changelog' - required: true - default: '- Several improvements' + description: 'Hotfix changelog' + required: false + default: '' issue: description: 'Launcher issue' required: true @@ -40,6 +50,7 @@ jobs: prepare-version-files: name: Prepare files for hotfix + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] needs: queue-gate timeout-minutes: 15 @@ -49,235 +60,276 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 + - uses: actions/setup-node@v7 with: node-version: '24.x' - - - name: Set up pnpm - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@v5 with: version: 10.12.4 standalone: false - - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Validate inputs + - name: Validate preparation inputs env: - VERSION: ${{ github.event.inputs.version }} - ISSUE: ${{ github.event.inputs.issue }} - TITLE: ${{ github.event.inputs.title }} - CHANGELOG: ${{ github.event.inputs.changelog }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + TITLE: ${{ inputs.title }} + CHANGELOG: ${{ inputs.changelog }} run: | err=0 - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Version must be in semver format (e.g. 1.0.0)." - err=1 - fi - if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then - echo "::error::Issue must be a number (e.g. 123 or -1)." - err=1 - fi - if [[ ${#TITLE} -gt 1000 ]]; then - echo "::error::Title must be at most 1000 characters." - err=1 - fi - if [[ ${#CHANGELOG} -gt 50000 ]]; then - echo "::error::Changelog must be at most 50000 characters." - err=1 - fi + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Version must use MAJOR.MINOR.PATCH."; err=1; } + [[ "$ISSUE" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Issue must be a positive number."; err=1; } + [[ -n "$TITLE" && ${#TITLE} -le 1000 ]] || { echo "::error::Title must contain 1-1000 characters."; err=1; } + [[ -n "$CHANGELOG" && ${#CHANGELOG} -le 50000 ]] || { echo "::error::Changelog must contain 1-50000 characters."; err=1; } [[ $err -eq 0 ]] || exit 1 - - name: Update version uses: actions/github-script@v9 + env: + VERSION: ${{ inputs.version }} with: script: | const fs = require('fs'); - const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); - packageJson.version = '${{ github.event.inputs.version }}'; - fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2)); - - - name: Commit updated package.json and dist directory + const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + packageJson.version = process.env.VERSION; + fs.writeFileSync('./package.json', `${JSON.stringify(packageJson, null, 2)}\n`); + - name: Commit version files uses: EndBug/add-and-commit@v9 with: - add: './build/ ./package.json' + add: './package.json' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files and bumped version to ${{ github.event.inputs.version }} (hotfix)' + message: 'gh-action: prepare hotfix ${{ inputs.version }}' prepare-compiled-files: - name: Update compiled files + name: Build and validate prepared hotfix + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] - timeout-minutes: 20 + needs: prepare-version-files + timeout-minutes: 30 permissions: contents: write - needs: prepare-version-files steps: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up pnpm - # Avoid the standalone @pnpm/exe binary, which has no working Intel - # macOS build for this pnpm line. - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@v5 with: version: 10.12.4 standalone: false - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 + - uses: actions/setup-node@v7 with: node-version: '24.x' cache: pnpm - - - name: Pull latest changes - run: | - git config user.email "efraespada@gmail.com" - git config user.name "Efra Espada" - git pull --no-ff --no-edit - - - name: Install Dependencies + - name: Pull prepared version commit + run: git pull --no-ff --no-edit + - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Build Files - run: pnpm run build - - - name: Force add build directory - run: git add -f ./build/ - - - name: Commit updated dist directory + - name: Build and validate + run: | + pnpm run build + pnpm run validate:npm-package + pnpm run smoke:npm-package + - name: Commit compiled action uses: EndBug/add-and-commit@v9 with: + add: './build/' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files' + message: 'gh-action: compile hotfix ${{ inputs.version }}' - tag: - name: Publish version + promote: + name: Open production promotion + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] - timeout-minutes: 120 - needs: [ prepare-compiled-files ] + needs: prepare-compiled-files + timeout-minutes: 10 permissions: contents: read + issues: write + pull-requests: write + actions: write steps: - - name: Checkout Repository - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: persist-credentials: false - ref: ${{ github.ref_name }} - - - name: Copilot - Create Tag + - name: Create or resume promotion PR uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'create_tag' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: prepare_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-version: ${{ inputs.version }} + single-action-title: ${{ inputs.title }} + single-action-changelog: ${{ inputs.changelog }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-workflow: ${{ vars.RELEASE_WORKFLOW || 'release_workflow.yml' }} + hotfix-workflow: ${{ vars.HOTFIX_WORKFLOW || 'hotfix_workflow.yml' }} + release-reconciliation-strategy: ${{ vars.RELEASE_RECONCILIATION_STRATEGY || 'production-lineage' }} + hotfix-reconciliation-strategy: ${{ vars.HOTFIX_RECONCILIATION_STRATEGY || 'production-lineage' }} + reconciliation-pr-mode: ${{ vars.RECONCILIATION_PR_MODE || 'auto' }} + reconciliation-backmerge-mode: ${{ vars.RECONCILIATION_BACKMERGE_MODE || 'auto' }} + hotfix-active-release-policy: ${{ vars.HOTFIX_ACTIVE_RELEASE_POLICY || 'prefer-release' }} + reconciliation-tree: ${{ vars.RECONCILIATION_TREE || 'sync' }} + reconciliation-cleanup: ${{ vars.RECONCILIATION_CLEANUP || 'all' }} + reconciliation-issue-completion: ${{ vars.RECONCILIATION_ISSUE_COMPLETION || 'close' }} + orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} + orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} + orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} token: ${{ secrets.PAT }} - - - name: Copilot - Create Release + + tag: + name: Create or verify production tag + if: ${{ inputs.mode == 'publish' }} + runs-on: [self-hosted, codex] + needs: queue-gate + timeout-minutes: 10 + permissions: + contents: write + issues: write + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ vars.MAIN_BRANCH || 'master' }} + - name: Tag accepted production SHA uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'create_release' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - single-action-title: '${{ github.event.inputs.title }}' - single-action-changelog: '${{ github.event.inputs.changelog }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_tag + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - name: Copilot - Publish Github Action Version + publish-npm: + name: Publish @vypdev/copilot to npm + if: ${{ inputs.mode == 'publish' }} + runs-on: ubuntu-latest + environment: npm + timeout-minutes: 20 + needs: tag + permissions: + contents: read + id-token: write + steps: + - name: Checkout immutable release tag + uses: actions/checkout@v5 + with: + persist-credentials: false + ref: v${{ inputs.version }} + fetch-depth: 1 + - uses: actions/setup-node@v7 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false + - uses: pnpm/action-setup@v5 + with: + version: 10.12.4 + standalone: false + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate immutable package + env: + RELEASE_VERSION: ${{ inputs.version }} + run: | + test "$RELEASE_VERSION" = "$(node -p "require('./package.json').version")" + test "$(node -p "require('./package.json').name")" = "@vypdev/copilot" + pnpm run validate:npm-package + pnpm run smoke:npm-package + - name: Detect an existing publication + id: registry + env: + PACKAGE_NAME: '@vypdev/copilot' + RELEASE_VERSION: ${{ inputs.version }} + run: | + if [ "$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online 2>/dev/null || true)" = "$RELEASE_VERSION" ]; then + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + - name: Publish with npm trusted publishing + if: ${{ steps.registry.outputs.publish == 'true' }} + run: npm publish --access public + - name: Wait for npm registry visibility + env: + PACKAGE_NAME: '@vypdev/copilot' + RELEASE_VERSION: ${{ inputs.version }} + POLL_INTERVAL: ${{ vars.NPM_VISIBILITY_POLL_INTERVAL_SECONDS || '20' }} + POLL_TIMEOUT: ${{ vars.NPM_VISIBILITY_TIMEOUT_SECONDS || '120' }} + run: | + [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] && [ "$POLL_INTERVAL" -ge 10 ] && [ "$POLL_INTERVAL" -le 60 ] || { echo "::error::NPM visibility interval must be 10-60 seconds."; exit 1; } + [[ "$POLL_TIMEOUT" =~ ^[0-9]+$ ]] && [ "$POLL_TIMEOUT" -ge 60 ] && [ "$POLL_TIMEOUT" -le 900 ] || { echo "::error::NPM visibility timeout must be 60-900 seconds."; exit 1; } + attempts=0 + max_attempts=$(( (POLL_TIMEOUT + POLL_INTERVAL - 1) / POLL_INTERVAL )) + until [ "$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online 2>/dev/null || true)" = "$RELEASE_VERSION" ]; do + attempts=$((attempts + 1)) + [ "$attempts" -lt "$max_attempts" ] || { echo "::error::$PACKAGE_NAME@$RELEASE_VERSION was not visible after $POLL_TIMEOUT seconds."; exit 1; } + echo "$PACKAGE_NAME@$RELEASE_VERSION is not visible yet; retrying in $POLL_INTERVAL seconds." + sleep "$POLL_INTERVAL" + done + - name: Verify published package identity + env: + PACKAGE_NAME: '@vypdev/copilot' + RELEASE_VERSION: ${{ inputs.version }} + run: | + registry_version="$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online)" + registry_git_head="$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" gitHead --prefer-online)" + local_git_head="$(git rev-parse HEAD)" + [ "$registry_version" = "$RELEASE_VERSION" ] || { echo "::error::Registry version identity does not match $RELEASE_VERSION."; exit 1; } + [ "$registry_git_head" = "$local_git_head" ] || { echo "::error::Registry gitHead $registry_git_head does not match accepted production SHA $local_git_head."; exit 1; } + + finalize-hotfix: + name: Finalize published hotfix + if: ${{ inputs.mode == 'publish' }} + runs-on: [self-hosted, codex] + timeout-minutes: 15 + needs: publish-npm + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + ref: v${{ inputs.version }} + - name: Create or verify GitHub Release uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'publish_github_action' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_release + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - - name: Copilot - Deploy success notification + - name: Update major Action reference + uses: ./ + with: + single-action: publish_github_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + token: ${{ secrets.PAT }} + - name: Start development reconciliation uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: published_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} report-failure: - name: Report deployment failure + name: Report orchestration failure runs-on: [self-hosted, codex] timeout-minutes: 5 - needs: [ queue-gate, prepare-version-files, prepare-compiled-files, tag ] - if: ${{ failure() && github.event.inputs.issue != '-1' }} + needs: [queue-gate, prepare-version-files, prepare-compiled-files, promote, tag, publish-npm, finalize-hotfix] + if: ${{ failure() && inputs.issue != '-1' }} permissions: contents: read issues: write @@ -285,15 +337,26 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false - - name: Report failure on launcher issue + - name: Persist publication failure + if: ${{ inputs.mode == 'publish' }} + uses: ./ + with: + single-action: failed_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + single-action-message: Publication failed. Review ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} before retrying. + token: ${{ secrets.PAT }} + - name: Report preparation failure + if: ${{ inputs.mode == 'prepare' }} uses: ./ with: single-action: publish_issue_comment - single-action-issue: '${{ github.event.inputs.issue }}' + single-action-issue: ${{ inputs.issue }} single-action-message: | - ## ❌ Hotfix deployment failed + ## ❌ Hotfix orchestration needs attention - The hotfix workflow did not complete successfully. + Phase `${{ inputs.mode }}` failed. Published artifacts, if any, remain unchanged and the operation can be retried from durable issue state. [Review the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - token: ${{ github.token }} + token: ${{ secrets.PAT }} diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml index 79c47d09d..0170d5541 100644 --- a/.github/workflows/release_workflow.yml +++ b/.github/workflows/release_workflow.yml @@ -3,18 +3,28 @@ name: Task - Release on: workflow_dispatch: inputs: - version: - description: 'Release version' + mode: + description: 'Internal orchestration phase' required: true - default: '1.0.0' + default: prepare + type: choice + options: [prepare, publish] + operation-id: + description: 'Internal durable operation identifier' + required: false + default: '' + version: + description: 'Release version (MAJOR.MINOR.PATCH)' + required: false + default: '' title: - description: 'Title' - required: true - default: 'New Version' + description: 'Release title' + required: false + default: '' changelog: - description: 'Changelog' - required: true - default: '- Several improvements' + description: 'Release changelog' + required: false + default: '' issue: description: 'Launcher issue' required: true @@ -40,6 +50,7 @@ jobs: prepare-version-files: name: Prepare files for release + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] needs: queue-gate timeout-minutes: 15 @@ -49,314 +60,276 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 + - uses: actions/setup-node@v7 with: node-version: '24.x' - - - name: Set up pnpm - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@v5 with: version: 10.12.4 standalone: false - - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Validate inputs + - name: Validate preparation inputs env: - VERSION: ${{ github.event.inputs.version }} - ISSUE: ${{ github.event.inputs.issue }} - TITLE: ${{ github.event.inputs.title }} - CHANGELOG: ${{ github.event.inputs.changelog }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + TITLE: ${{ inputs.title }} + CHANGELOG: ${{ inputs.changelog }} run: | err=0 - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Version must be in semver format (e.g. 1.0.0)." - err=1 - fi - if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then - echo "::error::Issue must be a number (e.g. 123 or -1)." - err=1 - fi - if [[ ${#TITLE} -gt 1000 ]]; then - echo "::error::Title must be at most 1000 characters." - err=1 - fi - if [[ ${#CHANGELOG} -gt 50000 ]]; then - echo "::error::Changelog must be at most 50000 characters." - err=1 - fi + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Version must use MAJOR.MINOR.PATCH."; err=1; } + [[ "$ISSUE" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Issue must be a positive number."; err=1; } + [[ -n "$TITLE" && ${#TITLE} -le 1000 ]] || { echo "::error::Title must contain 1-1000 characters."; err=1; } + [[ -n "$CHANGELOG" && ${#CHANGELOG} -le 50000 ]] || { echo "::error::Changelog must contain 1-50000 characters."; err=1; } [[ $err -eq 0 ]] || exit 1 - - name: Update version uses: actions/github-script@v9 + env: + VERSION: ${{ inputs.version }} with: script: | const fs = require('fs'); - const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); - packageJson.version = '${{ github.event.inputs.version }}'; - fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2)); - - - name: Commit updated package.json and dist directory + const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + packageJson.version = process.env.VERSION; + fs.writeFileSync('./package.json', `${JSON.stringify(packageJson, null, 2)}\n`); + - name: Commit version files uses: EndBug/add-and-commit@v9 with: - add: './build/ ./package.json' + add: './package.json' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files and bumped version to ${{ github.event.inputs.version }}' + message: 'gh-action: prepare release ${{ inputs.version }}' prepare-compiled-files: - name: Update compiled files + name: Build and validate prepared release + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] - timeout-minutes: 20 + needs: prepare-version-files + timeout-minutes: 30 permissions: contents: write - needs: prepare-version-files steps: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up pnpm - # Avoid the standalone @pnpm/exe binary, which has no working Intel - # macOS build for this pnpm line. - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@v5 with: version: 10.12.4 standalone: false - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 + - uses: actions/setup-node@v7 with: node-version: '24.x' cache: pnpm - - - name: Pull latest changes - run: | - git config user.email "efraespada@gmail.com" - git config user.name "Efra Espada" - git pull --no-ff --no-edit - - - name: Install Dependencies + - name: Pull prepared version commit + run: git pull --no-ff --no-edit + - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Build Files - run: pnpm run build - - - name: Force add build directory - run: git add -f ./build/ - - - name: Commit updated dist directory + - name: Build and validate + run: | + pnpm run build + pnpm run validate:npm-package + pnpm run smoke:npm-package + - name: Commit compiled action uses: EndBug/add-and-commit@v9 with: + add: './build/' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files' + message: 'gh-action: compile release ${{ inputs.version }}' - tag: - name: Create version tag + promote: + name: Open production promotion + if: ${{ inputs.mode == 'prepare' }} runs-on: [self-hosted, codex] - timeout-minutes: 120 - needs: [ prepare-compiled-files ] + needs: prepare-compiled-files + timeout-minutes: 10 permissions: contents: read + issues: write + pull-requests: write + actions: write steps: - - name: Checkout Repository - uses: actions/checkout@v5 + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Create or resume promotion PR + uses: ./ + with: + single-action: prepare_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-version: ${{ inputs.version }} + single-action-title: ${{ inputs.title }} + single-action-changelog: ${{ inputs.changelog }} + main-branch: ${{ vars.MAIN_BRANCH || 'master' }} + development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} + release-tree: ${{ vars.RELEASE_TREE || 'release' }} + hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} + release-workflow: ${{ vars.RELEASE_WORKFLOW || 'release_workflow.yml' }} + hotfix-workflow: ${{ vars.HOTFIX_WORKFLOW || 'hotfix_workflow.yml' }} + release-reconciliation-strategy: ${{ vars.RELEASE_RECONCILIATION_STRATEGY || 'production-lineage' }} + hotfix-reconciliation-strategy: ${{ vars.HOTFIX_RECONCILIATION_STRATEGY || 'production-lineage' }} + reconciliation-pr-mode: ${{ vars.RECONCILIATION_PR_MODE || 'auto' }} + reconciliation-backmerge-mode: ${{ vars.RECONCILIATION_BACKMERGE_MODE || 'auto' }} + hotfix-active-release-policy: ${{ vars.HOTFIX_ACTIVE_RELEASE_POLICY || 'prefer-release' }} + reconciliation-tree: ${{ vars.RECONCILIATION_TREE || 'sync' }} + reconciliation-cleanup: ${{ vars.RECONCILIATION_CLEANUP || 'all' }} + reconciliation-issue-completion: ${{ vars.RECONCILIATION_ISSUE_COMPLETION || 'close' }} + orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} + orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} + orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} + token: ${{ secrets.PAT }} + + tag: + name: Create or verify production tag + if: ${{ inputs.mode == 'publish' }} + runs-on: [self-hosted, codex] + needs: queue-gate + timeout-minutes: 10 + permissions: + contents: write + issues: write + steps: + - uses: actions/checkout@v5 with: persist-credentials: false - ref: ${{ github.ref_name }} - - - name: Copilot - Create Tag + ref: ${{ vars.MAIN_BRANCH || 'master' }} + - name: Tag accepted production SHA uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'create_tag' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_tag + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} publish-npm: name: Publish @vypdev/copilot to npm + if: ${{ inputs.mode == 'publish' }} runs-on: ubuntu-latest environment: npm timeout-minutes: 20 - needs: [ tag ] + needs: tag permissions: contents: read id-token: write steps: - - name: Checkout release tag + - name: Checkout immutable release tag uses: actions/checkout@v5 with: persist-credentials: false - ref: v${{ github.event.inputs.version }} + ref: v${{ inputs.version }} fetch-depth: 1 - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 + - uses: actions/setup-node@v7 with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' package-manager-cache: false - - - name: Set up pnpm - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@v5 with: version: 10.12.4 standalone: false - - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Validate release identity and package contents + - name: Validate immutable package env: - RELEASE_TAG: v${{ github.event.inputs.version }} + RELEASE_VERSION: ${{ inputs.version }} run: | - test "$RELEASE_TAG" = "v$(node -p "require('./package.json').version")" + test "$RELEASE_VERSION" = "$(node -p "require('./package.json').version")" test "$(node -p "require('./package.json').name")" = "@vypdev/copilot" pnpm run validate:npm-package pnpm run smoke:npm-package - - - name: Publish @vypdev/copilot + - name: Detect an existing publication + id: registry + env: + PACKAGE_NAME: '@vypdev/copilot' + RELEASE_VERSION: ${{ inputs.version }} + run: | + if [ "$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online 2>/dev/null || true)" = "$RELEASE_VERSION" ]; then + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + - name: Publish with npm trusted publishing + if: ${{ steps.registry.outputs.publish == 'true' }} run: npm publish --access public - - - name: Wait for npm registry availability + - name: Wait for npm registry visibility env: PACKAGE_NAME: '@vypdev/copilot' - RELEASE_VERSION: ${{ github.event.inputs.version }} + RELEASE_VERSION: ${{ inputs.version }} + POLL_INTERVAL: ${{ vars.NPM_VISIBILITY_POLL_INTERVAL_SECONDS || '20' }} + POLL_TIMEOUT: ${{ vars.NPM_VISIBILITY_TIMEOUT_SECONDS || '120' }} run: | + [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] && [ "$POLL_INTERVAL" -ge 10 ] && [ "$POLL_INTERVAL" -le 60 ] || { echo "::error::NPM visibility interval must be 10-60 seconds."; exit 1; } + [[ "$POLL_TIMEOUT" =~ ^[0-9]+$ ]] && [ "$POLL_TIMEOUT" -ge 60 ] && [ "$POLL_TIMEOUT" -le 900 ] || { echo "::error::NPM visibility timeout must be 60-900 seconds."; exit 1; } attempts=0 - max_attempts=7 - until published_version="$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online 2>/dev/null)" \ - && [ "$published_version" = "$RELEASE_VERSION" ]; do + max_attempts=$(( (POLL_TIMEOUT + POLL_INTERVAL - 1) / POLL_INTERVAL )) + until [ "$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online 2>/dev/null || true)" = "$RELEASE_VERSION" ]; do attempts=$((attempts + 1)) - if [ "$attempts" -ge "$max_attempts" ]; then - echo "::error::$PACKAGE_NAME@$RELEASE_VERSION was not visible in the npm registry after 120 seconds." - exit 1 - fi - echo "$PACKAGE_NAME@$RELEASE_VERSION is not visible in the npm registry yet; retrying in 20 seconds." - sleep 20 + [ "$attempts" -lt "$max_attempts" ] || { echo "::error::$PACKAGE_NAME@$RELEASE_VERSION was not visible after $POLL_TIMEOUT seconds."; exit 1; } + echo "$PACKAGE_NAME@$RELEASE_VERSION is not visible yet; retrying in $POLL_INTERVAL seconds." + sleep "$POLL_INTERVAL" done - echo "$PACKAGE_NAME@$RELEASE_VERSION is available in the npm registry." + - name: Verify published package identity + env: + PACKAGE_NAME: '@vypdev/copilot' + RELEASE_VERSION: ${{ inputs.version }} + run: | + registry_version="$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --prefer-online)" + registry_git_head="$(npm view "$PACKAGE_NAME@$RELEASE_VERSION" gitHead --prefer-online)" + local_git_head="$(git rev-parse HEAD)" + [ "$registry_version" = "$RELEASE_VERSION" ] || { echo "::error::Registry version identity does not match $RELEASE_VERSION."; exit 1; } + [ "$registry_git_head" = "$local_git_head" ] || { echo "::error::Registry gitHead $registry_git_head does not match accepted production SHA $local_git_head."; exit 1; } finalize-release: - name: Finalize GitHub release + name: Finalize published release + if: ${{ inputs.mode == 'publish' }} runs-on: [self-hosted, codex] - timeout-minutes: 120 - needs: [ publish-npm ] + timeout-minutes: 15 + needs: publish-npm permissions: - contents: read + contents: write + issues: write + pull-requests: write steps: - - name: Checkout release tag - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: persist-credentials: false - ref: v${{ github.event.inputs.version }} - fetch-depth: 1 - - - name: Copilot - Create Release + ref: v${{ inputs.version }} + - name: Create or verify GitHub Release uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'create_release' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - single-action-title: '${{ github.event.inputs.title }}' - single-action-changelog: '${{ github.event.inputs.changelog }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_release + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - - name: Copilot - Publish Github Action Version + - name: Update major Action reference uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'publish_github_action' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: publish_github_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - - name: Copilot - Deploy success notification + - name: Start development reconciliation uses: ./ - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: published_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} report-failure: - name: Report deployment failure + name: Report orchestration failure runs-on: [self-hosted, codex] timeout-minutes: 5 - needs: [ queue-gate, prepare-version-files, prepare-compiled-files, tag, publish-npm, finalize-release ] - if: ${{ failure() && github.event.inputs.issue != '-1' }} + needs: [queue-gate, prepare-version-files, prepare-compiled-files, promote, tag, publish-npm, finalize-release] + if: ${{ failure() && inputs.issue != '-1' }} permissions: contents: read issues: write @@ -364,15 +337,26 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false - - name: Report failure on launcher issue + - name: Persist publication failure + if: ${{ inputs.mode == 'publish' }} + uses: ./ + with: + single-action: failed_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + single-action-message: Publication failed. Review ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} before retrying. + token: ${{ secrets.PAT }} + - name: Report preparation failure + if: ${{ inputs.mode == 'prepare' }} uses: ./ with: single-action: publish_issue_comment - single-action-issue: '${{ github.event.inputs.issue }}' + single-action-issue: ${{ inputs.issue }} single-action-message: | - ## ❌ Release deployment failed + ## ❌ Release orchestration needs attention - The release workflow did not complete successfully. + Phase `${{ inputs.mode }}` failed. Published artifacts, if any, remain unchanged and the operation can be retried from durable issue state. [Review the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) token: ${{ secrets.PAT }} diff --git a/.github/workflows/repowise.yml b/.github/workflows/repowise.yml index 45b3193a7..5cf497767 100644 --- a/.github/workflows/repowise.yml +++ b/.github/workflows/repowise.yml @@ -5,6 +5,8 @@ on: branches: [master] pull_request: types: [opened, synchronize, reopened] + merge_group: + types: [checks_requested] workflow_dispatch: concurrency: diff --git a/action.yml b/action.yml index 3503cf90d..7dfc85303 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,9 @@ inputs: single-action-message: description: "Markdown message for the publish_issue_comment single action." default: "" + single-action-operation-id: + description: "Expected durable deployment operation ID for trusted continuation actions." + default: "" single-action-comment-id: description: "Optional issue comment ID to replace or append to." default: "" @@ -420,6 +423,39 @@ inputs: hotfix-workflow: description: "Hotfix workflow for running hotfix deploys." default: "hotfix_workflow.yml" + release-reconciliation-strategy: + description: "Release reconciliation source after production publication: production-lineage, canonical-gitflow, or manual." + default: "production-lineage" + hotfix-reconciliation-strategy: + description: "Hotfix reconciliation source after production publication: production-lineage, canonical-gitflow, or manual." + default: "production-lineage" + reconciliation-pr-mode: + description: "Managed PR behavior: auto, auto-merge, merge-queue, create-only, or legacy-wait." + default: "auto" + reconciliation-backmerge-mode: + description: "Back-merge branch shape: auto, direct, or sync-branch." + default: "auto" + hotfix-active-release-policy: + description: "Hotfix reconciliation target policy: prefer-release, development, or both." + default: "prefer-release" + reconciliation-tree: + description: "Safe prefix for ephemeral reconciliation branches." + default: "sync" + reconciliation-cleanup: + description: "Branch cleanup after reconciliation: all, source-only, sync-only, or none." + default: "all" + reconciliation-issue-completion: + description: "Launcher issue behavior after completion: close or keep-open." + default: "close" + orchestration-presentation-mode: + description: "Release control-center detail: guided, compact, or quiet." + default: "guided" + orchestration-diagrams: + description: "Render fixed-label Mermaid diagrams with a textual equivalent in guided mode." + default: "true" + orchestration-comment-mode: + description: "Issue notification policy: update or milestones." + default: "update" desired-assignees-count: description: "The number of assignees desired for the issue or pull request. If set to 0, no assignees will be added. If the number exceeds the available members, all members will be assigned. Max 10." default: "1" diff --git a/build/api/index.js b/build/api/index.js index 4af5b2620..cf62f32ed 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -2726,8 +2726,9 @@ exports.migrateConfigurationPayload = migrateConfigurationPayload; const branch_configuration_1 = __nccwpck_require__(1934); const recommendation_state_1 = __nccwpck_require__(8514); const model_input_1 = __nccwpck_require__(4637); +const deployment_operation_1 = __nccwpck_require__(2730); /** Version of the durable configuration contract stored in issue/PR content. */ -exports.CONFIG_SCHEMA_VERSION = 2; +exports.CONFIG_SCHEMA_VERSION = 3; /** * Normalizes persisted configuration without silently losing fields from a * newer installation. Unknown keys are deliberately retained so a downgrade @@ -2771,6 +2772,9 @@ class Config { this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); this.releaseBranch = (0, model_input_1.readOptionalString)(input, 'releaseBranch'); + this.releaseOriginBranch = (0, model_input_1.readOptionalString)(input, 'releaseOriginBranch'); + this.releaseOriginSha = (0, model_input_1.readOptionalString)(input, 'releaseOriginSha'); + this.hotfixOriginSha = (0, model_input_1.readOptionalString)(input, 'hotfixOriginSha'); this.parentBranch = (0, model_input_1.readOptionalString)(input, 'parentBranch'); this.workingBranch = (0, model_input_1.readOptionalString)(input, 'workingBranch'); if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { @@ -2779,6 +2783,9 @@ class Config { if ((0, recommendation_state_1.isRecommendationState)(input['recommendationState'])) { this.recommendationState = input['recommendationState']; } + if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + this.deploymentOrchestration = input['deploymentOrchestration']; + } } } exports.Config = Config; @@ -2797,6 +2804,7 @@ const commit_1 = __nccwpck_require__(7525); const config_1 = __nccwpck_require__(450); const github_user_policy_1 = __nccwpck_require__(4403); const issue_inactivity_1 = __nccwpck_require__(8572); +const deployment_configuration_1 = __nccwpck_require__(2495); class Execution { get eventName() { return this.inputs?.eventName ?? ''; @@ -2887,6 +2895,7 @@ class Execution { this.hotfix = components.hotfix; this.project = components.projects; this.workflows = components.workflows; + this.deployment = components.deployment ?? { ...deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION }; this.tokenUser = components.tokenUser; this.inactivityThresholdHours = components.inactivityThresholdHours ?? issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new config_1.Config({}); @@ -3246,6 +3255,294 @@ function resolveBugbotReviewEffort(configured, complexity) { } +/***/ }), + +/***/ 2495: +/***/ ((__unused_webpack_module, exports) => { + + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = exports.ORCHESTRATION_COMMENT_MODES = exports.ORCHESTRATION_PRESENTATION_MODES = exports.RECONCILIATION_ISSUE_COMPLETION_MODES = exports.RECONCILIATION_CLEANUP_MODES = exports.HOTFIX_ACTIVE_RELEASE_POLICIES = exports.RECONCILIATION_BACKMERGE_MODES = exports.RECONCILIATION_PR_MODES = exports.RECONCILIATION_STRATEGIES = void 0; +exports.validateDeploymentConfiguration = validateDeploymentConfiguration; +exports.isSafeBranchTree = isSafeBranchTree; +exports.parseDeploymentEnum = parseDeploymentEnum; +exports.RECONCILIATION_STRATEGIES = [ + "production-lineage", + "canonical-gitflow", + "manual", +]; +exports.RECONCILIATION_PR_MODES = [ + "auto", + "auto-merge", + "merge-queue", + "create-only", + "legacy-wait", +]; +exports.RECONCILIATION_BACKMERGE_MODES = [ + "auto", + "direct", + "sync-branch", +]; +exports.HOTFIX_ACTIVE_RELEASE_POLICIES = [ + "prefer-release", + "development", + "both", +]; +exports.RECONCILIATION_CLEANUP_MODES = [ + "all", + "source-only", + "sync-only", + "none", +]; +exports.RECONCILIATION_ISSUE_COMPLETION_MODES = ["close", "keep-open"]; +exports.ORCHESTRATION_PRESENTATION_MODES = ["guided", "compact", "quiet"]; +exports.ORCHESTRATION_COMMENT_MODES = ["update", "milestones"]; +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = { + releaseReconciliationStrategy: "production-lineage", + hotfixReconciliationStrategy: "production-lineage", + reconciliationPullRequestMode: "auto", + reconciliationBackmergeMode: "auto", + hotfixActiveReleasePolicy: "prefer-release", + reconciliationTree: "sync", + reconciliationCleanup: "all", + reconciliationIssueCompletion: "close", + orchestrationPresentationMode: "guided", + orchestrationDiagrams: true, + orchestrationCommentMode: "update", +}; +function validateDeploymentConfiguration(configuration, context) { + const errors = []; + for (const [name, value, allowed] of [ + ["release reconciliation strategy", configuration.releaseReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["hotfix reconciliation strategy", configuration.hotfixReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["reconciliation PR mode", configuration.reconciliationPullRequestMode, exports.RECONCILIATION_PR_MODES], + ["reconciliation back-merge mode", configuration.reconciliationBackmergeMode, exports.RECONCILIATION_BACKMERGE_MODES], + ["hotfix active-release policy", configuration.hotfixActiveReleasePolicy, exports.HOTFIX_ACTIVE_RELEASE_POLICIES], + ["reconciliation cleanup", configuration.reconciliationCleanup, exports.RECONCILIATION_CLEANUP_MODES], + ["reconciliation issue completion", configuration.reconciliationIssueCompletion, exports.RECONCILIATION_ISSUE_COMPLETION_MODES], + ["orchestration presentation mode", configuration.orchestrationPresentationMode, exports.ORCHESTRATION_PRESENTATION_MODES], + ["orchestration comment mode", configuration.orchestrationCommentMode, exports.ORCHESTRATION_COMMENT_MODES], + ]) { + if (!allowed.includes(value)) { + errors.push(`The ${name} must be one of: ${allowed.join(", ")}.`); + } + } + if (typeof configuration.orchestrationDiagrams !== "boolean") { + errors.push("Orchestration diagrams must be a boolean."); + } + if (context.productionBranch === context.developmentBranch) { + errors.push("Production and development branches must be different."); + } + const protectedNames = new Set([context.productionBranch, context.developmentBranch]); + for (const [label, tree] of [ + ["release", context.releaseTree], + ["hotfix", context.hotfixTree], + ["reconciliation", configuration.reconciliationTree], + ]) { + if (!isSafeBranchTree(tree)) { + errors.push(`The ${label} branch prefix must be a safe, non-empty Git ref segment.`); + } + else if (protectedNames.has(tree)) { + errors.push(`The ${label} branch prefix cannot equal a protected long-lived branch.`); + } + } + if (configuration.reconciliationPullRequestMode === "merge-queue" + && context.mergeQueueWorkflowSupported === false) { + errors.push("Merge-queue mode requires merge_group support in every required workflow."); + } + if ((configuration.releaseReconciliationStrategy === "manual" + || configuration.hotfixReconciliationStrategy === "manual") + && configuration.reconciliationIssueCompletion === "close") { + errors.push("Manual reconciliation cannot close the launcher issue automatically."); + } + return errors; +} +function isSafeBranchTree(value) { + const tree = value.trim(); + return tree.length > 0 + && tree.length <= 100 + && !tree.startsWith("/") + && !tree.endsWith("/") + && !tree.includes("..") + && !tree.includes("@{") + && !/[~^:?*[\\\]\s]/.test(tree); +} +function parseDeploymentEnum(value, allowed, fallback) { + if (value === undefined || value === null || String(value).trim() === "") { + return { value: fallback, valid: true }; + } + const normalized = String(value).trim(); + return allowed.includes(normalized) + ? { value: normalized, valid: true } + : { value: fallback, valid: false }; +} + + +/***/ }), + +/***/ 2730: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_PHASES = void 0; +exports.transitionDeploymentOperation = transitionDeploymentOperation; +exports.blockDeploymentOperation = blockDeploymentOperation; +exports.resumeBlockedDeployment = resumeBlockedDeployment; +exports.completeReconciliationTarget = completeReconciliationTarget; +exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; +exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; +const deployment_configuration_1 = __nccwpck_require__(2495); +exports.DEPLOYMENT_PHASES = [ + "preparing", + "promotion_pr_pending", + "promoted", + "publishing", + "published", + "reconciliation_pending", + "completed", + "blocked", +]; +const NORMAL_TRANSITIONS = { + preparing: ["promotion_pr_pending"], + promotion_pr_pending: ["promoted"], + promoted: ["publishing"], + publishing: ["published"], + published: ["reconciliation_pending", "completed"], + reconciliation_pending: ["completed"], + completed: [], +}; +function transitionDeploymentOperation(operation, expectedPhase, nextPhase) { + if (operation.phase === nextPhase) { + return { kind: "noop", operation, reason: `Operation is already ${nextPhase}.` }; + } + if (operation.phase !== expectedPhase) { + return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; + } + if (nextPhase === "blocked") { + return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + } + if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { + return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; + } + return { kind: "advance", operation: { ...operation, phase: nextPhase, lastFailure: null } }; +} +function blockDeploymentOperation(operation, category, message, retryable) { + if (operation.phase === "completed") + return operation; + const previousPhase = operation.phase === "blocked" + ? operation.lastFailure?.previousPhase ?? "preparing" + : operation.phase; + return { + ...operation, + phase: "blocked", + lastFailure: { category, message: sanitizeDeploymentMessage(message), retryable, previousPhase }, + }; +} +function resumeBlockedDeployment(operation) { + if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; + } + return { + kind: "advance", + operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + }; +} +function completeReconciliationTarget(operation, pullRequest) { + const targets = operation.reconciliationTargets.map((target) => target.pullRequest === pullRequest ? { ...target, status: "completed" } : target); + return { + ...operation, + reconciliationTargets: targets, + lastFailure: null, + }; +} +function sanitizeDeploymentMessage(value) { + return value + .replace(/::/g, "﹕﹕") + .replace(/@(?=[A-Za-z0-9_-])/g, "@\u200b") + .replace(//g, "-->") + .slice(0, 2000); +} +function isDeploymentOperationSnapshot(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const operation = value; + return typeof operation.operationId === "string" + && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.kind === "release" || operation.kind === "hotfix") + && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) + && typeof operation.title === "string" && operation.title.length <= 1000 + && typeof operation.changelog === "string" && operation.changelog.length <= 50000 + && exports.DEPLOYMENT_PHASES.includes(operation.phase) + && deployment_configuration_1.RECONCILIATION_STRATEGIES.includes(operation.strategy) + && deployment_configuration_1.RECONCILIATION_PR_MODES.includes(operation.prMode) + && (operation.selectedPrMode === undefined + || ["auto-merge", "merge-queue", "create-only", "legacy-wait"].includes(operation.selectedPrMode)) + && deployment_configuration_1.RECONCILIATION_BACKMERGE_MODES.includes(operation.backmergeMode) + && deployment_configuration_1.HOTFIX_ACTIVE_RELEASE_POLICIES.includes(operation.hotfixActiveReleasePolicy) + && deployment_configuration_1.RECONCILIATION_CLEANUP_MODES.includes(operation.cleanup) + && deployment_configuration_1.RECONCILIATION_ISSUE_COMPLETION_MODES.includes(operation.issueCompletion) + && deployment_configuration_1.ORCHESTRATION_PRESENTATION_MODES.includes(operation.presentationMode) + && typeof operation.diagrams === "boolean" + && deployment_configuration_1.ORCHESTRATION_COMMENT_MODES.includes(operation.commentMode) + && isSafePersistedRef(operation.sourceBranch) + && isFullSha(operation.sourceSha) + && isSafePersistedRef(operation.originBranch) + && isFullSha(operation.originSha) + && isSafePersistedRef(operation.productionBranch) + && isSafePersistedRef(operation.developmentBranch) + && typeof operation.reconciliationTree === "string" + && typeof operation.tag === "string" && operation.tag === `v${operation.version}` + && typeof operation.publicationWorkflow === "string" && isSafeWorkflowName(operation.publicationWorkflow) + && (operation.promotionPullRequest === undefined || isPositiveInteger(operation.promotionPullRequest)) + && (operation.productionSha === undefined || isFullSha(operation.productionSha)) + && typeof operation.publicationVerified === "boolean" + && Array.isArray(operation.reconciliationTargets) + && operation.reconciliationTargets.every(isReconciliationTarget) + && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); +} +function isFullSha(value) { + return typeof value === "string" && /^[a-f0-9]{40}$/i.test(value); +} +function isPositiveInteger(value) { + return typeof value === "number" && Number.isSafeInteger(value) && value > 0; +} +function isSafePersistedRef(value) { + return typeof value === "string" + && value.length > 0 + && value.length <= 200 + && !value.includes("..") + && !value.includes("@{") + && !/[\s~^:?*[\\\]]/.test(value); +} +function isSafeWorkflowName(value) { + return value.length <= 200 && !value.includes("..") && /^[A-Za-z0-9][A-Za-z0-9._/-]*\.ya?ml$/.test(value); +} +function isReconciliationTarget(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const target = value; + return isSafePersistedRef(target.targetBranch) + && isSafePersistedRef(target.sourceBranch) + && isFullSha(target.sourceSha) + && (target.syncBranch === undefined || isSafePersistedRef(target.syncBranch)) + && (target.pullRequest === undefined || isPositiveInteger(target.pullRequest)) + && ["pending", "completed", "blocked"].includes(target.status); +} +function isDeploymentFailure(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const failure = value; + return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category) + && typeof failure.message === "string" + && failure.message.length <= 2000 + && typeof failure.retryable === "boolean" + && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] + .includes(failure.previousPhase); +} + + /***/ }), /***/ 4403: diff --git a/build/api/src/data/model/config.d.ts b/build/api/src/data/model/config.d.ts index edcbcfbcf..02fee543d 100644 --- a/build/api/src/data/model/config.d.ts +++ b/build/api/src/data/model/config.d.ts @@ -1,8 +1,9 @@ import { BranchConfiguration } from "./branch_configuration"; import { RecommendationState } from "./recommendation_state"; import { Result } from "./result"; +import { type DeploymentOperationSnapshot } from '../../domain/deployment_operation'; /** Version of the durable configuration contract stored in issue/PR content. */ -export declare const CONFIG_SCHEMA_VERSION = 2; +export declare const CONFIG_SCHEMA_VERSION = 3; export interface ConfigurationMigrationResult { readonly payload: Record; readonly sourceVersion: number; @@ -23,6 +24,10 @@ export declare class Config { parentBranch: string | undefined; hotfixOriginBranch: string | undefined; hotfixBranch: string | undefined; + releaseOriginBranch: string | undefined; + releaseOriginSha: string | undefined; + hotfixOriginSha: string | undefined; + deploymentOrchestration: DeploymentOperationSnapshot | undefined; results: Result[]; branchConfiguration: BranchConfiguration | undefined; recommendationState: RecommendationState | undefined; diff --git a/build/api/src/data/model/execution.d.ts b/build/api/src/data/model/execution.d.ts index ab02613ac..c0d6c234c 100644 --- a/build/api/src/data/model/execution.d.ts +++ b/build/api/src/data/model/execution.d.ts @@ -19,6 +19,7 @@ import { Welcome } from "./welcome"; import { Workflows } from "./workflows"; import type { ExecutionInputs } from './execution_inputs'; import type { ExecutionComponents } from './execution_components'; +import { type DeploymentConfigurationValues } from '../../domain/deployment_configuration'; export declare class Execution { debug: boolean; welcome: Welcome | undefined; @@ -46,6 +47,7 @@ export declare class Execution { issue: Issue; pullRequest: PullRequest; workflows: Workflows; + deployment: DeploymentConfigurationValues; project: Projects; previousConfiguration: Config | undefined; currentConfiguration: Config; diff --git a/build/api/src/data/model/execution_components.d.ts b/build/api/src/data/model/execution_components.d.ts index dd57bcfc5..add650a3e 100644 --- a/build/api/src/data/model/execution_components.d.ts +++ b/build/api/src/data/model/execution_components.d.ts @@ -16,6 +16,7 @@ import type { Tokens } from './tokens'; import type { Welcome } from './welcome'; import type { Workflows } from './workflows'; import type { ExecutionInputs } from './execution_inputs'; +import type { DeploymentConfigurationValues } from '../../domain/deployment_configuration'; /** Immutable construction contract for the runtime execution aggregate. */ export interface ExecutionComponents { debug: boolean; @@ -35,6 +36,7 @@ export interface ExecutionComponents { release: Release; hotfix: Hotfix; workflows: Workflows; + deployment?: DeploymentConfigurationValues; projects: Projects; tokenUser?: string; welcome?: Welcome; diff --git a/build/api/src/data/model/single_action.d.ts b/build/api/src/data/model/single_action.d.ts index 620995936..d087a058d 100644 --- a/build/api/src/data/model/single_action.d.ts +++ b/build/api/src/data/model/single_action.d.ts @@ -20,6 +20,7 @@ export declare class SingleAction { title: string; changelog: string; message: string; + operationId: string; commentId: number; commentIdInput: string; commentMode: string; @@ -35,9 +36,14 @@ export declare class SingleAction { get isCloseInactiveIssuesAction(): boolean; get isPublishIssueCommentAction(): boolean; get isCheckBranchSyncAction(): boolean; + get isPrepareDeploymentAction(): boolean; + get isContinueDeploymentAction(): boolean; + get isPublishedDeploymentAction(): boolean; + get isFailedDeploymentAction(): boolean; + get isDeploymentOrchestrationAction(): boolean; get enabledSingleAction(): boolean; get validSingleAction(): boolean; get isSingleActionWithoutIssue(): boolean; get throwError(): boolean; - constructor(currentSingleAction: string, issue: string, version: string, title: string, changelog: string, message?: string, commentId?: string, commentMode?: string); + constructor(currentSingleAction: string, issue: string, version: string, title: string, changelog: string, message?: string, commentId?: string, commentMode?: string, operationId?: string); } diff --git a/build/api/src/domain/deployment_configuration.d.ts b/build/api/src/domain/deployment_configuration.d.ts new file mode 100644 index 000000000..81ad8cae9 --- /dev/null +++ b/build/api/src/domain/deployment_configuration.d.ts @@ -0,0 +1,43 @@ +export declare const RECONCILIATION_STRATEGIES: readonly ["production-lineage", "canonical-gitflow", "manual"]; +export type ReconciliationStrategy = (typeof RECONCILIATION_STRATEGIES)[number]; +export declare const RECONCILIATION_PR_MODES: readonly ["auto", "auto-merge", "merge-queue", "create-only", "legacy-wait"]; +export type ReconciliationPullRequestMode = (typeof RECONCILIATION_PR_MODES)[number]; +export declare const RECONCILIATION_BACKMERGE_MODES: readonly ["auto", "direct", "sync-branch"]; +export type ReconciliationBackmergeMode = (typeof RECONCILIATION_BACKMERGE_MODES)[number]; +export declare const HOTFIX_ACTIVE_RELEASE_POLICIES: readonly ["prefer-release", "development", "both"]; +export type HotfixActiveReleasePolicy = (typeof HOTFIX_ACTIVE_RELEASE_POLICIES)[number]; +export declare const RECONCILIATION_CLEANUP_MODES: readonly ["all", "source-only", "sync-only", "none"]; +export type ReconciliationCleanupMode = (typeof RECONCILIATION_CLEANUP_MODES)[number]; +export declare const RECONCILIATION_ISSUE_COMPLETION_MODES: readonly ["close", "keep-open"]; +export type ReconciliationIssueCompletionMode = (typeof RECONCILIATION_ISSUE_COMPLETION_MODES)[number]; +export declare const ORCHESTRATION_PRESENTATION_MODES: readonly ["guided", "compact", "quiet"]; +export type OrchestrationPresentationMode = (typeof ORCHESTRATION_PRESENTATION_MODES)[number]; +export declare const ORCHESTRATION_COMMENT_MODES: readonly ["update", "milestones"]; +export type OrchestrationCommentMode = (typeof ORCHESTRATION_COMMENT_MODES)[number]; +export interface DeploymentConfigurationValues { + releaseReconciliationStrategy: ReconciliationStrategy; + hotfixReconciliationStrategy: ReconciliationStrategy; + reconciliationPullRequestMode: ReconciliationPullRequestMode; + reconciliationBackmergeMode: ReconciliationBackmergeMode; + hotfixActiveReleasePolicy: HotfixActiveReleasePolicy; + reconciliationTree: string; + reconciliationCleanup: ReconciliationCleanupMode; + reconciliationIssueCompletion: ReconciliationIssueCompletionMode; + orchestrationPresentationMode: OrchestrationPresentationMode; + orchestrationDiagrams: boolean; + orchestrationCommentMode: OrchestrationCommentMode; +} +export declare const DEFAULT_DEPLOYMENT_CONFIGURATION: Readonly; +export interface DeploymentConfigurationValidationContext { + readonly productionBranch: string; + readonly developmentBranch: string; + readonly releaseTree: string; + readonly hotfixTree: string; + readonly mergeQueueWorkflowSupported?: boolean; +} +export declare function validateDeploymentConfiguration(configuration: DeploymentConfigurationValues, context: DeploymentConfigurationValidationContext): string[]; +export declare function isSafeBranchTree(value: string): boolean; +export declare function parseDeploymentEnum(value: unknown, allowed: readonly T[], fallback: T): { + value: T; + valid: boolean; +}; diff --git a/build/api/src/domain/deployment_operation.d.ts b/build/api/src/domain/deployment_operation.d.ts new file mode 100644 index 000000000..5dd955ebe --- /dev/null +++ b/build/api/src/domain/deployment_operation.d.ts @@ -0,0 +1,70 @@ +import type { HotfixActiveReleasePolicy, OrchestrationCommentMode, OrchestrationPresentationMode, ReconciliationBackmergeMode, ReconciliationCleanupMode, ReconciliationIssueCompletionMode, ReconciliationPullRequestMode, ReconciliationStrategy } from "./deployment_configuration"; +export declare const DEPLOYMENT_PHASES: readonly ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed", "blocked"]; +export type DeploymentPhase = (typeof DEPLOYMENT_PHASES)[number]; +export type DeploymentKind = "release" | "hotfix"; +export type ManagedPullRequestPhase = "promotion" | "reconciliation"; +export type ReconciliationTargetStatus = "pending" | "completed" | "blocked"; +export interface DeploymentFailure { + readonly category: "promotion" | "publication" | "reconciliation" | "cleanup"; + readonly message: string; + readonly retryable: boolean; + readonly previousPhase: Exclude; +} +export interface ReconciliationTargetState { + readonly targetBranch: string; + readonly sourceBranch: string; + readonly sourceSha: string; + readonly syncBranch?: string; + readonly pullRequest?: number; + readonly status: ReconciliationTargetStatus; +} +export interface DeploymentOperationSnapshot { + readonly operationId: string; + readonly kind: DeploymentKind; + readonly version: string; + readonly title: string; + readonly changelog: string; + readonly phase: DeploymentPhase; + readonly strategy: ReconciliationStrategy; + readonly prMode: ReconciliationPullRequestMode; + readonly selectedPrMode?: Exclude; + readonly backmergeMode: ReconciliationBackmergeMode; + readonly hotfixActiveReleasePolicy: HotfixActiveReleasePolicy; + readonly cleanup: ReconciliationCleanupMode; + readonly issueCompletion: ReconciliationIssueCompletionMode; + readonly presentationMode: OrchestrationPresentationMode; + readonly diagrams: boolean; + readonly commentMode: OrchestrationCommentMode; + readonly sourceBranch: string; + readonly sourceSha: string; + readonly originBranch: string; + readonly originSha: string; + readonly productionBranch: string; + readonly developmentBranch: string; + readonly reconciliationTree: string; + readonly promotionPullRequest?: number; + readonly productionSha?: string; + readonly tag: string; + readonly publicationWorkflow: string; + readonly publicationVerified: boolean; + readonly reconciliationTargets: readonly ReconciliationTargetState[]; + readonly lastFailure?: DeploymentFailure | null; +} +export type DeploymentTransitionDecision = { + readonly kind: "advance"; + readonly operation: DeploymentOperationSnapshot; +} | { + readonly kind: "noop"; + readonly operation: DeploymentOperationSnapshot; + readonly reason: string; +} | { + readonly kind: "invalid"; + readonly operation: DeploymentOperationSnapshot; + readonly reason: string; +}; +export declare function transitionDeploymentOperation(operation: DeploymentOperationSnapshot, expectedPhase: DeploymentPhase, nextPhase: DeploymentPhase): DeploymentTransitionDecision; +export declare function blockDeploymentOperation(operation: DeploymentOperationSnapshot, category: DeploymentFailure["category"], message: string, retryable: boolean): DeploymentOperationSnapshot; +export declare function resumeBlockedDeployment(operation: DeploymentOperationSnapshot): DeploymentTransitionDecision; +export declare function completeReconciliationTarget(operation: DeploymentOperationSnapshot, pullRequest: number): DeploymentOperationSnapshot; +export declare function sanitizeDeploymentMessage(value: string): string; +export declare function isDeploymentOperationSnapshot(value: unknown): value is DeploymentOperationSnapshot; diff --git a/build/cli/index.js b/build/cli/index.js index 27fa3d6cb..a5281c2db 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -54861,6 +54861,65 @@ exports.DEFAULT_IMAGE_CONFIG = { }; +/***/ }), + +/***/ 30098: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readDeploymentConfiguration = readDeploymentConfiguration; +const application_error_1 = __nccwpck_require__(75999); +const deployment_configuration_1 = __nccwpck_require__(22495); +const input_keys_1 = __nccwpck_require__(88539); +function readDeploymentConfiguration(getInput, branches) { + const errors = []; + const readEnum = (key, allowed, fallback) => { + const parsed = (0, deployment_configuration_1.parseDeploymentEnum)(getInput(key), allowed, fallback); + if (!parsed.valid) + errors.push(`${key} must be one of: ${allowed.join(", ")}.`); + return parsed.value; + }; + const configuration = { + releaseReconciliationStrategy: readEnum(input_keys_1.INPUT_KEYS.RELEASE_RECONCILIATION_STRATEGY, deployment_configuration_1.RECONCILIATION_STRATEGIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.releaseReconciliationStrategy), + hotfixReconciliationStrategy: readEnum(input_keys_1.INPUT_KEYS.HOTFIX_RECONCILIATION_STRATEGY, deployment_configuration_1.RECONCILIATION_STRATEGIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixReconciliationStrategy), + reconciliationPullRequestMode: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_PR_MODE, deployment_configuration_1.RECONCILIATION_PR_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationPullRequestMode), + reconciliationBackmergeMode: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_BACKMERGE_MODE, deployment_configuration_1.RECONCILIATION_BACKMERGE_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationBackmergeMode), + hotfixActiveReleasePolicy: readEnum(input_keys_1.INPUT_KEYS.HOTFIX_ACTIVE_RELEASE_POLICY, deployment_configuration_1.HOTFIX_ACTIVE_RELEASE_POLICIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixActiveReleasePolicy), + reconciliationTree: String(getInput(input_keys_1.INPUT_KEYS.RECONCILIATION_TREE) + ?? deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree).trim() + || deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree, + reconciliationCleanup: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_CLEANUP, deployment_configuration_1.RECONCILIATION_CLEANUP_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationCleanup), + reconciliationIssueCompletion: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_ISSUE_COMPLETION, deployment_configuration_1.RECONCILIATION_ISSUE_COMPLETION_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationIssueCompletion), + orchestrationPresentationMode: readEnum(input_keys_1.INPUT_KEYS.ORCHESTRATION_PRESENTATION_MODE, deployment_configuration_1.ORCHESTRATION_PRESENTATION_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationPresentationMode), + orchestrationDiagrams: readBoolean(getInput(input_keys_1.INPUT_KEYS.ORCHESTRATION_DIAGRAMS), deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationDiagrams, input_keys_1.INPUT_KEYS.ORCHESTRATION_DIAGRAMS, errors), + orchestrationCommentMode: readEnum(input_keys_1.INPUT_KEYS.ORCHESTRATION_COMMENT_MODE, deployment_configuration_1.ORCHESTRATION_COMMENT_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationCommentMode), + }; + errors.push(...(0, deployment_configuration_1.validateDeploymentConfiguration)(configuration, { + productionBranch: branches.productionBranch || "master", + developmentBranch: branches.developmentBranch || "develop", + releaseTree: branches.releaseTree || "release", + hotfixTree: branches.hotfixTree || "hotfix", + })); + if (errors.length > 0) { + throw new application_error_1.ApplicationError(`Invalid deployment configuration: ${errors.join(" ")}`, "validation"); + } + return configuration; +} +function readBoolean(value, fallback, name, errors) { + if (value === undefined || value === null || String(value).trim() === "") + return fallback; + const normalized = String(value).trim().toLowerCase(); + if (normalized === "true") + return true; + if (normalized === "false") + return false; + errors.push(`${name} must be true or false.`); + return fallback; +} + + /***/ }), /***/ 20236: @@ -55092,6 +55151,7 @@ const image_configuration_builder_1 = __nccwpck_require__(9246); const pull_request_description_1 = __nccwpck_require__(45315); const issue_inactivity_1 = __nccwpck_require__(38572); const review_configuration_1 = __nccwpck_require__(3994); +const deployment_configuration_builder_1 = __nccwpck_require__(30098); function input(additionalParams, actionInputs, key) { return (0, action_input_source_1.resolveActionInput)(additionalParams, actionInputs, key); } @@ -55107,6 +55167,7 @@ function readLocalCoreConfiguration(additionalParams, actionInputs) { singleActionTitle: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE), singleActionChangelog: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG), singleActionMessage: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_MESSAGE), + singleActionOperationId: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_OPERATION_ID), singleActionCommentId: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_ID), singleActionCommentMode: input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_MODE), inactivityThresholdHours: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(input(additionalParams, actionInputs, input_keys_1.INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS), @@ -55293,6 +55354,10 @@ function readThresholds(additionalParams, actionInputs) { } function readLocalWorkflowConfiguration(additionalParams, actionInputs) { const read = (key) => input(additionalParams, actionInputs, key); + const mainBranch = read(input_keys_1.INPUT_KEYS.MAIN_BRANCH); + const developmentBranch = read(input_keys_1.INPUT_KEYS.DEVELOPMENT_BRANCH); + const releaseTree = read(input_keys_1.INPUT_KEYS.RELEASE_TREE); + const hotfixTree = read(input_keys_1.INPUT_KEYS.HOTFIX_TREE); return { imageConfiguration: (0, image_configuration_builder_1.buildImageConfiguration)((key) => additionalParams[key] ?? actionInputs[key]), releaseWorkflow: read(input_keys_1.INPUT_KEYS.RELEASE_WORKFLOW), @@ -55302,12 +55367,12 @@ function readLocalWorkflowConfiguration(additionalParams, actionInputs) { issueLocale: read(input_keys_1.INPUT_KEYS.ISSUES_LOCALE) ?? locale_1.Locale.DEFAULT, pullRequestLocale: read(input_keys_1.INPUT_KEYS.PULL_REQUESTS_LOCALE) ?? locale_1.Locale.DEFAULT, ...readThresholds(additionalParams, actionInputs), - mainBranch: read(input_keys_1.INPUT_KEYS.MAIN_BRANCH), - developmentBranch: read(input_keys_1.INPUT_KEYS.DEVELOPMENT_BRANCH), + mainBranch, + developmentBranch, featureTree: read(input_keys_1.INPUT_KEYS.FEATURE_TREE), bugfixTree: read(input_keys_1.INPUT_KEYS.BUGFIX_TREE), - hotfixTree: read(input_keys_1.INPUT_KEYS.HOTFIX_TREE), - releaseTree: read(input_keys_1.INPUT_KEYS.RELEASE_TREE), + hotfixTree, + releaseTree, docsTree: read(input_keys_1.INPUT_KEYS.DOCS_TREE), choreTree: read(input_keys_1.INPUT_KEYS.CHORE_TREE), commitPrefixBuilder: read(input_keys_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', @@ -55317,6 +55382,12 @@ function readLocalWorkflowConfiguration(additionalParams, actionInputs) { pullRequestDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), pullRequestDesiredReviewersCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), pullRequestMergeTimeout: (0, input_number_policy_1.parseNonNegativeIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), + deployment: (0, deployment_configuration_builder_1.readDeploymentConfiguration)(read, { + productionBranch: mainBranch, + developmentBranch, + releaseTree, + hotfixTree, + }), }; } @@ -55340,11 +55411,11 @@ const configuration_builders_1 = __nccwpck_require__(19094); const branches_builder_1 = __nccwpck_require__(30085); const size_threshold_builder_1 = __nccwpck_require__(39757); function buildLocalActionExecution(configuration, additionalParams) { - const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, inactivityThresholdHours, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, bugbotReviewConfiguration, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, } = configuration; + const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId, inactivityThresholdHours, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, titleEmoji, branchManagementEmoji, imageConfiguration, token, agentModel, aiPullRequestDescription, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, bugbotReviewConfiguration, agentTasks, branchManagementLauncherLabel, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, deployment, } = configuration; return (0, execution_builder_1.buildExecution)({ debug, inactivityThresholdHours, - singleAction: new single_action_1.SingleAction(singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode), + singleAction: new single_action_1.SingleAction(singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId), commitPrefixBuilder, issue: (0, configuration_builders_1.buildIssue)(branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, additionalParams), pullRequest: (0, configuration_builders_1.buildPullRequest)(pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, additionalParams), @@ -55400,6 +55471,7 @@ function buildLocalActionExecution(configuration, additionalParams) { release: new release_1.Release(), hotfix: new hotfix_1.Hotfix(), workflows: (0, configuration_builders_1.buildWorkflows)(releaseWorkflow, hotfixWorkflow), + deployment, projects: (0, configuration_builders_1.buildProjects)({ projects, issueCreated: projectColumnIssueCreated, @@ -55796,6 +55868,7 @@ exports.INPUT_KEYS = { SINGLE_ACTION_TITLE: 'single-action-title', SINGLE_ACTION_CHANGELOG: 'single-action-changelog', SINGLE_ACTION_MESSAGE: 'single-action-message', + SINGLE_ACTION_OPERATION_ID: 'single-action-operation-id', SINGLE_ACTION_COMMENT_ID: 'single-action-comment-id', SINGLE_ACTION_COMMENT_MODE: 'single-action-comment-mode', INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', @@ -55884,6 +55957,17 @@ exports.INPUT_KEYS = { // Workflows RELEASE_WORKFLOW: 'release-workflow', HOTFIX_WORKFLOW: 'hotfix-workflow', + RELEASE_RECONCILIATION_STRATEGY: 'release-reconciliation-strategy', + HOTFIX_RECONCILIATION_STRATEGY: 'hotfix-reconciliation-strategy', + RECONCILIATION_PR_MODE: 'reconciliation-pr-mode', + RECONCILIATION_BACKMERGE_MODE: 'reconciliation-backmerge-mode', + HOTFIX_ACTIVE_RELEASE_POLICY: 'hotfix-active-release-policy', + RECONCILIATION_TREE: 'reconciliation-tree', + RECONCILIATION_CLEANUP: 'reconciliation-cleanup', + RECONCILIATION_ISSUE_COMPLETION: 'reconciliation-issue-completion', + ORCHESTRATION_PRESENTATION_MODE: 'orchestration-presentation-mode', + ORCHESTRATION_DIAGRAMS: 'orchestration-diagrams', + ORCHESTRATION_COMMENT_MODE: 'orchestration-comment-mode', // Emoji EMOJI_LABELED_TITLE: 'emoji-labeled-title', BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', @@ -57009,6 +57093,497 @@ function buildDeploymentMergePlan(configuration) { } +/***/ }), + +/***/ 1779: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.validateDeploymentContinuation = validateDeploymentContinuation; +/** + * Rejects forged, stale, or out-of-order workflow continuations before a + * publication-side mutation is attempted. Legacy releases without durable + * orchestration state remain supported during migration. + */ +function validateDeploymentContinuation(operation, expectedOperationId, allowedPhases, expectedVersion) { + if (!operation) + return undefined; + if (!expectedOperationId) + return "single-action-operation-id is required for a durable deployment continuation."; + if (expectedOperationId !== operation.operationId) { + return `Deployment operation mismatch: expected ${operation.operationId}, received ${expectedOperationId}.`; + } + if (!expectedVersion) + return "single-action-version is required for a durable publication continuation."; + if (expectedVersion !== operation.version) { + return `Deployment version mismatch: expected ${operation.version}, received ${expectedVersion}.`; + } + const effectivePhase = operation.phase === "blocked" && operation.lastFailure?.retryable + ? operation.lastFailure.previousPhase + : operation.phase; + if (!allowedPhases.includes(effectivePhase)) { + return `Deployment operation ${operation.operationId} cannot continue publication from phase ${operation.phase}.`; + } + return undefined; +} + + +/***/ }), + +/***/ 54037: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.projectDeploymentLabels = projectDeploymentLabels; +const copilot_lifecycle_1 = __nccwpck_require__(72418); +function projectDeploymentLabels(current, operation, labels) { + const managed = new Set((0, copilot_lifecycle_1.managedLifecycleLabelNames)(labels.lifecycle)); + let projected = current.filter((label) => !managed.has(label)); + if (operation.publicationVerified) { + projected = projected.filter((label) => label !== labels.deploy); + if (!projected.includes(labels.deployed)) + projected.push(labels.deployed); + } + const selectedMode = operation.selectedPrMode ?? operation.prMode; + if (operation.phase === "completed") + projected.push(labels.lifecycle.verified); + else if (operation.phase === "blocked") + projected.push(labels.lifecycle.blocked, labels.lifecycle.awaitingMaintainer); + else if ((operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending") && selectedMode === "create-only") { + projected.push(labels.lifecycle.ready, labels.lifecycle.awaitingMaintainer); + } + else if (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending") { + projected.push(labels.lifecycle.reviewing); + } + else { + projected.push(labels.lifecycle.inProgress); + } + return [...new Set(projected)]; +} + + +/***/ }), + +/***/ 8352: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildInitialDeploymentOperation = buildInitialDeploymentOperation; +exports.selectPullRequestMode = selectPullRequestMode; +exports.selectBackmergeMode = selectBackmergeMode; +exports.selectReconciliationTargetBranches = selectReconciliationTargetBranches; +exports.reconciliationSource = reconciliationSource; +exports.buildReconciliationTarget = buildReconciliationTarget; +exports.buildReconciliationBranchName = buildReconciliationBranchName; +exports.validateInitialDeploymentInput = validateInitialDeploymentInput; +function buildInitialDeploymentOperation(input) { + const strategy = input.kind === "release" + ? input.configuration.releaseReconciliationStrategy + : input.configuration.hotfixReconciliationStrategy; + return { + operationId: input.operationId, + kind: input.kind, + version: input.version, + title: input.title, + changelog: input.changelog, + phase: "preparing", + strategy, + prMode: input.configuration.reconciliationPullRequestMode, + backmergeMode: input.configuration.reconciliationBackmergeMode, + hotfixActiveReleasePolicy: input.configuration.hotfixActiveReleasePolicy, + cleanup: input.configuration.reconciliationCleanup, + issueCompletion: input.configuration.reconciliationIssueCompletion, + presentationMode: input.configuration.orchestrationPresentationMode, + diagrams: input.configuration.orchestrationDiagrams, + commentMode: input.configuration.orchestrationCommentMode, + sourceBranch: input.sourceBranch, + sourceSha: input.sourceSha, + originBranch: input.originBranch, + originSha: input.originSha, + productionBranch: input.productionBranch, + developmentBranch: input.developmentBranch, + reconciliationTree: input.configuration.reconciliationTree, + tag: `v${input.version}`, + publicationWorkflow: input.publicationWorkflow, + publicationVerified: false, + reconciliationTargets: [], + lastFailure: null, + }; +} +function selectPullRequestMode(configured, capabilities) { + if (configured === "create-only" || configured === "legacy-wait") { + return { kind: "mode", mode: configured, reason: "Explicitly configured." }; + } + if (configured === "merge-queue" || (configured === "auto" && capabilities.mergeQueueRequired)) { + return capabilities.mergeQueueRequired + ? { kind: "mode", mode: "merge-queue", reason: "The target requires its merge queue." } + : { kind: "unsupported", reason: "The target does not expose a required merge queue." }; + } + if (configured === "auto-merge") { + return capabilities.autoMergeAllowed + ? { kind: "mode", mode: "auto-merge", reason: "Native auto-merge was explicitly configured." } + : { kind: "unsupported", reason: "Native auto-merge is disabled for this repository." }; + } + if (capabilities.immediatelyMergeable) { + return { kind: "mode", mode: "auto-merge", reason: "GitHub reports the PR ready; native auto-merge preserves branch protection." }; + } + return capabilities.autoMergeAllowed + ? { kind: "mode", mode: "auto-merge", reason: "GitHub will merge after checks and reviews complete." } + : { kind: "mode", mode: "create-only", reason: "Repository auto-merge is unavailable; maintainer merge is required." }; +} +function selectBackmergeMode(configured, requiresStrictStatusChecks, directHeadIsUpToDate, directSourceIsExact = true) { + const directIsUnsafe = !directSourceIsExact + || (requiresStrictStatusChecks && !directHeadIsUpToDate); + if (configured === "direct" && directIsUnsafe) { + const reason = !directSourceIsExact + ? "Direct reconciliation was rejected because its source branch no longer points at the stored release SHA. Use auto or sync-branch to keep this operation isolated." + : "Direct reconciliation cannot satisfy the target's strict up-to-date rule without merging development into production. Use auto or sync-branch."; + return { kind: "unsupported", reason }; + } + if (configured === "sync-branch" || (configured === "auto" && directIsUnsafe)) { + return { + kind: "mode", + mode: "sync-branch", + reason: !directSourceIsExact + ? "A dedicated sync branch pins the stored release SHA after the source branch advanced." + : "A dedicated sync branch satisfies the target's strict up-to-date rule without changing production.", + }; + } + return { kind: "mode", mode: "direct", reason: "The exact source can be reconciled directly into this target." }; +} +function selectReconciliationTargetBranches(operation, activeReleaseBranches) { + if (operation.strategy === "manual") + return { kind: "manual" }; + if (operation.kind === "release") { + return { kind: "targets", targetBranches: [operation.developmentBranch] }; + } + const releases = [...new Set(activeReleaseBranches.filter(Boolean))]; + if (operation.hotfixActiveReleasePolicy !== "development" && releases.length > 1) { + return { kind: "blocked", reason: "Multiple active release branches require an explicit hotfix reconciliation decision." }; + } + if (operation.hotfixActiveReleasePolicy === "development" || releases.length === 0) { + return { kind: "targets", targetBranches: [operation.developmentBranch] }; + } + if (operation.hotfixActiveReleasePolicy === "prefer-release") { + return { kind: "targets", targetBranches: releases }; + } + return { kind: "targets", targetBranches: [...releases, operation.developmentBranch] }; +} +function reconciliationSource(operation) { + return operation.strategy === "canonical-gitflow" + ? { branch: operation.sourceBranch, sha: operation.sourceSha } + : { branch: operation.productionBranch, sha: operation.productionSha ?? "" }; +} +function buildReconciliationTarget(operation, targetBranch, mode) { + const source = reconciliationSource(operation); + return { + targetBranch, + sourceBranch: source.branch, + sourceSha: source.sha, + syncBranch: mode === "sync-branch" ? buildReconciliationBranchName(operation, targetBranch) : undefined, + status: "pending", + }; +} +function buildReconciliationBranchName(operation, targetBranch) { + const safeTarget = targetBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, ""); + const safeOperation = operation.operationId.replace(/[^A-Za-z0-9]/g, "").slice(0, 8).toLowerCase(); + return `${operation.reconciliationTree}/${operation.kind}-${operation.version}-to-${safeTarget}-${safeOperation}`; +} +function validateInitialDeploymentInput(input) { + const errors = []; + if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(input.version)) + errors.push("Version must use MAJOR.MINOR.PATCH format."); + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(input.sourceBranch)) + errors.push("Source branch is invalid."); + if (!/^[a-f0-9]{40}$/i.test(input.sourceSha)) + errors.push("Source SHA must be a full commit SHA."); + if (!/^[a-f0-9]{40}$/i.test(input.originSha)) + errors.push("Origin SHA must be a full commit SHA."); + if (input.sourceBranch === input.productionBranch || input.sourceBranch === input.developmentBranch) { + errors.push("A frozen release/hotfix branch is required as the deployment source."); + } + return errors; +} + + +/***/ }), + +/***/ 83221: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_DASHBOARD_MARKER = void 0; +exports.deploymentDashboardMarker = deploymentDashboardMarker; +exports.renderDeploymentDashboard = renderDeploymentDashboard; +exports.renderPromotionPullRequest = renderPromotionPullRequest; +exports.renderReconciliationPullRequest = renderReconciliationPullRequest; +exports.renderDeploymentJobSummary = renderDeploymentJobSummary; +exports.normalizeLocale = normalizeLocale; +const managed_pull_request_1 = __nccwpck_require__(95914); +const EN = { + release: "Release", hotfix: "Hotfix", currentStatus: "Current status", + noAction: "No action is required while GitHub owns the pending transition.", actionRequired: "Action required", + progress: "Progress", currentTransition: "Current transition", whatNext: "What happens next", links: "Links", + technical: "Technical details", alreadyPublished: "Package status: already published", notPublished: "Package status: not published", + productionUpdated: "Production updated", developmentSynchronized: "Development synchronized", yes: "Yes", no: "No", + from: "From", to: "To", state: "State", compare: "Compare changes", controlCenter: "Release control center", + purpose: "Purpose", afterMerge: "After merge", purposePromotion: "accept the prepared change in production", + purposeReconciliation: "bring the accepted production state back to the development line", + afterPromotion: "After merge, Copilot will tag and publish the accepted production commit.", + noRepublish: "Merging or closing this PR cannot publish the package again.", + origin: "Origin", preparedSource: "Prepared source", destination: "Destination", publication: "Publication", + productionFact: "Production fact", developmentTarget: "Development target", completionEffect: "Completion effect", + closeIssue: "Close issue after all targets", keepIssue: "Keep issue open", readyBeforeReview: "Ready before review", + buildValidation: "Build and release validation", packageSmoke: "Package smoke test", + protectedChecks: "Protected-branch checks and reviews", syncReason: "A dedicated sync branch preserves target-only commits and isolates target-dependent checks.", + protectedFacts: "What Copilot protected", cut: "Source cut", promotion: "Production promotion", reconciliation: "Development reconciliation", + cleanup: "Cleanup and issue completion", jobSummary: "Deployment orchestration", result: "Result", + externalWait: "Waiting externally", workflowFailure: "Workflow failed", previousPhase: "Previous phase", resultingPhase: "Resulting phase", + retryable: "Retryable", createdReused: "Created, reused, or skipped", fallback: "prepared -> production PR -> accepted -> published -> reconciled -> complete", + phase: { + preparing: "preparing the version", promotion_pr_pending: "waiting for production approval", promoted: "accepted in production", + publishing: "publishing artifacts", published: "published; preparing development reconciliation", + reconciliation_pending: "waiting for development reconciliation", completed: "completed", blocked: "needs attention", + }, + diagram: ["Source snapshot", "Version prepared", "Production PR", "Accepted in production", "Package and release", "Development reconciliation", "Complete"], +}; +const ES = { + release: "Release", hotfix: "Hotfix", currentStatus: "Estado actual", + noAction: "No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.", actionRequired: "Acción necesaria", + progress: "Progreso", currentTransition: "Transición actual", whatNext: "Qué ocurrirá después", links: "Enlaces", + technical: "Detalles técnicos", alreadyPublished: "Estado del paquete: ya publicado", notPublished: "Estado del paquete: no publicado", + productionUpdated: "Producción actualizada", developmentSynchronized: "Desarrollo sincronizado", yes: "Sí", no: "No", + from: "Origen", to: "Destino", state: "Estado", compare: "Comparar cambios", controlCenter: "Centro de control de la release", + purpose: "Propósito", afterMerge: "Después del merge", purposePromotion: "aceptar en producción el cambio preparado", + purposeReconciliation: "llevar el estado aceptado en producción de vuelta a desarrollo", + afterPromotion: "Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.", + noRepublish: "Mergear o cerrar esta PR no puede volver a publicar el paquete.", + origin: "Origen", preparedSource: "Fuente preparada", destination: "Destino", publication: "Publicación", + productionFact: "Estado de producción", developmentTarget: "Destino de desarrollo", completionEffect: "Efecto al completar", + closeIssue: "Cerrar la issue tras todos los destinos", keepIssue: "Mantener la issue abierta", readyBeforeReview: "Listo antes de revisar", + buildValidation: "Build y validación de release", packageSmoke: "Smoke test del paquete", + protectedChecks: "Checks y revisiones de la rama protegida", syncReason: "Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.", + protectedFacts: "Qué ha protegido Copilot", cut: "Corte de la fuente", promotion: "Promoción a producción", reconciliation: "Reconciliación con desarrollo", + cleanup: "Limpieza y cierre de la issue", jobSummary: "Orquestación del despliegue", result: "Resultado", + externalWait: "Esperando fuera del workflow", workflowFailure: "Workflow fallido", previousPhase: "Fase anterior", resultingPhase: "Fase resultante", + retryable: "Reintentable", createdReused: "Creado, reutilizado u omitido", fallback: "preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada", + phase: { + preparing: "preparando la versión", promotion_pr_pending: "esperando aprobación en producción", promoted: "aceptada en producción", + publishing: "publicando artefactos", published: "publicada; preparando la reconciliación", + reconciliation_pending: "esperando reconciliación con desarrollo", completed: "completada", blocked: "necesita atención", + }, + diagram: ["Snapshot de origen", "Versión preparada", "PR de producción", "Aceptada en producción", "Paquete y release", "Reconciliación con desarrollo", "Completada"], +}; +exports.DEPLOYMENT_DASHBOARD_MARKER = "copilot-deployment-dashboard"; +function deploymentDashboardMarker(operationId, issue) { + return ``; +} +function renderDeploymentDashboard(operation, context) { + const messages = messagesFor(context.issueLocale); + const title = operation.kind === "release" ? messages.release : messages.hotfix; + const action = deploymentAction(operation, messages); + const lines = [ + deploymentDashboardMarker(operation.operationId, context.issue), "", + `# ${operation.phase === "blocked" ? "❌" : operation.phase === "completed" ? "✅" : "🚀"} ${title} ${inline(operation.version)}`, "", + `> **${messages.currentStatus}: ${messages.phase[operation.phase]}.**`, + ]; + if (action.required) + lines.push("", `## ${messages.actionRequired}`, "", action.message); + else + lines.push(`> ${action.message}`); + lines.push(""); + if (operation.phase === "blocked") + lines.push(...factTable(operation, messages), "", `## ${messages.protectedFacts}`, "", protectedFact(operation, messages), ""); + if (operation.presentationMode !== "quiet") + lines.push(`## ${messages.progress}`, "", ...progressLines(operation, messages), ""); + if (operation.presentationMode === "guided" && operation.diagrams) + lines.push(...deploymentDiagram(messages), ""); + if (operation.presentationMode !== "quiet") { + lines.push(`## ${messages.currentTransition}`, "", ...transitionTable(operation, messages), "", `## ${messages.whatNext}`, "", nextDescription(operation, messages), "", `## ${messages.links}`, "", deploymentLinks(operation, context, messages).join(" · "), ""); + } + lines.push("
", `${messages.technical}`, "", `- Operation: ${inline(operation.operationId)}`, `- Strategy: ${inline(operation.strategy)}`, `- PR mode: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- Source SHA: ${inline(operation.sourceSha)}`, `- Production SHA: ${inline(operation.productionSha ?? "pending")}`, "
"); + return lines.join("\n"); +} +function renderPromotionPullRequest(operation, context) { + const messages = messagesFor(context.pullRequestLocale); + const kind = operation.kind === "release" ? "release" : "hotfix"; + const title = `${kind}(${safeText(operation.version)}): promote to ${safeText(operation.productionBranch)}`; + const body = [ + `# 🚀 ${capitalize(messages.purposePromotion)}`, "", + `> **${messages.purpose}:** ${capitalize(messages.purposePromotion)}.`, + `> **${messages.afterMerge}:** ${messages.afterPromotion}`, "", + `| ${messages.origin} | ${messages.preparedSource} | ${messages.destination} | ${messages.publication} |`, + "|---|---|---|---|", + `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.afterMerge} |`, "", + `## ${messages.readyBeforeReview}`, "", + `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, "", + `## ${messages.afterMerge}`, "", + `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, "", + `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", + "
", `${messages.technical}`, "", + `Operation ${inline(operation.operationId)}; strategy ${inline(operation.strategy)}; merge mode ${inline(operation.prMode)}.`, + "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "promotion", issue: context.issue }), + ].join("\n"); + return { title, body }; +} +function renderReconciliationPullRequest(operation, target, context) { + const messages = messagesFor(context.pullRequestLocale); + const kind = operation.kind === "release" ? "release" : "hotfix"; + const title = `${kind}(${safeText(operation.version)}): reconcile ${safeText(target.sourceBranch)} into ${safeText(target.targetBranch)}`; + const body = [ + `# 🔄 ${capitalize(messages.purposeReconciliation)}`, "", + `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, "", + `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, "|---|---|---|", + `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === "close" ? messages.closeIssue : messages.keepIssue} |`, "", + ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ""] : []), + `${messages.noRepublish}`, "", + `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", + "
", `${messages.technical}`, "", `Operation ${inline(operation.operationId)}; source SHA ${inline(target.sourceSha)}.`, + "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "reconciliation", issue: context.issue }), + ].join("\n"); + return { title, body }; +} +function renderDeploymentJobSummary(operation, context, previousPhase, operations = []) { + const messages = messagesFor(context.issueLocale); + const externallyPending = operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"; + const result = operation.phase === "blocked" ? messages.workflowFailure : externallyPending ? messages.externalWait : messages.phase[operation.phase]; + return [ + `# ${operation.phase === "blocked" ? "❌" : externallyPending ? "⏳" : "✅"} ${messages.jobSummary}`, "", + `> **${messages.result}: ${result}.**`, "", + `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, "|---|---|---|", + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, "", + `- Operation: ${inline(operation.operationId)}`, + `- ${messages.origin}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, + `- ${messages.preparedSource}: ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)}`, + `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : "pending")}`, + `- ${messages.publication}: ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- ${messages.createdReused}: ${safeText(operations.join(", ") || "none")}`, "", + deploymentLinks(operation, context, messages).join(" · "), + ].join("\n"); +} +function progressLines(operation, messages) { + const phase = operation.phase === "blocked" ? operation.lastFailure?.previousPhase ?? "preparing" : operation.phase; + const reached = (expected) => phaseRank(phase) >= phaseRank(expected); + return [ + `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, + `- [x] ${messages.buildValidation} + ${messages.packageSmoke}`, + `- [${operation.productionSha || reached("promoted") ? "x" : " "}] ${messages.promotion}: ${inline(operation.productionBranch)}`, + `- [${operation.publicationVerified ? "x" : " "}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- [${reconciliationCompleted(operation) ? "x" : " "}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, + `- [${operation.phase === "completed" ? "x" : " "}] ${messages.cleanup}`, + ]; +} +function deploymentDiagram(messages) { + const [source, prepared, production, accepted, publication, reconciliation, complete] = messages.diagram; + return [ + "```mermaid", "flowchart LR", ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, + ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, ` B --> C[${complete}]`, "```", "", messages.fallback, + ]; +} +function transitionTable(operation, messages) { + const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + const from = activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch; + const to = activeTarget?.targetBranch ?? operation.productionBranch; + return [`| ${messages.from} | ${messages.to} | ${messages.state} |`, "|---|---|---|", `| ${inline(from)} | ${inline(to)} | ${inline(messages.phase[operation.phase])} |`]; +} +function factTable(operation, messages) { + return [ + `| ${messages.productionUpdated} | ${messages.alreadyPublished} | ${messages.developmentSynchronized} |`, "|---|---|---|", + `| ${operation.productionSha ? messages.yes : messages.no} | ${operation.publicationVerified ? messages.yes : messages.no} | ${reconciliationCompleted(operation) ? messages.yes : messages.no} |`, + ]; +} +function protectedFact(operation, messages) { + if (operation.publicationVerified) + return `${messages.alreadyPublished}; ${messages.noRepublish}`; + if (operation.productionSha) + return `${messages.productionUpdated}: ${messages.yes}. ${messages.notPublished}.`; + return `${messages.productionUpdated}: ${messages.no}. ${messages.notPublished}.`; +} +function nextDescription(operation, messages) { + if (operation.phase === "blocked") { + const diagnostic = safeText(operation.lastFailure?.message ?? messages.workflowFailure); + return `${diagnostic}. ${operation.lastFailure?.retryable ? `${messages.actionRequired}: retry after correcting the cause.` : `${messages.actionRequired}: manual intervention is required.`}`; + } + if (operation.phase === "promotion_pr_pending") + return messages.afterPromotion; + if (operation.phase === "publishing" || operation.phase === "promoted") + return messages.afterPromotion; + if (operation.phase === "reconciliation_pending" || operation.phase === "published") + return messages.noRepublish; + if (operation.phase === "completed") + return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; + return `${messages.promotion}: ${inline(operation.sourceBranch)} -> ${inline(operation.productionBranch)}.`; +} +function deploymentAction(operation, messages) { + const manual = (operation.selectedPrMode ?? operation.prMode) === "create-only" + && (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"); + if (manual) + return { required: true, message: `${messages.protectedChecks}: review and merge the managed PR when GitHub reports it ready.` }; + if (operation.phase !== "blocked") + return { required: false, message: messages.noAction }; + return { + required: true, + message: operation.lastFailure?.retryable + ? `${safeText(operation.lastFailure.message)}. Retry after correcting the cause.` + : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. Manual intervention is required.`, + }; +} +function deploymentLinks(operation, context, messages) { + const links = [`[${messages.controlCenter}](${issueUrl(context)})`]; + links.push(`[${safeText(operation.sourceBranch)} branch](${branchUrl(context, operation.sourceBranch)})`); + links.push(`[${shortSha(operation.originSha)} origin commit](${commitUrl(context, operation.originSha)})`); + links.push(`[${shortSha(operation.sourceSha)} prepared commit](${commitUrl(context, operation.sourceSha)})`); + const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + links.push(`[${messages.compare}](${compareUrl(context, activeTarget?.targetBranch ?? operation.productionBranch, activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch)})`); + if (operation.promotionPullRequest) + links.push(`[Promotion PR #${operation.promotionPullRequest}](${pullRequestUrl(context, operation.promotionPullRequest)})`); + for (const target of operation.reconciliationTargets) { + if (target.pullRequest) + links.push(`[Reconciliation PR #${target.pullRequest}](${pullRequestUrl(context, target.pullRequest)})`); + } + if (operation.productionSha) + links.push(`[${shortSha(operation.productionSha)} production commit](${commitUrl(context, operation.productionSha)})`); + if (operation.publicationVerified) { + links.push(`[${safeText(operation.tag)} GitHub Release](${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)})`); + links.push(`[v${safeText(operation.version.split(".")[0])} Action tag](${branchUrl(context, `v${operation.version.split(".")[0]}`)})`); + if (context.packageName) + links.push(`[${safeText(context.packageName)}@${safeText(operation.version)} on npm](${npmVersionUrl(context.packageName, operation.version)})`); + } + if (context.workflowRunUrl) + links.push(`[Workflow run](${safeUrl(context.workflowRunUrl)})`); + return links; +} +function messagesFor(locale) { return normalizeLocale(locale) === "es-ES" ? ES : EN; } +function normalizeLocale(locale) { return locale.toLowerCase().startsWith("es") ? "es-ES" : "en-US"; } +function reconciliationCompleted(operation) { + return operation.phase === "completed" + || (operation.reconciliationTargets.length > 0 + && operation.reconciliationTargets.every((target) => target.status === "completed")); +} +function phaseRank(phase) { return ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"].indexOf(phase); } +function repositoryUrl(context) { return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; } +function issueUrl(context) { return `${repositoryUrl(context)}/issues/${context.issue}`; } +function pullRequestUrl(context, number) { return `${repositoryUrl(context)}/pull/${number}`; } +function branchUrl(context, branch) { return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; } +function commitUrl(context, sha) { return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; } +function npmVersionUrl(packageName, version) { return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; } +function compareUrl(context, base, head) { return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; } +function inline(value) { return `\`${safeText(value)}\``; } +function safeText(value) { return value.replace(/[\r\n`<>]/g, "").replace(/@/g, "@\u200b").replace(/::/g, "﹕﹕").slice(0, 240); } +function safeMarkerValue(value) { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128); } +function safeUrl(value) { return /^https:\/\/github\.com\//.test(value) ? value : "https://github.com"; } +function shortSha(value) { return safeText(value).slice(0, 7); } +function capitalize(value) { return value.charAt(0).toUpperCase() + value.slice(1); } + + /***/ }), /***/ 72712: @@ -57398,6 +57973,7 @@ exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; const agent_1 = __nccwpck_require__(89040); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -57474,6 +58050,7 @@ function createDefaultSetupConfiguration() { issueLocale: 'en-US', pullRequestLocale: 'en-US', commitPrefixTransforms: 'replace-slash', + ...deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION, }, ai: { pullRequestDescription: true, @@ -57643,6 +58220,17 @@ function buildSetupRepositoryVariables(configuration) { add('ISSUES_LOCALE', repository.issueLocale); add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + add('RELEASE_RECONCILIATION_STRATEGY', repository.releaseReconciliationStrategy); + add('HOTFIX_RECONCILIATION_STRATEGY', repository.hotfixReconciliationStrategy); + add('RECONCILIATION_PR_MODE', repository.reconciliationPullRequestMode); + add('RECONCILIATION_BACKMERGE_MODE', repository.reconciliationBackmergeMode); + add('HOTFIX_ACTIVE_RELEASE_POLICY', repository.hotfixActiveReleasePolicy); + add('RECONCILIATION_TREE', repository.reconciliationTree); + add('RECONCILIATION_CLEANUP', repository.reconciliationCleanup); + add('RECONCILIATION_ISSUE_COMPLETION', repository.reconciliationIssueCompletion); + add('ORCHESTRATION_PRESENTATION_MODE', repository.orchestrationPresentationMode); + add('ORCHESTRATION_DIAGRAMS', repository.orchestrationDiagrams); + add('ORCHESTRATION_COMMENT_MODE', repository.orchestrationCommentMode); add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); @@ -57688,6 +58276,17 @@ function buildSetupActionInputs(configuration) { 'issues-locale': repository.issueLocale, 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'release-reconciliation-strategy': repository.releaseReconciliationStrategy, + 'hotfix-reconciliation-strategy': repository.hotfixReconciliationStrategy, + 'reconciliation-pr-mode': repository.reconciliationPullRequestMode, + 'reconciliation-backmerge-mode': repository.reconciliationBackmergeMode, + 'hotfix-active-release-policy': repository.hotfixActiveReleasePolicy, + 'reconciliation-tree': repository.reconciliationTree, + 'reconciliation-cleanup': repository.reconciliationCleanup, + 'reconciliation-issue-completion': repository.reconciliationIssueCompletion, + 'orchestration-presentation-mode': repository.orchestrationPresentationMode, + 'orchestration-diagrams': String(repository.orchestrationDiagrams), + 'orchestration-comment-mode': repository.orchestrationCommentMode, 'ai-pull-request-description': String(ai.pullRequestDescription), 'ai-pull-request-description-mode': (0, pull_request_description_1.normalizePullRequestDescriptionMode)(ai.pullRequestDescriptionMode), 'ai-ignore-files': ai.ignoreFiles, @@ -57737,6 +58336,9 @@ function buildSetupWarnings(configuration) { if (configuration.features.release !== false && configuration.features.hotfix !== false) { warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); } + if (configuration.repository.reconciliationPullRequestMode === 'merge-queue') { + warnings.push('Merge queue mode requires every required first-party and third-party check to support the merge_group event; setup can validate only the bundled Copilot bridge.'); + } if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); } @@ -57936,6 +58538,7 @@ const setup_configuration_defaults_1 = __nccwpck_require__(23381); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); function validateSetupConfiguration(configuration) { const errors = []; const nonEmpty = [ @@ -57984,6 +58587,25 @@ function validateSetupConfiguration(configuration) { if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } + errors.push(...(0, deployment_configuration_1.validateDeploymentConfiguration)({ + releaseReconciliationStrategy: configuration.repository.releaseReconciliationStrategy, + hotfixReconciliationStrategy: configuration.repository.hotfixReconciliationStrategy, + reconciliationPullRequestMode: configuration.repository.reconciliationPullRequestMode, + reconciliationBackmergeMode: configuration.repository.reconciliationBackmergeMode, + hotfixActiveReleasePolicy: configuration.repository.hotfixActiveReleasePolicy, + reconciliationTree: configuration.repository.reconciliationTree, + reconciliationCleanup: configuration.repository.reconciliationCleanup, + reconciliationIssueCompletion: configuration.repository.reconciliationIssueCompletion, + orchestrationPresentationMode: configuration.repository.orchestrationPresentationMode, + orchestrationDiagrams: configuration.repository.orchestrationDiagrams, + orchestrationCommentMode: configuration.repository.orchestrationCommentMode, + }, { + productionBranch: configuration.repository.mainBranch, + developmentBranch: configuration.repository.developmentBranch, + releaseTree: configuration.repository.releaseTree, + hotfixTree: configuration.repository.hotfixTree, + mergeQueueWorkflowSupported: true, + })); errors.push(...(0, setup_configuration_storage_policy_1.validateStorageConfiguration)(configuration.storage)); for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; @@ -58712,11 +59334,16 @@ exports.runCreateRelease = runCreateRelease; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); const create_release_policy_1 = __nccwpck_require__(76549); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runCreateRelease(param, taskId, repositoryReleasePort) { + const operation = param.currentConfiguration.deploymentOrchestration; + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(operation, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return [failureResult(taskId, continuationError)]; const input = { - version: param.singleAction.version, - title: param.singleAction.title, - changelog: param.singleAction.changelog, + version: param.singleAction.version || operation?.version || '', + title: param.singleAction.title || operation?.title || '', + changelog: param.singleAction.changelog || operation?.changelog || '', }; const validationError = (0, create_release_policy_1.validateReleaseInput)(input); if (validationError) { @@ -58790,13 +59417,18 @@ exports.runCreateTag = runCreateTag; const result_1 = __nccwpck_require__(73817); const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runCreateTag(param, taskId, repositoryTagPort) { const validationFailure = validateTagInput(param, taskId); if (validationFailure) return [validationFailure]; - const tagName = `v${param.singleAction.version}`; + const operation = param.currentConfiguration.deploymentOrchestration; + const version = param.singleAction.version || operation?.version || ''; + const tagName = `v${version}`; try { - const sha1Tag = await repositoryTagPort.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token); + const sha1Tag = operation?.productionSha + ? await repositoryTagPort.createOrVerifyTagAtSha(param.owner, param.repo, operation.productionSha, tagName, param.tokens.token) + : await repositoryTagPort.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token); return sha1Tag ? [new result_1.Result({ id: taskId, success: true, executed: true, steps: [`Tag ${tagName} is ready: ${sha1Tag}`] })] : noTagResult(taskId, tagName); } @@ -58806,11 +59438,15 @@ async function runCreateTag(param, taskId, repositoryTagPort) { } } function validateTagInput(param, taskId) { - if (param.singleAction.version.length === 0) { + const operation = param.currentConfiguration.deploymentOrchestration; + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(operation, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [continuationError] }); + if (param.singleAction.version.length === 0 && !operation?.version) { (0, logging_ports_1.logError)('Version is not set.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } - if (param.currentConfiguration.releaseBranch === undefined) { + if (!operation?.productionSha && param.currentConfiguration.releaseBranch === undefined) { (0, logging_ports_1.logError)('Working branch not found in configuration.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: ['Release branch not found in issue configuration.'] }); } @@ -58971,6 +59607,591 @@ function mergeFailureResult(param, mergesAttempted) { } +/***/ }), + +/***/ 36850: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentOrchestrationUseCase = void 0; +const deployment_plan_policy_1 = __nccwpck_require__(8352); +const deployment_presentation_policy_1 = __nccwpck_require__(83221); +const deployment_operation_1 = __nccwpck_require__(92730); +const managed_pull_request_1 = __nccwpck_require__(95914); +const result_1 = __nccwpck_require__(73817); +const deployment_lifecycle_policy_1 = __nccwpck_require__(54037); +const TASK_ID = "DeploymentOrchestrationUseCase"; +class DeploymentOrchestrationUseCase { + constructor(dependencies) { + this.dependencies = dependencies; + this.taskId = TASK_ID; + this.checkpoints = new WeakMap(); + } + async invoke(execution) { + const initial = execution.currentConfiguration.deploymentOrchestration; + this.checkpoints.set(execution, initial ? { operationId: initial.operationId, phase: initial.phase } : undefined); + try { + if (execution.singleAction.isPrepareDeploymentAction) + return [await this.prepare(execution)]; + if (execution.singleAction.isDeployedAction && initial) + return [await this.published(execution)]; + if (execution.singleAction.isContinueDeploymentAction) + return [await this.continue(execution)]; + if (execution.singleAction.isPublishedDeploymentAction) + return [await this.published(execution)]; + if (execution.singleAction.isFailedDeploymentAction) + return [await this.failed(execution)]; + return []; + } + catch (error) { + await this.recordUnexpectedFailure(execution, error); + return [new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: ["Deployment orchestration is blocked. No unsafe transition was performed."], + errors: [error], + })]; + } + } + async prepare(execution) { + const existing = execution.currentConfiguration.deploymentOrchestration; + if (existing) { + if (existing.version !== execution.singleAction.version) { + throw new Error(`Issue already owns deployment operation ${existing.operationId} for version ${existing.version}.`); + } + if (existing.phase === "blocked" + && (!existing.lastFailure?.retryable + || !["preparing", "promotion_pr_pending"].includes(existing.lastFailure.previousPhase))) { + await this.publishDashboard(execution, existing); + return blockedResult(existing, "The prepare mode cannot resume this blocked deployment phase."); + } + const resumed = existing.phase === "blocked" ? (0, deployment_operation_1.resumeBlockedDeployment)(existing) : undefined; + const current = resumed?.kind === "advance" ? resumed.operation : existing; + if (current !== existing) { + execution.currentConfiguration.deploymentOrchestration = current; + await this.persist(execution); + } + if (current.phase === "preparing" || current.phase === "promotion_pr_pending") { + const currentSourceSha = await this.dependencies.git.getBranchSha(execution.owner, execution.repo, current.sourceBranch, execution.tokens.token); + if (currentSourceSha !== current.sourceSha) { + return await this.block(execution, current, "promotion", "The prepared source branch changed after its immutable SHA was stored.", false); + } + return await this.ensurePromotion(execution, current); + } + await this.publishDashboard(execution, current); + return success(`Deployment ${current.operationId} is already ${current.phase}; reused its durable state.`); + } + const kind = deploymentKind(execution); + const sourceBranch = kind === "release" + ? execution.currentConfiguration.releaseBranch + : execution.currentConfiguration.hotfixBranch; + if (!sourceBranch) + throw new Error(`No prepared ${kind} branch is stored on the launcher issue.`); + const sourceSha = await this.dependencies.git.getBranchSha(execution.owner, execution.repo, sourceBranch, execution.tokens.token); + const originBranch = kind === "release" + ? execution.currentConfiguration.releaseOriginBranch ?? execution.branches.development + : execution.currentConfiguration.hotfixOriginBranch ?? execution.currentConfiguration.parentBranch ?? execution.branches.defaultBranch; + const persistedOrigin = kind === "release" + ? execution.currentConfiguration.releaseOriginSha + : execution.currentConfiguration.hotfixOriginSha; + const originSha = persistedOrigin ?? await this.dependencies.git.getMergeBaseSha(execution.owner, execution.repo, originBranch, sourceBranch, execution.tokens.token); + const operation = (0, deployment_plan_policy_1.buildInitialDeploymentOperation)({ + operationId: this.dependencies.operationId(), + kind, + version: execution.singleAction.version, + title: execution.singleAction.title, + changelog: execution.singleAction.changelog, + sourceBranch, + sourceSha, + originBranch, + originSha, + productionBranch: execution.branches.defaultBranch, + developmentBranch: execution.branches.development, + configuration: execution.deployment, + publicationWorkflow: kind === "release" ? execution.workflows.release : execution.workflows.hotfix, + }); + const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ + operationId: operation.operationId, + kind, + version: operation.version, + title: operation.title, + changelog: operation.changelog, + sourceBranch, + sourceSha, + originBranch, + originSha, + productionBranch: operation.productionBranch, + developmentBranch: operation.developmentBranch, + configuration: execution.deployment, + publicationWorkflow: operation.publicationWorkflow, + }); + if (errors.length > 0) + throw new Error(errors.join(" ")); + execution.currentConfiguration.deploymentOrchestration = operation; + if (kind === "release") { + execution.currentConfiguration.releaseOriginBranch = originBranch; + execution.currentConfiguration.releaseOriginSha = originSha; + } + else { + execution.currentConfiguration.hotfixOriginSha = originSha; + } + await this.persist(execution); + await this.publishDashboard(execution, operation); + return await this.ensurePromotion(execution, operation); + } + async ensurePromotion(execution, operation) { + const promotion = await this.createOrReusePullRequest(execution, operation, "promotion"); + if (promotion.merged) + return await this.advancePromotion(execution, operation, promotion); + if (promotion.state === "closed") + return await this.block(execution, operation, "promotion", `Promotion PR #${promotion.number} was closed without merge.`, true); + if (promotion.headSha !== operation.sourceSha) { + return await this.block(execution, operation, "promotion", `Promotion PR #${promotion.number} does not contain the persisted prepared SHA.`, false); + } + const pending = operation.phase === "promotion_pr_pending" + ? { ...operation, promotionPullRequest: promotion.number } + : (0, deployment_operation_1.transitionDeploymentOperation)({ ...operation, promotionPullRequest: promotion.number }, "preparing", "promotion_pr_pending").operation; + if (pending.phase !== "promotion_pr_pending") + throw new Error(`Cannot prepare promotion from ${operation.phase}.`); + execution.currentConfiguration.deploymentOrchestration = pending; + await this.persist(execution); + const managed = await this.configureMergeBehavior(execution, pending, promotion); + return success(managed.selectedPrMode === "create-only" + ? `Promotion PR #${promotion.number} is ready for maintainer review; this runner does not wait.` + : `Promotion PR #${promotion.number} is managed by GitHub; this runner does not wait for checks.`); + } + async continue(execution) { + let operation = requireOperation(execution); + const pullRequestNumber = execution.pullRequest.number; + if (pullRequestNumber < 1) + throw new Error("The continuation event has no pull request number."); + const pullRequest = await this.dependencies.pullRequests.getPullRequest(execution.owner, execution.repo, pullRequestNumber, execution.tokens.token); + const identity = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body); + if (!identity || identity.operationId !== operation.operationId || identity.issue !== execution.singleAction.issue) { + throw new Error(`PR #${pullRequest.number} is not owned by deployment operation ${operation.operationId}.`); + } + if (operation.phase === "blocked") { + const previousPhase = operation.lastFailure?.previousPhase; + const eventCanResume = operation.lastFailure?.retryable === true + && (identity.phase === "promotion" + ? previousPhase === "preparing" || previousPhase === "promotion_pr_pending" + : previousPhase === "reconciliation_pending"); + if (!eventCanResume) { + await this.publishDashboard(execution, operation); + return success(`PR #${pullRequest.number} cannot resume the existing ${operation.lastFailure?.category ?? "deployment"} block; the original diagnosis was preserved.`); + } + const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); + if (resumed.kind === "advance") { + operation = resumed.operation; + execution.currentConfiguration.deploymentOrchestration = operation; + await this.persist(execution); + } + } + if (pullRequest.repositoryFullName.toLowerCase() !== `${execution.owner}/${execution.repo}`.toLowerCase()) { + throw new Error("Cross-repository deployment continuation was rejected."); + } + if (pullRequest.state !== "closed") + return success(`PR #${pullRequest.number} is still open; no transition was applied.`); + if (!pullRequest.merged) { + return await this.block(execution, operation, identity.phase === "promotion" ? "promotion" : "reconciliation", `Managed ${identity.phase} PR #${pullRequest.number} was closed without merge.`, true); + } + if (identity.phase === "promotion") + return await this.advancePromotion(execution, operation, pullRequest); + return await this.advanceReconciliation(execution, operation, pullRequest); + } + async advancePromotion(execution, operation, pullRequest) { + if (["promoted", "publishing", "published", "reconciliation_pending", "completed"].includes(operation.phase)) { + return success(`Duplicate promotion event for PR #${pullRequest.number} was ignored; operation is ${operation.phase}.`); + } + if (operation.phase !== "promotion_pr_pending" && operation.phase !== "preparing") { + return success(`Out-of-order promotion event was ignored while operation is ${operation.phase}.`); + } + if (pullRequest.headBranch !== operation.sourceBranch || pullRequest.baseBranch !== operation.productionBranch || pullRequest.headSha !== operation.sourceSha) { + return await this.block(execution, operation, "promotion", "Promotion PR branches or prepared SHA do not match durable state.", false); + } + const productionSha = pullRequest.mergeCommitSha; + if (!productionSha) + return await this.block(execution, operation, "promotion", "Merged promotion PR has no production merge SHA.", true); + const [mergeReachable, sourceReachable] = await Promise.all([ + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, productionSha, execution.tokens.token), + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, operation.sourceSha, execution.tokens.token), + ]); + if (!mergeReachable || !sourceReachable) { + return await this.block(execution, operation, "promotion", "GitHub does not confirm that the accepted production branch contains the promotion commit.", true); + } + let promoted = { ...operation, promotionPullRequest: pullRequest.number, productionSha, phase: "promoted", lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = promoted; + await this.persist(execution); + promoted = { ...promoted, phase: "publishing" }; + execution.currentConfiguration.deploymentOrchestration = promoted; + await this.persist(execution); + await this.publishDashboard(execution, promoted); + await this.publishMilestone(execution, promoted, "promotion-merged", `✅ Promotion PR #${pullRequest.number} merged. Publication is starting from production SHA \`${productionSha}\`.`); + await this.dependencies.continuation.dispatch(execution.owner, execution.repo, operation.publicationWorkflow, operation.productionBranch, operation.operationId, execution.singleAction.issue, operation.version, execution.tokens.token); + return success(`Promotion PR #${pullRequest.number} was verified; publication continuation was dispatched from ${operation.productionBranch}.`); + } + async published(execution) { + let operation = requireOperation(execution); + if (operation.phase === "blocked" && operation.lastFailure?.retryable) { + const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); + if (resumed.kind === "advance") { + operation = resumed.operation; + execution.currentConfiguration.deploymentOrchestration = operation; + await this.persist(execution); + } + } + if (operation.phase === "reconciliation_pending" && operation.publicationVerified) { + return await this.ensureNextReconciliation(execution, operation) + ?? success(`Publication for ${operation.tag} was already verified; reconciliation state was recovered.`); + } + if (operation.phase === "completed" && operation.publicationVerified) { + await this.publishDashboard(execution, operation); + return success(`Publication for ${operation.tag} was already verified; duplicate notification ignored.`); + } + if (operation.phase !== "published" && operation.phase !== "publishing" && operation.phase !== "promoted") { + throw new Error(`Publication cannot advance from phase ${operation.phase}.`); + } + if (!operation.productionSha) + throw new Error("The accepted production SHA is missing."); + const reachable = await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, operation.productionSha, execution.tokens.token); + if (!reachable) + return await this.block(execution, operation, "publication", "Published SHA is not reachable from the stored production branch.", false); + let published = { ...operation, phase: "published", publicationVerified: true, lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = published; + await this.persist(execution); + await this.publishMilestone(execution, published, "publication-complete", `📦 ${published.tag} is published from accepted production SHA \`${published.productionSha}\`.`); + const activeReleases = operation.kind === "hotfix" + ? (await this.dependencies.git.listBranches(execution.owner, execution.repo, execution.branches.releaseTree, execution.tokens.token)) + .filter((branch) => branch !== operation.sourceBranch) + : []; + const decision = (0, deployment_plan_policy_1.selectReconciliationTargetBranches)(published, activeReleases); + if (decision.kind === "blocked") + return await this.block(execution, published, "reconciliation", decision.reason, false); + if (decision.kind === "manual") { + await this.publishDashboard(execution, published); + return success(`${published.tag} is published. Manual reconciliation is configured, so the issue remains open.`); + } + published = { + ...published, + reconciliationTargets: decision.targetBranches.map((target) => (0, deployment_plan_policy_1.buildReconciliationTarget)(published, target, "direct")), + phase: "reconciliation_pending", + }; + execution.currentConfiguration.deploymentOrchestration = published; + await this.persist(execution); + return await this.ensureNextReconciliation(execution, published) + ?? success(`${published.tag} is published; development reconciliation is now managed by GitHub.`); + } + async failed(execution) { + const operation = requireOperation(execution); + if (operation.phase === "completed") + return success(`Deployment ${operation.operationId} is already complete; a stale failure report was ignored.`); + if (operation.phase === "blocked") { + await this.publishDashboard(execution, operation); + return new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: [`Deployment ${operation.operationId} remains blocked; its original failure classification was preserved.`], + errors: [new Error(operation.lastFailure?.message ?? "Deployment remains blocked.")], + }); + } + const category = operation.phase === "preparing" || operation.phase === "promotion_pr_pending" + ? "promotion" + : operation.phase === "promoted" || operation.phase === "publishing" + ? "publication" + : operation.lastFailure?.category ?? "reconciliation"; + const message = execution.singleAction.message || `The ${category} workflow failed. Review the linked workflow run before retrying.`; + return await this.block(execution, operation, category, message, true); + } + async advanceReconciliation(execution, operation, pullRequest) { + if (operation.phase === "completed") + return success(`Duplicate reconciliation event for PR #${pullRequest.number} was ignored.`); + if (operation.phase !== "reconciliation_pending") + return success(`Out-of-order reconciliation event ignored while operation is ${operation.phase}.`); + const target = operation.reconciliationTargets.find((item) => item.pullRequest === pullRequest.number); + if (!target) + return await this.block(execution, operation, "reconciliation", `PR #${pullRequest.number} is not a configured reconciliation target.`, false); + if (pullRequest.baseBranch !== target.targetBranch || pullRequest.headBranch !== (target.syncBranch ?? target.sourceBranch)) { + return await this.block(execution, operation, "reconciliation", "Reconciliation PR branches do not match durable state.", false); + } + const mergeSha = pullRequest.mergeCommitSha; + if (!mergeSha || !(await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.targetBranch, mergeSha, execution.tokens.token))) { + return await this.block(execution, operation, "reconciliation", "The reconciliation merge is not reachable from its target branch.", true); + } + if (!(await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.targetBranch, target.sourceSha, execution.tokens.token))) { + return await this.block(execution, operation, "reconciliation", "The reconciliation target does not contain the stored release SHA.", false); + } + const updated = (0, deployment_operation_1.completeReconciliationTarget)(operation, pullRequest.number); + execution.currentConfiguration.deploymentOrchestration = updated; + await this.persist(execution); + if (!updated.reconciliationTargets.every((item) => item.status === "completed")) { + return await this.ensureNextReconciliation(execution, updated) + ?? success(`Reconciliation PR #${pullRequest.number} completed; the next configured target is ready.`); + } + return await this.finalizeReconciliation(execution, updated, ` after reconciliation PR #${pullRequest.number}`); + } + async finalizeReconciliation(execution, operation, completionContext = "") { + try { + await this.cleanup(execution, operation); + if (operation.issueCompletion === "close") { + await this.dependencies.issues.closeIssue(execution.owner, execution.repo, execution.singleAction.issue, execution.tokens.token); + } + } + catch (error) { + const message = error instanceof Error ? error.message : String(error); + return await this.block(execution, operation, "cleanup", message, true); + } + const completed = { ...operation, phase: "completed", lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = completed; + await this.persist(execution); + await this.publishDashboard(execution, completed); + await this.publishMilestone(execution, completed, "orchestration-complete", `✅ Deployment ${completed.tag} and every configured reconciliation target are complete.`); + return success(`Deployment ${completed.tag} completed${completionContext}.`); + } + async ensureNextReconciliation(execution, operation) { + const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending" && target.pullRequest === undefined); + if (index < 0) { + if (operation.reconciliationTargets.length > 0 + && operation.reconciliationTargets.every((target) => target.status === "completed")) { + return await this.finalizeReconciliation(execution, operation); + } + await this.publishDashboard(execution, operation); + return; + } + let target = operation.reconciliationTargets[index]; + const capabilities = await this.dependencies.pullRequests.getTargetCapabilities(execution.owner, execution.repo, target.targetBranch, execution.tokens.token); + const [targetSha, currentSourceSha] = await Promise.all([ + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.targetBranch, execution.tokens.token), + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.sourceBranch, execution.tokens.token), + ]); + const directUpToDate = await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.sourceBranch, targetSha, execution.tokens.token).catch(() => false); + const mode = (0, deployment_plan_policy_1.selectBackmergeMode)(operation.backmergeMode, capabilities.requiresStrictStatusChecks, directUpToDate, currentSourceSha === target.sourceSha); + if (mode.kind === "unsupported") { + return await this.block(execution, operation, "reconciliation", mode.reason, false); + } + if (mode.mode === "sync-branch") { + target = (0, deployment_plan_policy_1.buildReconciliationTarget)(operation, target.targetBranch, "sync-branch"); + await this.dependencies.git.createOrVerifyBranch(execution.owner, execution.repo, target.syncBranch, targetSha, execution.tokens.token); + await this.dependencies.git.mergeCommitIntoBranch(execution.owner, execution.repo, target.syncBranch, targetSha, execution.tokens.token); + await this.dependencies.git.mergeCommitIntoBranch(execution.owner, execution.repo, target.syncBranch, target.sourceSha, execution.tokens.token); + } + const operationWithMode = replaceTarget(operation, index, target); + const pullRequest = await this.createOrReusePullRequest(execution, operationWithMode, "reconciliation", target); + if (pullRequest.state === "closed" && !pullRequest.merged) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} was closed without merge.`, true); + } + if (target.syncBranch) { + const [syncHead, sourceIncluded] = await Promise.all([ + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.syncBranch, execution.tokens.token), + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.syncBranch, target.sourceSha, execution.tokens.token), + ]); + if (pullRequest.headSha !== syncHead || !sourceIncluded) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} does not contain the verified sync-branch state.`, false); + } + } + else if (pullRequest.headSha !== target.sourceSha) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} source moved away from the stored release SHA.`, false); + } + const withPullRequest = replaceTarget(operationWithMode, index, { ...target, pullRequest: pullRequest.number }); + execution.currentConfiguration.deploymentOrchestration = withPullRequest; + await this.persist(execution); + if (pullRequest.merged) { + return await this.advanceReconciliation(execution, withPullRequest, pullRequest); + } + await this.configureMergeBehavior(execution, withPullRequest, pullRequest); + return undefined; + } + async createOrReusePullRequest(execution, operation, phase, target) { + const headBranch = target?.syncBranch ?? target?.sourceBranch ?? operation.sourceBranch; + const baseBranch = target?.targetBranch ?? operation.productionBranch; + const query = { + owner: execution.owner, + repository: execution.repo, + operationId: operation.operationId, + phase, + issue: execution.singleAction.issue, + headBranch, + baseBranch, + token: execution.tokens.token, + }; + const existing = await this.dependencies.pullRequests.findManagedPullRequests(query); + if (existing.length > 1) + throw new Error(`Multiple managed ${phase} PRs match operation ${operation.operationId}.`); + if (existing[0]) + return existing[0]; + const context = presentationContext(execution); + const content = phase === "promotion" + ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, context) + : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, target, context); + return await this.dependencies.pullRequests.createManagedPullRequest({ ...query, ...content }); + } + async configureMergeBehavior(execution, operation, pullRequest) { + const capabilities = await this.dependencies.pullRequests.getTargetCapabilities(execution.owner, execution.repo, pullRequest.baseBranch, execution.tokens.token, pullRequest.number); + const decision = (0, deployment_plan_policy_1.selectPullRequestMode)(operation.prMode, capabilities); + if (decision.kind === "unsupported") + throw new Error(decision.reason); + const managed = { ...operation, selectedPrMode: decision.mode }; + execution.currentConfiguration.deploymentOrchestration = managed; + await this.persist(execution); + await this.publishDashboard(execution, managed); + if (decision.mode === "auto-merge") { + if (operation.prMode === "auto" && capabilities.immediatelyMergeable) { + await this.dependencies.pullRequests.mergePullRequest(execution.owner, execution.repo, pullRequest.number, execution.tokens.token); + } + else { + await this.dependencies.pullRequests.enableAutoMerge(execution.owner, execution.repo, pullRequest.nodeId, execution.tokens.token); + } + } + else if (decision.mode === "merge-queue") { + await this.dependencies.pullRequests.enqueuePullRequest(execution.owner, execution.repo, pullRequest.nodeId, execution.tokens.token); + } + else if (decision.mode === "legacy-wait") { + await this.dependencies.legacyMerge.waitAndMerge(execution.owner, execution.repo, pullRequest.headBranch, pullRequest.number, pullRequest.baseBranch, execution.pullRequest.mergeTimeout, execution.tokens.token); + } + return managed; + } + async cleanup(execution, operation) { + const deleteSource = operation.cleanup === "all" || operation.cleanup === "source-only"; + const deleteSync = operation.cleanup === "all" || operation.cleanup === "sync-only"; + if (deleteSync) { + for (const target of operation.reconciliationTargets) { + if (target.syncBranch) + await this.dependencies.git.deleteBranch(execution.owner, execution.repo, target.syncBranch, execution.tokens.token); + } + } + if (deleteSource) + await this.dependencies.git.deleteBranch(execution.owner, execution.repo, operation.sourceBranch, execution.tokens.token); + } + async projectDeploymentLabels(execution, operation) { + const labels = await this.dependencies.labels.getLabels(execution.owner, execution.repo, execution.singleAction.issue, execution.tokens.token); + const next = (0, deployment_lifecycle_policy_1.projectDeploymentLabels)(labels, operation, execution.labels); + if (next.join("\0") !== labels.join("\0")) { + await this.dependencies.labels.setLabels(execution.owner, execution.repo, execution.singleAction.issue, next, execution.tokens.token); + } + } + async block(execution, operation, category, message, retryable) { + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); + execution.currentConfiguration.deploymentOrchestration = blocked; + await this.persist(execution); + await this.publishDashboard(execution, blocked); + await this.publishMilestone(execution, blocked, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.message}`); + return new result_1.Result({ id: TASK_ID, success: false, executed: true, steps: [message], errors: [new Error(message)] }); + } + async persist(execution) { + const expected = this.checkpoints.get(execution); + const query = { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + token: execution.tokens.token, + }; + const actual = await this.dependencies.state.load(query); + if (!sameCheckpoint(actual, expected)) { + throw new Error("Concurrent deployment state change detected; reload the launcher issue and retry."); + } + await this.dependencies.state.save({ ...query, state: execution.currentConfiguration }); + const operation = execution.currentConfiguration.deploymentOrchestration; + this.checkpoints.set(execution, operation ? { operationId: operation.operationId, phase: operation.phase } : undefined); + if (operation) + await this.projectDeploymentLabels(execution, operation); + } + async publishDashboard(execution, operation) { + const marker = (0, deployment_presentation_policy_1.deploymentDashboardMarker)(operation.operationId, execution.singleAction.issue); + const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(execution)); + const current = await this.dependencies.presentation.findDashboard(execution.owner, execution.repo, execution.singleAction.issue, marker, execution.tokens.token); + if (current) + await this.dependencies.presentation.updateDashboard(execution.owner, execution.repo, execution.singleAction.issue, current.id, body, execution.tokens.token); + else + await this.dependencies.presentation.createDashboard(execution.owner, execution.repo, execution.singleAction.issue, body, execution.tokens.token); + } + async publishMilestone(execution, operation, name, body) { + if (operation.commentMode !== "milestones") + return; + const marker = ``; + await this.dependencies.presentation.publishMilestone(execution.owner, execution.repo, execution.singleAction.issue, marker, body, execution.tokens.token); + } + async recordUnexpectedFailure(execution, error) { + const operation = execution.currentConfiguration.deploymentOrchestration; + if (!operation || operation.phase === "completed" || operation.phase === "blocked") + return; + const message = error instanceof Error ? error.message : String(error); + const category = operation.phase === "preparing" || operation.phase === "promotion_pr_pending" + ? "promotion" + : operation.phase === "promoted" || operation.phase === "publishing" + ? "publication" + : "reconciliation"; + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, true); + execution.currentConfiguration.deploymentOrchestration = blocked; + try { + await this.persist(execution); + await this.publishDashboard(execution, blocked); + } + catch { + // Preserve the original provider failure returned by invoke. + } + } +} +exports.DeploymentOrchestrationUseCase = DeploymentOrchestrationUseCase; +function requireOperation(execution) { + const operation = execution.currentConfiguration.deploymentOrchestration; + if (!operation) + throw new Error("No durable deployment operation exists on the launcher issue."); + if (!execution.singleAction.operationId && !execution.singleAction.isDeployedAction) { + throw new Error("single-action-operation-id is required for a durable deployment continuation."); + } + if (execution.singleAction.operationId && execution.singleAction.operationId !== operation.operationId) { + throw new Error(`Deployment operation mismatch: expected ${operation.operationId}, received ${execution.singleAction.operationId}.`); + } + if ((execution.singleAction.isPublishedDeploymentAction || execution.singleAction.isFailedDeploymentAction) + && execution.singleAction.version !== operation.version) { + throw new Error(`Deployment version mismatch: expected ${operation.version}, received ${execution.singleAction.version || "empty"}.`); + } + return operation; +} +function deploymentKind(execution) { + if (execution.currentConfiguration.hotfixBranch && !execution.currentConfiguration.releaseBranch) + return "hotfix"; + if (execution.currentConfiguration.releaseBranch && !execution.currentConfiguration.hotfixBranch) + return "release"; + if (execution.labels.isHotfix) + return "hotfix"; + if (execution.labels.isRelease) + return "release"; + throw new Error("The launcher issue does not identify exactly one release or hotfix source branch."); +} +function presentationContext(execution) { + return { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + issueLocale: execution.locale.issue, + pullRequestLocale: execution.locale.pullRequest, + packageName: execution.owner === "vypdev" && execution.repo === "copilot" ? "@vypdev/copilot" : undefined, + }; +} +function replaceTarget(operation, index, target) { + return { + ...operation, + reconciliationTargets: operation.reconciliationTargets.map((current, currentIndex) => currentIndex === index ? target : current), + }; +} +function success(step) { + return new result_1.Result({ id: TASK_ID, success: true, executed: true, steps: [step] }); +} +function blockedResult(operation, fallback) { + const message = operation.lastFailure?.message ?? fallback; + return new result_1.Result({ id: TASK_ID, success: false, executed: true, steps: [message], errors: [new Error(message)] }); +} +function sameCheckpoint(actual, expected) { + if (!actual || !expected) + return actual === undefined && expected === undefined; + return actual.operationId === expected.operationId && actual.phase === expected.phase; +} + + /***/ }), /***/ 38575: @@ -59584,11 +60805,13 @@ exports.runPublishGithubAction = runPublishGithubAction; const result_1 = __nccwpck_require__(73817); const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runPublishGithubAction(param, taskId, repositoryTagPort, repositoryReleasePort) { const validationFailure = validateVersion(param, taskId); if (validationFailure) return [validationFailure]; - const sourceTag = `v${param.singleAction.version}`; + const version = param.singleAction.version || param.currentConfiguration?.deploymentOrchestration?.version || ''; + const sourceTag = `v${version}`; const targetTag = sourceTag.split('.')[0]; try { await repositoryTagPort.updateTag(param.owner, param.repo, sourceTag, targetTag, param.tokens.token); @@ -59607,7 +60830,10 @@ async function runPublishGithubAction(param, taskId, repositoryTagPort, reposito } } function validateVersion(param, taskId) { - if (param.singleAction.version.length > 0) + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(param.currentConfiguration?.deploymentOrchestration, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [continuationError] }); + if (param.singleAction.version.length > 0 || param.currentConfiguration?.deploymentOrchestration?.version) return undefined; (0, logging_ports_1.logError)('Version is not set.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); @@ -61140,6 +62366,10 @@ async function runSetupExecution(execution, dependencies) { if (await (0, resolve_execution_issue_number_1.resolveExecutionIssueNumber)(execution, dependencies.issueSetupPort) === undefined) return; execution.previousConfiguration = await loadPreviousConfiguration(execution, dependencies.configurationPort); + execution.currentConfiguration.deploymentOrchestration = execution.previousConfiguration?.deploymentOrchestration; + execution.currentConfiguration.releaseOriginBranch = execution.previousConfiguration?.releaseOriginBranch; + execution.currentConfiguration.releaseOriginSha = execution.previousConfiguration?.releaseOriginSha; + execution.currentConfiguration.hotfixOriginSha = execution.previousConfiguration?.hotfixOriginSha; await loadIssueLabels(execution, dependencies.issueSetupPort); execution.release.active = execution.labels.isRelease; execution.hotfix.active = execution.labels.isHotfix; @@ -61958,7 +63188,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); const single_action_workflow_1 = __nccwpck_require__(6130); class SingleActionUseCase { - constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase, actorAuthorizationPort, publishIssueCommentUseCase, observeBranchSyncUseCase) { + constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase, actorAuthorizationPort, publishIssueCommentUseCase, observeBranchSyncUseCase, deploymentOrchestrationUseCase) { this.deployedActionUseCase = deployedActionUseCase; this.publishGithubActionUseCase = publishGithubActionUseCase; this.createReleaseUseCase = createReleaseUseCase; @@ -61972,6 +63202,7 @@ class SingleActionUseCase { this.actorAuthorizationPort = actorAuthorizationPort; this.publishIssueCommentUseCase = publishIssueCommentUseCase; this.observeBranchSyncUseCase = observeBranchSyncUseCase; + this.deploymentOrchestrationUseCase = deploymentOrchestrationUseCase; this.taskId = "SingleActionUseCase"; } async invoke(param) { @@ -62000,6 +63231,7 @@ class SingleActionUseCase { closeInactiveIssuesUseCase: this.closeInactiveIssuesUseCase, publishIssueCommentUseCase: this.publishIssueCommentUseCase, observeBranchSyncUseCase: this.observeBranchSyncUseCase, + deploymentOrchestrationUseCase: this.deploymentOrchestrationUseCase, }); } } @@ -62030,6 +63262,10 @@ async function runSingleActionWorkflow(param, taskId, ports) { } (0, logging_ports_1.logDebugInfo)(`SingleAction: dispatching to handler for action: ${param.singleAction.currentSingleAction}.`); const action = [ + { + active: param.singleAction.isDeployedAction && Boolean(param.currentConfiguration.deploymentOrchestration), + useCase: ports.deploymentOrchestrationUseCase, + }, { active: param.singleAction.isDeployedAction, useCase: ports.deployedActionUseCase }, { active: param.singleAction.isPublishGithubAction, useCase: ports.publishGithubActionUseCase }, { active: param.singleAction.isCreateReleaseAction, useCase: ports.createReleaseUseCase }, @@ -62042,6 +63278,7 @@ async function runSingleActionWorkflow(param, taskId, ports) { { active: param.singleAction.isCloseInactiveIssuesAction, useCase: ports.closeInactiveIssuesUseCase }, { active: param.singleAction.isPublishIssueCommentAction, useCase: ports.publishIssueCommentUseCase }, { active: param.singleAction.isCheckBranchSyncAction, useCase: ports.observeBranchSyncUseCase }, + { active: param.singleAction.isDeploymentOrchestrationAction, useCase: ports.deploymentOrchestrationUseCase }, ].find(({ active, useCase }) => active && useCase !== undefined); if (!action || !action.useCase) return []; @@ -67873,6 +69110,8 @@ async function prepareHotfixBranch(param, commitTagQuery, linkedBranchCommand, b const lastAction = linkResult.at(-1); if (!lastAction?.success) return linkResult; + if (branchOid) + param.currentConfiguration.hotfixOriginSha = branchOid; (0, logging_ports_1.logDebugInfo)(`Hotfix branch successfully linked to issue: ${JSON.stringify(linkResult)}`); return [ new result_1.Result({ @@ -68016,6 +69255,7 @@ async function prepareReleaseBranch(param, linkedBranchCommand, branches, taskId if (!lastAction?.success) return linkResult; const branchName = (0, result_1.getResultPayload)(lastAction.payload)?.newBranchName; + const baseSha = (0, result_1.getResultPayload)(lastAction.payload)?.baseSha; if (typeof branchName !== "string" || branchName.length === 0) { return [ new result_1.Result({ @@ -68026,16 +69266,18 @@ async function prepareReleaseBranch(param, linkedBranchCommand, branches, taskId }), ]; } + if (typeof baseSha === "string" && baseSha.length > 0) { + param.currentConfiguration.releaseOriginBranch = param.branches.development; + param.currentConfiguration.releaseOriginSha = baseSha; + } const fence = "```"; - const inlineCode = "`"; const reminders = [ `Before deploying, apply any change needed in [**${release.branch}**](${releaseUrl}):\n> ${fence}bash\n> git fetch -v && git checkout ${release.branch}\n> ${fence}\n>\n> Version files, changelogs..`, ]; const commitPrefix = await buildConfiguredCommitPrefix(param, branchName); if (commitPrefix) reminders.push(`Commit the needed changes with this prefix:\n> ${fence}\n>${commitPrefix}\n> ${fence}`); - reminders.push(`Create the tag version in [**${release.branch}**](${releaseUrl}).\n> Avoid using ${inlineCode}git merge --squash${inlineCode}, otherwise the created tag will be lost.`); - reminders.push(`Add the **${param.labels.deploy}** label to run the ${inlineCode}${param.workflows.release}${inlineCode} workflow.`); + reminders.push(`Add the **${param.labels.deploy}** label to run the \`${param.workflows.release}\` workflow. Copilot will create the immutable version tag only after the production promotion PR merges.`); reminders.push(buildReleaseReminder(param, releaseUrl, developmentUrl, mainUrl)); (0, logging_ports_1.logDebugInfo)(`Release branch successfully linked to issue: ${JSON.stringify(linkResult)}`); return [ @@ -68058,8 +69300,7 @@ async function buildConfiguredCommitPrefix(param, branchName) { } function buildReleaseReminder(param, releaseUrl, developmentUrl, mainUrl) { const branch = param.release.branch; - const inlineCode = "`"; - return `After deploying, the new changes on [${inlineCode}${branch}${inlineCode}](${releaseUrl}) must end on [${inlineCode}${param.branches.development}${inlineCode}](${developmentUrl}) and [${inlineCode}${param.branches.main}${inlineCode}](${mainUrl}).\n> **Quick actions:**\n> [New PR](https://github.com/${param.owner}/${param.repo}/compare/${param.branches.development}...${branch}?expand=1) from [${inlineCode}${branch}${inlineCode}](${releaseUrl}) to [${inlineCode}${param.branches.development}${inlineCode}](${developmentUrl}).\n> [New PR](https://github.com/${param.owner}/${param.repo}/compare/${param.branches.main}...${branch}?expand=1) from [${inlineCode}${branch}${inlineCode}](${releaseUrl}) to [${inlineCode}${param.branches.main}${inlineCode}](${mainUrl}).`; + return `Copilot will promote [\`${branch}\`](${releaseUrl}) into [\`${param.branches.main}\`](${mainUrl}) before publication, then reconcile the accepted production commit into the current [\`${param.branches.development}\`](${developmentUrl}) branch. Do not create the version tag or either merge PR manually unless the issue control center requests recovery.`; } @@ -70393,8 +71634,18 @@ const REPOSITORY_STRING_KEYS = new Set([ 'issueLocale', 'pullRequestLocale', 'commitPrefixTransforms', + 'releaseReconciliationStrategy', + 'hotfixReconciliationStrategy', + 'reconciliationPullRequestMode', + 'reconciliationBackmergeMode', + 'hotfixActiveReleasePolicy', + 'reconciliationTree', + 'reconciliationCleanup', + 'reconciliationIssueCompletion', + 'orchestrationPresentationMode', + 'orchestrationCommentMode', ]); -const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush']); +const REPOSITORY_BOOLEAN_KEYS = new Set(['branchManagementAlways', 'reopenIssueOnPush', 'orchestrationDiagrams']); const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'mergeTimeout', 'inactivityThresholdHours']); const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'bugbotEffort', 'bugbotOrganizationRules', 'provisioningMode']); const AI_NUMBER_KEYS = new Set(['bugbotCommentLimit']); @@ -70620,6 +71871,17 @@ class SetupPromptAdapter { repository.issueLocale = await this.askText('Issue comment locale', repository.issueLocale); repository.pullRequestLocale = await this.askText('Pull-request comment locale', repository.pullRequestLocale); repository.commitPrefixTransforms = await this.askText('Commit prefix transforms', repository.commitPrefixTransforms); + repository.releaseReconciliationStrategy = await this.askChoice('Release reconciliation strategy', ['production-lineage', 'canonical-gitflow', 'manual'], repository.releaseReconciliationStrategy); + repository.hotfixReconciliationStrategy = await this.askChoice('Hotfix reconciliation strategy', ['production-lineage', 'canonical-gitflow', 'manual'], repository.hotfixReconciliationStrategy); + repository.reconciliationPullRequestMode = await this.askChoice('Managed reconciliation PR mode', ['auto', 'auto-merge', 'merge-queue', 'create-only', 'legacy-wait'], repository.reconciliationPullRequestMode); + repository.reconciliationBackmergeMode = await this.askChoice('Reconciliation back-merge mode', ['auto', 'direct', 'sync-branch'], repository.reconciliationBackmergeMode); + repository.hotfixActiveReleasePolicy = await this.askChoice('Hotfix target while a release is active', ['prefer-release', 'development', 'both'], repository.hotfixActiveReleasePolicy); + repository.reconciliationTree = await this.askText('Reconciliation branch prefix', repository.reconciliationTree); + repository.reconciliationCleanup = await this.askChoice('Branch cleanup after reconciliation', ['all', 'source-only', 'sync-only', 'none'], repository.reconciliationCleanup); + repository.reconciliationIssueCompletion = await this.askChoice('Launcher issue behavior after reconciliation', ['close', 'keep-open'], repository.reconciliationIssueCompletion); + repository.orchestrationPresentationMode = await this.askChoice('Release control-center detail', ['guided', 'compact', 'quiet'], repository.orchestrationPresentationMode); + repository.orchestrationDiagrams = await this.askBoolean('Show accessible Mermaid release diagrams?', repository.orchestrationDiagrams); + repository.orchestrationCommentMode = await this.askChoice('Release lifecycle comment mode', ['update', 'milestones'], repository.orchestrationCommentMode); console.log((0, setup_prompt_rendering_1.color)('\n4. Configure AI, projects, and release safety\n', 36)); const ai = defaults.ai; ai.pullRequestDescription = await this.askBoolean('Generate AI pull-request descriptions?', ai.pullRequestDescription); @@ -71001,6 +72263,10 @@ exports.ACTIONS = { CLOSE_INACTIVE_ISSUES: 'close_inactive_issues_action', PUBLISH_ISSUE_COMMENT: 'publish_issue_comment', CHECK_BRANCH_SYNC: 'check_branch_sync_action', + PREPARE_DEPLOYMENT: 'prepare_deployment_action', + CONTINUE_DEPLOYMENT: 'continue_deployment_action', + PUBLISHED_DEPLOYMENT: 'published_deployment_action', + FAILED_DEPLOYMENT: 'failed_deployment_action', }; @@ -71212,8 +72478,9 @@ exports.migrateConfigurationPayload = migrateConfigurationPayload; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); +const deployment_operation_1 = __nccwpck_require__(92730); /** Version of the durable configuration contract stored in issue/PR content. */ -exports.CONFIG_SCHEMA_VERSION = 2; +exports.CONFIG_SCHEMA_VERSION = 3; /** * Normalizes persisted configuration without silently losing fields from a * newer installation. Unknown keys are deliberately retained so a downgrade @@ -71257,6 +72524,9 @@ class Config { this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); this.releaseBranch = (0, model_input_1.readOptionalString)(input, 'releaseBranch'); + this.releaseOriginBranch = (0, model_input_1.readOptionalString)(input, 'releaseOriginBranch'); + this.releaseOriginSha = (0, model_input_1.readOptionalString)(input, 'releaseOriginSha'); + this.hotfixOriginSha = (0, model_input_1.readOptionalString)(input, 'hotfixOriginSha'); this.parentBranch = (0, model_input_1.readOptionalString)(input, 'parentBranch'); this.workingBranch = (0, model_input_1.readOptionalString)(input, 'workingBranch'); if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { @@ -71265,6 +72535,9 @@ class Config { if ((0, recommendation_state_1.isRecommendationState)(input['recommendationState'])) { this.recommendationState = input['recommendationState']; } + if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + this.deploymentOrchestration = input['deploymentOrchestration']; + } } } exports.Config = Config; @@ -71302,6 +72575,7 @@ const commit_1 = __nccwpck_require__(57525); const config_1 = __nccwpck_require__(90450); const github_user_policy_1 = __nccwpck_require__(84403); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); class Execution { get eventName() { return this.inputs?.eventName ?? ''; @@ -71392,6 +72666,7 @@ class Execution { this.hotfix = components.hotfix; this.project = components.projects; this.workflows = components.workflows; + this.deployment = components.deployment ?? { ...deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION }; this.tokenUser = components.tokenUser; this.inactivityThresholdHours = components.inactivityThresholdHours ?? issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new config_1.Config({}); @@ -72290,6 +73565,24 @@ class SingleAction { get isCheckBranchSyncAction() { return this.currentSingleAction === action_types_1.ACTIONS.CHECK_BRANCH_SYNC; } + get isPrepareDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.PREPARE_DEPLOYMENT; + } + get isContinueDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.CONTINUE_DEPLOYMENT; + } + get isPublishedDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT; + } + get isFailedDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.FAILED_DEPLOYMENT; + } + get isDeploymentOrchestrationAction() { + return this.isPrepareDeploymentAction + || this.isContinueDeploymentAction + || this.isPublishedDeploymentAction + || this.isFailedDeploymentAction; + } get enabledSingleAction() { return this.currentSingleAction.length > 0; } @@ -72304,7 +73597,7 @@ class SingleAction { get throwError() { return this.actionsThrowError.indexOf(this.currentSingleAction) > -1; } - constructor(currentSingleAction, issue, version, title, changelog, message = '', commentId = '', commentMode = '') { + constructor(currentSingleAction, issue, version, title, changelog, message = '', commentId = '', commentMode = '', operationId = '') { this.actions = [ action_types_1.ACTIONS.DEPLOYED, action_types_1.ACTIONS.PUBLISH_GITHUB_ACTION, @@ -72318,6 +73611,10 @@ class SingleAction { action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, action_types_1.ACTIONS.PUBLISH_ISSUE_COMMENT, action_types_1.ACTIONS.CHECK_BRANCH_SYNC, + action_types_1.ACTIONS.PREPARE_DEPLOYMENT, + action_types_1.ACTIONS.CONTINUE_DEPLOYMENT, + action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT, + action_types_1.ACTIONS.FAILED_DEPLOYMENT, ]; /** * Actions that throw an error if the last step failed @@ -72329,6 +73626,10 @@ class SingleAction { action_types_1.ACTIONS.CREATE_TAG, action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, action_types_1.ACTIONS.PUBLISH_ISSUE_COMMENT, + action_types_1.ACTIONS.PREPARE_DEPLOYMENT, + action_types_1.ACTIONS.CONTINUE_DEPLOYMENT, + action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT, + action_types_1.ACTIONS.FAILED_DEPLOYMENT, ]; /** * Actions that do not require an issue @@ -72350,6 +73651,7 @@ class SingleAction { this.title = ''; this.changelog = ''; this.message = ''; + this.operationId = ''; this.commentId = -1; this.commentIdInput = ''; this.commentMode = ''; @@ -72360,6 +73662,7 @@ class SingleAction { this.commentIdInput = commentId.trim(); this.commentId = (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.commentIdInput) ?? -1; this.commentMode = commentMode.trim().toLowerCase(); + this.operationId = operationId.trim(); this.currentSingleAction = currentSingleAction; if (!this.isSingleActionWithoutIssue) { this.issue = (0, positive_integer_policy_1.parsePositiveSafeInteger)(issue) ?? -1; @@ -74170,13 +75473,14 @@ function missingLinkedBranchResult(branchName) { function unexpectedLinkedBranchResult(branchName) { return new result_1.Result({ id: RESULT_ID, success: false, executed: true, steps: [`Linked branch creation returned an unexpected branch ref for ${branchName}.`] }); } -function createdLinkedBranchResult(owner, repo, baseBranchName, newBranchName) { +function createdLinkedBranchResult(owner, repo, baseBranchName, newBranchName, baseSha) { return new result_1.Result({ id: RESULT_ID, success: true, executed: true, payload: { baseBranchName, + baseSha, baseBranchUrl: `https://github.com/${owner}/${repo}/tree/${baseBranchName}`, newBranchName, newBranchUrl: `https://github.com/${owner}/${repo}/tree/${newBranchName}`, @@ -74240,7 +75544,7 @@ async function runCreateLinkedBranch(client, owner, repo, baseBranchName, newBra return [(0, linked_branch_result_policy_1.missingLinkedBranchResult)(newBranchName)]; if (!(0, linked_branch_policy_1.isExpectedLinkedBranchRef)(linkedBranch.ref?.name, newBranchName)) return [(0, linked_branch_result_policy_1.unexpectedLinkedBranchResult)(newBranchName)]; - return [(0, linked_branch_result_policy_1.createdLinkedBranchResult)(owner, repo, baseBranchName, newBranchName)]; + return [(0, linked_branch_result_policy_1.createdLinkedBranchResult)(owner, repo, baseBranchName, newBranchName, identifiers.branchOid)]; } catch (error) { if ((0, github_error_policy_1.isGithubAlreadyExists)(error)) { @@ -74679,6 +75983,324 @@ function withCause(message, cause) { } +/***/ }), + +/***/ 77509: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentContinuationRepository = void 0; +class DeploymentContinuationRepository { + constructor(workflow) { + this.workflow = workflow; + } + async dispatch(owner, repository, workflow, ref, operationId, issue, version, token) { + await this.workflow.executeWorkflow(owner, repository, ref, workflow, { + mode: "publish", + "operation-id": operationId, + issue: String(issue), + version, + }, token); + } +} +exports.DeploymentContinuationRepository = DeploymentContinuationRepository; + + +/***/ }), + +/***/ 91985: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentPresentationRepository = void 0; +class DeploymentPresentationRepository { + constructor(issues) { + this.issues = issues; + } + async findDashboard(owner, repository, issue, marker, token) { + const comments = await this.issues.listIssueComments(owner, repository, issue, token); + const matches = comments.filter((comment) => comment.body?.includes(marker)); + if (matches.length > 1) + throw new Error(`Multiple deployment dashboards match ${marker}.`); + const match = matches[0]; + return match ? { id: match.id, body: match.body ?? "" } : undefined; + } + async createDashboard(owner, repository, issue, body, token) { + await this.issues.addComment(owner, repository, issue, body, token); + } + async updateDashboard(owner, repository, issue, commentId, body, token) { + await this.issues.updateComment(owner, repository, issue, commentId, body, token); + } + async publishMilestone(owner, repository, issue, marker, body, token) { + const comments = await this.issues.listIssueComments(owner, repository, issue, token); + if (comments.some((comment) => comment.body?.includes(marker))) + return; + await this.issues.addComment(owner, repository, issue, `${body}\n\n${marker}`, token); + } +} +exports.DeploymentPresentationRepository = DeploymentPresentationRepository; + + +/***/ }), + +/***/ 3182: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentStateRepository = void 0; +const config_1 = __nccwpck_require__(90450); +const configuration_handler_1 = __nccwpck_require__(40188); +const configuration_payload_policy_1 = __nccwpck_require__(58043); +class DeploymentStateRepository { + constructor(issues) { + this.issues = issues; + this.block = new configuration_handler_1.ConfigurationHandler(issues); + } + async load(query) { + const description = await this.issues.getDescription(query.owner, query.repository, query.issue, query.token); + const raw = this.block.getContent(description); + if (!raw) + return undefined; + return new config_1.Config(JSON.parse(raw)).deploymentOrchestration; + } + async save(command) { + const description = await this.issues.getDescription(command.owner, command.repository, command.issue, command.token); + const stored = this.block.getContent(description); + const payload = (0, configuration_payload_policy_1.buildConfigurationPayload)({ currentConfiguration: command.state }, stored); + const updated = this.block.updateContent(description, payload); + if (updated === undefined) + throw new Error("Issue configuration markers are missing or inconsistent."); + await this.issues.updateDescription(command.owner, command.repository, command.issue, updated, command.token); + } +} +exports.DeploymentStateRepository = DeploymentStateRepository; + + +/***/ }), + +/***/ 22368: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.GithubDeploymentRepository = void 0; +const managed_pull_request_1 = __nccwpck_require__(95914); +class GithubDeploymentRepository { + constructor(clientProvider) { + this.clientProvider = clientProvider; + } + async findManagedPullRequests(query) { + const client = this.clientProvider.getClient(query.token); + const pullRequests = await client.paginate(client.rest.pulls.list, { + owner: query.owner, + repo: query.repository, + state: "all", + head: `${query.owner}:${query.headBranch}`, + base: query.baseBranch, + per_page: 100, + }); + return pullRequests + .filter((pullRequest) => { + const marker = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body); + return marker?.operationId === query.operationId + && marker.phase === query.phase + && marker.issue === query.issue; + }) + .map((pullRequest) => mapPullRequest(pullRequest, query.owner, query.repository)); + } + async createManagedPullRequest(command) { + const client = this.clientProvider.getClient(command.token); + const { data } = await client.rest.pulls.create({ + owner: command.owner, + repo: command.repository, + head: command.headBranch, + base: command.baseBranch, + title: command.title, + body: command.body, + maintainer_can_modify: false, + }); + return mapPullRequest(data, command.owner, command.repository); + } + async getPullRequest(owner, repository, pullRequest, token) { + const { data } = await this.clientProvider.getClient(token).rest.pulls.get({ + owner, + repo: repository, + pull_number: pullRequest, + }); + return mapPullRequest(data, owner, repository); + } + async getTargetCapabilities(owner, repository, targetBranch, token, pullRequest) { + const client = this.clientProvider.getClient(token); + const [{ data: repositoryData }, protection, queue, pullRequestState] = await Promise.all([ + client.rest.repos.get({ owner, repo: repository }), + readBranchProtection(client, owner, repository, targetBranch), + readMergeQueueRequirement(client, owner, repository, targetBranch), + pullRequest === undefined + ? Promise.resolve(undefined) + : client.rest.pulls.get({ owner, repo: repository, pull_number: pullRequest }).then(({ data }) => data), + ]); + return { + autoMergeAllowed: repositoryData.allow_auto_merge === true, + mergeQueueRequired: queue, + immediatelyMergeable: pullRequestState?.mergeable === true && pullRequestState.mergeable_state === "clean", + requiresStrictStatusChecks: protection?.required_status_checks?.strict === true, + }; + } + async enableAutoMerge(owner, repository, pullRequestNodeId, token) { + await this.clientProvider.getClient(token).graphql(`mutation EnableDeploymentAutoMerge($pullRequestId: ID!) { + enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: MERGE}) { + pullRequest { id } + } + }`, { pullRequestId: pullRequestNodeId, owner, repository }); + } + async enqueuePullRequest(owner, repository, pullRequestNodeId, token) { + await this.clientProvider.getClient(token).graphql(`mutation EnqueueDeploymentPullRequest($pullRequestId: ID!) { + enqueuePullRequest(input: {pullRequestId: $pullRequestId}) { mergeQueueEntry { id } } + }`, { pullRequestId: pullRequestNodeId, owner, repository }); + } + async mergePullRequest(owner, repository, pullRequest, token) { + const { data } = await this.clientProvider.getClient(token).rest.pulls.merge({ + owner, + repo: repository, + pull_number: pullRequest, + merge_method: "merge", + }); + if (!data.merged || !data.sha) + throw new Error(data.message ?? `Pull request #${pullRequest} was not merged.`); + return data.sha; + } + async getBranchSha(owner, repository, branch, token) { + const { data } = await this.clientProvider.getClient(token).rest.git.getRef({ owner, repo: repository, ref: `heads/${branch}` }); + return data.object.sha; + } + async getMergeBaseSha(owner, repository, base, head, token) { + const { data } = await this.clientProvider.getClient(token).rest.repos.compareCommits({ owner, repo: repository, base, head }); + const sha = data.merge_base_commit?.sha; + if (!sha) + throw new Error(`GitHub returned no merge base for ${base}...${head}.`); + return sha; + } + async isCommitReachable(owner, repository, branch, sha, token) { + const { data } = await this.clientProvider.getClient(token).rest.repos.compareCommits({ owner, repo: repository, base: sha, head: branch }); + return data.merge_base_commit?.sha === sha; + } + async createOrVerifyBranch(owner, repository, branch, sha, token) { + const client = this.clientProvider.getClient(token); + try { + const { data } = await client.rest.git.getRef({ owner, repo: repository, ref: `heads/${branch}` }); + if (data.object.sha !== sha) { + const { data: comparison } = await client.rest.repos.compareCommits({ owner, repo: repository, base: sha, head: branch }); + if (comparison.merge_base_commit?.sha !== sha) + throw new Error(`Branch ${branch} already exists at a different SHA.`); + } + } + catch (error) { + if (!isNotFound(error)) + throw error; + await client.rest.git.createRef({ owner, repo: repository, ref: `refs/heads/${branch}`, sha }); + } + } + async mergeCommitIntoBranch(owner, repository, branch, sourceSha, token) { + const client = this.clientProvider.getClient(token); + const { data: comparison } = await client.rest.repos.compareCommits({ owner, repo: repository, base: sourceSha, head: branch }); + if (comparison.merge_base_commit?.sha === sourceSha) + return await this.getBranchSha(owner, repository, branch, token); + const { data } = await client.rest.repos.merge({ + owner, + repo: repository, + base: branch, + head: sourceSha, + commit_message: `chore(release): reconcile ${sourceSha.slice(0, 7)} into ${branch}`, + }); + if (!data.merged || !data.sha) + throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); + return data.sha; + } + async deleteBranch(owner, repository, branch, token) { + try { + await this.clientProvider.getClient(token).rest.git.deleteRef({ owner, repo: repository, ref: `heads/${branch}` }); + } + catch (error) { + if (!isNotFound(error)) + throw error; + } + } + async listBranches(owner, repository, prefix, token) { + const client = this.clientProvider.getClient(token); + const branches = await client.paginate(client.rest.repos.listBranches, { owner, repo: repository, per_page: 100 }); + return branches.map(({ name }) => name).filter((name) => name.startsWith(`${prefix}/`)); + } +} +exports.GithubDeploymentRepository = GithubDeploymentRepository; +function mapPullRequest(value, owner, repository) { + return { + number: value.number, + nodeId: value.node_id, + body: value.body ?? "", + headBranch: value.head.ref, + headSha: value.head.sha, + baseBranch: value.base.ref, + state: value.state === "closed" ? "closed" : "open", + merged: value.merged === true, + mergeCommitSha: value.merge_commit_sha ?? undefined, + repositoryFullName: value.base.repo?.full_name ?? value.head.repo?.full_name ?? `${owner}/${repository}`, + }; +} +async function readBranchProtection(client, owner, repository, branch) { + try { + return (await client.rest.repos.getBranchProtection({ owner, repo: repository, branch })).data; + } + catch (error) { + if (isNotFound(error)) + return undefined; + throw error; + } +} +async function readMergeQueueRequirement(client, owner, repository, branch) { + const response = await client.graphql(`query DeploymentTargetRules($owner: String!, $repository: String!, $qualifiedName: String!) { + repository(owner: $owner, name: $repository) { + ref(qualifiedName: $qualifiedName) { branchProtectionRule { requiresMergeQueue } } + } + }`, { owner, repository, qualifiedName: `refs/heads/${branch}` }); + return response.repository?.ref?.branchProtectionRule?.requiresMergeQueue === true; +} +function isNotFound(error) { + return typeof error === "object" && error !== null && "status" in error && error.status === 404; +} + + +/***/ }), + +/***/ 70245: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.LegacyDeploymentMergeRepository = void 0; +const merge_checks_waiter_1 = __nccwpck_require__(43989); +const merge_pull_request_flow_1 = __nccwpck_require__(81775); +class LegacyDeploymentMergeRepository { + constructor(clients, waiter = new merge_checks_waiter_1.MergeChecksWaiter()) { + this.clients = clients; + this.waiter = waiter; + } + async waitAndMerge(owner, repository, headBranch, pullRequest, baseBranch, timeoutSeconds, token) { + const client = this.clients.getClient(token); + await this.waiter.wait(client, owner, repository, headBranch, pullRequest, timeoutSeconds); + await (0, merge_pull_request_flow_1.mergePullRequest)(client, owner, repository, pullRequest, headBranch, baseBranch); + } +} +exports.LegacyDeploymentMergeRepository = LegacyDeploymentMergeRepository; + + /***/ }), /***/ 26331: @@ -78010,16 +79632,28 @@ class RepositoryReleasePublicationRepository { this.createRelease = async (owner, repository, version, title, changelog, token) => { try { const octokit = this.githubClient.getClient(token); - const { data: release } = await octokit.rest.repos.createRelease({ - owner, - repo: repository, - tag_name: version, - name: (0, release_tag_policy_1.releaseName)(version, title), - body: changelog, - draft: false, - prerelease: false, - }); - return release.html_url; + try { + const { data: release } = await octokit.rest.repos.createRelease({ + owner, + repo: repository, + tag_name: version, + name: (0, release_tag_policy_1.releaseName)(version, title), + body: changelog, + draft: false, + prerelease: false, + }); + return release.html_url; + } + catch (error) { + if (!isAlreadyExists(error)) + throw error; + const { data: existing } = await octokit.rest.repos.getReleaseByTag({ + owner, + repo: repository, + tag: version, + }); + return existing.html_url; + } } catch (error) { (0, logger_1.logError)(`Error creating release: ${error}`); @@ -78029,6 +79663,10 @@ class RepositoryReleasePublicationRepository { } } exports.RepositoryReleasePublicationRepository = RepositoryReleasePublicationRepository; +function isAlreadyExists(error) { + return typeof error === 'object' && error !== null && 'status' in error + && error.status === 422; +} /***/ }), @@ -78100,8 +79738,7 @@ class RepositoryTagRepository { const octokit = this.githubClient.getClient(token); const sourceTagSha = await (0, repository_tag_query_1.getRepositoryTagSha)(octokit, owner, repository, sourceTag); if (!sourceTagSha) { - (0, logger_1.logError)(`The '${sourceTag}' tag does not exist in the remote repository`); - return; + throw new Error(`The '${sourceTag}' tag does not exist in the remote repository.`); } const foundTargetTag = await (0, repository_tag_query_1.findRepositoryTag)(octokit, owner, repository, targetTag); if (foundTargetTag) { @@ -78123,6 +79760,10 @@ class RepositoryTagRepository { sha: sourceTagSha, }); } + const verifiedTargetSha = await (0, repository_tag_query_1.getRepositoryTagSha)(octokit, owner, repository, targetTag); + if (verifiedTargetSha !== sourceTagSha) { + throw new Error(`Moving tag '${targetTag}' was not verified at ${sourceTagSha}.`); + } }; this.createTag = async (owner, repository, branch, tag, token) => { const octokit = this.githubClient.getClient(token); @@ -78151,6 +79792,23 @@ class RepositoryTagRepository { throw error; } }; + this.createOrVerifyTagAtSha = async (owner, repository, sha, tag, token) => { + const octokit = this.githubClient.getClient(token); + const existingTag = await (0, repository_tag_query_1.findRepositoryTag)(octokit, owner, repository, tag); + if (existingTag) { + if (existingTag.object.sha !== sha) { + throw new Error(`Immutable tag '${tag}' exists at ${existingTag.object.sha}, expected ${sha}.`); + } + return sha; + } + await octokit.rest.git.createRef({ + owner, + repo: repository, + ref: `refs/tags/${tag}`, + sha, + }); + return sha; + }; } } exports.RepositoryTagRepository = RepositoryTagRepository; @@ -79418,6 +81076,296 @@ function lifecycleStateFromLabels(currentLabels, labels = exports.DEFAULT_COPILO } +/***/ }), + +/***/ 22495: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = exports.ORCHESTRATION_COMMENT_MODES = exports.ORCHESTRATION_PRESENTATION_MODES = exports.RECONCILIATION_ISSUE_COMPLETION_MODES = exports.RECONCILIATION_CLEANUP_MODES = exports.HOTFIX_ACTIVE_RELEASE_POLICIES = exports.RECONCILIATION_BACKMERGE_MODES = exports.RECONCILIATION_PR_MODES = exports.RECONCILIATION_STRATEGIES = void 0; +exports.validateDeploymentConfiguration = validateDeploymentConfiguration; +exports.isSafeBranchTree = isSafeBranchTree; +exports.parseDeploymentEnum = parseDeploymentEnum; +exports.RECONCILIATION_STRATEGIES = [ + "production-lineage", + "canonical-gitflow", + "manual", +]; +exports.RECONCILIATION_PR_MODES = [ + "auto", + "auto-merge", + "merge-queue", + "create-only", + "legacy-wait", +]; +exports.RECONCILIATION_BACKMERGE_MODES = [ + "auto", + "direct", + "sync-branch", +]; +exports.HOTFIX_ACTIVE_RELEASE_POLICIES = [ + "prefer-release", + "development", + "both", +]; +exports.RECONCILIATION_CLEANUP_MODES = [ + "all", + "source-only", + "sync-only", + "none", +]; +exports.RECONCILIATION_ISSUE_COMPLETION_MODES = ["close", "keep-open"]; +exports.ORCHESTRATION_PRESENTATION_MODES = ["guided", "compact", "quiet"]; +exports.ORCHESTRATION_COMMENT_MODES = ["update", "milestones"]; +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = { + releaseReconciliationStrategy: "production-lineage", + hotfixReconciliationStrategy: "production-lineage", + reconciliationPullRequestMode: "auto", + reconciliationBackmergeMode: "auto", + hotfixActiveReleasePolicy: "prefer-release", + reconciliationTree: "sync", + reconciliationCleanup: "all", + reconciliationIssueCompletion: "close", + orchestrationPresentationMode: "guided", + orchestrationDiagrams: true, + orchestrationCommentMode: "update", +}; +function validateDeploymentConfiguration(configuration, context) { + const errors = []; + for (const [name, value, allowed] of [ + ["release reconciliation strategy", configuration.releaseReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["hotfix reconciliation strategy", configuration.hotfixReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["reconciliation PR mode", configuration.reconciliationPullRequestMode, exports.RECONCILIATION_PR_MODES], + ["reconciliation back-merge mode", configuration.reconciliationBackmergeMode, exports.RECONCILIATION_BACKMERGE_MODES], + ["hotfix active-release policy", configuration.hotfixActiveReleasePolicy, exports.HOTFIX_ACTIVE_RELEASE_POLICIES], + ["reconciliation cleanup", configuration.reconciliationCleanup, exports.RECONCILIATION_CLEANUP_MODES], + ["reconciliation issue completion", configuration.reconciliationIssueCompletion, exports.RECONCILIATION_ISSUE_COMPLETION_MODES], + ["orchestration presentation mode", configuration.orchestrationPresentationMode, exports.ORCHESTRATION_PRESENTATION_MODES], + ["orchestration comment mode", configuration.orchestrationCommentMode, exports.ORCHESTRATION_COMMENT_MODES], + ]) { + if (!allowed.includes(value)) { + errors.push(`The ${name} must be one of: ${allowed.join(", ")}.`); + } + } + if (typeof configuration.orchestrationDiagrams !== "boolean") { + errors.push("Orchestration diagrams must be a boolean."); + } + if (context.productionBranch === context.developmentBranch) { + errors.push("Production and development branches must be different."); + } + const protectedNames = new Set([context.productionBranch, context.developmentBranch]); + for (const [label, tree] of [ + ["release", context.releaseTree], + ["hotfix", context.hotfixTree], + ["reconciliation", configuration.reconciliationTree], + ]) { + if (!isSafeBranchTree(tree)) { + errors.push(`The ${label} branch prefix must be a safe, non-empty Git ref segment.`); + } + else if (protectedNames.has(tree)) { + errors.push(`The ${label} branch prefix cannot equal a protected long-lived branch.`); + } + } + if (configuration.reconciliationPullRequestMode === "merge-queue" + && context.mergeQueueWorkflowSupported === false) { + errors.push("Merge-queue mode requires merge_group support in every required workflow."); + } + if ((configuration.releaseReconciliationStrategy === "manual" + || configuration.hotfixReconciliationStrategy === "manual") + && configuration.reconciliationIssueCompletion === "close") { + errors.push("Manual reconciliation cannot close the launcher issue automatically."); + } + return errors; +} +function isSafeBranchTree(value) { + const tree = value.trim(); + return tree.length > 0 + && tree.length <= 100 + && !tree.startsWith("/") + && !tree.endsWith("/") + && !tree.includes("..") + && !tree.includes("@{") + && !/[~^:?*[\\\]\s]/.test(tree); +} +function parseDeploymentEnum(value, allowed, fallback) { + if (value === undefined || value === null || String(value).trim() === "") { + return { value: fallback, valid: true }; + } + const normalized = String(value).trim(); + return allowed.includes(normalized) + ? { value: normalized, valid: true } + : { value: fallback, valid: false }; +} + + +/***/ }), + +/***/ 92730: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_PHASES = void 0; +exports.transitionDeploymentOperation = transitionDeploymentOperation; +exports.blockDeploymentOperation = blockDeploymentOperation; +exports.resumeBlockedDeployment = resumeBlockedDeployment; +exports.completeReconciliationTarget = completeReconciliationTarget; +exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; +exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; +const deployment_configuration_1 = __nccwpck_require__(22495); +exports.DEPLOYMENT_PHASES = [ + "preparing", + "promotion_pr_pending", + "promoted", + "publishing", + "published", + "reconciliation_pending", + "completed", + "blocked", +]; +const NORMAL_TRANSITIONS = { + preparing: ["promotion_pr_pending"], + promotion_pr_pending: ["promoted"], + promoted: ["publishing"], + publishing: ["published"], + published: ["reconciliation_pending", "completed"], + reconciliation_pending: ["completed"], + completed: [], +}; +function transitionDeploymentOperation(operation, expectedPhase, nextPhase) { + if (operation.phase === nextPhase) { + return { kind: "noop", operation, reason: `Operation is already ${nextPhase}.` }; + } + if (operation.phase !== expectedPhase) { + return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; + } + if (nextPhase === "blocked") { + return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + } + if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { + return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; + } + return { kind: "advance", operation: { ...operation, phase: nextPhase, lastFailure: null } }; +} +function blockDeploymentOperation(operation, category, message, retryable) { + if (operation.phase === "completed") + return operation; + const previousPhase = operation.phase === "blocked" + ? operation.lastFailure?.previousPhase ?? "preparing" + : operation.phase; + return { + ...operation, + phase: "blocked", + lastFailure: { category, message: sanitizeDeploymentMessage(message), retryable, previousPhase }, + }; +} +function resumeBlockedDeployment(operation) { + if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; + } + return { + kind: "advance", + operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + }; +} +function completeReconciliationTarget(operation, pullRequest) { + const targets = operation.reconciliationTargets.map((target) => target.pullRequest === pullRequest ? { ...target, status: "completed" } : target); + return { + ...operation, + reconciliationTargets: targets, + lastFailure: null, + }; +} +function sanitizeDeploymentMessage(value) { + return value + .replace(/::/g, "﹕﹕") + .replace(/@(?=[A-Za-z0-9_-])/g, "@\u200b") + .replace(//g, "-->") + .slice(0, 2000); +} +function isDeploymentOperationSnapshot(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const operation = value; + return typeof operation.operationId === "string" + && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.kind === "release" || operation.kind === "hotfix") + && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) + && typeof operation.title === "string" && operation.title.length <= 1000 + && typeof operation.changelog === "string" && operation.changelog.length <= 50000 + && exports.DEPLOYMENT_PHASES.includes(operation.phase) + && deployment_configuration_1.RECONCILIATION_STRATEGIES.includes(operation.strategy) + && deployment_configuration_1.RECONCILIATION_PR_MODES.includes(operation.prMode) + && (operation.selectedPrMode === undefined + || ["auto-merge", "merge-queue", "create-only", "legacy-wait"].includes(operation.selectedPrMode)) + && deployment_configuration_1.RECONCILIATION_BACKMERGE_MODES.includes(operation.backmergeMode) + && deployment_configuration_1.HOTFIX_ACTIVE_RELEASE_POLICIES.includes(operation.hotfixActiveReleasePolicy) + && deployment_configuration_1.RECONCILIATION_CLEANUP_MODES.includes(operation.cleanup) + && deployment_configuration_1.RECONCILIATION_ISSUE_COMPLETION_MODES.includes(operation.issueCompletion) + && deployment_configuration_1.ORCHESTRATION_PRESENTATION_MODES.includes(operation.presentationMode) + && typeof operation.diagrams === "boolean" + && deployment_configuration_1.ORCHESTRATION_COMMENT_MODES.includes(operation.commentMode) + && isSafePersistedRef(operation.sourceBranch) + && isFullSha(operation.sourceSha) + && isSafePersistedRef(operation.originBranch) + && isFullSha(operation.originSha) + && isSafePersistedRef(operation.productionBranch) + && isSafePersistedRef(operation.developmentBranch) + && typeof operation.reconciliationTree === "string" + && typeof operation.tag === "string" && operation.tag === `v${operation.version}` + && typeof operation.publicationWorkflow === "string" && isSafeWorkflowName(operation.publicationWorkflow) + && (operation.promotionPullRequest === undefined || isPositiveInteger(operation.promotionPullRequest)) + && (operation.productionSha === undefined || isFullSha(operation.productionSha)) + && typeof operation.publicationVerified === "boolean" + && Array.isArray(operation.reconciliationTargets) + && operation.reconciliationTargets.every(isReconciliationTarget) + && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); +} +function isFullSha(value) { + return typeof value === "string" && /^[a-f0-9]{40}$/i.test(value); +} +function isPositiveInteger(value) { + return typeof value === "number" && Number.isSafeInteger(value) && value > 0; +} +function isSafePersistedRef(value) { + return typeof value === "string" + && value.length > 0 + && value.length <= 200 + && !value.includes("..") + && !value.includes("@{") + && !/[\s~^:?*[\\\]]/.test(value); +} +function isSafeWorkflowName(value) { + return value.length <= 200 && !value.includes("..") && /^[A-Za-z0-9][A-Za-z0-9._/-]*\.ya?ml$/.test(value); +} +function isReconciliationTarget(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const target = value; + return isSafePersistedRef(target.targetBranch) + && isSafePersistedRef(target.sourceBranch) + && isFullSha(target.sourceSha) + && (target.syncBranch === undefined || isSafePersistedRef(target.syncBranch)) + && (target.pullRequest === undefined || isPositiveInteger(target.pullRequest)) + && ["pending", "completed", "blocked"].includes(target.status); +} +function isDeploymentFailure(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const failure = value; + return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category) + && typeof failure.message === "string" + && failure.message.length <= 2000 + && typeof failure.retryable === "boolean" + && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] + .includes(failure.previousPhase); +} + + /***/ }), /***/ 84403: @@ -79492,6 +81440,38 @@ function normalize(value) { } +/***/ }), + +/***/ 95914: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildManagedPullRequestMarker = buildManagedPullRequestMarker; +exports.parseManagedPullRequestMarker = parseManagedPullRequestMarker; +exports.isSafeOperationId = isSafeOperationId; +const MANAGED_PULL_REQUEST_PATTERN = //; +function buildManagedPullRequestMarker(identity) { + if (!isSafeOperationId(identity.operationId) || !Number.isSafeInteger(identity.issue) || identity.issue < 1) { + throw new Error("Managed pull request identity is invalid."); + } + return ``; +} +function parseManagedPullRequestMarker(body) { + const match = MANAGED_PULL_REQUEST_PATTERN.exec(body ?? ""); + if (!match) + return undefined; + const issue = Number(match[3]); + if (!Number.isSafeInteger(issue) || issue < 1 || !isSafeOperationId(match[1])) + return undefined; + return { operationId: match[1], phase: match[2], issue }; +} +function isSafeOperationId(value) { + return /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(value); +} + + /***/ }), /***/ 19879: @@ -79715,20 +81695,25 @@ const SETUP_WORKFLOWS = [ { file: 'copilot_pull_request_comment.yml', feature: 'pullRequestComments' }, { file: 'release_workflow.yml', feature: 'release' }, { file: 'hotfix_workflow.yml', feature: 'hotfix' }, + { file: 'copilot_deployment_orchestration.yml', feature: ['release', 'hotfix'] }, { file: 'agent-cli-provisioning.yml', feature: 'agentProvisioning' }, { file: 'copilot_credential_health.yml', feature: 'credentialHealth' }, { file: 'copilot_close_inactive_issues.yml', feature: 'inactiveIssueClosure' }, ]; function enabledSetupWorkflowFiles(features) { return SETUP_WORKFLOWS - .filter(({ feature }) => features[feature] !== false) + .filter(({ feature }) => featureEnabled(feature, features)) .map(({ file }) => file); } function isSetupWorkflowEnabled(file, features) { if (!features) return true; const definition = SETUP_WORKFLOWS.find((candidate) => candidate.file === file); - return !definition || features[definition.feature] !== false; + return !definition || featureEnabled(definition.feature, features); +} +function featureEnabled(feature, features) { + const candidates = Array.isArray(feature) ? feature : [feature]; + return candidates.some((candidate) => features[candidate] !== false); } @@ -80753,6 +82738,16 @@ const branch_dependency_repository_1 = __nccwpck_require__(9627); const branch_sync_workspace_adapter_1 = __nccwpck_require__(81849); const observe_branch_sync_use_case_1 = __nccwpck_require__(84542); const sync_branch_use_case_1 = __nccwpck_require__(392); +const deployment_orchestration_use_case_1 = __nccwpck_require__(36850); +const github_deployment_repository_1 = __nccwpck_require__(22368); +const deployment_continuation_repository_1 = __nccwpck_require__(77509); +const deployment_presentation_repository_1 = __nccwpck_require__(91985); +const deployment_state_repository_1 = __nccwpck_require__(3182); +const legacy_deployment_merge_repository_1 = __nccwpck_require__(70245); +const octokit_deployment_adapter_1 = __nccwpck_require__(46819); +const workflow_dispatch_repository_1 = __nccwpck_require__(29509); +const github_workflow_client_factory_1 = __nccwpck_require__(29839); +const node_crypto_1 = __nccwpck_require__(6005); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution, bugbot.telemetry); @@ -80761,7 +82756,19 @@ function createSingleActionUseCaseCompositionRoot() { const repositoryTagPort = new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()); const repositoryReleasePort = new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()); const issueDescriptionQueryPort = (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(); - return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase(issueDescriptionQueryPort), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), issueDescriptionQueryPort)); + const deploymentRepository = new github_deployment_repository_1.GithubDeploymentRepository(new octokit_deployment_adapter_1.OctokitDeploymentClientAdapter()); + const deploymentOrchestration = new deployment_orchestration_use_case_1.DeploymentOrchestrationUseCase({ + pullRequests: deploymentRepository, + git: deploymentRepository, + continuation: new deployment_continuation_repository_1.DeploymentContinuationRepository(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)())), + legacyMerge: new legacy_deployment_merge_repository_1.LegacyDeploymentMergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)()), + presentation: new deployment_presentation_repository_1.DeploymentPresentationRepository(issueDescriptionQueryPort), + state: new deployment_state_repository_1.DeploymentStateRepository(issueDescriptionQueryPort), + labels: (0, issue_labels_composition_root_1.createIssueLabelRepository)(), + issues: (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), + operationId: node_crypto_1.randomUUID, + }); + return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase(issueDescriptionQueryPort), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), issueDescriptionQueryPort), deploymentOrchestration); } function createIssueCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -81387,6 +83394,24 @@ class OctokitCredentialHealthClientAdapter { exports.OctokitCredentialHealthClientAdapter = OctokitCredentialHealthClientAdapter; +/***/ }), + +/***/ 46819: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OctokitDeploymentClientAdapter = void 0; +const octokit_client_resolver_1 = __nccwpck_require__(54047); +class OctokitDeploymentClientAdapter { + getClient(token) { + return (0, octokit_client_resolver_1.getOctokitClient)(token); + } +} +exports.OctokitDeploymentClientAdapter = OctokitDeploymentClientAdapter; + + /***/ }), /***/ 29996: @@ -82438,6 +84463,10 @@ function buildConfigurationPayload(execution, storedRaw) { parentBranch: current.parentBranch, hotfixOriginBranch: current.hotfixOriginBranch, hotfixBranch: current.hotfixBranch, + releaseOriginBranch: current.releaseOriginBranch, + releaseOriginSha: current.releaseOriginSha, + hotfixOriginSha: current.hotfixOriginSha, + deploymentOrchestration: current.deploymentOrchestration, branchConfiguration: current.branchConfiguration, recommendationState: current.recommendationState, }; diff --git a/build/github_action/index.js b/build/github_action/index.js index e27d67604..c66ce1775 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -50776,6 +50776,65 @@ exports.DEFAULT_IMAGE_CONFIG = { }; +/***/ }), + +/***/ 30098: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readDeploymentConfiguration = readDeploymentConfiguration; +const application_error_1 = __nccwpck_require__(75999); +const deployment_configuration_1 = __nccwpck_require__(22495); +const input_keys_1 = __nccwpck_require__(88539); +function readDeploymentConfiguration(getInput, branches) { + const errors = []; + const readEnum = (key, allowed, fallback) => { + const parsed = (0, deployment_configuration_1.parseDeploymentEnum)(getInput(key), allowed, fallback); + if (!parsed.valid) + errors.push(`${key} must be one of: ${allowed.join(", ")}.`); + return parsed.value; + }; + const configuration = { + releaseReconciliationStrategy: readEnum(input_keys_1.INPUT_KEYS.RELEASE_RECONCILIATION_STRATEGY, deployment_configuration_1.RECONCILIATION_STRATEGIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.releaseReconciliationStrategy), + hotfixReconciliationStrategy: readEnum(input_keys_1.INPUT_KEYS.HOTFIX_RECONCILIATION_STRATEGY, deployment_configuration_1.RECONCILIATION_STRATEGIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixReconciliationStrategy), + reconciliationPullRequestMode: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_PR_MODE, deployment_configuration_1.RECONCILIATION_PR_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationPullRequestMode), + reconciliationBackmergeMode: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_BACKMERGE_MODE, deployment_configuration_1.RECONCILIATION_BACKMERGE_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationBackmergeMode), + hotfixActiveReleasePolicy: readEnum(input_keys_1.INPUT_KEYS.HOTFIX_ACTIVE_RELEASE_POLICY, deployment_configuration_1.HOTFIX_ACTIVE_RELEASE_POLICIES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixActiveReleasePolicy), + reconciliationTree: String(getInput(input_keys_1.INPUT_KEYS.RECONCILIATION_TREE) + ?? deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree).trim() + || deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree, + reconciliationCleanup: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_CLEANUP, deployment_configuration_1.RECONCILIATION_CLEANUP_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationCleanup), + reconciliationIssueCompletion: readEnum(input_keys_1.INPUT_KEYS.RECONCILIATION_ISSUE_COMPLETION, deployment_configuration_1.RECONCILIATION_ISSUE_COMPLETION_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationIssueCompletion), + orchestrationPresentationMode: readEnum(input_keys_1.INPUT_KEYS.ORCHESTRATION_PRESENTATION_MODE, deployment_configuration_1.ORCHESTRATION_PRESENTATION_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationPresentationMode), + orchestrationDiagrams: readBoolean(getInput(input_keys_1.INPUT_KEYS.ORCHESTRATION_DIAGRAMS), deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationDiagrams, input_keys_1.INPUT_KEYS.ORCHESTRATION_DIAGRAMS, errors), + orchestrationCommentMode: readEnum(input_keys_1.INPUT_KEYS.ORCHESTRATION_COMMENT_MODE, deployment_configuration_1.ORCHESTRATION_COMMENT_MODES, deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationCommentMode), + }; + errors.push(...(0, deployment_configuration_1.validateDeploymentConfiguration)(configuration, { + productionBranch: branches.productionBranch || "master", + developmentBranch: branches.developmentBranch || "develop", + releaseTree: branches.releaseTree || "release", + hotfixTree: branches.hotfixTree || "hotfix", + })); + if (errors.length > 0) { + throw new application_error_1.ApplicationError(`Invalid deployment configuration: ${errors.join(" ")}`, "validation"); + } + return configuration; +} +function readBoolean(value, fallback, name, errors) { + if (value === undefined || value === null || String(value).trim() === "") + return fallback; + const normalized = String(value).trim().toLowerCase(); + if (normalized === "true") + return true; + if (normalized === "false") + return false; + errors.push(`${name} must be true or false.`); + return fallback; +} + + /***/ }), /***/ 20236: @@ -51085,6 +51144,7 @@ const action_summary_policy_1 = __nccwpck_require__(72995); const copilot_lifecycle_1 = __nccwpck_require__(72418); const copilot_evidence_policy_1 = __nccwpck_require__(47632); const configuration_persistence_policy_1 = __nccwpck_require__(65388); +const deployment_presentation_policy_1 = __nccwpck_require__(83221); async function finishGithubAction(execution, results, issueNotificationPort, configurationStorePort, evidencePort, summaryPort) { const stepCount = results.reduce((acc, result) => acc + (result.steps?.length ?? 0), 0); const errorCount = results.reduce((acc, result) => acc + (result.errors?.length ?? 0), 0); @@ -51092,11 +51152,13 @@ async function finishGithubAction(execution, results, issueNotificationPort, con execution.currentConfiguration.results = results; core.setOutput('bugbot-telemetry', JSON.stringify(extractBugbotTelemetry(results))); const dryRun = results.some((result) => (0, result_1.getResultPayload)(result.payload)?.dryRun === true); - if (!dryRun && !execution.singleAction.isPublishIssueCommentAction) { + const ownsDeploymentPresentation = execution.singleAction.isDeploymentOrchestrationAction + || (execution.singleAction.isDeployedAction && Boolean(execution.currentConfiguration.deploymentOrchestration)); + if (!dryRun && !execution.singleAction.isPublishIssueCommentAction && !ownsDeploymentPresentation) { await new publish_resume_use_case_1.PublishResultUseCase(issueNotificationPort, (0, logger_adapter_1.createLogReportAdapter)()).invoke(execution); } - else if (execution.singleAction.isPublishIssueCommentAction) { - (0, logger_1.logInfo)('Result publication skipped: the issue-comment single action publishes its own content.'); + else if (execution.singleAction.isPublishIssueCommentAction || ownsDeploymentPresentation) { + (0, logger_1.logInfo)('Generic result publication skipped: this single action owns its user-facing presentation.'); } else { (0, logger_1.logInfo)('Bugbot dry-run: result publication and repository configuration persistence are disabled.'); @@ -51122,19 +51184,32 @@ function extractBugbotTelemetry(results) { }); } async function writeActionSummary(execution, summaryPort) { - const summaryText = (0, action_summary_policy_1.buildActionSummary)({ - owner: execution.owner, - repository: execution.repo, - eventName: execution.eventName, - issueNumber: execution.issue?.number ?? -1, - pullRequestNumber: execution.pullRequest?.number ?? -1, - lifecycleState: (0, copilot_lifecycle_1.lifecycleStateFromLabels)(execution.isPullRequest - ? execution.labels?.currentPullRequestLabels ?? [] - : execution.labels?.currentIssueLabels ?? [], execution.labels?.lifecycle), - pullRequestDescriptionMode: execution.ai?.getPullRequestDescriptionMode?.(), - failOnUnresolvedFindings: execution.ai?.getBugbotReviewConfiguration?.().failOnUnresolved === true, - results: execution.currentConfiguration.results, - }); + const operation = execution.currentConfiguration.deploymentOrchestration; + const summaryText = (execution.singleAction.isDeploymentOrchestrationAction || execution.singleAction.isDeployedAction) && operation + ? (0, deployment_presentation_policy_1.renderDeploymentJobSummary)(operation, { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + issueLocale: execution.locale.issue, + pullRequestLocale: execution.locale.pullRequest, + packageName: execution.owner === 'vypdev' && execution.repo === 'copilot' ? '@vypdev/copilot' : undefined, + workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID + ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + : undefined, + }, operation.lastFailure?.previousPhase, execution.currentConfiguration.results.flatMap((result) => result.steps ?? [])) + : (0, action_summary_policy_1.buildActionSummary)({ + owner: execution.owner, + repository: execution.repo, + eventName: execution.eventName, + issueNumber: execution.issue?.number ?? -1, + pullRequestNumber: execution.pullRequest?.number ?? -1, + lifecycleState: (0, copilot_lifecycle_1.lifecycleStateFromLabels)(execution.isPullRequest + ? execution.labels?.currentPullRequestLabels ?? [] + : execution.labels?.currentIssueLabels ?? [], execution.labels?.lifecycle), + pullRequestDescriptionMode: execution.ai?.getPullRequestDescriptionMode?.(), + failOnUnresolvedFindings: execution.ai?.getBugbotReviewConfiguration?.().failOnUnresolved === true, + results: execution.currentConfiguration.results, + }); if (!summaryPort) return summaryText; try { @@ -51235,6 +51310,7 @@ const configuration_builders_1 = __nccwpck_require__(19094); const project_details_loader_1 = __nccwpck_require__(73448); const issue_inactivity_1 = __nccwpck_require__(38572); const agent_task_activation_policy_1 = __nccwpck_require__(46855); +const deployment_configuration_builder_1 = __nccwpck_require__(30098); async function buildGithubActionExecution(input) { const { getInput, eventInputs, projectQuery, debug, singleAction, token } = input; const aiInputs = input.aiInputs ?? (0, github_action_ai_inputs_1.readGithubActionAiInputs)(getInput); @@ -51254,6 +51330,12 @@ async function buildGithubActionExecution(input) { const localeInputs = (0, github_action_locale_inputs_1.readGithubActionLocaleInputs)(getInput); const sizeThresholdInputs = (0, github_action_threshold_inputs_1.readGithubActionThresholdInputs)(getInput); const branchInputs = (0, github_action_branch_inputs_1.readGithubActionBranchInputs)(getInput); + const deployment = (0, deployment_configuration_builder_1.readDeploymentConfiguration)(getInput, { + productionBranch: branchInputs.defaultBranch, + developmentBranch: branchInputs.development, + releaseTree: branchInputs.releaseTree, + hotfixTree: branchInputs.hotfixTree, + }); return (0, execution_builder_1.buildExecution)({ debug, inactivityThresholdHours: (0, input_number_policy_1.parseBoundedPositiveIntegerInput)(getInput(input_keys_1.INPUT_KEYS.INACTIVITY_THRESHOLD_HOURS), issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS, issue_inactivity_1.MAX_INACTIVITY_THRESHOLD_HOURS), @@ -51273,6 +51355,7 @@ async function buildGithubActionExecution(input) { release: new release_1.Release(), hotfix: new hotfix_1.Hotfix(), workflows: (0, configuration_builders_1.buildWorkflows)(workflowInputs.release, workflowInputs.hotfix), + deployment, projects: (0, configuration_builders_1.buildProjects)(projectInputs), tokenUser: input.tokenUser, inputs: eventInputs, @@ -51286,7 +51369,7 @@ function disableAgentTasks(tasks) { }])); } function readGithubActionSingleAction(getInput) { - return new single_action_1.SingleAction(getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_MESSAGE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_ID), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_MODE)); + return new single_action_1.SingleAction(getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_ISSUE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_TITLE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_CHANGELOG), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_MESSAGE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_ID), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_COMMENT_MODE), getInput(input_keys_1.INPUT_KEYS.SINGLE_ACTION_OPERATION_ID)); } function getCommitPrefixBuilder(getInput) { return getInput(input_keys_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash'; @@ -52097,6 +52180,7 @@ exports.INPUT_KEYS = { SINGLE_ACTION_TITLE: 'single-action-title', SINGLE_ACTION_CHANGELOG: 'single-action-changelog', SINGLE_ACTION_MESSAGE: 'single-action-message', + SINGLE_ACTION_OPERATION_ID: 'single-action-operation-id', SINGLE_ACTION_COMMENT_ID: 'single-action-comment-id', SINGLE_ACTION_COMMENT_MODE: 'single-action-comment-mode', INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', @@ -52185,6 +52269,17 @@ exports.INPUT_KEYS = { // Workflows RELEASE_WORKFLOW: 'release-workflow', HOTFIX_WORKFLOW: 'hotfix-workflow', + RELEASE_RECONCILIATION_STRATEGY: 'release-reconciliation-strategy', + HOTFIX_RECONCILIATION_STRATEGY: 'hotfix-reconciliation-strategy', + RECONCILIATION_PR_MODE: 'reconciliation-pr-mode', + RECONCILIATION_BACKMERGE_MODE: 'reconciliation-backmerge-mode', + HOTFIX_ACTIVE_RELEASE_POLICY: 'hotfix-active-release-policy', + RECONCILIATION_TREE: 'reconciliation-tree', + RECONCILIATION_CLEANUP: 'reconciliation-cleanup', + RECONCILIATION_ISSUE_COMPLETION: 'reconciliation-issue-completion', + ORCHESTRATION_PRESENTATION_MODE: 'orchestration-presentation-mode', + ORCHESTRATION_DIAGRAMS: 'orchestration-diagrams', + ORCHESTRATION_COMMENT_MODE: 'orchestration-comment-mode', // Emoji EMOJI_LABELED_TITLE: 'emoji-labeled-title', BRANCH_MANAGEMENT_EMOJI: 'branch-management-emoji', @@ -53625,6 +53720,497 @@ function buildDeploymentMergePlan(configuration) { } +/***/ }), + +/***/ 1779: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.validateDeploymentContinuation = validateDeploymentContinuation; +/** + * Rejects forged, stale, or out-of-order workflow continuations before a + * publication-side mutation is attempted. Legacy releases without durable + * orchestration state remain supported during migration. + */ +function validateDeploymentContinuation(operation, expectedOperationId, allowedPhases, expectedVersion) { + if (!operation) + return undefined; + if (!expectedOperationId) + return "single-action-operation-id is required for a durable deployment continuation."; + if (expectedOperationId !== operation.operationId) { + return `Deployment operation mismatch: expected ${operation.operationId}, received ${expectedOperationId}.`; + } + if (!expectedVersion) + return "single-action-version is required for a durable publication continuation."; + if (expectedVersion !== operation.version) { + return `Deployment version mismatch: expected ${operation.version}, received ${expectedVersion}.`; + } + const effectivePhase = operation.phase === "blocked" && operation.lastFailure?.retryable + ? operation.lastFailure.previousPhase + : operation.phase; + if (!allowedPhases.includes(effectivePhase)) { + return `Deployment operation ${operation.operationId} cannot continue publication from phase ${operation.phase}.`; + } + return undefined; +} + + +/***/ }), + +/***/ 54037: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.projectDeploymentLabels = projectDeploymentLabels; +const copilot_lifecycle_1 = __nccwpck_require__(72418); +function projectDeploymentLabels(current, operation, labels) { + const managed = new Set((0, copilot_lifecycle_1.managedLifecycleLabelNames)(labels.lifecycle)); + let projected = current.filter((label) => !managed.has(label)); + if (operation.publicationVerified) { + projected = projected.filter((label) => label !== labels.deploy); + if (!projected.includes(labels.deployed)) + projected.push(labels.deployed); + } + const selectedMode = operation.selectedPrMode ?? operation.prMode; + if (operation.phase === "completed") + projected.push(labels.lifecycle.verified); + else if (operation.phase === "blocked") + projected.push(labels.lifecycle.blocked, labels.lifecycle.awaitingMaintainer); + else if ((operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending") && selectedMode === "create-only") { + projected.push(labels.lifecycle.ready, labels.lifecycle.awaitingMaintainer); + } + else if (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending") { + projected.push(labels.lifecycle.reviewing); + } + else { + projected.push(labels.lifecycle.inProgress); + } + return [...new Set(projected)]; +} + + +/***/ }), + +/***/ 8352: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildInitialDeploymentOperation = buildInitialDeploymentOperation; +exports.selectPullRequestMode = selectPullRequestMode; +exports.selectBackmergeMode = selectBackmergeMode; +exports.selectReconciliationTargetBranches = selectReconciliationTargetBranches; +exports.reconciliationSource = reconciliationSource; +exports.buildReconciliationTarget = buildReconciliationTarget; +exports.buildReconciliationBranchName = buildReconciliationBranchName; +exports.validateInitialDeploymentInput = validateInitialDeploymentInput; +function buildInitialDeploymentOperation(input) { + const strategy = input.kind === "release" + ? input.configuration.releaseReconciliationStrategy + : input.configuration.hotfixReconciliationStrategy; + return { + operationId: input.operationId, + kind: input.kind, + version: input.version, + title: input.title, + changelog: input.changelog, + phase: "preparing", + strategy, + prMode: input.configuration.reconciliationPullRequestMode, + backmergeMode: input.configuration.reconciliationBackmergeMode, + hotfixActiveReleasePolicy: input.configuration.hotfixActiveReleasePolicy, + cleanup: input.configuration.reconciliationCleanup, + issueCompletion: input.configuration.reconciliationIssueCompletion, + presentationMode: input.configuration.orchestrationPresentationMode, + diagrams: input.configuration.orchestrationDiagrams, + commentMode: input.configuration.orchestrationCommentMode, + sourceBranch: input.sourceBranch, + sourceSha: input.sourceSha, + originBranch: input.originBranch, + originSha: input.originSha, + productionBranch: input.productionBranch, + developmentBranch: input.developmentBranch, + reconciliationTree: input.configuration.reconciliationTree, + tag: `v${input.version}`, + publicationWorkflow: input.publicationWorkflow, + publicationVerified: false, + reconciliationTargets: [], + lastFailure: null, + }; +} +function selectPullRequestMode(configured, capabilities) { + if (configured === "create-only" || configured === "legacy-wait") { + return { kind: "mode", mode: configured, reason: "Explicitly configured." }; + } + if (configured === "merge-queue" || (configured === "auto" && capabilities.mergeQueueRequired)) { + return capabilities.mergeQueueRequired + ? { kind: "mode", mode: "merge-queue", reason: "The target requires its merge queue." } + : { kind: "unsupported", reason: "The target does not expose a required merge queue." }; + } + if (configured === "auto-merge") { + return capabilities.autoMergeAllowed + ? { kind: "mode", mode: "auto-merge", reason: "Native auto-merge was explicitly configured." } + : { kind: "unsupported", reason: "Native auto-merge is disabled for this repository." }; + } + if (capabilities.immediatelyMergeable) { + return { kind: "mode", mode: "auto-merge", reason: "GitHub reports the PR ready; native auto-merge preserves branch protection." }; + } + return capabilities.autoMergeAllowed + ? { kind: "mode", mode: "auto-merge", reason: "GitHub will merge after checks and reviews complete." } + : { kind: "mode", mode: "create-only", reason: "Repository auto-merge is unavailable; maintainer merge is required." }; +} +function selectBackmergeMode(configured, requiresStrictStatusChecks, directHeadIsUpToDate, directSourceIsExact = true) { + const directIsUnsafe = !directSourceIsExact + || (requiresStrictStatusChecks && !directHeadIsUpToDate); + if (configured === "direct" && directIsUnsafe) { + const reason = !directSourceIsExact + ? "Direct reconciliation was rejected because its source branch no longer points at the stored release SHA. Use auto or sync-branch to keep this operation isolated." + : "Direct reconciliation cannot satisfy the target's strict up-to-date rule without merging development into production. Use auto or sync-branch."; + return { kind: "unsupported", reason }; + } + if (configured === "sync-branch" || (configured === "auto" && directIsUnsafe)) { + return { + kind: "mode", + mode: "sync-branch", + reason: !directSourceIsExact + ? "A dedicated sync branch pins the stored release SHA after the source branch advanced." + : "A dedicated sync branch satisfies the target's strict up-to-date rule without changing production.", + }; + } + return { kind: "mode", mode: "direct", reason: "The exact source can be reconciled directly into this target." }; +} +function selectReconciliationTargetBranches(operation, activeReleaseBranches) { + if (operation.strategy === "manual") + return { kind: "manual" }; + if (operation.kind === "release") { + return { kind: "targets", targetBranches: [operation.developmentBranch] }; + } + const releases = [...new Set(activeReleaseBranches.filter(Boolean))]; + if (operation.hotfixActiveReleasePolicy !== "development" && releases.length > 1) { + return { kind: "blocked", reason: "Multiple active release branches require an explicit hotfix reconciliation decision." }; + } + if (operation.hotfixActiveReleasePolicy === "development" || releases.length === 0) { + return { kind: "targets", targetBranches: [operation.developmentBranch] }; + } + if (operation.hotfixActiveReleasePolicy === "prefer-release") { + return { kind: "targets", targetBranches: releases }; + } + return { kind: "targets", targetBranches: [...releases, operation.developmentBranch] }; +} +function reconciliationSource(operation) { + return operation.strategy === "canonical-gitflow" + ? { branch: operation.sourceBranch, sha: operation.sourceSha } + : { branch: operation.productionBranch, sha: operation.productionSha ?? "" }; +} +function buildReconciliationTarget(operation, targetBranch, mode) { + const source = reconciliationSource(operation); + return { + targetBranch, + sourceBranch: source.branch, + sourceSha: source.sha, + syncBranch: mode === "sync-branch" ? buildReconciliationBranchName(operation, targetBranch) : undefined, + status: "pending", + }; +} +function buildReconciliationBranchName(operation, targetBranch) { + const safeTarget = targetBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, ""); + const safeOperation = operation.operationId.replace(/[^A-Za-z0-9]/g, "").slice(0, 8).toLowerCase(); + return `${operation.reconciliationTree}/${operation.kind}-${operation.version}-to-${safeTarget}-${safeOperation}`; +} +function validateInitialDeploymentInput(input) { + const errors = []; + if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(input.version)) + errors.push("Version must use MAJOR.MINOR.PATCH format."); + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(input.sourceBranch)) + errors.push("Source branch is invalid."); + if (!/^[a-f0-9]{40}$/i.test(input.sourceSha)) + errors.push("Source SHA must be a full commit SHA."); + if (!/^[a-f0-9]{40}$/i.test(input.originSha)) + errors.push("Origin SHA must be a full commit SHA."); + if (input.sourceBranch === input.productionBranch || input.sourceBranch === input.developmentBranch) { + errors.push("A frozen release/hotfix branch is required as the deployment source."); + } + return errors; +} + + +/***/ }), + +/***/ 83221: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_DASHBOARD_MARKER = void 0; +exports.deploymentDashboardMarker = deploymentDashboardMarker; +exports.renderDeploymentDashboard = renderDeploymentDashboard; +exports.renderPromotionPullRequest = renderPromotionPullRequest; +exports.renderReconciliationPullRequest = renderReconciliationPullRequest; +exports.renderDeploymentJobSummary = renderDeploymentJobSummary; +exports.normalizeLocale = normalizeLocale; +const managed_pull_request_1 = __nccwpck_require__(95914); +const EN = { + release: "Release", hotfix: "Hotfix", currentStatus: "Current status", + noAction: "No action is required while GitHub owns the pending transition.", actionRequired: "Action required", + progress: "Progress", currentTransition: "Current transition", whatNext: "What happens next", links: "Links", + technical: "Technical details", alreadyPublished: "Package status: already published", notPublished: "Package status: not published", + productionUpdated: "Production updated", developmentSynchronized: "Development synchronized", yes: "Yes", no: "No", + from: "From", to: "To", state: "State", compare: "Compare changes", controlCenter: "Release control center", + purpose: "Purpose", afterMerge: "After merge", purposePromotion: "accept the prepared change in production", + purposeReconciliation: "bring the accepted production state back to the development line", + afterPromotion: "After merge, Copilot will tag and publish the accepted production commit.", + noRepublish: "Merging or closing this PR cannot publish the package again.", + origin: "Origin", preparedSource: "Prepared source", destination: "Destination", publication: "Publication", + productionFact: "Production fact", developmentTarget: "Development target", completionEffect: "Completion effect", + closeIssue: "Close issue after all targets", keepIssue: "Keep issue open", readyBeforeReview: "Ready before review", + buildValidation: "Build and release validation", packageSmoke: "Package smoke test", + protectedChecks: "Protected-branch checks and reviews", syncReason: "A dedicated sync branch preserves target-only commits and isolates target-dependent checks.", + protectedFacts: "What Copilot protected", cut: "Source cut", promotion: "Production promotion", reconciliation: "Development reconciliation", + cleanup: "Cleanup and issue completion", jobSummary: "Deployment orchestration", result: "Result", + externalWait: "Waiting externally", workflowFailure: "Workflow failed", previousPhase: "Previous phase", resultingPhase: "Resulting phase", + retryable: "Retryable", createdReused: "Created, reused, or skipped", fallback: "prepared -> production PR -> accepted -> published -> reconciled -> complete", + phase: { + preparing: "preparing the version", promotion_pr_pending: "waiting for production approval", promoted: "accepted in production", + publishing: "publishing artifacts", published: "published; preparing development reconciliation", + reconciliation_pending: "waiting for development reconciliation", completed: "completed", blocked: "needs attention", + }, + diagram: ["Source snapshot", "Version prepared", "Production PR", "Accepted in production", "Package and release", "Development reconciliation", "Complete"], +}; +const ES = { + release: "Release", hotfix: "Hotfix", currentStatus: "Estado actual", + noAction: "No se requiere ninguna acción mientras GitHub gestiona la transición pendiente.", actionRequired: "Acción necesaria", + progress: "Progreso", currentTransition: "Transición actual", whatNext: "Qué ocurrirá después", links: "Enlaces", + technical: "Detalles técnicos", alreadyPublished: "Estado del paquete: ya publicado", notPublished: "Estado del paquete: no publicado", + productionUpdated: "Producción actualizada", developmentSynchronized: "Desarrollo sincronizado", yes: "Sí", no: "No", + from: "Origen", to: "Destino", state: "Estado", compare: "Comparar cambios", controlCenter: "Centro de control de la release", + purpose: "Propósito", afterMerge: "Después del merge", purposePromotion: "aceptar en producción el cambio preparado", + purposeReconciliation: "llevar el estado aceptado en producción de vuelta a desarrollo", + afterPromotion: "Tras el merge, Copilot etiquetará y publicará el commit aceptado en producción.", + noRepublish: "Mergear o cerrar esta PR no puede volver a publicar el paquete.", + origin: "Origen", preparedSource: "Fuente preparada", destination: "Destino", publication: "Publicación", + productionFact: "Estado de producción", developmentTarget: "Destino de desarrollo", completionEffect: "Efecto al completar", + closeIssue: "Cerrar la issue tras todos los destinos", keepIssue: "Mantener la issue abierta", readyBeforeReview: "Listo antes de revisar", + buildValidation: "Build y validación de release", packageSmoke: "Smoke test del paquete", + protectedChecks: "Checks y revisiones de la rama protegida", syncReason: "Una rama de sincronización dedicada preserva los commits exclusivos del destino y aísla sus checks.", + protectedFacts: "Qué ha protegido Copilot", cut: "Corte de la fuente", promotion: "Promoción a producción", reconciliation: "Reconciliación con desarrollo", + cleanup: "Limpieza y cierre de la issue", jobSummary: "Orquestación del despliegue", result: "Resultado", + externalWait: "Esperando fuera del workflow", workflowFailure: "Workflow fallido", previousPhase: "Fase anterior", resultingPhase: "Fase resultante", + retryable: "Reintentable", createdReused: "Creado, reutilizado u omitido", fallback: "preparada -> PR de producción -> aceptada -> publicada -> reconciliada -> completada", + phase: { + preparing: "preparando la versión", promotion_pr_pending: "esperando aprobación en producción", promoted: "aceptada en producción", + publishing: "publicando artefactos", published: "publicada; preparando la reconciliación", + reconciliation_pending: "esperando reconciliación con desarrollo", completed: "completada", blocked: "necesita atención", + }, + diagram: ["Snapshot de origen", "Versión preparada", "PR de producción", "Aceptada en producción", "Paquete y release", "Reconciliación con desarrollo", "Completada"], +}; +exports.DEPLOYMENT_DASHBOARD_MARKER = "copilot-deployment-dashboard"; +function deploymentDashboardMarker(operationId, issue) { + return ``; +} +function renderDeploymentDashboard(operation, context) { + const messages = messagesFor(context.issueLocale); + const title = operation.kind === "release" ? messages.release : messages.hotfix; + const action = deploymentAction(operation, messages); + const lines = [ + deploymentDashboardMarker(operation.operationId, context.issue), "", + `# ${operation.phase === "blocked" ? "❌" : operation.phase === "completed" ? "✅" : "🚀"} ${title} ${inline(operation.version)}`, "", + `> **${messages.currentStatus}: ${messages.phase[operation.phase]}.**`, + ]; + if (action.required) + lines.push("", `## ${messages.actionRequired}`, "", action.message); + else + lines.push(`> ${action.message}`); + lines.push(""); + if (operation.phase === "blocked") + lines.push(...factTable(operation, messages), "", `## ${messages.protectedFacts}`, "", protectedFact(operation, messages), ""); + if (operation.presentationMode !== "quiet") + lines.push(`## ${messages.progress}`, "", ...progressLines(operation, messages), ""); + if (operation.presentationMode === "guided" && operation.diagrams) + lines.push(...deploymentDiagram(messages), ""); + if (operation.presentationMode !== "quiet") { + lines.push(`## ${messages.currentTransition}`, "", ...transitionTable(operation, messages), "", `## ${messages.whatNext}`, "", nextDescription(operation, messages), "", `## ${messages.links}`, "", deploymentLinks(operation, context, messages).join(" · "), ""); + } + lines.push("
", `${messages.technical}`, "", `- Operation: ${inline(operation.operationId)}`, `- Strategy: ${inline(operation.strategy)}`, `- PR mode: ${inline(operation.selectedPrMode ?? operation.prMode)}`, `- Source SHA: ${inline(operation.sourceSha)}`, `- Production SHA: ${inline(operation.productionSha ?? "pending")}`, "
"); + return lines.join("\n"); +} +function renderPromotionPullRequest(operation, context) { + const messages = messagesFor(context.pullRequestLocale); + const kind = operation.kind === "release" ? "release" : "hotfix"; + const title = `${kind}(${safeText(operation.version)}): promote to ${safeText(operation.productionBranch)}`; + const body = [ + `# 🚀 ${capitalize(messages.purposePromotion)}`, "", + `> **${messages.purpose}:** ${capitalize(messages.purposePromotion)}.`, + `> **${messages.afterMerge}:** ${messages.afterPromotion}`, "", + `| ${messages.origin} | ${messages.preparedSource} | ${messages.destination} | ${messages.publication} |`, + "|---|---|---|---|", + `| ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)} | ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)} | ${inline(operation.productionBranch)} | ${messages.afterMerge} |`, "", + `## ${messages.readyBeforeReview}`, "", + `- ✅ ${messages.buildValidation}`, `- ✅ ${messages.packageSmoke}`, `- ⏳ ${messages.protectedChecks}`, "", + `## ${messages.afterMerge}`, "", + `- ${messages.afterPromotion}`, `- ${messages.reconciliation}: ${inline(operation.developmentBranch)}.`, "", + `[${messages.compare}](${compareUrl(context, operation.productionBranch, operation.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", + "
", `${messages.technical}`, "", + `Operation ${inline(operation.operationId)}; strategy ${inline(operation.strategy)}; merge mode ${inline(operation.prMode)}.`, + "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "promotion", issue: context.issue }), + ].join("\n"); + return { title, body }; +} +function renderReconciliationPullRequest(operation, target, context) { + const messages = messagesFor(context.pullRequestLocale); + const kind = operation.kind === "release" ? "release" : "hotfix"; + const title = `${kind}(${safeText(operation.version)}): reconcile ${safeText(target.sourceBranch)} into ${safeText(target.targetBranch)}`; + const body = [ + `# 🔄 ${capitalize(messages.purposeReconciliation)}`, "", + `> **${messages.alreadyPublished}.** ${messages.noRepublish}`, "", + `| ${messages.productionFact} | ${messages.developmentTarget} | ${messages.completionEffect} |`, "|---|---|---|", + `| ${inline(`${operation.tag}@${shortSha(operation.productionSha ?? target.sourceSha)}`)} | ${inline(target.targetBranch)} | ${operation.issueCompletion === "close" ? messages.closeIssue : messages.keepIssue} |`, "", + ...(target.syncBranch ? [`${messages.syncReason} ${inline(target.syncBranch)}`, ""] : []), + `${messages.noRepublish}`, "", + `[${messages.compare}](${compareUrl(context, target.targetBranch, target.syncBranch ?? target.sourceBranch)}) · [${messages.controlCenter}](${issueUrl(context)})`, "", + "
", `${messages.technical}`, "", `Operation ${inline(operation.operationId)}; source SHA ${inline(target.sourceSha)}.`, + "
", "", (0, managed_pull_request_1.buildManagedPullRequestMarker)({ operationId: operation.operationId, phase: "reconciliation", issue: context.issue }), + ].join("\n"); + return { title, body }; +} +function renderDeploymentJobSummary(operation, context, previousPhase, operations = []) { + const messages = messagesFor(context.issueLocale); + const externallyPending = operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"; + const result = operation.phase === "blocked" ? messages.workflowFailure : externallyPending ? messages.externalWait : messages.phase[operation.phase]; + return [ + `# ${operation.phase === "blocked" ? "❌" : externallyPending ? "⏳" : "✅"} ${messages.jobSummary}`, "", + `> **${messages.result}: ${result}.**`, "", + `| ${messages.previousPhase} | ${messages.resultingPhase} | ${messages.retryable} |`, "|---|---|---|", + `| ${inline(previousPhase ?? operation.phase)} | ${inline(operation.phase)} | ${operation.lastFailure?.retryable ? messages.yes : messages.no} |`, "", + `- Operation: ${inline(operation.operationId)}`, + `- ${messages.origin}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, + `- ${messages.preparedSource}: ${inline(`${operation.sourceBranch}@${shortSha(operation.sourceSha)}`)}`, + `- ${messages.productionFact}: ${inline(operation.productionSha ? `${operation.productionBranch}@${shortSha(operation.productionSha)}` : "pending")}`, + `- ${messages.publication}: ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- ${messages.createdReused}: ${safeText(operations.join(", ") || "none")}`, "", + deploymentLinks(operation, context, messages).join(" · "), + ].join("\n"); +} +function progressLines(operation, messages) { + const phase = operation.phase === "blocked" ? operation.lastFailure?.previousPhase ?? "preparing" : operation.phase; + const reached = (expected) => phaseRank(phase) >= phaseRank(expected); + return [ + `- [x] ${messages.cut}: ${inline(`${operation.originBranch}@${shortSha(operation.originSha)}`)}`, + `- [x] ${messages.buildValidation} + ${messages.packageSmoke}`, + `- [${operation.productionSha || reached("promoted") ? "x" : " "}] ${messages.promotion}: ${inline(operation.productionBranch)}`, + `- [${operation.publicationVerified ? "x" : " "}] ${operation.publicationVerified ? messages.alreadyPublished : messages.notPublished}`, + `- [${reconciliationCompleted(operation) ? "x" : " "}] ${messages.reconciliation}: ${inline(operation.developmentBranch)}`, + `- [${operation.phase === "completed" ? "x" : " "}] ${messages.cleanup}`, + ]; +} +function deploymentDiagram(messages) { + const [source, prepared, production, accepted, publication, reconciliation, complete] = messages.diagram; + return [ + "```mermaid", "flowchart LR", ` D[${source}] --> R[${prepared}]`, ` R --> P[${production}]`, + ` P --> A[${accepted}]`, ` A --> N[${publication}]`, ` N --> B[${reconciliation}]`, ` B --> C[${complete}]`, "```", "", messages.fallback, + ]; +} +function transitionTable(operation, messages) { + const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + const from = activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch; + const to = activeTarget?.targetBranch ?? operation.productionBranch; + return [`| ${messages.from} | ${messages.to} | ${messages.state} |`, "|---|---|---|", `| ${inline(from)} | ${inline(to)} | ${inline(messages.phase[operation.phase])} |`]; +} +function factTable(operation, messages) { + return [ + `| ${messages.productionUpdated} | ${messages.alreadyPublished} | ${messages.developmentSynchronized} |`, "|---|---|---|", + `| ${operation.productionSha ? messages.yes : messages.no} | ${operation.publicationVerified ? messages.yes : messages.no} | ${reconciliationCompleted(operation) ? messages.yes : messages.no} |`, + ]; +} +function protectedFact(operation, messages) { + if (operation.publicationVerified) + return `${messages.alreadyPublished}; ${messages.noRepublish}`; + if (operation.productionSha) + return `${messages.productionUpdated}: ${messages.yes}. ${messages.notPublished}.`; + return `${messages.productionUpdated}: ${messages.no}. ${messages.notPublished}.`; +} +function nextDescription(operation, messages) { + if (operation.phase === "blocked") { + const diagnostic = safeText(operation.lastFailure?.message ?? messages.workflowFailure); + return `${diagnostic}. ${operation.lastFailure?.retryable ? `${messages.actionRequired}: retry after correcting the cause.` : `${messages.actionRequired}: manual intervention is required.`}`; + } + if (operation.phase === "promotion_pr_pending") + return messages.afterPromotion; + if (operation.phase === "publishing" || operation.phase === "promoted") + return messages.afterPromotion; + if (operation.phase === "reconciliation_pending" || operation.phase === "published") + return messages.noRepublish; + if (operation.phase === "completed") + return `${messages.productionUpdated}: ${messages.yes}. ${messages.developmentSynchronized}: ${messages.yes}.`; + return `${messages.promotion}: ${inline(operation.sourceBranch)} -> ${inline(operation.productionBranch)}.`; +} +function deploymentAction(operation, messages) { + const manual = (operation.selectedPrMode ?? operation.prMode) === "create-only" + && (operation.phase === "promotion_pr_pending" || operation.phase === "reconciliation_pending"); + if (manual) + return { required: true, message: `${messages.protectedChecks}: review and merge the managed PR when GitHub reports it ready.` }; + if (operation.phase !== "blocked") + return { required: false, message: messages.noAction }; + return { + required: true, + message: operation.lastFailure?.retryable + ? `${safeText(operation.lastFailure.message)}. Retry after correcting the cause.` + : `${safeText(operation.lastFailure?.message ?? messages.workflowFailure)}. Manual intervention is required.`, + }; +} +function deploymentLinks(operation, context, messages) { + const links = [`[${messages.controlCenter}](${issueUrl(context)})`]; + links.push(`[${safeText(operation.sourceBranch)} branch](${branchUrl(context, operation.sourceBranch)})`); + links.push(`[${shortSha(operation.originSha)} origin commit](${commitUrl(context, operation.originSha)})`); + links.push(`[${shortSha(operation.sourceSha)} prepared commit](${commitUrl(context, operation.sourceSha)})`); + const activeTarget = operation.reconciliationTargets.find((target) => target.status !== "completed"); + links.push(`[${messages.compare}](${compareUrl(context, activeTarget?.targetBranch ?? operation.productionBranch, activeTarget?.syncBranch ?? activeTarget?.sourceBranch ?? operation.sourceBranch)})`); + if (operation.promotionPullRequest) + links.push(`[Promotion PR #${operation.promotionPullRequest}](${pullRequestUrl(context, operation.promotionPullRequest)})`); + for (const target of operation.reconciliationTargets) { + if (target.pullRequest) + links.push(`[Reconciliation PR #${target.pullRequest}](${pullRequestUrl(context, target.pullRequest)})`); + } + if (operation.productionSha) + links.push(`[${shortSha(operation.productionSha)} production commit](${commitUrl(context, operation.productionSha)})`); + if (operation.publicationVerified) { + links.push(`[${safeText(operation.tag)} GitHub Release](${repositoryUrl(context)}/releases/tag/${encodeURIComponent(operation.tag)})`); + links.push(`[v${safeText(operation.version.split(".")[0])} Action tag](${branchUrl(context, `v${operation.version.split(".")[0]}`)})`); + if (context.packageName) + links.push(`[${safeText(context.packageName)}@${safeText(operation.version)} on npm](${npmVersionUrl(context.packageName, operation.version)})`); + } + if (context.workflowRunUrl) + links.push(`[Workflow run](${safeUrl(context.workflowRunUrl)})`); + return links; +} +function messagesFor(locale) { return normalizeLocale(locale) === "es-ES" ? ES : EN; } +function normalizeLocale(locale) { return locale.toLowerCase().startsWith("es") ? "es-ES" : "en-US"; } +function reconciliationCompleted(operation) { + return operation.phase === "completed" + || (operation.reconciliationTargets.length > 0 + && operation.reconciliationTargets.every((target) => target.status === "completed")); +} +function phaseRank(phase) { return ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"].indexOf(phase); } +function repositoryUrl(context) { return `https://github.com/${encodeURIComponent(context.owner)}/${encodeURIComponent(context.repository)}`; } +function issueUrl(context) { return `${repositoryUrl(context)}/issues/${context.issue}`; } +function pullRequestUrl(context, number) { return `${repositoryUrl(context)}/pull/${number}`; } +function branchUrl(context, branch) { return `${repositoryUrl(context)}/tree/${encodeURIComponent(branch)}`; } +function commitUrl(context, sha) { return `${repositoryUrl(context)}/commit/${encodeURIComponent(sha)}`; } +function npmVersionUrl(packageName, version) { return `https://www.npmjs.com/package/${encodeURIComponent(packageName)}/v/${encodeURIComponent(version)}`; } +function compareUrl(context, base, head) { return `${repositoryUrl(context)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`; } +function inline(value) { return `\`${safeText(value)}\``; } +function safeText(value) { return value.replace(/[\r\n`<>]/g, "").replace(/@/g, "@\u200b").replace(/::/g, "﹕﹕").slice(0, 240); } +function safeMarkerValue(value) { return value.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 128); } +function safeUrl(value) { return /^https:\/\/github\.com\//.test(value) ? value : "https://github.com"; } +function shortSha(value) { return safeText(value).slice(0, 7); } +function capitalize(value) { return value.charAt(0).toUpperCase() + value.slice(1); } + + /***/ }), /***/ 72712: @@ -54363,6 +54949,7 @@ exports.createDefaultSetupConfiguration = createDefaultSetupConfiguration; exports.mergeSetupConfiguration = mergeSetupConfiguration; const agent_1 = __nccwpck_require__(89040); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); exports.SETUP_AGENT_TASKS = [ 'planner', 'findings', @@ -54439,6 +55026,7 @@ function createDefaultSetupConfiguration() { issueLocale: 'en-US', pullRequestLocale: 'en-US', commitPrefixTransforms: 'replace-slash', + ...deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION, }, ai: { pullRequestDescription: true, @@ -54608,6 +55196,17 @@ function buildSetupRepositoryVariables(configuration) { add('ISSUES_LOCALE', repository.issueLocale); add('PULL_REQUESTS_LOCALE', repository.pullRequestLocale); add('COMMIT_PREFIX_TRANSFORMS', repository.commitPrefixTransforms); + add('RELEASE_RECONCILIATION_STRATEGY', repository.releaseReconciliationStrategy); + add('HOTFIX_RECONCILIATION_STRATEGY', repository.hotfixReconciliationStrategy); + add('RECONCILIATION_PR_MODE', repository.reconciliationPullRequestMode); + add('RECONCILIATION_BACKMERGE_MODE', repository.reconciliationBackmergeMode); + add('HOTFIX_ACTIVE_RELEASE_POLICY', repository.hotfixActiveReleasePolicy); + add('RECONCILIATION_TREE', repository.reconciliationTree); + add('RECONCILIATION_CLEANUP', repository.reconciliationCleanup); + add('RECONCILIATION_ISSUE_COMPLETION', repository.reconciliationIssueCompletion); + add('ORCHESTRATION_PRESENTATION_MODE', repository.orchestrationPresentationMode); + add('ORCHESTRATION_DIAGRAMS', repository.orchestrationDiagrams); + add('ORCHESTRATION_COMMENT_MODE', repository.orchestrationCommentMode); add('AI_PULL_REQUEST_DESCRIPTION', configuration.ai.pullRequestDescription); add('AI_PULL_REQUEST_DESCRIPTION_MODE', configuration.ai.pullRequestDescriptionMode); add('AI_IGNORE_FILES', configuration.ai.ignoreFiles); @@ -54653,6 +55252,17 @@ function buildSetupActionInputs(configuration) { 'issues-locale': repository.issueLocale, 'pull-requests-locale': repository.pullRequestLocale, 'commit-prefix-transforms': repository.commitPrefixTransforms, + 'release-reconciliation-strategy': repository.releaseReconciliationStrategy, + 'hotfix-reconciliation-strategy': repository.hotfixReconciliationStrategy, + 'reconciliation-pr-mode': repository.reconciliationPullRequestMode, + 'reconciliation-backmerge-mode': repository.reconciliationBackmergeMode, + 'hotfix-active-release-policy': repository.hotfixActiveReleasePolicy, + 'reconciliation-tree': repository.reconciliationTree, + 'reconciliation-cleanup': repository.reconciliationCleanup, + 'reconciliation-issue-completion': repository.reconciliationIssueCompletion, + 'orchestration-presentation-mode': repository.orchestrationPresentationMode, + 'orchestration-diagrams': String(repository.orchestrationDiagrams), + 'orchestration-comment-mode': repository.orchestrationCommentMode, 'ai-pull-request-description': String(ai.pullRequestDescription), 'ai-pull-request-description-mode': (0, pull_request_description_1.normalizePullRequestDescriptionMode)(ai.pullRequestDescriptionMode), 'ai-ignore-files': ai.ignoreFiles, @@ -54702,6 +55312,9 @@ function buildSetupWarnings(configuration) { if (configuration.features.release !== false && configuration.features.hotfix !== false) { warnings.push('Release and hotfix workflows require the workflow PAT Secret and a writable token.'); } + if (configuration.repository.reconciliationPullRequestMode === 'merge-queue') { + warnings.push('Merge queue mode requires every required first-party and third-party check to support the merge_group event; setup can validate only the bundled Copilot bridge.'); + } if (configuration.ai.provisioningMode === 'always') { warnings.push('Always-provision mode requires pinned CLI versions or a Cursor installer checksum in repository Variables.'); } @@ -54901,6 +55514,7 @@ const setup_configuration_defaults_1 = __nccwpck_require__(23381); const agent_configuration_validation_policy_1 = __nccwpck_require__(60596); const setup_configuration_storage_policy_1 = __nccwpck_require__(2554); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); function validateSetupConfiguration(configuration) { const errors = []; const nonEmpty = [ @@ -54949,6 +55563,25 @@ function validateSetupConfiguration(configuration) { if (!['auto', 'always', 'disabled'].includes(configuration.ai.provisioningMode)) { errors.push('Agent provisioning must be auto, always, or disabled.'); } + errors.push(...(0, deployment_configuration_1.validateDeploymentConfiguration)({ + releaseReconciliationStrategy: configuration.repository.releaseReconciliationStrategy, + hotfixReconciliationStrategy: configuration.repository.hotfixReconciliationStrategy, + reconciliationPullRequestMode: configuration.repository.reconciliationPullRequestMode, + reconciliationBackmergeMode: configuration.repository.reconciliationBackmergeMode, + hotfixActiveReleasePolicy: configuration.repository.hotfixActiveReleasePolicy, + reconciliationTree: configuration.repository.reconciliationTree, + reconciliationCleanup: configuration.repository.reconciliationCleanup, + reconciliationIssueCompletion: configuration.repository.reconciliationIssueCompletion, + orchestrationPresentationMode: configuration.repository.orchestrationPresentationMode, + orchestrationDiagrams: configuration.repository.orchestrationDiagrams, + orchestrationCommentMode: configuration.repository.orchestrationCommentMode, + }, { + productionBranch: configuration.repository.mainBranch, + developmentBranch: configuration.repository.developmentBranch, + releaseTree: configuration.repository.releaseTree, + hotfixTree: configuration.repository.hotfixTree, + mergeQueueWorkflowSupported: true, + })); errors.push(...(0, setup_configuration_storage_policy_1.validateStorageConfiguration)(configuration.storage)); for (const task of setup_configuration_defaults_1.SETUP_AGENT_TASKS) { const agent = configuration.agents[task]; @@ -55677,11 +56310,16 @@ exports.runCreateRelease = runCreateRelease; const result_1 = __nccwpck_require__(73817); const logging_ports_1 = __nccwpck_require__(6152); const create_release_policy_1 = __nccwpck_require__(76549); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runCreateRelease(param, taskId, repositoryReleasePort) { + const operation = param.currentConfiguration.deploymentOrchestration; + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(operation, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return [failureResult(taskId, continuationError)]; const input = { - version: param.singleAction.version, - title: param.singleAction.title, - changelog: param.singleAction.changelog, + version: param.singleAction.version || operation?.version || '', + title: param.singleAction.title || operation?.title || '', + changelog: param.singleAction.changelog || operation?.changelog || '', }; const validationError = (0, create_release_policy_1.validateReleaseInput)(input); if (validationError) { @@ -55755,13 +56393,18 @@ exports.runCreateTag = runCreateTag; const result_1 = __nccwpck_require__(73817); const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runCreateTag(param, taskId, repositoryTagPort) { const validationFailure = validateTagInput(param, taskId); if (validationFailure) return [validationFailure]; - const tagName = `v${param.singleAction.version}`; + const operation = param.currentConfiguration.deploymentOrchestration; + const version = param.singleAction.version || operation?.version || ''; + const tagName = `v${version}`; try { - const sha1Tag = await repositoryTagPort.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token); + const sha1Tag = operation?.productionSha + ? await repositoryTagPort.createOrVerifyTagAtSha(param.owner, param.repo, operation.productionSha, tagName, param.tokens.token) + : await repositoryTagPort.createTag(param.owner, param.repo, param.currentConfiguration.releaseBranch, tagName, param.tokens.token); return sha1Tag ? [new result_1.Result({ id: taskId, success: true, executed: true, steps: [`Tag ${tagName} is ready: ${sha1Tag}`] })] : noTagResult(taskId, tagName); } @@ -55771,11 +56414,15 @@ async function runCreateTag(param, taskId, repositoryTagPort) { } } function validateTagInput(param, taskId) { - if (param.singleAction.version.length === 0) { + const operation = param.currentConfiguration.deploymentOrchestration; + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(operation, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [continuationError] }); + if (param.singleAction.version.length === 0 && !operation?.version) { (0, logging_ports_1.logError)('Version is not set.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); } - if (param.currentConfiguration.releaseBranch === undefined) { + if (!operation?.productionSha && param.currentConfiguration.releaseBranch === undefined) { (0, logging_ports_1.logError)('Working branch not found in configuration.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: ['Release branch not found in issue configuration.'] }); } @@ -55936,6 +56583,591 @@ function mergeFailureResult(param, mergesAttempted) { } +/***/ }), + +/***/ 36850: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentOrchestrationUseCase = void 0; +const deployment_plan_policy_1 = __nccwpck_require__(8352); +const deployment_presentation_policy_1 = __nccwpck_require__(83221); +const deployment_operation_1 = __nccwpck_require__(92730); +const managed_pull_request_1 = __nccwpck_require__(95914); +const result_1 = __nccwpck_require__(73817); +const deployment_lifecycle_policy_1 = __nccwpck_require__(54037); +const TASK_ID = "DeploymentOrchestrationUseCase"; +class DeploymentOrchestrationUseCase { + constructor(dependencies) { + this.dependencies = dependencies; + this.taskId = TASK_ID; + this.checkpoints = new WeakMap(); + } + async invoke(execution) { + const initial = execution.currentConfiguration.deploymentOrchestration; + this.checkpoints.set(execution, initial ? { operationId: initial.operationId, phase: initial.phase } : undefined); + try { + if (execution.singleAction.isPrepareDeploymentAction) + return [await this.prepare(execution)]; + if (execution.singleAction.isDeployedAction && initial) + return [await this.published(execution)]; + if (execution.singleAction.isContinueDeploymentAction) + return [await this.continue(execution)]; + if (execution.singleAction.isPublishedDeploymentAction) + return [await this.published(execution)]; + if (execution.singleAction.isFailedDeploymentAction) + return [await this.failed(execution)]; + return []; + } + catch (error) { + await this.recordUnexpectedFailure(execution, error); + return [new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: ["Deployment orchestration is blocked. No unsafe transition was performed."], + errors: [error], + })]; + } + } + async prepare(execution) { + const existing = execution.currentConfiguration.deploymentOrchestration; + if (existing) { + if (existing.version !== execution.singleAction.version) { + throw new Error(`Issue already owns deployment operation ${existing.operationId} for version ${existing.version}.`); + } + if (existing.phase === "blocked" + && (!existing.lastFailure?.retryable + || !["preparing", "promotion_pr_pending"].includes(existing.lastFailure.previousPhase))) { + await this.publishDashboard(execution, existing); + return blockedResult(existing, "The prepare mode cannot resume this blocked deployment phase."); + } + const resumed = existing.phase === "blocked" ? (0, deployment_operation_1.resumeBlockedDeployment)(existing) : undefined; + const current = resumed?.kind === "advance" ? resumed.operation : existing; + if (current !== existing) { + execution.currentConfiguration.deploymentOrchestration = current; + await this.persist(execution); + } + if (current.phase === "preparing" || current.phase === "promotion_pr_pending") { + const currentSourceSha = await this.dependencies.git.getBranchSha(execution.owner, execution.repo, current.sourceBranch, execution.tokens.token); + if (currentSourceSha !== current.sourceSha) { + return await this.block(execution, current, "promotion", "The prepared source branch changed after its immutable SHA was stored.", false); + } + return await this.ensurePromotion(execution, current); + } + await this.publishDashboard(execution, current); + return success(`Deployment ${current.operationId} is already ${current.phase}; reused its durable state.`); + } + const kind = deploymentKind(execution); + const sourceBranch = kind === "release" + ? execution.currentConfiguration.releaseBranch + : execution.currentConfiguration.hotfixBranch; + if (!sourceBranch) + throw new Error(`No prepared ${kind} branch is stored on the launcher issue.`); + const sourceSha = await this.dependencies.git.getBranchSha(execution.owner, execution.repo, sourceBranch, execution.tokens.token); + const originBranch = kind === "release" + ? execution.currentConfiguration.releaseOriginBranch ?? execution.branches.development + : execution.currentConfiguration.hotfixOriginBranch ?? execution.currentConfiguration.parentBranch ?? execution.branches.defaultBranch; + const persistedOrigin = kind === "release" + ? execution.currentConfiguration.releaseOriginSha + : execution.currentConfiguration.hotfixOriginSha; + const originSha = persistedOrigin ?? await this.dependencies.git.getMergeBaseSha(execution.owner, execution.repo, originBranch, sourceBranch, execution.tokens.token); + const operation = (0, deployment_plan_policy_1.buildInitialDeploymentOperation)({ + operationId: this.dependencies.operationId(), + kind, + version: execution.singleAction.version, + title: execution.singleAction.title, + changelog: execution.singleAction.changelog, + sourceBranch, + sourceSha, + originBranch, + originSha, + productionBranch: execution.branches.defaultBranch, + developmentBranch: execution.branches.development, + configuration: execution.deployment, + publicationWorkflow: kind === "release" ? execution.workflows.release : execution.workflows.hotfix, + }); + const errors = (0, deployment_plan_policy_1.validateInitialDeploymentInput)({ + operationId: operation.operationId, + kind, + version: operation.version, + title: operation.title, + changelog: operation.changelog, + sourceBranch, + sourceSha, + originBranch, + originSha, + productionBranch: operation.productionBranch, + developmentBranch: operation.developmentBranch, + configuration: execution.deployment, + publicationWorkflow: operation.publicationWorkflow, + }); + if (errors.length > 0) + throw new Error(errors.join(" ")); + execution.currentConfiguration.deploymentOrchestration = operation; + if (kind === "release") { + execution.currentConfiguration.releaseOriginBranch = originBranch; + execution.currentConfiguration.releaseOriginSha = originSha; + } + else { + execution.currentConfiguration.hotfixOriginSha = originSha; + } + await this.persist(execution); + await this.publishDashboard(execution, operation); + return await this.ensurePromotion(execution, operation); + } + async ensurePromotion(execution, operation) { + const promotion = await this.createOrReusePullRequest(execution, operation, "promotion"); + if (promotion.merged) + return await this.advancePromotion(execution, operation, promotion); + if (promotion.state === "closed") + return await this.block(execution, operation, "promotion", `Promotion PR #${promotion.number} was closed without merge.`, true); + if (promotion.headSha !== operation.sourceSha) { + return await this.block(execution, operation, "promotion", `Promotion PR #${promotion.number} does not contain the persisted prepared SHA.`, false); + } + const pending = operation.phase === "promotion_pr_pending" + ? { ...operation, promotionPullRequest: promotion.number } + : (0, deployment_operation_1.transitionDeploymentOperation)({ ...operation, promotionPullRequest: promotion.number }, "preparing", "promotion_pr_pending").operation; + if (pending.phase !== "promotion_pr_pending") + throw new Error(`Cannot prepare promotion from ${operation.phase}.`); + execution.currentConfiguration.deploymentOrchestration = pending; + await this.persist(execution); + const managed = await this.configureMergeBehavior(execution, pending, promotion); + return success(managed.selectedPrMode === "create-only" + ? `Promotion PR #${promotion.number} is ready for maintainer review; this runner does not wait.` + : `Promotion PR #${promotion.number} is managed by GitHub; this runner does not wait for checks.`); + } + async continue(execution) { + let operation = requireOperation(execution); + const pullRequestNumber = execution.pullRequest.number; + if (pullRequestNumber < 1) + throw new Error("The continuation event has no pull request number."); + const pullRequest = await this.dependencies.pullRequests.getPullRequest(execution.owner, execution.repo, pullRequestNumber, execution.tokens.token); + const identity = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body); + if (!identity || identity.operationId !== operation.operationId || identity.issue !== execution.singleAction.issue) { + throw new Error(`PR #${pullRequest.number} is not owned by deployment operation ${operation.operationId}.`); + } + if (operation.phase === "blocked") { + const previousPhase = operation.lastFailure?.previousPhase; + const eventCanResume = operation.lastFailure?.retryable === true + && (identity.phase === "promotion" + ? previousPhase === "preparing" || previousPhase === "promotion_pr_pending" + : previousPhase === "reconciliation_pending"); + if (!eventCanResume) { + await this.publishDashboard(execution, operation); + return success(`PR #${pullRequest.number} cannot resume the existing ${operation.lastFailure?.category ?? "deployment"} block; the original diagnosis was preserved.`); + } + const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); + if (resumed.kind === "advance") { + operation = resumed.operation; + execution.currentConfiguration.deploymentOrchestration = operation; + await this.persist(execution); + } + } + if (pullRequest.repositoryFullName.toLowerCase() !== `${execution.owner}/${execution.repo}`.toLowerCase()) { + throw new Error("Cross-repository deployment continuation was rejected."); + } + if (pullRequest.state !== "closed") + return success(`PR #${pullRequest.number} is still open; no transition was applied.`); + if (!pullRequest.merged) { + return await this.block(execution, operation, identity.phase === "promotion" ? "promotion" : "reconciliation", `Managed ${identity.phase} PR #${pullRequest.number} was closed without merge.`, true); + } + if (identity.phase === "promotion") + return await this.advancePromotion(execution, operation, pullRequest); + return await this.advanceReconciliation(execution, operation, pullRequest); + } + async advancePromotion(execution, operation, pullRequest) { + if (["promoted", "publishing", "published", "reconciliation_pending", "completed"].includes(operation.phase)) { + return success(`Duplicate promotion event for PR #${pullRequest.number} was ignored; operation is ${operation.phase}.`); + } + if (operation.phase !== "promotion_pr_pending" && operation.phase !== "preparing") { + return success(`Out-of-order promotion event was ignored while operation is ${operation.phase}.`); + } + if (pullRequest.headBranch !== operation.sourceBranch || pullRequest.baseBranch !== operation.productionBranch || pullRequest.headSha !== operation.sourceSha) { + return await this.block(execution, operation, "promotion", "Promotion PR branches or prepared SHA do not match durable state.", false); + } + const productionSha = pullRequest.mergeCommitSha; + if (!productionSha) + return await this.block(execution, operation, "promotion", "Merged promotion PR has no production merge SHA.", true); + const [mergeReachable, sourceReachable] = await Promise.all([ + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, productionSha, execution.tokens.token), + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, operation.sourceSha, execution.tokens.token), + ]); + if (!mergeReachable || !sourceReachable) { + return await this.block(execution, operation, "promotion", "GitHub does not confirm that the accepted production branch contains the promotion commit.", true); + } + let promoted = { ...operation, promotionPullRequest: pullRequest.number, productionSha, phase: "promoted", lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = promoted; + await this.persist(execution); + promoted = { ...promoted, phase: "publishing" }; + execution.currentConfiguration.deploymentOrchestration = promoted; + await this.persist(execution); + await this.publishDashboard(execution, promoted); + await this.publishMilestone(execution, promoted, "promotion-merged", `✅ Promotion PR #${pullRequest.number} merged. Publication is starting from production SHA \`${productionSha}\`.`); + await this.dependencies.continuation.dispatch(execution.owner, execution.repo, operation.publicationWorkflow, operation.productionBranch, operation.operationId, execution.singleAction.issue, operation.version, execution.tokens.token); + return success(`Promotion PR #${pullRequest.number} was verified; publication continuation was dispatched from ${operation.productionBranch}.`); + } + async published(execution) { + let operation = requireOperation(execution); + if (operation.phase === "blocked" && operation.lastFailure?.retryable) { + const resumed = (0, deployment_operation_1.resumeBlockedDeployment)(operation); + if (resumed.kind === "advance") { + operation = resumed.operation; + execution.currentConfiguration.deploymentOrchestration = operation; + await this.persist(execution); + } + } + if (operation.phase === "reconciliation_pending" && operation.publicationVerified) { + return await this.ensureNextReconciliation(execution, operation) + ?? success(`Publication for ${operation.tag} was already verified; reconciliation state was recovered.`); + } + if (operation.phase === "completed" && operation.publicationVerified) { + await this.publishDashboard(execution, operation); + return success(`Publication for ${operation.tag} was already verified; duplicate notification ignored.`); + } + if (operation.phase !== "published" && operation.phase !== "publishing" && operation.phase !== "promoted") { + throw new Error(`Publication cannot advance from phase ${operation.phase}.`); + } + if (!operation.productionSha) + throw new Error("The accepted production SHA is missing."); + const reachable = await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, operation.productionBranch, operation.productionSha, execution.tokens.token); + if (!reachable) + return await this.block(execution, operation, "publication", "Published SHA is not reachable from the stored production branch.", false); + let published = { ...operation, phase: "published", publicationVerified: true, lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = published; + await this.persist(execution); + await this.publishMilestone(execution, published, "publication-complete", `📦 ${published.tag} is published from accepted production SHA \`${published.productionSha}\`.`); + const activeReleases = operation.kind === "hotfix" + ? (await this.dependencies.git.listBranches(execution.owner, execution.repo, execution.branches.releaseTree, execution.tokens.token)) + .filter((branch) => branch !== operation.sourceBranch) + : []; + const decision = (0, deployment_plan_policy_1.selectReconciliationTargetBranches)(published, activeReleases); + if (decision.kind === "blocked") + return await this.block(execution, published, "reconciliation", decision.reason, false); + if (decision.kind === "manual") { + await this.publishDashboard(execution, published); + return success(`${published.tag} is published. Manual reconciliation is configured, so the issue remains open.`); + } + published = { + ...published, + reconciliationTargets: decision.targetBranches.map((target) => (0, deployment_plan_policy_1.buildReconciliationTarget)(published, target, "direct")), + phase: "reconciliation_pending", + }; + execution.currentConfiguration.deploymentOrchestration = published; + await this.persist(execution); + return await this.ensureNextReconciliation(execution, published) + ?? success(`${published.tag} is published; development reconciliation is now managed by GitHub.`); + } + async failed(execution) { + const operation = requireOperation(execution); + if (operation.phase === "completed") + return success(`Deployment ${operation.operationId} is already complete; a stale failure report was ignored.`); + if (operation.phase === "blocked") { + await this.publishDashboard(execution, operation); + return new result_1.Result({ + id: TASK_ID, + success: false, + executed: true, + steps: [`Deployment ${operation.operationId} remains blocked; its original failure classification was preserved.`], + errors: [new Error(operation.lastFailure?.message ?? "Deployment remains blocked.")], + }); + } + const category = operation.phase === "preparing" || operation.phase === "promotion_pr_pending" + ? "promotion" + : operation.phase === "promoted" || operation.phase === "publishing" + ? "publication" + : operation.lastFailure?.category ?? "reconciliation"; + const message = execution.singleAction.message || `The ${category} workflow failed. Review the linked workflow run before retrying.`; + return await this.block(execution, operation, category, message, true); + } + async advanceReconciliation(execution, operation, pullRequest) { + if (operation.phase === "completed") + return success(`Duplicate reconciliation event for PR #${pullRequest.number} was ignored.`); + if (operation.phase !== "reconciliation_pending") + return success(`Out-of-order reconciliation event ignored while operation is ${operation.phase}.`); + const target = operation.reconciliationTargets.find((item) => item.pullRequest === pullRequest.number); + if (!target) + return await this.block(execution, operation, "reconciliation", `PR #${pullRequest.number} is not a configured reconciliation target.`, false); + if (pullRequest.baseBranch !== target.targetBranch || pullRequest.headBranch !== (target.syncBranch ?? target.sourceBranch)) { + return await this.block(execution, operation, "reconciliation", "Reconciliation PR branches do not match durable state.", false); + } + const mergeSha = pullRequest.mergeCommitSha; + if (!mergeSha || !(await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.targetBranch, mergeSha, execution.tokens.token))) { + return await this.block(execution, operation, "reconciliation", "The reconciliation merge is not reachable from its target branch.", true); + } + if (!(await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.targetBranch, target.sourceSha, execution.tokens.token))) { + return await this.block(execution, operation, "reconciliation", "The reconciliation target does not contain the stored release SHA.", false); + } + const updated = (0, deployment_operation_1.completeReconciliationTarget)(operation, pullRequest.number); + execution.currentConfiguration.deploymentOrchestration = updated; + await this.persist(execution); + if (!updated.reconciliationTargets.every((item) => item.status === "completed")) { + return await this.ensureNextReconciliation(execution, updated) + ?? success(`Reconciliation PR #${pullRequest.number} completed; the next configured target is ready.`); + } + return await this.finalizeReconciliation(execution, updated, ` after reconciliation PR #${pullRequest.number}`); + } + async finalizeReconciliation(execution, operation, completionContext = "") { + try { + await this.cleanup(execution, operation); + if (operation.issueCompletion === "close") { + await this.dependencies.issues.closeIssue(execution.owner, execution.repo, execution.singleAction.issue, execution.tokens.token); + } + } + catch (error) { + const message = error instanceof Error ? error.message : String(error); + return await this.block(execution, operation, "cleanup", message, true); + } + const completed = { ...operation, phase: "completed", lastFailure: null }; + execution.currentConfiguration.deploymentOrchestration = completed; + await this.persist(execution); + await this.publishDashboard(execution, completed); + await this.publishMilestone(execution, completed, "orchestration-complete", `✅ Deployment ${completed.tag} and every configured reconciliation target are complete.`); + return success(`Deployment ${completed.tag} completed${completionContext}.`); + } + async ensureNextReconciliation(execution, operation) { + const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending" && target.pullRequest === undefined); + if (index < 0) { + if (operation.reconciliationTargets.length > 0 + && operation.reconciliationTargets.every((target) => target.status === "completed")) { + return await this.finalizeReconciliation(execution, operation); + } + await this.publishDashboard(execution, operation); + return; + } + let target = operation.reconciliationTargets[index]; + const capabilities = await this.dependencies.pullRequests.getTargetCapabilities(execution.owner, execution.repo, target.targetBranch, execution.tokens.token); + const [targetSha, currentSourceSha] = await Promise.all([ + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.targetBranch, execution.tokens.token), + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.sourceBranch, execution.tokens.token), + ]); + const directUpToDate = await this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.sourceBranch, targetSha, execution.tokens.token).catch(() => false); + const mode = (0, deployment_plan_policy_1.selectBackmergeMode)(operation.backmergeMode, capabilities.requiresStrictStatusChecks, directUpToDate, currentSourceSha === target.sourceSha); + if (mode.kind === "unsupported") { + return await this.block(execution, operation, "reconciliation", mode.reason, false); + } + if (mode.mode === "sync-branch") { + target = (0, deployment_plan_policy_1.buildReconciliationTarget)(operation, target.targetBranch, "sync-branch"); + await this.dependencies.git.createOrVerifyBranch(execution.owner, execution.repo, target.syncBranch, targetSha, execution.tokens.token); + await this.dependencies.git.mergeCommitIntoBranch(execution.owner, execution.repo, target.syncBranch, targetSha, execution.tokens.token); + await this.dependencies.git.mergeCommitIntoBranch(execution.owner, execution.repo, target.syncBranch, target.sourceSha, execution.tokens.token); + } + const operationWithMode = replaceTarget(operation, index, target); + const pullRequest = await this.createOrReusePullRequest(execution, operationWithMode, "reconciliation", target); + if (pullRequest.state === "closed" && !pullRequest.merged) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} was closed without merge.`, true); + } + if (target.syncBranch) { + const [syncHead, sourceIncluded] = await Promise.all([ + this.dependencies.git.getBranchSha(execution.owner, execution.repo, target.syncBranch, execution.tokens.token), + this.dependencies.git.isCommitReachable(execution.owner, execution.repo, target.syncBranch, target.sourceSha, execution.tokens.token), + ]); + if (pullRequest.headSha !== syncHead || !sourceIncluded) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} does not contain the verified sync-branch state.`, false); + } + } + else if (pullRequest.headSha !== target.sourceSha) { + return await this.block(execution, operationWithMode, "reconciliation", `Reconciliation PR #${pullRequest.number} source moved away from the stored release SHA.`, false); + } + const withPullRequest = replaceTarget(operationWithMode, index, { ...target, pullRequest: pullRequest.number }); + execution.currentConfiguration.deploymentOrchestration = withPullRequest; + await this.persist(execution); + if (pullRequest.merged) { + return await this.advanceReconciliation(execution, withPullRequest, pullRequest); + } + await this.configureMergeBehavior(execution, withPullRequest, pullRequest); + return undefined; + } + async createOrReusePullRequest(execution, operation, phase, target) { + const headBranch = target?.syncBranch ?? target?.sourceBranch ?? operation.sourceBranch; + const baseBranch = target?.targetBranch ?? operation.productionBranch; + const query = { + owner: execution.owner, + repository: execution.repo, + operationId: operation.operationId, + phase, + issue: execution.singleAction.issue, + headBranch, + baseBranch, + token: execution.tokens.token, + }; + const existing = await this.dependencies.pullRequests.findManagedPullRequests(query); + if (existing.length > 1) + throw new Error(`Multiple managed ${phase} PRs match operation ${operation.operationId}.`); + if (existing[0]) + return existing[0]; + const context = presentationContext(execution); + const content = phase === "promotion" + ? (0, deployment_presentation_policy_1.renderPromotionPullRequest)(operation, context) + : (0, deployment_presentation_policy_1.renderReconciliationPullRequest)(operation, target, context); + return await this.dependencies.pullRequests.createManagedPullRequest({ ...query, ...content }); + } + async configureMergeBehavior(execution, operation, pullRequest) { + const capabilities = await this.dependencies.pullRequests.getTargetCapabilities(execution.owner, execution.repo, pullRequest.baseBranch, execution.tokens.token, pullRequest.number); + const decision = (0, deployment_plan_policy_1.selectPullRequestMode)(operation.prMode, capabilities); + if (decision.kind === "unsupported") + throw new Error(decision.reason); + const managed = { ...operation, selectedPrMode: decision.mode }; + execution.currentConfiguration.deploymentOrchestration = managed; + await this.persist(execution); + await this.publishDashboard(execution, managed); + if (decision.mode === "auto-merge") { + if (operation.prMode === "auto" && capabilities.immediatelyMergeable) { + await this.dependencies.pullRequests.mergePullRequest(execution.owner, execution.repo, pullRequest.number, execution.tokens.token); + } + else { + await this.dependencies.pullRequests.enableAutoMerge(execution.owner, execution.repo, pullRequest.nodeId, execution.tokens.token); + } + } + else if (decision.mode === "merge-queue") { + await this.dependencies.pullRequests.enqueuePullRequest(execution.owner, execution.repo, pullRequest.nodeId, execution.tokens.token); + } + else if (decision.mode === "legacy-wait") { + await this.dependencies.legacyMerge.waitAndMerge(execution.owner, execution.repo, pullRequest.headBranch, pullRequest.number, pullRequest.baseBranch, execution.pullRequest.mergeTimeout, execution.tokens.token); + } + return managed; + } + async cleanup(execution, operation) { + const deleteSource = operation.cleanup === "all" || operation.cleanup === "source-only"; + const deleteSync = operation.cleanup === "all" || operation.cleanup === "sync-only"; + if (deleteSync) { + for (const target of operation.reconciliationTargets) { + if (target.syncBranch) + await this.dependencies.git.deleteBranch(execution.owner, execution.repo, target.syncBranch, execution.tokens.token); + } + } + if (deleteSource) + await this.dependencies.git.deleteBranch(execution.owner, execution.repo, operation.sourceBranch, execution.tokens.token); + } + async projectDeploymentLabels(execution, operation) { + const labels = await this.dependencies.labels.getLabels(execution.owner, execution.repo, execution.singleAction.issue, execution.tokens.token); + const next = (0, deployment_lifecycle_policy_1.projectDeploymentLabels)(labels, operation, execution.labels); + if (next.join("\0") !== labels.join("\0")) { + await this.dependencies.labels.setLabels(execution.owner, execution.repo, execution.singleAction.issue, next, execution.tokens.token); + } + } + async block(execution, operation, category, message, retryable) { + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, retryable); + execution.currentConfiguration.deploymentOrchestration = blocked; + await this.persist(execution); + await this.publishDashboard(execution, blocked); + await this.publishMilestone(execution, blocked, "reconciliation-blocked", `❌ Deployment blocked: ${blocked.lastFailure?.message}`); + return new result_1.Result({ id: TASK_ID, success: false, executed: true, steps: [message], errors: [new Error(message)] }); + } + async persist(execution) { + const expected = this.checkpoints.get(execution); + const query = { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + token: execution.tokens.token, + }; + const actual = await this.dependencies.state.load(query); + if (!sameCheckpoint(actual, expected)) { + throw new Error("Concurrent deployment state change detected; reload the launcher issue and retry."); + } + await this.dependencies.state.save({ ...query, state: execution.currentConfiguration }); + const operation = execution.currentConfiguration.deploymentOrchestration; + this.checkpoints.set(execution, operation ? { operationId: operation.operationId, phase: operation.phase } : undefined); + if (operation) + await this.projectDeploymentLabels(execution, operation); + } + async publishDashboard(execution, operation) { + const marker = (0, deployment_presentation_policy_1.deploymentDashboardMarker)(operation.operationId, execution.singleAction.issue); + const body = (0, deployment_presentation_policy_1.renderDeploymentDashboard)(operation, presentationContext(execution)); + const current = await this.dependencies.presentation.findDashboard(execution.owner, execution.repo, execution.singleAction.issue, marker, execution.tokens.token); + if (current) + await this.dependencies.presentation.updateDashboard(execution.owner, execution.repo, execution.singleAction.issue, current.id, body, execution.tokens.token); + else + await this.dependencies.presentation.createDashboard(execution.owner, execution.repo, execution.singleAction.issue, body, execution.tokens.token); + } + async publishMilestone(execution, operation, name, body) { + if (operation.commentMode !== "milestones") + return; + const marker = ``; + await this.dependencies.presentation.publishMilestone(execution.owner, execution.repo, execution.singleAction.issue, marker, body, execution.tokens.token); + } + async recordUnexpectedFailure(execution, error) { + const operation = execution.currentConfiguration.deploymentOrchestration; + if (!operation || operation.phase === "completed" || operation.phase === "blocked") + return; + const message = error instanceof Error ? error.message : String(error); + const category = operation.phase === "preparing" || operation.phase === "promotion_pr_pending" + ? "promotion" + : operation.phase === "promoted" || operation.phase === "publishing" + ? "publication" + : "reconciliation"; + const blocked = (0, deployment_operation_1.blockDeploymentOperation)(operation, category, message, true); + execution.currentConfiguration.deploymentOrchestration = blocked; + try { + await this.persist(execution); + await this.publishDashboard(execution, blocked); + } + catch { + // Preserve the original provider failure returned by invoke. + } + } +} +exports.DeploymentOrchestrationUseCase = DeploymentOrchestrationUseCase; +function requireOperation(execution) { + const operation = execution.currentConfiguration.deploymentOrchestration; + if (!operation) + throw new Error("No durable deployment operation exists on the launcher issue."); + if (!execution.singleAction.operationId && !execution.singleAction.isDeployedAction) { + throw new Error("single-action-operation-id is required for a durable deployment continuation."); + } + if (execution.singleAction.operationId && execution.singleAction.operationId !== operation.operationId) { + throw new Error(`Deployment operation mismatch: expected ${operation.operationId}, received ${execution.singleAction.operationId}.`); + } + if ((execution.singleAction.isPublishedDeploymentAction || execution.singleAction.isFailedDeploymentAction) + && execution.singleAction.version !== operation.version) { + throw new Error(`Deployment version mismatch: expected ${operation.version}, received ${execution.singleAction.version || "empty"}.`); + } + return operation; +} +function deploymentKind(execution) { + if (execution.currentConfiguration.hotfixBranch && !execution.currentConfiguration.releaseBranch) + return "hotfix"; + if (execution.currentConfiguration.releaseBranch && !execution.currentConfiguration.hotfixBranch) + return "release"; + if (execution.labels.isHotfix) + return "hotfix"; + if (execution.labels.isRelease) + return "release"; + throw new Error("The launcher issue does not identify exactly one release or hotfix source branch."); +} +function presentationContext(execution) { + return { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + issueLocale: execution.locale.issue, + pullRequestLocale: execution.locale.pullRequest, + packageName: execution.owner === "vypdev" && execution.repo === "copilot" ? "@vypdev/copilot" : undefined, + }; +} +function replaceTarget(operation, index, target) { + return { + ...operation, + reconciliationTargets: operation.reconciliationTargets.map((current, currentIndex) => currentIndex === index ? target : current), + }; +} +function success(step) { + return new result_1.Result({ id: TASK_ID, success: true, executed: true, steps: [step] }); +} +function blockedResult(operation, fallback) { + const message = operation.lastFailure?.message ?? fallback; + return new result_1.Result({ id: TASK_ID, success: false, executed: true, steps: [message], errors: [new Error(message)] }); +} +function sameCheckpoint(actual, expected) { + if (!actual || !expected) + return actual === undefined && expected === undefined; + return actual.operationId === expected.operationId && actual.phase === expected.phase; +} + + /***/ }), /***/ 38575: @@ -56549,11 +57781,13 @@ exports.runPublishGithubAction = runPublishGithubAction; const result_1 = __nccwpck_require__(73817); const input_keys_1 = __nccwpck_require__(88539); const logging_ports_1 = __nccwpck_require__(6152); +const deployment_continuation_guard_1 = __nccwpck_require__(1779); async function runPublishGithubAction(param, taskId, repositoryTagPort, repositoryReleasePort) { const validationFailure = validateVersion(param, taskId); if (validationFailure) return [validationFailure]; - const sourceTag = `v${param.singleAction.version}`; + const version = param.singleAction.version || param.currentConfiguration?.deploymentOrchestration?.version || ''; + const sourceTag = `v${version}`; const targetTag = sourceTag.split('.')[0]; try { await repositoryTagPort.updateTag(param.owner, param.repo, sourceTag, targetTag, param.tokens.token); @@ -56572,7 +57806,10 @@ async function runPublishGithubAction(param, taskId, repositoryTagPort, reposito } } function validateVersion(param, taskId) { - if (param.singleAction.version.length > 0) + const continuationError = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(param.currentConfiguration?.deploymentOrchestration, param.singleAction.operationId, ["publishing"], param.singleAction.version); + if (continuationError) + return new result_1.Result({ id: taskId, success: false, executed: true, errors: [continuationError] }); + if (param.singleAction.version.length > 0 || param.currentConfiguration?.deploymentOrchestration?.version) return undefined; (0, logging_ports_1.logError)('Version is not set.'); return new result_1.Result({ id: taskId, success: false, executed: true, errors: [`${input_keys_1.INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`] }); @@ -58269,6 +59506,10 @@ async function runSetupExecution(execution, dependencies) { if (await (0, resolve_execution_issue_number_1.resolveExecutionIssueNumber)(execution, dependencies.issueSetupPort) === undefined) return; execution.previousConfiguration = await loadPreviousConfiguration(execution, dependencies.configurationPort); + execution.currentConfiguration.deploymentOrchestration = execution.previousConfiguration?.deploymentOrchestration; + execution.currentConfiguration.releaseOriginBranch = execution.previousConfiguration?.releaseOriginBranch; + execution.currentConfiguration.releaseOriginSha = execution.previousConfiguration?.releaseOriginSha; + execution.currentConfiguration.hotfixOriginSha = execution.previousConfiguration?.hotfixOriginSha; await loadIssueLabels(execution, dependencies.issueSetupPort); execution.release.active = execution.labels.isRelease; execution.hotfix.active = execution.labels.isHotfix; @@ -58707,7 +59948,7 @@ const logging_ports_1 = __nccwpck_require__(6152); const task_emoji_1 = __nccwpck_require__(46103); const single_action_workflow_1 = __nccwpck_require__(6130); class SingleActionUseCase { - constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase, actorAuthorizationPort, publishIssueCommentUseCase, observeBranchSyncUseCase) { + constructor(deployedActionUseCase, publishGithubActionUseCase, createReleaseUseCase, createTagUseCase, thinkUseCase, initialSetupUseCase, checkProgressUseCase, detectPotentialProblemsUseCase, recommendStepsUseCase, closeInactiveIssuesUseCase, actorAuthorizationPort, publishIssueCommentUseCase, observeBranchSyncUseCase, deploymentOrchestrationUseCase) { this.deployedActionUseCase = deployedActionUseCase; this.publishGithubActionUseCase = publishGithubActionUseCase; this.createReleaseUseCase = createReleaseUseCase; @@ -58721,6 +59962,7 @@ class SingleActionUseCase { this.actorAuthorizationPort = actorAuthorizationPort; this.publishIssueCommentUseCase = publishIssueCommentUseCase; this.observeBranchSyncUseCase = observeBranchSyncUseCase; + this.deploymentOrchestrationUseCase = deploymentOrchestrationUseCase; this.taskId = "SingleActionUseCase"; } async invoke(param) { @@ -58749,6 +59991,7 @@ class SingleActionUseCase { closeInactiveIssuesUseCase: this.closeInactiveIssuesUseCase, publishIssueCommentUseCase: this.publishIssueCommentUseCase, observeBranchSyncUseCase: this.observeBranchSyncUseCase, + deploymentOrchestrationUseCase: this.deploymentOrchestrationUseCase, }); } } @@ -58779,6 +60022,10 @@ async function runSingleActionWorkflow(param, taskId, ports) { } (0, logging_ports_1.logDebugInfo)(`SingleAction: dispatching to handler for action: ${param.singleAction.currentSingleAction}.`); const action = [ + { + active: param.singleAction.isDeployedAction && Boolean(param.currentConfiguration.deploymentOrchestration), + useCase: ports.deploymentOrchestrationUseCase, + }, { active: param.singleAction.isDeployedAction, useCase: ports.deployedActionUseCase }, { active: param.singleAction.isPublishGithubAction, useCase: ports.publishGithubActionUseCase }, { active: param.singleAction.isCreateReleaseAction, useCase: ports.createReleaseUseCase }, @@ -58791,6 +60038,7 @@ async function runSingleActionWorkflow(param, taskId, ports) { { active: param.singleAction.isCloseInactiveIssuesAction, useCase: ports.closeInactiveIssuesUseCase }, { active: param.singleAction.isPublishIssueCommentAction, useCase: ports.publishIssueCommentUseCase }, { active: param.singleAction.isCheckBranchSyncAction, useCase: ports.observeBranchSyncUseCase }, + { active: param.singleAction.isDeploymentOrchestrationAction, useCase: ports.deploymentOrchestrationUseCase }, ].find(({ active, useCase }) => active && useCase !== undefined); if (!action || !action.useCase) return []; @@ -64772,6 +66020,8 @@ async function prepareHotfixBranch(param, commitTagQuery, linkedBranchCommand, b const lastAction = linkResult.at(-1); if (!lastAction?.success) return linkResult; + if (branchOid) + param.currentConfiguration.hotfixOriginSha = branchOid; (0, logging_ports_1.logDebugInfo)(`Hotfix branch successfully linked to issue: ${JSON.stringify(linkResult)}`); return [ new result_1.Result({ @@ -64915,6 +66165,7 @@ async function prepareReleaseBranch(param, linkedBranchCommand, branches, taskId if (!lastAction?.success) return linkResult; const branchName = (0, result_1.getResultPayload)(lastAction.payload)?.newBranchName; + const baseSha = (0, result_1.getResultPayload)(lastAction.payload)?.baseSha; if (typeof branchName !== "string" || branchName.length === 0) { return [ new result_1.Result({ @@ -64925,16 +66176,18 @@ async function prepareReleaseBranch(param, linkedBranchCommand, branches, taskId }), ]; } + if (typeof baseSha === "string" && baseSha.length > 0) { + param.currentConfiguration.releaseOriginBranch = param.branches.development; + param.currentConfiguration.releaseOriginSha = baseSha; + } const fence = "```"; - const inlineCode = "`"; const reminders = [ `Before deploying, apply any change needed in [**${release.branch}**](${releaseUrl}):\n> ${fence}bash\n> git fetch -v && git checkout ${release.branch}\n> ${fence}\n>\n> Version files, changelogs..`, ]; const commitPrefix = await buildConfiguredCommitPrefix(param, branchName); if (commitPrefix) reminders.push(`Commit the needed changes with this prefix:\n> ${fence}\n>${commitPrefix}\n> ${fence}`); - reminders.push(`Create the tag version in [**${release.branch}**](${releaseUrl}).\n> Avoid using ${inlineCode}git merge --squash${inlineCode}, otherwise the created tag will be lost.`); - reminders.push(`Add the **${param.labels.deploy}** label to run the ${inlineCode}${param.workflows.release}${inlineCode} workflow.`); + reminders.push(`Add the **${param.labels.deploy}** label to run the \`${param.workflows.release}\` workflow. Copilot will create the immutable version tag only after the production promotion PR merges.`); reminders.push(buildReleaseReminder(param, releaseUrl, developmentUrl, mainUrl)); (0, logging_ports_1.logDebugInfo)(`Release branch successfully linked to issue: ${JSON.stringify(linkResult)}`); return [ @@ -64957,8 +66210,7 @@ async function buildConfiguredCommitPrefix(param, branchName) { } function buildReleaseReminder(param, releaseUrl, developmentUrl, mainUrl) { const branch = param.release.branch; - const inlineCode = "`"; - return `After deploying, the new changes on [${inlineCode}${branch}${inlineCode}](${releaseUrl}) must end on [${inlineCode}${param.branches.development}${inlineCode}](${developmentUrl}) and [${inlineCode}${param.branches.main}${inlineCode}](${mainUrl}).\n> **Quick actions:**\n> [New PR](https://github.com/${param.owner}/${param.repo}/compare/${param.branches.development}...${branch}?expand=1) from [${inlineCode}${branch}${inlineCode}](${releaseUrl}) to [${inlineCode}${param.branches.development}${inlineCode}](${developmentUrl}).\n> [New PR](https://github.com/${param.owner}/${param.repo}/compare/${param.branches.main}...${branch}?expand=1) from [${inlineCode}${branch}${inlineCode}](${releaseUrl}) to [${inlineCode}${param.branches.main}${inlineCode}](${mainUrl}).`; + return `Copilot will promote [\`${branch}\`](${releaseUrl}) into [\`${param.branches.main}\`](${mainUrl}) before publication, then reconcile the accepted production commit into the current [\`${param.branches.development}\`](${developmentUrl}) branch. Do not create the version tag or either merge PR manually unless the issue control center requests recovery.`; } @@ -65825,6 +67077,10 @@ exports.ACTIONS = { CLOSE_INACTIVE_ISSUES: 'close_inactive_issues_action', PUBLISH_ISSUE_COMMENT: 'publish_issue_comment', CHECK_BRANCH_SYNC: 'check_branch_sync_action', + PREPARE_DEPLOYMENT: 'prepare_deployment_action', + CONTINUE_DEPLOYMENT: 'continue_deployment_action', + PUBLISHED_DEPLOYMENT: 'published_deployment_action', + FAILED_DEPLOYMENT: 'failed_deployment_action', }; @@ -66036,8 +67292,9 @@ exports.migrateConfigurationPayload = migrateConfigurationPayload; const branch_configuration_1 = __nccwpck_require__(71934); const recommendation_state_1 = __nccwpck_require__(68514); const model_input_1 = __nccwpck_require__(14637); +const deployment_operation_1 = __nccwpck_require__(92730); /** Version of the durable configuration contract stored in issue/PR content. */ -exports.CONFIG_SCHEMA_VERSION = 2; +exports.CONFIG_SCHEMA_VERSION = 3; /** * Normalizes persisted configuration without silently losing fields from a * newer installation. Unknown keys are deliberately retained so a downgrade @@ -66081,6 +67338,9 @@ class Config { this.hotfixOriginBranch = (0, model_input_1.readOptionalString)(input, 'hotfixOriginBranch'); this.hotfixBranch = (0, model_input_1.readOptionalString)(input, 'hotfixBranch'); this.releaseBranch = (0, model_input_1.readOptionalString)(input, 'releaseBranch'); + this.releaseOriginBranch = (0, model_input_1.readOptionalString)(input, 'releaseOriginBranch'); + this.releaseOriginSha = (0, model_input_1.readOptionalString)(input, 'releaseOriginSha'); + this.hotfixOriginSha = (0, model_input_1.readOptionalString)(input, 'hotfixOriginSha'); this.parentBranch = (0, model_input_1.readOptionalString)(input, 'parentBranch'); this.workingBranch = (0, model_input_1.readOptionalString)(input, 'workingBranch'); if (input['branchConfiguration'] !== undefined && input['branchConfiguration'] !== null) { @@ -66089,6 +67349,9 @@ class Config { if ((0, recommendation_state_1.isRecommendationState)(input['recommendationState'])) { this.recommendationState = input['recommendationState']; } + if ((0, deployment_operation_1.isDeploymentOperationSnapshot)(input['deploymentOrchestration'])) { + this.deploymentOrchestration = input['deploymentOrchestration']; + } } } exports.Config = Config; @@ -66126,6 +67389,7 @@ const commit_1 = __nccwpck_require__(57525); const config_1 = __nccwpck_require__(90450); const github_user_policy_1 = __nccwpck_require__(84403); const issue_inactivity_1 = __nccwpck_require__(38572); +const deployment_configuration_1 = __nccwpck_require__(22495); class Execution { get eventName() { return this.inputs?.eventName ?? ''; @@ -66216,6 +67480,7 @@ class Execution { this.hotfix = components.hotfix; this.project = components.projects; this.workflows = components.workflows; + this.deployment = components.deployment ?? { ...deployment_configuration_1.DEFAULT_DEPLOYMENT_CONFIGURATION }; this.tokenUser = components.tokenUser; this.inactivityThresholdHours = components.inactivityThresholdHours ?? issue_inactivity_1.DEFAULT_INACTIVITY_THRESHOLD_HOURS; this.currentConfiguration = new config_1.Config({}); @@ -67114,6 +68379,24 @@ class SingleAction { get isCheckBranchSyncAction() { return this.currentSingleAction === action_types_1.ACTIONS.CHECK_BRANCH_SYNC; } + get isPrepareDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.PREPARE_DEPLOYMENT; + } + get isContinueDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.CONTINUE_DEPLOYMENT; + } + get isPublishedDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT; + } + get isFailedDeploymentAction() { + return this.currentSingleAction === action_types_1.ACTIONS.FAILED_DEPLOYMENT; + } + get isDeploymentOrchestrationAction() { + return this.isPrepareDeploymentAction + || this.isContinueDeploymentAction + || this.isPublishedDeploymentAction + || this.isFailedDeploymentAction; + } get enabledSingleAction() { return this.currentSingleAction.length > 0; } @@ -67128,7 +68411,7 @@ class SingleAction { get throwError() { return this.actionsThrowError.indexOf(this.currentSingleAction) > -1; } - constructor(currentSingleAction, issue, version, title, changelog, message = '', commentId = '', commentMode = '') { + constructor(currentSingleAction, issue, version, title, changelog, message = '', commentId = '', commentMode = '', operationId = '') { this.actions = [ action_types_1.ACTIONS.DEPLOYED, action_types_1.ACTIONS.PUBLISH_GITHUB_ACTION, @@ -67142,6 +68425,10 @@ class SingleAction { action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, action_types_1.ACTIONS.PUBLISH_ISSUE_COMMENT, action_types_1.ACTIONS.CHECK_BRANCH_SYNC, + action_types_1.ACTIONS.PREPARE_DEPLOYMENT, + action_types_1.ACTIONS.CONTINUE_DEPLOYMENT, + action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT, + action_types_1.ACTIONS.FAILED_DEPLOYMENT, ]; /** * Actions that throw an error if the last step failed @@ -67153,6 +68440,10 @@ class SingleAction { action_types_1.ACTIONS.CREATE_TAG, action_types_1.ACTIONS.CLOSE_INACTIVE_ISSUES, action_types_1.ACTIONS.PUBLISH_ISSUE_COMMENT, + action_types_1.ACTIONS.PREPARE_DEPLOYMENT, + action_types_1.ACTIONS.CONTINUE_DEPLOYMENT, + action_types_1.ACTIONS.PUBLISHED_DEPLOYMENT, + action_types_1.ACTIONS.FAILED_DEPLOYMENT, ]; /** * Actions that do not require an issue @@ -67174,6 +68465,7 @@ class SingleAction { this.title = ''; this.changelog = ''; this.message = ''; + this.operationId = ''; this.commentId = -1; this.commentIdInput = ''; this.commentMode = ''; @@ -67184,6 +68476,7 @@ class SingleAction { this.commentIdInput = commentId.trim(); this.commentId = (0, positive_integer_policy_1.parsePositiveSafeInteger)(this.commentIdInput) ?? -1; this.commentMode = commentMode.trim().toLowerCase(); + this.operationId = operationId.trim(); this.currentSingleAction = currentSingleAction; if (!this.isSingleActionWithoutIssue) { this.issue = (0, positive_integer_policy_1.parsePositiveSafeInteger)(issue) ?? -1; @@ -69121,13 +70414,14 @@ function missingLinkedBranchResult(branchName) { function unexpectedLinkedBranchResult(branchName) { return new result_1.Result({ id: RESULT_ID, success: false, executed: true, steps: [`Linked branch creation returned an unexpected branch ref for ${branchName}.`] }); } -function createdLinkedBranchResult(owner, repo, baseBranchName, newBranchName) { +function createdLinkedBranchResult(owner, repo, baseBranchName, newBranchName, baseSha) { return new result_1.Result({ id: RESULT_ID, success: true, executed: true, payload: { baseBranchName, + baseSha, baseBranchUrl: `https://github.com/${owner}/${repo}/tree/${baseBranchName}`, newBranchName, newBranchUrl: `https://github.com/${owner}/${repo}/tree/${newBranchName}`, @@ -69191,7 +70485,7 @@ async function runCreateLinkedBranch(client, owner, repo, baseBranchName, newBra return [(0, linked_branch_result_policy_1.missingLinkedBranchResult)(newBranchName)]; if (!(0, linked_branch_policy_1.isExpectedLinkedBranchRef)(linkedBranch.ref?.name, newBranchName)) return [(0, linked_branch_result_policy_1.unexpectedLinkedBranchResult)(newBranchName)]; - return [(0, linked_branch_result_policy_1.createdLinkedBranchResult)(owner, repo, baseBranchName, newBranchName)]; + return [(0, linked_branch_result_policy_1.createdLinkedBranchResult)(owner, repo, baseBranchName, newBranchName, identifiers.branchOid)]; } catch (error) { if ((0, github_error_policy_1.isGithubAlreadyExists)(error)) { @@ -69662,6 +70956,324 @@ class CopilotEvidenceRepository { exports.CopilotEvidenceRepository = CopilotEvidenceRepository; +/***/ }), + +/***/ 77509: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentContinuationRepository = void 0; +class DeploymentContinuationRepository { + constructor(workflow) { + this.workflow = workflow; + } + async dispatch(owner, repository, workflow, ref, operationId, issue, version, token) { + await this.workflow.executeWorkflow(owner, repository, ref, workflow, { + mode: "publish", + "operation-id": operationId, + issue: String(issue), + version, + }, token); + } +} +exports.DeploymentContinuationRepository = DeploymentContinuationRepository; + + +/***/ }), + +/***/ 91985: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentPresentationRepository = void 0; +class DeploymentPresentationRepository { + constructor(issues) { + this.issues = issues; + } + async findDashboard(owner, repository, issue, marker, token) { + const comments = await this.issues.listIssueComments(owner, repository, issue, token); + const matches = comments.filter((comment) => comment.body?.includes(marker)); + if (matches.length > 1) + throw new Error(`Multiple deployment dashboards match ${marker}.`); + const match = matches[0]; + return match ? { id: match.id, body: match.body ?? "" } : undefined; + } + async createDashboard(owner, repository, issue, body, token) { + await this.issues.addComment(owner, repository, issue, body, token); + } + async updateDashboard(owner, repository, issue, commentId, body, token) { + await this.issues.updateComment(owner, repository, issue, commentId, body, token); + } + async publishMilestone(owner, repository, issue, marker, body, token) { + const comments = await this.issues.listIssueComments(owner, repository, issue, token); + if (comments.some((comment) => comment.body?.includes(marker))) + return; + await this.issues.addComment(owner, repository, issue, `${body}\n\n${marker}`, token); + } +} +exports.DeploymentPresentationRepository = DeploymentPresentationRepository; + + +/***/ }), + +/***/ 3182: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeploymentStateRepository = void 0; +const config_1 = __nccwpck_require__(90450); +const configuration_handler_1 = __nccwpck_require__(40188); +const configuration_payload_policy_1 = __nccwpck_require__(58043); +class DeploymentStateRepository { + constructor(issues) { + this.issues = issues; + this.block = new configuration_handler_1.ConfigurationHandler(issues); + } + async load(query) { + const description = await this.issues.getDescription(query.owner, query.repository, query.issue, query.token); + const raw = this.block.getContent(description); + if (!raw) + return undefined; + return new config_1.Config(JSON.parse(raw)).deploymentOrchestration; + } + async save(command) { + const description = await this.issues.getDescription(command.owner, command.repository, command.issue, command.token); + const stored = this.block.getContent(description); + const payload = (0, configuration_payload_policy_1.buildConfigurationPayload)({ currentConfiguration: command.state }, stored); + const updated = this.block.updateContent(description, payload); + if (updated === undefined) + throw new Error("Issue configuration markers are missing or inconsistent."); + await this.issues.updateDescription(command.owner, command.repository, command.issue, updated, command.token); + } +} +exports.DeploymentStateRepository = DeploymentStateRepository; + + +/***/ }), + +/***/ 22368: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.GithubDeploymentRepository = void 0; +const managed_pull_request_1 = __nccwpck_require__(95914); +class GithubDeploymentRepository { + constructor(clientProvider) { + this.clientProvider = clientProvider; + } + async findManagedPullRequests(query) { + const client = this.clientProvider.getClient(query.token); + const pullRequests = await client.paginate(client.rest.pulls.list, { + owner: query.owner, + repo: query.repository, + state: "all", + head: `${query.owner}:${query.headBranch}`, + base: query.baseBranch, + per_page: 100, + }); + return pullRequests + .filter((pullRequest) => { + const marker = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body); + return marker?.operationId === query.operationId + && marker.phase === query.phase + && marker.issue === query.issue; + }) + .map((pullRequest) => mapPullRequest(pullRequest, query.owner, query.repository)); + } + async createManagedPullRequest(command) { + const client = this.clientProvider.getClient(command.token); + const { data } = await client.rest.pulls.create({ + owner: command.owner, + repo: command.repository, + head: command.headBranch, + base: command.baseBranch, + title: command.title, + body: command.body, + maintainer_can_modify: false, + }); + return mapPullRequest(data, command.owner, command.repository); + } + async getPullRequest(owner, repository, pullRequest, token) { + const { data } = await this.clientProvider.getClient(token).rest.pulls.get({ + owner, + repo: repository, + pull_number: pullRequest, + }); + return mapPullRequest(data, owner, repository); + } + async getTargetCapabilities(owner, repository, targetBranch, token, pullRequest) { + const client = this.clientProvider.getClient(token); + const [{ data: repositoryData }, protection, queue, pullRequestState] = await Promise.all([ + client.rest.repos.get({ owner, repo: repository }), + readBranchProtection(client, owner, repository, targetBranch), + readMergeQueueRequirement(client, owner, repository, targetBranch), + pullRequest === undefined + ? Promise.resolve(undefined) + : client.rest.pulls.get({ owner, repo: repository, pull_number: pullRequest }).then(({ data }) => data), + ]); + return { + autoMergeAllowed: repositoryData.allow_auto_merge === true, + mergeQueueRequired: queue, + immediatelyMergeable: pullRequestState?.mergeable === true && pullRequestState.mergeable_state === "clean", + requiresStrictStatusChecks: protection?.required_status_checks?.strict === true, + }; + } + async enableAutoMerge(owner, repository, pullRequestNodeId, token) { + await this.clientProvider.getClient(token).graphql(`mutation EnableDeploymentAutoMerge($pullRequestId: ID!) { + enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: MERGE}) { + pullRequest { id } + } + }`, { pullRequestId: pullRequestNodeId, owner, repository }); + } + async enqueuePullRequest(owner, repository, pullRequestNodeId, token) { + await this.clientProvider.getClient(token).graphql(`mutation EnqueueDeploymentPullRequest($pullRequestId: ID!) { + enqueuePullRequest(input: {pullRequestId: $pullRequestId}) { mergeQueueEntry { id } } + }`, { pullRequestId: pullRequestNodeId, owner, repository }); + } + async mergePullRequest(owner, repository, pullRequest, token) { + const { data } = await this.clientProvider.getClient(token).rest.pulls.merge({ + owner, + repo: repository, + pull_number: pullRequest, + merge_method: "merge", + }); + if (!data.merged || !data.sha) + throw new Error(data.message ?? `Pull request #${pullRequest} was not merged.`); + return data.sha; + } + async getBranchSha(owner, repository, branch, token) { + const { data } = await this.clientProvider.getClient(token).rest.git.getRef({ owner, repo: repository, ref: `heads/${branch}` }); + return data.object.sha; + } + async getMergeBaseSha(owner, repository, base, head, token) { + const { data } = await this.clientProvider.getClient(token).rest.repos.compareCommits({ owner, repo: repository, base, head }); + const sha = data.merge_base_commit?.sha; + if (!sha) + throw new Error(`GitHub returned no merge base for ${base}...${head}.`); + return sha; + } + async isCommitReachable(owner, repository, branch, sha, token) { + const { data } = await this.clientProvider.getClient(token).rest.repos.compareCommits({ owner, repo: repository, base: sha, head: branch }); + return data.merge_base_commit?.sha === sha; + } + async createOrVerifyBranch(owner, repository, branch, sha, token) { + const client = this.clientProvider.getClient(token); + try { + const { data } = await client.rest.git.getRef({ owner, repo: repository, ref: `heads/${branch}` }); + if (data.object.sha !== sha) { + const { data: comparison } = await client.rest.repos.compareCommits({ owner, repo: repository, base: sha, head: branch }); + if (comparison.merge_base_commit?.sha !== sha) + throw new Error(`Branch ${branch} already exists at a different SHA.`); + } + } + catch (error) { + if (!isNotFound(error)) + throw error; + await client.rest.git.createRef({ owner, repo: repository, ref: `refs/heads/${branch}`, sha }); + } + } + async mergeCommitIntoBranch(owner, repository, branch, sourceSha, token) { + const client = this.clientProvider.getClient(token); + const { data: comparison } = await client.rest.repos.compareCommits({ owner, repo: repository, base: sourceSha, head: branch }); + if (comparison.merge_base_commit?.sha === sourceSha) + return await this.getBranchSha(owner, repository, branch, token); + const { data } = await client.rest.repos.merge({ + owner, + repo: repository, + base: branch, + head: sourceSha, + commit_message: `chore(release): reconcile ${sourceSha.slice(0, 7)} into ${branch}`, + }); + if (!data.merged || !data.sha) + throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); + return data.sha; + } + async deleteBranch(owner, repository, branch, token) { + try { + await this.clientProvider.getClient(token).rest.git.deleteRef({ owner, repo: repository, ref: `heads/${branch}` }); + } + catch (error) { + if (!isNotFound(error)) + throw error; + } + } + async listBranches(owner, repository, prefix, token) { + const client = this.clientProvider.getClient(token); + const branches = await client.paginate(client.rest.repos.listBranches, { owner, repo: repository, per_page: 100 }); + return branches.map(({ name }) => name).filter((name) => name.startsWith(`${prefix}/`)); + } +} +exports.GithubDeploymentRepository = GithubDeploymentRepository; +function mapPullRequest(value, owner, repository) { + return { + number: value.number, + nodeId: value.node_id, + body: value.body ?? "", + headBranch: value.head.ref, + headSha: value.head.sha, + baseBranch: value.base.ref, + state: value.state === "closed" ? "closed" : "open", + merged: value.merged === true, + mergeCommitSha: value.merge_commit_sha ?? undefined, + repositoryFullName: value.base.repo?.full_name ?? value.head.repo?.full_name ?? `${owner}/${repository}`, + }; +} +async function readBranchProtection(client, owner, repository, branch) { + try { + return (await client.rest.repos.getBranchProtection({ owner, repo: repository, branch })).data; + } + catch (error) { + if (isNotFound(error)) + return undefined; + throw error; + } +} +async function readMergeQueueRequirement(client, owner, repository, branch) { + const response = await client.graphql(`query DeploymentTargetRules($owner: String!, $repository: String!, $qualifiedName: String!) { + repository(owner: $owner, name: $repository) { + ref(qualifiedName: $qualifiedName) { branchProtectionRule { requiresMergeQueue } } + } + }`, { owner, repository, qualifiedName: `refs/heads/${branch}` }); + return response.repository?.ref?.branchProtectionRule?.requiresMergeQueue === true; +} +function isNotFound(error) { + return typeof error === "object" && error !== null && "status" in error && error.status === 404; +} + + +/***/ }), + +/***/ 70245: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.LegacyDeploymentMergeRepository = void 0; +const merge_checks_waiter_1 = __nccwpck_require__(43989); +const merge_pull_request_flow_1 = __nccwpck_require__(81775); +class LegacyDeploymentMergeRepository { + constructor(clients, waiter = new merge_checks_waiter_1.MergeChecksWaiter()) { + this.clients = clients; + this.waiter = waiter; + } + async waitAndMerge(owner, repository, headBranch, pullRequest, baseBranch, timeoutSeconds, token) { + const client = this.clients.getClient(token); + await this.waiter.wait(client, owner, repository, headBranch, pullRequest, timeoutSeconds); + await (0, merge_pull_request_flow_1.mergePullRequest)(client, owner, repository, pullRequest, headBranch, baseBranch); + } +} +exports.LegacyDeploymentMergeRepository = LegacyDeploymentMergeRepository; + + /***/ }), /***/ 26331: @@ -72993,16 +74605,28 @@ class RepositoryReleasePublicationRepository { this.createRelease = async (owner, repository, version, title, changelog, token) => { try { const octokit = this.githubClient.getClient(token); - const { data: release } = await octokit.rest.repos.createRelease({ - owner, - repo: repository, - tag_name: version, - name: (0, release_tag_policy_1.releaseName)(version, title), - body: changelog, - draft: false, - prerelease: false, - }); - return release.html_url; + try { + const { data: release } = await octokit.rest.repos.createRelease({ + owner, + repo: repository, + tag_name: version, + name: (0, release_tag_policy_1.releaseName)(version, title), + body: changelog, + draft: false, + prerelease: false, + }); + return release.html_url; + } + catch (error) { + if (!isAlreadyExists(error)) + throw error; + const { data: existing } = await octokit.rest.repos.getReleaseByTag({ + owner, + repo: repository, + tag: version, + }); + return existing.html_url; + } } catch (error) { (0, logger_1.logError)(`Error creating release: ${error}`); @@ -73012,6 +74636,10 @@ class RepositoryReleasePublicationRepository { } } exports.RepositoryReleasePublicationRepository = RepositoryReleasePublicationRepository; +function isAlreadyExists(error) { + return typeof error === 'object' && error !== null && 'status' in error + && error.status === 422; +} /***/ }), @@ -73083,8 +74711,7 @@ class RepositoryTagRepository { const octokit = this.githubClient.getClient(token); const sourceTagSha = await (0, repository_tag_query_1.getRepositoryTagSha)(octokit, owner, repository, sourceTag); if (!sourceTagSha) { - (0, logger_1.logError)(`The '${sourceTag}' tag does not exist in the remote repository`); - return; + throw new Error(`The '${sourceTag}' tag does not exist in the remote repository.`); } const foundTargetTag = await (0, repository_tag_query_1.findRepositoryTag)(octokit, owner, repository, targetTag); if (foundTargetTag) { @@ -73106,6 +74733,10 @@ class RepositoryTagRepository { sha: sourceTagSha, }); } + const verifiedTargetSha = await (0, repository_tag_query_1.getRepositoryTagSha)(octokit, owner, repository, targetTag); + if (verifiedTargetSha !== sourceTagSha) { + throw new Error(`Moving tag '${targetTag}' was not verified at ${sourceTagSha}.`); + } }; this.createTag = async (owner, repository, branch, tag, token) => { const octokit = this.githubClient.getClient(token); @@ -73134,6 +74765,23 @@ class RepositoryTagRepository { throw error; } }; + this.createOrVerifyTagAtSha = async (owner, repository, sha, tag, token) => { + const octokit = this.githubClient.getClient(token); + const existingTag = await (0, repository_tag_query_1.findRepositoryTag)(octokit, owner, repository, tag); + if (existingTag) { + if (existingTag.object.sha !== sha) { + throw new Error(`Immutable tag '${tag}' exists at ${existingTag.object.sha}, expected ${sha}.`); + } + return sha; + } + await octokit.rest.git.createRef({ + owner, + repo: repository, + ref: `refs/tags/${tag}`, + sha, + }); + return sha; + }; } } exports.RepositoryTagRepository = RepositoryTagRepository; @@ -74333,6 +75981,296 @@ function lifecycleStateFromLabels(currentLabels, labels = exports.DEFAULT_COPILO } +/***/ }), + +/***/ 22495: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = exports.ORCHESTRATION_COMMENT_MODES = exports.ORCHESTRATION_PRESENTATION_MODES = exports.RECONCILIATION_ISSUE_COMPLETION_MODES = exports.RECONCILIATION_CLEANUP_MODES = exports.HOTFIX_ACTIVE_RELEASE_POLICIES = exports.RECONCILIATION_BACKMERGE_MODES = exports.RECONCILIATION_PR_MODES = exports.RECONCILIATION_STRATEGIES = void 0; +exports.validateDeploymentConfiguration = validateDeploymentConfiguration; +exports.isSafeBranchTree = isSafeBranchTree; +exports.parseDeploymentEnum = parseDeploymentEnum; +exports.RECONCILIATION_STRATEGIES = [ + "production-lineage", + "canonical-gitflow", + "manual", +]; +exports.RECONCILIATION_PR_MODES = [ + "auto", + "auto-merge", + "merge-queue", + "create-only", + "legacy-wait", +]; +exports.RECONCILIATION_BACKMERGE_MODES = [ + "auto", + "direct", + "sync-branch", +]; +exports.HOTFIX_ACTIVE_RELEASE_POLICIES = [ + "prefer-release", + "development", + "both", +]; +exports.RECONCILIATION_CLEANUP_MODES = [ + "all", + "source-only", + "sync-only", + "none", +]; +exports.RECONCILIATION_ISSUE_COMPLETION_MODES = ["close", "keep-open"]; +exports.ORCHESTRATION_PRESENTATION_MODES = ["guided", "compact", "quiet"]; +exports.ORCHESTRATION_COMMENT_MODES = ["update", "milestones"]; +exports.DEFAULT_DEPLOYMENT_CONFIGURATION = { + releaseReconciliationStrategy: "production-lineage", + hotfixReconciliationStrategy: "production-lineage", + reconciliationPullRequestMode: "auto", + reconciliationBackmergeMode: "auto", + hotfixActiveReleasePolicy: "prefer-release", + reconciliationTree: "sync", + reconciliationCleanup: "all", + reconciliationIssueCompletion: "close", + orchestrationPresentationMode: "guided", + orchestrationDiagrams: true, + orchestrationCommentMode: "update", +}; +function validateDeploymentConfiguration(configuration, context) { + const errors = []; + for (const [name, value, allowed] of [ + ["release reconciliation strategy", configuration.releaseReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["hotfix reconciliation strategy", configuration.hotfixReconciliationStrategy, exports.RECONCILIATION_STRATEGIES], + ["reconciliation PR mode", configuration.reconciliationPullRequestMode, exports.RECONCILIATION_PR_MODES], + ["reconciliation back-merge mode", configuration.reconciliationBackmergeMode, exports.RECONCILIATION_BACKMERGE_MODES], + ["hotfix active-release policy", configuration.hotfixActiveReleasePolicy, exports.HOTFIX_ACTIVE_RELEASE_POLICIES], + ["reconciliation cleanup", configuration.reconciliationCleanup, exports.RECONCILIATION_CLEANUP_MODES], + ["reconciliation issue completion", configuration.reconciliationIssueCompletion, exports.RECONCILIATION_ISSUE_COMPLETION_MODES], + ["orchestration presentation mode", configuration.orchestrationPresentationMode, exports.ORCHESTRATION_PRESENTATION_MODES], + ["orchestration comment mode", configuration.orchestrationCommentMode, exports.ORCHESTRATION_COMMENT_MODES], + ]) { + if (!allowed.includes(value)) { + errors.push(`The ${name} must be one of: ${allowed.join(", ")}.`); + } + } + if (typeof configuration.orchestrationDiagrams !== "boolean") { + errors.push("Orchestration diagrams must be a boolean."); + } + if (context.productionBranch === context.developmentBranch) { + errors.push("Production and development branches must be different."); + } + const protectedNames = new Set([context.productionBranch, context.developmentBranch]); + for (const [label, tree] of [ + ["release", context.releaseTree], + ["hotfix", context.hotfixTree], + ["reconciliation", configuration.reconciliationTree], + ]) { + if (!isSafeBranchTree(tree)) { + errors.push(`The ${label} branch prefix must be a safe, non-empty Git ref segment.`); + } + else if (protectedNames.has(tree)) { + errors.push(`The ${label} branch prefix cannot equal a protected long-lived branch.`); + } + } + if (configuration.reconciliationPullRequestMode === "merge-queue" + && context.mergeQueueWorkflowSupported === false) { + errors.push("Merge-queue mode requires merge_group support in every required workflow."); + } + if ((configuration.releaseReconciliationStrategy === "manual" + || configuration.hotfixReconciliationStrategy === "manual") + && configuration.reconciliationIssueCompletion === "close") { + errors.push("Manual reconciliation cannot close the launcher issue automatically."); + } + return errors; +} +function isSafeBranchTree(value) { + const tree = value.trim(); + return tree.length > 0 + && tree.length <= 100 + && !tree.startsWith("/") + && !tree.endsWith("/") + && !tree.includes("..") + && !tree.includes("@{") + && !/[~^:?*[\\\]\s]/.test(tree); +} +function parseDeploymentEnum(value, allowed, fallback) { + if (value === undefined || value === null || String(value).trim() === "") { + return { value: fallback, valid: true }; + } + const normalized = String(value).trim(); + return allowed.includes(normalized) + ? { value: normalized, valid: true } + : { value: fallback, valid: false }; +} + + +/***/ }), + +/***/ 92730: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DEPLOYMENT_PHASES = void 0; +exports.transitionDeploymentOperation = transitionDeploymentOperation; +exports.blockDeploymentOperation = blockDeploymentOperation; +exports.resumeBlockedDeployment = resumeBlockedDeployment; +exports.completeReconciliationTarget = completeReconciliationTarget; +exports.sanitizeDeploymentMessage = sanitizeDeploymentMessage; +exports.isDeploymentOperationSnapshot = isDeploymentOperationSnapshot; +const deployment_configuration_1 = __nccwpck_require__(22495); +exports.DEPLOYMENT_PHASES = [ + "preparing", + "promotion_pr_pending", + "promoted", + "publishing", + "published", + "reconciliation_pending", + "completed", + "blocked", +]; +const NORMAL_TRANSITIONS = { + preparing: ["promotion_pr_pending"], + promotion_pr_pending: ["promoted"], + promoted: ["publishing"], + publishing: ["published"], + published: ["reconciliation_pending", "completed"], + reconciliation_pending: ["completed"], + completed: [], +}; +function transitionDeploymentOperation(operation, expectedPhase, nextPhase) { + if (operation.phase === nextPhase) { + return { kind: "noop", operation, reason: `Operation is already ${nextPhase}.` }; + } + if (operation.phase !== expectedPhase) { + return { kind: "noop", operation, reason: `Expected ${expectedPhase}, found ${operation.phase}.` }; + } + if (nextPhase === "blocked") { + return { kind: "advance", operation: { ...operation, phase: nextPhase } }; + } + if (expectedPhase === "blocked" || !NORMAL_TRANSITIONS[expectedPhase].includes(nextPhase)) { + return { kind: "invalid", operation, reason: `Transition ${expectedPhase} -> ${nextPhase} is not allowed.` }; + } + return { kind: "advance", operation: { ...operation, phase: nextPhase, lastFailure: null } }; +} +function blockDeploymentOperation(operation, category, message, retryable) { + if (operation.phase === "completed") + return operation; + const previousPhase = operation.phase === "blocked" + ? operation.lastFailure?.previousPhase ?? "preparing" + : operation.phase; + return { + ...operation, + phase: "blocked", + lastFailure: { category, message: sanitizeDeploymentMessage(message), retryable, previousPhase }, + }; +} +function resumeBlockedDeployment(operation) { + if (operation.phase !== "blocked" || !operation.lastFailure?.retryable) { + return { kind: "invalid", operation, reason: "Operation is not retryable from blocked state." }; + } + return { + kind: "advance", + operation: { ...operation, phase: operation.lastFailure.previousPhase, lastFailure: null }, + }; +} +function completeReconciliationTarget(operation, pullRequest) { + const targets = operation.reconciliationTargets.map((target) => target.pullRequest === pullRequest ? { ...target, status: "completed" } : target); + return { + ...operation, + reconciliationTargets: targets, + lastFailure: null, + }; +} +function sanitizeDeploymentMessage(value) { + return value + .replace(/::/g, "﹕﹕") + .replace(/@(?=[A-Za-z0-9_-])/g, "@\u200b") + .replace(//g, "-->") + .slice(0, 2000); +} +function isDeploymentOperationSnapshot(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const operation = value; + return typeof operation.operationId === "string" + && /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operation.operationId) + && (operation.kind === "release" || operation.kind === "hotfix") + && typeof operation.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(operation.version) + && typeof operation.title === "string" && operation.title.length <= 1000 + && typeof operation.changelog === "string" && operation.changelog.length <= 50000 + && exports.DEPLOYMENT_PHASES.includes(operation.phase) + && deployment_configuration_1.RECONCILIATION_STRATEGIES.includes(operation.strategy) + && deployment_configuration_1.RECONCILIATION_PR_MODES.includes(operation.prMode) + && (operation.selectedPrMode === undefined + || ["auto-merge", "merge-queue", "create-only", "legacy-wait"].includes(operation.selectedPrMode)) + && deployment_configuration_1.RECONCILIATION_BACKMERGE_MODES.includes(operation.backmergeMode) + && deployment_configuration_1.HOTFIX_ACTIVE_RELEASE_POLICIES.includes(operation.hotfixActiveReleasePolicy) + && deployment_configuration_1.RECONCILIATION_CLEANUP_MODES.includes(operation.cleanup) + && deployment_configuration_1.RECONCILIATION_ISSUE_COMPLETION_MODES.includes(operation.issueCompletion) + && deployment_configuration_1.ORCHESTRATION_PRESENTATION_MODES.includes(operation.presentationMode) + && typeof operation.diagrams === "boolean" + && deployment_configuration_1.ORCHESTRATION_COMMENT_MODES.includes(operation.commentMode) + && isSafePersistedRef(operation.sourceBranch) + && isFullSha(operation.sourceSha) + && isSafePersistedRef(operation.originBranch) + && isFullSha(operation.originSha) + && isSafePersistedRef(operation.productionBranch) + && isSafePersistedRef(operation.developmentBranch) + && typeof operation.reconciliationTree === "string" + && typeof operation.tag === "string" && operation.tag === `v${operation.version}` + && typeof operation.publicationWorkflow === "string" && isSafeWorkflowName(operation.publicationWorkflow) + && (operation.promotionPullRequest === undefined || isPositiveInteger(operation.promotionPullRequest)) + && (operation.productionSha === undefined || isFullSha(operation.productionSha)) + && typeof operation.publicationVerified === "boolean" + && Array.isArray(operation.reconciliationTargets) + && operation.reconciliationTargets.every(isReconciliationTarget) + && (operation.lastFailure === undefined || operation.lastFailure === null || isDeploymentFailure(operation.lastFailure)); +} +function isFullSha(value) { + return typeof value === "string" && /^[a-f0-9]{40}$/i.test(value); +} +function isPositiveInteger(value) { + return typeof value === "number" && Number.isSafeInteger(value) && value > 0; +} +function isSafePersistedRef(value) { + return typeof value === "string" + && value.length > 0 + && value.length <= 200 + && !value.includes("..") + && !value.includes("@{") + && !/[\s~^:?*[\\\]]/.test(value); +} +function isSafeWorkflowName(value) { + return value.length <= 200 && !value.includes("..") && /^[A-Za-z0-9][A-Za-z0-9._/-]*\.ya?ml$/.test(value); +} +function isReconciliationTarget(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const target = value; + return isSafePersistedRef(target.targetBranch) + && isSafePersistedRef(target.sourceBranch) + && isFullSha(target.sourceSha) + && (target.syncBranch === undefined || isSafePersistedRef(target.syncBranch)) + && (target.pullRequest === undefined || isPositiveInteger(target.pullRequest)) + && ["pending", "completed", "blocked"].includes(target.status); +} +function isDeploymentFailure(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) + return false; + const failure = value; + return ["promotion", "publication", "reconciliation", "cleanup"].includes(failure.category) + && typeof failure.message === "string" + && failure.message.length <= 2000 + && typeof failure.retryable === "boolean" + && ["preparing", "promotion_pr_pending", "promoted", "publishing", "published", "reconciliation_pending", "completed"] + .includes(failure.previousPhase); +} + + /***/ }), /***/ 84403: @@ -74407,6 +76345,38 @@ function normalize(value) { } +/***/ }), + +/***/ 95914: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildManagedPullRequestMarker = buildManagedPullRequestMarker; +exports.parseManagedPullRequestMarker = parseManagedPullRequestMarker; +exports.isSafeOperationId = isSafeOperationId; +const MANAGED_PULL_REQUEST_PATTERN = //; +function buildManagedPullRequestMarker(identity) { + if (!isSafeOperationId(identity.operationId) || !Number.isSafeInteger(identity.issue) || identity.issue < 1) { + throw new Error("Managed pull request identity is invalid."); + } + return ``; +} +function parseManagedPullRequestMarker(body) { + const match = MANAGED_PULL_REQUEST_PATTERN.exec(body ?? ""); + if (!match) + return undefined; + const issue = Number(match[3]); + if (!Number.isSafeInteger(issue) || issue < 1 || !isSafeOperationId(match[1])) + return undefined; + return { operationId: match[1], phase: match[2], issue }; +} +function isSafeOperationId(value) { + return /^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(value); +} + + /***/ }), /***/ 19879: @@ -74630,20 +76600,25 @@ const SETUP_WORKFLOWS = [ { file: 'copilot_pull_request_comment.yml', feature: 'pullRequestComments' }, { file: 'release_workflow.yml', feature: 'release' }, { file: 'hotfix_workflow.yml', feature: 'hotfix' }, + { file: 'copilot_deployment_orchestration.yml', feature: ['release', 'hotfix'] }, { file: 'agent-cli-provisioning.yml', feature: 'agentProvisioning' }, { file: 'copilot_credential_health.yml', feature: 'credentialHealth' }, { file: 'copilot_close_inactive_issues.yml', feature: 'inactiveIssueClosure' }, ]; function enabledSetupWorkflowFiles(features) { return SETUP_WORKFLOWS - .filter(({ feature }) => features[feature] !== false) + .filter(({ feature }) => featureEnabled(feature, features)) .map(({ file }) => file); } function isSetupWorkflowEnabled(file, features) { if (!features) return true; const definition = SETUP_WORKFLOWS.find((candidate) => candidate.file === file); - return !definition || features[definition.feature] !== false; + return !definition || featureEnabled(definition.feature, features); +} +function featureEnabled(feature, features) { + const candidates = Array.isArray(feature) ? feature : [feature]; + return candidates.some((candidate) => features[candidate] !== false); } @@ -75481,6 +77456,16 @@ const branch_dependency_repository_1 = __nccwpck_require__(9627); const branch_sync_workspace_adapter_1 = __nccwpck_require__(81849); const observe_branch_sync_use_case_1 = __nccwpck_require__(84542); const sync_branch_use_case_1 = __nccwpck_require__(392); +const deployment_orchestration_use_case_1 = __nccwpck_require__(36850); +const github_deployment_repository_1 = __nccwpck_require__(22368); +const deployment_continuation_repository_1 = __nccwpck_require__(77509); +const deployment_presentation_repository_1 = __nccwpck_require__(91985); +const deployment_state_repository_1 = __nccwpck_require__(3182); +const legacy_deployment_merge_repository_1 = __nccwpck_require__(70245); +const octokit_deployment_adapter_1 = __nccwpck_require__(46819); +const workflow_dispatch_repository_1 = __nccwpck_require__(29509); +const github_workflow_client_factory_1 = __nccwpck_require__(29839); +const node_crypto_1 = __nccwpck_require__(6005); function createDetectPotentialProblemsUseCase() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); return new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase((0, agent_capability_composition_root_1.createFindingsQueryPort)(), bugbot.context, bugbot.publication, bugbot.resolution, bugbot.telemetry); @@ -75489,7 +77474,19 @@ function createSingleActionUseCaseCompositionRoot() { const repositoryTagPort = new repository_tag_repository_1.RepositoryTagRepository((0, github_release_client_factory_1.createReleaseClient)()); const repositoryReleasePort = new repository_release_publication_repository_1.RepositoryReleasePublicationRepository((0, github_release_client_factory_1.createReleaseClient)()); const issueDescriptionQueryPort = (0, issue_content_composition_root_1.createIssueContentCompositionRoot)(); - return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase(issueDescriptionQueryPort), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), issueDescriptionQueryPort)); + const deploymentRepository = new github_deployment_repository_1.GithubDeploymentRepository(new octokit_deployment_adapter_1.OctokitDeploymentClientAdapter()); + const deploymentOrchestration = new deployment_orchestration_use_case_1.DeploymentOrchestrationUseCase({ + pullRequests: deploymentRepository, + git: deploymentRepository, + continuation: new deployment_continuation_repository_1.DeploymentContinuationRepository(new workflow_dispatch_repository_1.WorkflowDispatchRepository((0, github_workflow_client_factory_1.createWorkflowDispatchClient)())), + legacyMerge: new legacy_deployment_merge_repository_1.LegacyDeploymentMergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)()), + presentation: new deployment_presentation_repository_1.DeploymentPresentationRepository(issueDescriptionQueryPort), + state: new deployment_state_repository_1.DeploymentStateRepository(issueDescriptionQueryPort), + labels: (0, issue_labels_composition_root_1.createIssueLabelRepository)(), + issues: (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), + operationId: node_crypto_1.randomUUID, + }); + return new single_action_use_case_1.SingleActionUseCase(new deployed_action_use_case_1.DeployedActionUseCase((0, issue_labels_composition_root_1.createIssueLabelRepository)(), (0, issue_interaction_composition_root_1.createIssueClosureRepository)(), new merge_repository_1.MergeRepository((0, github_branch_client_factory_1.createBranchMergeClient)())), new publish_github_action_use_case_1.PublishGithubActionUseCase(repositoryTagPort, repositoryReleasePort), new create_release_use_case_1.CreateReleaseUseCase(repositoryReleasePort), new create_tag_use_case_1.CreateTagUseCase(repositoryTagPort), new think_use_case_1.ThinkUseCase(issueDescriptionQueryPort, (0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, initial_setup_composition_root_1.createInitialSetupCompositionRoot)(), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(), createDetectPotentialProblemsUseCase(), new recommend_steps_use_case_1.RecommendStepsUseCase(issueDescriptionQueryPort, (0, agent_capability_composition_root_1.createFindingsQueryPort)()), (0, issue_inactivity_composition_root_1.createCloseInactiveIssuesUseCase)(), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)(), new publish_issue_comment_use_case_1.PublishIssueCommentUseCase(issueDescriptionQueryPort), new observe_branch_sync_use_case_1.ObserveBranchSyncUseCase(new branch_dependency_repository_1.BranchDependencyRepository((0, github_project_client_factory_1.createGraphqlTransportClient)()), new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), issueDescriptionQueryPort), deploymentOrchestration); } function createIssueCommentUseCaseCompositionRoot() { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(); @@ -76125,6 +78122,24 @@ function getOctokitClient(token) { } +/***/ }), + +/***/ 46819: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OctokitDeploymentClientAdapter = void 0; +const octokit_client_resolver_1 = __nccwpck_require__(54047); +class OctokitDeploymentClientAdapter { + getClient(token) { + return (0, octokit_client_resolver_1.getOctokitClient)(token); + } +} +exports.OctokitDeploymentClientAdapter = OctokitDeploymentClientAdapter; + + /***/ }), /***/ 29996: @@ -76843,6 +78858,10 @@ function buildConfigurationPayload(execution, storedRaw) { parentBranch: current.parentBranch, hotfixOriginBranch: current.hotfixOriginBranch, hotfixBranch: current.hotfixBranch, + releaseOriginBranch: current.releaseOriginBranch, + releaseOriginSha: current.releaseOriginSha, + hotfixOriginSha: current.hotfixOriginSha, + deploymentOrchestration: current.deploymentOrchestration, branchConfiguration: current.branchConfiguration, recommendationState: current.recommendationState, }; diff --git a/docs.json b/docs.json index 5491e81a3..774cb6479 100644 --- a/docs.json +++ b/docs.json @@ -338,6 +338,11 @@ "href": "/issues/branch-synchronization", "icon": "code-branch" }, + { + "title": "Release orchestration", + "href": "/issues/deployment-orchestration", + "icon": "rocket" + }, { "title": "Notifications and auto-close", "href": "/issues/notifications-and-auto-close", diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 60f8c1091..ef6b0f268 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -117,6 +117,23 @@ Copilot provides extensive configuration options to customize your workflow. Use - `release-workflow`: Release workflow filename (default: "release_workflow.yml") - `hotfix-workflow`: Hotfix workflow filename (default: "hotfix_workflow.yml") + + ## Deployment orchestration + + - `release-reconciliation-strategy`: `production-lineage` (default), `canonical-gitflow`, or `manual` + - `hotfix-reconciliation-strategy`: `production-lineage` (default), `canonical-gitflow`, or `manual` + - `reconciliation-pr-mode`: `auto` (default), `auto-merge`, `merge-queue`, `create-only`, or deprecated `legacy-wait` + - `reconciliation-backmerge-mode`: `auto` (default), `direct`, or `sync-branch` + - `hotfix-active-release-policy`: `prefer-release` (default), `development`, or `both` + - `reconciliation-tree`: Safe ephemeral branch prefix (default: `sync`) + - `reconciliation-cleanup`: `all` (default), `source-only`, `sync-only`, or `none` + - `reconciliation-issue-completion`: `close` (default) or `keep-open` + - `orchestration-presentation-mode`: `guided` (default), `compact`, or `quiet` + - `orchestration-diagrams`: Show a fixed Mermaid diagram and textual equivalent (default: `true`) + - `orchestration-comment-mode`: `update` (default) or `milestones` + + See [Release and hotfix orchestration](/issues/deployment-orchestration) + before changing these coordinated safety settings. @@ -185,6 +202,7 @@ may be forwarded through Repository Variables instead. | `single-action-title` | empty | Title used by `create_release`. | | `single-action-changelog` | empty | Markdown body used by `create_release`. | | `single-action-message` | empty | Markdown body published by `publish_issue_comment`. | +| `single-action-operation-id` | empty | Internal durable deployment identity. Continuation workflows must pass the exact ID parsed or dispatched by Copilot; do not set it for ordinary actions. | | `single-action-comment-id` | empty | Existing issue comment ID to replace or append to. | | `single-action-comment-mode` | empty | `create`, `replace`, or `append`; inferred from whether a comment ID is present when omitted. | | `inactivity-threshold-hours` | `168` | Hours without activity before the scheduled action closes an eligible waiting issue. | diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 68a77d8b8..6b5cc5921 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -111,3 +111,43 @@ translate GitHub GraphQL/REST responses; and the Git adapter owns transient merg state. Verification runs without credentials. The adapter rejects changes to Git heads, merge metadata, the non-conflicted index, or the prepared path set, then re-fetches both remote heads immediately before the trusted commit/push boundary. + +## Deployment orchestration boundary + +Release and hotfix deployment is a durable state machine rather than one +runner-owned script: + +```text +preparing -> promotion_pr_pending -> promoted -> publishing + -> published -> reconciliation_pending -> completed + +any non-terminal phase -> blocked -> previous safe phase after correction +``` + +`deployment_configuration.ts`, `deployment_operation.ts`, and the pure planning, +lifecycle, continuation-guard, and presentation policies own bounded values and +decisions. They have no GitHub client, workflow payload, process, timer, or +filesystem dependency. `DeploymentOrchestrationUseCase` receives the narrow +`DeploymentOrchestrationContext`; the legacy `Execution` aggregate is only +structurally adapted by the single-action boundary. + +Application ports expose managed-PR identity, target capabilities, exact commit +reachability, trusted reconciliation refs, workflow continuation, durable issue +state, and presentation operations. `GithubDeploymentRepository` maps REST and +GraphQL provider facts. `DeploymentStateRepository` owns the hidden issue block +mapping and preserves unknown/future configuration keys. The composition root is +the only place that binds these repositories and the Octokit adapter. + +Durable transitions compare the operation ID and expected stored phase before +each write. PR and publication side effects use deterministic markers, exact +branch/SHA facts, and postcondition verification. A continuation event or +workflow dispatch is only a wake-up signal: it must present the same operation +ID, then authoritative issue and GitHub state are reloaded. The privileged PR +completion workflow checks out the trusted base and never executes the PR head. + +Presentation is a separate mutation-free boundary. A typed English/Spanish +catalog renders the durable issue dashboard, promotion and reconciliation PRs, +and deployment Job Summary. Provider mutations accept those rendered values but +cannot choose phase transitions. Architecture tests enforce the narrow context, +provider-neutral port vocabulary, mutation-free renderer, global dependency +direction, and acyclic production imports. diff --git a/docs/development/release-process.mdx b/docs/development/release-process.mdx index a0d8ef909..eae8cc1fd 100644 --- a/docs/development/release-process.mdx +++ b/docs/development/release-process.mdx @@ -26,13 +26,15 @@ corepack pnpm run smoke:npm-package corepack pnpm run validate:documentation ``` -The project [`release_workflow.yml`](https://github.com/vypdev/copilot/blob/master/.github/workflows/release_workflow.yml) coordinates npm publishing with the rest of the release. It creates the version tag, publishes that exact tag to npm on a GitHub-hosted runner, and only then creates the GitHub release, updates the major GitHub Action reference, opens the merge PRs, and closes the launcher issue: +The project [`release_workflow.yml`](https://github.com/vypdev/copilot/blob/master/.github/workflows/release_workflow.yml) coordinates npm publishing with the rest of the release. It first promotes the frozen release branch to production through a GitHub-managed PR. After that PR merges, the continuation creates the version tag at the accepted production SHA, publishes that exact tag to npm on a GitHub-hosted runner, creates the GitHub Release, updates the major GitHub Action reference, and begins development reconciliation: ```text -create tag -> publish npm -> finalize GitHub release +prepare release -> production PR -> accepted production SHA + -> create tag -> publish npm -> finalize GitHub release + -> production-to-development reconciliation -> close issue ``` -The npm job installs the frozen dependency graph before validating and smoke-testing the package. After publishing, it waits until `npm view` exposes the exact version, polling every 20 seconds for up to 2 minutes. It uses npm trusted publishing with these least-privilege permissions: +The npm job installs the frozen dependency graph before validating and smoke-testing the package. After publishing, it waits until `npm view` exposes the exact version, polling every 20 seconds for up to 2 minutes, then verifies that the registry `gitHead` is the accepted production commit. It uses npm trusted publishing with these least-privilege permissions: ```yaml permissions: @@ -40,10 +42,13 @@ permissions: id-token: write ``` -The npm package must be configured to trust this repository and workflow before automated publication. Configure the trusted publisher for organization `vypdev`, repository `copilot`, workflow filename `release_workflow.yml`, environment `npm`, and allow the `npm publish` action. The job publishes with `npm publish --access public`; trusted publishing generates provenance automatically and does not store an npm token in the repository. npm trusted publishing currently requires a GitHub-hosted runner. +The npm package must be configured to trust this repository and workflow before automated publication. Configure trusted publishers for organization `vypdev`, repository `copilot`, environment `npm`, and both publishing workflow filenames: `release_workflow.yml` and `hotfix_workflow.yml`. The job publishes with `npm publish --access public`; trusted publishing generates provenance automatically and does not store an npm token in the repository. npm trusted publishing currently requires a GitHub-hosted runner. The repository-specific workflow is intentionally not copied to `setup/workflows`: consumers of the action do not publish `@vypdev/copilot` when running their own release workflows. +For branch origins, merge modes, recovery, and repository setup, see [Release +and hotfix orchestration](/issues/deployment-orchestration). + To verify a published version independently: ```bash diff --git a/docs/issues/deployment-orchestration.mdx b/docs/issues/deployment-orchestration.mdx new file mode 100644 index 000000000..8781c8e0b --- /dev/null +++ b/docs/issues/deployment-orchestration.mdx @@ -0,0 +1,257 @@ +--- +title: Release and hotfix orchestration +description: Production-first promotion, publication, reconciliation, configuration, and recovery. +--- + +# Release and hotfix orchestration + +Copilot treats a deployment as a durable, event-driven operation. The workflow +finishes after it creates a protected pull request; GitHub then wakes a separate +continuation when that PR is merged. A runner does not remain polling while +checks or reviews are pending. + +## Recommended default + +```mermaid +flowchart LR + D[Development snapshot] --> R[Release branch] + R --> P[Production PR] + P --> A[Accepted production commit] + A --> N[npm and GitHub Release] + N --> B[Development reconciliation PR] + B --> C[Complete] +``` + +The same route in text is: + +```text +develop@cut-SHA + -> release/@prepared-SHA + -> PR to master + -> accepted master merge-SHA + -> immutable version tag + npm + GitHub Release + -> master merge-SHA reconciled into current develop + -> cleanup and issue completion +``` + +`master`, `develop`, `release`, and `sync` are defaults. Your configured branch +names and prefixes are snapshotted when the operation starts. + + + A release comes from the exact `development-branch` commit that existed when + its release branch was cut. It does not come from production. Commits added + later to development stay outside that release and are preserved during the + final reconciliation. + + +## Release lifecycle + +1. Copilot cuts `/` from the exact current + `development-branch` SHA and stores both the origin branch and origin SHA on + the launcher issue. +2. Version files, build output, validation, and package smoke tests are committed + on that frozen release branch. +3. Copilot creates or reuses one marked promotion PR from the release branch to + `main-branch`, then asks GitHub for auto-merge, merge queue, or maintainer + merge according to configuration. +4. The preparation run exits successfully. Pending checks are an external wait, + not a workflow failure. +5. After the marked PR merges, Copilot verifies its repository, marker, stored + head SHA, base, and accepted production SHA. +6. The publication continuation creates `v` at that exact production + SHA, publishes the package, waits for registry visibility, creates the + GitHub Release, and updates the moving major Action tag. +7. Copilot creates or reuses a reconciliation PR into current development. The + default source is the accepted production commit. +8. After reconciliation merges, Copilot applies cleanup, closes or retains the + issue as configured, and marks the operation complete. + +The immutable facts are visible in one issue control-center comment. Duplicate +events and retries update that comment instead of creating another operation. + +## Hotfix lifecycle + +A hotfix follows the same production-first route with one important origin +difference: + +```text +latest accepted production tag + -> hotfix/ + -> PR to production + -> publish accepted production commit + -> active release and/or development reconciliation +``` + +The hotfix origin SHA is the commit behind the selected production version tag, +not the current development head. If one active release exists, the default +`prefer-release` policy reconciles the hotfix into it so the release does not +lose the production fix. Without an active release it reconciles into +development. More than one active release blocks an ambiguous decision instead +of guessing. + +## Configuration reference + +Every value below is a bounded Action input, supported by `copilot setup`, and +persisted for the lifetime of an operation. + +| Input | Default | Allowed values or limit | Use | +|---|---|---|---| +| `release-reconciliation-strategy` | `production-lineage` | `production-lineage`, `canonical-gitflow`, `manual` | Select the release source carried back after publication. | +| `hotfix-reconciliation-strategy` | `production-lineage` | `production-lineage`, `canonical-gitflow`, `manual` | Select the hotfix source carried back after publication. | +| `reconciliation-pr-mode` | `auto` | `auto`, `auto-merge`, `merge-queue`, `create-only`, `legacy-wait` | Select who merges managed PRs. | +| `reconciliation-backmerge-mode` | `auto` | `auto`, `direct`, `sync-branch` | Select the reconciliation PR head shape. | +| `hotfix-active-release-policy` | `prefer-release` | `prefer-release`, `development`, `both` | Select hotfix reconciliation targets. | +| `reconciliation-tree` | `sync` | Safe Git ref prefix, 1–100 characters | Prefix for deterministic ephemeral branches. | +| `reconciliation-cleanup` | `all` | `all`, `source-only`, `sync-only`, `none` | Select branches removed only after all targets complete. | +| `reconciliation-issue-completion` | `close` | `close`, `keep-open` | Select final launcher-issue behavior. | +| `orchestration-presentation-mode` | `guided` | `guided`, `compact`, `quiet` | Select issue dashboard detail. | +| `orchestration-diagrams` | `true` | `true`, `false` | Show the fixed Mermaid view in guided mode. | +| `orchestration-comment-mode` | `update` | `update`, `milestones` | Update one dashboard or add at most four milestone comments. | + +`production-lineage` is recommended because the development branch visibly +inherits the commit accepted in production. `canonical-gitflow` instead carries +the frozen release/hotfix head to development. `manual` publishes but creates no +reconciliation PR and always leaves the issue open. + +`auto` PR mode selects a required merge queue first, native auto-merge when +available, and otherwise a maintainer-owned PR. `legacy-wait` is the only mode +that may use `merge-timeout`; it is retained for migration and is not the +default. + +`auto` back-merge mode uses a unique sync branch when a strict target has +advanced. The branch starts at the current target and receives the trusted +reconciliation commit server-side, preserving target-only commits and giving +target-dependent checks a unique head SHA. + +### Invalid combinations + +Copilot rejects configuration before mutation when: + +- production and development are the same branch; +- a release, hotfix, or reconciliation prefix is empty, unsafe, or equals a + long-lived branch; +- `merge-queue` is selected without required `merge_group` workflow support; or +- either reconciliation strategy is `manual` while issue completion is `close`. + +Branch prefixes cannot contain whitespace, `..`, `@{`, Git ref control +characters, or a leading/trailing slash. Arbitrary refs, shell fragments, and +custom merge graphs are not accepted. + +## Repository setup + +Run `copilot setup` or copy all three generated workflow contracts: + +- `release_workflow.yml` and `hotfix_workflow.yml`, each with internal + `prepare` and `publish` modes; +- `copilot_deployment_orchestration.yml`, listening only to + `pull_request: closed` and checking out the trusted PR base; and +- `merge_group: checks_requested` on every workflow that provides a required + check for merge-queue targets. + +The continuation workflow uses the PAT to ensure bot-created PR events can wake +GitHub Actions. Grant only the repository permissions needed by enabled +features: Contents read/write, Issues read/write, Pull requests read/write, and +Actions read/write for continuation dispatch. The normal `GITHUB_TOKEN` remains +read-only where it is sufficient. Never use `pull_request_target` to execute a +managed PR head. + +### npm trusted publishing for this repository + +The project workflows publish `@vypdev/copilot` from `ubuntu-latest` through +npm OIDC. No npm key is stored in GitHub. + +1. In GitHub, create an Environment named `npm`. Add required reviewers only if + you intentionally want a human publication gate. +2. On npm, add GitHub Actions trusted publishers for organization `vypdev`, + repository `copilot`, environment `npm`, and each workflow that publishes: + `release_workflow.yml` and `hotfix_workflow.yml`. +3. Keep `id-token: write` scoped only to the `publish-npm` job. +4. Do not define `NPM_TOKEN` or `NODE_AUTH_TOKEN` for this path. + +The job checks out the immutable tag, installs the frozen lockfile, validates +package name/version and tarball contents, smoke-tests the package, skips an +already-visible exact version, and polls `npm view` every 20 seconds for a +bounded 120 seconds by default. Before finalization it also verifies that npm's +registry `gitHead` matches the accepted production SHA. + +The reusable setup templates intentionally contain a generic publication +extension point. Consumer repositories must implement their own registry or +deployment publication and then invoke `published_deployment_action` with the +same `operation-id`. + +## Protected branches and merge queues + +- Enable merge commits for managed production-lineage PRs; squash/rebase do not + provide the ancestry guarantee required by this strategy. +- Enable repository auto-merge before explicitly selecting `auto-merge`. +- When a branch requires a merge queue, every required GitHub Actions check + must run for `merge_group`. Confirm equivalent support for third-party checks + such as code-health services before selecting queue mode. +- Strict development branches normally cause `auto` back-merge to use a sync + branch when development advanced after the release cut. +- Closing a managed PR without merge blocks the operation; it never bypasses + protection and never publishes from an unaccepted commit. + +## What maintainers see + +The launcher issue is the control center. `guided` shows status, a checklist, +diagram, current branch transition, next action, links, and collapsed technical +facts. `compact` removes the diagram and expanded context. `quiet` retains the +durable status, actionable failures, human requests, and completion without +routine detail. + +Promotion PRs state that publication occurs only after merge. Reconciliation +PRs state prominently that the package is already public and that merging or +closing the PR cannot publish it again. Issue text follows `issues-locale`; PR +text follows `pull-requests-locale`; Spanish and English are built in, with a +safe English fallback. + +## Recovery by visible phase + +Start with the phase shown at the top of the issue control center: + +```text +promotion_pr_pending + open PR -> let GitHub finish checks/reviews + closed unmerged -> reopen/correct the PR, then rerun prepare + +publishing + tag exists at stored production SHA -> rerun publish mode; tag is reused + npm version exists -> rerun; package publish is skipped and verified + tag points elsewhere -> stop and resolve the immutable-tag conflict + +reconciliation_pending + PR open -> merge through normal protection + PR closed unmerged -> correct/reopen it, then replay the continuation + package is already public -> never change version or republish + +blocked + retryable -> correct the stated cause and rerun the phase + not retryable -> inspect stored SHAs/branches and choose an explicit recovery + +completed + no publication action is needed; replayed events are safe no-ops +``` + +If a workflow is canceled between tag creation and npm publication, rerun its +`publish` mode with the launcher issue and persisted operation ID. If npm or the +GitHub Release already exists, the continuation verifies and reuses it. An +immutable version tag at another SHA is never moved. + +For legacy in-progress issues without v3 orchestration state, the compatibility +path remains available for one migration period. `deployed_action` uses the new +state-aware path when state exists and otherwise retains the old behavior. +New releases always use the production-first state machine. + +## Security and durable facts + +The issue stores the operation ID, kind, version, exact release/hotfix source +SHA, release development-origin SHA or hotfix tag-origin SHA, branch roles, +strategy, publication workflow, production merge SHA, PR identities, and +reconciliation state. Secrets and OIDC claims are never persisted. + +Continuation inputs are wake-up hints. Before mutation, Copilot reloads issue +state and requires the submitted `operation-id`, expected phase, same-repository +managed marker, exact branch pair, stored source SHA, and production +reachability to agree. A fork event, forged marker, stale dispatch, or reordered +delivery cannot authorize publication. diff --git a/docs/issues/type/hotfix.mdx b/docs/issues/type/hotfix.mdx index 523f533b6..fc45d080e 100644 --- a/docs/issues/type/hotfix.mdx +++ b/docs/issues/type/hotfix.mdx @@ -11,7 +11,8 @@ The hotfix process follows these steps after creating an issue with `hotfix` and - A hotfix branch is created from `main-branch` at the latest tag version + A hotfix branch is created from the latest accepted production version tag, + and that exact origin SHA is persisted. A bugfix branch is created from the hotfix branch to implement the fix @@ -22,20 +23,26 @@ The hotfix process follows these steps after creating an issue with `hotfix` and The bugfix is merged back into the hotfix branch via Pull Request - - A new version is deployed from the hotfix branch by adding the `deploy-label` to the issue + + Copilot opens the hotfix PR to `main-branch` and lets GitHub enforce its + checks, reviews, auto-merge, or merge queue. - - After successful deployment, the hotfix branch is merged: - - First into `main-branch` with the new version tag - - Then into `development-branch` to keep changes synchronized + + Only after promotion merges, Copilot tags and publishes the exact accepted + production merge SHA. + + + By default, the fix is reconciled into one active release when present; + otherwise it is reconciled into development. `development` and `both` are + explicit alternatives. The issue is automatically closed with the `deployed-label` label once the process completes -This workflow ensures that critical fixes reach production quickly while maintaining code quality and proper version control. The process is designed to minimize disruption while addressing urgent issues. +See [Release and hotfix orchestration](/issues/deployment-orchestration) for +configuration, multiple-active-release handling, OIDC setup, and recovery. Deploy workflow files should be located in the `.github/workflows` directory of your repository. @@ -57,60 +64,9 @@ This workflow ensures that critical fixes reach production quickly while maintai ``` - - The hotfix deployment workflow is defined in the `hotfix_workflow.yml` file. - - The workflow requires the following inputs: - - `version`: The new version number - - `title`: Title for the release - - `changelog`: Description of changes - - `issue`: ID of the originating issue - - Remember to create a new tag for the new deployed version. - - ```yml - name: Task - Hotfix - - on: - workflow_dispatch: - inputs: - version: - description: 'Hotfix version' - required: true - default: '1.0.0' - title: - description: 'Title' - required: true - default: 'New Version' - changelog: - description: 'Changelog' - required: true - default: '- Several improvements' - issue: - description: 'Launcher issue' - required: true - default: '-1' - - jobs: - deploy: - name: Git Board - Issue - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - // create a new tag version here - - // deploy logic here - - - name: Git Board - Deploy success notification - uses: vypdev/copilot@v3 - if: ${{ success() }} - with: - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - token: ${{ secrets.PAT }} - ``` + + The workflow creates or verifies it after production accepts the hotfix. The + immutable tag is never created from an unmerged hotfix head. ## Example @@ -122,7 +78,8 @@ For a hotfix issue #123 fixing a critical login bug: 3. Fix implemented and tested on bugfix branch 4. PR merged into `hotfix/1.2.4` 5. Version 1.2.4 deployed to production -6. Changes merged to `master` and `develop` +6. Promotion merged to `master`; package and GitHub Release published from that commit +7. Fix reconciled into the configured active release and/or `develop` ## Label diff --git a/docs/issues/type/release.mdx b/docs/issues/type/release.mdx index 0ef2e5988..181266c28 100644 --- a/docs/issues/type/release.mdx +++ b/docs/issues/type/release.mdx @@ -7,35 +7,40 @@ Release issues are used to track and manage the process of creating new versions ## Workflow -The release process follows these steps after creating an issue with `release` and `branched` labels: +The release process follows these steps after creating an issue with `release` +and `branched` labels: - - A release branch is created from `development-branch` + + Copilot creates the release branch from the exact current + `development-branch` SHA and persists that origin. Development commits made + after this cut are not part of the release. - - A feature branch is created from the release branch to implement any remaining changes + + Version, build, changelog, fixes, validation, and package smoke-test changes + are completed on the release line. - - The needed changes are applied on the feature branch + + Copilot opens `release/ -> main-branch` and delegates checks, + reviews, auto-merge, or merge queue behavior to GitHub. The workflow exits + while that external transition is pending. - - The feature branch is merged back into the release branch via Pull Request + + Only after the promotion PR merges, Copilot tags its accepted production + merge SHA and starts the configured package/GitHub publication. - - A new version is deployed from the release branch by adding the `deploy-label` to the issue + + By default, the accepted production commit is reconciled into the current + development branch, preserving development commits made after the cut. - - After successful deployment, the release branch is merged: - - First into `main-branch` with the new version tag - - Then back into `development-branch` to keep changes synchronized - - - The issue is automatically closed with the `deployed-label` label once the process completes + + The configured temporary branches are removed and the launcher issue is + closed or retained only after all reconciliation targets complete. -This workflow ensures that releases are properly planned, tested, and deployed while maintaining code quality and proper version control. +See [Release and hotfix orchestration](/issues/deployment-orchestration) for the +full configuration, OIDC setup, strict-branch behavior, UI, and recovery guide. Deploy workflow files should be located in the `.github/workflows` directory of your repository. @@ -57,60 +62,10 @@ This workflow ensures that releases are properly planned, tested, and deployed w ``` - - The release deployment workflow is defined in the `release_workflow.yml` file. - - The workflow requires the following inputs: - - `version`: The new version number - - `title`: Title for the release - - `changelog`: Description of changes - - `issue`: ID of the originating issue - - Remember to create a new tag for the new deployed version. - - ```yml - name: Task - Release - - on: - workflow_dispatch: - inputs: - version: - description: 'Release version' - required: true - default: '1.0.0' - title: - description: 'Title' - required: true - default: 'New Version' - changelog: - description: 'Changelog' - required: true - default: '- Several improvements' - issue: - description: 'Launcher issue' - required: true - default: '-1' - - jobs: - deploy: - name: Git Board - Issue - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - // create a new tag version here - - // deploy logic here - - - name: Git Board - Deploy success notification - uses: vypdev/copilot@v3 - if: ${{ success() }} - with: - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - token: ${{ secrets.PAT }} - ``` + + The workflow creates or verifies the immutable version tag automatically, + after the production promotion merges. The tag must point to the accepted + `main-branch` merge SHA; maintainers do not create it from the release branch. ## Example @@ -122,7 +77,9 @@ For a release issue #456 releasing version 1.3.0: 3. Changes applied and tested on feature branch 4. PR merged into `release/1.3.0` 5. Version 1.3.0 deployed to production (add `deploy` label to the issue) -6. Release branch merged to `master` (with tag) and back into `develop` +6. Promotion PR merged into `master`; `v1.3.0` created at that accepted commit +7. Package and GitHub Release published +8. Accepted production commit reconciled into current `develop` ## Label diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index e528aefa2..d7ed32144 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -12,13 +12,13 @@ const workflowDirectories = [ const QUEUE_WAIT_MINUTES = 90; const QUEUE_GATE_TIMEOUT_MINUTES = 120; const PREPARE_VERSION_TIMEOUT_MINUTES = 15; -const PREPARE_COMPILED_TIMEOUT_MINUTES = 20; -const TAG_TIMEOUT_MINUTES = 120; +const PREPARE_COMPILED_TIMEOUT_MINUTES = 30; +const TAG_TIMEOUT_MINUTES = 10; const NPM_PUBLISH_TIMEOUT_MINUTES = 20; -const FINALIZE_RELEASE_TIMEOUT_MINUTES = 120; +const FINALIZE_RELEASE_TIMEOUT_MINUTES = 15; const FAILURE_REPORT_TIMEOUT_MINUTES = 5; const MIN_QUEUE_JOB_TIMEOUT_MINUTES = QUEUE_GATE_TIMEOUT_MINUTES; -const FAILURE_REPORT_CONDITION = "${{ failure() && github.event.inputs.issue != '-1' }}"; +const FAILURE_REPORT_CONDITION = "${{ failure() && inputs.issue != '-1' }}"; const DISTRIBUTED_COPILOT_ACTION = 'vypdev/copilot@v3'; const CHECKOUT_ACTION = 'actions/checkout@v5'; @@ -39,8 +39,8 @@ const QUEUE_WORKFLOW_MANIFEST = Object.freeze([ ['copilot_pull_request.yml', 'Copilot - Pull Request', 'copilot-pull-requests'], ['copilot_pull_request_comment.yml', 'Copilot - Pull Request Comment', 'copilot-pull-requests'], ['copilot_close_inactive_issues.yml', 'Copilot - Close Inactive Issues', 'copilot-inactive-issues'], - ['hotfix_workflow.yml', 'Task - Hotfix', 'tag'], - ['release_workflow.yml', 'Task - Release', 'tag'], + ['hotfix_workflow.yml', 'Task - Hotfix', 'queue-gate'], + ['release_workflow.yml', 'Task - Release', 'queue-gate'], ].map(([file, workflowName, jobId]) => ({ file, workflowName, jobId }))); const MUTATION_WORKFLOW_MANIFEST = Object.freeze([ @@ -164,7 +164,7 @@ function assertRunner(file, workflow) { for (const [jobId, job] of Object.entries(workflow.jobs ?? {})) { const expected = relativeFile.startsWith('setup/workflows/') ? ['ubuntu-latest'] - : relativeFile === '.github/workflows/release_workflow.yml' && jobId === 'publish-npm' + : /^\.github\/workflows\/(?:release|hotfix)_workflow\.yml$/.test(relativeFile) && jobId === 'publish-npm' ? ['ubuntu-latest'] : relativeFile === '.github/workflows/repowise.yml' ? ['self-hosted', 'coolify'] @@ -365,8 +365,8 @@ function assertNpmPublishJob(relativeFile, job) { if (!job) throw new Error(`${relativeFile} must define publish-npm.`); assertExactTimeout(relativeFile, 'publish-npm', job, NPM_PUBLISH_TIMEOUT_MINUTES); assertExactNeeds(relativeFile, 'publish-npm', job, ['tag']); - if (job.if !== undefined) { - throw new Error(`${relativeFile} publish-npm must be a required release gate.`); + if (job.if !== "${{ inputs.mode == 'publish' }}") { + throw new Error(`${relativeFile} publish-npm must run only in publish mode.`); } if (job.environment !== 'npm') { throw new Error(`${relativeFile} publish-npm must use the npm environment.`); @@ -384,24 +384,34 @@ function assertNpmPublishJob(relativeFile, job) { const checkout = steps.find(step => step?.uses === CHECKOUT_ACTION); const setupNode = steps.find(step => step?.uses === 'actions/setup-node@v7'); const install = steps.find(step => step?.run === 'pnpm install --frozen-lockfile'); - const validation = steps.find(step => step?.name === 'Validate release identity and package contents'); + const validation = steps.find(step => step?.name === 'Validate immutable package'); const publish = steps.find(step => step?.run === 'npm publish --access public'); - const availability = steps.find(step => step?.name === 'Wait for npm registry availability'); - if (checkout?.with?.ref !== 'v${{ github.event.inputs.version }}' + const availability = steps.find(step => step?.name === 'Wait for npm registry visibility'); + const identity = steps.find(step => step?.name === 'Verify published package identity'); + const detection = steps.find(step => step?.name === 'Detect an existing publication'); + if (checkout?.with?.ref !== 'v${{ inputs.version }}' || checkout.with?.['fetch-depth'] !== 1 || setupNode?.with?.['node-version'] !== '24.x' || setupNode.with?.['registry-url'] !== 'https://registry.npmjs.org' || setupNode.with?.['package-manager-cache'] !== false || !install - || validation?.env?.RELEASE_TAG !== 'v${{ github.event.inputs.version }}' + || validation?.env?.RELEASE_VERSION !== '${{ inputs.version }}' || !String(validation?.run ?? '').includes('pnpm run validate:npm-package') || !String(validation?.run ?? '').includes('pnpm run smoke:npm-package') || !publish + || publish.if !== "${{ steps.registry.outputs.publish == 'true' }}" + || !String(detection?.run ?? '').includes('npm view "$PACKAGE_NAME@$RELEASE_VERSION" version') || availability?.env?.PACKAGE_NAME !== '@vypdev/copilot' - || availability.env?.RELEASE_VERSION !== '${{ github.event.inputs.version }}' + || availability.env?.RELEASE_VERSION !== '${{ inputs.version }}' || !String(availability.run ?? '').includes('npm view "$PACKAGE_NAME@$RELEASE_VERSION" version') - || !String(availability.run ?? '').includes('max_attempts=7') - || !String(availability.run ?? '').includes('sleep 20')) { + || availability.env?.POLL_INTERVAL !== "${{ vars.NPM_VISIBILITY_POLL_INTERVAL_SECONDS || '20' }}" + || availability.env?.POLL_TIMEOUT !== "${{ vars.NPM_VISIBILITY_TIMEOUT_SECONDS || '120' }}" + || !String(availability.run ?? '').includes('sleep "$POLL_INTERVAL"') + || identity?.env?.PACKAGE_NAME !== '@vypdev/copilot' + || identity.env?.RELEASE_VERSION !== '${{ inputs.version }}' + || !String(identity.run ?? '').includes('npm view "$PACKAGE_NAME@$RELEASE_VERSION" gitHead') + || !String(identity.run ?? '').includes('git rev-parse HEAD') + || !String(identity.run ?? '').includes('"$registry_git_head" = "$local_git_head"')) { throw new Error(`${relativeFile} publish-npm must install, validate, and publish the exact release tag.`); } if (JSON.stringify(job).includes('NPM_TOKEN') || JSON.stringify(job).includes('NODE_AUTH_TOKEN')) { @@ -482,40 +492,59 @@ function assertFailureReportingJob(relativeFile, job, expectedNeeds, expectedKin } function assertActiveFailureReporter(relativeFile, steps, expectedKind) { - if (steps.length !== 2 + if (steps.length !== 3 || steps[0]?.uses !== CHECKOUT_ACTION || steps[0]?.with?.['persist-credentials'] !== false - || steps[1]?.uses !== './') { + || steps[1]?.uses !== './' + || steps[2]?.uses !== './') { throw new Error(`${relativeFile} report-failure must checkout safely and invoke the local Copilot action.`); } - const inputs = steps[1].with ?? {}; + assertDurableFailureReporter(relativeFile, steps[1]); + const inputs = steps[2].with ?? {}; const expectedTitle = expectedKind === 'release' ? 'Release' : 'Hotfix'; - const expectedToken = expectedKind === 'release' ? '${{ secrets.PAT }}' : '${{ github.token }}'; - if (inputs['single-action'] !== 'publish_issue_comment' - || inputs['single-action-issue'] !== '${{ github.event.inputs.issue }}' + const expectedToken = '${{ secrets.PAT }}'; + if (steps[2].if !== "${{ inputs.mode == 'prepare' }}" + || inputs['single-action'] !== 'publish_issue_comment' + || inputs['single-action-issue'] !== '${{ inputs.issue }}' || inputs.token !== expectedToken - || !String(inputs['single-action-message'] ?? '').includes(`## ❌ ${expectedTitle} deployment failed`) + || !String(inputs['single-action-message'] ?? '').includes(`## ❌ ${expectedTitle} orchestration needs attention`) || !String(inputs['single-action-message'] ?? '').includes('${{ github.run_id }}')) { throw new Error(`${relativeFile} report-failure must invoke publish_issue_comment with the launcher issue and run link.`); } } function assertSetupFailureReporter(relativeFile, steps, expectedKind) { - if (steps.length !== 1 - || steps[0]?.uses !== DISTRIBUTED_COPILOT_ACTION) { + if (steps.length !== 2 + || steps[0]?.uses !== DISTRIBUTED_COPILOT_ACTION + || steps[1]?.uses !== DISTRIBUTED_COPILOT_ACTION) { throw new Error(`${relativeFile} report-failure must invoke the released Copilot action.`); } - const inputs = steps[0].with ?? {}; + assertDurableFailureReporter(relativeFile, steps[0]); + const inputs = steps[1].with ?? {}; const expectedTitle = expectedKind === 'release' ? 'Release' : 'Hotfix'; - if (inputs['single-action'] !== 'publish_issue_comment' - || inputs['single-action-issue'] !== '${{ github.event.inputs.issue }}' - || inputs.token !== '${{ github.token }}' - || !String(inputs['single-action-message'] ?? '').includes(`## ❌ ${expectedTitle} deployment failed`) + if (steps[1].if !== "${{ inputs.mode == 'prepare' }}" + || inputs['single-action'] !== 'publish_issue_comment' + || inputs['single-action-issue'] !== '${{ inputs.issue }}' + || inputs.token !== '${{ secrets.PAT }}' + || !String(inputs['single-action-message'] ?? '').includes(`## ❌ ${expectedTitle} orchestration needs attention`) || !String(inputs['single-action-message'] ?? '').includes('${{ github.run_id }}')) { throw new Error(`${relativeFile} report-failure must invoke publish_issue_comment with the launcher issue and run link.`); } } +function assertDurableFailureReporter(relativeFile, step) { + const inputs = step?.with ?? {}; + if (step?.if !== "${{ inputs.mode == 'publish' }}" + || inputs['single-action'] !== 'failed_deployment_action' + || inputs['single-action-issue'] !== '${{ inputs.issue }}' + || inputs['single-action-operation-id'] !== '${{ inputs.operation-id }}' + || inputs['single-action-version'] !== '${{ inputs.version }}' + || inputs.token !== '${{ secrets.PAT }}' + || !String(inputs['single-action-message'] ?? '').includes('${{ github.run_id }}')) { + throw new Error(`${relativeFile} report-failure must persist publish-mode failure against the durable operation.`); + } +} + function assertMutationWorkflow(file, workflow) { const relativeFile = relativeWorkflow(file); const manifest = MUTATION_WORKFLOW_MANIFEST.find(entry => relativeFile.endsWith(`/${entry.file}`)); @@ -524,12 +553,10 @@ function assertMutationWorkflow(file, workflow) { throw new Error(`${relativeFile} must have workflow name ${JSON.stringify(manifest.workflowName)}.`); } const setup = relativeFile.startsWith('setup/workflows/'); - const coordinatedNpmRelease = !setup && manifest.file === 'release_workflow.yml'; + const finalJob = manifest.file === 'release_workflow.yml' ? 'finalize-release' : 'finalize-hotfix'; const expectedJobs = setup - ? ['queue-gate', 'prepare-version-files', 'tag', 'report-failure'] - : coordinatedNpmRelease - ? ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'tag', 'publish-npm', 'finalize-release', 'report-failure'] - : ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'tag', 'report-failure']; + ? ['queue-gate', 'prepare-version-files', 'promote', 'publish', 'report-failure'] + : ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'promote', 'tag', 'publish-npm', finalJob, 'report-failure']; const actualJobs = Object.keys(workflow.jobs ?? {}); if (actualJobs.length !== expectedJobs.length || expectedJobs.some(jobId => !actualJobs.includes(jobId))) { throw new Error(`${relativeFile} must define the exact gate-first job graph.`); @@ -537,35 +564,33 @@ function assertMutationWorkflow(file, workflow) { assertNoConcurrency(relativeFile, workflow); assertQueueGateJob(file, workflow, setup ? DISTRIBUTED_COPILOT_ACTION : './'); assertExactTimeout(relativeFile, 'queue-gate', workflow.jobs['queue-gate'], QUEUE_GATE_TIMEOUT_MINUTES); - assertExactTimeout(relativeFile, 'prepare-version-files', workflow.jobs['prepare-version-files'], PREPARE_VERSION_TIMEOUT_MINUTES); assertExactNeeds(relativeFile, 'queue-gate', workflow.jobs['queue-gate'], []); assertExactNeeds(relativeFile, 'prepare-version-files', workflow.jobs['prepare-version-files'], ['queue-gate']); if (setup) { - assertExactTimeout(relativeFile, 'tag', workflow.jobs.tag, TAG_TIMEOUT_MINUTES); - assertTagPermissions(relativeFile, workflow.jobs.tag); - assertExactNeeds(relativeFile, 'tag', workflow.jobs.tag, ['prepare-version-files']); + assertExactTimeout(relativeFile, 'prepare-version-files', workflow.jobs['prepare-version-files'], 30); + assertExactNeeds(relativeFile, 'promote', workflow.jobs.promote, ['prepare-version-files']); + assertExactNeeds(relativeFile, 'publish', workflow.jobs.publish, ['queue-gate']); + assertPhaseConditions(relativeFile, workflow.jobs, 'publish'); assertFailureReportingJob( relativeFile, workflow.jobs['report-failure'], - ['queue-gate', 'prepare-version-files', 'tag'], + ['queue-gate', 'prepare-version-files', 'promote', 'publish'], manifest.file.startsWith('release') ? 'release' : 'hotfix', ); } else { + assertExactTimeout(relativeFile, 'prepare-version-files', workflow.jobs['prepare-version-files'], PREPARE_VERSION_TIMEOUT_MINUTES); assertExactTimeout(relativeFile, 'prepare-compiled-files', workflow.jobs['prepare-compiled-files'], PREPARE_COMPILED_TIMEOUT_MINUTES); assertExactTimeout(relativeFile, 'tag', workflow.jobs.tag, TAG_TIMEOUT_MINUTES); - assertTagPermissions(relativeFile, workflow.jobs.tag); assertExactNeeds(relativeFile, 'prepare-compiled-files', workflow.jobs['prepare-compiled-files'], ['prepare-version-files']); - assertExactNeeds(relativeFile, 'tag', workflow.jobs.tag, ['prepare-compiled-files']); - if (coordinatedNpmRelease) { - assertNpmPublishJob(relativeFile, workflow.jobs['publish-npm']); - assertFinalizeReleaseJob(relativeFile, workflow.jobs['finalize-release']); - } + assertExactNeeds(relativeFile, 'promote', workflow.jobs.promote, ['prepare-compiled-files']); + assertExactNeeds(relativeFile, 'tag', workflow.jobs.tag, ['queue-gate']); + assertNpmPublishJob(relativeFile, workflow.jobs['publish-npm']); + assertPublishedFinalizeJob(relativeFile, workflow.jobs[finalJob], finalJob); + assertPhaseConditions(relativeFile, workflow.jobs, finalJob); assertFailureReportingJob( relativeFile, workflow.jobs['report-failure'], - coordinatedNpmRelease - ? ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'tag', 'publish-npm', 'finalize-release'] - : ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'tag'], + ['queue-gate', 'prepare-version-files', 'prepare-compiled-files', 'promote', 'tag', 'publish-npm', finalJob], manifest.file.startsWith('release') ? 'release' : 'hotfix', ); } @@ -573,6 +598,89 @@ function assertMutationWorkflow(file, workflow) { return true; } +function assertPhaseConditions(relativeFile, jobs, finalJob) { + for (const jobId of ['prepare-version-files', 'prepare-compiled-files', 'promote'].filter(id => jobs[id])) { + if (jobs[jobId].if !== "${{ inputs.mode == 'prepare' }}") { + throw new Error(`${relativeFile} job ${jobId} must run only in prepare mode.`); + } + } + for (const jobId of ['tag', 'publish-npm', 'publish', finalJob].filter(id => jobs[id])) { + if (jobs[jobId].if !== "${{ inputs.mode == 'publish' }}") { + throw new Error(`${relativeFile} job ${jobId} must run only in publish mode.`); + } + } + const preparationActions = Object.values(jobs).flatMap(job => job.steps ?? []) + .filter(step => isCopilotAction(step) && step.with?.['single-action'] === 'prepare_deployment_action'); + if (preparationActions.length !== 1 || preparationActions[0].with?.token !== '${{ secrets.PAT }}') { + throw new Error(`${relativeFile} must create exactly one promotion through the PAT-backed prepare_deployment_action.`); + } + const publishedActions = Object.values(jobs).flatMap(job => job.steps ?? []) + .filter(step => isCopilotAction(step) && step.with?.['single-action'] === 'published_deployment_action'); + if (publishedActions.length !== 1) { + throw new Error(`${relativeFile} must project publication through published_deployment_action.`); + } + const trustedContinuations = Object.values(jobs).flatMap(job => job.steps ?? []) + .filter(step => isCopilotAction(step) && [ + 'create_tag', + 'create_release', + 'publish_github_action', + 'published_deployment_action', + ].includes(step.with?.['single-action'])); + if (trustedContinuations.some(step => step.with?.['single-action-operation-id'] !== '${{ inputs.operation-id }}' + || step.with?.['single-action-version'] !== '${{ inputs.version }}')) { + throw new Error(`${relativeFile} must bind every publication mutation to inputs.operation-id and inputs.version.`); + } +} + +function assertPublishedFinalizeJob(relativeFile, job, jobId) { + assertExactTimeout(relativeFile, jobId, job, FINALIZE_RELEASE_TIMEOUT_MINUTES); + assertExactNeeds(relativeFile, jobId, job, ['publish-npm']); + const actions = (job.steps ?? []).filter(isCopilotAction).map(step => step.with?.['single-action']); + for (const required of ['create_release', 'publish_github_action', 'published_deployment_action']) { + if (!actions.includes(required)) throw new Error(`${relativeFile} ${jobId} must invoke ${required}.`); + } +} + +function assertDeploymentContinuationWorkflow(file, workflow) { + const relativeFile = relativeWorkflow(file); + if (!relativeFile.endsWith('/copilot_deployment_orchestration.yml')) return; + if (workflow.on?.pull_request?.types?.join(',') !== 'closed' || workflow.on?.pull_request_target) { + throw new Error(`${relativeFile} must use only pull_request: closed as its wake-up event.`); + } + const job = workflow.jobs?.continue; + if (!job + || !String(job.if ?? '').includes('github.event.pull_request.head.repo.full_name == github.repository') + || !String(job.if ?? '').includes('/); + if (!match) core.setFailed('The managed deployment marker is malformed.'); + else { + core.setOutput('operation-id', match[1]); + core.setOutput('issue', match[3]); + } + - name: Checkout trusted base branch + uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 1 + - name: Advance durable deployment operation + uses: vypdev/copilot@v3 + with: + single-action: continue_deployment_action + single-action-issue: ${{ steps.identity.outputs.issue }} + single-action-operation-id: ${{ steps.identity.outputs.operation-id }} + token: ${{ secrets.PAT }} diff --git a/setup/workflows/copilot_pull_request.yml b/setup/workflows/copilot_pull_request.yml index 795297bf2..fca0bf249 100644 --- a/setup/workflows/copilot_pull_request.yml +++ b/setup/workflows/copilot_pull_request.yml @@ -5,8 +5,22 @@ on: types: [opened, reopened, edited, closed, synchronize] pull_request_review: types: [submitted, edited, dismissed] + merge_group: + types: [checks_requested] jobs: + copilot-merge-group: + if: ${{ github.event_name == 'merge_group' }} + name: Copilot - Pull Request + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + checks: write + contents: read + steps: + - name: Confirm merge-group compatibility + run: echo "Copilot PR analysis already ran on each constituent pull request." + copilot-pull-requests: if: ${{ (vars.COPILOT_BOT_LOGIN == '' || github.actor != vars.COPILOT_BOT_LOGIN) && github.event.pull_request.head.repo.full_name == github.repository }} name: Copilot - Pull Request diff --git a/setup/workflows/hotfix_workflow.yml b/setup/workflows/hotfix_workflow.yml index 21924e499..15ea4d22a 100644 --- a/setup/workflows/hotfix_workflow.yml +++ b/setup/workflows/hotfix_workflow.yml @@ -3,18 +3,28 @@ name: Task - Hotfix on: workflow_dispatch: inputs: - version: - description: 'Hotfix version' + mode: + description: 'Internal orchestration phase' required: true - default: '1.0.0' + default: prepare + type: choice + options: [prepare, publish] + operation-id: + description: 'Internal durable operation identifier' + required: false + default: '' + version: + description: 'Hotfix version (MAJOR.MINOR.PATCH)' + required: false + default: '' title: - description: 'Title' - required: true - default: 'New Version' + description: 'Hotfix title' + required: false + default: '' changelog: - description: 'Changelog' - required: true - default: '- Several improvements' + description: 'Hotfix changelog' + required: false + default: '' issue: description: 'Launcher issue' required: true @@ -39,194 +49,152 @@ jobs: token: ${{ github.token }} prepare-version-files: - name: Prepare files for hotfix + name: Prepare and validate hotfix + if: ${{ inputs.mode == 'prepare' }} runs-on: ubuntu-latest needs: queue-gate - timeout-minutes: 15 + timeout-minutes: 30 permissions: contents: write steps: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 - with: - node-version: '24.x' - - - name: Set up pnpm - uses: pnpm/action-setup@v5 - with: - version: 10.12.4 - standalone: false - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Validate inputs + - name: Validate preparation inputs env: - VERSION: ${{ github.event.inputs.version }} - ISSUE: ${{ github.event.inputs.issue }} - TITLE: ${{ github.event.inputs.title }} - CHANGELOG: ${{ github.event.inputs.changelog }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + TITLE: ${{ inputs.title }} + CHANGELOG: ${{ inputs.changelog }} run: | - err=0 - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Version must be in semver format (e.g. 1.0.0)." - err=1 - fi - if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then - echo "::error::Issue must be a number (e.g. 123 or -1)." - err=1 - fi - if [[ ${#TITLE} -gt 1000 ]]; then - echo "::error::Title must be at most 1000 characters." - err=1 - fi - if [[ ${#CHANGELOG} -gt 50000 ]]; then - echo "::error::Changelog must be at most 50000 characters." - err=1 - fi - [[ $err -eq 0 ]] || exit 1 - - # Example: generic step to perform the version update or compilation (uncomment and adjust as needed) - # - name: Generic step - # uses: whatever/action@v2 - - - name: Commit updated package.json and dist directory + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Version must use MAJOR.MINOR.PATCH."; exit 1; } + [[ "$ISSUE" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Issue must be a positive number."; exit 1; } + [[ -n "$TITLE" && ${#TITLE} -le 1000 ]] || { echo "::error::Title must contain 1-1000 characters."; exit 1; } + [[ -n "$CHANGELOG" && ${#CHANGELOG} -le 50000 ]] || { echo "::error::Changelog must contain 1-50000 characters."; exit 1; } + # Add project-specific version, build, validation, smoke-test and generated-file steps here. + - name: Commit prepared hotfix files uses: EndBug/add-and-commit@v9 with: - add: './build/ ./package.json' + add: './' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files and bumped version to ${{ github.event.inputs.version }} (hotfix)' + message: 'gh-action: prepare hotfix ${{ inputs.version }}' - tag: - name: Publish version + promote: + name: Open production promotion + if: ${{ inputs.mode == 'prepare' }} runs-on: ubuntu-latest - timeout-minutes: 120 - needs: [ prepare-version-files ] + needs: prepare-version-files + timeout-minutes: 10 permissions: contents: read + issues: write + pull-requests: write + actions: write steps: - - name: Checkout Repository - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: persist-credentials: false - - - name: Copilot - Create Tag + - name: Create or resume promotion PR uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} + single-action: prepare_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-version: ${{ inputs.version }} + single-action-title: ${{ inputs.title }} + single-action-changelog: ${{ inputs.changelog }} main-branch: ${{ vars.MAIN_BRANCH || 'master' }} development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} release-tree: ${{ vars.RELEASE_TREE || 'release' }} hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'create_tag' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + release-workflow: ${{ vars.RELEASE_WORKFLOW || 'release_workflow.yml' }} + hotfix-workflow: ${{ vars.HOTFIX_WORKFLOW || 'hotfix_workflow.yml' }} + release-reconciliation-strategy: ${{ vars.RELEASE_RECONCILIATION_STRATEGY || 'production-lineage' }} + hotfix-reconciliation-strategy: ${{ vars.HOTFIX_RECONCILIATION_STRATEGY || 'production-lineage' }} + reconciliation-pr-mode: ${{ vars.RECONCILIATION_PR_MODE || 'auto' }} + reconciliation-backmerge-mode: ${{ vars.RECONCILIATION_BACKMERGE_MODE || 'auto' }} + hotfix-active-release-policy: ${{ vars.HOTFIX_ACTIVE_RELEASE_POLICY || 'prefer-release' }} + reconciliation-tree: ${{ vars.RECONCILIATION_TREE || 'sync' }} + reconciliation-cleanup: ${{ vars.RECONCILIATION_CLEANUP || 'all' }} + reconciliation-issue-completion: ${{ vars.RECONCILIATION_ISSUE_COMPLETION || 'close' }} + orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} + orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} + orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} token: ${{ secrets.PAT }} - - - name: Copilot - Create Release + + publish: + name: Publish accepted production commit + if: ${{ inputs.mode == 'publish' }} + runs-on: ubuntu-latest + needs: queue-gate + timeout-minutes: 30 + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ vars.MAIN_BRANCH || 'master' }} + - name: Create or verify production tag uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - main-branch: ${{ vars.MAIN_BRANCH || 'master' }} - development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} - release-tree: ${{ vars.RELEASE_TREE || 'release' }} - hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'create_release' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - single-action-title: '${{ github.event.inputs.title }}' - single-action-changelog: '${{ github.event.inputs.changelog }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_tag + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - # Example: generic step to perform the deployment (uncomment and adjust as needed) - # - name: Generic step - # uses: whatever/action@v2 - - - name: Copilot - Deploy success notification + # Add project-specific deployment here. It runs only after production promotion merged. + - name: Create or verify GitHub Release uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - main-branch: ${{ vars.MAIN_BRANCH || 'master' }} - development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} - release-tree: ${{ vars.RELEASE_TREE || 'release' }} - hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_release + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + token: ${{ secrets.PAT }} + - name: Start configured reconciliation + uses: vypdev/copilot@v3 + with: + single-action: published_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} report-failure: - name: Report deployment failure + name: Report orchestration failure runs-on: ubuntu-latest timeout-minutes: 5 - needs: [ queue-gate, prepare-version-files, tag ] - if: ${{ failure() && github.event.inputs.issue != '-1' }} + needs: [queue-gate, prepare-version-files, promote, publish] + if: ${{ failure() && inputs.issue != '-1' }} permissions: issues: write steps: - - name: Report failure on launcher issue - # This tag is published after the release changes reach master. + - name: Persist publication failure + if: ${{ inputs.mode == 'publish' }} + uses: vypdev/copilot@v3 + with: + single-action: failed_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + single-action-message: Publication failed. Review ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} before retrying. + token: ${{ secrets.PAT }} + - name: Report preparation failure + if: ${{ inputs.mode == 'prepare' }} uses: vypdev/copilot@v3 with: single-action: publish_issue_comment - single-action-issue: '${{ github.event.inputs.issue }}' + single-action-issue: ${{ inputs.issue }} single-action-message: | - ## ❌ Hotfix deployment failed + ## ❌ Hotfix orchestration needs attention - The hotfix workflow did not complete successfully. + Phase `${{ inputs.mode }}` failed. Review the control center before retrying. [Review the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - token: ${{ github.token }} + token: ${{ secrets.PAT }} diff --git a/setup/workflows/release_workflow.yml b/setup/workflows/release_workflow.yml index 22623f8bb..579b2cb07 100644 --- a/setup/workflows/release_workflow.yml +++ b/setup/workflows/release_workflow.yml @@ -3,18 +3,28 @@ name: Task - Release on: workflow_dispatch: inputs: - version: - description: 'Release version' + mode: + description: 'Internal orchestration phase' required: true - default: '1.0.0' + default: prepare + type: choice + options: [prepare, publish] + operation-id: + description: 'Internal durable operation identifier' + required: false + default: '' + version: + description: 'Release version (MAJOR.MINOR.PATCH)' + required: false + default: '' title: - description: 'Title' - required: true - default: 'New Version' + description: 'Release title' + required: false + default: '' changelog: - description: 'Changelog' - required: true - default: '- Several improvements' + description: 'Release changelog' + required: false + default: '' issue: description: 'Launcher issue' required: true @@ -39,194 +49,152 @@ jobs: token: ${{ github.token }} prepare-version-files: - name: Prepare files for release + name: Prepare and validate release + if: ${{ inputs.mode == 'prepare' }} runs-on: ubuntu-latest needs: queue-gate - timeout-minutes: 15 + timeout-minutes: 30 permissions: contents: write steps: - uses: actions/checkout@v5 with: persist-credentials: true - - - name: Set up Node.js 24 - uses: actions/setup-node@v7 - with: - node-version: '24.x' - - - name: Set up pnpm - uses: pnpm/action-setup@v5 - with: - version: 10.12.4 - standalone: false - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Validate inputs + - name: Validate preparation inputs env: - VERSION: ${{ github.event.inputs.version }} - ISSUE: ${{ github.event.inputs.issue }} - TITLE: ${{ github.event.inputs.title }} - CHANGELOG: ${{ github.event.inputs.changelog }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + TITLE: ${{ inputs.title }} + CHANGELOG: ${{ inputs.changelog }} run: | - err=0 - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Version must be in semver format (e.g. 1.0.0)." - err=1 - fi - if ! [[ "$ISSUE" =~ ^-?[0-9]+$ ]]; then - echo "::error::Issue must be a number (e.g. 123 or -1)." - err=1 - fi - if [[ ${#TITLE} -gt 1000 ]]; then - echo "::error::Title must be at most 1000 characters." - err=1 - fi - if [[ ${#CHANGELOG} -gt 50000 ]]; then - echo "::error::Changelog must be at most 50000 characters." - err=1 - fi - [[ $err -eq 0 ]] || exit 1 - - # Example: generic step to perform the version update or compilation (uncomment and adjust as needed) - # - name: Generic step - # uses: whatever/action@v2 - - - name: Commit updated package.json and dist directory + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Version must use MAJOR.MINOR.PATCH."; exit 1; } + [[ "$ISSUE" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Issue must be a positive number."; exit 1; } + [[ -n "$TITLE" && ${#TITLE} -le 1000 ]] || { echo "::error::Title must contain 1-1000 characters."; exit 1; } + [[ -n "$CHANGELOG" && ${#CHANGELOG} -le 50000 ]] || { echo "::error::Changelog must contain 1-50000 characters."; exit 1; } + # Add project-specific version, build, validation, smoke-test and generated-file steps here. + - name: Commit prepared release files uses: EndBug/add-and-commit@v9 with: - add: './build/ ./package.json' + add: './' committer_name: GitHub Actions committer_email: actions@github.com default_author: user_info - message: 'gh-action: updated compiled files and bumped version to ${{ github.event.inputs.version }} (release)' + message: 'gh-action: prepare release ${{ inputs.version }}' - tag: - name: Publish version + promote: + name: Open production promotion + if: ${{ inputs.mode == 'prepare' }} runs-on: ubuntu-latest - timeout-minutes: 120 - needs: [ prepare-version-files ] + needs: prepare-version-files + timeout-minutes: 10 permissions: contents: read + issues: write + pull-requests: write + actions: write steps: - - name: Checkout Repository - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: persist-credentials: false - - - name: Copilot - Create Tag + - name: Create or resume promotion PR uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} + single-action: prepare_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-version: ${{ inputs.version }} + single-action-title: ${{ inputs.title }} + single-action-changelog: ${{ inputs.changelog }} main-branch: ${{ vars.MAIN_BRANCH || 'master' }} development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} release-tree: ${{ vars.RELEASE_TREE || 'release' }} hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'create_tag' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + release-workflow: ${{ vars.RELEASE_WORKFLOW || 'release_workflow.yml' }} + hotfix-workflow: ${{ vars.HOTFIX_WORKFLOW || 'hotfix_workflow.yml' }} + release-reconciliation-strategy: ${{ vars.RELEASE_RECONCILIATION_STRATEGY || 'production-lineage' }} + hotfix-reconciliation-strategy: ${{ vars.HOTFIX_RECONCILIATION_STRATEGY || 'production-lineage' }} + reconciliation-pr-mode: ${{ vars.RECONCILIATION_PR_MODE || 'auto' }} + reconciliation-backmerge-mode: ${{ vars.RECONCILIATION_BACKMERGE_MODE || 'auto' }} + hotfix-active-release-policy: ${{ vars.HOTFIX_ACTIVE_RELEASE_POLICY || 'prefer-release' }} + reconciliation-tree: ${{ vars.RECONCILIATION_TREE || 'sync' }} + reconciliation-cleanup: ${{ vars.RECONCILIATION_CLEANUP || 'all' }} + reconciliation-issue-completion: ${{ vars.RECONCILIATION_ISSUE_COMPLETION || 'close' }} + orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }} + orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }} + orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }} + issues-locale: ${{ vars.ISSUES_LOCALE || 'en-US' }} + pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || 'en-US' }} token: ${{ secrets.PAT }} - - - name: Copilot - Create Release + + publish: + name: Publish accepted production commit + if: ${{ inputs.mode == 'publish' }} + runs-on: ubuntu-latest + needs: queue-gate + timeout-minutes: 30 + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + ref: ${{ vars.MAIN_BRANCH || 'master' }} + - name: Create or verify production tag uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - main-branch: ${{ vars.MAIN_BRANCH || 'master' }} - development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} - release-tree: ${{ vars.RELEASE_TREE || 'release' }} - hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'create_release' - single-action-issue: '${{ github.event.inputs.issue }}' - single-action-version: '${{ github.event.inputs.version }}' - single-action-title: '${{ github.event.inputs.title }}' - single-action-changelog: '${{ github.event.inputs.changelog }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_tag + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} - - # Example: generic step to perform the deployment (uncomment and adjust as needed) - # - name: Generic step - # uses: whatever/action@v2 - - - name: Copilot - Deploy success notification + # Add project-specific deployment here. It runs only after production promotion merged. + - name: Create or verify GitHub Release uses: vypdev/copilot@v3 - if: ${{ success() }} with: - debug: ${{ vars.DEBUG }} - main-branch: ${{ vars.MAIN_BRANCH || 'master' }} - development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }} - release-tree: ${{ vars.RELEASE_TREE || 'release' }} - hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }} - single-action: 'deployed_action' - single-action-issue: '${{ github.event.inputs.issue }}' - agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} - agent-model-provider: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} - agent-model: ${{ vars.AGENT_MODEL || 'gpt-5.6-luna' }} - agent-effort: ${{ vars.AGENT_EFFORT }} - agent-command: ${{ vars.AGENT_COMMAND }} - findings-provider: ${{ vars.FINDINGS_PROVIDER }} - findings-model-provider: ${{ vars.FINDINGS_MODEL_PROVIDER }} - findings-model: ${{ vars.FINDINGS_MODEL }} - findings-effort: ${{ vars.FINDINGS_EFFORT }} - findings-command: ${{ vars.FINDINGS_COMMAND }} - fixer-provider: ${{ vars.FIXER_PROVIDER }} - fixer-model-provider: ${{ vars.FIXER_MODEL_PROVIDER }} - fixer-model: ${{ vars.FIXER_MODEL }} - fixer-effort: ${{ vars.FIXER_EFFORT }} - fixer-command: ${{ vars.FIXER_COMMAND }} + single-action: create_release + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + token: ${{ secrets.PAT }} + - name: Start configured reconciliation + uses: vypdev/copilot@v3 + with: + single-action: published_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} token: ${{ secrets.PAT }} report-failure: - name: Report deployment failure + name: Report orchestration failure runs-on: ubuntu-latest timeout-minutes: 5 - needs: [ queue-gate, prepare-version-files, tag ] - if: ${{ failure() && github.event.inputs.issue != '-1' }} + needs: [queue-gate, prepare-version-files, promote, publish] + if: ${{ failure() && inputs.issue != '-1' }} permissions: issues: write steps: - - name: Report failure on launcher issue - # This tag is published after the release changes reach master. + - name: Persist publication failure + if: ${{ inputs.mode == 'publish' }} + uses: vypdev/copilot@v3 + with: + single-action: failed_deployment_action + single-action-issue: ${{ inputs.issue }} + single-action-operation-id: ${{ inputs.operation-id }} + single-action-version: ${{ inputs.version }} + single-action-message: Publication failed. Review ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} before retrying. + token: ${{ secrets.PAT }} + - name: Report preparation failure + if: ${{ inputs.mode == 'prepare' }} uses: vypdev/copilot@v3 with: single-action: publish_issue_comment - single-action-issue: '${{ github.event.inputs.issue }}' + single-action-issue: ${{ inputs.issue }} single-action-message: | - ## ❌ Release deployment failed + ## ❌ Release orchestration needs attention - The release workflow did not complete successfully. + Phase `${{ inputs.mode }}` failed. Review the control center before retrying. [Review the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - token: ${{ github.token }} + token: ${{ secrets.PAT }} diff --git a/specs/configurable-release-orchestration-traceability.md b/specs/configurable-release-orchestration-traceability.md new file mode 100644 index 000000000..43ac94b1d --- /dev/null +++ b/specs/configurable-release-orchestration-traceability.md @@ -0,0 +1,90 @@ +# Configurable release orchestration traceability + +This matrix accompanies +[`configurable-release-orchestration.md`](./configurable-release-orchestration.md). +Paths are repository-relative. Automated checks are deterministic and do not +call live providers. AC-40 and the human portion of AC-46 remain evidence gates +for the implementation pull request because source code cannot establish visual +readability in GitHub's desktop/mobile and light/dark renderers. + +## Test-budget ledger + +The feature floor is allocated without double-counting cases: + +| Area | Required | Assigned evidence | +|---|---:|---| +| Domain/configuration/planning | 18 | First 18 distinct rows in `deployment_configuration.test.ts`; additional planning cases are surplus. | +| State/idempotent orchestration | 18 | First 18 cases in `deployment_orchestration_use_case.test.ts`; domain transition cases are surplus. | +| GitHub/repository adapters | 12 | First 12 cases in `github_deployment_repository.test.ts`; state, legacy, release, and tag adapters are surplus. | +| Workflow/setup contracts | 8 | Gate-first DAG, continuation, merge-group, operation identity, OIDC, PAT, polling, and failure-projection cases in `validate_workflow_contract.test.ts`. | +| UI/localization/sanitization | 10 | First 10 cases in `deployment_presentation_policy.test.ts`; lifecycle and Job Summary cases are surplus. | +| Integration/replay/security | 6 | Concurrent state, forged marker, cross-repository event, duplicate event, cancellation recovery, and cleanup replay cases in `deployment_orchestration_use_case.test.ts`. | +| **Total assigned** | **72** | The implementation adds substantially more cases than the non-overlapping floor. | + +Repository-wide coverage thresholds remain in `jest.config.js`; architecture, +workflow, documentation, type, lint, package, build, and Graphify checks are +separate gates. + +## Acceptance criteria + +| AC | Implementation | Automated evidence | User/operator documentation | Status | +|---:|---|---|---|---| +| 1 | `prepare_release_branch.ts`, schema-v3 origin fields | linked-branch, plan, orchestration tests | `/issues/deployment-orchestration` | Automated | +| 2 | Immutable `sourceSha`; promotion verifies the exact prepared head | plan and orchestration stale-head tests | `/issues/deployment-orchestration` | Automated | +| 3 | Release preparation jobs commit version/build output before promotion | workflow contract and release repository tests | `/issues/type/release` | Automated | +| 4 | Prepare/publish workflow modes and guarded publication actions | workflow and continuation-guard tests | `/issues/deployment-orchestration` | Automated | +| 5 | `createOrVerifyTagAtSha(productionSha)` | tag repository and orchestration tests | `/development/release-process` | Automated | +| 6 | Event-driven PR completion; no polling outside `legacy-wait` | workflow and use-case tests | `/issues/deployment-orchestration` | Automated | +| 7 | Deterministic sync branch created from target SHA | plan, adapter, and use-case strict-target tests | `/issues/deployment-orchestration` | Automated | +| 8 | `selectBackmergeMode` and server-side sync merge | plan and orchestration tests | `/issues/deployment-orchestration` | Automated | +| 9 | Direct mode for compatible targets | plan tests | `/issues/deployment-orchestration` | Automated | +| 10 | Managed merged-PR wake-up and publication dispatch | orchestration and workflow tests | `/issues/deployment-orchestration` | Automated | +| 11 | Closed-unmerged promotion blocks before publication | orchestration tests | recovery decision tree | Automated | +| 12 | Closed-unmerged reconciliation blocks; cleanup is deferred | orchestration replay tests | recovery decision tree | Automated | +| 13 | Phase-aware duplicate event no-ops | state and orchestration tests | recovery decision tree | Automated | +| 14 | Retryable publication block and immutable tag reuse | continuation-guard, tag, and workflow tests | recovery decision tree | Automated | +| 15 | Registry detection skips an exact visible version and verifies its `gitHead` against production | parsed workflow contract tests | `/development/release-process` | Automated | +| 16 | Conflicting immutable tag throws and is never moved | tag repository tests | recovery decision tree | Automated | +| 17 | GitHub Release create-or-reuse on `422` | release repository tests | `/issues/deployment-orchestration` | Automated | +| 18 | Pure label projection is idempotent | lifecycle policy and orchestration tests | `/issues/deployment-orchestration` | Automated | +| 19 | Operation snapshots branch roles and strategies | plan/state tests | configuration reference | Automated | +| 20 | `reconciliationSource` selects frozen source head | plan tests | configuration reference | Automated | +| 21 | `reconciliationSource` selects accepted production SHA | plan tests | configuration reference | Automated | +| 22 | Hotfix target-selection policy: prefer/development/both | plan and orchestration tests | hotfix lifecycle | Automated | +| 23 | Multiple active releases block `prefer-release` | plan and orchestration tests | hotfix lifecycle | Automated | +| 24 | Manual strategy creates no target and keeps issue open | configuration, plan, and orchestration tests | configuration reference | Automated | +| 25 | Cleanup/issue-completion enums and retryable cleanup | configuration, lifecycle, and replay tests | configuration reference | Automated | +| 26 | Dedicated PAT-backed completion workflow bypasses generic bot gate only for marked PRs | workflow contract tests | repository setup | Automated | +| 27 | Operation/version guard, marker, repository, branches, and SHA verification | guard, marker, adapter, and use-case security tests | trust/recovery sections | Automated | +| 28 | Boundary and setup validation for enums/invalid combinations | configuration and setup-policy tests | configuration reference | Automated | +| 29 | `environment: npm`; job-local `id-token: write`; no npm token | workflow contract tests | npm trusted publishing section | Automated | +| 30 | Provider-neutral app/domain; Octokit DTOs remain infrastructure-only | architecture boundary tests | `/development/architecture` | Automated | +| 31 | Stable operation-scoped dashboard find/update and duplicate detection | presentation repository, renderer, and replay tests | What maintainers see | Automated | +| 32 | Guided/compact/quiet policies retain actionable states | presentation tests | What maintainers see | Automated | +| 33 | Fixed-label Mermaid plus textual fallback; untrusted values excluded | presentation tests | What maintainers see | Automated | +| 34 | Typed `en-US`/`es-ES` catalog and safe fallback | presentation tests | What maintainers see | Automated | +| 35 | Deterministic ordered promotion/reconciliation PR renderers | presentation tests | release/hotfix lifecycle | Automated | +| 36 | Blocked fact table and already-public reconciliation notice | presentation tests | recovery decision tree | Automated | +| 37 | Descriptive issue, PR, compare, commit, branch, tag, release, npm, major-tag, and run links | presentation link tests | What maintainers see | Automated | +| 38 | Publication verification replaces `deploy` with `deployed`; lifecycle continues as reviewing | lifecycle and orchestration tests | What maintainers see | Automated | +| 39 | One dashboard; stable milestone markers; maximum four named milestones in policy | presentation repository and orchestration tests | What maintainers see | Automated | +| 40 | GitHub desktop/mobile, light/dark, Mermaid/fallback review | Renderer tests provide deterministic fixtures; real GitHub screenshots belong in the implementation PR | UX acceptance section of SDD | **Manual PR gate** | +| 41 | Persisted messages and rendered values sanitize mentions, commands, HTML/markers, headings, and Mermaid inputs | domain and presentation security tests | trust section | Automated | +| 42 | Dedicated Job Summary distinguishes external wait from workflow failure | presentation and Action completion tests | What maintainers see | Automated | +| 43 | Route registered; Action input names verified; workflow examples parsed | documentation and workflow validators | all linked pages | Automated | +| 44 | Narrow context, provider-neutral ports, mutation-free presentation, acyclic graph | architecture and dependency-cycle tests | `/development/architecture` | Automated | +| 45 | Non-overlapping 72-case ledger plus repository coverage gate | this ledger and Jest coverage | `/development/testing` | Automated | +| 46 | Ten-second comprehension: kind, phase, publication, transition, and action | semantic renderer assertions; final comprehension judgment belongs in PR review | What maintainers see | **Automated semantics + manual PR gate** | + +## Implementation surfaces + +- Domain: `src/domain/deployment_configuration.ts`, + `src/domain/deployment_operation.ts`, and `src/domain/managed_pull_request.ts`. +- Application: deployment planning, continuation guard, lifecycle, presentation, + orchestration use case, and semantic ports under `src/application/`. +- Adapters: `src/data/repository/deployment/`, release/tag repositories, and + `src/infrastructure/github/octokit_deployment_adapter.ts`. +- Entrypoints: `action.yml`, execution builders, single-action routing, active + workflows, and setup workflow templates. +- UX: the issue control center, managed PR bodies, bounded milestone comments, + and the deployment-specific Job Summary. diff --git a/src/actions/__tests__/deployment_configuration_builder.test.ts b/src/actions/__tests__/deployment_configuration_builder.test.ts new file mode 100644 index 000000000..43b239015 --- /dev/null +++ b/src/actions/__tests__/deployment_configuration_builder.test.ts @@ -0,0 +1,24 @@ +import { INPUT_KEYS } from "../../application/contracts/input_keys"; +import { readDeploymentConfiguration } from "../deployment_configuration_builder"; + +const branches = { + productionBranch: "master", + developmentBranch: "develop", + releaseTree: "release", + hotfixTree: "hotfix", +}; + +describe("deployment configuration builder", () => { + it("applies safe defaults when optional Action inputs are omitted", () => { + expect(readDeploymentConfiguration(() => undefined, branches)).toEqual(expect.objectContaining({ + releaseReconciliationStrategy: "production-lineage", + reconciliationPullRequestMode: "auto", + reconciliationTree: "sync", + })); + }); + + it("rejects unknown strategy values instead of silently normalizing them", () => { + const read = (key: string) => key === INPUT_KEYS.RELEASE_RECONCILIATION_STRATEGY ? "PRODUCTION-LINEAGE" : undefined; + expect(() => readDeploymentConfiguration(read, branches)).toThrow("release-reconciliation-strategy must be one of"); + }); +}); diff --git a/src/actions/deployment_configuration_builder.ts b/src/actions/deployment_configuration_builder.ts new file mode 100644 index 000000000..e2f9a4be4 --- /dev/null +++ b/src/actions/deployment_configuration_builder.ts @@ -0,0 +1,110 @@ +import { ApplicationError } from "../application/errors/application_error"; +import { + DEFAULT_DEPLOYMENT_CONFIGURATION, + HOTFIX_ACTIVE_RELEASE_POLICIES, + ORCHESTRATION_COMMENT_MODES, + ORCHESTRATION_PRESENTATION_MODES, + RECONCILIATION_BACKMERGE_MODES, + RECONCILIATION_CLEANUP_MODES, + RECONCILIATION_ISSUE_COMPLETION_MODES, + RECONCILIATION_PR_MODES, + RECONCILIATION_STRATEGIES, + parseDeploymentEnum, + validateDeploymentConfiguration, + type DeploymentConfigurationValues, +} from "../domain/deployment_configuration"; +import { INPUT_KEYS } from "../application/contracts/input_keys"; + +export interface DeploymentBranchInputContext { + readonly productionBranch: string; + readonly developmentBranch: string; + readonly releaseTree: string; + readonly hotfixTree: string; +} + +export function readDeploymentConfiguration( + getInput: (key: string) => unknown, + branches: DeploymentBranchInputContext, +): DeploymentConfigurationValues { + const errors: string[] = []; + const readEnum = (key: string, allowed: readonly T[], fallback: T): T => { + const parsed = parseDeploymentEnum(getInput(key), allowed, fallback); + if (!parsed.valid) errors.push(`${key} must be one of: ${allowed.join(", ")}.`); + return parsed.value; + }; + const configuration: DeploymentConfigurationValues = { + releaseReconciliationStrategy: readEnum( + INPUT_KEYS.RELEASE_RECONCILIATION_STRATEGY, + RECONCILIATION_STRATEGIES, + DEFAULT_DEPLOYMENT_CONFIGURATION.releaseReconciliationStrategy, + ), + hotfixReconciliationStrategy: readEnum( + INPUT_KEYS.HOTFIX_RECONCILIATION_STRATEGY, + RECONCILIATION_STRATEGIES, + DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixReconciliationStrategy, + ), + reconciliationPullRequestMode: readEnum( + INPUT_KEYS.RECONCILIATION_PR_MODE, + RECONCILIATION_PR_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationPullRequestMode, + ), + reconciliationBackmergeMode: readEnum( + INPUT_KEYS.RECONCILIATION_BACKMERGE_MODE, + RECONCILIATION_BACKMERGE_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationBackmergeMode, + ), + hotfixActiveReleasePolicy: readEnum( + INPUT_KEYS.HOTFIX_ACTIVE_RELEASE_POLICY, + HOTFIX_ACTIVE_RELEASE_POLICIES, + DEFAULT_DEPLOYMENT_CONFIGURATION.hotfixActiveReleasePolicy, + ), + reconciliationTree: String(getInput(INPUT_KEYS.RECONCILIATION_TREE) + ?? DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree).trim() + || DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationTree, + reconciliationCleanup: readEnum( + INPUT_KEYS.RECONCILIATION_CLEANUP, + RECONCILIATION_CLEANUP_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationCleanup, + ), + reconciliationIssueCompletion: readEnum( + INPUT_KEYS.RECONCILIATION_ISSUE_COMPLETION, + RECONCILIATION_ISSUE_COMPLETION_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.reconciliationIssueCompletion, + ), + orchestrationPresentationMode: readEnum( + INPUT_KEYS.ORCHESTRATION_PRESENTATION_MODE, + ORCHESTRATION_PRESENTATION_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationPresentationMode, + ), + orchestrationDiagrams: readBoolean( + getInput(INPUT_KEYS.ORCHESTRATION_DIAGRAMS), + DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationDiagrams, + INPUT_KEYS.ORCHESTRATION_DIAGRAMS, + errors, + ), + orchestrationCommentMode: readEnum( + INPUT_KEYS.ORCHESTRATION_COMMENT_MODE, + ORCHESTRATION_COMMENT_MODES, + DEFAULT_DEPLOYMENT_CONFIGURATION.orchestrationCommentMode, + ), + }; + errors.push(...validateDeploymentConfiguration(configuration, { + productionBranch: branches.productionBranch || "master", + developmentBranch: branches.developmentBranch || "develop", + releaseTree: branches.releaseTree || "release", + hotfixTree: branches.hotfixTree || "hotfix", + })); + if (errors.length > 0) { + throw new ApplicationError(`Invalid deployment configuration: ${errors.join(" ")}`, "validation"); + } + return configuration; +} + +function readBoolean(value: unknown, fallback: boolean, name: string, errors: string[]): boolean { + if (value === undefined || value === null || String(value).trim() === "") return fallback; + const normalized = String(value).trim().toLowerCase(); + if (normalized === "true") return true; + if (normalized === "false") return false; + errors.push(`${name} must be true or false.`); + return fallback; +} diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index c18b14b46..c4ea6212c 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -14,6 +14,7 @@ import type { CopilotEvidencePort } from '../application/ports/copilot_evidence_ import { buildCopilotEvidence } from '../application/policies/copilot_evidence_policy'; import type { ActionSummaryPort } from '../application/ports/action_summary_ports'; import { shouldPersistConfiguration } from '../application/policies/configuration_persistence_policy'; +import { renderDeploymentJobSummary } from '../application/policies/deployment_presentation_policy'; export async function finishGithubAction( execution: Execution, @@ -30,10 +31,12 @@ export async function finishGithubAction( execution.currentConfiguration.results = results; core.setOutput('bugbot-telemetry', JSON.stringify(extractBugbotTelemetry(results))); const dryRun = results.some((result) => getResultPayload(result.payload)?.dryRun === true); - if (!dryRun && !execution.singleAction.isPublishIssueCommentAction) { + const ownsDeploymentPresentation = execution.singleAction.isDeploymentOrchestrationAction + || (execution.singleAction.isDeployedAction && Boolean(execution.currentConfiguration.deploymentOrchestration)); + if (!dryRun && !execution.singleAction.isPublishIssueCommentAction && !ownsDeploymentPresentation) { await new PublishResultUseCase(issueNotificationPort, createLogReportAdapter()).invoke(execution); - } else if (execution.singleAction.isPublishIssueCommentAction) { - logInfo('Result publication skipped: the issue-comment single action publishes its own content.'); + } else if (execution.singleAction.isPublishIssueCommentAction || ownsDeploymentPresentation) { + logInfo('Generic result publication skipped: this single action owns its user-facing presentation.'); } else { logInfo('Bugbot dry-run: result publication and repository configuration persistence are disabled.'); } @@ -58,7 +61,20 @@ function extractBugbotTelemetry(results: readonly Result[]): unknown[] { } async function writeActionSummary(execution: Execution, summaryPort?: ActionSummaryPort): Promise { - const summaryText = buildActionSummary({ + const operation = execution.currentConfiguration.deploymentOrchestration; + const summaryText = (execution.singleAction.isDeploymentOrchestrationAction || execution.singleAction.isDeployedAction) && operation + ? renderDeploymentJobSummary(operation, { + owner: execution.owner, + repository: execution.repo, + issue: execution.singleAction.issue, + issueLocale: execution.locale.issue, + pullRequestLocale: execution.locale.pullRequest, + packageName: execution.owner === 'vypdev' && execution.repo === 'copilot' ? '@vypdev/copilot' : undefined, + workflowRunUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID + ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + : undefined, + }, operation.lastFailure?.previousPhase, execution.currentConfiguration.results.flatMap((result) => result.steps ?? [])) + : buildActionSummary({ owner: execution.owner, repository: execution.repo, eventName: execution.eventName, @@ -73,7 +89,7 @@ async function writeActionSummary(execution: Execution, summaryPort?: ActionSumm pullRequestDescriptionMode: execution.ai?.getPullRequestDescriptionMode?.(), failOnUnresolvedFindings: execution.ai?.getBugbotReviewConfiguration?.().failOnUnresolved === true, results: execution.currentConfiguration.results, - }); + }); if (!summaryPort) return summaryText; try { await summaryPort.publish(summaryText); diff --git a/src/actions/github_action_execution.ts b/src/actions/github_action_execution.ts index baeff3178..24a48da7a 100644 --- a/src/actions/github_action_execution.ts +++ b/src/actions/github_action_execution.ts @@ -28,6 +28,7 @@ import type { buildGithubActionEventInputs } from './github_event_inputs'; import { DEFAULT_INACTIVITY_THRESHOLD_HOURS, MAX_INACTIVITY_THRESHOLD_HOURS } from '../domain/issue_inactivity'; import { activeAgentTasks } from '../application/policies/agent_task_activation_policy'; import type { AgentTaskConfiguration } from '../domain/agent'; +import { readDeploymentConfiguration } from './deployment_configuration_builder'; export interface GithubActionExecutionInput { readonly getInput: typeof getGithubActionInput; @@ -77,6 +78,12 @@ export async function buildGithubActionExecution( const localeInputs = readGithubActionLocaleInputs(getInput); const sizeThresholdInputs = readGithubActionThresholdInputs(getInput); const branchInputs = readGithubActionBranchInputs(getInput); + const deployment = readDeploymentConfiguration(getInput, { + productionBranch: branchInputs.defaultBranch, + developmentBranch: branchInputs.development, + releaseTree: branchInputs.releaseTree, + hotfixTree: branchInputs.hotfixTree, + }); return buildExecution({ debug, @@ -127,6 +134,7 @@ export async function buildGithubActionExecution( release: new Release(), hotfix: new Hotfix(), workflows: buildWorkflows(workflowInputs.release, workflowInputs.hotfix), + deployment, projects: buildProjects(projectInputs), tokenUser: input.tokenUser, inputs: eventInputs, @@ -153,6 +161,7 @@ export function readGithubActionSingleAction(getInput: typeof getGithubActionInp getInput(INPUT_KEYS.SINGLE_ACTION_MESSAGE), getInput(INPUT_KEYS.SINGLE_ACTION_COMMENT_ID), getInput(INPUT_KEYS.SINGLE_ACTION_COMMENT_MODE), + getInput(INPUT_KEYS.SINGLE_ACTION_OPERATION_ID), ); } diff --git a/src/actions/local_action_configuration_sections.ts b/src/actions/local_action_configuration_sections.ts index f48566ebb..25ca7b342 100644 --- a/src/actions/local_action_configuration_sections.ts +++ b/src/actions/local_action_configuration_sections.ts @@ -14,6 +14,7 @@ import { buildImageConfiguration } from './image_configuration_builder'; import { normalizePullRequestDescriptionMode } from '../domain/pull_request_description'; import { DEFAULT_INACTIVITY_THRESHOLD_HOURS, MAX_INACTIVITY_THRESHOLD_HOURS } from '../domain/issue_inactivity'; import { normalizeBugbotReviewEffort, parseBugbotOrganizationRules } from '../domain/bugbot/review_configuration'; +import { readDeploymentConfiguration } from './deployment_configuration_builder'; export type LocalActionInputs = ReturnType; @@ -36,6 +37,7 @@ export function readLocalCoreConfiguration( singleActionTitle: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_TITLE), singleActionChangelog: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_CHANGELOG), singleActionMessage: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_MESSAGE), + singleActionOperationId: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_OPERATION_ID), singleActionCommentId: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_COMMENT_ID), singleActionCommentMode: input(additionalParams, actionInputs, INPUT_KEYS.SINGLE_ACTION_COMMENT_MODE), inactivityThresholdHours: parseBoundedPositiveIntegerInput( @@ -257,6 +259,10 @@ export function readLocalWorkflowConfiguration( actionInputs: LocalActionInputs, ) { const read = (key: string) => input(additionalParams, actionInputs, key); + const mainBranch = read(INPUT_KEYS.MAIN_BRANCH); + const developmentBranch = read(INPUT_KEYS.DEVELOPMENT_BRANCH); + const releaseTree = read(INPUT_KEYS.RELEASE_TREE); + const hotfixTree = read(INPUT_KEYS.HOTFIX_TREE); return { imageConfiguration: buildImageConfiguration((key) => additionalParams[key] ?? actionInputs[key]), releaseWorkflow: read(INPUT_KEYS.RELEASE_WORKFLOW), @@ -266,12 +272,12 @@ export function readLocalWorkflowConfiguration( issueLocale: read(INPUT_KEYS.ISSUES_LOCALE) ?? Locale.DEFAULT, pullRequestLocale: read(INPUT_KEYS.PULL_REQUESTS_LOCALE) ?? Locale.DEFAULT, ...readThresholds(additionalParams, actionInputs), - mainBranch: read(INPUT_KEYS.MAIN_BRANCH), - developmentBranch: read(INPUT_KEYS.DEVELOPMENT_BRANCH), + mainBranch, + developmentBranch, featureTree: read(INPUT_KEYS.FEATURE_TREE), bugfixTree: read(INPUT_KEYS.BUGFIX_TREE), - hotfixTree: read(INPUT_KEYS.HOTFIX_TREE), - releaseTree: read(INPUT_KEYS.RELEASE_TREE), + hotfixTree, + releaseTree, docsTree: read(INPUT_KEYS.DOCS_TREE), choreTree: read(INPUT_KEYS.CHORE_TREE), commitPrefixBuilder: read(INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', @@ -281,5 +287,11 @@ export function readLocalWorkflowConfiguration( pullRequestDesiredAssigneesCount: parseIntegerInput(read(INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), pullRequestDesiredReviewersCount: parseIntegerInput(read(INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), pullRequestMergeTimeout: parseNonNegativeIntegerInput(read(INPUT_KEYS.PULL_REQUEST_MERGE_TIMEOUT), 0), + deployment: readDeploymentConfiguration(read, { + productionBranch: mainBranch, + developmentBranch, + releaseTree, + hotfixTree, + }), }; } diff --git a/src/actions/local_action_execution.ts b/src/actions/local_action_execution.ts index a29257b74..ea86112d4 100644 --- a/src/actions/local_action_execution.ts +++ b/src/actions/local_action_execution.ts @@ -15,7 +15,7 @@ export function buildLocalActionExecution( ) { const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, - singleActionMessage, singleActionCommentId, singleActionCommentMode, + singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId, inactivityThresholdHours, commitPrefixBuilder, branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, pullRequestMergeTimeout, @@ -39,6 +39,7 @@ export function buildLocalActionExecution( featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, + deployment, } = configuration; return buildExecution({ debug, @@ -52,6 +53,7 @@ export function buildLocalActionExecution( singleActionMessage, singleActionCommentId, singleActionCommentMode, + singleActionOperationId, ), commitPrefixBuilder, issue: buildIssue(branchManagementAlways, reopenIssueOnPush, issueDesiredAssigneesCount, additionalParams), @@ -121,6 +123,7 @@ export function buildLocalActionExecution( release: new Release(), hotfix: new Hotfix(), workflows: buildWorkflows(releaseWorkflow, hotfixWorkflow), + deployment, projects: buildProjects({ projects, issueCreated: projectColumnIssueCreated, diff --git a/src/application/__tests__/architecture_boundaries.test.ts b/src/application/__tests__/architecture_boundaries.test.ts index f8c8a9cf4..423ba1b81 100644 --- a/src/application/__tests__/architecture_boundaries.test.ts +++ b/src/application/__tests__/architecture_boundaries.test.ts @@ -156,6 +156,28 @@ describe('application architecture boundaries', () => { expect(source).not.toContain("data/model/execution'"); expect(source).toContain('LifecycleSynchronizationExecution'); }); + + it('keeps deployment orchestration dependent on its narrow application context', () => { + const source = readFileSync( + join(applicationRoot, 'usecases/actions/deployment_orchestration_use_case.ts'), + 'utf8', + ); + expect(source).not.toContain('data/model/execution'); + expect(source).toContain('DeploymentOrchestrationContext'); + expect(source).toContain('DeploymentStateStorePort'); + }); + + it('keeps deployment application ports provider-neutral', () => { + const source = readFileSync(join(applicationRoot, 'ports/deployment_orchestration_ports.ts'), 'utf8'); + expect(source).not.toMatch(/@octokit|@actions|infrastructure\/|GithubDeployment/); + expect(source).not.toMatch(/graphql|pull_number|merge_commit_sha|node_id/); + }); + + it('keeps deployment presentation policies mutation-free', () => { + const source = readFileSync(join(applicationRoot, 'policies/deployment_presentation_policy.ts'), 'utf8'); + expect(source).not.toMatch(/ports\//); + expect(source).not.toMatch(/updateDescription|addComment|createManagedPullRequest|deleteBranch|dispatch/); + }); }); describe('failure policy ownership', () => { diff --git a/src/application/contracts/input_keys.ts b/src/application/contracts/input_keys.ts index 91dd0edac..a28371d06 100644 --- a/src/application/contracts/input_keys.ts +++ b/src/application/contracts/input_keys.ts @@ -14,6 +14,7 @@ export const INPUT_KEYS = { SINGLE_ACTION_TITLE: 'single-action-title', SINGLE_ACTION_CHANGELOG: 'single-action-changelog', SINGLE_ACTION_MESSAGE: 'single-action-message', + SINGLE_ACTION_OPERATION_ID: 'single-action-operation-id', SINGLE_ACTION_COMMENT_ID: 'single-action-comment-id', SINGLE_ACTION_COMMENT_MODE: 'single-action-comment-mode', INACTIVITY_THRESHOLD_HOURS: 'inactivity-threshold-hours', @@ -111,6 +112,17 @@ export const INPUT_KEYS = { // Workflows RELEASE_WORKFLOW: 'release-workflow', HOTFIX_WORKFLOW: 'hotfix-workflow', + RELEASE_RECONCILIATION_STRATEGY: 'release-reconciliation-strategy', + HOTFIX_RECONCILIATION_STRATEGY: 'hotfix-reconciliation-strategy', + RECONCILIATION_PR_MODE: 'reconciliation-pr-mode', + RECONCILIATION_BACKMERGE_MODE: 'reconciliation-backmerge-mode', + HOTFIX_ACTIVE_RELEASE_POLICY: 'hotfix-active-release-policy', + RECONCILIATION_TREE: 'reconciliation-tree', + RECONCILIATION_CLEANUP: 'reconciliation-cleanup', + RECONCILIATION_ISSUE_COMPLETION: 'reconciliation-issue-completion', + ORCHESTRATION_PRESENTATION_MODE: 'orchestration-presentation-mode', + ORCHESTRATION_DIAGRAMS: 'orchestration-diagrams', + ORCHESTRATION_COMMENT_MODE: 'orchestration-comment-mode', // Emoji EMOJI_LABELED_TITLE: 'emoji-labeled-title', diff --git a/src/application/policies/__tests__/configuration_persistence_policy.test.ts b/src/application/policies/__tests__/configuration_persistence_policy.test.ts index b3c9c0538..f629e81a9 100644 --- a/src/application/policies/__tests__/configuration_persistence_policy.test.ts +++ b/src/application/policies/__tests__/configuration_persistence_policy.test.ts @@ -27,6 +27,10 @@ describe('shouldPersistConfiguration', () => { ACTIONS.DETECT_POTENTIAL_PROBLEMS, ACTIONS.INITIAL_SETUP, ACTIONS.PUBLISH_ISSUE_COMMENT, + ACTIONS.PREPARE_DEPLOYMENT, + ACTIONS.CONTINUE_DEPLOYMENT, + ACTIONS.PUBLISHED_DEPLOYMENT, + ACTIONS.FAILED_DEPLOYMENT, ])('does not persist configuration for the %s single action', (actionName) => { expect(shouldPersistConfiguration({ isSingleAction: true, diff --git a/src/application/policies/__tests__/deployment_continuation_guard.test.ts b/src/application/policies/__tests__/deployment_continuation_guard.test.ts new file mode 100644 index 000000000..47ed69731 --- /dev/null +++ b/src/application/policies/__tests__/deployment_continuation_guard.test.ts @@ -0,0 +1,50 @@ +import { validateDeploymentContinuation } from "../deployment_continuation_guard"; +import type { DeploymentOperationSnapshot } from "../../../domain/deployment_operation"; + +const operation = { + operationId: "operation-12345678", + version: "3.4.0", + phase: "publishing", +} as DeploymentOperationSnapshot; + +describe("validateDeploymentContinuation", () => { + it("keeps legacy publication actions compatible when no operation exists", () => { + expect(validateDeploymentContinuation(undefined, "", ["publishing"], "")).toBeUndefined(); + }); + + it("accepts the matching operation only in an allowed phase", () => { + expect(validateDeploymentContinuation(operation, operation.operationId, ["publishing"], "3.4.0")).toBeUndefined(); + }); + + it.each([ + ["", "is required"], + ["forged-operation", "mismatch"], + ])("rejects an untrusted operation identity %#", (operationId, message) => { + expect(validateDeploymentContinuation(operation, operationId, ["publishing"], "3.4.0")).toContain(message); + }); + + it("rejects a matching but out-of-order continuation", () => { + expect(validateDeploymentContinuation({ ...operation, phase: "promotion_pr_pending" }, operation.operationId, ["publishing"], "3.4.0")) + .toContain("cannot continue publication"); + }); + + it("allows a retryable publication block to resume through idempotent publication steps", () => { + expect(validateDeploymentContinuation({ + ...operation, + phase: "blocked", + lastFailure: { category: "publication", message: "registry timeout", retryable: true, previousPhase: "publishing" }, + }, operation.operationId, ["publishing"], "3.4.0")).toBeUndefined(); + }); + + it("does not allow a non-retryable block through publication guards", () => { + expect(validateDeploymentContinuation({ + ...operation, + phase: "blocked", + lastFailure: { category: "publication", message: "tag conflict", retryable: false, previousPhase: "publishing" }, + }, operation.operationId, ["publishing"], "3.4.0")).toContain("cannot continue publication"); + }); + + it.each([["", "is required"], ["9.9.9", "version mismatch"]])("rejects an untrusted publication version %#", (version, message) => { + expect(validateDeploymentContinuation(operation, operation.operationId, ["publishing"], version)).toContain(message); + }); +}); diff --git a/src/application/policies/__tests__/deployment_lifecycle_policy.test.ts b/src/application/policies/__tests__/deployment_lifecycle_policy.test.ts new file mode 100644 index 000000000..ef6234310 --- /dev/null +++ b/src/application/policies/__tests__/deployment_lifecycle_policy.test.ts @@ -0,0 +1,44 @@ +import { DEFAULT_COPILOT_LIFECYCLE_LABELS } from "../../../domain/copilot_lifecycle"; +import type { DeploymentOperationSnapshot } from "../../../domain/deployment_operation"; +import { projectDeploymentLabels } from "../deployment_lifecycle_policy"; + +const labels = { deploy: "deploy", deployed: "deployed", lifecycle: DEFAULT_COPILOT_LIFECYCLE_LABELS }; +const operation = (phase: DeploymentOperationSnapshot["phase"], overrides: Partial = {}) => ({ + phase, + prMode: "auto", + publicationVerified: false, + ...overrides, +}) as DeploymentOperationSnapshot; + +describe("projectDeploymentLabels", () => { + it.each([ + ["preparing", "state:in-progress"], + ["publishing", "state:in-progress"], + ["promotion_pr_pending", "state:reviewing"], + ["reconciliation_pending", "state:reviewing"], + ["completed", "state:verified"], + ] as const)("projects %s to %s", (phase, expected) => { + expect(projectDeploymentLabels(["release", "state:planned"], operation(phase), labels)) + .toEqual(["release", expected]); + }); + + it("projects a maintainer-owned PR as ready and awaiting a maintainer", () => { + expect(projectDeploymentLabels([], operation("promotion_pr_pending", { selectedPrMode: "create-only" }), labels)) + .toEqual(["state:ready", "state:awaiting-maintainer"]); + }); + + it("projects blocked state without retaining an obsolete lifecycle label", () => { + expect(projectDeploymentLabels(["state:reviewing"], operation("blocked"), labels)) + .toEqual(["state:blocked", "state:awaiting-maintainer"]); + }); + + it("replaces deploy with deployed immediately after publication verification", () => { + expect(projectDeploymentLabels(["release", "deploy"], operation("reconciliation_pending", { publicationVerified: true }), labels)) + .toEqual(["release", "deployed", "state:reviewing"]); + }); + + it("is idempotent for an already projected label set", () => { + const projected = projectDeploymentLabels(["release", "deployed", "state:verified"], operation("completed", { publicationVerified: true }), labels); + expect(projectDeploymentLabels(projected, operation("completed", { publicationVerified: true }), labels)).toEqual(projected); + }); +}); diff --git a/src/application/policies/__tests__/deployment_plan_policy.test.ts b/src/application/policies/__tests__/deployment_plan_policy.test.ts new file mode 100644 index 000000000..34c97e4c6 --- /dev/null +++ b/src/application/policies/__tests__/deployment_plan_policy.test.ts @@ -0,0 +1,186 @@ +import { DEFAULT_DEPLOYMENT_CONFIGURATION } from "../../../domain/deployment_configuration"; +import type { DeploymentOperationSnapshot } from "../../../domain/deployment_operation"; +import { + buildInitialDeploymentOperation, + buildReconciliationBranchName, + buildReconciliationTarget, + reconciliationSource, + selectBackmergeMode, + selectPullRequestMode, + selectReconciliationTargetBranches, + validateInitialDeploymentInput, +} from "../deployment_plan_policy"; + +const sha = (letter: string) => letter.repeat(40); +const operation = (overrides: Partial = {}): DeploymentOperationSnapshot => ({ + operationId: "operation-12345678", + kind: "release", + version: "3.4.0", + title: "Release", + changelog: "Changes", + phase: "published", + strategy: "production-lineage", + prMode: "auto", + backmergeMode: "auto", + hotfixActiveReleasePolicy: "prefer-release", + cleanup: "all", + issueCompletion: "close", + presentationMode: "guided", + diagrams: true, + commentMode: "update", + sourceBranch: "release/3.4.0", + sourceSha: sha("a"), + originBranch: "develop", + originSha: sha("b"), + productionBranch: "master", + developmentBranch: "develop", + reconciliationTree: "sync", + tag: "v3.4.0", + publicationWorkflow: "release_workflow.yml", + productionSha: sha("c"), + publicationVerified: true, + reconciliationTargets: [], + lastFailure: null, + ...overrides, +}); + +describe("deployment plan policy", () => { + it("snapshots a release plan from the development cut", () => { + const value = buildInitialDeploymentOperation({ + operationId: "operation-12345678", + kind: "release", + version: "3.4.0", + title: "Release", + changelog: "Changes", + sourceBranch: "release/3.4.0", + sourceSha: sha("a"), + originBranch: "develop", + originSha: sha("b"), + productionBranch: "master", + developmentBranch: "develop", + configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, + publicationWorkflow: "release_workflow.yml", + }); + expect(value).toEqual(expect.objectContaining({ phase: "preparing", originBranch: "develop", originSha: sha("b"), tag: "v3.4.0" })); + }); + + it("selects the hotfix strategy independently", () => { + const value = buildInitialDeploymentOperation({ + operationId: "operation-12345678", kind: "hotfix", version: "3.4.1", title: "Hotfix", changelog: "Fix", + sourceBranch: "hotfix/3.4.1", sourceSha: sha("a"), originBranch: "v3.4.0", originSha: sha("b"), + productionBranch: "master", developmentBranch: "develop", publicationWorkflow: "hotfix_workflow.yml", + configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION, hotfixReconciliationStrategy: "canonical-gitflow" }, + }); + expect(value.strategy).toBe("canonical-gitflow"); + }); + + it("selects merge queue when required by the target", () => { + expect(selectPullRequestMode("auto", { autoMergeAllowed: true, mergeQueueRequired: true, immediatelyMergeable: false, requiresStrictStatusChecks: true })) + .toEqual(expect.objectContaining({ kind: "mode", mode: "merge-queue" })); + }); + + it("selects native auto-merge while checks are pending", () => { + expect(selectPullRequestMode("auto", { autoMergeAllowed: true, mergeQueueRequired: false, immediatelyMergeable: false, requiresStrictStatusChecks: true })) + .toEqual(expect.objectContaining({ kind: "mode", mode: "auto-merge" })); + }); + + it("falls back to create-only when auto-merge is unavailable", () => { + expect(selectPullRequestMode("auto", { autoMergeAllowed: false, mergeQueueRequired: false, immediatelyMergeable: false, requiresStrictStatusChecks: false })) + .toEqual(expect.objectContaining({ kind: "mode", mode: "create-only" })); + }); + + it("rejects explicit auto-merge when the repository disables it", () => { + expect(selectPullRequestMode("auto-merge", { autoMergeAllowed: false, mergeQueueRequired: false, immediatelyMergeable: false, requiresStrictStatusChecks: false }).kind) + .toBe("unsupported"); + }); + + it("rejects explicit queue mode on a target without a required queue", () => { + expect(selectPullRequestMode("merge-queue", { autoMergeAllowed: true, mergeQueueRequired: false, immediatelyMergeable: false, requiresStrictStatusChecks: false }).kind) + .toBe("unsupported"); + }); + + it.each(["create-only", "legacy-wait"] as const)("preserves explicit %s mode", (mode) => { + expect(selectPullRequestMode(mode, { autoMergeAllowed: false, mergeQueueRequired: false, immediatelyMergeable: false, requiresStrictStatusChecks: false })) + .toEqual(expect.objectContaining({ kind: "mode", mode })); + }); + + it("uses a sync branch for a strict target whose source is stale", () => { + expect(selectBackmergeMode("auto", true, false)).toEqual(expect.objectContaining({ kind: "mode", mode: "sync-branch" })); + }); + + it("allows a direct back-merge for a compatible target", () => { + expect(selectBackmergeMode("auto", false, false)).toEqual(expect.objectContaining({ kind: "mode", mode: "direct" })); + }); + + it("rejects explicit direct back-merge when strict checks would require a reverse merge", () => { + expect(selectBackmergeMode("direct", true, false)).toEqual(expect.objectContaining({ kind: "unsupported" })); + }); + + it("uses an isolated sync branch when the mutable reconciliation source advanced", () => { + expect(selectBackmergeMode("auto", false, true, false)) + .toEqual(expect.objectContaining({ kind: "mode", mode: "sync-branch" })); + }); + + it("rejects an explicit direct reconciliation whose source no longer matches its stored SHA", () => { + expect(selectBackmergeMode("direct", false, true, false)) + .toEqual(expect.objectContaining({ kind: "unsupported" })); + }); + + it("isolates reconciliation when the mutable source branch advanced past the stored SHA", () => { + expect(selectBackmergeMode("auto", false, true, false)).toEqual(expect.objectContaining({ kind: "mode", mode: "sync-branch" })); + }); + + it("rejects an explicitly direct reconciliation from an advanced source branch", () => { + expect(selectBackmergeMode("direct", false, true, false)).toEqual(expect.objectContaining({ kind: "unsupported" })); + }); + + it("reconciles every release into development", () => { + expect(selectReconciliationTargetBranches(operation(), [])).toEqual({ kind: "targets", targetBranches: ["develop"] }); + }); + + it("leaves manual releases published without targets", () => { + expect(selectReconciliationTargetBranches(operation({ strategy: "manual" }), [])).toEqual({ kind: "manual" }); + }); + + it("prefers the single active release for a hotfix", () => { + expect(selectReconciliationTargetBranches(operation({ kind: "hotfix" }), ["release/3.5.0"])) + .toEqual({ kind: "targets", targetBranches: ["release/3.5.0"] }); + }); + + it("blocks ambiguous active releases", () => { + expect(selectReconciliationTargetBranches(operation({ kind: "hotfix" }), ["release/3.5.0", "release/3.6.0"]).kind).toBe("blocked"); + }); + + it("targets both active release and development in order", () => { + expect(selectReconciliationTargetBranches(operation({ kind: "hotfix", hotfixActiveReleasePolicy: "both" }), ["release/3.5.0"])) + .toEqual({ kind: "targets", targetBranches: ["release/3.5.0", "develop"] }); + }); + + it("uses production ancestry for the default reconciliation source", () => { + expect(reconciliationSource(operation())).toEqual({ branch: "master", sha: sha("c") }); + }); + + it("uses the frozen source for canonical Gitflow", () => { + expect(reconciliationSource(operation({ strategy: "canonical-gitflow" }))).toEqual({ branch: "release/3.4.0", sha: sha("a") }); + }); + + it("creates deterministic collision-resistant sync branch names", () => { + expect(buildReconciliationBranchName(operation(), "team/develop")) + .toBe("sync/release-3.4.0-to-team-develop-operatio"); + }); + + it("builds a target from the configured ancestry source", () => { + expect(buildReconciliationTarget(operation(), "develop", "sync-branch")) + .toEqual(expect.objectContaining({ sourceBranch: "master", targetBranch: "develop", syncBranch: expect.stringMatching(/^sync\//) })); + }); + + it("rejects invalid initial deployment facts", () => { + const input = { + operationId: "operation-12345678", kind: "release" as const, version: "v3", title: "", changelog: "", + sourceBranch: "master", sourceSha: "bad", originBranch: "develop", originSha: "bad", + productionBranch: "master", developmentBranch: "develop", publicationWorkflow: "release_workflow.yml", + configuration: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, + }; + expect(validateInitialDeploymentInput(input)).toHaveLength(4); + }); +}); diff --git a/src/application/policies/__tests__/deployment_presentation_policy.test.ts b/src/application/policies/__tests__/deployment_presentation_policy.test.ts new file mode 100644 index 000000000..e7e92c6ed --- /dev/null +++ b/src/application/policies/__tests__/deployment_presentation_policy.test.ts @@ -0,0 +1,214 @@ +import type { DeploymentOperationSnapshot, DeploymentPhase } from "../../../domain/deployment_operation"; +import { + deploymentDashboardMarker, + normalizeLocale, + renderDeploymentDashboard, + renderDeploymentJobSummary, + renderPromotionPullRequest, + renderReconciliationPullRequest, +} from "../deployment_presentation_policy"; + +const operation = (phase: DeploymentPhase = "promotion_pr_pending", overrides: Partial = {}): DeploymentOperationSnapshot => ({ + operationId: "operation-12345678", + kind: "release", + version: "3.4.0", + title: "Release", + changelog: "Changes", + phase, + strategy: "production-lineage", + prMode: "auto", + selectedPrMode: "auto-merge", + backmergeMode: "auto", + hotfixActiveReleasePolicy: "prefer-release", + cleanup: "all", + issueCompletion: "close", + presentationMode: "guided", + diagrams: true, + commentMode: "update", + sourceBranch: "release/3.4.0", + sourceSha: "a".repeat(40), + originBranch: "develop", + originSha: "b".repeat(40), + productionBranch: "master", + developmentBranch: "develop", + reconciliationTree: "sync", + promotionPullRequest: 401, + productionSha: undefined, + tag: "v3.4.0", + publicationWorkflow: "release_workflow.yml", + publicationVerified: false, + reconciliationTargets: [], + lastFailure: null, + ...overrides, +}); + +const context = { + owner: "vypdev", + repository: "copilot", + issue: 355, + issueLocale: "en-US", + pullRequestLocale: "en-US", + workflowRunUrl: "https://github.com/vypdev/copilot/actions/runs/1", + packageName: "@vypdev/copilot", +}; + +describe("deployment presentation policy", () => { + it("uses a stable operation-scoped dashboard marker", () => { + expect(deploymentDashboardMarker("operation-12345678", 355)) + .toBe(''); + }); + + it("orders guided status before progress and technical detail", () => { + const body = renderDeploymentDashboard(operation(), context); + expect(body.indexOf("Current status")).toBeLessThan(body.indexOf("## Progress")); + expect(body.indexOf("## Progress")).toBeLessThan(body.indexOf("Technical details")); + }); + + it("renders a fixed-label Mermaid diagram plus text fallback", () => { + const body = renderDeploymentDashboard(operation(), context); + expect(body).toContain("```mermaid"); + expect(body).toContain("prepared -> production PR -> accepted -> published -> reconciled -> complete"); + expect(body).not.toContain("release/3.4.0 -->"); + }); + + it("removes the diagram in compact mode", () => { + expect(renderDeploymentDashboard(operation(undefined, { presentationMode: "compact" }), context)).not.toContain("```mermaid"); + }); + + it("suppresses secondary sections in quiet mode but preserves state", () => { + const body = renderDeploymentDashboard(operation("publishing", { presentationMode: "quiet" }), context); + expect(body).toContain("publishing artifacts"); + expect(body).not.toContain("## Progress"); + expect(body).toContain("Technical details"); + }); + + it("shows an explicit human action for create-only promotion", () => { + const body = renderDeploymentDashboard(operation(), context); + expect(body).toContain("No action is required"); + expect(renderDeploymentDashboard(operation(undefined, { selectedPrMode: "create-only" }), context)).toContain("Action required"); + }); + + it("distinguishes a blocked pre-publication operation", () => { + const body = renderDeploymentDashboard(operation("blocked", { + lastFailure: { category: "promotion", message: "PR was closed", retryable: true, previousPhase: "promotion_pr_pending" }, + }), context); + expect(body).toContain("❌"); + expect(body).toContain("needs attention"); + expect(body).toContain("PR was closed"); + expect(body.indexOf("## Action required")).toBeLessThan(body.indexOf("## Progress")); + expect(body).toContain("Production updated"); + }); + + it("distinguishes published-but-not-reconciled state", () => { + const body = renderDeploymentDashboard(operation("reconciliation_pending", { publicationVerified: true, productionSha: "c".repeat(40) }), context); + expect(body).toContain("Package status: already published"); + expect(body).toContain("waiting for development reconciliation"); + }); + + it("keeps completed reconciliation visible when only cleanup is blocked", () => { + const body = renderDeploymentDashboard(operation("blocked", { + publicationVerified: true, + productionSha: "c".repeat(40), + reconciliationTargets: [{ targetBranch: "develop", sourceBranch: "master", sourceSha: "c".repeat(40), pullRequest: 402, status: "completed" }], + lastFailure: { category: "cleanup", message: "Branch deletion failed", retryable: true, previousPhase: "reconciliation_pending" }, + }), context); + expect(body).toContain("| Yes | Yes | Yes |"); + expect(body).toContain("- [x] Development reconciliation: `develop`"); + expect(body).toContain("- [ ] Cleanup and issue completion"); + }); + + it("renders completed state with a success label", () => { + expect(renderDeploymentDashboard(operation("completed", { publicationVerified: true }), context)).toContain("✅ Release `3.4.0`"); + }); + + it("renders primary Spanish status and instructions", () => { + const body = renderDeploymentDashboard(operation(), { ...context, issueLocale: "es-ES" }); + expect(body).toContain("Estado actual"); + expect(body).toContain("Qué ocurrirá después"); + expect(body).toContain("No se requiere ninguna acción"); + }); + + it("localizes the complete Spanish diagram and promotion PR surface", () => { + const localized = { ...context, issueLocale: "es-ES", pullRequestLocale: "es-ES" }; + const dashboard = renderDeploymentDashboard(operation(), localized); + const promotion = renderPromotionPullRequest(operation(), localized); + expect(dashboard).toContain("PR de producción"); + expect(dashboard).toContain("Reconciliación con desarrollo"); + expect(promotion.body).toContain("Listo antes de revisar"); + expect(promotion.body).toContain("Después del merge"); + }); + + it("falls unsupported locales back to English", () => { + expect(normalizeLocale("fr-FR")).toBe("en-US"); + }); + + it("normalizes regional Spanish locales", () => { + expect(normalizeLocale("es-MX")).toBe("es-ES"); + }); + + it("renders a deterministic promotion PR with ownership marker", () => { + const value = renderPromotionPullRequest(operation(), context); + expect(value.title).toBe("release(3.4.0): promote to master"); + expect(value.body).toContain('phase="promotion" issue="355"'); + expect(value.body).toContain("After merge"); + }); + + it("renders a reconciliation PR that cannot republish", () => { + const target = { targetBranch: "develop", sourceBranch: "master", sourceSha: "c".repeat(40), pullRequest: 402, status: "pending" as const }; + const value = renderReconciliationPullRequest(operation("reconciliation_pending", { publicationVerified: true, productionSha: "c".repeat(40) }), target, context); + expect(value.title).toBe("release(3.4.0): reconcile master into develop"); + expect(value.body).toContain("cannot publish the package again"); + expect(value.body).toContain('phase="reconciliation" issue="355"'); + }); + + it("localizes the reconciliation PR and explains a sync branch", () => { + const target = { targetBranch: "develop", sourceBranch: "master", sourceSha: "c".repeat(40), syncBranch: "sync/release", status: "pending" as const }; + const value = renderReconciliationPullRequest( + operation("reconciliation_pending", { publicationVerified: true, productionSha: "c".repeat(40) }), + target, + { ...context, pullRequestLocale: "es-ES" }, + ); + expect(value.body).toContain("ya publicado"); + expect(value.body).toContain("rama de sincronización dedicada"); + }); + + it("sanitizes headings, HTML, code fences, and mentions from untrusted values", () => { + const value = renderPromotionPullRequest(operation(undefined, { version: "3.4.0`\n# @team