From 16e89d255e37ec535dd50d07a6ef62964dc64dac Mon Sep 17 00:00:00 2001 From: Emily Rockman Date: Tue, 2 Apr 2024 16:59:37 -0500 Subject: [PATCH] add latest-wrangler (#114) --- .github/actions/latest-wrangler/Dockerfile | 14 ++++ .github/actions/latest-wrangler/README.md | 50 +++++++++++++ .github/actions/latest-wrangler/action.yml | 21 ++++++ .../examples/example_workflow.yml | 26 +++++++ .../examples/example_workflow_dispatch.json | 6 ++ .github/actions/latest-wrangler/main.py | 71 +++++++++++++++++++ .github/workflows/release-docker.yml | 4 +- 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 .github/actions/latest-wrangler/Dockerfile create mode 100644 .github/actions/latest-wrangler/README.md create mode 100644 .github/actions/latest-wrangler/action.yml create mode 100644 .github/actions/latest-wrangler/examples/example_workflow.yml create mode 100644 .github/actions/latest-wrangler/examples/example_workflow_dispatch.json create mode 100644 .github/actions/latest-wrangler/main.py diff --git a/.github/actions/latest-wrangler/Dockerfile b/.github/actions/latest-wrangler/Dockerfile new file mode 100644 index 0000000..4a6c2b9 --- /dev/null +++ b/.github/actions/latest-wrangler/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3-slim AS builder +ADD . /app +WORKDIR /app + +# We are installing a dependency here directly into our app source dir +RUN pip install --target=/app requests packaging + +# A distroless container image with Python and some basics like SSL certificates +# https://github.com/GoogleContainerTools/distroless +FROM gcr.io/distroless/python3-debian10 +COPY --from=builder /app /app +WORKDIR /app +ENV PYTHONPATH /app +CMD ["/app/main.py"] diff --git a/.github/actions/latest-wrangler/README.md b/.github/actions/latest-wrangler/README.md new file mode 100644 index 0000000..fe1fd86 --- /dev/null +++ b/.github/actions/latest-wrangler/README.md @@ -0,0 +1,50 @@ +# Github package 'latest' tag wrangler for containers +## Usage + +Plug in the necessary inputs to determine if the container being built should be tagged 'latest; at the package level, for example `dbt-redshift:latest`. + +## Inputs +| Input | Description | +| - | - | +| `package` | Name of the GH package to check against | +| `new_version` | Semver of new container | +| `gh_token` | GH token with package read scope| +| `halt_on_missing` | Return non-zero exit code if requested package does not exist. (defaults to false)| + + +## Outputs +| Output | Description | +| - | - | +| `latest` | Wether or not the new container should be tagged 'latest'| +| `minor_latest` | Wether or not the new container should be tagged major.minor.latest | + +## Example workflow +```yaml +name: Ship it! +on: + workflow_dispatch: + inputs: + package: + description: The package to publish + required: true + version_number: + description: The version number + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Wrangle latest tag + id: is_latest + uses: dbt-labs/dbt-release/latest-wrangler@main + with: + package: ${{ github.event.inputs.package }} + new_version: ${{ github.event.inputs.new_version }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + - name: Print the results + run: | + echo "Is it latest? Survey says: ${{ steps.is_latest.outputs.latest }} !" + echo "Is it minor.latest? Survey says: ${{ steps.is_latest.outputs.minor_latest }} !" +``` diff --git a/.github/actions/latest-wrangler/action.yml b/.github/actions/latest-wrangler/action.yml new file mode 100644 index 0000000..4ad97da --- /dev/null +++ b/.github/actions/latest-wrangler/action.yml @@ -0,0 +1,21 @@ +name: "GitHub package `latest` tag wrangler for containers" +description: "Determines if the published image should include `latest` tags" + +inputs: + package_name: + description: "Package being published (i.e. `dbt-core`, `dbt-redshift`, etc.)" + required: true + new_version: + description: "SemVer of the package being published (i.e. 1.7.2, 1.8.0a1, etc.)" + required: true + github_token: + description: "Auth token for GitHub (must have view packages scope)" + required: true + +outputs: + tags: + description: "A list of tags to associate with this version" + +runs: + using: "docker" + image: "Dockerfile" diff --git a/.github/actions/latest-wrangler/examples/example_workflow.yml b/.github/actions/latest-wrangler/examples/example_workflow.yml new file mode 100644 index 0000000..b9ac05a --- /dev/null +++ b/.github/actions/latest-wrangler/examples/example_workflow.yml @@ -0,0 +1,26 @@ +name: Ship it! +on: + workflow_dispatch: + inputs: + package: + description: The package to publish + required: true + version_number: + description: The version number + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Wrangle latest tag + id: is_latest + uses: ./.github/actions/latest-wrangler + with: + package: ${{ github.event.inputs.package }} + new_version: ${{ github.event.inputs.new_version }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + - name: Print the results + run: | + echo "Is it latest? Survey says: ${{ steps.is_latest.outputs.latest }} !" diff --git a/.github/actions/latest-wrangler/examples/example_workflow_dispatch.json b/.github/actions/latest-wrangler/examples/example_workflow_dispatch.json new file mode 100644 index 0000000..29667a4 --- /dev/null +++ b/.github/actions/latest-wrangler/examples/example_workflow_dispatch.json @@ -0,0 +1,6 @@ +{ + "inputs": { + "version_number": "1.0.1", + "package": "dbt-redshift" + } +} diff --git a/.github/actions/latest-wrangler/main.py b/.github/actions/latest-wrangler/main.py new file mode 100644 index 0000000..e001abe --- /dev/null +++ b/.github/actions/latest-wrangler/main.py @@ -0,0 +1,71 @@ +import os +from packaging.version import Version, parse +import requests +import sys +from typing import List + + +def main(): + package_name: str = os.environ["INPUT_PACKAGE_NAME"] + new_version: Version = parse(os.environ["INPUT_NEW_VERSION"]) + github_token: str = os.environ["INPUT_GITHUB_TOKEN"] + + response = _package_metadata(package_name, github_token) + published_versions = _published_versions(response) + new_version_tags = _new_version_tags(new_version, published_versions) + _register_tags(new_version_tags, package_name) + + +def _package_metadata(package_name: str, github_token: str) -> requests.Response: + url = f"https://api.github.com/orgs/dbt-labs/packages/container/{package_name}/versions" + return requests.get(url, auth=("", github_token)) + + +def _published_versions(response: requests.Response) -> List[Version]: + package_metadata = response.json() + return [ + parse(tag) + for version in package_metadata + for tag in version["metadata"]["container"]["tags"] + if "latest" not in tag + ] + + +def _new_version_tags(new_version: Version, published_versions: List[Version]) -> List[str]: + # the package version is always a tag + tags = [str(new_version)] + + # pre-releases don't get tagged with `latest` + if new_version.is_prerelease: + return tags + + if new_version > max(published_versions): + tags.append("latest") + + published_patches = [ + version + for version in published_versions + if version.major == new_version.major and version.minor == new_version.minor + ] + if new_version > max(published_patches): + tags.append(f"{new_version.major}.{new_version.minor}.latest") + + return tags + + +def _register_tags(tags: List[str], package_name: str) -> None: + fully_qualified_tags = ",".join([f"ghcr.io/dbt-labs/{package_name}:{tag}" for tag in tags]) + github_output = os.environ.get("GITHUB_OUTPUT") + with open(github_output, "at", encoding="utf-8") as gh_output: + gh_output.write(f"fully_qualified_tags={fully_qualified_tags}") + + +def _validate_response(response: requests.Response) -> None: + message = response["message"] + if response.status_code != 200: + print(f"Call to GitHub API failed: {response.status_code} - {message}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index e6c863d..4e288d9 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -61,7 +61,9 @@ jobs: - name: Get the tags to publish id: tags - uses: ./.github/actions/latest-wrangler + # this cannot be relative because this workflow is called from multiple repos + # in addition to a manual trigger + uses: dbt-labs/dbt-release/latest-wrangler@main with: package_name: ${{ inputs.package }} new_version: ${{ inputs.version_number }}