Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions .github/workflows/release-automation-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ jobs:
user: ${{ steps.detect.outputs.user }}
should_continue: ${{ steps.detect.outputs.should_continue }}
confirm_tag: ${{ steps.detect.outputs.confirm_tag }}
trigger_pr_number: ${{ steps.detect.outputs.trigger_pr_number }}
trigger_pr_url: ${{ steps.detect.outputs.trigger_pr_url }}
trigger: ${{ steps.detect.outputs.trigger }}
release_tag: ${{ steps.state.outputs.release_tag }}
state: ${{ steps.state.outputs.state }}
Expand Down Expand Up @@ -173,8 +171,6 @@ jobs:
id: detect
if: steps.fork-guard.outputs.issues_enabled == 'true'
working-directory: _tooling
env:
GITHUB_TOKEN: ${{ github.token }}
run: python -m release_automation.scripts.trigger

- name: Derive Release State
Expand Down Expand Up @@ -267,8 +263,6 @@ jobs:
CTX_IDENTITY_CONSENT_MANAGEMENT_RELEASE: ${{ steps.state.outputs.identity_consent_management_release }}
CTX_COMMON_CACHE_STATUS: ${{ steps.cache-sync.outputs.common_cache_status }}
CTX_COMMON_CACHE_DETAILS: ${{ steps.cache-sync.outputs.common_cache_details }}
CTX_TRIGGER_PR_NUMBER: ${{ steps.detect.outputs.trigger_pr_number }}
CTX_TRIGGER_PR_URL: ${{ steps.detect.outputs.trigger_pr_url }}
GITHUB_TOKEN: ${{ github.token }}

# ── Response: one bot comment, template chosen by the policy ──
Expand Down Expand Up @@ -1893,35 +1887,11 @@ jobs:
{
"state": "planned",
"trigger_type": "${{ needs.plan.outputs.trigger_type }}",
"trigger_pr_number": "${{ needs.plan.outputs.trigger_pr_number }}",
"trigger_pr_url": "${{ needs.plan.outputs.trigger_pr_url }}",
"release_plan_url": "${{ github.server_url }}/${{ github.repository }}/blob/main/release-plan.yaml",
"closed_issue_number": "${{ github.event.issue.number }}",
"closed_issue_url": "${{ github.event.issue.html_url }}"
}

- name: Post Config Drift Warning
if: |
needs.plan.outputs.trigger_type == 'release_plan_change' &&
needs.plan.outputs.release_issue_number != '' &&
(needs.plan.outputs.state == 'snapshot-active' ||
needs.plan.outputs.state == 'draft-ready')
uses: ./_tooling/shared-actions/post-bot-comment
with:
issue_number: ${{ needs.plan.outputs.release_issue_number }}
release_tag: ${{ needs.plan.outputs.release_tag }}
run_id: ${{ github.run_id }}
template: config_drift_warning
base_context: ${{ needs.plan.outputs.base_context }}
github_token: ${{ steps.app-token.outputs.token || github.token }}
context: |
{
"trigger_type": "release_plan_change",
"trigger_pr_number": "${{ needs.plan.outputs.trigger_pr_number }}",
"trigger_pr_url": "${{ needs.plan.outputs.trigger_pr_url }}",
"release_plan_url": "${{ github.server_url }}/${{ github.repository }}/blob/main/release-plan.yaml"
}

- name: Post Common Sync PR Created
if: |
steps.sync-pr.outputs.sync_status == 'created' &&
Expand Down
9 changes: 3 additions & 6 deletions release_automation/docs/technical-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ tooling/
│ ├── templates/
│ │ ├── bot_messages/
│ │ │ ├── command_rejected.md
│ │ │ ├── config_drift_warning.md
│ │ │ ├── config_error.md
│ │ │ ├── draft_created.md
│ │ │ ├── draft_revoked.md
Expand Down Expand Up @@ -271,7 +270,6 @@ Renders bot messages from Mustache templates using pystache with strict mode (`m
| `interim_processing.md` | Immediate feedback for slash commands being processed |
| `command_rejected.md` | Command not allowed in current state or insufficient permissions |
| `config_error.md` | Invalid `release-plan.yaml` (missing, malformed, or incomplete) |
| `config_drift_warning.md` | `release-plan.yaml` changed on main while snapshot is active |
| `snapshot_created.md` | Snapshot and Release PR created successfully |
| `snapshot_failed.md` | Snapshot creation failed |
| `snapshot_discarded.md` | Snapshot discarded by user |
Expand Down Expand Up @@ -363,7 +361,6 @@ All context variables are defined in a single `BotContext` dataclass (`bot_conte
| `command` | str | `""` | check-trigger |
| `command_args` | str | `""` | check-trigger |
| `user` | str | `""` | check-trigger |
| `trigger_pr_number` | str | `""` | check-trigger |
| **State fields** | | | |
| `release_tag` | str | `""` | derive-state |
| `state` | str | `""` | derive-state |
Expand Down Expand Up @@ -806,7 +803,7 @@ stateDiagram-v2
- Auto-reopen only applies when snapshot or draft is active (prevents orphaned artifacts)
- NOT_PLANNED is derived from `target_release_type: none`, not from issue closure

**Config drift warning:** When `release-plan.yaml` changes on main while a snapshot is active, the automation posts a `config_drift_warning` message on the Release Issue. The snapshot remains valid — the warning is informational, alerting maintainers that the plan has diverged from the active snapshot.
**Plan changes during a snapshot:** An active snapshot is not affected by a later `release-plan.yaml` edit on main — it carries its own `release-metadata.yaml`. Validation blocks `release-plan.yaml` edits while a snapshot is active, and only `/create-snapshot` is gated on the common-file cache check.

### 5.3 Context Data Flow

Expand All @@ -829,7 +826,7 @@ release-metadata.yaml (snapshot branch, if exists)
│
▼
┌──────────────────────┐
│ assemble-context │◄── check-trigger outputs (command, user, trigger_pr_number)
│ assemble-context │◄── check-trigger outputs (command, user, trigger_type)
│ └─ build_context() │◄── derive-state outputs (all above)
│ │◄── command action outputs (release_pr_url, draft_release_url, ...)
│ │◄── workflow env (workflow_run_url)
Expand Down Expand Up @@ -933,7 +930,7 @@ fi
| No issue | Valid plan (type ≠ none) | Pass | Create issue (PLANNED) |
| PLANNED | Plan updated | Pass | Update issue |
| PLANNED | type → none | Pass | Set NOT_PLANNED, suggest close |
| SNAPSHOT_ACTIVE | Any change | **Block (exit 1)** | Config drift warning posted |
| SNAPSHOT_ACTIVE | Any change | **Block (exit 1)** | — |
| DRAFT_READY | Any change | **Block (exit 1)** | — |
| PUBLISHED | Any change | Pass (new release cycle) | Create new issue if type ≠ none |

Expand Down
9 changes: 1 addition & 8 deletions release_automation/scripts/bot_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ class BotContext:
command: str = ""
command_args: str = ""
user: str = ""
trigger_pr_number: str = ""
trigger_type: str = "" # "workflow_dispatch", "issue_close", "release_plan_change"
trigger_pr_url: str = ""
trigger_type: str = "" # "slash_command", "workflow_dispatch", "push", "issue_close", ...

# Issue creation fields (for issue_created template)
closed_issue_number: str = ""
Expand Down Expand Up @@ -72,7 +70,6 @@ class BotContext:
state_published: bool = False
trigger_workflow_dispatch: bool = False
trigger_issue_close: bool = False
trigger_release_plan_change: bool = False
has_meta_release: bool = False
has_reason: bool = False

Expand Down Expand Up @@ -115,7 +112,6 @@ def derive_flags(self) -> None:
self.state_published = self.state == config.STATE_PUBLISHED
self.trigger_workflow_dispatch = self.trigger_type == "workflow_dispatch"
self.trigger_issue_close = self.trigger_type == "issue_close"
self.trigger_release_plan_change = self.trigger_type == "release_plan_change"
self.has_meta_release = bool(self.meta_release)
self.has_reason = bool(self.reason)
self.has_sync_pr = bool(self.sync_pr_url)
Expand All @@ -142,9 +138,7 @@ def to_dict(self) -> Dict[str, Any]:
"command": self.command,
"command_args": self.command_args,
"user": self.user,
"trigger_pr_number": self.trigger_pr_number,
"trigger_type": self.trigger_type,
"trigger_pr_url": self.trigger_pr_url,
# Issue creation fields
"closed_issue_number": self.closed_issue_number,
"closed_issue_url": self.closed_issue_url,
Expand Down Expand Up @@ -181,7 +175,6 @@ def to_dict(self) -> Dict[str, Any]:
"state_published": self.state_published,
"trigger_workflow_dispatch": self.trigger_workflow_dispatch,
"trigger_issue_close": self.trigger_issue_close,
"trigger_release_plan_change": self.trigger_release_plan_change,
"has_meta_release": self.has_meta_release,
"has_reason": self.has_reason,
# Display fields
Expand Down
22 changes: 1 addition & 21 deletions release_automation/scripts/github_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import subprocess
import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional

import yaml

Expand Down Expand Up @@ -562,26 +562,6 @@ def find_pr_for_branch(self, head_branch: str) -> Optional[int]:
except (GitHubClientError, ValueError):
return None

def find_merged_pr_for_commit(self, sha: str) -> Optional[Tuple[int, str]]:
"""
Find the merged PR a commit belongs to (a push event carries no PR data).

Args:
sha: Commit SHA, usually the push head commit

Returns:
(PR number, PR URL) of the first merged PR, or None if there is
none or the lookup fails
"""
try:
output = self._run_gh(["api", f"repos/{self.repo}/commits/{sha}/pulls"])
for pr in json.loads(output or "[]"):
if pr.get("merged_at"):
return pr["number"], pr["html_url"]
except (GitHubClientError, ValueError) as e:
print(f"Warning: Could not look up PR: {e}")
return None

# -------------------------------------------------------------------------
# Retry helper for eventual consistency
# -------------------------------------------------------------------------
Expand Down
59 changes: 10 additions & 49 deletions release_automation/scripts/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
Turns the event that started the workflow into a Trigger: which path the
workflow takes (slash command, issue event, PR merge, dispatch, push) and the
command it carries. parse_trigger() is a pure function of the event name and
the payload at GITHUB_EVENT_PATH; the one network lookup the push path needs
(the PR a commit was merged from) is done by main() through GitHubClient.
the payload at GITHUB_EVENT_PATH.

Usage (workflow step):
python3 -m release_automation.scripts.trigger
Expand All @@ -17,9 +16,9 @@
import sys
import uuid
from dataclasses import asdict, dataclass
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict

from .config import RELEASE_PLAN_FILE, SNAPSHOT_BRANCH_PREFIX
from .config import SNAPSHOT_BRANCH_PREFIX

RELEASE_ISSUE_LABEL = "release-issue"

Expand Down Expand Up @@ -52,7 +51,7 @@ class Trigger:

Attributes:
trigger_type: slash_command | issue_close | issue_reopen | pr_merge |
workflow_dispatch | release_plan_change | push | unknown; empty
workflow_dispatch | push | unknown; empty
when the event concerns no Release Issue
command: Slash command name without the slash, or sync-issue for
dispatch and push
Expand All @@ -62,7 +61,6 @@ class Trigger:
user: Commenter, issue author, PR merger or actor, by event
should_continue: Whether the workflow proceeds past check-trigger
confirm_tag: Tag given with /publish-release --confirm
head_sha: Commit a push event points at (for the merged-PR lookup)
"""
trigger_type: str = ""
command: str = ""
Expand All @@ -71,7 +69,6 @@ class Trigger:
user: str = ""
should_continue: bool = False
confirm_tag: str = ""
head_sha: str = ""


def _is_release_issue(issue: Dict[str, Any]) -> bool:
Expand Down Expand Up @@ -142,28 +139,10 @@ def _parse_pull_request(payload: Dict[str, Any]) -> Trigger:
return Trigger(trigger_type=TRIGGER_UNKNOWN)


def _parse_push(payload: Dict[str, Any], actor: str, sha: str) -> Trigger:
changed = {
path
for commit in payload.get("commits") or []
for key in ("added", "modified", "removed")
for path in commit.get(key) or []
}
head_commit = payload.get("head_commit") or {}
return Trigger(
trigger_type="release_plan_change" if RELEASE_PLAN_FILE in changed else "push",
command="sync-issue",
user=actor,
should_continue=True,
head_sha=head_commit.get("id") or sha,
)


def parse_trigger(
event_name: str,
payload: Dict[str, Any],
actor: str = "",
sha: str = "",
) -> Trigger:
"""
Classify an event.
Expand All @@ -172,7 +151,6 @@ def parse_trigger(
event_name: GITHUB_EVENT_NAME (the caller's event in a reusable workflow)
payload: Parsed JSON at GITHUB_EVENT_PATH
actor: GITHUB_ACTOR, used as user for dispatch and push
sha: GITHUB_SHA, fallback head commit for push

Returns:
Trigger; should_continue is False for events the workflow ignores
Expand All @@ -183,16 +161,14 @@ def parse_trigger(
return _parse_issue_event(payload)
if event_name == "pull_request":
return _parse_pull_request(payload)
if event_name == "workflow_dispatch":
# Dispatch only ever syncs the Release Issue; the command input is ignored.
if event_name in ("workflow_dispatch", "push"):
# Both only ever sync the Release Issue; the dispatch command input is ignored.
return Trigger(
trigger_type="workflow_dispatch",
trigger_type=event_name,
command="sync-issue",
user=actor,
should_continue=True,
)
if event_name == "push":
return _parse_push(payload, actor, sha)
return Trigger(trigger_type=TRIGGER_UNKNOWN)


Expand All @@ -204,18 +180,15 @@ def write_outputs(path: str, outputs: Dict[str, str]) -> None:
f.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n")


def to_outputs(trigger: Trigger, merged_pr: Optional[Tuple[int, str]]) -> Dict[str, str]:
def to_outputs(trigger: Trigger) -> Dict[str, str]:
"""String outputs under the check-trigger job's output names, plus the Trigger as JSON."""
outputs = asdict(trigger)
del outputs["head_sha"]
outputs["trigger"] = json.dumps(asdict(trigger))
outputs["should_continue"] = "true" if trigger.should_continue else "false"
outputs["trigger_pr_number"] = str(merged_pr[0]) if merged_pr else ""
outputs["trigger_pr_url"] = merged_pr[1] if merged_pr else ""
return outputs


def main(client=None) -> int:
def main() -> int:
event_name = os.environ["GITHUB_EVENT_NAME"]
with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as f:
payload = json.load(f)
Expand All @@ -224,21 +197,9 @@ def main(client=None) -> int:
event_name,
payload,
actor=os.environ.get("GITHUB_ACTOR", ""),
sha=os.environ.get("GITHUB_SHA", ""),
)

merged_pr = None
if event_name == "push":
if client is None:
from .github_client import GitHubClient
client = GitHubClient(
repo=os.environ["GITHUB_REPOSITORY"], token=os.environ.get("GITHUB_TOKEN")
)
merged_pr = client.find_merged_pr_for_commit(trigger.head_sha)
if merged_pr:
print(f"Found merged PR: #{merged_pr[0]}")

outputs = to_outputs(trigger, merged_pr)
outputs = to_outputs(trigger)
print(f"Event: {event_name}")
print(f"Action: {payload.get('action') or 'N/A'}")
for name, value in outputs.items():
Expand Down
2 changes: 0 additions & 2 deletions release_automation/scripts/workflow_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ def parse_json_list(json_str):
"src_commit_sha": os.environ.get("CTX_SRC_COMMIT_SHA", ""), # Not in BotContext directly? Check.

# Trigger PR (for push events — PR that triggered the release-plan.yaml change)
"trigger_pr_number": os.environ.get("CTX_TRIGGER_PR_NUMBER", ""),
"trigger_pr_url": os.environ.get("CTX_TRIGGER_PR_URL", ""),

# Dependencies
"commonalities_release": os.environ.get("CTX_COMMONALITIES_RELEASE", ""),
Expand Down

This file was deleted.

3 changes: 0 additions & 3 deletions release_automation/templates/bot_messages/issue_created.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ Created via workflow dispatch from [`release-plan.yaml`]({{release_plan_url}}).
{{#trigger_issue_close}}
Created to replace closed [#{{closed_issue_number}}]({{closed_issue_url}}).
{{/trigger_issue_close}}
{{#trigger_release_plan_change}}
Created after [`release-plan.yaml`]({{release_plan_url}}) update{{#trigger_pr_number}} (PR [#{{trigger_pr_number}}]({{trigger_pr_url}})){{/trigger_pr_number}}.
{{/trigger_release_plan_change}}

<details><summary><b>Configuration:</b> Release {{release_tag}}{{#short_type}} ({{short_type}}{{#has_meta_release}}, {{meta_release}}{{/has_meta_release}}){{/short_type}}</summary>

Expand Down
Loading
Loading