From da8055cb79ed5398cd78c02839193f8ffbea32eb Mon Sep 17 00:00:00 2001 From: David Culbreth Date: Thu, 26 Mar 2026 08:46:18 -0500 Subject: [PATCH] publishable docker compose? --- .gitea/workflows/publish.yml | 141 +++++++++++++++++++++++++++++++++ scripts/package-docker-dist.sh | 44 ++++++++++ 2 files changed, 185 insertions(+) create mode 100644 scripts/package-docker-dist.sh diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 58313b0..0178d61 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -20,6 +20,7 @@ on: - executor - notifier - agent + - docker-dist - web default: all push: @@ -361,6 +362,146 @@ jobs: ;; esac + publish-docker-dist: + name: Publish Docker Dist Bundle + runs-on: build-amd64 + needs: metadata + if: | + github.event_name != 'workflow_dispatch' || + inputs.target_image == 'all' || + inputs.target_image == 'docker-dist' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build docker dist bundle + shell: bash + run: | + set -euo pipefail + bash scripts/package-docker-dist.sh docker/dist dist/attune-docker-dist.tar.gz + + - name: Upload docker dist archive + uses: actions/upload-artifact@v4 + with: + name: attune-docker-dist-${{ needs.metadata.outputs.image_tag }} + path: dist/attune-docker-dist.tar.gz + if-no-files-found: error + + - name: Attach docker dist archive to release + if: github.ref_type == 'tag' + shell: bash + env: + REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + run: | + set -euo pipefail + + if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then + echo "CONTAINER_REGISTRY_USERNAME and CONTAINER_REGISTRY_PASSWORD are required to attach the docker dist archive to a release" + exit 1 + fi + + api_base="${{ github.server_url }}/api/v1" + owner_repo="${{ github.repository }}" + tag_name="${{ github.ref_name }}" + archive_path="dist/attune-docker-dist.tar.gz" + asset_name="attune-docker-dist-${tag_name}.tar.gz" + + release_response_file="$(mktemp)" + status_code="$(curl -sS -o "${release_response_file}" -w '%{http_code}' \ + -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \ + "${api_base}/repos/${owner_repo}/releases/tags/${tag_name}")" + + if [ "${status_code}" = "404" ]; then + create_payload="$(TAG_NAME="${tag_name}" python3 - <<'PY' + import json + import os + + tag = os.environ["TAG_NAME"] + print(json.dumps({ + "tag_name": tag, + "name": tag, + "draft": False, + "prerelease": "-" in tag, + })) + PY + )" + + status_code="$(curl -sS -o "${release_response_file}" -w '%{http_code}' \ + -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \ + -H "Content-Type: application/json" \ + -X POST \ + -d "${create_payload}" \ + "${api_base}/repos/${owner_repo}/releases")" + fi + + case "${status_code}" in + 200|201) + ;; + *) + echo "Failed to fetch or create release for tag ${tag_name}" + cat "${release_response_file}" + exit 1 + ;; + esac + + release_id="$(python3 - "${release_response_file}" <<'PY' + import json + import sys + + with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + print(data["id"]) + PY + )" + + existing_asset_id="$(python3 - "${release_response_file}" "${asset_name}" <<'PY' + import json + import sys + + with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + name = sys.argv[2] + for asset in data.get("assets", []): + if asset.get("name") == name: + print(asset["id"]) + break + PY + )" + + if [ -n "${existing_asset_id}" ]; then + curl -sS \ + -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \ + -X DELETE \ + "${api_base}/repos/${owner_repo}/releases/${release_id}/assets/${existing_asset_id}" \ + >/dev/null + fi + + encoded_asset_name="$(ASSET_NAME="${asset_name}" python3 - <<'PY' + import os + import urllib.parse + + print(urllib.parse.quote(os.environ["ASSET_NAME"], safe="")) + PY + )" + + upload_response_file="$(mktemp)" + status_code="$(curl -sS -o "${upload_response_file}" -w '%{http_code}' \ + -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \ + -H "Content-Type: application/gzip" \ + --data-binary "@${archive_path}" \ + "${api_base}/repos/${owner_repo}/releases/${release_id}/assets?name=${encoded_asset_name}")" + + case "${status_code}" in + 201) + ;; + *) + echo "Failed to upload release asset ${asset_name}" + cat "${upload_response_file}" + exit 1 + ;; + esac + publish-rust-images: name: Publish ${{ matrix.image.name }} (${{ matrix.arch }}) runs-on: ${{ matrix.runner_label }} diff --git a/scripts/package-docker-dist.sh b/scripts/package-docker-dist.sh new file mode 100644 index 0000000..f09d5a1 --- /dev/null +++ b/scripts/package-docker-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bundle_dir="${1:-${repo_root}/docker/dist}" +archive_path="${2:-${repo_root}/dist/attune-docker-dist.tar.gz}" + +template_dir="${repo_root}/docker/dist" + +mkdir -p "${bundle_dir}/docker" "${bundle_dir}/migrations" "${bundle_dir}/packs" "${bundle_dir}/scripts" +mkdir -p "$(dirname "${archive_path}")" + +copy_file() { + local src="$1" + local dst="$2" + mkdir -p "$(dirname "${dst}")" + cp "${src}" "${dst}" +} + +# Keep the distributable compose file and README as the maintained templates. +if [ "${bundle_dir}" != "${template_dir}" ]; then + copy_file "${template_dir}/docker-compose.yaml" "${bundle_dir}/docker-compose.yaml" + copy_file "${template_dir}/README.md" "${bundle_dir}/README.md" +fi + +copy_file "${repo_root}/config.docker.yaml" "${bundle_dir}/config.docker.yaml" +copy_file "${repo_root}/docker/run-migrations.sh" "${bundle_dir}/docker/run-migrations.sh" +copy_file "${repo_root}/docker/init-user.sh" "${bundle_dir}/docker/init-user.sh" +copy_file "${repo_root}/docker/init-packs.sh" "${bundle_dir}/docker/init-packs.sh" +copy_file "${repo_root}/docker/init-roles.sql" "${bundle_dir}/docker/init-roles.sql" +copy_file "${repo_root}/docker/nginx.conf" "${bundle_dir}/docker/nginx.conf" +copy_file "${repo_root}/docker/inject-env.sh" "${bundle_dir}/docker/inject-env.sh" +copy_file "${repo_root}/scripts/load_core_pack.py" "${bundle_dir}/scripts/load_core_pack.py" + +rm -rf "${bundle_dir}/migrations" "${bundle_dir}/packs/core" +mkdir -p "${bundle_dir}/migrations" "${bundle_dir}/packs" +cp -R "${repo_root}/migrations/." "${bundle_dir}/migrations/" +cp -R "${repo_root}/packs/core" "${bundle_dir}/packs/core" + +tar -C "$(dirname "${bundle_dir}")" -czf "${archive_path}" "$(basename "${bundle_dir}")" + +echo "Docker dist bundle refreshed at ${bundle_dir}" +echo "Docker dist archive created at ${archive_path}"