From 1d7239e80230fef9508f90faf05741e3e58e36f2 Mon Sep 17 00:00:00 2001 From: Shawn Wang <32839114+enwaiax@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:33:04 +0800 Subject: [PATCH 1/3] ci: modernize Docker image supply chain --- .github/workflows/builder.yaml | 127 +++++++++++++++++------- .github/workflows/image-build.yml | 142 +++++++++++++++++++++++++++ .github/workflows/validate.yml | 36 +++++++ README.md | 1 + docs/image-publishing.md | 42 ++++++++ maddy/Dockerfile | 2 +- netease-cloud-music-tasks/Dockerfile | 2 +- 7 files changed, 314 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/image-build.yml create mode 100644 docs/image-publishing.md diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml index 46898b7..395b7bb 100644 --- a/.github/workflows/builder.yaml +++ b/.github/workflows/builder.yaml @@ -1,52 +1,107 @@ -name: "docker build release" +name: Docker image supply chain -on: +on: + pull_request: + paths: + - 'maddy/**' + - 'download-bot/**' + - 'netease-cloud-music-tasks/**' + - 'x-ui/**' + - '.github/workflows/builder.yaml' + - '.github/workflows/image-build.yml' workflow_dispatch: inputs: project: - description: 'Project' + description: Image to build + type: choice required: true - default: + options: + - maddy + - download-bot + - netease-cloud-music-tasks + - x-ui + publish: + description: Push the multi-architecture image to Docker Hub + type: boolean + required: true + default: false + tag: + description: Additional Docker tag (letters, numbers, dot, dash and underscore only) + type: string + required: false + default: '' + +permissions: + contents: read + +concurrency: + group: images-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - build: + changes: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + any: ${{ steps.matrix.outputs.any }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v7 with: - submodules: true + fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Docker Hub login + - name: Select affected images + id: matrix env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - echo "${DOCKERHUB_TOKEN}" | docker login --username ${DOCKERHUB_USERNAME} --password-stdin + set -euo pipefail + mapfile -t files < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + projects=() - - name: Set up Docker Buildx - id: buildx - uses: crazy-max/ghaction-docker-buildx@v1 - with: - buildx-version: latest + workflow_changed=false + for file in "${files[@]}"; do + if [[ "$file" == ".github/workflows/builder.yaml" || "$file" == ".github/workflows/image-build.yml" ]]; then + workflow_changed=true + fi + done - - name: Build Dockerfile - env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - run: | - docker buildx build \ - --platform=linux/amd64,linux/arm64 \ - --output "type=image,push=true" \ - --file ${{ github.event.inputs.project }}/Dockerfile ./${{ github.event.inputs.project }} \ - --tag $(echo "${DOCKERHUB_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ github.event.inputs.project }}:latest - - - name: Sync README.md - uses: ms-jpq/sync-dockerhub-readme@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - repository: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.inputs.project }} - readme: "${{ github.event.inputs.project }}/README.md" + for project in maddy download-bot netease-cloud-music-tasks x-ui; do + if [[ "$workflow_changed" == true ]] || printf '%s\n' "${files[@]}" | grep -q "^${project}/"; then + projects+=("$project") + fi + done + + matrix=$(printf '%s\n' "${projects[@]}" | jq -R . | jq -sc '{project: map(select(length > 0))}') + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + if (( ${#projects[@]} > 0 )); then + echo "any=true" >> "$GITHUB_OUTPUT" + else + echo "any=false" >> "$GITHUB_OUTPUT" + fi + + validate: + needs: changes + if: github.event_name == 'pull_request' && needs.changes.outputs.any == 'true' + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.changes.outputs.matrix) }} + uses: ./.github/workflows/image-build.yml + with: + project: ${{ matrix.project }} + platforms: linux/amd64 + publish: false + + publish: + if: github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/image-build.yml + permissions: + contents: read + with: + project: ${{ inputs.project }} + platforms: linux/amd64,linux/arm64 + publish: ${{ inputs.publish }} + tag: ${{ inputs.tag }} + secrets: inherit diff --git a/.github/workflows/image-build.yml b/.github/workflows/image-build.yml new file mode 100644 index 0000000..63f8b8d --- /dev/null +++ b/.github/workflows/image-build.yml @@ -0,0 +1,142 @@ +name: Reusable image build + +on: + workflow_call: + inputs: + project: + description: Repository project directory and image name + type: string + required: true + platforms: + description: Comma-separated target platforms + type: string + required: true + publish: + description: Push to Docker Hub + type: boolean + required: true + tag: + description: Optional additional image tag + type: string + required: false + default: '' + secrets: + DOCKERHUB_USERNAME: + required: false + DOCKERHUB_TOKEN: + required: false + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Validate controlled inputs + env: + PROJECT: ${{ inputs.project }} + PLATFORMS: ${{ inputs.platforms }} + PUBLISH: ${{ inputs.publish }} + TAG: ${{ inputs.tag }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + set -euo pipefail + case "$PROJECT" in + maddy|download-bot|netease-cloud-music-tasks|x-ui) ;; + *) echo "Unsupported project: $PROJECT" >&2; exit 1 ;; + esac + if [[ "$PLATFORMS" != "linux/amd64" && "$PLATFORMS" != "linux/amd64,linux/arm64" ]]; then + echo "Unsupported platform set: $PLATFORMS" >&2 + exit 1 + fi + if [[ -n "$TAG" && ! "$TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then + echo "Invalid Docker tag: $TAG" >&2 + exit 1 + fi + if [[ "$PUBLISH" == "true" && ( -z "$DOCKERHUB_USERNAME" || -z "$DOCKERHUB_TOKEN" ) ]]; then + echo "Docker Hub credentials are required for publishing" >&2 + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Set up QEMU + if: contains(inputs.platforms, ',') + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + if: inputs.publish + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Prepare image metadata + id: meta + env: + PROJECT: ${{ inputs.project }} + PUBLISH: ${{ inputs.publish }} + TAG: ${{ inputs.tag }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + run: | + set -euo pipefail + if [[ "$PUBLISH" == "true" ]]; then + namespace=$(printf '%s' "$DOCKERHUB_USERNAME" | tr '[:upper:]' '[:lower:]') + else + namespace=awesome-docker-ci + fi + image="$namespace/$PROJECT" + { + echo "image=$image" + echo "tags<> "$GITHUB_OUTPUT" + + - name: Build image + id: build + uses: docker/build-push-action@v7 + with: + context: ${{ inputs.project }} + file: ${{ inputs.project }}/Dockerfile + platforms: ${{ inputs.platforms }} + push: ${{ inputs.publish }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha,scope=${{ inputs.project }} + cache-to: type=gha,mode=max,scope=${{ inputs.project }} + provenance: ${{ inputs.publish && 'mode=max' || false }} + sbom: ${{ inputs.publish }} + + - name: Verify published manifest platforms + if: inputs.publish + env: + IMAGE: ${{ steps.meta.outputs.image }} + run: | + set -euo pipefail + docker buildx imagetools inspect "$IMAGE:latest" > manifest.txt + grep -q 'linux/amd64' manifest.txt + grep -q 'linux/arm64' manifest.txt + + - name: Update Docker Hub description + if: inputs.publish + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ steps.meta.outputs.image }} + readme-filepath: ${{ inputs.project }}/README.md + enable-url-completion: true diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4ecee9e..63e7a46 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -6,18 +6,53 @@ on: - 'site/**' - '**/*.md' - '.github/workflows/validate.yml' + - '.github/workflows/builder.yaml' + - '.github/workflows/image-build.yml' - '.github/ISSUE_TEMPLATE/**' + - 'maddy/**' + - 'download-bot/**' + - 'netease-cloud-music-tasks/**' + - 'x-ui/**' push: branches: [main] paths: - 'site/**' - '**/*.md' - '.github/workflows/validate.yml' + - '.github/workflows/builder.yaml' + - '.github/workflows/image-build.yml' + - 'maddy/**' + - 'download-bot/**' + - 'netease-cloud-music-tasks/**' + - 'x-ui/**' permissions: contents: read jobs: + dockerfiles: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: [maddy, download-bot, netease-cloud-music-tasks, x-ui] + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Validate Dockerfile + uses: hadolint/hadolint-action@v3.4.0 + with: + dockerfile: ${{ matrix.project }}/Dockerfile + failure-threshold: error + ignore: DL3020 + + - name: Validate Compose file + if: hashFiles(format('{0}/docker-compose.yml', matrix.project)) != '' + run: docker compose -f "${{ matrix.project }}/docker-compose.yml" config -q + website: runs-on: ubuntu-latest defaults: @@ -59,6 +94,7 @@ jobs: README.md CONTRIBUTING.md SECURITY.md + docs/image-publishing.md download-bot/README.md maddy/README.md maddy/rainloop/README.md diff --git a/README.md b/README.md index 988d4ce..b9160e6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The new Astro website provides searchable project cards, consistent quick-start - [Contributing guide](CONTRIBUTING.md) - [Security policy](SECURITY.md) +- [Image publishing workflow](docs/image-publishing.md) ### Docker usage notes diff --git a/docs/image-publishing.md b/docs/image-publishing.md new file mode 100644 index 0000000..9535443 --- /dev/null +++ b/docs/image-publishing.md @@ -0,0 +1,42 @@ +# Image publishing + +Docker images are validated on pull requests and published only by an explicit manual workflow run. + +## Pull-request validation + +Changes under `maddy/`, `download-bot/`, `netease-cloud-music-tasks/`, or `x-ui/` trigger an AMD64 Buildx build without registry credentials and without pushing an image. A change to the supply-chain workflow validates all four projects. + +The repository validation workflow also runs Hadolint and parses available Compose files. + +## Manual dry run + +Open **Actions → Docker image supply chain → Run workflow** and select: + +- a project from the controlled list; +- `publish: false`; +- no additional tag. + +The workflow builds AMD64 and ARM64 with QEMU but does not log in or push. + +## Publishing + +Publishing requires repository secrets `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. + +Run the same workflow with `publish: true`. It publishes: + +- `latest`; +- `sha-<12-character commit>`; +- the optional validated tag. + +Published images include BuildKit provenance and an SBOM. The workflow then verifies that the manifest contains `linux/amd64` and `linux/arm64`, and updates the Docker Hub description from the project's README. + +## Safety properties + +- project names are selected from a fixed allow-list; +- arbitrary paths and platform strings are rejected; +- pull requests never receive Docker Hub credentials; +- registry login and README synchronization occur only during explicit publishing; +- per-project GitHub Actions cache scopes prevent cache collisions; +- concurrent runs on the same branch cancel older work. + +Do not publish an archived project merely because it still builds. A successful build is not a security or maintenance guarantee. diff --git a/maddy/Dockerfile b/maddy/Dockerfile index 36fd439..b0ad343 100644 --- a/maddy/Dockerfile +++ b/maddy/Dockerfile @@ -6,7 +6,7 @@ RUN set -ex ;\ WORKDIR /maddy ADD ./maddy ./ -ENV LDFLAGS -static +ENV LDFLAGS=-static RUN go mod download RUN mkdir -p /pkg/data COPY ./maddy/maddy.conf /pkg/data/maddy.conf diff --git a/netease-cloud-music-tasks/Dockerfile b/netease-cloud-music-tasks/Dockerfile index eea7341..9913cf0 100644 --- a/netease-cloud-music-tasks/Dockerfile +++ b/netease-cloud-music-tasks/Dockerfile @@ -1,4 +1,4 @@ -FROM python:alpine as builder +FROM python:alpine AS builder RUN apk update && apk add --no-cache tzdata alpine-sdk libffi-dev ADD ./NeteaseCloudMusicTasks/requirements.txt /tmp/requirements.txt From 96c256160e088f65fb4ad2cb93c92a0d1594f18e Mon Sep 17 00:00:00 2001 From: Shawn Wang <32839114+enwaiax@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:39:50 +0800 Subject: [PATCH 2/3] ci: gate publishing to reproducible images --- .github/workflows/builder.yaml | 3 +-- .github/workflows/image-build.yml | 2 +- docs/image-publishing.md | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml index 395b7bb..0e29a58 100644 --- a/.github/workflows/builder.yaml +++ b/.github/workflows/builder.yaml @@ -19,7 +19,6 @@ on: - maddy - download-bot - netease-cloud-music-tasks - - x-ui publish: description: Push the multi-architecture image to Docker Hub type: boolean @@ -95,7 +94,7 @@ jobs: publish: false publish: - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && github.repository == 'enwaiax/awesome-docker' uses: ./.github/workflows/image-build.yml permissions: contents: read diff --git a/.github/workflows/image-build.yml b/.github/workflows/image-build.yml index 63f8b8d..aee5851 100644 --- a/.github/workflows/image-build.yml +++ b/.github/workflows/image-build.yml @@ -44,7 +44,7 @@ jobs: run: | set -euo pipefail case "$PROJECT" in - maddy|download-bot|netease-cloud-music-tasks|x-ui) ;; + maddy|download-bot|netease-cloud-music-tasks) ;; *) echo "Unsupported project: $PROJECT" >&2; exit 1 ;; esac if [[ "$PLATFORMS" != "linux/amd64" && "$PLATFORMS" != "linux/amd64,linux/arm64" ]]; then diff --git a/docs/image-publishing.md b/docs/image-publishing.md index 9535443..46fcb1e 100644 --- a/docs/image-publishing.md +++ b/docs/image-publishing.md @@ -10,7 +10,7 @@ The repository validation workflow also runs Hadolint and parses available Compo ## Manual dry run -Open **Actions → Docker image supply chain → Run workflow** and select: +Open **Actions → Docker image supply chain → Run workflow** on the `main` branch and select: - a project from the controlled list; - `publish: false`; @@ -20,7 +20,7 @@ The workflow builds AMD64 and ARM64 with QEMU but does not log in or push. ## Publishing -Publishing requires repository secrets `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. +Publishing requires repository secrets `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. `x-ui` is intentionally excluded from manual publishing because its current Dockerfile clones a floating upstream revision instead of building the repository-pinned source. Run the same workflow with `publish: true`. It publishes: From ea1f984d29cbf71197fe196bf435f6b3fd29a257 Mon Sep 17 00:00:00 2001 From: Shawn Wang <32839114+enwaiax@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:42:16 +0800 Subject: [PATCH 3/3] ci: exclude non-reproducible x-ui image builds --- .github/workflows/builder.yaml | 2 +- docs/image-publishing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml index 0e29a58..f7fbf98 100644 --- a/.github/workflows/builder.yaml +++ b/.github/workflows/builder.yaml @@ -67,7 +67,7 @@ jobs: fi done - for project in maddy download-bot netease-cloud-music-tasks x-ui; do + for project in maddy download-bot netease-cloud-music-tasks; do if [[ "$workflow_changed" == true ]] || printf '%s\n' "${files[@]}" | grep -q "^${project}/"; then projects+=("$project") fi diff --git a/docs/image-publishing.md b/docs/image-publishing.md index 46fcb1e..e8f14aa 100644 --- a/docs/image-publishing.md +++ b/docs/image-publishing.md @@ -4,7 +4,7 @@ Docker images are validated on pull requests and published only by an explicit m ## Pull-request validation -Changes under `maddy/`, `download-bot/`, `netease-cloud-music-tasks/`, or `x-ui/` trigger an AMD64 Buildx build without registry credentials and without pushing an image. A change to the supply-chain workflow validates all four projects. +Changes under `maddy/`, `download-bot/`, or `netease-cloud-music-tasks/` trigger an AMD64 Buildx build without registry credentials and without pushing an image. A change to the supply-chain workflow validates all three publishable projects. `x-ui` still receives static Dockerfile and Compose checks, but image builds are intentionally excluded until its source inputs are reproducible. The repository validation workflow also runs Hadolint and parses available Compose files.