diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a2734e0 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,87 @@ +name: Docker image + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +env: + IMAGE_NAME: ghcr.io/${{ github.repository }} + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - name: Run tests + run: ./scripts/test.sh + + build: + name: Build and publish + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + - name: Build and push image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true diff --git a/AGENTS.md b/AGENTS.md index 334244f..edaf7da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,4 @@ - Do not commit SSClash or Mihomo binaries to this repository. - The Dockerfile may link to official release URLs and users build the image for their own deployment. - Do not publish a prebuilt image containing SSClash without permission from its copyright holder. +- GitHub Actions may push the amd64 image to private GHCR for this deployment; do not make the package public without that permission. diff --git a/README.md b/README.md index dc6476d..5d8dbee 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,19 @@ Versions are pinned in the Dockerfile: SSClash is verified against the checksum file from its official release. Mihomo amd64 and arm64 archives are verified against pinned SHA-256 values. To update either component, update the version and checksums together, then run the complete test suite. +## GitHub Container Registry + +The GitHub Actions workflow builds `linux/amd64` only. SSClash-Go and Mihomo are downloaded and checksum-verified during the Docker build, so the resulting container never downloads executable files at startup. + +The workflow runs tests before building, publishes to `ghcr.io//mohomo-docker`, attaches SBOM and provenance, and creates these tags: + +- `latest` and `main` from the default branch; +- the Git tag and major/minor tags from releases such as `v1.2.3`; +- an immutable `sha-` tag; +- pull-request tags for build validation only, without pushing. + +Keep the GHCR package visibility **private**. The workflow deliberately does not attempt to change package visibility. Making an image containing SSClash-Go available to third parties conflicts with the upstream binary license unless the copyright holder grants permission. + ## Tests ```sh diff --git a/scripts/test.sh b/scripts/test.sh index 3513ed9..d82d032 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,6 +4,8 @@ set -eu project_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$project_root" +./tests/workflow-contract.sh + unformatted=$(gofmt -l cmd internal) if [ -n "$unformatted" ]; then echo "Go files require formatting:" >&2 diff --git a/tests/workflow-contract.sh b/tests/workflow-contract.sh new file mode 100755 index 0000000..05f16f8 --- /dev/null +++ b/tests/workflow-contract.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +workflow=.github/workflows/docker.yml +test -f "$workflow" || { + echo "missing GHCR workflow: $workflow" >&2 + exit 1 +} + +grep -F 'packages: write' "$workflow" >/dev/null +# Match the GitHub expression literally. +# shellcheck disable=SC2016 +grep -F 'ghcr.io/${{ github.repository }}' "$workflow" >/dev/null +grep -F 'platforms: linux/amd64' "$workflow" >/dev/null +grep -F 'needs: test' "$workflow" >/dev/null +# Match the GitHub expression literally. +# shellcheck disable=SC2016 +grep -F 'push: ${{ github.event_name != '\''pull_request'\'' }}' "$workflow" >/dev/null + +if grep -Ei 'arm64|setup-qemu' "$workflow" >/dev/null; then + echo "workflow must build linux/amd64 only and must not configure QEMU" >&2 + exit 1 +fi + +uses_count=$(grep -Ec '^[[:space:]]+uses:' "$workflow") +pinned_count=$(grep -Ec '^[[:space:]]+uses: [^ ]+@[0-9a-f]{40}([[:space:]]|$)' "$workflow") +if [ "$uses_count" -eq 0 ] || [ "$uses_count" -ne "$pinned_count" ]; then + echo "every GitHub Action must be pinned to a full commit SHA" >&2 + exit 1 +fi