Giter VIP home page Giter VIP logo

containers's People

Contributors

moki-bot[bot] avatar qlonik avatar

Watchers

 avatar  avatar

containers's Issues

Upstream Differences Dashboard ๐Ÿค–

This issue show differences in github action workflows between this repository
and upstream repository onedr0p/containers.

Differences in files

.github/CODE_OF_CONDUCT.md
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
deleted file mode 100644
index 915019a..0000000
--- a/.github/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, religion, or sexual identity
-and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
-  and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the
-  overall community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or
-  advances of any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email
-  address, without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
-posting via an official social media account, or acting as an appointed
-representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at [email protected].
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series
-of actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or
-permanent ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior,  harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within
-the community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
-
-Community Impact Guidelines were inspired by [Mozilla's code of conduct
-enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
.github/actions/collect-changes/action.yaml
diff --git a/.github/actions/collect-changes/action.yaml b/.github/actions/collect-changes/action.yaml
new file mode 100644
index 0000000..4c1f504
--- /dev/null
+++ b/.github/actions/collect-changes/action.yaml
@@ -0,0 +1,34 @@
+name: "Collect changes"
+description: "Collects and stores changed files/containers"
+
+outputs:
+  changesDetected:
+    description: "Whether or not changes to containers have been detected"
+    value: ${{ steps.filter.outputs.addedOrModified }}
+  addedOrModifiedFiles:
+    description: "A list of the files changed"
+    value: ${{ steps.filter.outputs.addedOrModified_files }}
+  addedOrModifiedImages:
+    description: "A list of the containers changed"
+    value: ${{ steps.filter-containers.outputs.addedOrModifiedImages }}
+
+runs:
+  using: "composite"
+  steps:
+    - name: Collect changed files
+      uses: dorny/paths-filter@v2
+      id: filter
+      with:
+        list-files: json
+        filters: |
+          addedOrModified:
+            - added|modified: 'apps/*/**'
+    - name: Collect changed containers
+      if: |
+        steps.filter.outputs.addedOrModified == 'true'
+      id: filter-containers
+      shell: bash
+      run: |
+        PATHS='${{ steps.filter.outputs.addedOrModified_files }}'
+        OUTPUT=$(echo $PATHS | jq --raw-output -c 'map(. |= split("/")[1]) | unique')
+        echo "addedOrModifiedImages=${OUTPUT}" >> $GITHUB_OUTPUT
.github/labels.yaml
diff --git a/.github/labels.yaml b/.github/labels.yaml
deleted file mode 100644
index 262ff7b..0000000
--- a/.github/labels.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-- name: renovate/container
-  color: "027fa0"
-- name: type/patch
-  color: "ffeC19"
-- name: type/minor
-  color: "ff9800"
-- name: type/major
-  color: "f6412d"
-- name: hold
-  color: "ee0701"
.github/renovate.json5
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 0df5cb6..6d11058 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -1,50 +1,34 @@
 {
-  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
   "extends": [
-    "config:recommended",
+    "config:base",
     "docker:enableMajor",
-    ":dependencyDashboard",
     ":disableRateLimiting",
-    ":timezone(America/New_York)",
-    ":semanticCommits"
+    ":semanticCommits",
+    ":automergeDigest"
   ],
+  "platform": "github",
+  "username": "moki-bot[bot]",
+  "repositories": ["qlonik/containers"],
+  "onboarding": false,
+  "requireConfig": "optional",
+  "gitAuthor": "moki-bot <125534110+moki-bot[bot]@users.noreply.github.com>",
   "dependencyDashboardTitle": "Renovate Dashboard ๐Ÿค–",
-  "suppressNotifications": ["prEditedNotification", "prIgnoreNotification"],
+  "suppressNotifications": ["prIgnoreNotification"],
+  "commitBodyTable": true,
   "packageRules": [
     {
-      "addLabels": ["renovate/container", "type/major"],
-      "additionalBranchPrefix": "{{parentDir}}-",
-      "commitMessageExtra": " ( {{currentVersion}} โ†’ {{newVersion}} )",
-      "commitMessagePrefix": "feat({{parentDir}})!: ",
-      "commitMessageTopic": "{{depName}}",
-      "labels": ["app/{{parentDir}}"],
-      "matchDatasources": ["docker"],
-      "matchFileNames": ["apps/**/Dockerfile"],
-      "matchUpdateTypes": ["major"]
-    },
-    {
-      "addLabels": ["renovate/container", "type/minor"],
-      "additionalBranchPrefix": "{{parentDir}}-",
-      "commitMessageExtra": "( {{currentVersion}} โ†’ {{newVersion}} )",
-      "commitMessageTopic": "{{depName}}",
-      "labels": ["app/{{parentDir}}"],
-      "matchDatasources": ["docker"],
-      "matchFileNames": ["apps/**/Dockerfile"],
-      "matchUpdateTypes": ["minor"],
-      "semanticCommitScope": "{{parentDir}}",
-      "semanticCommitType": "feat"
+      "description": "Auto-merge Github Actions",
+      "matchDatasources": ["github-tags"],
+      "automerge": true,
+      "automergeType": "branch",
+      "ignoreTests": true,
+      "matchUpdateTypes": ["minor", "patch"],
+      "matchPackagePatterns": ["renovatebot/github-action"]
     },
     {
-      "addLabels": ["renovate/container", "type/patch"],
-      "additionalBranchPrefix": "{{parentDir}}-",
-      "commitMessageExtra": "( {{currentVersion}} โ†’ {{newVersion}} )",
-      "commitMessageTopic": "{{depName}}",
-      "labels": ["app/{{parentDir}}"],
       "matchDatasources": ["docker"],
-      "matchFileNames": ["apps/**/Dockerfile"],
-      "matchUpdateTypes": ["patch"],
-      "semanticCommitScope": "{{parentDir}}",
-      "semanticCommitType": "fix"
+      "matchUpdateTypes": ["digest"],
+      "commitMessagePrefix": "๐Ÿ“ฃ "
     }
   ]
 }
.github/scripts/fetch.mjs
diff --git a/.github/scripts/fetch.mjs b/.github/scripts/fetch.mjs
new file mode 100755
index 0000000..3b2dc9d
--- /dev/null
+++ b/.github/scripts/fetch.mjs
@@ -0,0 +1,26 @@
+#!/usr/bin/env zx
+
+// Builds a JSON string what images and their channels to process
+// [
+//   {"app":"ubuntu", "channel": "focal"},
+//   {"app"...
+// ]
+
+$.verbose = false
+import { Published } from './published.mjs';
+
+let output = []
+for (const path of await glob(['apps/*/metadata.json'])) {
+  let {app, channels} = await fs.readJson(path);
+
+  for (const channel of channels) {
+    let publishedVersion = await Published(app, channel.name, channel.stable)
+    let upstreamVersion = await $`./.github/scripts/upstream.sh ${app} ${channel.name}`
+
+    if (publishedVersion != upstreamVersion.stdout) {
+      output.push({"app": app, "channel": channel.name})
+    }
+  }
+}
+
+console.log(`::set-output name=changes::${JSON.stringify(output)}`)
.github/scripts/fetch.sh
diff --git a/.github/scripts/fetch.sh b/.github/scripts/fetch.sh
new file mode 100755
index 0000000..7ad24e6
--- /dev/null
+++ b/.github/scripts/fetch.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# FIXME: Consider rewriting this in Python3
+
+# Overview:
+# Builds a JSON string what images and their channels to process
+# Outputs:
+# [
+#    {"app":"ubuntu", "channel": "focal"},
+#    {"app":"ubuntu", "channel": "jammy"},
+#    {"app"...
+# ]
+
+shopt -s lastpipe
+
+FETCH_ALL=false
+if [ "$1" == "all" ]; then
+    FETCH_ALL=true
+fi
+
+declare -A app_channel_array
+find ./apps -name metadata.json | while read -r metadata; do
+    declare -a __channels=()
+    app="$(jq --raw-output '.app' "${metadata}")"
+    jq --raw-output -c '.channels | .[]' "${metadata}" | while read -r channels; do
+        channel="$(jq --raw-output '.name' <<< "${channels}")"
+        stable="$(jq --raw-output '.stable' <<< "${channels}")"
+        if [ ${FETCH_ALL} == true ]; then
+            __channels+=("${channel}")
+        else
+            published_version=$(./.github/scripts/published.sh "${app}" "${channel}" "${stable}")
+            upstream_version=$(./.github/scripts/upstream.sh "${app}" "${channel}" "${stable}")
+            if [[ "${published_version}" != "${upstream_version}" && "${upstream_version}" != "" && "${upstream_version}" != "null" ]]; then
+                echo "${app}$([[ ! ${stable} == false ]] || echo "-${channel}"):${published_version:-<NOTFOUND>} -> ${upstream_version}"
+                __channels+=("${channel}")
+            fi
+        fi
+    done
+    if [[ "${#__channels[@]}" -gt 0 ]]; then
+        app_channel_array[$app]="${__channels[*]}"
+    fi
+done
+
+output="[]"
+if [[ "${#app_channel_array[@]}" -gt 0 ]]; then
+    declare -a changes_array=()
+    for app in "${!app_channel_array[@]}"; do
+        #shellcheck disable=SC2086
+        if [[ -n "${app}" ]]; then
+            for channel in ${app_channel_array[$app]}; do
+                changes_array+=("$(jo app="$app" channel="$channel")")
+            done
+        fi
+    done
+    #shellcheck disable=SC2048,SC2086
+    output="$(jo -a ${changes_array[*]})"
+fi
+
+echo "changes=${output}" >> $GITHUB_OUTPUT
.github/scripts/json-to-yaml.py
diff --git a/.github/scripts/json-to-yaml.py b/.github/scripts/json-to-yaml.py
deleted file mode 100644
index fedd817..0000000
--- a/.github/scripts/json-to-yaml.py
+++ /dev/null
@@ -1,27 +0,0 @@
-
-import os
-import json
-import yaml
-
-def json_to_yaml(subdir, file):
-    obj = None
-
-    json_file = os.path.join(subdir, file)
-    with open(json_file) as f:
-        obj = json.load(f)
-
-    yaml_file = os.path.join(subdir, "metadata.yaml")
-    with open(yaml_file, "w") as f:
-        yaml.dump(obj, f)
-
-    os.remove(json_file)
-
-
-if __name__ == "__main__":
-
-    for subdir, dirs, files in os.walk("./apps"):
-        for f in files:
-            if f != "metadata.json":
-                continue
-            json_to_yaml(subdir, f)
-
.github/scripts/prepare-images.py
diff --git a/.github/scripts/prepare-images.py b/.github/scripts/prepare-images.py
new file mode 100755
index 0000000..c6e0c3c
--- /dev/null
+++ b/.github/scripts/prepare-images.py
@@ -0,0 +1,92 @@
+#! /usr/bin/env python3
+
+import sys
+
+import json
+import subprocess
+
+from datetime import datetime, timezone
+from os.path import isfile
+
+
+def get_upstream_version(app, channel):
+    latest_script = f"./apps/{app}/ci/latest.sh"
+    if not isfile(latest_script):
+        return ""
+
+    args = [latest_script, channel]
+    output = subprocess.check_output(args)
+    return output.decode("utf-8").strip()
+
+if __name__ == "__main__":
+
+    changed_apps = json.loads(sys.argv[1])
+    forRelease = sys.argv[2] == "true"
+
+    out = {"manifestsToBuild": [], "imagePlatformPermutations": []}
+
+    for app in changed_apps:
+        name = app["app"]
+        channel = str(app["channel"])
+        with open(f"./apps/{name}/metadata.json") as f:
+            metadata = json.load(f)
+
+        # Generate Config
+        cfg = {}
+        for ch in metadata["channels"]:
+            if str(ch["name"]) == channel:
+                cfg = ch
+                break
+
+        app["chan_build_date"] = datetime.now(timezone.utc).isoformat()
+        app["chan_stable"] = cfg["stable"]
+        app["chan_tests_enabled"] = cfg["tests"]["enabled"]
+        app["chan_tests_type"] = cfg["tests"]["type"]
+        app["chan_upstream_version"] = get_upstream_version(name, channel)
+
+        if app["chan_tests_enabled"] and app["chan_tests_type"] == "cli":
+            app["chan_goss_args"] = "tail -f /dev/null"
+
+        if app.get("base", False):
+            app["chan_label_type"] ="org.opencontainers.image.base"
+        else:
+            app["chan_label_type"]="org.opencontainers.image"
+
+        if isfile(f"./apps/{name}/{channel}/Dockerfile"):
+            app["chan_dockerfile"] = f"./apps/{name}/{channel}/Dockerfile"
+            app["chan_goss_config"] = f"./apps/{name}/{channel}/goss.yaml"
+        else:
+            app["chan_dockerfile"] = f"./apps/{name}/Dockerfile"
+            app["chan_goss_config"] = f"./apps/{name}/ci/goss.yaml"
+
+        if app["chan_stable"]:
+            app["chan_image_name"] = name
+            app["chan_tag_rolling"] = f"{name}:rolling"
+            app["chan_tag_version"] = f"{name}:{app['chan_upstream_version']}"
+            app["chan_tag_testing"] = f"{name}:testing"
+        else:
+            app["chan_image_name"] = f"{name}-{channel}"
+            app["chan_tag_rolling"] = f"{name}-{channel}:rolling"
+            app["chan_tag_version"] = f"{name}-{channel}:{app['chan_upstream_version']}"
+            app["chan_tag_testing"] = f"{name}-{channel}:testing"
+
+        for platform in cfg["platforms"]:
+            if platform != "linux/amd64" and not forRelease:
+                continue
+            to_append = app.copy()
+            to_append["platform"] = platform
+            if platform != "linux/amd64":
+                to_append["chan_tests_enabled"] = False
+            out["imagePlatformPermutations"].append(to_append)
+
+        manifest = {
+            "image": app["chan_image_name"],
+            "app": name,
+            "channel": channel,
+            "tags": [app["chan_tag_rolling"], app["chan_tag_version"]],
+            "platforms": cfg["platforms"],
+            "version": app["chan_upstream_version"],
+        }
+        out["manifestsToBuild"].append(manifest)
+
+    print(json.dumps(out))
.github/scripts/prepare-matrices.py
diff --git a/.github/scripts/prepare-matrices.py b/.github/scripts/prepare-matrices.py
deleted file mode 100644
index 47dd818..0000000
--- a/.github/scripts/prepare-matrices.py
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/usr/bin/env python3
-import importlib.util
-import sys
-import os
-
-import json
-import yaml
-import requests
-
-from subprocess import check_output
-
-from os.path import isfile
-
-repo_owner = os.environ.get('REPO_OWNER', os.environ.get('GITHUB_REPOSITORY_OWNER'))
-
-TESTABLE_PLATFORMS = ["linux/amd64"]
-
-def load_metadata_file_yaml(file_path):
-    with open(file_path, "r") as f:
-        return yaml.safe_load(f)
-
-def load_metadata_file_json(file_path):
-    with open(file_path, "r") as f:
-        return json.load(f)
-
-def get_latest_version_py(latest_py_path, channel_name):
-    spec = importlib.util.spec_from_file_location("latest", latest_py_path)
-    latest = importlib.util.module_from_spec(spec)
-    sys.modules["latest"] = latest
-    spec.loader.exec_module(latest)
-    return latest.get_latest(channel_name)
-
-def get_latest_version_sh(latest_sh_path, channel_name):
-    out = check_output([latest_sh_path, channel_name])
-    return out.decode("utf-8").strip()
-
-def get_latest_version(subdir, channel_name):
-    ci_dir =  os.path.join(subdir, "ci")
-    if os.path.isfile(os.path.join(ci_dir, "latest.py")):
-        return get_latest_version_py(os.path.join(ci_dir, "latest.py"), channel_name)
-    elif os.path.isfile(os.path.join(ci_dir, "latest.sh")):
-        return get_latest_version_sh(os.path.join(ci_dir, "latest.sh"), channel_name)
-    elif os.path.isfile(os.path.join(subdir, channel_name, "latest.py")):
-       return get_latest_version_py(os.path.join(subdir, channel_name, "latest.py"), channel_name)
-    elif os.path.isfile(os.path.join(subdir, channel_name, "latest.sh")):
-        return get_latest_version_sh(os.path.join(subdir, channel_name, "latest.sh"), channel_name)
-    return None
-
-def get_published_version(image_name):
-    r = requests.get(
-        f"https://api.github.com/users/{repo_owner}/packages/container/{image_name}/versions",
-        headers={
-            "Accept": "application/vnd.github.v3+json",
-            "Authorization": "token " + os.environ["TOKEN"]
-        },
-    )
-
-    if r.status_code != 200:
-        return None
-
-    data = json.loads(r.text)
-    for image in data:
-        tags = image["metadata"]["container"]["tags"]
-        if "rolling" in tags:
-            tags.remove("rolling")
-            # Assume the longest string is the complete version number
-            return max(tags, key=len)
-
-def get_image_metadata(subdir, meta, forRelease=False, force=False, channels=None):
-    imagesToBuild = {
-        "images": [],
-        "imagePlatforms": []
-    }
-
-    if channels is None:
-        channels = meta["channels"]
-    else:
-        channels = [channel for channel in meta["channels"] if channel["name"] in channels]
-
-
-    for channel in channels:
-        version = get_latest_version(subdir, channel["name"])
-        if version is None:
-            continue
-
-        # Image Name
-        toBuild = {}
-        if channel.get("stable", False):
-            toBuild["name"] = meta["app"]
-        else:
-            toBuild["name"] = "-".join([meta["app"], channel["name"]])
-
-        # Skip if latest version already published
-        if not force:
-            published = get_published_version(toBuild["name"])
-            if published is not None and published == version:
-                continue
-            toBuild["published_version"] = published
-
-        toBuild["version"] = version
-
-        # Image Tags
-        toBuild["tags"] = ["rolling", version]
-        if meta.get("semver", False):
-            parts = version.split(".")[:-1]
-            while len(parts) > 0:
-                toBuild["tags"].append(".".join(parts))
-                parts = parts[:-1]
-
-        # Platform Metadata
-        for platform in channel["platforms"]:
-
-            if platform not in TESTABLE_PLATFORMS and not forRelease:
-                continue
-
-            toBuild.setdefault("platforms", []).append(platform)
-
-            target_os = platform.split("/")[0]
-            target_arch = platform.split("/")[1]
-
-            platformToBuild = {}
-            platformToBuild["name"] = toBuild["name"]
-            platformToBuild["platform"] = platform
-            platformToBuild["target_os"] = target_os
-            platformToBuild["target_arch"] = target_arch
-            platformToBuild["version"] = version
-            platformToBuild["channel"] = channel["name"]
-            platformToBuild["label_type"]="org.opencontainers.image"
-
-            if isfile(os.path.join(subdir, channel["name"], "Dockerfile")):
-                platformToBuild["dockerfile"] = os.path.join(subdir, channel["name"], "Dockerfile")
-                platformToBuild["context"] = os.path.join(subdir, channel["name"])
-                platformToBuild["goss_config"] = os.path.join(subdir, channel["name"], "goss.yaml")
-            else:
-                platformToBuild["dockerfile"] = os.path.join(subdir, "Dockerfile")
-                platformToBuild["context"] = subdir
-                platformToBuild["goss_config"] = os.path.join(subdir, "ci", "goss.yaml")
-
-            platformToBuild["goss_args"] = "tail -f /dev/null" if channel["tests"].get("type", "web") == "cli" else ""
-
-            platformToBuild["tests_enabled"] = channel["tests"]["enabled"] and platform in TESTABLE_PLATFORMS
-
-            imagesToBuild["imagePlatforms"].append(platformToBuild)
-        imagesToBuild["images"].append(toBuild)
-    return imagesToBuild
-
-if __name__ == "__main__":
-    apps = sys.argv[1]
-    forRelease = sys.argv[2] == "true"
-    force = sys.argv[3] == "true"
-    imagesToBuild = {
-        "images": [],
-        "imagePlatforms": []
-    }
-
-    if apps != "all":
-        channels=None
-        apps = apps.split(",")
-        if len(sys.argv) == 5:
-            channels = sys.argv[4].split(",")
-
-        for app in apps:
-            if not os.path.exists(os.path.join("./apps", app)):
-                print(f"App \"{app}\" not found")
-                exit(1)
-
-            meta = None
-            if os.path.isfile(os.path.join("./apps", app, "metadata.yaml")):
-                meta = load_metadata_file_yaml(os.path.join("./apps", app, "metadata.yaml"))
-            elif os.path.isfile(os.path.join("./apps", app, "metadata.json")):
-                meta = load_metadata_file_json(os.path.join("./apps", app, "metadata.json"))
-
-            imageToBuild = get_image_metadata(os.path.join("./apps", app), meta, forRelease, force=force, channels=channels)
-            if imageToBuild is not None:
-                imagesToBuild["images"].extend(imageToBuild["images"])
-                imagesToBuild["imagePlatforms"].extend(imageToBuild["imagePlatforms"])
-    else:
-        for subdir, dirs, files in os.walk("./apps"):
-            for file in files:
-                meta = None
-                if file == "metadata.yaml":
-                    meta = load_metadata_file_yaml(os.path.join(subdir, file))
-                elif file == "metadata.json":
-                    meta = load_metadata_file_json(os.path.join(subdir, file))
-                else:
-                    continue
-                if meta is not None:
-                    imageToBuild = get_image_metadata(subdir, meta, forRelease, force=force)
-                    if imageToBuild is not None:
-                        imagesToBuild["images"].extend(imageToBuild["images"])
-                        imagesToBuild["imagePlatforms"].extend(imageToBuild["imagePlatforms"])
-    print(json.dumps(imagesToBuild))
.github/scripts/published.mjs
diff --git a/.github/scripts/published.mjs b/.github/scripts/published.mjs
new file mode 100644
index 0000000..37dadb7
--- /dev/null
+++ b/.github/scripts/published.mjs
@@ -0,0 +1,18 @@
+export async function Published(app, channel, stable) {
+  let headers = {}
+  if (process.env.TOKEN) {
+    headers = {
+      Accept: 'application/vnd.github.v3+json',
+      Authorization: `token ${process.env.TOKEN}`
+    }
+  }
+  app = (stable ? app : `${app}-${channel}`)
+  let res = await fetch(`https://api.github.com/users/qlonik/packages/container/${app}/versions`, { headers })
+  let data = await res.json()
+  try {
+    // Assume first image found and first tag found is the most recent pushed tag
+    return data[0].metadata.container.tags[0];
+  } catch {
+    console.log(`Error finding published version for ${app}`)
+  }
+}
.github/scripts/published.sh
diff --git a/.github/scripts/published.sh b/.github/scripts/published.sh
new file mode 100755
index 0000000..4dfd375
--- /dev/null
+++ b/.github/scripts/published.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+APP="${1}"
+CHANNEL="${2}"
+STABLE="${3}"
+# TOKEN="${TOKEN}"
+
+if [[ -z "${STABLE}" || "${STABLE}" == false ]]; then
+    APP="${APP}-${CHANNEL}"
+fi
+
+tags=$( \
+    curl -fsSL \
+        -H "Accept: application/vnd.github.v3+json" \
+        -H "Authorization: token ${TOKEN}" \
+        "https://api.github.com/users/qlonik/packages/container/${APP}/versions" \
+        2>/dev/null
+)
+
+if [[ -z "${tags}" ]]; then
+    exit 0
+fi
+
+current_tags=$( \
+    jq --compact-output \
+        'map( select( .metadata.container.tags[] | contains("rolling") ) | .metadata.container.tags[] )' \
+            <<< "${tags}" \
+)
+
+tag=$( \
+    jq --compact-output \
+        'map( select( index("rolling") | not ) )' \
+            <<< "${current_tags}"
+)
+
+printf "%s" "$(jq --raw-output '.[0]' <<< "${tag}")"
.github/scripts/render-readme.py
diff --git a/.github/scripts/render-readme.py b/.github/scripts/render-readme.py
deleted file mode 100644
index f161a50..0000000
--- a/.github/scripts/render-readme.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import os
-import json
-import requests
-import yaml
-
-from jinja2 import Environment, PackageLoader, select_autoescape
-
-repo_owner = os.environ.get('REPO_OWNER', os.environ.get('GITHUB_REPOSITORY_OWNER'))
-repo_name = os.environ.get('REPO_NAME', os.environ.get('GITHUB_REPOSITORY'))
-
-env = Environment(
-    loader=PackageLoader("render-readme"),
-    autoescape=select_autoescape()
-)
-
-def load_metadata_file_yaml(file_path):
-    with open(file_path, "r") as f:
-        return yaml.safe_load(f)
-
-def load_metadata_file_json(file_path):
-    with open(file_path, "r") as f:
-        return json.load(f)
-
-def load_metadata_file(file_path):
-    if file_path.endswith(".json"):
-        return load_metadata_file_json(file_path)
-    elif file_path.endswith(".yaml"):
-        return load_metadata_file_yaml(file_path)
-    return None
-
-if __name__ == "__main__":
-    app_images = []
-    for subdir, dirs, files in os.walk("./apps"):
-        for file in files:
-            if file != "metadata.yaml" and file != "metadata.json":
-                continue
-            meta = load_metadata_file(os.path.join(subdir, file))
-            for channel in meta["channels"]:
-                name = ""
-                if channel.get("stable", False):
-                    name = meta["app"]
-                else:
-                    name = "-".join([meta["app"], channel["name"]])
-                image = {
-                    "name": name,
-                    "channel": channel["name"],
-                    "html_url": f"https://github.com/{repo_name}/pkgs/container/{name}",
-                    "owner": repo_owner
-                }
-
-                app_images.append(image)
-
-    template = env.get_template("README.md.j2")
-    with open("./README.md", "w") as f:
-        f.write(template.render(app_images=app_images))
.github/scripts/requirements.txt
diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
deleted file mode 100644
index 37641f6..0000000
--- a/.github/scripts/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-requests
-pyyaml
-packaging
-jinja2
\ No newline at end of file
.github/scripts/templates/README.md.j2
diff --git a/.github/scripts/templates/README.md.j2 b/.github/scripts/templates/README.md.j2
deleted file mode 100644
index c9a061c..0000000
--- a/.github/scripts/templates/README.md.j2
+++ /dev/null
@@ -1,112 +0,0 @@
-<!---
-NOTE: AUTO-GENERATED FILE
-to edit this file, instead edit its template at: ./github/scripts/templates/README.md.j2
--->
-<div align="center">
-
-
-## Containers
-
-_An opinionated collection of container images_
-
-</div>
-
-<div align="center">
-
-![GitHub Repo stars](https://img.shields.io/github/stars/onedr0p/containers?style=for-the-badge)
-![GitHub forks](https://img.shields.io/github/forks/onedr0p/containers?style=for-the-badge)
-![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/onedr0p/containers/release-scheduled.yaml?style=for-the-badge&label=Scheduled%20Release)
-
-</div>
-
-Welcome to my container images, if looking for a container start by [browsing the GitHub Packages page for this repo's packages](https://github.com/onedr0p?tab=packages&repo_name=containers).
-
-## Mission statement
-
-The goal of this project is to support [semantically versioned](https://semver.org/), [rootless](https://rootlesscontaine.rs/), and [multiple architecture](https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/) containers for various applications.
-
-It also adheres to a [KISS principle](https://en.wikipedia.org/wiki/KISS_principle), logging to stdout, [one process per container](https://testdriven.io/tips/59de3279-4a2d-4556-9cd0-b444249ed31e/), no [s6-overlay](https://github.com/just-containers/s6-overlay) and all images are built on top of [Alpine](https://hub.docker.com/_/alpine) or [Ubuntu](https://hub.docker.com/_/ubuntu).
-
-## Tag immutability
-
-The containers built here do not use immutable tags, as least not in the more common way you have seen from [linuxserver.io](https://fleet.linuxserver.io/) or [Bitnami](https://bitnami.com/stacks/containers).
-
-We do take a similar approach but instead of appending a `-ls69` or `-r420` prefix to the tag we instead insist on pinning to the sha256 digest of the image, while this is not as pretty it is just as functional in making the images immutable.
-
-| Container                                          | Immutable |
-|----------------------------------------------------|-----------|
-| `ghcr.io/onedr0p/sonarr:rolling`                   | โŒ         |
-| `ghcr.io/onedr0p/sonarr:3.0.8.1507`                | โŒ         |
-| `ghcr.io/onedr0p/sonarr:rolling@sha256:8053...`    | โœ…         |
-| `ghcr.io/onedr0p/sonarr:3.0.8.1507@sha256:8053...` | โœ…         |
-
-_If pinning an image to the sha256 digest, tools like [Renovate](https://github.com/renovatebot/renovate) support updating the container on a digest or application version change._
-
-## Passing arguments to a application
-
-Some applications do not support defining configuration via environment variables and instead only allow certain config to be set in the command line arguments for the app. To circumvent this, for applications that have an `entrypoint.sh` read below.
-
-1. First read the Kubernetes docs on [defining command and arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/).
-2. Look up the documentation for the application and find a argument you would like to set.
-3. Set the argument in the `args` section, be sure to include `entrypoint.sh` as the first arg and any application specific arguments thereafter.
-
-    ```yaml
-    args:
-      - /entrypoint.sh
-      - --port
-      - "8080"
-    ```
-
-## Configuration volume
-
-For applications that need to have persistent configuration data the config volume is hardcoded to `/config` inside the container. This is not able to be changed in most cases.
-
-## Available Images
-
-Each Image will be built with a `rolling` tag, along with tags specific to it's version. Available Images Below
-
-Container | Channel | Image
---- | --- | ---
-{% for image in app_images | sort(attribute="name") -%}
-[{{ image.name }}]({{ image.html_url }}) | {{ image.channel }} | ghcr.io/{{ image.owner }}/{{ image.name }}
-{% endfor %}
-
-## Contributing
-
-1. Install [Docker](https://docs.docker.com/get-docker/), [Taskfile](https://taskfile.dev/) & [Cuelang](https://cuelang.org/)
-2. Get familiar with the structure of the repositroy
-3. Find a similar application in the apps directory
-4. Copy & Paste an application and update the directory name
-5. Update `metadata.json`, `Dockerfile`, `ci/latest.sh`, `ci/goss.yaml` and make it suit the application build
-6. Include any additional files if required
-7. Use Taskfile to build and test your image
-
-    ```ruby
-    task APP=sonarr CHANNEL=main test
-    ```
-
-### Automated tags
-
-Here's an example of how tags are created in the GitHub workflows, be careful with `metadata.json` as it does affect the outcome of how the tags will be created when the application is built.
-
-| Application | Channel   | Stable  | Generated Tag               |
-|-------------|-----------|---------|-----------------------------|
-| `sonarr`    | `develop` | `false` | `sonarr-develop:3.0.8.1538` |
-| `sonarr`    | `develop` | `false` | `sonarr-develop:rolling`    |
-| `sonarr`    | `main`    | `true`  | `sonarr:3.0.8.1507`         |
-| `sonarr`    | `main`    | `true`  | `sonarr:rolling`            |
-
-## Deprecations
-
-Containers here can be **deprecated** at any point, this could be for any reason described below.
-
-1. The upstream application is **no longer actively developed**
-2. The upstream application has an **official upstream container** that follows closely to the mission statement described here
-3. The upstream application has been **replaced with a better alternative**
-4. The **maintenance burden** of keeping the container here **is too bothersome**
-
-**Note**: Deprecated containers will remained published to this repo for 6 months after which they will be pruned.
-## Credits
-
-A lot of inspiration and ideas are thanks to the hard work of [hotio.dev](https://hotio.dev/) and [linuxserver.io](https://www.linuxserver.io/) contributors.
-
.github/scripts/upstream.sh
diff --git a/.github/scripts/upstream.sh b/.github/scripts/upstream.sh
new file mode 100755
index 0000000..c39ae39
--- /dev/null
+++ b/.github/scripts/upstream.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+APP="${1}"
+CHANNEL="${2}"
+
+if test -f "./apps/${APP}/ci/latest.sh"; then
+    bash ./apps/"${APP}"/ci/latest.sh "${CHANNEL}"
+fi
.github/workflows/action-image-build.yaml
diff --git a/.github/workflows/action-image-build.yaml b/.github/workflows/action-image-build.yaml
new file mode 100644
index 0000000..62187d7
--- /dev/null
+++ b/.github/workflows/action-image-build.yaml
@@ -0,0 +1,267 @@
+---
+name: "Action: Image Build"
+
+on:
+  workflow_call:
+    inputs:
+      imagesToBuild:
+        description: |-
+          Pass a json string with the images and channels to build. e.g.:
+            [
+              {
+                "app": "sonarr",
+                "channel": "main"
+              },
+              {
+                "app": "sonarr",
+                "channel": "develop"
+              }
+            ]
+        required: false
+        type: string
+        default: ''
+
+      pushImages:
+        required: false
+        default: 'false'
+        type: string
+
+      sendNotification:
+        required: false
+        default: 'false'
+        type: string
+
+jobs:
+  prepare:
+    name: Prepare Matrices
+    runs-on: ubuntu-latest
+    outputs:
+      matrices: ${{ steps.generate-matrices.outputs.matrices }}
+    if: ${{ inputs.imagesToBuild != '' && inputs.imagesToBuild != '[]' }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+
+      - name: Log images to build
+        run: |
+          echo 'imagesToBuild=${{ inputs.imagesToBuild }}'
+          echo 'pushImages=${{ inputs.pushImages }}'
+          echo 'sendNotification=${{ inputs.sendNotification }}'
+
+      - name: Setup CUE
+        uses: cue-lang/setup-cue@0be332bb74c8a2f07821389447ba3163e2da3bfb
+
+      - name: Validate Image Metadata
+        shell: bash
+        # 'xargs' rather than 'find -exec' so we can capture the status code
+        run: |
+          find ./apps/ -name metadata.json | xargs -I {} cue vet --schema '#Spec' {} ./metadata.rules.cue
+
+      - name: Generate Matrices
+        id: generate-matrices
+        run: |
+          matrices=$(./.github/scripts/prepare-images.py '${{ inputs.imagesToBuild }}' '${{ inputs.pushImages }}')
+          echo "matrices=${matrices}" >> $GITHUB_OUTPUT
+
+  build-platform-images:
+    name: Build/Test ${{ matrix.image.chan_image_name }} - ${{ matrix.image.platform }}
+    runs-on: ubuntu-latest
+    needs:
+      - prepare
+    strategy:
+      matrix:
+        image: ["${{ fromJson(needs.prepare.outputs.matrices).imagePlatformPermutations }}"]
+      fail-fast: false
+    steps:
+      - name: Log Matrix Input
+        run: |
+          cat << EOF
+          ${{ toJSON(matrix.image) }}
+          EOF
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+
+      - name: Setup GHCR
+        uses: docker/login-action@v2
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Setup Tools
+        shell: bash
+        run: sudo apt-get install -y moreutils jo
+
+      - name: Setup Goss
+        if: ${{ matrix.image.chan_tests_enabled }}
+        uses: e1himself/[email protected]
+        with:
+          version: v0.3.21
+
+      - name: Prepare Build Outputs
+        id: prepare-build-outputs
+        run: |
+          if [[ ${{ inputs.pushImages }} == 'true' ]]; then
+              image_name="ghcr.io/${{ github.repository_owner }}/${{ matrix.image.chan_image_name }}"
+              outputs="type=image,name=${image_name},push-by-digest=true,name-canonical=true,push=true"
+          else
+              image_name="ghcr.io/${{ github.repository_owner }}/${{ matrix.image.chan_tag_testing }}"
+              outputs="type=docker,name=${image_name},push=false"
+          fi
+          echo "image_name=${image_name}" >> $GITHUB_OUTPUT
+          echo "outputs=${outputs}" >> $GITHUB_OUTPUT
+
+      - name: Build Image
+        uses: docker/build-push-action@v4
+        id: build
+        with:
+          build-args: |-
+            VERSION=${{ matrix.image.chan_upstream_version }}
+            CHANNEL=${{ matrix.image.channel }}
+          context: .
+          platforms: ${{ matrix.image.platform }}
+          file: ${{ matrix.image.chan_dockerfile }}
+          outputs: ${{ steps.prepare-build-outputs.outputs.outputs }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+
+      - name: Run Goss Tests
+        id: dgoss
+        if: ${{ matrix.image.chan_tests_enabled }}
+        shell: bash
+        env:
+          CONTAINER_RUNTIME: docker
+          GOSS_FILE: ${{ matrix.image.chan_goss_config }}
+          GOSS_OPTS: --retry-timeout 60s --sleep 2s --color --format documentation
+          GOSS_SLEEP: 2
+          GOSS_FILES_STRATEGY: cp
+          CONTAINER_LOG_OUTPUT: goss_container_log_output
+        run: |
+          if [[ '${{ inputs.pushImages }}' == 'true' ]]; then
+              image_name="${{ steps.prepare-build-outputs.outputs.image_name }}@${{ steps.build.outputs.digest }}"
+          else
+              image_name="${{ steps.prepare-build-outputs.outputs.image_name }}"
+          fi
+          dgoss run ${image_name} ${{ matrix.image.chan_goss_args }}
+
+      - name: Export Digest
+        id: export-digest
+        if: ${{ inputs.pushImages == 'true' }}
+        run: |
+          mkdir -p /tmp/${{ matrix.image.chan_image_name }}/digests
+          digest="${{ steps.build.outputs.digest }}"
+          echo "${{ matrix.image.platform }}" > "/tmp/${{ matrix.image.chan_image_name }}/digests/${digest#sha256:}"
+
+      - name: Upload Digest
+        if: ${{ inputs.pushImages == 'true' }}
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ matrix.image.chan_image_name }}
+          path: /tmp/${{ matrix.image.chan_image_name }}/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    name: Merge ${{ matrix.manifest.image }}
+    runs-on: ubuntu-latest
+    needs:
+      - prepare
+      - build-platform-images
+    # Always run merge, as the prior matrix is all or nothing. We test for prior step failure
+    # in the "Test Failed Bit" step. This ensures if one app fails, others can still complete.
+    if: ${{ always() && inputs.pushImages == 'true'}}
+    strategy:
+      matrix:
+        manifest: ["${{ fromJSON(needs.prepare.outputs.matrices).manifestsToBuild }}"]
+      fail-fast: false
+    steps:
+      - name: Download Digests
+        uses: actions/download-artifact@v3
+        with:
+          name: ${{ matrix.manifest.image }}
+          path: /tmp/${{ matrix.manifest.image }}
+
+      - name: Ensure all platforms were built
+        id: ensure-platforms
+        run: |
+            EXPECTED_COUNT=$(cat << EOF | jq ". | length"
+            ${{ toJSON(matrix.manifest.platforms) }}
+            EOF
+            )
+            ACTUAL_COUNT=$(ls -1 /tmp/${{ matrix.manifest.image }}/digests | wc -l)
+            if [[ $EXPECTED_COUNT != $ACTUAL_COUNT ]]; then
+                echo "Expected $EXPECTED_COUNT platforms, but only found $ACTUAL_COUNT"
+                echo "Expected: ${{ toJSON(matrix.manifest.platforms) }}"
+                echo "Actual: $(cat /tmp/${{ matrix.manifest.image }}/digests/*)"
+                exit 1
+            fi
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+
+      - name: Setup GHCR
+        uses: docker/login-action@v2
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Log Files
+        working-directory: /tmp/${{ matrix.manifest.image }}/digests
+        run: |
+          ls -la
+          cat *
+
+      # https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
+      - name: Merge Manifests
+        id: merge
+        working-directory: /tmp/${{ matrix.manifest.image }}/digests
+        env:
+          TAGS: ${{ toJSON(matrix.manifest.tags) }}
+        run: |
+          docker buildx imagetools create $(jq -cr '. | map("-t ghcr.io/${{ github.repository_owner }}/" + .) | join(" ")'  <<< "$TAGS") \
+              $(printf 'ghcr.io/${{ github.repository_owner }}/${{ matrix.manifest.image }}@sha256:%s ' *)
+
+      - name: Inspect image
+        run: |
+          docker buildx imagetools inspect ghcr.io/${{ github.repository_owner }}/${{ matrix.manifest.image }}:${{ matrix.manifest.version }}
+
+      - name: Build successful
+        id: build-success
+        if: ${{ always() && steps.merge.outcome == 'success' && steps.failure-test.outcome == 'success' }}
+        run: |-
+          echo "message=๐ŸŽ‰ ${{ matrix.manifest.app }}-${{ matrix.manifest.channel }} (${{ matrix.manifest.version }})" >> $GITHUB_OUTPUT
+          echo "color=0x00FF00" >> $GITHUB_OUTPUT
+
+      - name: Build failed
+        id: build-failed
+        if: ${{ always() && (steps.merge.outcome == 'failure' || steps.failure-test.outcome == 'failure') }}
+        run: |-
+          echo "message=๐Ÿ’ฅ ${{ matrix.manifest.app }}-${{ matrix.manifest.channel }} (${{ matrix.manifest.version }})" >> $GITHUB_OUTPUT
+          echo "color=0xFF0000" >> $GITHUB_OUTPUT
+
+      - name: Send Discord Webhook
+        uses: sarisia/actions-status-discord@v1
+        if: ${{ always() && inputs.sendNotification == 'true' }}
+        with:
+          webhook: ${{ secrets.DISCORD_WEBHOOK }}
+          title: ${{ steps.build-failed.outputs.message || steps.build-success.outputs.message }}
+          color: ${{ steps.build-failed.outputs.color || steps.build-success.outputs.color }}
+          username: GitHub Actions
+
+  # Summarize matrix https://github.community/t/status-check-for-a-matrix-jobs/127354/7
+  build_success:
+    name: Build matrix success
+    runs-on: ubuntu-latest
+    needs:
+      - merge
+    if: ${{ always() }}
+    steps:
+      - name: Check build matrix status
+        if: ${{ (inputs.imagesToBuild != '' && inputs.imagesToBuild != '[]') && (needs.merge.result != 'success' && needs.merge.result != 'skipped') }}
+        run: exit 1
.github/workflows/build-images.yaml
diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml
deleted file mode 100644
index 9c5b06c..0000000
--- a/.github/workflows/build-images.yaml
+++ /dev/null
@@ -1,351 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Image Build"
-
-on:
-  workflow_call:
-    inputs:
-      appsToBuild:
-        required: false
-        type: string
-        default: ""
-      channelsToBuild:
-        required: false
-        type: string
-        default: ""
-      pushImages:
-        required: false
-        default: false
-        type: boolean
-      sendNotifications:
-        required: false
-        default: false
-        type: boolean
-      force:
-        required: false
-        default: true
-        type: boolean
-        description: Force rebuild
-    secrets:
-      BOT_APP_ID:
-        description: The App ID of the GitHub App
-        required: true
-      BOT_APP_PRIVATE_KEY:
-        description: The private key of the GitHub App
-        required: true
-
-jobs:
-  prepare:
-    name: Prepare to Build
-    runs-on: ubuntu-latest
-    outputs:
-      matrices: ${{ steps.prepare-matrices.outputs.matrices }}
-    steps:
-      - name: Lowercase repository owner
-        shell: bash
-        run: echo "LOWERCASE_REPO_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
-
-      - name: Generate Token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: "${{ secrets.BOT_APP_ID }}"
-          private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
-
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-
-      - name: Setup Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: 3.x
-          cache: pip
-
-      - name: Install Python Requirements
-        shell: bash
-        run: pip install -r ./.github/scripts/requirements.txt && pip freeze
-
-      - name: Prepare Matrices
-        id: prepare-matrices
-        env:
-          TOKEN: ${{ steps.app-token.outputs.token }}
-        shell: bash
-        run: |
-          if [[ -z "${{ inputs.appsToBuild }}" ]]; then
-              matrices=$(python ./.github/scripts/prepare-matrices.py "all" "${{ inputs.pushImages }}" "${{ inputs.force }}")
-          else
-            if [[ -z "${{ inputs.channelsToBuild }}" ]]; then
-              matrices=$(python ./.github/scripts/prepare-matrices.py "${{ inputs.appsToBuild }}" "${{ inputs.pushImages }}" "${{ inputs.force }}")
-            else
-              matrices=$(python ./.github/scripts/prepare-matrices.py "${{ inputs.appsToBuild }}" "${{ inputs.pushImages }}" "${{ inputs.force }}" "${{ inputs.channelsToBuild }}")
-            fi
-          fi
-          echo "matrices=${matrices}" >> $GITHUB_OUTPUT
-          echo "${matrices}"
-
-  build-platform-images:
-    name: Build/Test ${{ matrix.image.name }} (${{ matrix.image.platform }})
-    needs: prepare
-    runs-on: ubuntu-latest
-    if: ${{ toJSON(fromJSON(needs.prepare.outputs.matrices).imagePlatforms) != '[]' && toJSON(fromJSON(needs.prepare.outputs.matrices).imagePlatforms) != '' }}
-    strategy:
-      fail-fast: false
-      matrix:
-        image:
-          ["${{ fromJSON(needs.prepare.outputs.matrices).imagePlatforms }}"]
-    permissions:
-      contents: read
-      packages: write
-    steps:
-      - name: Lowercase repository owner
-        shell: bash
-        run: echo "LOWERCASE_REPO_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
-
-      - name: Log Matrix Input
-        shell: bash
-        run: |
-          cat << EOF
-          ${{ toJSON(matrix.image)}}
-          EOF
-
-      - name: Validate Matrix Input
-        shell: bash
-        run: |
-          if [[ -z "${{ matrix.image.name }}" ]]; then
-              echo "image.name is empty"
-              exit 1
-          fi
-          if [[ -z "${{ matrix.image.version }}" ]]; then
-              echo "image.version is empty"
-              exit 1
-          fi
-          if [[ -z "${{ matrix.image.context }}" ]]; then
-              echo "image.context is empty"
-              exit 1
-          fi
-          if [[ -z "${{ matrix.image.dockerfile }}" ]]; then
-              echo "image.dockerfile is empty"
-              exit 1
-          fi
-          if [[ -z "${{ matrix.image.platform }}" ]]; then
-              echo "image.platform is empty"
-              exit 1
-          fi
-          if [[ -z "${{ matrix.image.tests_enabled }}" ]]; then
-              echo "image.tests_enabled is empty"
-              exit 1
-          fi
-          echo "${{ matrix.image.name }}" | grep -E "[a-zA-Z0-9_\.\-]+" || "Image Name is invalid"
-          echo "${{ matrix.image.version }}" | grep -E "[a-zA-Z0-9_\.\-]+" || "Image Version is invalid"
-
-      - name: Generate Token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: "${{ secrets.BOT_APP_ID }}"
-          private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
-
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-
-      - name: Setup Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: "${{ github.actor }}"
-          password: "${{ secrets.GITHUB_TOKEN }}"
-
-      - name: Setup Goss
-        if: ${{ matrix.image.tests_enabled }}
-        uses: e1himself/goss-installation-action@v1
-        with:
-          version: latest
-
-      - name: Prepare Build Outputs
-        id: prepare-build-outputs
-        shell: bash
-        run: |
-          if [[ "${{ inputs.pushImages }}" == "true" ]]; then
-              image_name="ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{ matrix.image.name }}"
-              outputs="type=image,name=${image_name},push-by-digest=true,name-canonical=true,push=true"
-          else
-              image_name="ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{ matrix.image.name }}:zztesting"
-              outputs="type=docker,name=${image_name},push=false"
-          fi
-          echo "image_name=${image_name}" >> $GITHUB_OUTPUT
-          echo "outputs=${outputs}" >> $GITHUB_OUTPUT
-
-      - name: Build Image
-        uses: docker/build-push-action@v5
-        id: build
-        with:
-          build-args: |-
-            VERSION=${{ matrix.image.version }}
-            REVISION=${{ github.sha }}
-            CHANNEL=${{ matrix.image.channel }}
-          # TODO: Use ${{ matrix.image.context }}, requires updates to all dockerfiles :-(
-          context: .
-          platforms: ${{ matrix.image.platform }}
-          file: ${{ matrix.image.dockerfile }}
-          outputs: ${{ steps.prepare-build-outputs.outputs.outputs }}
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
-          labels: |-
-            org.opencontainers.image.title=${{ matrix.image.name }}
-            org.opencontainers.image.url=https://ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{ matrix.image.name }}
-            org.opencontainers.image.version=${{ matrix.image.version }}
-            org.opencontainers.image.revision=${{ github.sha }}
-            org.opencontainers.image.vendor=${{ env.LOWERCASE_REPO_OWNER }}
-
-      - name: Run Goss Tests
-        id: dgoss
-        if: ${{ matrix.image.tests_enabled }}
-        env:
-          CONTAINER_RUNTIME: docker
-          GOSS_FILE: ${{ matrix.image.goss_config }}
-          GOSS_OPTS: --retry-timeout 60s --sleep 2s --color --format documentation
-          GOSS_SLEEP: 2
-          GOSS_FILES_STRATEGY: cp
-          CONTAINER_LOG_OUTPUT: goss_container_log_output
-        shell: bash
-        run: |
-          if [[ '${{ inputs.pushImages }}' == 'true' ]]; then
-              image_name="${{ steps.prepare-build-outputs.outputs.image_name }}@${{ steps.build.outputs.digest }}"
-          else
-              image_name="${{ steps.prepare-build-outputs.outputs.image_name }}"
-          fi
-          dgoss run ${image_name} ${{ matrix.image.goss_args }}
-
-      - name: Export Digest
-        id: export-digest
-        if: ${{ inputs.pushImages }}
-        shell: bash
-        run: |
-          mkdir -p /tmp/${{ matrix.image.name }}/digests
-          digest="${{ steps.build.outputs.digest }}"
-          echo "${{ matrix.image.name }}" > "/tmp/${{ matrix.image.name }}/digests/${digest#sha256:}"
-
-      - name: Upload Digest
-        if: ${{ inputs.pushImages}}
-        uses: actions/upload-artifact@v4
-        with:
-          name: ${{ matrix.image.name }}-${{ matrix.image.target_os }}-${{ matrix.image.target_arch }}
-          path: /tmp/${{ matrix.image.name }}/*
-          if-no-files-found: error
-          retention-days: 1
-
-  merge:
-    name: Merge ${{ matrix.image.name }}
-    runs-on: ubuntu-latest
-    needs: ["prepare", "build-platform-images"]
-    # Always run merge, as the prior matrix is all or nothing. We test for prior step failure
-    # in the "Test Failed Bit" step. This ensures if one app fails, others can still complete.
-    if: ${{ always() && inputs.pushImages && toJSON(fromJSON(needs.prepare.outputs.matrices).images) != '[]' && toJSON(fromJSON(needs.prepare.outputs.matrices).images) != '' }}
-    strategy:
-      matrix:
-        image: ["${{ fromJSON(needs.prepare.outputs.matrices).images }}"]
-      fail-fast: false
-    steps:
-      - name: Lowercase repository owner
-        shell: bash
-        run: echo "LOWERCASE_REPO_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
-
-      - name: Download Digests
-        uses: actions/download-artifact@v4
-        with:
-          pattern: "${{ matrix.image.name }}-{linux,darwin}-{amd64,arm64}"
-          merge-multiple: true
-          path: /tmp/${{ matrix.image.name }}
-
-      - name: Ensure all platforms were built
-        id: ensure-platforms
-        shell: bash
-        run: |
-          EXPECTED_COUNT=$(cat << EOF | jq ". | length"
-          ${{ toJSON(matrix.image.platforms) }}
-          EOF
-          )
-          ACTUAL_COUNT=$(ls -1 /tmp/${{ matrix.image.name }}/digests | wc -l)
-          if [[ $EXPECTED_COUNT != $ACTUAL_COUNT ]]; then
-              echo "Expected $EXPECTED_COUNT platforms, but only found $ACTUAL_COUNT"
-              echo "Expected: ${{ toJSON(matrix.image.platforms) }}"
-              echo "Actual: $(cat /tmp/${{ matrix.image.name }}/digests/*)"
-              exit 1
-          fi
-
-      - name: Setup Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: "${{ github.actor }}"
-          password: "${{ secrets.GITHUB_TOKEN }}"
-
-      - name: Log Files
-        working-directory: /tmp/${{ matrix.image.name }}/digests
-        shell: bash
-        run: |
-          ls -la
-          cat *
-
-      - name: Merge Manifests
-        id: merge
-        working-directory: /tmp/${{ matrix.image.name }}/digests
-        env:
-          TAGS: ${{ toJSON(matrix.image.tags) }}
-        shell: bash
-        run: |
-          docker buildx imagetools create $(jq -cr '. | map("-t ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{matrix.image.name}}:" + .) | join(" ")'  <<< "$TAGS") \
-              $(printf 'ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{ matrix.image.name }}@sha256:%s ' *)
-
-      - name: Inspect image
-        id: inspect
-        shell: bash
-        run: |
-          docker buildx imagetools inspect ghcr.io/${{ env.LOWERCASE_REPO_OWNER }}/${{ matrix.image.name }}:${{ matrix.image.tags[0] }}
-
-      - name: Build successful
-        id: build-success
-        if: ${{ always() && steps.merge.outcome == 'success' && steps.inspect.outcome == 'success' }}
-        shell: bash
-        run: |
-          echo "message=๐ŸŽ‰ ${{ matrix.image.name }} (${{ matrix.image.tags[0] }})" >> $GITHUB_OUTPUT
-          echo "color=0x00FF00" >> $GITHUB_OUTPUT
-
-      - name: Build failed
-        id: build-failed
-        if: ${{ always() && (steps.merge.outcome == 'failure' || steps.inspect.outcome == 'failure') }}
-        shell: bash
-        run: |
-          echo "message=๐Ÿ’ฅ ${{ matrix.image.name }} (${{ matrix.image.tags[0] }})" >> $GITHUB_OUTPUT
-          echo "color=0xFF0000" >> $GITHUB_OUTPUT
-
-      - name: Send Discord Webhook
-        uses: sarisia/actions-status-discord@v1
-        if: ${{ always() && inputs.sendNotifications == 'true' }}
-        with:
-          webhook: ${{ secrets.DISCORD_WEBHOOK }}
-          title: ${{ steps.build-failed.outputs.message || steps.build-success.outputs.message }}
-          color: ${{ steps.build-failed.outputs.color || steps.build-success.outputs.color }}
-          username: GitHub Actions
-
-  # Summarize matrix https://github.community/t/status-check-for-a-matrix-jobs/127354/7
-  build_success:
-    name: Build matrix success
-    runs-on: ubuntu-latest
-    needs: ["prepare", "merge"]
-    if: ${{ always() }}
-    steps:
-      - name: Check build matrix status
-        if: ${{ (inputs.appsToBuild != '' && inputs.appsToBuild != '[]') && (needs.merge.result != 'success' && needs.merge.result != 'skipped' && needs.prepare.result != 'success') }}
-        shell: bash
-        run: exit 1
.github/workflows/get-changed-images.yaml
diff --git a/.github/workflows/get-changed-images.yaml b/.github/workflows/get-changed-images.yaml
deleted file mode 100644
index c32ed6e..0000000
--- a/.github/workflows/get-changed-images.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Get Changed Images"
-
-on:
-  workflow_call:
-    outputs:
-      addedOrModified:
-        description: "Whether any files were added or modified"
-        value: ${{ jobs.get-changed-images.outputs.addedOrModified }}
-      addedOrModifiedImages:
-        description: "The images that were added or modified"
-        value: ${{ jobs.get-changed-images.outputs.addedOrModifiedImages }}
-
-jobs:
-  get-changed-images:
-    name: Get Changed Images
-    runs-on: ubuntu-latest
-    outputs:
-      addedOrModified: "${{ steps.changed-files.outputs.all_changed_and_modified_files_count > 0 }}"
-      addedOrModifiedImages: "${{ steps.changed-containers.outputs.addedOrModifiedImages }}"
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-
-      - name: Get Changed Files
-        id: changed-files
-        uses: tj-actions/changed-files@v44
-        with:
-          files: apps/**
-          dir_names: true
-          dir_names_max_depth: 2
-          json: true
-          escape_json: false
-
-      - if: ${{ steps.changed-files.outputs.all_changed_and_modified_files_count > 0 }}
-        name: Determine changed images
-        id: changed-containers
-        shell: bash
-        run: |
-          IMAGES=$( \
-              echo '${{ steps.changed-files.outputs.all_changed_and_modified_files }}' \
-                  | jq --raw-output 'map(sub("^apps/"; "")) | join(",")' \
-          )
-          echo "addedOrModifiedImages=${IMAGES}" >> $GITHUB_OUTPUT
.github/workflows/image-rebuild.yaml
diff --git a/.github/workflows/image-rebuild.yaml b/.github/workflows/image-rebuild.yaml
new file mode 100644
index 0000000..a30c3eb
--- /dev/null
+++ b/.github/workflows/image-rebuild.yaml
@@ -0,0 +1,67 @@
+---
+name: "Image: Rebuild"
+
+on:
+  push:
+    branches:
+      - main
+    paths:
+      - "apps/**"
+      - "!apps/**/metadata.json"
+      - "!apps/**/README.md"
+
+jobs:
+  get-changes:
+    name: Collect changes
+    runs-on: ubuntu-latest
+    outputs:
+      addedOrModifiedImages: ${{ steps.collect-changes.outputs.addedOrModifiedImages }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Collect changes
+        id: collect-changes
+        uses: ./.github/actions/collect-changes
+
+  determine-images:
+    name: Determine Images to Build
+    runs-on: ubuntu-latest
+    needs: ["get-changes"]
+    outputs:
+      matrix: ${{ steps.get-changed.outputs.changes }}
+    if: ${{ needs.get-changes.outputs.addedOrModifiedImages }} != '[]'
+    steps:
+      - name: Install tools
+        run: sudo apt-get install moreutils jo
+
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Fetch modified images
+        id: get-changed
+        shell: bash
+        run: |
+          declare -a changes_array=()
+          while read -r app
+          do
+            while read -r channel
+            do
+              change="$(jo app="$app" channel="$channel")"
+              changes_array+=($change)
+            done < <(jq -r '.channels[] | .name' "./apps/$app/metadata.json")
+          done < <(echo '${{ needs.get-changes.outputs.addedOrModifiedImages }}' | jq --raw-output -c '.[]')
+
+          output="$(jo -a ${changes_array[*]})"
+          echo "changes=${output}" >> $GITHUB_OUTPUT
+
+  images-build:
+    uses: ./.github/workflows/action-image-build.yaml
+    needs:
+      - determine-images
+    with:
+      imagesToBuild: "${{ needs.determine-images.outputs.matrix }}"
+      pushImages: "true"
+    secrets: inherit
.github/workflows/label-sync.yaml
diff --git a/.github/workflows/label-sync.yaml b/.github/workflows/label-sync.yaml
deleted file mode 100644
index 019f264..0000000
--- a/.github/workflows/label-sync.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Label Sync"
-
-on:
-  workflow_dispatch:
-  push:
-    branches: ["main"]
-    paths:
-      - .github/labels.yaml
-      - .github/workflows/label-sync.yaml
-      - apps/**
-  schedule:
-    - cron: "0 0 * * *" # Every day at midnight
-
-jobs:
-  label-sync:
-    name: Label Sync
-    runs-on: ubuntu-latest
-    steps:
-      - name: Generate Token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: "${{ secrets.BOT_APP_ID }}"
-          private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
-
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-
-      - name: Setup Homebrew
-        uses: Homebrew/actions/setup-homebrew@master
-
-      - name: Setup Workflow Tools
-        shell: bash
-        run: brew install go-task
-
-      - name: Append app labels to the labels config file
-        shell: bash
-        run: task append-app-labels
-
-      - name: Sync Labels
-        uses: EndBug/label-sync@v2
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-          config-file: .github/labels.yaml
-          delete-other-labels: true
.github/workflows/pr-metadata.yaml
diff --git a/.github/workflows/pr-metadata.yaml b/.github/workflows/pr-metadata.yaml
new file mode 100644
index 0000000..a023c17
--- /dev/null
+++ b/.github/workflows/pr-metadata.yaml
@@ -0,0 +1,58 @@
+---
+name: "Pull Request: Metadata"
+
+on:
+  workflow_call:
+    outputs:
+      isFork:
+        description: "Is the PR coming from a forked repo?"
+        value: ${{ jobs.pr-metadata.outputs.isFork }}
+      addedOrModified:
+        description: "Does the PR contain any changes?"
+        value: ${{ jobs.pr-changes.outputs.addedOrModified }}
+      addedOrModifiedFiles:
+        description: "A list of the files changed in this PR"
+        value: ${{ jobs.pr-changes.outputs.addedOrModifiedFiles }}
+      addedOrModifiedImages:
+        description: "A list of the containers changed in this PR"
+        value: ${{ jobs.pr-changes.outputs.addedOrModifiedImages }}
+
+jobs:
+  pr-metadata:
+    name: Collect PR metadata
+    runs-on: ubuntu-latest
+    outputs:
+      isRenovatePR: ${{ startsWith(steps.branch-name.outputs.current_branch, 'renovate/') }}
+      isFork: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+    steps:
+      - name: Get branch name
+        id: branch-name
+        uses: tj-actions/branch-names@v6
+
+      - name: Save PR data to file
+        env:
+          PR_NUMBER: ${{ github.event.number }}
+        run: |
+          echo $PR_NUMBER > pr_number.txt
+
+      - name: Store pr data in artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: pr_metadata
+          path: ./pr_number.txt
+          retention-days: 5
+
+  pr-changes:
+    name: Collect PR changes
+    runs-on: ubuntu-latest
+    outputs:
+      addedOrModified: ${{ steps.collect-changes.outputs.changesDetected }}
+      addedOrModifiedFiles: ${{ steps.collect-changes.outputs.addedOrModifiedFiles }}
+      addedOrModifiedImages: ${{ steps.collect-changes.outputs.addedOrModifiedImages }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Collect changes
+        id: collect-changes
+        uses: ./.github/actions/collect-changes
.github/workflows/pr-validate.yaml
diff --git a/.github/workflows/pr-validate.yaml b/.github/workflows/pr-validate.yaml
index 9f03163..ecc52ba 100644
--- a/.github/workflows/pr-validate.yaml
+++ b/.github/workflows/pr-validate.yaml
@@ -1,29 +1,55 @@
 ---
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
 name: "Pull Request: Validate"
 
 on:
   pull_request:
-    branches: ["main"]
-    types: ["opened", "synchronize", "reopened"]
+    branches:
+      - main
 
 concurrency:
   group: ${{ github.head_ref }}-pr-validate
   cancel-in-progress: true
 
 jobs:
-  simple-checks:
-    uses: onedr0p/containers/.github/workflows/simple-checks.yaml@main
+  pr-metadata:
+    uses: ./.github/workflows/pr-metadata.yaml
 
-  get-changed-images:
-    uses: onedr0p/containers/.github/workflows/get-changed-images.yaml@main
+  generate-build-matrix:
+    name: Generate matrix for building images
+    runs-on: ubuntu-latest
+    needs: ["pr-metadata"]
+    outputs:
+      matrix: ${{ steps.get-changed.outputs.changes }}
+    if: ${{ needs.pr-metadata.outputs.addedOrModifiedImages }} != '[]'
+    steps:
+      - name: Install tools
+        run: sudo apt-get install moreutils jo
 
-  build-images:
-    needs: ["simple-checks", "get-changed-images"]
-    if: ${{ needs.get-changed-images.outputs.addedOrModified == 'true' }}
-    uses: onedr0p/containers/.github/workflows/build-images.yaml@main
-    secrets: inherit
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Fetch modified images
+        id: get-changed
+        shell: bash
+        run: |
+          declare -a changes_array=()
+          while read -r app
+          do
+            while read -r channel
+            do
+              change="$(jo app="$app" channel="$channel")"
+              changes_array+=($change)
+            done < <(jq --raw-output -c '.channels[] | .name' "./apps/$app/metadata.json")
+          done < <(echo '${{ needs.pr-metadata.outputs.addedOrModifiedImages }}' | jq --raw-output -c '.[]')
+
+          output="$(jo -a ${changes_array[*]})"
+          echo "changes=${output}" >> $GITHUB_OUTPUT
+
+  images-build:
+    uses: ./.github/workflows/action-image-build.yaml
+    needs: ["generate-build-matrix"]
     with:
-      appsToBuild: "${{ needs.get-changed-images.outputs.addedOrModifiedImages }}"
-      pushImages: false
-      sendNotifications: false
+      imagesToBuild: "${{ needs.generate-build-matrix.outputs.matrix }}"
+    secrets: inherit
.github/workflows/release-manual.yaml
diff --git a/.github/workflows/release-manual.yaml b/.github/workflows/release-manual.yaml
new file mode 100644
index 0000000..c5db349
--- /dev/null
+++ b/.github/workflows/release-manual.yaml
@@ -0,0 +1,67 @@
+---
+name: "Manual Release"
+
+concurrency:
+  group: container-release
+  cancel-in-progress: false
+
+on:
+  workflow_dispatch:
+    inputs:
+      app:
+        description: "App (ex: radarr)"
+        default: ""
+        required: true
+      channels:
+        description: "Channels (ex: master,develop)"
+        default: ""
+        required: true
+      push:
+        description: "Push"
+        default: "false"
+        required: true
+
+env:
+  TOKEN: ${{ secrets.TOKEN }}
+
+jobs:
+  determine-images:
+    name: Determine Images to Build
+    runs-on: ubuntu-latest
+    outputs:
+      imagesToBuild: ${{ steps.determine-images.outputs.changes }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Install tools
+        run: sudo apt-get install moreutils jo
+
+      - name: Determine images to build
+        id: determine-images
+        shell: bash
+        run: |
+          if [ "${{ inputs.app }}" = "ALL" ]; then
+            ./.github/scripts/fetch.sh all
+          else
+            output="[]"
+            IFS=',' read -a channels <<< "${{ inputs.channels }}"
+            declare -a images_array=()
+            for channel in "${channels[@]}"; do
+              image="$(jo app="${{ inputs.app }}" channel="$channel")"
+              images_array+=($image)
+            done
+
+            output="$(jo -a ${images_array[*]})"
+            echo "changes=${output}" >> $GITHUB_OUTPUT
+            echo "Changes:\n ${output}"
+          fi
+
+  images-build:
+    uses: ./.github/workflows/action-image-build.yaml
+    if: needs.determine-images.outputs.imagesToBuild != '[]'
+    needs: ["determine-images"]
+    with:
+      imagesToBuild: "${{ needs.determine-images.outputs.imagesToBuild }}"
+      pushImages: "${{ github.event.inputs.push }}"
+    secrets: inherit
.github/workflows/release-on-merge.yaml
diff --git a/.github/workflows/release-on-merge.yaml b/.github/workflows/release-on-merge.yaml
deleted file mode 100644
index 273c807..0000000
--- a/.github/workflows/release-on-merge.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: Release on Merge
-
-concurrency:
-  group: container-release
-  cancel-in-progress: false
-
-on:
-  push:
-    branches: ["main"]
-    paths:
-      - "apps/**"
-      - ".github/scripts/templates/**"
-      - "!apps/**/metadata.json"
-      - "!apps/**/metadata.yaml"
-      - "!apps/**/README.md"
-
-jobs:
-  simple-checks:
-    uses: onedr0p/containers/.github/workflows/simple-checks.yaml@main
-
-  get-changed-images:
-    needs: ["simple-checks"]
-    uses: onedr0p/containers/.github/workflows/get-changed-images.yaml@main
-
-  build-images:
-    needs: ["simple-checks", "get-changed-images"]
-    if: ${{ needs.get-changed-images.outputs.addedOrModified == 'true' }}
-    uses: onedr0p/containers/.github/workflows/build-images.yaml@main
-    secrets: inherit
-    with:
-      appsToBuild: "${{ needs.get-changed-images.outputs.addedOrModifiedImages }}"
-      pushImages: true
-      sendNotifications: true
-
-  render-readme:
-    name: Render Readme
-    needs: build-images
-    if: ${{ always() && needs.build-images.result != 'failure' }}
-    uses: ./.github/workflows/render-readme.yaml
-    secrets: inherit
.github/workflows/release-schedule.yaml
diff --git a/.github/workflows/release-schedule.yaml b/.github/workflows/release-schedule.yaml
new file mode 100644
index 0000000..0fcb28f
--- /dev/null
+++ b/.github/workflows/release-schedule.yaml
@@ -0,0 +1,51 @@
+---
+name: "Scheduled Release"
+
+concurrency:
+  group: container-release
+  cancel-in-progress: false
+
+on:
+  workflow_dispatch:
+  schedule:
+    # NOTE: The longest build for an app is ~1h30m
+    - cron: "0 */6 * * *"
+
+env:
+  TOKEN: ${{ secrets.TOKEN }}
+
+jobs:
+  determine-images:
+    name: Determine Images to Build
+    runs-on: ubuntu-latest
+    outputs:
+      imagesToBuild: ${{ steps.fetch.outputs.changes }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Install tools
+        run: sudo apt-get install moreutils jo
+
+      - name: Generate Token
+        uses: tibdex/github-app-token@v1
+        id: generate-token
+        with:
+          app_id: "${{ secrets.BOT_APP_ID }}"
+          private_key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
+
+      - name: Fetch new app versions
+        id: fetch
+        run: ./.github/scripts/fetch.sh
+        env:
+          TOKEN: "${{ steps.generate-token.outputs.token }}"
+
+  images-build:
+    uses: ./.github/workflows/action-image-build.yaml
+    if: needs.determine-images.outputs.imagesToBuild != '[]'
+    needs: ["determine-images"]
+    with:
+      imagesToBuild: "${{ needs.determine-images.outputs.imagesToBuild }}"
+      pushImages: "true"
+      sendNotification: "true"
+    secrets: inherit
.github/workflows/release-scheduled.yaml
diff --git a/.github/workflows/release-scheduled.yaml b/.github/workflows/release-scheduled.yaml
deleted file mode 100644
index 73d3906..0000000
--- a/.github/workflows/release-scheduled.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: Scheduled Release
-
-concurrency:
-  group: container-release
-  cancel-in-progress: false
-
-on:
-  workflow_dispatch:
-    inputs:
-      appsToBuild:
-        description: App(s) to build
-        required: false
-        type: string
-        default: all
-      force:
-        description: Force rebuild
-        type: boolean
-        default: false
-        required: true
-  schedule:
-    - cron: "0 * * * *"
-
-jobs:
-  simple-checks:
-    name: Simple Checks
-    uses: onedr0p/containers/.github/workflows/simple-checks.yaml@main
-
-  build-images:
-    name: Build Images
-    needs: simple-checks
-    uses: onedr0p/containers/.github/workflows/build-images.yaml@main
-    secrets: inherit
-    permissions:
-      contents: read
-      packages: write
-    with:
-      appsToBuild: ${{ inputs.appsToBuild }}
-      force: ${{ inputs.force == true }}
-      pushImages: true
-      sendNotifications: true
-
-  render-readme:
-    name: Render Readme
-    needs: build-images
-    uses: ./.github/workflows/render-readme.yaml
-    secrets: inherit
.github/workflows/render-readme.yaml
diff --git a/.github/workflows/render-readme.yaml b/.github/workflows/render-readme.yaml
deleted file mode 100644
index fe89bac..0000000
--- a/.github/workflows/render-readme.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Render Readme"
-
-on:
-  workflow_call:
-    secrets:
-      BOT_APP_ID:
-        description: The App ID of the GitHub App
-        required: true
-      BOT_APP_PRIVATE_KEY:
-        description: The private key of the GitHub App
-        required: true
-
-jobs:
-  render-readme:
-    name: Render README
-    runs-on: ubuntu-latest
-    steps:
-      - name: Generate Token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: "${{ secrets.BOT_APP_ID }}"
-          private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
-
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-
-      - name: Setup Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: 3.x
-          cache: pip
-
-      - name: Install Python Requirements
-        shell: bash
-        run: pip install -r ./.github/scripts/requirements.txt && pip freeze
-
-      - name: Render README
-        env:
-          GITHUB_TOKEN: "${{ steps.app-token.outputs.token }}"
-        shell: bash
-        run: python ./.github/scripts/render-readme.py
-
-      - name: Commit Changes
-        shell: bash
-        run: |
-          git config --global user.name "bot-ross"
-          git config --global user.email "98030736+bot-ross[bot]@users.noreply.github.com"
-          git add ./README.md
-          git commit -m "chore: render README.md" || echo "No changes to commit"
-          git push origin || echo "No changes to push"
.github/workflows/renovate-schedule.yaml
diff --git a/.github/workflows/renovate-schedule.yaml b/.github/workflows/renovate-schedule.yaml
new file mode 100644
index 0000000..16ee21d
--- /dev/null
+++ b/.github/workflows/renovate-schedule.yaml
@@ -0,0 +1,52 @@
+---
+name: "Renovate"
+
+on:
+  workflow_dispatch:
+    inputs:
+      dryRun:
+        description: "Dry-Run"
+        default: "false"
+        required: false
+      logLevel:
+        description: "Log-Level"
+        default: "debug"
+        required: false
+  schedule:
+    - cron: "0 * * * *"
+  push:
+    branches: ["main"]
+    paths:
+      - ".github/renovate.json5"
+      - ".github/renovate/**.json5"
+
+env:
+  LOG_LEVEL: debug
+  RENOVATE_DRY_RUN: false
+  RENOVATE_CONFIG_FILE: .github/renovate.json5
+
+jobs:
+  renovate:
+    name: Renovate
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Generate Token
+        uses: tibdex/github-app-token@v1
+        id: generate-token
+        with:
+          app_id: "${{ secrets.BOT_APP_ID }}"
+          private_key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
+
+      - name: Override default config from dispatch variables
+        run: |
+          echo "RENOVATE_DRY_RUN=${{ github.event.inputs.dryRun || env.RENOVATE_DRY_RUN }}" >> "${GITHUB_ENV}"
+          echo "LOG_LEVEL=${{ github.event.inputs.logLevel || env.LOG_LEVEL }}" >> "${GITHUB_ENV}"
+
+      - name: Renovate
+        uses: renovatebot/github-action@f9c81dddc9b589e4e6ae0326d1e36f6bc415d230 # v39.2.4
+        with:
+          configurationFile: "${{ env.RENOVATE_CONFIG_FILE }}"
+          token: "${{ steps.generate-token.outputs.token }}"
.github/workflows/renovate.yaml
diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml
deleted file mode 100644
index b8551e0..0000000
--- a/.github/workflows/renovate.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Renovate"
-
-on:
-  workflow_dispatch:
-    inputs:
-      dryRun:
-        description: Dry Run
-        default: "false"
-        required: false
-      logLevel:
-        description: Log Level
-        default: debug
-        required: false
-      version:
-        description: Renovate version
-        default: latest
-        required: false
-  schedule:
-    - cron: "0 * * * *" # Every hour
-  push:
-    branches: ["main"]
-    paths:
-      - .github/renovate.json5
-      - .github/renovate/**.json5
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
-  cancel-in-progress: true
-
-env:
-  LOG_LEVEL: "${{ inputs.logLevel || 'debug' }}"
-  RENOVATE_AUTODISCOVER: true
-  RENOVATE_AUTODISCOVER_FILTER: "${{ github.repository }}"
-  RENOVATE_DRY_RUN: "${{ inputs.dryRun == true }}"
-  RENOVATE_PLATFORM: github
-  RENOVATE_PLATFORM_COMMIT: true
-  WORKFLOW_RENOVATE_VERSION: "${{ inputs.version || 'latest' }}"
-
-jobs:
-  renovate:
-    name: Renovate
-    runs-on: ubuntu-latest
-    steps:
-      - name: Generate Token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: "${{ secrets.BOT_APP_ID }}"
-          private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}"
-
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          token: "${{ steps.app-token.outputs.token }}"
-
-      - name: Renovate
-        uses: renovatebot/[email protected]
-        with:
-          configurationFile: .github/renovate.json5
-          token: "${{ steps.app-token.outputs.token }}"
-          renovate-version: "${{ env.WORKFLOW_RENOVATE_VERSION }}"
.github/workflows/simple-checks.yaml
diff --git a/.github/workflows/simple-checks.yaml b/.github/workflows/simple-checks.yaml
deleted file mode 100644
index 1a7607e..0000000
--- a/.github/workflows/simple-checks.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
----
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
-name: "Simple Checks"
-
-on:
-  workflow_call:
-
-jobs:
-  metadata-validation:
-    name: Validate Image Metadata
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-
-      - name: Get Changed Files
-        id: changed-files
-        uses: tj-actions/changed-files@v44
-        with:
-          files_yaml: |
-            cue:
-              - metadata.rules.cue
-              - apps/**/metadata.json
-              - apps/**/metadata.yaml
-
-      - name: Setup CUE
-        if: ${{ steps.changed-files.outputs.cue_any_changed == 'true' }}
-        uses: cue-lang/[email protected]
-
-      # Run against all files to ensure they are tested if the cue schema is changed.
-      - name: Validate image metadata
-        if: ${{ steps.changed-files.outputs.cue_any_changed == 'true' }}
-        shell: bash
-        run: |
-          find ./apps/ -name metadata.json | xargs -I {} cue vet --schema '#Spec' {} ./metadata.rules.cue
-          find ./apps/ -name metadata.yaml | xargs -I {} cue vet --schema '#Spec' {} ./metadata.rules.cue
metadata.rules.cue
diff --git a/metadata.rules.cue b/metadata.rules.cue
index d8b53ae..b144c35 100644
--- a/metadata.rules.cue
+++ b/metadata.rules.cue
@@ -1,19 +1,18 @@
 #Spec: {
-    app: #AppName
-    semver?: bool
-    channels: [...#Channels]
+	app:  #NonEmptyString
+	base: bool
+	channels: [...#Channels]
 }
 
 #Channels: {
-    name: #ChannelName
-    platforms: [...#Platforms]
-    stable: bool
-    tests: {
-        enabled: bool
-        type?:   =~"^(cli|web)$"
-    }
+	name: #NonEmptyString
+	platforms: [...#AcceptedPlatforms]
+	stable: bool
+	tests: {
+		enabled: bool
+		type?:   =~"^(cli|web)$"
+	}
 }
 
-#AppName:     string & !="" & =~"^[a-zA-Z0-9_-]+$"
-#ChannelName: string & !="" & =~"^[a-zA-Z0-9._-]+$"
-#Platforms:   "linux/amd64" | "linux/arm64"
+#NonEmptyString:           string & !=""
+#AcceptedPlatforms:        "linux/amd64" | "linux/arm64"

Update `mc` deprecated commands

When using script, mc logs:

Adding policy ...
mc: <ERROR> Deprecated command. Please use 'mc admin policy create' instead.
Associating policy with the user ...
mc: <ERROR> Deprecated command. Please use 'mc admin policy attach' instead.

Renovate Dashboard ๐Ÿค–

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Repository problems

Renovate tried to run on this repository, but found these problems.

  • WARN: Found renovate config warnings

Errored

These updates encountered an error and will be retried. Click on a checkbox below to force a retry now.

  • chore(deps): update cue-lang/setup-cue digest to a93fa35
  • ๐Ÿ“ฃ Update ghcr.io/onedr0p/alpine:rolling Docker digest to 3fbc581
  • chore(deps): update e1himself/goss-installation-action action to v1.2.1
  • chore(deps): update actions/checkout action to v4
  • chore(deps): update docker/build-push-action action to v5
  • chore(deps): update docker/login-action action to v3
  • chore(deps): update docker/setup-buildx-action action to v3
  • chore(deps): update dorny/paths-filter action to v3
  • chore(deps): update github artifact actions to v4 (major) (actions/download-artifact, actions/upload-artifact)
  • chore(deps): update renovatebot/github-action action to v40
  • chore(deps): update tibdex/github-app-token action to v2
  • chore(deps): update tj-actions/branch-names action to v8

Detected dependencies

dockerfile
apps/actionsflow-webhook/Dockerfile
  • ghcr.io/onedr0p/alpine rolling@sha256:5d973006bf93ba66e38758694dfed6df091a13e712b6a075f7ed7bb6fd8c2189
apps/minio-init-bucket/Dockerfile
  • ghcr.io/onedr0p/alpine rolling@sha256:5d973006bf93ba66e38758694dfed6df091a13e712b6a075f7ed7bb6fd8c2189
github-actions
.github/actions/collect-changes/action.yaml
  • dorny/paths-filter v2
.github/workflows/action-image-build.yaml
  • actions/checkout v3
  • cue-lang/setup-cue 0be332bb74c8a2f07821389447ba3163e2da3bfb
  • actions/checkout v3
  • docker/setup-buildx-action v2
  • docker/login-action v2
  • e1himself/goss-installation-action v1.1.0
  • docker/build-push-action v4
  • actions/upload-artifact v3
  • actions/download-artifact v3
  • docker/setup-buildx-action v2
  • docker/login-action v2
  • sarisia/actions-status-discord v1
.github/workflows/image-rebuild.yaml
  • actions/checkout v3
  • actions/checkout v3
.github/workflows/pr-metadata.yaml
  • tj-actions/branch-names v6
  • actions/upload-artifact v3
  • actions/checkout v3
.github/workflows/pr-validate.yaml
  • actions/checkout v3
.github/workflows/release-manual.yaml
  • actions/checkout v3
.github/workflows/release-schedule.yaml
  • actions/checkout v3
  • tibdex/github-app-token v1
.github/workflows/renovate-schedule.yaml
  • actions/checkout v3
  • tibdex/github-app-token v1
  • renovatebot/github-action v39.2.4@f9c81dddc9b589e4e6ae0326d1e36f6bc415d230
.github/workflows/upstream-schedule.yaml
  • actions/checkout v3
  • tibdex/github-app-token v1
  • qlonik/create-an-issue v1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.