Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 90 additions & 36 deletions .github/workflows/builder.yaml
Original file line number Diff line number Diff line change
@@ -1,52 +1,106 @@
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
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; 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' && github.ref == 'refs/heads/main' && github.repository == 'enwaiax/awesome-docker'
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
142 changes: 142 additions & 0 deletions .github/workflows/image-build.yml
Original file line number Diff line number Diff line change
@@ -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) ;;
*) 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<<EOF"
echo "$image:sha-${GITHUB_SHA::12}"
if [[ "$PUBLISH" == "true" ]]; then
echo "$image:latest"
if [[ -n "$TAG" ]]; then
echo "$image:$TAG"
fi
fi
echo "EOF"
} >> "$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
36 changes: 36 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions docs/image-publishing.md
Original file line number Diff line number Diff line change
@@ -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/`, 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.

## Manual dry run

Open **Actions → Docker image supply chain → Run workflow** on the `main` branch 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`. `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:

- `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.
Loading
Loading