diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml index 46898b7..f7fbf98 100644 --- a/.github/workflows/builder.yaml +++ b/.github/workflows/builder.yaml @@ -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 diff --git a/.github/workflows/image-build.yml b/.github/workflows/image-build.yml new file mode 100644 index 0000000..aee5851 --- /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) ;; + *) 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..e8f14aa --- /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/`, 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. 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