Skip to content

Commit d6529c6

Browse files
Joe Reuterjatinyadav-cc
Joe Reuter
authored andcommitted
airbyte-ci: Add pypi publishing logic (airbytehq#34111)
1 parent 6e42622 commit d6529c6

File tree

19 files changed

+746
-27
lines changed

19 files changed

+746
-27
lines changed

.github/actions/run-dagger-pipeline/action.yml

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ inputs:
8383
description: "URL to airbyte-ci binary"
8484
required: false
8585
default: https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci
86+
python_registry_token:
87+
description: "Python registry API token to publish python package"
88+
required: false
8689

8790
runs:
8891
using: "composite"
@@ -182,3 +185,4 @@ runs:
182185
CI: "True"
183186
TAILSCALE_AUTH_KEY: ${{ inputs.tailscale_auth_key }}
184187
DOCKER_REGISTRY_MIRROR_URL: ${{ inputs.docker_registry_mirror_url }}
188+
PYTHON_REGISTRY_TOKEN: ${{ inputs.python_registry_token }}

.github/workflows/publish_connectors.yml

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
s3_build_cache_secret_key: ${{ secrets.SELF_RUNNER_AWS_SECRET_ACCESS_KEY }}
6464
tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }}
6565
subcommand: "connectors --concurrency=1 --execute-timeout=3600 --metadata-changes-only publish --main-release"
66+
python_registry_token: ${{ secrets.PYPI_TOKEN }}
6667

6768
- name: Publish connectors [manual]
6869
id: publish-connectors
@@ -84,6 +85,7 @@ jobs:
8485
s3_build_cache_secret_key: ${{ secrets.SELF_RUNNER_AWS_SECRET_ACCESS_KEY }}
8586
tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }}
8687
subcommand: "connectors ${{ github.event.inputs.connectors-options }} publish ${{ github.event.inputs.publish-options }}"
88+
python_registry_token: ${{ secrets.PYPI_TOKEN }}
8789

8890
set-instatus-incident-on-failure:
8991
name: Create Instatus Incident on Failure

airbyte-ci/connectors/pipelines/README.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,37 @@ This command runs formatting checks and reformats any code that would be reforma
491491

492492
Running `airbyte-ci format fix all` will format all of the different types of code. Run `airbyte-ci format fix --help` for subcommands to format only certain types of files.
493493

494+
### <a id="poetry-subgroup"></a>`poetry` command subgroup
495+
496+
Available commands:
497+
498+
- `airbyte-ci poetry publish`
499+
500+
### Options
501+
502+
| Option | Required | Default | Mapped environment variable | Description |
503+
| ------------------- | -------- | ------- | --------------------------- | ------------------------------------------------------------------------------------------- |
504+
| `--package-path` | True | | | The path to the python package to execute a poetry command on. |
505+
506+
### Examples
507+
508+
- Publish a python package: `airbyte-ci poetry --package-path=path/to/package publish --publish-name=my-package --publish-version="1.2.3" --python-registry-token="..." --registry-url="http://host.docker.internal:8012/"`
509+
510+
### <a id="format-check-command"></a>`publish` command
511+
512+
This command publishes poetry packages (using `pyproject.toml`) or python packages (using `setup.py`) to a python registry.
513+
514+
For poetry packages, the package name and version can be taken from the `pyproject.toml` file or be specified as options.
515+
516+
#### Options
517+
518+
| Option | Required | Default | Mapped environment variable | Description |
519+
| ------------------------- | -------- | ----------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------- |
520+
| `--publish-name` | False | | | The name of the package. Not required for poetry packages that define it in the `pyproject.toml` file |
521+
| `--publish-version` | False | | | The version of the package. Not required for poetry packages that define it in the `pyproject.toml` file |
522+
| `--python-registry-token` | True | | PYTHON_REGISTRY_TOKEN | The API token to authenticate with the registry. For pypi, the `pypi-` prefix needs to be specified |
523+
| `--registry-url` | False | https://pypi.org/simple | | The python registry to publish to. Defaults to main pypi |
524+
494525
### <a id="metadata-validate-command-subgroup"></a>`metadata` command subgroup
495526

496527
Available commands:
@@ -547,7 +578,8 @@ E.G.: running `pytest` on a specific test folder:
547578

548579
| Version | PR | Description |
549580
| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
550-
| 3.5.3 | [#34339](https://github.com/airbytehq/airbyte/pull/34339) | only do minimal changes on a connector version_bump |
581+
| 3.6.0 | [#34111](https://github.com/airbytehq/airbyte/pull/34111) | Add python registry publishing |
582+
| 3.5.3 | [#34339](https://github.com/airbytehq/airbyte/pull/34339) | only do minimal changes on a connector version_bump |
551583
| 3.5.2 | [#34381](https://github.com/airbytehq/airbyte/pull/34381) | Bind a sidecar docker host for `airbyte-ci test` |
552584
| 3.5.1 | [#34321](https://github.com/airbytehq/airbyte/pull/34321) | Upgrade to Dagger 0.9.6 . |
553585
| 3.5.0 | [#33313](https://github.com/airbytehq/airbyte/pull/33313) | Pass extra params after Gradle tasks. |

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,16 @@ def metadata_service_gcs_credentials_secret(self) -> Secret:
8989
def spec_cache_gcs_credentials_secret(self) -> Secret:
9090
return self.dagger_client.set_secret("spec_cache_gcs_credentials", self.spec_cache_gcs_credentials)
9191

92+
@property
93+
def pre_release_suffix(self) -> str:
94+
return self.git_revision[:10]
95+
9296
@property
9397
def docker_image_tag(self) -> str:
9498
# get the docker image tag from the parent class
9599
metadata_tag = super().docker_image_tag
96100
if self.pre_release:
97-
return f"{metadata_tag}-dev.{self.git_revision[:10]}"
101+
return f"{metadata_tag}-dev.{self.pre_release_suffix}"
98102
else:
99103
return metadata_tag
100104

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

+56
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
from pipelines.airbyte_ci.connectors.publish.context import PublishConnectorContext
1515
from pipelines.airbyte_ci.connectors.reports import ConnectorReport
1616
from pipelines.airbyte_ci.metadata.pipeline import MetadataUpload, MetadataValidation
17+
from pipelines.airbyte_ci.steps.python_registry import PublishToPythonRegistry, PythonRegistryPublishContext
1718
from pipelines.dagger.actions.remote_storage import upload_to_gcs
1819
from pipelines.dagger.actions.system import docker
20+
from pipelines.helpers.pip import is_package_published
1921
from pipelines.models.steps import Step, StepResult, StepStatus
2022
from pydantic import ValidationError
2123

@@ -52,6 +54,28 @@ async def _run(self) -> StepResult:
5254
return StepResult(self, status=StepStatus.SUCCESS, stdout=f"No manifest found for {self.context.docker_image}.")
5355

5456

57+
class CheckPythonRegistryPackageDoesNotExist(Step):
58+
context: PythonRegistryPublishContext
59+
title = "Check if the connector is published on python registry"
60+
61+
async def _run(self) -> StepResult:
62+
is_published = is_package_published(
63+
self.context.package_metadata.name, self.context.package_metadata.version, self.context.registry
64+
)
65+
if is_published:
66+
return StepResult(
67+
self,
68+
status=StepStatus.SKIPPED,
69+
stderr=f"{self.context.package_metadata.name} already exists in version {self.context.package_metadata.version}.",
70+
)
71+
else:
72+
return StepResult(
73+
self,
74+
status=StepStatus.SUCCESS,
75+
stdout=f"{self.context.package_metadata.name} does not exist in version {self.context.package_metadata.version}.",
76+
)
77+
78+
5579
class PushConnectorImageToRegistry(Step):
5680
context: PublishConnectorContext
5781
title = "Push connector image to registry"
@@ -259,6 +283,11 @@ def create_connector_report(results: List[StepResult]) -> ConnectorReport:
259283
check_connector_image_results = await CheckConnectorImageDoesNotExist(context).run()
260284
results.append(check_connector_image_results)
261285

286+
python_registry_steps, terminate_early = await _run_python_registry_publish_pipeline(context)
287+
results.extend(python_registry_steps)
288+
if terminate_early:
289+
return create_connector_report(results)
290+
262291
# If the connector image already exists, we don't need to build it, but we still need to upload the metadata file.
263292
# We also need to upload the spec to the spec cache bucket.
264293
if check_connector_image_results.status is StepStatus.SKIPPED:
@@ -312,6 +341,33 @@ def create_connector_report(results: List[StepResult]) -> ConnectorReport:
312341
return connector_report
313342

314343

344+
async def _run_python_registry_publish_pipeline(context: PublishConnectorContext) -> Tuple[List[StepResult], bool]:
345+
"""
346+
Run the python registry publish pipeline for a single connector.
347+
Return the results of the steps and a boolean indicating whether there was an error and the pipeline should be stopped.
348+
"""
349+
results: List[StepResult] = []
350+
# Try to convert the context to a PythonRegistryPublishContext. If it returns None, it means we don't need to publish to a python registry.
351+
python_registry_context = await PythonRegistryPublishContext.from_publish_connector_context(context)
352+
if not python_registry_context:
353+
return results, False
354+
355+
check_python_registry_package_exists_results = await CheckPythonRegistryPackageDoesNotExist(python_registry_context).run()
356+
results.append(check_python_registry_package_exists_results)
357+
if check_python_registry_package_exists_results.status is StepStatus.SKIPPED:
358+
context.logger.info("The connector version is already published on python registry.")
359+
elif check_python_registry_package_exists_results.status is StepStatus.SUCCESS:
360+
context.logger.info("The connector version is not published on python registry. Let's build and publish it.")
361+
publish_to_python_registry_results = await PublishToPythonRegistry(python_registry_context).run()
362+
results.append(publish_to_python_registry_results)
363+
if publish_to_python_registry_results.status is StepStatus.FAILURE:
364+
return results, True
365+
elif check_python_registry_package_exists_results.status is StepStatus.FAILURE:
366+
return results, True
367+
368+
return results, False
369+
370+
315371
def reorder_contexts(contexts: List[PublishConnectorContext]) -> List[PublishConnectorContext]:
316372
"""Reorder contexts so that the ones that are for strict-encrypt/secure connectors come first.
317373
The metadata upload on publish checks if the the connectors referenced in the metadata file are already published to DockerHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
"""
6+
Module exposing the format commands.
7+
"""
8+
from __future__ import annotations
9+
10+
import asyncclick as click
11+
from pipelines.cli.click_decorators import click_ignore_unused_kwargs, click_merge_args_into_context_obj
12+
from pipelines.cli.lazy_group import LazyGroup
13+
from pipelines.models.contexts.click_pipeline_context import ClickPipelineContext, pass_pipeline_context
14+
15+
16+
@click.group(
17+
name="poetry",
18+
help="Commands related to running poetry commands.",
19+
cls=LazyGroup,
20+
lazy_subcommands={
21+
"publish": "pipelines.airbyte_ci.poetry.publish.commands.publish",
22+
},
23+
)
24+
@click.option(
25+
"--package-path",
26+
help="The path to publish",
27+
type=click.STRING,
28+
required=True,
29+
)
30+
@click_merge_args_into_context_obj
31+
@pass_pipeline_context
32+
@click_ignore_unused_kwargs
33+
async def poetry(pipeline_context: ClickPipelineContext) -> None:
34+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
"""
6+
Module exposing the format commands.
7+
"""
8+
from __future__ import annotations
9+
10+
from typing import Optional
11+
12+
import asyncclick as click
13+
from packaging import version
14+
from pipelines.airbyte_ci.steps.python_registry import PublishToPythonRegistry
15+
from pipelines.cli.confirm_prompt import confirm
16+
from pipelines.cli.dagger_pipeline_command import DaggerPipelineCommand
17+
from pipelines.consts import DEFAULT_PYTHON_PACKAGE_REGISTRY_URL
18+
from pipelines.models.contexts.click_pipeline_context import ClickPipelineContext, pass_pipeline_context
19+
from pipelines.models.contexts.python_registry_publish import PythonRegistryPublishContext
20+
from pipelines.models.steps import StepStatus
21+
22+
23+
async def _has_metadata_yaml(context: PythonRegistryPublishContext) -> bool:
24+
dir_to_publish = context.get_repo_dir(context.package_path)
25+
return "metadata.yaml" in await dir_to_publish.entries()
26+
27+
28+
def _validate_python_version(_ctx: dict, _param: dict, value: Optional[str]) -> Optional[str]:
29+
"""
30+
Check if an given version is valid.
31+
"""
32+
if value is None:
33+
return value
34+
try:
35+
version.Version(value)
36+
return value
37+
except version.InvalidVersion:
38+
raise click.BadParameter(f"Version {value} is not a valid version.")
39+
40+
41+
@click.command(cls=DaggerPipelineCommand, name="publish", help="Publish a Python package to a registry.")
42+
@click.option(
43+
"--python-registry-token",
44+
help="Access token",
45+
type=click.STRING,
46+
required=True,
47+
envvar="PYTHON_REGISTRY_TOKEN",
48+
)
49+
@click.option(
50+
"--registry-url",
51+
help="Which registry to publish to. If not set, the default pypi is used. For test pypi, use https://test.pypi.org/legacy/",
52+
type=click.STRING,
53+
default=DEFAULT_PYTHON_PACKAGE_REGISTRY_URL,
54+
)
55+
@click.option(
56+
"--publish-name",
57+
help="The name of the package to publish. If not set, the name will be inferred from the pyproject.toml file of the package.",
58+
type=click.STRING,
59+
)
60+
@click.option(
61+
"--publish-version",
62+
help="The version of the package to publish. If not set, the version will be inferred from the pyproject.toml file of the package.",
63+
type=click.STRING,
64+
callback=_validate_python_version,
65+
)
66+
@pass_pipeline_context
67+
@click.pass_context
68+
async def publish(
69+
ctx: click.Context,
70+
click_pipeline_context: ClickPipelineContext,
71+
python_registry_token: str,
72+
registry_url: str,
73+
publish_name: Optional[str],
74+
publish_version: Optional[str],
75+
) -> bool:
76+
context = PythonRegistryPublishContext(
77+
is_local=ctx.obj["is_local"],
78+
git_branch=ctx.obj["git_branch"],
79+
git_revision=ctx.obj["git_revision"],
80+
ci_report_bucket=ctx.obj["ci_report_bucket_name"],
81+
report_output_prefix=ctx.obj["report_output_prefix"],
82+
gha_workflow_run_url=ctx.obj.get("gha_workflow_run_url"),
83+
dagger_logs_url=ctx.obj.get("dagger_logs_url"),
84+
pipeline_start_timestamp=ctx.obj.get("pipeline_start_timestamp"),
85+
ci_context=ctx.obj.get("ci_context"),
86+
ci_gcs_credentials=ctx.obj["ci_gcs_credentials"],
87+
python_registry_token=python_registry_token,
88+
registry=registry_url,
89+
package_path=ctx.obj["package_path"],
90+
package_name=publish_name,
91+
version=publish_version,
92+
)
93+
94+
dagger_client = await click_pipeline_context.get_dagger_client(pipeline_name=f"Publish {ctx.obj['package_path']} to python registry")
95+
context.dagger_client = dagger_client
96+
97+
if await _has_metadata_yaml(context):
98+
confirm(
99+
"It looks like you are trying to publish a connector. In most cases, the `connectors` command group should be used instead. Do you want to continue?",
100+
abort=True,
101+
)
102+
103+
publish_result = await PublishToPythonRegistry(context).run()
104+
105+
return publish_result.status is StepStatus.SUCCESS

0 commit comments

Comments
 (0)