Skip to content

Commit 4fdba39

Browse files
ci: add github auto-merge support, disable 'skip-ci' marker, use specific tags in commit messages (#61598)
Co-authored-by: Octavia Squidington III <[email protected]>
1 parent 64f6366 commit 4fdba39

File tree

9 files changed

+120
-36
lines changed

9 files changed

+120
-36
lines changed

airbyte-ci/connectors/auto_merge/src/auto_merge/main.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313
from github import Auth, Github
1414

15-
from .consts import AIRBYTE_REPO, AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, AUTO_MERGE_LABEL, BASE_BRANCH, MERGE_METHOD
15+
from .consts import (
16+
AIRBYTE_REPO,
17+
AUTO_MERGE_BYPASS_CI_CHECKS_LABEL,
18+
AUTO_MERGE_LABEL,
19+
BASE_BRANCH,
20+
MERGE_METHOD,
21+
)
1622
from .env import GITHUB_TOKEN, PRODUCTION
1723
from .helpers import generate_job_summary_as_markdown
1824
from .pr_validators import VALIDATOR_MAPPING
@@ -70,11 +76,23 @@ def get_pr_validators(pr: PullRequest) -> set[Callable]:
7076
Returns:
7177
list[callable]: The validators
7278
"""
73-
79+
validators: set[Callable] = set()
7480
for label in pr.labels:
7581
if label.name in VALIDATOR_MAPPING:
76-
return VALIDATOR_MAPPING[label.name]
77-
return VALIDATOR_MAPPING[AUTO_MERGE_LABEL]
82+
# Add these to our validators set:
83+
validators |= VALIDATOR_MAPPING[label.name]
84+
85+
if not validators:
86+
# We shouldn't reach this point, but if we do, we raise an error.
87+
# TODO: We could consider returning a dummy callable which always returns False,
88+
# but for now, we raise an error to ensure we catch any misconfigurations.
89+
raise ValueError(
90+
f"PR #{pr.number} does not have a valid auto-merge label. "
91+
f"Expected one of [{', '.join(VALIDATOR_MAPPING.keys())}], but got: "
92+
f"[{'; '.join(label.name for label in pr.labels)}]",
93+
)
94+
95+
return validators
7896

7997

8098
def merge_with_retries(pr: PullRequest, max_retries: int = 3, wait_time: int = 60) -> Optional[PullRequest]:
@@ -157,7 +175,9 @@ def auto_merge() -> None:
157175
f"repo:{AIRBYTE_REPO} is:pr label:{AUTO_MERGE_LABEL},{AUTO_MERGE_BYPASS_CI_CHECKS_LABEL} base:{BASE_BRANCH} state:open"
158176
)
159177
prs = [issue.as_pull_request() for issue in candidate_issues]
160-
logger.info(f"Found {len(prs)} open PRs targeting {BASE_BRANCH} with the {AUTO_MERGE_LABEL} label")
178+
logger.info(
179+
f"Found {len(prs)} open PRs targeting {BASE_BRANCH} with the '{AUTO_MERGE_LABEL}' or '{AUTO_MERGE_BYPASS_CI_CHECKS_LABEL}' label"
180+
)
161181
merged_prs = []
162182
for pr in prs:
163183
back_off_if_rate_limited(gh_client)

airbyte-ci/connectors/auto_merge/src/auto_merge/pr_validators.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
from typing import TYPE_CHECKING, Callable, Optional, Tuple
66

7-
from .consts import AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, AUTO_MERGE_LABEL, BASE_BRANCH, CONNECTOR_PATH_PREFIXES
7+
from .consts import (
8+
AUTO_MERGE_BYPASS_CI_CHECKS_LABEL,
9+
AUTO_MERGE_LABEL,
10+
BASE_BRANCH,
11+
CONNECTOR_PATH_PREFIXES,
12+
)
813

914
if TYPE_CHECKING:
1015
from github.Commit import Commit as GithubCommit
@@ -71,6 +76,10 @@ def head_commit_passes_all_required_checks(
7176
}
7277
# Let's declare faster checks first as the check_if_pr_is_auto_mergeable function fails fast.
7378
VALIDATOR_MAPPING: dict[str, set[Callable]] = {
79+
# Until we have an auto-approve mechanism, we use this pipeline to force-merge,
80+
# as long as all required checks pass. This doesn't bypass checks but it bypasses the
81+
# approval requirement:
7482
AUTO_MERGE_LABEL: COMMON_VALIDATORS | {has_auto_merge_label, head_commit_passes_all_required_checks},
83+
# These are pure registry updates, and can be auto-merged without any CI checks:
7584
AUTO_MERGE_BYPASS_CI_CHECKS_LABEL: COMMON_VALIDATORS | {has_auto_merge_bypass_ci_checks_label},
7685
}

airbyte-ci/connectors/pipelines/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ airbyte-ci connectors --language=low-code migrate-to-manifest-only
822822

823823
| Version | PR | Description |
824824
| ------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
825+
| 5.3.0 | [#61598](https://github.com/airbytehq/airbyte/pull/61598) | Add trackable commit text and github-native auto-merge in up-to-date, auto-merge, rc-promote, and rc-rollback |
825826
| 5.2.5 | [#60325](https://github.com/airbytehq/airbyte/pull/60325) | Update slack team to oc-extensibility-critical-systems |
826827
| 5.2.4 | [#59724](https://github.com/airbytehq/airbyte/pull/59724) | Fix components mounting and test dependencies for manifest-only unit tests |
827828
| 5.1.0 | [#53238](https://github.com/airbytehq/airbyte/pull/53238) | Add ability to opt out of version increment checks via metadata flag |

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/publish/pipeline.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
4+
from __future__ import annotations
45

56
import json
67
import os
@@ -15,7 +16,15 @@
1516
from airbyte_protocol.models.airbyte_protocol import ConnectorSpecification # type: ignore
1617
from auto_merge.consts import AUTO_MERGE_BYPASS_CI_CHECKS_LABEL # type: ignore
1718
from connector_ops.utils import METADATA_FILE_NAME, ConnectorLanguage # type: ignore
18-
from dagger import Container, Directory, ExecError, File, ImageLayerCompression, Platform, QueryError
19+
from dagger import (
20+
Container,
21+
Directory,
22+
ExecError,
23+
File,
24+
ImageLayerCompression,
25+
Platform,
26+
QueryError,
27+
)
1928
from pydantic import BaseModel, ValidationError
2029

2130
from pipelines import consts
@@ -26,7 +35,10 @@
2635
from pipelines.airbyte_ci.steps.bump_version import SetConnectorVersion
2736
from pipelines.airbyte_ci.steps.changelog import AddChangelogEntry
2837
from pipelines.airbyte_ci.steps.pull_request import CreateOrUpdatePullRequest
29-
from pipelines.airbyte_ci.steps.python_registry import PublishToPythonRegistry, PythonRegistryPublishContext
38+
from pipelines.airbyte_ci.steps.python_registry import (
39+
PublishToPythonRegistry,
40+
PythonRegistryPublishContext,
41+
)
3042
from pipelines.consts import LOCAL_BUILD_PLATFORM
3143
from pipelines.dagger.actions.remote_storage import upload_to_gcs
3244
from pipelines.dagger.actions.system import docker
@@ -635,12 +647,16 @@ def get_rollback_pr_creation_arguments(
635647
step_results: Iterable[StepResult],
636648
release_candidate_version: str,
637649
) -> Tuple[Tuple, Dict]:
638-
return (modified_files,), {
639-
"branch_id": f"{context.connector.technical_name}/rollback-{release_candidate_version}",
640-
"commit_message": "\n".join(step_result.step.title for step_result in step_results if step_result.success),
641-
"pr_title": f"🐙 {context.connector.technical_name}: Stop progressive rollout for {release_candidate_version}",
642-
"pr_body": f"The release candidate version {release_candidate_version} has been deemed unstable. This PR stops its progressive rollout. This PR will be automatically merged as part of the `auto-merge` workflow. This workflow runs every 2 hours.",
643-
}
650+
return (
651+
(modified_files,),
652+
{
653+
"branch_id": f"{context.connector.technical_name}/rollback-{release_candidate_version}",
654+
"commit_message": "[auto-publish] " # << We can skip Vercel builds if this is in the commit message
655+
+ "; ".join(step_result.step.title for step_result in step_results if step_result.success),
656+
"pr_title": f"🐙 {context.connector.technical_name}: Stop progressive rollout for {release_candidate_version}",
657+
"pr_body": f"The release candidate version {release_candidate_version} has been deemed unstable. This PR stops its progressive rollout. This PR will be automatically merged as part of the `auto-merge` workflow. This workflow runs every 2 hours.",
658+
},
659+
)
644660

645661

646662
async def run_connector_rollback_pipeline(context: PublishConnectorContext, semaphore: anyio.Semaphore) -> ConnectorReport:
@@ -675,7 +691,14 @@ async def run_connector_rollback_pipeline(context: PublishConnectorContext, sema
675691

676692
# Open PR when all previous steps are successful
677693
initial_pr_creation = CreateOrUpdatePullRequest(
678-
context, skip_ci=True, labels=[AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, "rollback-rc"]
694+
context,
695+
# We will merge even if the CI checks fail, due to this label:
696+
labels=[AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, "rollback-rc"],
697+
# Let GitHub auto-merge this if all checks pass before the next run
698+
# of our auto-merge workflow:
699+
github_auto_merge=True,
700+
# Don't skip CI, as it prevents the PR from auto-merging naturally:
701+
skip_ci=False,
679702
)
680703
pr_creation_args, pr_creation_kwargs = get_rollback_pr_creation_arguments(all_modified_files, context, results, current_version)
681704
initial_pr_creation_result = await initial_pr_creation.run(*pr_creation_args, **pr_creation_kwargs)
@@ -692,12 +715,16 @@ def get_promotion_pr_creation_arguments(
692715
release_candidate_version: str,
693716
promoted_version: str,
694717
) -> Tuple[Tuple, Dict]:
695-
return (modified_files,), {
696-
"branch_id": f"{context.connector.technical_name}/{promoted_version}",
697-
"commit_message": "\n".join(step_result.step.title for step_result in step_results if step_result.success),
698-
"pr_title": f"🐙 {context.connector.technical_name}: release {promoted_version}",
699-
"pr_body": f"The release candidate version {release_candidate_version} has been deemed stable and is now ready to be promoted to an official release ({promoted_version}). This PR will be automatically merged as part of the `auto-merge` workflow. This workflow runs every 2 hours.",
700-
}
718+
return (
719+
(modified_files,),
720+
{
721+
"branch_id": f"{context.connector.technical_name}/{promoted_version}",
722+
"commit_message": "[auto-publish] " # << We can skip Vercel builds if this is in the commit message
723+
+ "; ".join(step_result.step.title for step_result in step_results if step_result.success),
724+
"pr_title": f"🐙 {context.connector.technical_name}: release {promoted_version}",
725+
"pr_body": f"The release candidate version {release_candidate_version} has been deemed stable and is now ready to be promoted to an official release ({promoted_version}). This PR will be automatically merged as part of the `auto-merge` workflow. This workflow runs every 2 hours.",
726+
},
727+
)
701728

702729

703730
async def run_connector_promote_pipeline(context: PublishConnectorContext, semaphore: anyio.Semaphore) -> ConnectorReport:
@@ -742,7 +769,7 @@ async def run_connector_promote_pipeline(context: PublishConnectorContext, semap
742769

743770
# Open PR when all previous steps are successful
744771
promoted_version = set_promoted_version.promoted_version
745-
initial_pr_creation = CreateOrUpdatePullRequest(context, skip_ci=True)
772+
initial_pr_creation = CreateOrUpdatePullRequest(context, skip_ci=False)
746773
pr_creation_args, pr_creation_kwargs = get_promotion_pr_creation_arguments(
747774
all_modified_files, context, results, current_version, promoted_version
748775
)
@@ -768,7 +795,11 @@ async def run_connector_promote_pipeline(context: PublishConnectorContext, semap
768795
await add_changelog_entry.export_modified_files(context.connector.local_connector_documentation_directory)
769796
)
770797
post_changelog_pr_update = CreateOrUpdatePullRequest(
771-
context, skip_ci=True, labels=[AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, "promoted-rc"]
798+
context,
799+
skip_ci=False, # Don't skip CI, as it prevents the PR from auto-merging naturally.
800+
# We will merge even if the CI checks fail, due to the "bypass-ci-checks" label:
801+
labels=[AUTO_MERGE_BYPASS_CI_CHECKS_LABEL, "promoted-rc"],
802+
github_auto_merge=True, # Let GitHub auto-merge this if/when all required checks have passed.
772803
)
773804
pr_creation_args, pr_creation_kwargs = get_promotion_pr_creation_arguments(
774805
all_modified_files, context, results, current_version, promoted_version

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/up_to_date/pipeline.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,16 @@ def get_pr_creation_arguments(
6262
step_results: Iterable[StepResult],
6363
dependency_updates: Iterable[DependencyUpdate],
6464
) -> Tuple[Tuple, Dict]:
65-
return (modified_files,), {
66-
"branch_id": f"up-to-date/{context.connector.technical_name}",
67-
"commit_message": "\n".join(step_result.step.title for step_result in step_results if step_result.success),
68-
"pr_title": f"🐙 {context.connector.technical_name}: run up-to-date pipeline [{datetime.now(timezone.utc).strftime('%Y-%m-%d')}]",
69-
"pr_body": get_pr_body(context, step_results, dependency_updates),
70-
}
65+
return (
66+
(modified_files,),
67+
{
68+
"branch_id": f"up-to-date/{context.connector.technical_name}",
69+
"commit_message": "[up-to-date]" # << We can skip Vercel builds if this is in the commit message
70+
+ "; ".join(step_result.step.title for step_result in step_results if step_result.success),
71+
"pr_title": f"🐙 {context.connector.technical_name}: run up-to-date pipeline [{datetime.now(timezone.utc).strftime('%Y-%m-%d')}]",
72+
"pr_body": get_pr_body(context, step_results, dependency_updates),
73+
},
74+
)
7175

7276

7377
## MAIN FUNCTION
@@ -143,10 +147,13 @@ async def run_connector_up_to_date_pipeline(
143147

144148
# We open a PR even if build is failing.
145149
# This might allow a developer to fix the build in the PR.
146-
# ---
147-
# We are skipping CI on this first PR creation attempt to avoid useless runs:
148-
# the new changelog entry is missing, it will fail QA checks
149-
initial_pr_creation = CreateOrUpdatePullRequest(context, skip_ci=True, labels=DEFAULT_PR_LABELS)
150+
initial_pr_creation = CreateOrUpdatePullRequest(
151+
context,
152+
labels=DEFAULT_PR_LABELS,
153+
# Reduce pressure on rate limit, since we need to push a
154+
# a follow-on commit anyway once we have the PR number:
155+
skip_ci=True,
156+
)
150157
pr_creation_args, pr_creation_kwargs = get_pr_creation_arguments(
151158
all_modified_files, context, step_results, dependency_updates
152159
)
@@ -176,7 +183,15 @@ async def run_connector_up_to_date_pipeline(
176183
context.logger.info(f"Exported files following the changelog entry: {exported_modified_files}")
177184
all_modified_files.update(exported_modified_files)
178185
final_labels = DEFAULT_PR_LABELS + [AUTO_MERGE_PR_LABEL] if auto_merge else DEFAULT_PR_LABELS
179-
post_changelog_pr_update = CreateOrUpdatePullRequest(context, skip_ci=False, labels=final_labels)
186+
post_changelog_pr_update = CreateOrUpdatePullRequest(
187+
context,
188+
labels=final_labels,
189+
# For this 'up-to-date' pipeline, we want GitHub to merge organically
190+
# if/when all required checks pass. Maintainers can also easily disable
191+
# auto-merge if they want to review or update the PR before merging.
192+
github_auto_merge=auto_merge,
193+
skip_ci=False,
194+
)
180195
pr_creation_args, pr_creation_kwargs = get_pr_creation_arguments(
181196
all_modified_files, context, step_results, dependency_updates
182197
)

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/up_to_date/steps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class PoetryUpdate(StepModifyingFiles):
2727
context: ConnectorContext
2828
dev: bool
2929
specified_versions: dict[str, str]
30-
title = "Update versions of libraries in poetry."
30+
title = "Update versions of libraries in poetry"
3131

3232
def __init__(
3333
self,

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/steps/pull_request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ def __init__(
2424
context: PipelineContext,
2525
skip_ci: bool,
2626
labels: Optional[Iterable[str]] = None,
27+
github_auto_merge: bool = False,
2728
) -> None:
2829
super().__init__(context)
2930
self.skip_ci = skip_ci
3031
self.labels = labels or []
32+
self.github_auto_merge = github_auto_merge
3133

3234
async def _run(
3335
self,
@@ -51,6 +53,7 @@ async def _run(
5153
logger=self.logger,
5254
skip_ci=self.skip_ci,
5355
labels=self.labels,
56+
github_auto_merge=self.github_auto_merge,
5457
)
5558
except Exception as e:
5659
return StepResult(step=self, status=StepStatus.FAILURE, stderr=str(e), exc_info=e)

airbyte-ci/connectors/pipelines/pipelines/helpers/github.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def create_or_update_github_pull_request(
137137
skip_ci: bool = False,
138138
labels: Optional[Iterable[str]] = None,
139139
force_push: bool = True,
140+
github_auto_merge: bool = False,
140141
) -> github_sdk.PullRequest.PullRequest:
141142
logger = logger or main_logger
142143
g = github_sdk.Github(auth=github_sdk.Auth.Token(github_token))
@@ -222,6 +223,10 @@ def create_or_update_github_pull_request(
222223
pull_request.add_to_labels(label)
223224
logger.info(f"Added label {label} to pull request")
224225

226+
if github_auto_merge:
227+
logger.info("Enabling (native) GitHub auto-merge for the pull request")
228+
pull_request.enable_automerge("SQUASH")
229+
225230
return pull_request
226231

227232

airbyte-ci/connectors/pipelines/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pipelines"
7-
version = "5.2.5"
7+
version = "5.3.0"
88
description = "Packaged maintained by the connector operations team to perform CI for connectors' pipelines"
99
authors = ["Airbyte <[email protected]>"]
1010

0 commit comments

Comments
 (0)