diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 8daf00f..bc27637 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -47,6 +47,11 @@ jobs: - name: Generate Docker tags id: tags + env: + GITHUB_REF: ${{ github.ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITEA_REF: ${{ github.ref }} + GITEA_REF_NAME: ${{ github.ref_name }} run: | sh scripts/ci/docker-tags.sh "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" /tmp/docker-tags.txt TAGS="$(tr '\n' ' ' < /tmp/docker-tags.txt)" @@ -54,12 +59,42 @@ jobs: FIRST_TAG="$(head -1 /tmp/docker-tags.txt | sed 's/^-t //')" echo "first_tag=${FIRST_TAG}" >> "$GITHUB_OUTPUT" - - name: Build and push Docker image + - name: OCI labels (build metadata) + id: oci + env: + GITHUB_REF: ${{ github.ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITEA_REF: ${{ github.ref }} + GITEA_REF_NAME: ${{ github.ref_name }} run: | + set -eu + echo "created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT" + echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + ref="${GITEA_REF:-${GITHUB_REF:-}}" + ref_name="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}" + case "$ref" in + refs/tags/*) + echo "version=${ref_name}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "version=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Build and push Docker image + env: + OCI_REVISION: ${{ steps.oci.outputs.revision }} + OCI_VERSION: ${{ steps.oci.outputs.version }} + OCI_CREATED: ${{ steps.oci.outputs.created }} + run: | + set -eu docker buildx build \ --platform linux/amd64,linux/arm64 \ --push \ --no-cache \ + --build-arg "OCI_REVISION=${OCI_REVISION}" \ + --build-arg "OCI_VERSION=${OCI_VERSION}" \ + --build-arg "OCI_CREATED=${OCI_CREATED}" \ ${{ steps.tags.outputs.tags }} \ -f ./Dockerfile . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..021df23 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,149 @@ +# Build multi-arch image, push to GHCR, and keyless-sign the manifest (Cosign). +# +# Pinned third-party actions (bump tag and SHA together when upgrading). +# Automated check: first step resolves each tag via api.github.com and +# compares to the commit below. Manual bump helpers (resolve annotated tags): +# +# curl -sSf -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GH_TOKEN" \ +# "https://api.github.com/repos/OWNER/REPO/git/refs/tags/TAG" | jq . +# # if object.type is "tag", follow object.url and jq -r .object.sha +# +# Pinned refs: +# actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 +# docker/setup-qemu-action@v3.7.0 c7c53464625b32c7a7e944ae62b3e17d2b600130 +# docker/setup-buildx-action@v3.11.1 e468171a9de216ec08956ac3ada2f0791b6bd435 +# docker/login-action@v3.5.0 184bdaa0721073962dff0199f1fb9940f07167d1 +# docker/build-push-action@v6.18.0 263435318d21b8e681c14492fe198d362a7d2c83 +# sigstore/cosign-installer@v3.10.1 7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 + +name: Docker (GHCR) + +on: + workflow_dispatch: + push: + tags: + - "*" + +permissions: + contents: read + packages: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Verify action pins (GitHub API) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + hdr=(-H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${GH_TOKEN}") + api="https://api.github.com/repos" + resolve() { + local repo="$1" tag="$2" ref typ url + ref=$(curl -sSf "${hdr[@]}" "${api}/${repo}/git/refs/tags/${tag}") + typ=$(printf '%s' "$ref" | jq -r .object.type) + if [ "$typ" = "commit" ]; then + printf '%s' "$ref" | jq -r .object.sha + return + fi + url=$(printf '%s' "$ref" | jq -r .object.url) + curl -sSf "${hdr[@]}" "$url" | jq -r .object.sha + } + check() { + local repo="$1" tag="$2" want="$3" got + got=$(resolve "$repo" "$tag") + if [ "$got" != "$want" ]; then + printf 'Pin mismatch %s@%s: expected %s got %s\n' "$repo" "$tag" "$want" "$got" >&2 + exit 1 + fi + printf 'OK %s@%s -> %s\n' "$repo" "$tag" "$got" + } + check actions/checkout v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 + check docker/setup-qemu-action v3.7.0 c7c53464625b32c7a7e944ae62b3e17d2b600130 + check docker/setup-buildx-action v3.11.1 e468171a9de216ec08956ac3ada2f0791b6bd435 + check docker/login-action v3.5.0 184bdaa0721073962dff0199f1fb9940f07167d1 + check docker/build-push-action v6.18.0 263435318d21b8e681c14492fe198d362a7d2c83 + check sigstore/cosign-installer v3.10.1 7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 + + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: OCI label timestamps and version + id: oci + run: | + set -euo pipefail + echo "created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT" + if [ "${{ github.ref_type }}" = "tag" ]; then + echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + else + echo "version=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + fi + + - name: Registry image (GHCR lowercase) + id: image + run: | + set -euo pipefail + lower="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "name=${REGISTRY}/${lower}" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + + - name: Log in to GHCR + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Generate Docker tags + id: tags + env: + GITHUB_REF: ${{ github.ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + sh scripts/ci/docker-tags.sh "${{ steps.image.outputs.name }}" /tmp/docker-tags.txt + { + echo 'tags<> "$GITHUB_OUTPUT" + + - name: Build and push + id: build + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + build-args: | + OCI_REVISION=${{ github.sha }} + OCI_VERSION=${{ steps.oci.outputs.version }} + OCI_CREATED=${{ steps.oci.outputs.created }} + + - name: Install Cosign + uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 + + - name: Cosign sign (keyless) + env: + COSIGN_YES: "true" + run: | + set -euo pipefail + test -n "${{ steps.build.outputs.digest }}" + cosign sign "${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}" diff --git a/Dockerfile b/Dockerfile index 3d697cd..1c4189b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,6 +58,10 @@ RUN pip install --no-cache-dir . && \ # ---- STAGE 3: Final Image ---- FROM ${PYTHON_IMAGE}@${PYTHON_HASH} +ARG OCI_REVISION="" +ARG OCI_VERSION="" +ARG OCI_CREATED="" + RUN apk upgrade --no-cache && \ apk add --no-cache opusfile libffi espeak-ng su-exec && \ python -m pip install --no-cache-dir --upgrade "pip>=26.0" "setuptools" "jaraco.context>=6.1.0" && \ @@ -69,6 +73,14 @@ COPY --from=builder --chown=meshchat:meshchat /opt/venv /opt/venv COPY scripts/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh +LABEL org.opencontainers.image.source="https://git.quad4.io/RNS-Things/MeshChatX" +LABEL org.opencontainers.image.description="All in one Reticulum client." +LABEL org.opencontainers.image.licenses="MIT AND 0BSD" +LABEL org.opencontainers.image.authors="Quad4" +LABEL org.opencontainers.image.revision="${OCI_REVISION}" +LABEL org.opencontainers.image.version="${OCI_VERSION}" +LABEL org.opencontainers.image.created="${OCI_CREATED}" + ENV PATH="/opt/venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1