Repository: dbt-labs/dbt-core

Issue:
[CT-2288] [Bug] Latest containers are not released and the wrong versions are marked `latest`
### Current Behavior

We ran the workflow to generate the new containers for v1.4.4: [workflow output](https://github.com/dbt-labs/dbt-core/actions/runs/4308505619)

There were unexpected inputs found when trying to determine if this version is the latest (it is) in [this step](https://github.com/dbt-labs/dbt-core/actions/runs/4308505619/jobs/7514856225)

Which caused the build and publish steps to get skipped [here](https://github.com/dbt-labs/dbt-core/actions/runs/4308505619/jobs/7514867098)

We have not fully released `dbt-postgres` specifically.

Additionally the wrong package is marked as latest

![Screen Shot 2023-03-09 at 2 59 03 PM](https://user-images.githubusercontent.com/7070049/224156487-3fdaf7bf-b289-49c8-88a6-a3f6bffcbc78.png)

View all packages [here](https://github.com/orgs/dbt-labs/packages)

### Expected Behavior

1. everything gets released, and if it doesn't we get some kind of error
2. the correct version gets marked as `latest`

### Steps To Reproduce

Run [release-docker.yml](https://github.com/dbt-labs/dbt-core/blob/main/.github/workflows/release-docker.yml)

### Additional Context

Current Package Versions

| Package | Version Marked Latest | Most up to Date Package Version | Latest GitHub Release Version |
| ----- | ----- | ----- | ----- |
| dbt-core | 1.3.2 | 1.4.4 | n/a|
| dbt-postgres | 1.2.3 | 1.4.1 | 1.4.4|
| dbt-snowflake | 1.3.latest / 1.3.0 | 1.4.1 | 1.4.1 |
| dbt-redshift | 1.3.latest / 1.3.0 | 1.4.0 | 1.4.0 |
| dbt-bigquery | 1.3.latest / 1.3.0 | 1.4.1 | 1.4.1 |
| dbt-spark | 1.3.latest / 1.3.0 | 1.4.1 | 1.4.1 |


Discussion:
I am looking to add some CI to my dbt project and this is a blocker for me. I am on v1.4.6 and now need to look at reverting to v1.4.3 to get my project added to CI
Hi. Same here... Thanks
This issue is still partly happening, `dbt-postgres` images are not being published since 1.7.2. It might be related to the current moving of `dbt-postgres` out of `dbt-core` though.
`dbt-postgres` is back, so not affected anymore, sorry for the noise.

Resolving patch:
diff --git a/.github/actions/latest-wrangler/main.py b/.github/actions/latest-wrangler/main.py
--- a/.github/actions/latest-wrangler/main.py
+++ b/.github/actions/latest-wrangler/main.py
@@ -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()


Test patch: