diff --git a/.ci/generation/py2cs/__main__.py b/.ci/generation/py2cs/__main__.py index 07c89e9..b19d0af 100644 --- a/.ci/generation/py2cs/__main__.py +++ b/.ci/generation/py2cs/__main__.py @@ -1,4 +1,4 @@ -"""Command line: python -m py2cs check | manifest | diff | align | repos | plan | pr | close | status | verify | drafts | resolve | notes""" +"""Command line: python -m py2cs check | manifest | diff | align | repos | plan | pr | close | verify | drafts | resolve | notes""" from __future__ import annotations @@ -20,7 +20,6 @@ from py2cs.patch import FilePatch, PatchResult, run_patcher from py2cs.plan import Patcher, make_plan from py2cs.pr import PythonPr, close_linked, raise_pr -from py2cs.status import update_status from py2cs.reader import ReadErrors, read_package from py2cs.report import render_report from py2cs.resolve import ResolveError, load_listing, resolve_repo @@ -90,15 +89,6 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--token-env", default="PY2CS_CSHARP_TOKEN") p.add_argument("--dry-run", action="store_true") - p = sub.add_parser("status", help="set the commit status of a Python PR to whether its linked C# pull request is merged") - p.add_argument("--python-repo", required=True) - p.add_argument("--python-pr", required=True, type=int) - p.add_argument("--sha", required=True, help="head commit of the Python pull request") - p.add_argument("--no-csharp-change", action="store_true", help="the Python pull request needs no C# change: the status is success") - p.add_argument("--repos-file", required=True, type=Path, help="text file with the repositories C# pull requests are raised in") - p.add_argument("--token-env", default="PY2CS_PYTHON_TOKEN", help="environment variable with a token that can read the C# repositories and write statuses here") - p.add_argument("--dry-run", action="store_true") - p = sub.add_parser("verify", help="every module of a generated package imports, and it reads back as the C# manifest says") p.add_argument("--package", required=True, type=Path) p.add_argument("--manifest", type=Path) @@ -132,7 +122,7 @@ def main(argv: list[str] | None = None) -> int: return 2 def run(args: argparse.Namespace) -> int: - """Dispatcher of the commands: runs the command named in the arguments (check, manifest, diff, align, repos, plan, pr, close, status, verify, drafts or resolve), prints its result and returns the exit code. + """Dispatcher of the commands: runs the command named in the arguments (check, manifest, diff, align, repos, plan, pr, close, verify, drafts or resolve), prints its result and returns the exit code. Each branch wires the py2cs modules together for one workflow step; unknown commands return 2. """ @@ -253,18 +243,6 @@ def patch_with(patches: list[FilePatch]) -> list[PatchResult]: print(("would " if args.dry_run else "") + str(action)) return 0 - if args.command == "status": - repos = list(load_allowed(args.repos_file)) - client, offline = make_client(args.token_env, repos if args.dry_run else None) - if offline: - print("no token: offline dry run, nothing is read from or written to GitHub") - status = update_status(client, client, repos, args.python_repo, args.python_pr, args.sha, args.dry_run, args.no_csharp_change) - if status is None: - print(f"{args.python_repo}#{args.python_pr} has no linked C# pull request: no status") - else: - print(("would set " if args.dry_run else "set ") + f"the status {status.state}: {status.description}") - return 0 - if args.command == "verify": count, problems = verify_package(args.package, args.manifest) for problem in problems: diff --git a/.ci/generation/py2cs/ci.py b/.ci/generation/py2cs/ci.py index 72ade58..a9a0747 100644 --- a/.ci/generation/py2cs/ci.py +++ b/.ci/generation/py2cs/ci.py @@ -78,7 +78,7 @@ def verify_package(package: Path, manifest: Path | None = None) -> tuple[int, li return count, problems def make_client(token_env: str | None, offline_repos: list[str] | None = None) -> tuple[GitHubClient, bool]: - """Creates the GitHub client for the `pr`, `close` and `status` commands from a token held in an environment variable, and says whether it is an offline stand-in. + """Creates the GitHub client for the `pr` and `close` commands from a token held in an environment variable, and says whether it is an offline stand-in. The token is never on a command line. Without a token and with offline_repos, a MockGitHub with empty repositories stands in, so a dry run on a laptop prints what would be done without calling GitHub. """ diff --git a/.ci/generation/py2cs/github.py b/.ci/generation/py2cs/github.py index 55c8a22..f61970c 100644 --- a/.ci/generation/py2cs/github.py +++ b/.ci/generation/py2cs/github.py @@ -28,7 +28,6 @@ class PullRequest: body: str = "" draft: bool = True state: str = "open" - merged: bool = False @dataclass class RemoteFile: @@ -78,14 +77,6 @@ def upsert_comment(self, repo: str, number: int, marker: str, body: str) -> None def close_pull_request(self, repo: str, number: int) -> None: """Contract: closes a pull request without merging it. `close` calls it on the linked C# draft when its Python PR is closed unmerged.""" - def find_linked_pull_request(self, repo: str, head_branch: str) -> PullRequest | None: - """Contract: returns the newest pull request from the branch whatever its state (open, closed or merged), or None. `status` uses it to see whether the linked C# pull request is merged. - """ - - def set_commit_status(self, repo: str, sha: str, state: str, context: str, description: str, url: str) -> None: - """Contract: sets a commit status (state, context, description, link) on a commit of the repository. `status` uses it to publish the state of the linked C# pull request on the Python PR. - """ - # ------------------------------------------------------------------------------------------------ real client Transport = Callable[[str, str, dict[str, str], bytes | None], tuple[int, bytes]] @@ -210,31 +201,11 @@ def close_pull_request(self, repo: str, number: int) -> None: """ self._request("PATCH", f"/repos/{repo}/pulls/{number}", {"state": "closed"}) - def find_linked_pull_request(self, repo: str, head_branch: str) -> PullRequest | None: - """Looks up the newest pull request from the branch, open, closed or merged, or returns None. - - Used by `status` to find out whether the linked C# pull request is merged. - """ - owner = repo.split("/")[0] - found = self._request("GET", f"/repos/{repo}/pulls?state=all&sort=created&direction=desc&head={urllib.parse.quote(owner + ':' + head_branch, safe='')}") - return self._pull_request(found[0]) if found else None - - def set_commit_status(self, repo: str, sha: str, state: str, context: str, description: str, url: str) -> None: - """Posts a commit status to the GitHub statuses API, cutting the description to the 140 characters GitHub allows. Used by `status` to show the state of the linked C# pull request on the Python PR. - """ - body = {"state": state, "context": context, "description": description[:140], "target_url": url} - self._request("POST", f"/repos/{repo}/statuses/{sha}", body) - @staticmethod def _pull_request(data: dict[str, Any]) -> PullRequest: - # The list endpoint has merged_at but not merged - """Converts a pull request object from the GitHub API into a PullRequest. - - It treats a pull request with merged_at as merged, because the list endpoints do not return the merged flag. - """ - merged = bool(data.get("merged") or data.get("merged_at")) + """Converts a pull request object from the GitHub API into a PullRequest. Used by every client method that returns one.""" return PullRequest(int(data["number"]), str(data["html_url"]), str(data["head"]["ref"]), str(data["base"]["ref"]), str(data.get("title", "")), - str(data.get("body") or ""), bool(data.get("draft", False)), str(data.get("state", "open")), merged) + str(data.get("body") or ""), bool(data.get("draft", False)), str(data.get("state", "open"))) # ------------------------------------------------------------------------------------------------ mock client @@ -246,8 +217,6 @@ class MockRepository: pull_requests: list[PullRequest] = field(default_factory=lambda: []) labels: dict[int, list[str]] = field(default_factory=lambda: {}) comments: dict[int, list[str]] = field(default_factory=lambda: {}) - # (sha, context) -> (state, description, url) - statuses: dict[tuple[str, str], tuple[str, str, str]] = field(default_factory=lambda: {}) class MockGitHub: """An in-memory GitHub. `calls` records every call that changes something, so tests can check what was (not) written.""" @@ -342,15 +311,3 @@ def close_pull_request(self, repo: str, number: int) -> None: for pull in self._repo(repo).pull_requests: if pull.number == number: pull.state = "closed" - - def find_linked_pull_request(self, repo: str, head_branch: str) -> PullRequest | None: - """Returns the newest pull request (highest number) from the branch whatever its state, or None. Stands in for GitHub in tests and offline dry runs. - """ - matches = [p for p in self._repo(repo).pull_requests if p.head == head_branch] - return max(matches, key=lambda p: p.number) if matches else None - - def set_commit_status(self, repo: str, sha: str, state: str, context: str, description: str, url: str) -> None: - """Stores the status for the sha and context, cutting the description to 140 characters, and logs the call. Stands in for GitHub in tests and offline dry runs. - """ - self.calls.append(("set_commit_status", repo, sha, state)) - self._repo(repo).statuses[(sha, context)] = (state, description[:140], url) diff --git a/.ci/generation/py2cs/pr.py b/.ci/generation/py2cs/pr.py index 369c46e..53beb07 100644 --- a/.ci/generation/py2cs/pr.py +++ b/.ci/generation/py2cs/pr.py @@ -45,7 +45,7 @@ class PrResult: pull_request: PullRequest | None = None def branch_name(number: int) -> str: - """Returns the branch python-schema/pr- that holds the C# draft for Python pull request N. `pr`, `close` and `status` all derive the branch from it. + """Returns the branch python-schema/pr- that holds the C# draft for Python pull request N. `pr` and `close` both derive the branch from it. """ return f"{BRANCH_PREFIX}{number}" diff --git a/.ci/generation/py2cs/status.py b/.ci/generation/py2cs/status.py deleted file mode 100644 index ebb6ff1..0000000 --- a/.ci/generation/py2cs/status.py +++ /dev/null @@ -1,61 +0,0 @@ -"""A commit status on the Python pull request that says whether the linked C# pull request is merged. - -The C# is the source of truth, so a Python PR is only ready once its C# PR is merged. The status makes that visible on the PR, and -can be made a required check in the branch protection of develop. -""" - -from __future__ import annotations - -from dataclasses import dataclass - -from py2cs.github import GitHubClient, PullRequest -from py2cs.pr import branch_name - -CONTEXT = "linked C# pull request" - -@dataclass -class Status: - state: str # pending | success | failure - description: str - url: str = "" - -def short(repo: str, pull: PullRequest) -> str: - """Returns a linked pull request as Owner/Name#number for the status text. Used by `linked_status`.""" - return f"{repo}#{pull.number}" - -def linked_status(client: GitHubClient, repos: list[str], python_pr: int) -> Status | None: - """What the linked C# pull requests of one Python pull request say, or None when it has none. - - Merged everywhere is success; any that was closed without merging is a failure (the Python must then change or be closed); - otherwise it is still pending. - """ - found: list[tuple[str, PullRequest]] = [] - for repo in repos: - pull = client.find_linked_pull_request(repo, branch_name(python_pr)) - if pull is not None: - found.append((repo, pull)) - - if not found: - return None - - url = found[0][1].url - abandoned = [short(r, p) for r, p in found if p.state == "closed" and not p.merged] - if abandoned: - return Status("failure", f"{', '.join(abandoned)} was closed without merging", url) - - waiting = [short(r, p) for r, p in found if not p.merged] - if waiting: - return Status("pending", f"Waiting for {', '.join(waiting)} to be merged", url) - - return Status("success", f"Merged: {', '.join(short(r, p) for r, p in found)}", url) - -def update_status(csharp_client: GitHubClient, python_client: GitHubClient, repos: list[str], python_repo: str, python_pr: int, sha: str, - dry_run: bool = False, no_change: bool = False) -> Status | None: - """Writes the status on the head commit of the Python PR. Nothing is written when there is no linked pull request. - - With no_change (the Python PR needs no C# change) the status is success, so a required check does not wait for ever. - """ - status = Status("success", "No C# change is needed") if no_change else linked_status(csharp_client, repos, python_pr) - if status is not None and not dry_run: - python_client.set_commit_status(python_repo, sha, status.state, CONTEXT, status.description, status.url) - return status diff --git a/.ci/unit-tests/python/test_py2cs/test_status.py b/.ci/unit-tests/python/test_py2cs/test_status.py deleted file mode 100644 index 8837926..0000000 --- a/.ci/unit-tests/python/test_py2cs/test_status.py +++ /dev/null @@ -1,207 +0,0 @@ -"""The commit status that says whether the linked C# pull request of a Python PR is merged.""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from helpers import Recorder, mock_github -from py2cs.__main__ import main -from py2cs.github import MockGitHub, PullRequest, RestClient -from py2cs.pr import branch_name -from py2cs.status import CONTEXT, linked_status, update_status - -REPO = "BHoM/Demo_Toolkit" -OTHER = "BHoM/Other_Toolkit" -PYTHON_REPO = "BHoM/BHoM_PythonSchema" -SHA = "abc123" - -def linked(remote: MockGitHub, repo: str, number: int = 7, state: str = "open", merged: bool = False) -> PullRequest: - """Creates a draft PR on the linked branch of a Python PR in the given repository and sets its state and merged flag; returns it. - - The status tests use it to stand for a linked C# PR in each state. - """ - pull = remote.create_pull_request(repo, branch_name(number), "develop", "title", "body", True) - pull.state, pull.merged = state, merged - return pull - -def test_an_open_linked_draft_is_pending(): - """Checks that an open linked draft PR gives a pending status that names the PR and links to it. - - Protects the commit status that tells reviewers the C# PR is not merged yet. - """ - remote = mock_github(REPO) - pull = linked(remote, REPO) - status = linked_status(remote, [REPO], 7) - assert status is not None - assert (status.state, status.description, status.url) == ("pending", f"Waiting for {REPO}#{pull.number} to be merged", pull.url) - -def test_a_merged_linked_pr_is_success(): - """Checks that a merged linked PR gives a success status. - - Protects the commit status that allows the Python PR to merge once the C# is merged. - """ - remote = mock_github(REPO) - linked(remote, REPO, state="closed", merged=True) - status = linked_status(remote, [REPO], 7) - assert status is not None and (status.state, status.description) == ("success", f"Merged: {REPO}#1") - -def test_a_linked_pr_closed_without_merging_is_a_failure(): - """Checks that a linked PR closed without merging gives a failure status. - - Protects the commit status from passing a Python PR whose C# was abandoned. - """ - remote = mock_github(REPO) - linked(remote, REPO, state="closed", merged=False) - status = linked_status(remote, [REPO], 7) - assert status is not None and (status.state, status.description) == ("failure", f"{REPO}#1 was closed without merging") - -def test_no_linked_pr_means_no_status(): - """Checks that linked_status returns nothing when no linked PR exists, including when only another Python PR has a draft. - - Protects the status job from reporting on unrelated PRs. - """ - assert linked_status(mock_github(REPO), [REPO], 7) is None - remote = mock_github(REPO) - linked(remote, REPO, number=8) - assert linked_status(remote, [REPO], 7) is None, "another Python PR's draft is not this one's" - -def test_several_repositories_are_combined(): - """Checks that the statuses of several repositories combine into pending if any is open, success if all are merged and failure if any was closed without merging. - - Protects the status of a Python PR that changes classes of several C# repositories. - """ - remote = mock_github(REPO, OTHER) - linked(remote, REPO, state="closed", merged=True) - linked(remote, OTHER) - status = linked_status(remote, [REPO, OTHER], 7) - assert status is not None and status.state == "pending" and OTHER in status.description - - remote = mock_github(REPO, OTHER) - linked(remote, REPO, state="closed", merged=True) - linked(remote, OTHER, state="closed", merged=True) - status = linked_status(remote, [REPO, OTHER], 7) - assert status is not None and status.state == "success" - - remote = mock_github(REPO, OTHER) - linked(remote, REPO, state="closed", merged=True) - linked(remote, OTHER, state="closed", merged=False) - status = linked_status(remote, [REPO, OTHER], 7) - assert status is not None and status.state == "failure" - -def test_the_newest_pr_of_the_branch_counts(): - """Checks that an abandoned draft followed by a new one for the same Python PR gives pending, not failure. - - Protects the status when a developer reopens the linked work. - """ - remote = mock_github(REPO) - linked(remote, REPO, state="closed", merged=False) - newer = linked(remote, REPO) - status = linked_status(remote, [REPO], 7) - assert status is not None and status.state == "pending" and f"#{newer.number}" in status.description - -def test_the_status_is_written_on_the_head_commit_of_the_python_pr(): - """Checks that update_status writes the status with the right context on the head commit of the Python PR. - - Protects the status job of the linked-PR workflow. - """ - remote = mock_github(REPO) - linked(remote, REPO, state="closed", merged=True) - update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA) - assert remote.repositories[PYTHON_REPO].statuses[(SHA, CONTEXT)][0] == "success" - assert remote.calls[-1] == ("set_commit_status", PYTHON_REPO, SHA, "success") - -def test_nothing_is_written_without_a_linked_pr_or_on_a_dry_run(): - """Checks that update_status writes no status when there is no linked PR, and also none on a dry run, which still returns the status. - - Protects the status job from noise and the dry-run preview. - """ - remote = mock_github(REPO) - assert update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA) is None - assert not any(c[0] == "set_commit_status" for c in remote.calls) - - linked(remote, REPO) - remote.calls.clear() - status = update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA, dry_run=True) - assert status is not None and status.state == "pending" and remote.calls == [] - -def test_a_python_pr_that_needs_no_csharp_change_is_success(): - """Checks that update_status sets a success status saying no C# change is needed when no_change is given. - - Protects the status job for Python PRs that need no linked C# PR. - """ - remote = mock_github(REPO) - status = update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA, no_change=True) - assert status is not None and status.description == "No C# change is needed" - assert remote.repositories[PYTHON_REPO].statuses[(SHA, CONTEXT)][0] == "success" - -def test_a_new_status_replaces_the_old_one_for_the_same_commit(): - """Checks that a new status of the same context on the same commit replaces the old one, from pending to success. - - Protects the status job when the C# PR is merged later. - """ - remote = mock_github(REPO) - pull = linked(remote, REPO) - update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA) - assert remote.repositories[PYTHON_REPO].statuses[(SHA, CONTEXT)][0] == "pending" - - pull.state, pull.merged = "closed", True - update_status(remote, remote, [REPO], PYTHON_REPO, 7, SHA) - assert remote.repositories[PYTHON_REPO].statuses[(SHA, CONTEXT)][0] == "success" - assert len(remote.repositories[PYTHON_REPO].statuses) == 1 - -# ---------------------------------------------------------------- the command line - -def test_command_line_dry_run_without_a_token_is_offline(tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch): - """Checks that py2cs status --dry-run with no token runs offline and prints the status it would set, including with --no-csharp-change. - - Lets people preview the status command without GitHub access. - """ - monkeypatch.delenv("PY2CS_PYTHON_TOKEN", raising=False) - repos = tmp_path / "repos.txt" - repos.write_text(f"{REPO}\n", encoding="utf-8") - status = main(["status", "--python-repo", PYTHON_REPO, "--python-pr", "7", "--sha", SHA, "--repos-file", str(repos), "--dry-run"]) - out = capsys.readouterr().out - assert status == 0 and "offline dry run" in out and "no linked C# pull request: no status" in out - - assert main(["status", "--python-repo", PYTHON_REPO, "--python-pr", "7", "--sha", SHA, "--repos-file", str(repos), "--dry-run", "--no-csharp-change"]) == 0 - assert "would set the status success: No C# change is needed" in capsys.readouterr().out - -# ---------------------------------------------------------------- the REST calls - -def pull_json(number: int, state: str, merged_at: str | None) -> dict[str, object]: - """Builds the JSON of a pull request as the GitHub API returns it, for the given number, state and merged_at. - - The REST client tests use it to make canned answers. - """ - return {"number": number, "html_url": f"https://github.com/o/r/pull/{number}", "head": {"ref": "python-schema/pr-7"}, "base": {"ref": "develop"}, - "state": state, "draft": False, "merged_at": merged_at} - -def test_rest_client_finds_the_newest_pr_of_a_branch_in_any_state_and_knows_if_it_merged(): - """Checks that find_linked_pull_request returns the newest PR of the branch in any state, knows whether it merged, and returns nothing when there is none. - - Protects the GitHubClient that the status job and the sweep use. - """ - path = "/repos/o/r/pulls?state=all&sort=created&direction=desc&head=o%3Apython-schema%2Fpr-7" - transport = Recorder({("GET", path): (200, [pull_json(5, "closed", "2026-10-29T10:00:00Z"), pull_json(3, "closed", None)])}) - found = RestClient("tok", transport=transport).find_linked_pull_request("o/r", "python-schema/pr-7") - assert found is not None and (found.number, found.state, found.merged) == (5, "closed", True) - - transport = Recorder({("GET", path): (200, [pull_json(3, "closed", None)])}) - found = RestClient("tok", transport=transport).find_linked_pull_request("o/r", "python-schema/pr-7") - assert found is not None and not found.merged - - assert RestClient("tok", transport=Recorder({("GET", path): (200, [])})).find_linked_pull_request("o/r", "python-schema/pr-7") is None - -def test_rest_client_sets_a_commit_status_with_a_short_description(): - """Checks that set_commit_status posts to the commit status endpoint and cuts the description to 140 characters, the GitHub limit. - - Protects the status job from rejected API calls. - """ - transport = Recorder({("POST", "/repos/o/r/statuses/abc123"): (201, {})}) - RestClient("tok", transport=transport).set_commit_status("o/r", "abc123", "pending", CONTEXT, "x" * 300, "https://github.com/o/r/pull/3") - method, path, _, body = transport.requests[0] - assert (method, path) == ("POST", "/repos/o/r/statuses/abc123") - assert isinstance(body, dict) and body["state"] == "pending" and body["context"] == CONTEXT and len(str(body["description"])) == 140 diff --git a/.ci/unit-tests/python/test_py2cs/test_workflows.py b/.ci/unit-tests/python/test_py2cs/test_workflows.py index 63a9726..205e6e2 100644 --- a/.ci/unit-tests/python/test_py2cs/test_workflows.py +++ b/.ci/unit-tests/python/test_py2cs/test_workflows.py @@ -36,10 +36,9 @@ def test_all_workflows_are_valid_and_have_the_expected_jobs(): assert list(load("pr-checks.yml")["jobs"]) == ["checks", "alignment"] assert list(load("linked-csharp-pr.yml")["jobs"]) == ["linked"], "one job, so the pull request page shows one line" assert list(load("linked-csharp-pr-close.yml")["jobs"]) == ["close"] - assert list(load("linked-csharp-pr-sweep.yml")["jobs"]) == ["sweep"] def test_the_weekly_generation_commit_cannot_start_the_linked_pr_workflow(): - """Checks that none of the linked-PR workflows has a push trigger, that they run for pull requests only on src/bhom_schema paths, and that only the close workflow reacts to a closed pull request. + """Checks that neither linked-PR workflow has a push or schedule trigger, that they run for pull requests only on src/bhom_schema paths, and that only the close workflow reacts to a closed pull request. Protects against the weekly generation commit, which is a push, starting a linked C# PR, and the open pull request page from showing jobs that only matter on closing. """ @@ -52,9 +51,7 @@ def test_the_weekly_generation_commit_cannot_start_the_linked_pr_workflow(): assert set(closed) == {"pull_request", "workflow_dispatch"} assert closed["pull_request"]["types"] == ["closed"] and closed["pull_request"]["paths"] == ["src/bhom_schema/**"] - swept = load("linked-csharp-pr-sweep.yml")["on"] - assert set(swept) == {"schedule", "workflow_dispatch"} - assert swept["schedule"] == [{"cron": "17 * * * *"}] + assert "schedule" not in raised and "schedule" not in closed, "nothing polls: the C# alignment is re-run by hand after the C# merge" def test_the_weekly_generation_commits_directly_and_is_not_a_pull_request(): @@ -73,10 +70,10 @@ def test_workflows_start_with_read_only_permissions_and_raise_them_per_job(): Protects the token permissions of the workflows. """ - for name in ("linked-csharp-pr.yml", "linked-csharp-pr-close.yml", "linked-csharp-pr-sweep.yml", "pr-checks.yml"): + for name in ("linked-csharp-pr.yml", "linked-csharp-pr-close.yml", "pr-checks.yml"): assert load(name)["permissions"] == {"contents": "read"}, name - assert load("linked-csharp-pr.yml")["jobs"]["linked"]["permissions"] == {"contents": "read", "pull-requests": "write", "statuses": "write"} + assert load("linked-csharp-pr.yml")["jobs"]["linked"]["permissions"] == {"contents": "read", "pull-requests": "write"} assert "permissions" not in load("linked-csharp-pr-close.yml")["jobs"]["close"] @@ -132,13 +129,10 @@ def test_fork_pull_requests_get_no_linked_pull_request(): for name, job, gate in (("linked-csharp-pr.yml", "linked", "raise"), ("linked-csharp-pr-close.yml", "close", "close")): for step in load(name)["jobs"][job]["steps"]: - if step.get("id") == "ctx" or str(step.get("uses", "")).startswith("actions/checkout") or step.get("name") == "Set the commit status": + if step.get("id") == "ctx" or str(step.get("uses", "")).startswith("actions/checkout"): continue assert f"steps.ctx.outputs.{gate} == 'true'" in step["if"], step.get("name") - status = next(s for s in load("linked-csharp-pr.yml")["jobs"]["linked"]["steps"] if s.get("name") == "Set the commit status") - assert status["if"] == "steps.ctx.outputs.open == 'true'" - def test_the_main_bhom_repository_is_never_written_to(): """Checks that the list of converted repositories holds only the test repository and that BHoM is in the build list. @@ -166,42 +160,6 @@ def test_the_generation_and_the_checks_build_the_same_repositories(): text = path.read_text(encoding="utf-8") assert "Clone-Repositories.ps1 -List Included-repositories.txt" in text and "Build-Repositories.ps1 -List Included-repositories.txt" in text -def test_the_status_jobs_write_statuses_and_nothing_else(): - """Checks that the linked job and the sweep job get statuses write permission, the close job does not, and that no App token is made for the sweep. - - Protects the permissions of the status part of the linked-PR workflows. - """ - assert load("linked-csharp-pr.yml")["jobs"]["linked"]["permissions"]["statuses"] == "write" - sweep = load("linked-csharp-pr-sweep.yml")["jobs"]["sweep"] - assert sweep["permissions"] == {"contents": "read", "pull-requests": "read", "statuses": "write"} - assert "statuses" not in load("linked-csharp-pr-close.yml")["jobs"]["close"].get("permissions", {}) - - # The sweep reads the C# repositories with the workflow's own token, so no GitHub App token is made for it - assert not any(str(s.get("uses", "")).startswith("actions/create-github-app-token") for s in sweep["steps"]) - - -def test_the_hourly_sweep_is_its_own_workflow_and_no_other_workflow_runs_on_a_schedule_for_it(): - """Checks that the hourly sweep lives in its own workflow, which has the cron 17 * * * * and no pull_request trigger, and that the other linked-PR workflows have no schedule. - - Protects the pull request page from showing the sweep, and the hourly refresh from running the raise or close steps. - """ - assert load("linked-csharp-pr-sweep.yml")["on"]["schedule"] == [{"cron": "17 * * * *"}] - assert "pull_request" not in load("linked-csharp-pr-sweep.yml")["on"] - assert "schedule" not in load("linked-csharp-pr.yml")["on"] and "schedule" not in load("linked-csharp-pr-close.yml")["on"] - - -def test_a_python_pr_with_no_csharp_change_still_gets_a_status(): - """Checks that the linked job sets the status for every open pull request of this repository, also when no repository is affected, and passes --no-csharp-change then. - - Protects the required status from staying pending on a Python PR that needs no C# change. - """ - steps = load("linked-csharp-pr.yml")["jobs"]["linked"]["steps"] - status = next(s for s in steps if s.get("name") == "Set the commit status") - assert status["if"] == "steps.ctx.outputs.open == 'true'" - assert "--no-csharp-change" in status["run"] - - - def test_the_release_workflow_is_manual_and_only_the_publish_job_gets_an_identity_token(): """Checks that the release workflow starts only by hand, that contents are read-only by default, and that only the TestPyPI job can ask for an identity token. @@ -249,12 +207,12 @@ def test_the_pr_checks_build_only_when_the_cache_of_assemblies_misses(): def test_the_alignment_check_is_its_own_job_with_a_name_that_says_what_is_pending(): - """Checks that the C# alignment runs as its own job of pr-checks, named after the linked C# pull request, using the shared assemblies action and explaining a failure in the job summary, and that the main job no longer runs align. + """Checks that the C# alignment runs as its own job of pr-checks, named after the linked C# PR, using the shared assemblies action and explaining a failure in the job summary, and that the main job no longer runs align. Protects the pull request page from showing an unexplained failure while the C# pull request is still open. """ jobs = load("pr-checks.yml")["jobs"] - assert "linked C# pull request" in jobs["alignment"]["name"] + assert "linked C# PR" in jobs["alignment"]["name"] assert any(s.get("uses") == "./.github/actions/bhom-assemblies" for s in jobs["alignment"]["steps"]) assert any(s.get("name") == "Explain the failure" and "GITHUB_STEP_SUMMARY" in s["run"] for s in jobs["alignment"]["steps"]) assert not any("align" in s.get("run", "") for s in jobs["checks"]["steps"]) diff --git a/.github/actions/linked-pr-context/action.yml b/.github/actions/linked-pr-context/action.yml index 6be8375..38968ef 100644 --- a/.github/actions/linked-pr-context/action.yml +++ b/.github/actions/linked-pr-context/action.yml @@ -19,9 +19,6 @@ outputs: merged: description: true when the pull request was merged value: ${{ steps.pr.outputs.merged }} - open: - description: true for a pull request of this repository (not a fork) that is not closed - value: ${{ steps.gate.outputs.open }} raise: description: true when it is open and the change touches a C# repository value: ${{ steps.gate.outputs.raise }} @@ -110,7 +107,6 @@ runs: ANY: ${{ steps.repos.outputs.any }} run: | { - echo "open=$([ "$ELIGIBLE" = true ] && [ "$CLOSED" = false ] && echo true || echo false)" echo "raise=$([ "$ELIGIBLE" = true ] && [ "$CLOSED" = false ] && [ "$ANY" = true ] && echo true || echo false)" echo "close=$([ "$ELIGIBLE" = true ] && [ "$CLOSED" = true ] && [ "$ANY" = true ] && echo true || echo false)" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/linked-csharp-pr-close.yml b/.github/workflows/linked-csharp-pr-close.yml index abe0bd2..040972a 100644 --- a/.github/workflows/linked-csharp-pr-close.yml +++ b/.github/workflows/linked-csharp-pr-close.yml @@ -1,4 +1,4 @@ -name: Linked C# pull request (close) +name: C# PR (close) # When the Python pull request is closed: unmerged closes the linked C# draft with a comment, merged only comments on it. # A separate workflow from linked-csharp-pr.yml, so that nothing of it shows on a pull request that is still open. @@ -29,7 +29,7 @@ concurrency: jobs: close: - name: close the C# draft + name: Close draft runs-on: ubuntu-latest steps: # The repository itself, only to reach the shared action below diff --git a/.github/workflows/linked-csharp-pr-sweep.yml b/.github/workflows/linked-csharp-pr-sweep.yml deleted file mode 100644 index e581776..0000000 --- a/.github/workflows/linked-csharp-pr-sweep.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Linked C# pull request (hourly status) - -# Every hour: has the C# pull request of an open Python PR been merged (or closed) since? Nothing tells this repository when that happens. -# This only reads the C# repositories, which must be public, and writes the commit status "linked C# pull request" on this repository. -# A schedule only, so nothing of it shows on a pull request. - -on: - schedule: - - cron: '17 * * * *' - workflow_dispatch: - -permissions: - contents: read - -jobs: - sweep: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - statuses: write - steps: - # The default branch, so the tooling is the trusted one - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Update the status of every open Python PR that changes the classes - shell: bash - env: - GH_TOKEN: ${{ github.token }} - PYTHONPATH: .ci/generation - PY2CS_PYTHON_TOKEN: ${{ github.token }} - run: | - gh pr list --state open --limit 100 --json number,headRefOid,headRepositoryOwner,files \ - --jq '.[] | select(.headRepositoryOwner.login == "${{ github.repository_owner }}") | select(any(.files[]; .path | startswith("src/bhom_schema/"))) | "\(.number) \(.headRefOid)"' | - while read -r number sha; do - echo "Python PR #$number" - python -m py2cs status --python-repo "${{ github.repository }}" --python-pr "$number" --sha "$sha" --repos-file Converted-repositories.txt - done diff --git a/.github/workflows/linked-csharp-pr.yml b/.github/workflows/linked-csharp-pr.yml index 74db51b..5c57bc9 100644 --- a/.github/workflows/linked-csharp-pr.yml +++ b/.github/workflows/linked-csharp-pr.yml @@ -1,12 +1,10 @@ -name: Linked C# pull request +name: C# PR # A pull request that changes the Python classes raises a linked DRAFT pull request in the repository that owns the C# classes. # It never raises anything for a push, so the weekly generation (a push to develop) cannot start it. # -# It also sets a commit status on the Python PR, "linked C# pull request": pending while the C# draft is open, success once it is -# merged, failure if it was closed without merging. Nothing tells this repository when a C# PR is merged, so an hourly run -# (linked-csharp-pr-sweep.yml) looks at every open Python PR and updates the status. Make the status a required check on develop to stop a -# Python PR being merged first. Closing the Python PR is handled by linked-csharp-pr-close.yml. +# The comment it leaves on the Python PR links the draft. A Python PR is only ready once its C# PR is merged: the "C# alignment" job of +# pr-checks.yml fails until then, and is re-run by hand after the merge. Closing the Python PR is handled by linked-csharp-pr-close.yml. # # Where C# pull requests may be raised is listed in Converted-repositories.txt. The GitHub App token is made only for the repositories the # change touches, which are always listed there. @@ -37,12 +35,11 @@ concurrency: jobs: linked: - name: raise or update the C# draft + name: Raise or update draft runs-on: ubuntu-latest permissions: contents: read pull-requests: write - statuses: write steps: # The repository itself, only to reach the shared action below - uses: actions/checkout@v4 @@ -121,19 +118,3 @@ jobs: --python-repo "${{ github.repository }}" --python-pr "${{ steps.ctx.outputs.number }}" \ --python-url "${{ steps.ctx.outputs.url }}" $flags done - - # After the draft is raised or updated, or when the change needs no C# change at all: pending until the C# pull request is merged - - name: Set the commit status - if: steps.ctx.outputs.open == 'true' - shell: bash - env: - PYTHONPATH: head/.ci/generation - PY2CS_PYTHON_TOKEN: ${{ github.token }} - DRY_RUN: ${{ inputs.dry_run }} - RAISED: ${{ steps.ctx.outputs.raise }} - run: | - flags="" - if [ "$DRY_RUN" = "true" ]; then flags="--dry-run"; fi - if [ "$RAISED" != "true" ]; then flags="$flags --no-csharp-change"; fi - python -m py2cs status --python-repo "${{ github.repository }}" --python-pr "${{ steps.ctx.outputs.number }}" \ - --sha "$(git -C head rev-parse HEAD)" --repos-file base/Converted-repositories.txt $flags diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 9762066..b5a007e 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -1,4 +1,4 @@ -name: PR checks +name: PR on: pull_request: @@ -14,6 +14,7 @@ concurrency: jobs: checks: + name: Build and tests runs-on: windows-latest env: # Real-type tests fail instead of being skipped when an assembly is missing @@ -73,7 +74,7 @@ jobs: # Its own job, so that the pull request page names what is pending. The Python PR is a proposal: this only passes once the linked C# pull # request is merged, because the assemblies built here come from the C# develop branches. After that merge, re-run this job. alignment: - name: C# alignment (fails until the linked C# pull request is merged) + name: C# alignment (fails until the linked C# PR is merged) runs-on: windows-latest env: PYTHONPATH: ${{ github.workspace }}/.ci/generation @@ -137,7 +138,7 @@ jobs: The classes changed in this pull request are compared with the Python that the generator writes from the **merged C#**. They differ, which usually means the linked C# pull request is not merged yet. - 1. Open the linked draft pull request (the commit status `linked C# pull request` links it) and merge it in the C# repository. + 1. Open the linked draft pull request (the comment on this pull request links it) and merge it in the C# repository. 2. Re-run this job. It should then pass. If the C# is already merged and it still fails, the differences are listed in the log of the step `py2cs align`: either the C# review changed the class (copy that change into the Python), or the generator and py2cs disagree (a bug to fix in this repository). diff --git a/docs/CODING-PLAN.md b/docs/CODING-PLAN.md index 62da0dd..579d6bb 100644 --- a/docs/CODING-PLAN.md +++ b/docs/CODING-PLAN.md @@ -9,7 +9,7 @@ taken so far (section 9). ## Implementation status (phases 0 to 6 done, 7 and 8 not started) -Run `.ci/unit-tests/run-all.ps1`; it builds, generates, drafts, round-trips, runs NUnit (92 tests), pytest (199 tests) and Pyright. +Run `.ci/unit-tests/run-all.ps1`; it builds, generates, drafts, round-trips, runs NUnit (92 tests), pytest (183 tests) and Pyright. Where the implementation differs from the plan above: diff --git a/docs/Private-Repositories.md b/docs/Private-Repositories.md index 11c33e6..30ce9ed 100644 --- a/docs/Private-Repositories.md +++ b/docs/Private-Repositories.md @@ -8,9 +8,8 @@ repositories, for example the ones in the BuroHappoldEngineering organisation, a | Part | Why it fails | |---|---| -| Hourly sweep in `linked-csharp-pr-sweep.yml` | It reads the C# pull requests with this repository's `GITHUB_TOKEN`, which can only read this repository and public repositories | | `.github/scripts/Clone-Repositories.ps1` | It clones anonymously over HTTPS | -| The token steps in `linked-csharp-pr.yml` and `linked-csharp-pr-close.yml` | It passes `owner: ${{ github.repository_owner }}`, so it only works for C# repositories in the same organisation as this one. Repositories in another organisation fail even without the sweep | +| The token steps in `linked-csharp-pr.yml` and `linked-csharp-pr-close.yml` | It passes `owner: ${{ github.repository_owner }}`, so it only works for C# repositories in the same organisation as this one. Repositories in another organisation fail | | Public logs and artifacts | This repository is public, so its Actions logs and uploaded artifacts are public. The weekly generation uploads a report and a manifest that list class names, warnings and build output. Private code built or generated here would be exposed | | `src/bhom_schema` | Classes generated from a private repository and committed here would be public | @@ -41,10 +40,10 @@ This applies to every deployment, because each one reads and writes C# repositor | Option | How it works | Pros | Cons | |---|---|---|---| -| 1. One GitHub App, matrix per repository | Take the owner from each repository's `Owner/Name`. Make one token per repository in a matrix leg. The sweep becomes a matrix plus one final job that combines the results into a single status | Same GitHub-maintained action. No new secrets in the C# repositories. Small change | The App must be installed in both organisations, and its visibility must be "Any account" (a private App can only be installed in its own organisation). The sweep is more complex | +| 1. One GitHub App, matrix per repository | Take the owner from each repository's `Owner/Name`. Make one token per repository in a matrix leg. | Same GitHub-maintained action. No new secrets in the C# repositories. Small change | The App must be installed in both organisations, and its visibility must be "Any account" (a private App can only be installed in its own organisation). | | 2. One App per organisation | Same as 1, with one App in BHoM and one in BuroHappoldEngineering. Each deployment holds the secrets of its own App | Strongest separation: nothing crosses organisations. Natural with option B | Two Apps to set up and maintain | -| 3. `py2cs` makes the tokens itself | `py2cs` signs a JWT with the App key and asks GitHub for an installation token per repository. The sweep stays one simple loop | One step, any number of owners | Needs a JWT/crypto library, and the code that holds the private key grows. Best avoided | -| 4. The C# repositories report their own state | A workflow in each C# repository, on pull request merge or close, sets the status on the Python PR | Event-driven: no hourly polling, no cross-organisation reads | Needs a workflow and secrets in every C# repository. The public repositories, BHoM/BHoM in particular, cannot be changed yet | +| 3. `py2cs` makes the tokens itself | `py2cs` signs a JWT with the App key and asks GitHub for an installation token per repository. | One step, any number of owners | Needs a JWT/crypto library, and the code that holds the private key grows. Best avoided | +| 4. The C# repositories report their own state | A workflow in each C# repository, on pull request merge or close, tells the Python PR (for example by re-running its alignment check) | Event-driven: no manual re-run, no cross-organisation reads | Needs a workflow and secrets in every C# repository. The public repositories, BHoM/BHoM in particular, cannot be changed yet | | 5. A personal access token or bot account | One long-lived token that can read the repositories | Simplest | Tied to a person or account, long-lived, and hard to scope across two organisations. Not recommended | ## Recommendation @@ -54,11 +53,10 @@ This applies to every deployment, because each one reads and writes C# repositor 3. In both cases: - take the owner from each repository's `Owner/Name`; - make the cloning script use a token for private repositories; - - make the sweep a per-repository matrix with one final job; - keep uploaded artifacts and verbose logs out of any deployment that handles private code. -Token option 4 is a good later addition for private repositories, because their owners control them. It cannot replace the sweep for -BHoM/BHoM. +Token option 4 is a good later addition for private repositories, because their owners control them. The public BHoM/BHoM cannot have a +workflow added yet. ## Questions to settle before any code @@ -69,7 +67,7 @@ BHoM/BHoM. These places assume public repositories in one organisation, and would change: -- `.github/workflows/linked-csharp-pr.yml` and `linked-csharp-pr-close.yml`: `owner: ${{ github.repository_owner }}` in the token steps, and `linked-csharp-pr-sweep.yml`. +- `.github/workflows/linked-csharp-pr.yml` and `linked-csharp-pr-close.yml`: `owner: ${{ github.repository_owner }}` in the token steps. - `.github/scripts/Clone-Repositories.ps1`: anonymous `git clone`. - `.github/workflows/python-schema-generation.yml`: uploads the report and manifest as a public artifact. - `Converted-repositories.txt` and `Included-repositories.txt`: one list each, for one deployment. diff --git a/docs/Testing-On-GitHub.md b/docs/Testing-On-GitHub.md index b0a73d7..8a8431d 100644 --- a/docs/Testing-On-GitHub.md +++ b/docs/Testing-On-GitHub.md @@ -129,21 +129,21 @@ Goal: a class that nothing else uses, written from scratch. | Where | What you should see | |---|---| -| **PR checks / checks** on the Python PR | Build, NUnit, pytest and Pyright pass. `py2cs check` passes | -| **PR checks / C# alignment** on the Python PR | **Fails until the linked C# PR is merged**, with an explanation in the job summary: it compares the Python with what the C# `develop` generates. That failure is the proof that the two are not in step yet | -| **Linked C# pull request / raise or update the C# draft** | Green. The step *Which repositories does the change touch?* names `BHoM/CSharpSchema_Test`; *Push the branch and open or update the draft pull request* pushes `python-schema/pr-` and opens a **draft** PR | +| **PR / Build and tests** on the Python PR | Build, NUnit, pytest and Pyright pass. `py2cs check` passes | +| **PR / C# alignment** on the Python PR | **Fails until the linked C# PR is merged**, with an explanation in the job summary: it compares the Python with what the C# `develop` generates. That failure is the proof that the two are not in step yet | +| **C# PR / Raise or update draft** | Green. The step *Which repositories does the change touch?* names `BHoM/CSharpSchema_Test`; *Push the branch and open or update the draft pull request* pushes `python-schema/pr-` and opens a **draft** PR | | The C# draft PR | The files and lines in the table at the top of the exercise, a link back to the Python PR, labels, and nothing else changed. Open the diff and read it as a C# reviewer would. Check that it builds | -| Commit status on the Python PR | `linked C# pull request`: pending while the draft is open | +| Comment on the Python PR | `Draft C# pull request: `, written once and updated in place on later pushes | | The Python PR after a push | Pushing again updates the same branch and the same draft; no second PR | -To try the flow before relying on the automatic run, start *Linked C# pull request* by hand with the Python PR number: with `dry_run` ticked it +To try the flow before relying on the automatic run, start *C# PR* by hand with the Python PR number: with `dry_run` ticked it prints what it would do and proves the App token; unticked it does it. ## Closing the loop (every exercise) 1. In CSharpSchema_Test, review the draft, fix anything wrong **in C#** (the C# is the source of truth), mark it ready and merge it. -2. Back on the Python PR: within the hour the status becomes `success` (or start *Linked C# pull request* by hand to refresh it now). -3. Re-run **C# alignment**. It must now pass: the Python generated from the merged C# equals the Python in the PR. +2. Back on the Python PR, re-run **C# alignment** by hand (Re-run jobs); nothing re-runs it for you. +3. Check **C# alignment**. It must now pass: the Python generated from the merged C# equals the Python in the PR. If it still fails, the message lists what differs; either the C# review changed something (copy the change into the Python) or the generator and `py2cs` disagree (a bug to fix in this repository). The NUnit tests and their golden files use their own test classes (`.ci/unit-tests/csharp/Fixtures`), so a merged diff --git a/docs/Workflows.md b/docs/Workflows.md index 18e2033..7fed964 100644 --- a/docs/Workflows.md +++ b/docs/Workflows.md @@ -7,10 +7,9 @@ written to.** BHoM, BHoM_Engine and JSONSchema_Toolkit are only cloned and built | Workflow | When | What it does | |---|---|---| | [python-schema-generation.yml](../.github/workflows/python-schema-generation.yml) | Every Saturday at 4 AM UTC, and by hand | Clones and builds the C# repositories, generates `src/bhom_schema` from the C#, checks it, and commits it straight to `develop`. Not a pull request. If a check fails, nothing is committed | -| [pr-checks.yml](../.github/workflows/pr-checks.yml) | Every pull request to `develop` | Two jobs. **checks**: installs the built assemblies of the repositories in `Included-repositories.txt` (the shared action `.github/actions/bhom-assemblies` caches them under the current commit of each repository, so unchanged repositories are not rebuilt), clones BHoM_JSONSchema read only as the test oracle for the JSON, runs `run-all.ps1` (build, NUnit, pytest, Pyright) and, for a PR that changes `src/bhom_schema`, `py2cs check`. **C# alignment (fails until the linked C# pull request is merged)**: for such a PR, builds the generator and runs `py2cs align`; when it fails it explains in the job summary that the linked C# pull request has to be merged first, then the job re-run | -| [linked-csharp-pr.yml](../.github/workflows/linked-csharp-pr.yml) | A pull request that changes `src/bhom_schema`, and by hand | One job, **raise or update the C# draft**: finds the C# repositories the change touches, raises or updates a linked **draft** pull request in each, and sets the commit status `linked C# pull request` on the Python PR. The shared action `.github/actions/linked-pr-context` does the finding | +| [pr-checks.yml](../.github/workflows/pr-checks.yml) | Every pull request to `develop` | Two jobs. **Build and tests**: installs the built assemblies of the repositories in `Included-repositories.txt` (the shared action `.github/actions/bhom-assemblies` caches them under the current commit of each repository, so unchanged repositories are not rebuilt), clones BHoM_JSONSchema read only as the test oracle for the JSON, runs `run-all.ps1` (build, NUnit, pytest, Pyright) and, for a PR that changes `src/bhom_schema`, `py2cs check`. **C# alignment (fails until the linked C# PR is merged)**: for such a PR, builds the generator and runs `py2cs align`; when it fails it explains in the job summary that the linked C# pull request has to be merged first, then the job re-run | +| [linked-csharp-pr.yml](../.github/workflows/linked-csharp-pr.yml) | A pull request that changes `src/bhom_schema`, and by hand | One job, **Raise or update draft**: finds the C# repositories the change touches, raises or updates a linked **draft** pull request in each, and comments on the Python PR with its link. The shared action `.github/actions/linked-pr-context` does the finding | | [linked-csharp-pr-close.yml](../.github/workflows/linked-csharp-pr-close.yml) | A Python pull request that changes `src/bhom_schema` is closed, and by hand | Closes the linked C# draft with a comment when the Python PR was closed unmerged, or only comments when it was merged. A separate workflow so that nothing of it shows on an open pull request | -| [linked-csharp-pr-sweep.yml](../.github/workflows/linked-csharp-pr-sweep.yml) | Every hour, and by hand | Refreshes the commit status `linked C# pull request` of every open Python PR that changes the classes. A schedule only, so nothing of it shows on a pull request | | [release.yml](../.github/workflows/release.yml) | By hand only (dry run by default) | Regenerates with `--release X.Y`, checks, builds the wheel and the release notes; when not a dry run, pushes branch `vX.Y` and tag `vX.Y.0`, creates the GitHub Release and publishes to TestPyPI. See "Making a release" | ## How they fit together @@ -35,13 +34,13 @@ written to.** BHoM, BHoM_Engine and JSONSchema_Toolkit are only cloned and built │ pr-checks.yml │ │ linked-csharp-pr.yml │ │ on every PR to develop │ │ on a PR that changes src/bhom_schema │ │ build, NUnit, pytest, │ │ │ -│ Pyright; py2cs check; │ │ one job: raise or update the C# draft, and set │ -│ C# alignment (own job) │ │ the commit status on the Python PR │ +│ Pyright; py2cs check; │ │ one job: raise or update the draft, and add │ +│ C# alignment (own job) │ │ a comment on the Python PR with its link │ │ (writes nothing) │ │ │ └──────────────────────────────┘ │ PR closed: linked-csharp-pr-close.yml │ - │ every hour: linked-csharp-pr-sweep.yml │ - │ (refreshes the status of every │ - │ open Python PR) │ + │ │ + │ │ + │ │ └─────────────────────┬───────────────────────────┘ │ the C# draft is reviewed and merged in C# ▼ @@ -104,7 +103,7 @@ The linked pull request can be tried without writing anything: Without a token the dry run is offline: it prints the branch, the files, the PR title and the labels, and never calls GitHub. It does not write into the clone. -2. **On GitHub, dry run.** Open a Python PR, then run *Linked C# pull request* with its number and `dry_run` ticked (the default). It +2. **On GitHub, dry run.** Open a Python PR, then run *C# PR* with its number and `dry_run` ticked (the default). It makes the App token for `CSharpSchema_Test` only, which proves the installation and permissions, and prints what it would do. 3. **On GitHub, live.** Run it again with `dry_run` unticked. It pushes `python-schema/pr-` to CSharpSchema_Test and opens a draft PR. Run it once more to see that it updates the same branch and PR. When the Python PR is closed without merging, the workflow @@ -127,20 +126,12 @@ TestPyPI is reached with trusted publishing, so no password or token is stored. Real PyPI is added the same way when BHoM/BHoM is in scope. The branch `v10.0` is left alone afterwards. -## The linked pull request status +## When is a Python pull request ready? -Nothing tells this repository when a C# pull request is merged, so `linked-csharp-pr.yml` also sets a commit status on the Python PR -and `linked-csharp-pr-sweep.yml` refreshes it every hour: - -| Status | Meaning | -|---|---| -| pending | the C# draft is open: waiting for it to be merged | -| success | the C# pull request is merged (or the Python change needs no C# change) | -| failure | the C# pull request was closed without merging: change the Python, or close it | - -To stop a Python PR being merged before its C# PR, make the status `linked C# pull request` a **required check** on `develop` -(Settings, Branches). The hourly run only reads the C# repositories with this repository's own token, so they must be public. A -Python PR with no linked draft gets no status from the hourly run. +A Python PR is a proposal: it is ready once its linked C# pull request is merged. The **C# alignment** job of `pr-checks.yml` is the check for +that. It fails until the Python equals what the C# `develop` generates, and passes after. Nothing tells this repository when a C# PR is merged, +so after the merge **re-run that job by hand** (Re-run jobs on the PR page). The comment that `linked-csharp-pr.yml` leaves on the Python PR links +the C# draft. To stop a Python PR being merged first, make **PR / C# alignment** a required check on `develop` (Settings, Branches). ## The alignment check diff --git a/src/bhom_schema/FixtureAlpha_oM/Beam.py b/src/bhom_schema/FixtureAlpha_oM/Beam.py index 41c7048..a3e5ded 100644 --- a/src/bhom_schema/FixtureAlpha_oM/Beam.py +++ b/src/bhom_schema/FixtureAlpha_oM/Beam.py @@ -31,4 +31,3 @@ class Beam(BHoMObject): Start: PointLike | None = None Count: Annotated[int, ClrType('long')] = 3 Small: Annotated[float, ClrType('float')] = 0.5 - Material: str = Field(default='Concrete', description='Name of the material.')