From 0a1b1bb9dab55d22eefb38d6a62596d1228c49b5 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 6 Oct 2026 14:44:40 -0400 Subject: [PATCH 1/2] ci: reuse Python SDK upgrade PRs unless queued --- .github/workflows/posthog-upgrade.yml | 77 ++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/posthog-upgrade.yml b/.github/workflows/posthog-upgrade.yml index 576f83920..87be49c74 100644 --- a/.github/workflows/posthog-upgrade.yml +++ b/.github/workflows/posthog-upgrade.yml @@ -81,14 +81,60 @@ jobs: fi done - - name: Generate branch name - id: generate-branch-name + - name: Generate pull request details + id: generate-pr-details shell: bash env: + GH_TOKEN: ${{ steps.upgrader.outputs.token }} PACKAGE_NAME: ${{ github.event.inputs.package_name }} PACKAGE_VERSION: ${{ github.event.inputs.package_version }} run: | - echo "branch_name=${PACKAGE_NAME}-${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT" + TITLE_PREFIX="chore(deps): update ${PACKAGE_NAME} to " + EXISTING=$(gh pr list \ + --repo PostHog/posthog \ + --state open \ + --search "\"${TITLE_PREFIX}\" in:title" \ + --limit 100 \ + --json title,number,headRefName,headRepositoryOwner \ + --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}\")) | select(.headRepositoryOwner.login == \"PostHog\") | \"\(.number) \(.headRefName)\"" | head -n 1) + EXISTING_PR="${EXISTING%% *}" + EXISTING_BRANCH="${EXISTING#* }" + + # Updating a queued PR's branch restarts the merge queue's checks. + # Leave queued PRs alone, including when their status cannot be read. + QUEUED="" + if [ -n "$EXISTING_PR" ]; then + if TRUNK_STATUS=$(gh api "repos/PostHog/posthog/issues/${EXISTING_PR}/comments?per_page=100" \ + --paginate --slurp \ + --jq '[.[][] | select(.user.login == "trunk-io[bot]") | .body] | last // ""'); then + QUEUED=$(printf '%s' "$TRUNK_STATUS" \ + | grep -oE "Running tests on this pull request|waiting to start tests" | head -n 1 || true) + else + echo "::warning::Could not read the queue status of PR #${EXISTING_PR}; treating it as queued." + QUEUED="unknown" + fi + fi + + if [ -n "$EXISTING_BRANCH" ] && [ -z "$QUEUED" ]; then + BRANCH_NAME="$EXISTING_BRANCH" + elif [ -n "$QUEUED" ]; then + echo "::notice::PR #${EXISTING_PR} is in the merge queue; opening a separate PR instead of force-pushing its branch." + BRANCH_NAME="${PACKAGE_NAME}-upgrade-${PACKAGE_VERSION}" + else + BRANCH_NAME="${PACKAGE_NAME}-upgrade" + fi + + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + echo "title=${TITLE_PREFIX}${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT" + { + echo 'body<> "$GITHUB_OUTPUT" - name: Create main repo pull request id: main-repo-pr @@ -99,16 +145,11 @@ jobs: token: ${{ steps.upgrader.outputs.token }} # PostHog/posthog requires signed commits, so commit through the GitHub API. sign-commits: true - commit-message: "chore(deps): update ${{ github.event.inputs.package_name }} to ${{ github.event.inputs.package_version }}" - branch: ${{ steps.generate-branch-name.outputs.branch_name }} + commit-message: ${{ steps.generate-pr-details.outputs.title }} + branch: ${{ steps.generate-pr-details.outputs.branch_name }} delete-branch: true - title: "chore(deps): update ${{ github.event.inputs.package_name }} to ${{ github.event.inputs.package_version }}" - body: | - ## Changes - - PostHog Python SDK version ${{ github.event.inputs.package_version }} has been released. This updates the `posthoganalytics` and `posthog` dependency declarations to the same SDK version. - - [GitHub releases](https://github.com/PostHog/posthog-python/releases) • [`posthoganalytics` on PyPI](https://pypi.org/project/posthoganalytics/#history) • [`posthog` on PyPI](https://pypi.org/project/posthog/#history) + title: ${{ steps.generate-pr-details.outputs.title }} + body: ${{ steps.generate-pr-details.outputs.body }} - name: Output pull request result shell: bash @@ -119,6 +160,18 @@ jobs: run: | echo "PostHog pull request for $PACKAGE_NAME version $PACKAGE_VERSION ready: $PR_URL" + - name: Update pull request metadata + env: + GH_TOKEN: ${{ steps.upgrader.outputs.token }} + PR_BODY: ${{ steps.generate-pr-details.outputs.body }} + PR_NUMBER: ${{ steps.main-repo-pr.outputs.pull-request-number }} + PR_TITLE: ${{ steps.generate-pr-details.outputs.title }} + run: | + gh pr edit "$PR_NUMBER" \ + --repo PostHog/posthog \ + --title "$PR_TITLE" \ + --body "$PR_BODY" + - name: Assign reviewers env: GH_TOKEN: ${{ steps.upgrader.outputs.token }} From 0759bc141a9ec65e316d0398868f11e06b8ec732 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 6 Oct 2026 17:53:24 -0400 Subject: [PATCH 2/2] ci: protect reused upgrade PRs from stale and queued writes --- .github/workflows/posthog-upgrade.yml | 90 +++- posthog/test/test_posthog_upgrade_workflow.py | 388 ++++++++++++++++++ 2 files changed, 464 insertions(+), 14 deletions(-) create mode 100644 posthog/test/test_posthog_upgrade_workflow.py diff --git a/.github/workflows/posthog-upgrade.yml b/.github/workflows/posthog-upgrade.yml index 87be49c74..19a04aa24 100644 --- a/.github/workflows/posthog-upgrade.yml +++ b/.github/workflows/posthog-upgrade.yml @@ -17,6 +17,10 @@ on: permissions: contents: read +concurrency: + group: posthog-upgrade-${{ github.event.inputs.package_name }} + cancel-in-progress: false + jobs: posthog-upgrade: name: Upgrade PostHog package @@ -90,40 +94,94 @@ jobs: PACKAGE_VERSION: ${{ github.event.inputs.package_version }} run: | TITLE_PREFIX="chore(deps): update ${PACKAGE_NAME} to " + + skip_update() { + echo "::notice::$1" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + } + + # Serialize writers and reject stale reruns using Python package ordering. + check_version() { + local current_title="$1" current_version older + if [[ "$current_title" != "$TITLE_PREFIX"* ]]; then + echo "::error::Cannot determine the destination PR's package version." >&2 + exit 1 + fi + current_version="${current_title#"$TITLE_PREFIX"}" + older=$(uv run --no-project --with packaging==25.0 python - "$PACKAGE_VERSION" "$current_version" <<'PY' + import sys + from packaging.version import Version + + print("true" if Version(sys.argv[1]) < Version(sys.argv[2]) else "false") + PY + ) || { + echo "::error::Cannot compare package versions; leaving remote branches unchanged." >&2 + exit 1 + } + if [ "$older" = "true" ]; then + skip_update "Version $PACKAGE_VERSION is older than PR version $current_version; leaving it unchanged." + fi + } + + queue_status() { + local pr="$1" status + if status=$(gh api "repos/PostHog/posthog/issues/${pr}/comments?per_page=100" \ + --paginate --slurp \ + --jq '[.[][] | select(.user.login == "trunk-io[bot]") | .body] | last // ""'); then + printf '%s' "$status" \ + | grep -oE "Running tests on this pull request|waiting to start tests" | head -n 1 || true + else + echo "::warning::Could not read the queue status of PR #${pr}; treating it as queued." >&2 + echo "unknown" + fi + } + EXISTING=$(gh pr list \ --repo PostHog/posthog \ --state open \ - --search "\"${TITLE_PREFIX}\" in:title" \ + --search "\"${TITLE_PREFIX}\" in:title sort:created-desc" \ --limit 100 \ --json title,number,headRefName,headRepositoryOwner \ - --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}\")) | select(.headRepositoryOwner.login == \"PostHog\") | \"\(.number) \(.headRefName)\"" | head -n 1) - EXISTING_PR="${EXISTING%% *}" - EXISTING_BRANCH="${EXISTING#* }" + --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}\")) | select(.headRepositoryOwner.login == \"PostHog\") | [.number, .headRefName, .title] | @tsv" | head -n 1) + IFS=$'\t' read -r EXISTING_PR EXISTING_BRANCH EXISTING_TITLE <<< "$EXISTING" # Updating a queued PR's branch restarts the merge queue's checks. # Leave queued PRs alone, including when their status cannot be read. QUEUED="" if [ -n "$EXISTING_PR" ]; then - if TRUNK_STATUS=$(gh api "repos/PostHog/posthog/issues/${EXISTING_PR}/comments?per_page=100" \ - --paginate --slurp \ - --jq '[.[][] | select(.user.login == "trunk-io[bot]") | .body] | last // ""'); then - QUEUED=$(printf '%s' "$TRUNK_STATUS" \ - | grep -oE "Running tests on this pull request|waiting to start tests" | head -n 1 || true) - else - echo "::warning::Could not read the queue status of PR #${EXISTING_PR}; treating it as queued." - QUEUED="unknown" - fi + check_version "$EXISTING_TITLE" + QUEUED=$(queue_status "$EXISTING_PR") fi if [ -n "$EXISTING_BRANCH" ] && [ -z "$QUEUED" ]; then BRANCH_NAME="$EXISTING_BRANCH" elif [ -n "$QUEUED" ]; then - echo "::notice::PR #${EXISTING_PR} is in the merge queue; opening a separate PR instead of force-pushing its branch." + echo "::notice::PR #${EXISTING_PR} is in the merge queue; checking a separate branch instead." BRANCH_NAME="${PACKAGE_NAME}-upgrade-${PACKAGE_VERSION}" + # A rerun can collide with an already queued version-specific PR. + if ! DESTINATIONS=$(gh pr list \ + --repo PostHog/posthog \ + --state open \ + --head "$BRANCH_NAME" \ + --limit 100 \ + --json title,number,headRefName,isCrossRepository \ + --jq '.[] | select(.isCrossRepository == false) | [.number, .headRefName, .title] | @tsv'); then + skip_update "Could not check the fallback branch for an existing PR; leaving it unchanged." + fi + while IFS=$'\t' read -r DESTINATION_PR DESTINATION_BRANCH DESTINATION_TITLE; do + [ -n "$DESTINATION_PR" ] || continue + [ "$DESTINATION_BRANCH" = "$BRANCH_NAME" ] || continue + check_version "$DESTINATION_TITLE" + if [ -n "$(queue_status "$DESTINATION_PR")" ]; then + skip_update "Fallback PR #${DESTINATION_PR} is queued or its status is unknown; leaving it unchanged." + fi + done <<< "$DESTINATIONS" else BRANCH_NAME="${PACKAGE_NAME}-upgrade" fi + echo "skip=false" >> "$GITHUB_OUTPUT" echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" echo "title=${TITLE_PREFIX}${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT" { @@ -137,6 +195,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Create main repo pull request + if: steps.generate-pr-details.outputs.skip == 'false' id: main-repo-pr uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 env: @@ -152,6 +211,7 @@ jobs: body: ${{ steps.generate-pr-details.outputs.body }} - name: Output pull request result + if: steps.generate-pr-details.outputs.skip == 'false' shell: bash env: PACKAGE_NAME: ${{ github.event.inputs.package_name }} @@ -161,6 +221,7 @@ jobs: echo "PostHog pull request for $PACKAGE_NAME version $PACKAGE_VERSION ready: $PR_URL" - name: Update pull request metadata + if: steps.generate-pr-details.outputs.skip == 'false' env: GH_TOKEN: ${{ steps.upgrader.outputs.token }} PR_BODY: ${{ steps.generate-pr-details.outputs.body }} @@ -173,6 +234,7 @@ jobs: --body "$PR_BODY" - name: Assign reviewers + if: steps.generate-pr-details.outputs.skip == 'false' env: GH_TOKEN: ${{ steps.upgrader.outputs.token }} PR_NUMBER: ${{ steps.main-repo-pr.outputs.pull-request-number }} diff --git a/posthog/test/test_posthog_upgrade_workflow.py b/posthog/test/test_posthog_upgrade_workflow.py new file mode 100644 index 000000000..a8b91ecaa --- /dev/null +++ b/posthog/test/test_posthog_upgrade_workflow.py @@ -0,0 +1,388 @@ +"""Exercise the inline downstream-upgrade selector without GitHub writes.""" + +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import textwrap + +import pytest + + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/posthog-upgrade.yml" +TITLE_PREFIX = "chore(deps): update posthoganalytics to " +FALLBACK = "posthoganalytics-upgrade-7.64.1" +RUNNING = "Running tests on this pull request" +WAITING = "waiting to start tests" + + +def step_source(name): + return ( + WORKFLOW.read_text() + .split(f" - name: {name}\n", 1)[1] + .split(" - name:", 1)[0] + ) + + +def selector_script(): + return textwrap.dedent( + step_source("Generate pull request details").split(" run: |\n", 1)[1] + ) + + +def pr(number=1, version="7.63.0", branch="posthoganalytics-7.63.0", **kwargs): + return { + "number": number, + "title": TITLE_PREFIX + version, + "headRefName": branch, + "headRepositoryOwner": {"login": "PostHog"}, + "isCrossRepository": False, + "createdAt": number, + **kwargs, + } + + +def comments(*statuses): + return [[{"user": {"login": "trunk-io[bot]"}, "body": s} for s in statuses]] + + +# Execute the workflow's jq filters, not an alternate implementation of them. +# The fake provider sorts only when the request explicitly asks it to. +GH_MOCK = r""" +import json +import os +import subprocess +import sys + +args = sys.argv[1:] +with open(os.environ["MOCK_CALLS"], "a") as file: + file.write(json.dumps(args) + "\n") +fixture = json.loads(os.environ["MOCK_FIXTURE"]) +if args[:2] == ["pr", "list"]: + assert args[args.index("--repo") + 1] == "PostHog/posthog" + assert args[args.index("--state") + 1] == "open" + records = fixture["prs"] + if "--head" in args: + if fixture.get("fallback_fail"): + sys.exit(1) + head = args[args.index("--head") + 1] + records = [p for p in records if p["headRefName"] == head] + else: + if fixture.get("list_fail"): + sys.exit(1) + assert args.index("--search") < args.index("--limit") + search = args[args.index("--search") + 1] + assert '"chore(deps): update posthoganalytics to " in:title' in search + if "sort:created-desc" in search: + records = sorted(records, key=lambda p: p["createdAt"], reverse=True) + records = records[:int(args[args.index("--limit") + 1])] +elif args[0] == "api": + assert "--paginate" in args and "--slurp" in args + number = args[1].split("/")[4] + if int(number) in fixture.get("api_fail", []): + sys.exit(1) + records = fixture.get("comments", {}).get(number, [[]]) +else: + raise AssertionError("Unexpected GitHub call: " + repr(args)) +result = subprocess.run( + ["jq", "-r", args[args.index("--jq") + 1]], + input=json.dumps(records), text=True, capture_output=True, check=True, +) +sys.stdout.write(result.stdout) +""" + +UV_MOCK = """ +import os +import sys + +assert sys.argv[1:6] == ["run", "--no-project", "--with", "packaging==25.0", "python"] +os.execv(sys.executable, [sys.executable, *sys.argv[6:]]) +""" + + +@pytest.fixture +def select(tmp_path): + missing = [tool for tool in ("bash", "jq") if shutil.which(tool) is None] + if missing: + pytest.skip( + f"Install {', '.join(missing)} and make it available on PATH " + "to run the workflow shell-entry tests." + ) + + for name, source in [("gh", GH_MOCK), ("uv", UV_MOCK)]: + executable = tmp_path / name + executable.write_text(f"#!{sys.executable}\n" + source) + executable.chmod(0o755) + + def run(prs=(), version="7.64.1", **fixture): + output = tmp_path / "output" + output.write_text("") + calls = tmp_path / "calls" + calls.write_text("") + env = { + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "PACKAGE_NAME": "posthoganalytics", + "PACKAGE_VERSION": version, + "GITHUB_OUTPUT": str(output), + "MOCK_CALLS": str(calls), + "MOCK_FIXTURE": json.dumps({"prs": prs, **fixture}), + } + result = subprocess.run( + [ + "bash", + "--noprofile", + "--norc", + "-e", + "-o", + "pipefail", + "-c", + selector_script(), + ], + env=env, + text=True, + capture_output=True, + timeout=15, + ) + requests = [json.loads(line) for line in calls.read_text().splitlines()] + # Every scenario must use explicit chronological search, including skips. + assert "sort:created-desc" in requests[0][requests[0].index("--search") + 1] + return result, output.read_text(), requests + + return run + + +def assert_branch(result, branch, version="7.64.1"): + process, output, _ = result + assert process.returncode == 0, process.stderr + assert "skip=false\n" in output + assert f"branch_name={branch}\n" in output + assert f"title={TITLE_PREFIX}{version}\n" in output + assert f"PostHog Python SDK version {version} has been released." in output + assert "`posthoganalytics` and `posthog`" in output + + +def assert_skip(result): + process, output, _ = result + assert process.returncode == 0, process.stderr + assert output == "skip=true\n" + + +def assert_failure(result): + process, output, _ = result + assert process.returncode != 0 + assert output == "" + + +@pytest.mark.parametrize( + "prs,branch", + [ + ([], "posthoganalytics-upgrade"), + ([pr()], "posthoganalytics-7.63.0"), + ([pr(branch="posthoganalytics-upgrade")], "posthoganalytics-upgrade"), + ([pr(headRepositoryOwner={"login": "someone"})], "posthoganalytics-upgrade"), + ([pr(title="chore(deps): update other to 1.0.0")], "posthoganalytics-upgrade"), + ], + ids=["no-pr", "legacy-branch", "stable-branch", "fork", "other-package"], +) +def test_branch_selection(select, prs, branch): + assert_branch(select(prs), branch) + + +@pytest.mark.parametrize("status", [RUNNING, WAITING]) +def test_queued_selected_pr_uses_version_branch(select, status): + assert_branch(select([pr()], comments={"1": comments(status)}), FALLBACK) + + +def test_unknown_selected_queue_status_checks_fallback(select): + result = select([pr()], api_fail=[1]) + assert_branch(result, FALLBACK) + assert any("--head" in call for call in result[2]) + + +def test_pr_lookup_failure_fails_safely(select): + assert_failure(select(list_fail=True)) + + +@pytest.mark.parametrize( + "incoming,current,skip", + [ + ("7.10", "7.9", False), + ("7.9", "7.10", True), + ("7.64.1", "7.64.1", False), + ("7.64.1.0", "7.64.1", False), + ("7.64.1", "7.64.1rc1", False), + ("7.64.1rc1", "7.64.1", True), + ("7.64.1rc2", "7.64.1rc1", False), + ("7.64.1rc1", "7.64.1rc2", True), + ], +) +def test_pep440_version_ordering(select, incoming, current, skip): + result = select([pr(version=current)], version=incoming) + if skip: + assert_skip(result) + else: + assert_branch(result, "posthoganalytics-7.63.0", incoming) + + +@pytest.mark.parametrize( + "incoming,current", [("7.64.1", "unknown"), ("unknown", "7.64.1"), ("7.64.1", "")] +) +def test_unparseable_comparison_fails_safely(select, incoming, current): + assert_failure(select([pr(version=current)], version=incoming)) + + +def test_newer_run_then_older_rerun_preserves_shared_branch_version(select): + existing = pr(branch="posthoganalytics-upgrade") + assert_branch(select([existing]), existing["headRefName"]) + existing["title"] = TITLE_PREFIX + "7.64.1" # Metadata after the newer run. + assert_skip(select([existing], version="7.64.0")) + assert existing["title"] == TITLE_PREFIX + "7.64.1" + + +def test_different_versions_share_reusable_branch(select): + existing = pr(branch="posthoganalytics-upgrade") + for version in ["7.64.0", "7.64.1"]: + assert_branch( + select([existing], version=version), existing["headRefName"], version + ) + + +def test_older_run_skips_even_if_newer_selected_pr_is_queued(select): + assert_skip(select([pr(version="7.64.2")], comments={"1": comments(RUNNING)})) + + +@pytest.mark.parametrize("unknown", [False, True], ids=["queued", "unknown"]) +def test_same_version_fallback_is_protected(select, unknown): + result = select( + [pr(version="7.64.1", branch=FALLBACK)], + comments={"1": comments(RUNNING)}, + api_fail=[1] if unknown else [], + ) + assert_skip(result) + assert any("--head" in call for call in result[2]) + + +@pytest.mark.parametrize("unknown", [False, True], ids=["queued", "unknown"]) +def test_older_fallback_collision_is_independently_protected(select, unknown): + result = select( + [pr(branch=FALLBACK), pr(2, version="7.64.1", branch="newest")], + comments={"1": comments(RUNNING), "2": comments(WAITING)}, + api_fail=[1] if unknown else [], + ) + assert_skip(result) + apis = [call[1] for call in result[2] if call[0] == "api"] + assert apis == [ + "repos/PostHog/posthog/issues/2/comments?per_page=100", + "repos/PostHog/posthog/issues/1/comments?per_page=100", + ] + + +def test_fallback_lookup_failure_skips(select): + assert_skip(select([pr()], comments={"1": comments(RUNNING)}, fallback_fail=True)) + + +def test_unqueued_fallback_is_reused(select): + assert_branch( + select( + [pr(branch=FALLBACK), pr(2, version="7.64.1", branch="newest")], + comments={ + "1": comments(RUNNING, "Removed from queue"), + "2": comments(RUNNING), + }, + ), + FALLBACK, + ) + + +@pytest.mark.parametrize("current", ["7.64.2", "invalid", ""]) +def test_fallback_destination_version_is_checked(select, current): + result = select( + [pr(version=current, branch=FALLBACK), pr(2, branch="newest")], + comments={"2": comments(RUNNING)}, + ) + if current == "7.64.2": + assert_skip(result) + else: + assert_failure(result) + + +def test_fallback_with_unrecognized_title_fails_safely(select): + assert_failure( + select( + [pr(branch=FALLBACK, title="Another change"), pr(2, branch="newest")], + comments={"2": comments(RUNNING)}, + ) + ) + + +def test_fallback_lookup_ignores_cross_repository_pr(select): + assert_branch( + select( + [pr(branch=FALLBACK, isCrossRepository=True), pr(2, branch="newest")], + comments={"1": comments(RUNNING), "2": comments(RUNNING)}, + ), + FALLBACK, + ) + + +def test_search_orders_before_newest_only_queue_selection(select): + result = select([pr(1, branch="older"), pr(2, branch="newest")]) + assert_branch(result, "newest") + assert [call[1] for call in result[2] if call[0] == "api"] == [ + "repos/PostHog/posthog/issues/2/comments?per_page=100" + ] + + +@pytest.mark.parametrize("cleared", [False, True]) +def test_queue_comments_are_paginated_and_latest_trunk_status_wins(select, cleared): + pages = comments(RUNNING) + pages[0].extend([{"user": {"login": "someone"}, "body": RUNNING}] * 99) + pages += comments("Removed from queue" if cleared else WAITING) + assert_branch( + select([pr()], comments={"1": pages}), + "posthoganalytics-7.63.0" if cleared else FALLBACK, + ) + + +def test_other_commenters_do_not_determine_queue_status(select): + assert_branch( + select( + [pr()], comments={"1": [[{"user": {"login": "someone"}, "body": RUNNING}]]} + ), + "posthoganalytics-7.63.0", + ) + + +def test_writers_share_non_cancelling_package_concurrency(): + source = WORKFLOW.read_text() + concurrency = source.split("\nconcurrency:\n", 1)[1].split("\njobs:", 1)[0] + assert ( + "group: posthog-upgrade-${{ github.event.inputs.package_name }}" in concurrency + ) + assert "cancel-in-progress: false" in concurrency + + +def test_remote_mutations_are_gated_and_use_selector_outputs(): + for name in [ + "Create main repo pull request", + "Update pull request metadata", + "Assign reviewers", + ]: + assert "if: steps.generate-pr-details.outputs.skip == 'false'" in step_source( + name + ) + create = step_source("Create main repo pull request") + for field, output in [ + ("branch", "branch_name"), + ("title", "title"), + ("body", "body"), + ]: + assert ( + f"{field}: ${{{{ steps.generate-pr-details.outputs.{output} }}}}" in create + ) + metadata = step_source("Update pull request metadata") + assert "PR_TITLE: ${{ steps.generate-pr-details.outputs.title }}" in metadata + assert "PR_BODY: ${{ steps.generate-pr-details.outputs.body }}" in metadata