Skip to content

Commit

Permalink
Fix Docker release to correctly publish latest and 1.x.latest tags (
Browse files Browse the repository at this point in the history
#9495)

* simplify and modularize tagging logic
* change package field to dropdown, log inputs to publish, skip actual publish for testing
* add dry run option
* update to v3 of docker actions to migrate from node16 (deprecated) to node20
  • Loading branch information
mikealfare authored Jan 31, 2024
1 parent 9c8b28a commit dc3f608
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 172 deletions.
21 changes: 11 additions & 10 deletions .github/actions/latest-wrangler/action.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
name: "Github package 'latest' tag wrangler for containers"
description: "Determines wether or not a given dbt container should be given a bare 'latest' tag (I.E. dbt-core:latest)"
name: "GitHub package `latest` tag wrangler for containers"
description: "Determines if the published image should include `latest` tags"

inputs:
package_name:
description: "Package to check (I.E. dbt-core, dbt-redshift, etc)"
description: "Package being published (i.e. `dbt-core`, `dbt-redshift`, etc.)"
required: true
new_version:
description: "Semver of the container being built (I.E. 1.0.4)"
description: "SemVer of the package being published (i.e. 1.7.2, 1.8.0a1, etc.)"
required: true
gh_token:
description: "Auth token for github (must have view packages scope)"
github_token:
description: "Auth token for GitHub (must have view packages scope)"
required: true

outputs:
latest:
description: "Wether or not built container should be tagged latest (bool)"
minor_latest:
description: "Wether or not built container should be tagged minor.latest (bool)"
tags:
description: "A list of tags to associate with this version"

runs:
using: "docker"
image: "Dockerfile"
155 changes: 64 additions & 91 deletions .github/actions/latest-wrangler/main.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,71 @@
import os
import sys
from packaging.version import Version, parse
import requests
from distutils.util import strtobool
from typing import Union
from packaging.version import parse, Version
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
]

if __name__ == "__main__":

# get inputs
package = os.environ["INPUT_PACKAGE"]
new_version = parse(os.environ["INPUT_NEW_VERSION"])
gh_token = os.environ["INPUT_GH_TOKEN"]
halt_on_missing = strtobool(os.environ.get("INPUT_HALT_ON_MISSING", "False"))

# get package metadata from github
package_request = requests.get(
f"https://api.github.com/orgs/dbt-labs/packages/container/{package}/versions",
auth=("", gh_token),
)
package_meta = package_request.json()

# Log info if we don't get a 200
if package_request.status_code != 200:
print(f"Call to GH API failed: {package_request.status_code} {package_meta['message']}")

# Make an early exit if there is no matching package in github
if package_request.status_code == 404:
if halt_on_missing:
sys.exit(1)
# everything is the latest if the package doesn't exist
github_output = os.environ.get("GITHUB_OUTPUT")
with open(github_output, "at", encoding="utf-8") as gh_output:
gh_output.write("latest=True")
gh_output.write("minor_latest=True")
sys.exit(0)

# TODO: verify package meta is "correct"
# https://github.com/dbt-labs/dbt-core/issues/4640

# map versions and tags
version_tag_map = {
version["id"]: version["metadata"]["container"]["tags"] for version in package_meta
}

# is pre-release
pre_rel = True if any(x in str(new_version) for x in ["a", "b", "rc"]) else False

# semver of current latest
for version, tags in version_tag_map.items():
if "latest" in tags:
# N.B. This seems counterintuitive, but we expect any version tagged
# 'latest' to have exactly three associated tags:
# latest, major.minor.latest, and major.minor.patch.
# Subtracting everything that contains the string 'latest' gets us
# the major.minor.patch which is what's needed for comparison.
current_latest = parse([tag for tag in tags if "latest" not in tag][0])
else:
current_latest = False

# semver of current_minor_latest
for version, tags in version_tag_map.items():
if f"{new_version.major}.{new_version.minor}.latest" in tags:
# Similar to above, only now we expect exactly two tags:
# major.minor.patch and major.minor.latest
current_minor_latest = parse([tag for tag in tags if "latest" not in tag][0])
else:
current_minor_latest = False

def is_latest(
pre_rel: bool, new_version: Version, remote_latest: Union[bool, Version]
) -> bool:
"""Determine if a given contaier should be tagged 'latest' based on:
- it's pre-release status
- it's version
- the version of a previously identified container tagged 'latest'
:param pre_rel: Wether or not the version of the new container is a pre-release
:param new_version: The version of the new container
:param remote_latest: The version of the previously identified container that's
already tagged latest or False
"""
# is a pre-release = not latest
if pre_rel:
return False
# + no latest tag found = is latest
if not remote_latest:
return True
# + if remote version is lower than current = is latest, else not latest
return True if remote_latest <= new_version else False

latest = is_latest(pre_rel, new_version, current_latest)
minor_latest = is_latest(pre_rel, new_version, current_minor_latest)
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"latest={latest}")
gh_output.write(f"minor_latest={minor_latest}")
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()
124 changes: 53 additions & 71 deletions .github/workflows/release-docker.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# **what?**
# This workflow will generate a series of docker images for dbt and push them to the github container registry

#
# **why?**
# Docker images for dbt are used in a number of important places throughout the dbt ecosystem. This is how we keep those images up-to-date.

# Docker images for dbt are used in a number of important places throughout the dbt ecosystem.
# This is how we keep those images up-to-date.
#
# **when?**
# This is triggered manually

# **next steps**
# - build this into the release workflow (or conversly, break out the different release methods into their own workflow files)

name: Docker release

permissions:
Expand All @@ -19,100 +16,85 @@ on:
workflow_dispatch:
inputs:
package:
description: The package to release. _One_ of [dbt-core, dbt-redshift, dbt-bigquery, dbt-snowflake, dbt-spark, dbt-postgres]
required: true
description: The package to release
type: choice
options:
- dbt-core
- dbt-bigquery
- dbt-postgres
- dbt-redshift
- dbt-snowflake
- dbt-spark
required: true
version_number:
description: The release version number (i.e. 1.0.0b1). Do not include `latest` tags or a leading `v`!
required: true
description: The version number to release as a SemVer (e.g. 1.0.0b1, without `latest` or `v`)
required: true
dry_run:
description: Dry Run (don't publish)
type: boolean
default: false

jobs:
get_version_meta:
name: Get version meta
version_metadata:
name: Get version metadata
runs-on: ubuntu-latest
outputs:
major: ${{ steps.version.outputs.major }}
minor: ${{ steps.version.outputs.minor }}
patch: ${{ steps.version.outputs.patch }}
latest: ${{ steps.latest.outputs.latest }}
minor_latest: ${{ steps.latest.outputs.minor_latest }}
fully_qualified_tags: ${{ steps.tags.outputs.fully_qualified_tags }}
steps:
- uses: actions/checkout@v4
- name: Split version
id: version
run: |
IFS="." read -r MAJOR MINOR PATCH <<< ${{ github.event.inputs.version_number }}
echo "major=$MAJOR" >> $GITHUB_OUTPUT
echo "minor=$MINOR" >> $GITHUB_OUTPUT
echo "patch=$PATCH" >> $GITHUB_OUTPUT
- name: Check out the repo
uses: actions/checkout@v4

- name: Is pkg 'latest'
id: latest
- name: Get the tags to publish
id: tags
uses: ./.github/actions/latest-wrangler
with:
package: ${{ github.event.inputs.package }}
new_version: ${{ github.event.inputs.version_number }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
halt_on_missing: False
package_name: ${{ inputs.package }}
new_version: ${{ inputs.version_number }}
github_token: ${{ secrets.GITHUB_TOKEN }}

setup_image_builder:
name: Set up docker image builder
name: Set up Docker image builder
runs-on: ubuntu-latest
needs: [get_version_meta]
needs: [version_metadata]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3

build_and_push:
name: Build images and push to GHCR
runs-on: ubuntu-latest
needs: [setup_image_builder, get_version_meta]
needs: [setup_image_builder, version_metadata]
steps:
- name: Get docker build arg
id: build_arg
run: |
BUILD_ARG_NAME=$(echo ${{ github.event.inputs.package }} | sed 's/\-/_/g')
BUILD_ARG_VALUE=$(echo ${{ github.event.inputs.package }} | sed 's/postgres/core/g')
echo "build_arg_name=$BUILD_ARG_NAME" >> $GITHUB_OUTPUT
echo "build_arg_value=$BUILD_ARG_VALUE" >> $GITHUB_OUTPUT
BUILD_ARG_NAME=$(echo ${{ inputs.package }} | sed 's/\-/_/g')
BUILD_ARG_VALUE=$(echo ${{ inputs.package }} | sed 's/postgres/core/g')
echo "name=$BUILD_ARG_NAME" >> $GITHUB_OUTPUT
echo "value=$BUILD_ARG_VALUE" >> $GITHUB_OUTPUT
- name: Log in to the GHCR
uses: docker/login-action@v2
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push MAJOR.MINOR.PATCH tag
uses: docker/build-push-action@v5
with:
file: docker/Dockerfile
push: True
target: ${{ github.event.inputs.package }}
build-args: |
${{ steps.build_arg.outputs.build_arg_name }}_ref=${{ steps.build_arg.outputs.build_arg_value }}@v${{ github.event.inputs.version_number }}
tags: |
ghcr.io/dbt-labs/${{ github.event.inputs.package }}:${{ github.event.inputs.version_number }}
- name: Build and push MINOR.latest tag
uses: docker/build-push-action@v5
if: ${{ needs.get_version_meta.outputs.minor_latest == 'True' }}
with:
file: docker/Dockerfile
push: True
target: ${{ github.event.inputs.package }}
build-args: |
${{ steps.build_arg.outputs.build_arg_name }}_ref=${{ steps.build_arg.outputs.build_arg_value }}@v${{ github.event.inputs.version_number }}
tags: |
ghcr.io/dbt-labs/${{ github.event.inputs.package }}:${{ needs.get_version_meta.outputs.major }}.${{ needs.get_version_meta.outputs.minor }}.latest
- name: Log publishing configuration
shell: bash
run: |
echo Package: ${{ inputs.package }}
echo Version: ${{ inputs.version_number }}
echo Tags: ${{ needs.version_metadata.outputs.fully_qualified_tags }}
echo Build Arg Name: ${{ steps.build_arg.outputs.name }}
echo Build Arg Value: ${{ steps.build_arg.outputs.value }}
- name: Build and push latest tag
- name: Build and push `${{ inputs.package }}`
if: ${{ !inputs.dry_run }}
uses: docker/build-push-action@v5
if: ${{ needs.get_version_meta.outputs.latest == 'True' }}
with:
file: docker/Dockerfile
push: True
target: ${{ github.event.inputs.package }}
build-args: |
${{ steps.build_arg.outputs.build_arg_name }}_ref=${{ steps.build_arg.outputs.build_arg_value }}@v${{ github.event.inputs.version_number }}
tags: |
ghcr.io/dbt-labs/${{ github.event.inputs.package }}:latest
target: ${{ inputs.package }}
build-args: ${{ steps.build_arg.outputs.name }}_ref=${{ steps.build_arg.outputs.value }}@v${{ inputs.version_number }}
tags: ${{ needs.version_metadata.outputs.fully_qualified_tags }}

0 comments on commit dc3f608

Please sign in to comment.