potiuk commented on code in PR #74822: URL: https://github.com/apache/airflow/pull/74822#discussion_r4300385944
########## dev/breeze/src/airflow_breeze/utils/fork_ci_messages.py: ########## @@ -0,0 +1,193 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Texts the fork CI reconciler posts on pull requests (AIP-120). + +All wording lives here so it can be reviewed in one place. Texts must say that CI is free for the +contributor and finite for the project, must not characterise the recipient, and must give the +exact command to run. +""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass +from datetime import datetime + +STATUS_CONTEXT = "Fork CI / Tests (AMD)" +MARKER_PREFIX = "<!-- fork-ci-gate: " +MARKER_PATTERN = re.compile(r"<!-- fork-ci-gate: (?P<payload>.*?) -->") +DESIGN_URL = ( + "https://github.com/apache/airflow/blob/main/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md" +) +CONTRIBUTING_URL = ( + "https://github.com/apache/airflow/blob/main/contributing-docs/18_contribution_workflow.rst" +) +WHY_AUTOMATED_URL = ( + "https://github.com/apache/airflow/blob/main/contributing-docs/25_maintainer_pr_triage.md" + "#why-the-first-pass-is-automated" +) + +FOOTER = ( + "\n\n---\n" + "_This comment was posted automatically by the fork CI monitor from templates drafted with an " + "AI-assisted tool, so it may contain mistakes. An Apache Airflow maintainer, a real person, " + "will review the pull request once its fork CI is green. " + f"[Why the first pass is automated]({WHY_AUTOMATED_URL})._" +) + +WHY_FORK_CI = ( + "Pull requests from contributors run their CI in the contributor's own fork. GitHub Actions is free " + "and has no minute limit on public forks, while the CI capacity of the Apache Software Foundation " + "is shared by every ASF project and is finite." +) + + +@dataclass(frozen=True) +class Marker: + state: str + sha: str + since: datetime + + +def render_marker(marker: Marker) -> str: + payload = json.dumps( + {"state": marker.state, "sha": marker.sha, "since": marker.since.isoformat()}, sort_keys=True + ) + return f"{MARKER_PREFIX}{payload} -->" + + +def parse_marker(body: str) -> Marker | None: + """Return the marker of a comment, None when it has none; raise ValueError when it is malformed.""" + match = MARKER_PATTERN.search(body) + if match is None: + return None + try: + data = json.loads(match.group("payload")) + return Marker(state=data["state"], sha=data["sha"], since=datetime.fromisoformat(data["since"])) + except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e: + raise ValueError(f"Malformed fork CI marker: {match.group(0)!r}") from e + + +def fork_actions_url(fork: str) -> str: + return f"https://github.com/{fork}/actions/workflows/ci-amd.yml" + + +def _setup_instructions(fork: str) -> str: + return ( + "To get CI for this pull request:\n\n" + "1. Enable the workflow in your fork (once per fork):\n\n" + f" ```bash\n gh workflow enable ci-amd.yml -R {fork}\n ```\n\n" + " If your fork's `main` is older than the workflow, sync it first with " + f"`gh repo sync {fork} --branch main`.\n" + "2. Push your branch again. Every push then runs CI in your fork.\n\n" + "`breeze ci audit` checks your whole local and fork setup and prints the command that fixes " + f"anything missing. See [the contribution workflow]({CONTRIBUTING_URL})." + ) + + +_STATE_TEXTS = { + "setup_required": "**Fork CI: setup required.** CI for this pull request has not run in your fork yet, " + "because the `Tests (AMD)` workflow is not enabled there.", + "awaiting_push": "**Fork CI: waiting for a push.** The workflow is enabled in your fork, but it has not " + "run for the latest commit of this pull request. Push the branch again to start it.", + "running": "**Fork CI: running.** CI for the latest commit is running in your fork.", + "failed": "**Fork CI: failed.** CI for the latest commit failed in your fork. Fix the failures and " + "push again; this pull request stays a draft until the run is green.", + "green": "**Fork CI: green.** CI for the latest commit passed in your fork. The pull request is " + "ready for maintainer review.", + "green_unverified": "**Fork CI: green, needs a maintainer.** CI for the latest commit passed in your " + "fork, but this pull request changes files that define what CI runs (such as `.github/`, " + "`dev/breeze/` or `scripts/ci/`), and the fork run used those changed files. A maintainer will " + "review the change and run project CI for it; the pull request stays a draft until then.", + "closed": "**Fork CI: closed after inactivity.** CI did not run in your fork in time; see the " + "comment below for how to continue.", + "gate_bypassed": "**Fork CI: closed.** Project CI ran for this pull request although its CI runs in " + "the contributor's fork; see the comment below.", +} + + +def state_comment(marker: Marker, *, fork: str, run_url: str | None) -> str: + parts = [render_marker(marker), _STATE_TEXTS[marker.state], WHY_FORK_CI] + if run_url: + parts.append(f"Latest run: {run_url}") + if marker.state == "setup_required": + parts.append(_setup_instructions(fork)) + parts.append(f"How this works: [fork CI for external contributors]({DESIGN_URL}).") + return "\n\n".join(parts) + FOOTER + + +def released_comment(marker: Marker) -> str: + return ( + f"{render_marker(marker)}\n\n" + "**Project CI in use.** A maintainer added the `use project ci` label, so CI for this pull " + "request runs in apache/airflow. It starts on the next push or re-run." + FOOTER + ) + + +def nudge_comment(*, fork: str) -> str: + return ( + f"The `Tests (AMD)` workflow is now enabled in {fork}. Push this branch again to start CI " + "for this pull request; the workflow only runs for pushes made after it was enabled." + FOOTER + ) + + +def inactivity_close_comment(*, days: int, fork: str) -> str: + return ( + f"Closing this pull request because CI has not run in your fork for {days} days. {WHY_FORK_CI}\n\n" + "You are welcome to reopen it at any time after setting up fork CI:\n\n" + f"{_setup_instructions(fork)}\n\nReopening restarts the {days}-day period." + FOOTER + ) + + +def tamper_close_comment(*, run_url: str) -> str: + return ( + "Closing this pull request because project CI ran for it in apache/airflow although it is " + f"set to run its CI in the contributor's fork: {run_url}\n\n" + "The pull request changes files under `.github/workflows/`. Such changes cannot be verified " + "in apache/airflow without a maintainer, because they can change which CI runs. If the " + "workflow change is wanted, a maintainer can reopen the pull request and add the " + "`use project ci` label." + FOOTER + ) + + +_STATUS_DESCRIPTIONS = { + "setup_required": "Fork CI not set up - see the pull request comment", + "awaiting_push": "Fork CI enabled, waiting for a push - see the pull request comment", + "running": "Fork CI running - see Details", + "failed": "Fork CI failed - see Details", + "green": "Fork CI passed - see Details", + "green_unverified": "Fork CI passed, but CI files changed - a maintainer must run project CI", Review Comment: Good point — thanks! A `green_unverified` PR was indistinguishable from one whose fork run is still going, even though it only waits for a maintainer. Addressed in c1bdc0f50b: the reconciler now adds a `needs project ci` label when fork CI is green but the PR changes CI-defining files, and removes it as soon as the state is anything else (new push running, green, released via `use project ci`, closed). So `label:"needs project ci"` lists exactly the PRs waiting for a committer to review the CI change and add `use project ci`. It's documented in `08_fork_ci_for_external_contributors.md` and, in #74825, in the maintainer triage guide. --- Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
