diff --git a/.github/actions/setup-buildx/action.yml b/.github/actions/setup-buildx/action.yml new file mode 100644 index 0000000000..ba8981474d --- /dev/null +++ b/.github/actions/setup-buildx/action.yml @@ -0,0 +1,79 @@ +name: Set up Buildx +description: >- + Logs in to Docker Hub with a short-lived OIDC token and sets up a Buildx + builder, Docker Build Cloud by default. Requires `id-token: write`. + +inputs: + connection-id: + description: Docker Hub OIDC connection id. + required: true + expected-sha: + description: Optional 40-character commit SHA the run must be on. + required: false + default: '' + driver: + description: Buildx driver; `cloud` or `docker-container`. + required: false + default: cloud + endpoint: + description: Build Cloud endpoint (cloud driver only). + required: false + default: docker/docker-agent + builder: + description: Create a builder. Set false for registry-only work such as imagetools. + required: false + default: 'true' + +outputs: + builder: + description: Builder name to pass to build-push-action (empty when builder is false). + value: ${{ steps.buildx.outputs.name }} + +runs: + using: composite + steps: + - shell: bash + env: + CONNECTION_ID: ${{ inputs.connection-id }} + EXPECTED_SHA: ${{ inputs.expected-sha }} + run: | + set -euo pipefail + if [[ -n "$EXPECTED_SHA" ]]; then + if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "expected-sha must be a lowercase 40-character SHA" >&2 + exit 1 + fi + if [[ "$EXPECTED_SHA" != "$GITHUB_SHA" ]]; then + echo "expected-sha does not match the workflow run commit $GITHUB_SHA" >&2 + exit 1 + fi + fi + if [[ -z "$CONNECTION_ID" ]]; then + echo "DOCKERHUB_OIDC_CONNECTION_ID must be configured" >&2 + exit 1 + fi + if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then + echo "GitHub Actions OIDC is unavailable; the job needs id-token: write" >&2 + exit 1 + fi + + - id: oidc + uses: docker/oidc-action@96ba694c64860c7209bcd1ede7d698c71564ef78 # v1.2.0 + with: + connection-id: ${{ inputs.connection-id }} + expires-in: 3600 + + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: docker + password: ${{ steps.oidc.outputs.token }} + + - id: buildx + if: inputs.builder == 'true' + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + with: + driver: ${{ inputs.driver }} + endpoint: ${{ inputs.driver == 'cloud' && inputs.endpoint || '' }} + version: ${{ inputs.driver == 'cloud' && 'lab:edge' || '' }} + # Downloading buildx takes seconds; caching it costs ~40 MB per branch. + cache-binary: false diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml new file mode 100644 index 0000000000..609dc99ea6 --- /dev/null +++ b/.github/actions/setup-go/action.yml @@ -0,0 +1,81 @@ +name: Set up Go +description: >- + Installs Go and Task, then restores two caches: the module cache, shared by + every job and saved once per go.sum change, and the build cache, keyed per + job with the commit SHA as suffix so each job's compiled packages stay warm. + setup-go's built-in cache is keyed on go.sum alone, so a single snapshot is + shared by every job and never refreshed once saved. Caches are saved on main + only, when the job ends and only if `save` is true; everywhere else they are + restore-only so PR branches never fill the cache quota. + +inputs: + cache-name: + description: Build cache namespace; defaults to the job id. + required: false + default: ${{ github.job }} + save: + description: Save the build cache on main. Set false for jobs that share another job's namespace. + required: false + default: 'true' + task: + description: Install Task. + required: false + default: 'true' + +runs: + using: composite + steps: + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + + - if: inputs.task == 'true' + uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0 + with: + version: 3.53.1 + # task_checksums.txt of the release above: task_linux_amd64.tar.gz / task_windows_amd64.zip. + checksum: ${{ runner.os == 'Windows' && '27c0cd248c12cba03d8958d954a3df981c900be885ec9ce5f6a3cdc4e9a19316' || 'a54a408f6861ff921f6e87774180db31bacd8c1e7c944ca696db9fea49a82fc7' }} + + - id: paths + shell: bash + run: | + { + echo "mod=$(go env GOMODCACHE)" + echo "build=$(go env GOCACHE)" + } >> "$GITHUB_OUTPUT" + + # Module cache: exact key per go.sum, so main saves it once per dependency + # change and every job on every ref restores the same entry. + - if: github.ref == 'refs/heads/main' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.paths.outputs.mod }} + key: gomod-${{ runner.os }}-${{ hashFiles('go.sum') }} + restore-keys: gomod-${{ runner.os }}- + + - if: github.ref != 'refs/heads/main' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.paths.outputs.mod }} + key: gomod-${{ runner.os }}-${{ hashFiles('go.sum') }} + restore-keys: gomod-${{ runner.os }}- + + # Build cache: per job, SHA suffix so main refreshes it on every push. + - if: github.ref == 'refs/heads/main' && inputs.save == 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.paths.outputs.build }} + key: gobuild-${{ runner.os }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.sha }} + restore-keys: | + gobuild-${{ runner.os }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}- + gobuild-${{ runner.os }}-${{ inputs.cache-name }}- + + - if: github.ref != 'refs/heads/main' || inputs.save != 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.paths.outputs.build }} + key: gobuild-${{ runner.os }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.sha }} + restore-keys: | + gobuild-${{ runner.os }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}- + gobuild-${{ runner.os }}-${{ inputs.cache-name }}- diff --git a/.github/actions/setup-hugo/action.yml b/.github/actions/setup-hugo/action.yml new file mode 100644 index 0000000000..0859e35f69 --- /dev/null +++ b/.github/actions/setup-hugo/action.yml @@ -0,0 +1,22 @@ +name: Set up Hugo +description: >- + Installs the Hugo release every docs workflow builds with, verifying it + against the release's published checksums. Kept in sync with the docker/docs + HUGO_VERSION pin so docs.docker.com and github.io render the same way. + +runs: + using: composite + steps: + - shell: bash + env: + HUGO_VERSION: 0.163.0 + # hugo__checksums.txt entry for hugo_extended__linux-amd64.tar.gz. + HUGO_SHA256: 6291775b7d012f9b10fb377ba914e5b1589c0b0d2531b695fa9029be14750111 + run: | + set -euo pipefail + archive="hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + curl -fsSLo "${RUNNER_TEMP}/${archive}" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${archive}" + echo "${HUGO_SHA256} ${RUNNER_TEMP}/${archive}" | sha256sum -c - + sudo tar -xzf "${RUNNER_TEMP}/${archive}" -C /usr/local/bin hugo + hugo version diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..3c26d41162 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + # Workflows and the composite actions under .github/actions. Pins are SHAs; + # Dependabot bumps the SHA and its version comment. A version outside the org + # allow list fails CI visibly. Go modules are deliberately not here: the + # bump-go-dependencies skill handles them. + - package-ecosystem: github-actions + directories: + - / + - /.github/actions/* + schedule: + interval: weekly + day: monday + # Let a release settle before adopting it. + cooldown: + default-days: 7 + groups: + actions: + patterns: ["*"] + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4a1543d8b..5d9b07c9a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,20 @@ +# Quality gate for every PR and the release pipeline for main and v* tags. +# +# `gate` is the only check the main ruleset needs to require: it fails when any +# job it needs failed or was cancelled and passes when they succeeded or were +# skipped (docs-only PRs skip every Go job). Add a job to +# `gate.needs` to make it blocking; the ruleset never has to change. name: ci permissions: contents: read +# Main, tag and merge-queue runs get a group of their own so they are never +# queued behind or cancelled by another run; PR runs supersede the previous +# run on the ref. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + group: ${{ github.workflow }}-${{ github.event_name != 'pull_request' && github.run_id || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: workflow_dispatch: @@ -27,599 +36,406 @@ on: branches: [ main ] tags: [ "v*" ] pull_request: + merge_group: jobs: + # Docs-only PRs (every changed file under docs/) skip the Go jobs. Markdown + # elsewhere is test data, so only docs/ counts. Anything but a PR runs all. + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ github.event_name != 'pull_request' || steps.filter.outputs.docs_count != steps.filter.outputs.all_count }} + steps: + - id: filter + if: github.event_name == 'pull_request' + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + with: + filters: | + docs: + - 'docs/**' + all: + - '**' + lint: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: $/.github/actions/setup-go with: - go-version-file: go.mod - cache-dependency-path: go.sum + cache-name: test-linux + save: 'false' + task: 'false' - - name: Lint + - name: golangci-lint uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.13.1 + version: v2.13.2 + + # The rest of `task lint`: project cops and go.mod tidiness. + - name: Project cops + run: go run ./lint . + + - name: go mod tidy + run: go mod tidy --diff >/dev/null || (echo 'go.mod/go.sum are not tidy' && exit 1) - - name: Lint GitHub Actions + - name: Shell scripts + run: | + shellcheck scripts/*.sh + ./scripts/models-delta-test.sh + + - name: GitHub Actions uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 with: + version: 1.7.12 fail-on-error: true pyflakes: false + # actionlint does not know GitHub's `$/` same-repository action syntax + # yet. The action splits flags on whitespace, hence the \s pattern. + flags: -ignore action\s.\$/\S+.\sin\sinvalid\sformat - - name: Lint GitHub Workflows + # Static security audit of workflows and composite actions; config in + # .github/zizmor.yml. Informational findings do not fail the job. + - name: zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + advanced-security: false + annotations: true + min-severity: low + + - name: Workflow invariants run: ./scripts/workflow-lint.sh - build-and-test: + test-linux: + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Install Task - uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - with: - version: 3.51.1 + persist-credentials: false - - name: Create bin directory - run: mkdir -p "$HOME/bin" + - uses: $/.github/actions/setup-go - name: Build run: task build - - name: Cross-compile plan storage - run: task check-plan-cross - - - name: Run tests - run: | - task test - task test-binary + - name: Test + run: task test - - name: Test WASM provider registration - run: task test-wasm-providers + - name: Test binary + run: task test-binary - # Native Windows tests. Plan storage relies on OS-specific file locking and - # path semantics, so the full Go test suite runs natively on Windows in a - # single blocking `task test` step, mirroring the Linux job. - windows-tests: + # Plan storage relies on OS-specific file locking and path semantics, so the + # full suite also runs natively on Windows. + test-windows: + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: windows-latest timeout-minutes: 30 steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - go-version-file: go.mod - cache-dependency-path: go.sum + persist-credentials: false - - name: Install Task - uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - with: - version: 3.51.1 + - uses: $/.github/actions/setup-go - - name: Go environment - run: go env GOOS GOARCH CGO_ENABLED CC + # Real-time scanning of every compiled object and test binary is the + # dominant I/O cost on hosted Windows runners. + - name: Exclude build paths from Defender + continue-on-error: true + shell: pwsh + run: | + $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, (go env GOROOT), (go env GOPATH), (go env GOCACHE), (go env GOMODCACHE)) + Add-MpPreference -ExclusionPath $paths + Get-MpPreference | Select-Object -ExpandProperty ExclusionPath - - name: Run tests + - name: Test run: task test - license-check: + # Compile-only checks kept out of `test-linux` so they run alongside it. + cross-compile: + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Install go-licences - run: go install github.com/google/go-licenses@latest - - - name: Check licenses - run: go-licenses check . --allowed_licenses=Apache-2.0,MIT,BSD-3-Clause,BSD-2-Clause --ignore modernc.org/mathutil --ignore github.com/hashicorp/hcl/v2 - - build-image: - if: >- - github.repository == 'docker/docker-agent' && - github.event.repository.fork == false && ( - (github.event_name == 'pull_request' && - github.event.pull_request.base.repo.full_name == github.repository && - github.event.pull_request.head.repo.full_name == github.repository) || - (github.event_name == 'workflow_dispatch' && - startsWith(github.ref, 'refs/heads/') && - (github.ref != 'refs/heads/main' || inputs.image_mode == 'build-only') && - (inputs.image_mode == '' || inputs.image_mode == 'auto' || inputs.image_mode == 'build-only')) - ) - permissions: - contents: read - id-token: write - timeout-minutes: 30 - env: - DOCKER_BUILD_CLOUD_ENDPOINT: docker/docker-agent - DOCKERHUB_OIDC_CONNECTION_ID: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} - DOCKER_BUILD_RECORD_UPLOAD: 'false' - DOCKER_BUILD_SUMMARY: 'false' - # Cloud workers compile each native platform in parallel; GitHub runners - # only orchestrate the builds. - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ github.sha }} persist-credentials: false - - name: Validate Docker Build Cloud configuration - env: - EXPECTED_SHA: ${{ github.sha }} - EXPECTED_INPUT_SHA: ${{ inputs.expected_sha }} - shell: bash - run: | - set -euo pipefail - if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - echo "Workflow run commit must be a lowercase 40-character SHA" >&2 - exit 1 - fi - if [[ -n "${EXPECTED_INPUT_SHA}" ]]; then - if [[ ! "${EXPECTED_INPUT_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - echo "expected_sha must be a lowercase 40-character SHA" >&2 - exit 1 - fi - if [[ "${EXPECTED_INPUT_SHA}" != "${EXPECTED_SHA}" ]]; then - echo "expected_sha does not match the workflow run commit" >&2 - exit 1 - fi - fi - if ! checked_out_sha="$(git rev-parse HEAD)"; then - echo "Unable to determine the checked out commit" >&2 - exit 1 - fi - if [[ "${checked_out_sha}" != "${EXPECTED_SHA}" ]]; then - echo "Checked out commit does not match the workflow run commit" >&2 - exit 1 - fi - if [[ -z "${DOCKERHUB_OIDC_CONNECTION_ID}" ]]; then - echo "DOCKERHUB_OIDC_CONNECTION_ID must be configured" >&2 - exit 1 - fi - if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then - echo "GitHub Actions OIDC is unavailable for this workflow run" >&2 - exit 1 - fi - - - name: Get Docker Build Cloud OIDC token - id: docker_oidc - uses: docker/oidc-action@b048fb089ace3e15a5fbdd784f4784d284ce5e8d # v1.1.0 - with: - connection-id: ${{ env.DOCKERHUB_OIDC_CONNECTION_ID }} - expires-in: 3600 - - - name: Docker login - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: docker - password: ${{ steps.docker_oidc.outputs.token }} - - - name: Set up Docker Build Cloud - id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - with: - driver: cloud - endpoint: ${{ env.DOCKER_BUILD_CLOUD_ENDPOINT }} - version: lab:edge + - uses: $/.github/actions/setup-go - - name: Build image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev + - name: Plan storage build tags + run: task check-plan-cross - - name: Build template - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - target: template - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev + - name: WASM provider registration + run: task test-wasm-js - # Fork PRs cannot use repository variables or OIDC; build without credentials. - build-image-fork: - if: >- - github.repository == 'docker/docker-agent' && - github.event_name == 'pull_request' && - github.event.pull_request.base.repo.full_name == github.repository && - github.event.pull_request.head.repo.full_name != github.repository - permissions: - contents: read - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} + licenses: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - uses: $/.github/actions/setup-go with: - driver: docker-container - cache-binary: false + cache-name: test-linux + save: 'false' + task: 'false' - - name: Build image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev + - name: Check licenses + run: | + go install github.com/google/go-licenses@v1.6.0 + go-licenses check . --allowed_licenses=Apache-2.0,MIT,BSD-3-Clause,BSD-2-Clause --ignore modernc.org/mathutil --ignore github.com/hashicorp/hcl/v2 - - name: Build template - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + # Race detector on main only: it is several times slower than `task test` + # and must not sit on the PR critical path. Not needed by `gate`. + test-race: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - context: . - target: template - platforms: ${{ matrix.platform }} - outputs: type=cacheonly - push: false - load: false - sbom: false - provenance: false - github-token: '' - build-args: | - GIT_TAG=pr - GIT_COMMIT=dev + persist-credentials: false - build-and-push-image: + - uses: $/.github/actions/setup-go + + - name: Test with the race detector + run: task test-race + + # Builds the image and the sandbox template, on Build Cloud whenever the run + # can authenticate to Docker Hub: both platforms natively on one runner. + # main, v* tags and `image_mode: auto` dispatches on main push by digest, + # untagged; every other run builds into the cache only. Runs that cannot + # authenticate (PRs from forks, PRs opened by Dependabot and other bots + # running with a read-only token, CI in a fork of this repo) fall back to a + # plain builder on the runner, linux/amd64 only, nothing pushed, so the + # Dockerfile is still exercised. Not gated on the test jobs: nothing + # user-visible moves until `publish`. + image: + needs: changes if: >- - github.repository == 'docker/docker-agent' && - github.event.repository.fork == false && ( - (github.event_name == 'push' && github.ref == 'refs/heads/main') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || - (github.event_name == 'workflow_dispatch' && - github.ref == 'refs/heads/main' && - (inputs.image_mode == '' || inputs.image_mode == 'auto')) + needs.changes.outputs.code == 'true' && ( + github.event_name == 'pull_request' || + github.event_name == 'push' || + github.event_name == 'merge_group' || + (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/')) ) - needs: [ lint, build-and-test, windows-tests, license-check ] permissions: contents: read id-token: write + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + publish: ${{ steps.mode.outputs.publish }} + image-digest: ${{ steps.image.outputs.digest }} + template-digest: ${{ steps.template.outputs.digest }} env: - DOCKER_BUILD_CLOUD_ENDPOINT: docker/docker-agent - DOCKERHUB_OIDC_CONNECTION_ID: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} - DOCKER_BUILD_RECORD_UPLOAD: 'false' - DOCKER_BUILD_SUMMARY: 'false' - # Cloud workers build each native platform in parallel and push by digest; - # the merge jobs below assemble the multi-arch manifests. - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} + IMAGE: docker/docker-agent + TEMPLATE: docker/docker-agent-sbx-templates steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.sha }} persist-credentials: false - - name: Validate Docker Build Cloud configuration + - name: Decide build mode + id: mode env: - EXPECTED_SHA: ${{ github.sha }} - EXPECTED_INPUT_SHA: ${{ inputs.expected_sha }} - shell: bash + PUBLISH: >- + ${{ github.repository == 'docker/docker-agent' && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && + (inputs.image_mode == '' || inputs.image_mode == 'auto'))) && 'true' || 'false' }} + # Empty on fork PRs, Dependabot PRs and in forks of this repo. + CONNECTION_ID: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} run: | - set -euo pipefail - if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - echo "Workflow run commit must be a lowercase 40-character SHA" >&2 - exit 1 - fi - if [[ -n "${EXPECTED_INPUT_SHA}" ]]; then - if [[ ! "${EXPECTED_INPUT_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - echo "expected_sha must be a lowercase 40-character SHA" >&2 - exit 1 - fi - if [[ "${EXPECTED_INPUT_SHA}" != "${EXPECTED_SHA}" ]]; then - echo "expected_sha does not match the workflow run commit" >&2 - exit 1 - fi - fi - if ! checked_out_sha="$(git rev-parse HEAD)"; then - echo "Unable to determine the checked out commit" >&2 - exit 1 - fi - if [[ "${checked_out_sha}" != "${EXPECTED_SHA}" ]]; then - echo "Checked out commit does not match the workflow run commit" >&2 + # Build Cloud needs the Hub OIDC connection id and an OIDC token; the + # token endpoint is only exposed when id-token: write was granted. + if [[ -n "$CONNECTION_ID" && -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]]; then + builder=cloud + elif [[ "$PUBLISH" == true ]]; then + echo "publishing requires Build Cloud: DOCKERHUB_OIDC_CONNECTION_ID or the OIDC token is unavailable" >&2 exit 1 + else + builder=local + echo "::notice::Build Cloud unavailable for this run; building linux/amd64 on the runner" fi - if [[ -z "${DOCKERHUB_OIDC_CONNECTION_ID}" ]]; then - echo "DOCKERHUB_OIDC_CONNECTION_ID must be configured" >&2 - exit 1 + echo "builder=$builder" >> "$GITHUB_OUTPUT" + + if [[ "$PUBLISH" == true ]]; then + { + echo "publish=true" + echo "image-outputs=type=image,name=${IMAGE},push-by-digest=true,name-canonical=true,push=true" + echo "template-outputs=type=image,name=${TEMPLATE},push-by-digest=true,name-canonical=true,push=true" + echo "provenance=mode=max" + echo "git-tag=${GITHUB_REF_NAME}" + echo "git-commit=${GITHUB_SHA}" + # Build records are useful on PRs; skip the upload for release builds. + echo "record-upload=false" + } >> "$GITHUB_OUTPUT" + else + { + echo "publish=false" + echo "image-outputs=type=cacheonly" + echo "template-outputs=type=cacheonly" + echo "provenance=false" + echo "git-tag=pr" + echo "git-commit=dev" + # A read-only token (the local case) cannot upload the record artifact. + echo "record-upload=$([[ "$builder" == cloud ]] && echo true || echo false)" + } >> "$GITHUB_OUTPUT" fi - if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then - echo "GitHub Actions OIDC is unavailable for this workflow run" >&2 - exit 1 + if [[ "$builder" == cloud ]]; then + echo "platforms=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT" + else + echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT" fi - - name: Prepare platform name - run: echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - env: - platform: ${{ matrix.platform }} - - - name: Get Docker Hub OIDC token - id: docker_oidc - uses: docker/oidc-action@b048fb089ace3e15a5fbdd784f4784d284ce5e8d # v1.1.0 + - if: steps.mode.outputs.builder == 'cloud' + uses: $/.github/actions/setup-buildx + id: cloud with: - connection-id: ${{ env.DOCKERHUB_OIDC_CONNECTION_ID }} - expires-in: 3600 - - - name: Hub login - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: docker - password: ${{ steps.docker_oidc.outputs.token }} + connection-id: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} + expected-sha: ${{ inputs.expected_sha }} - - name: Set up Docker Build Cloud - id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - if: steps.mode.outputs.builder == 'local' + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + id: local with: - driver: cloud - endpoint: ${{ env.DOCKER_BUILD_CLOUD_ENDPOINT }} - version: lab:edge + cache-binary: false - name: Docker metadata id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: - images: | - docker/docker-agent + images: ${{ env.IMAGE }} - - name: Build and push by digest - id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + - name: Build image + id: image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + env: + DOCKER_BUILD_RECORD_UPLOAD: ${{ steps.mode.outputs.record-upload }} + DOCKER_BUILD_SUMMARY: ${{ steps.mode.outputs.record-upload }} with: - builder: ${{ steps.buildx.outputs.name }} + builder: ${{ steps.cloud.outputs.builder || steps.local.outputs.name }} context: . - platforms: ${{ matrix.platform }} + platforms: ${{ steps.mode.outputs.platforms }} labels: ${{ steps.meta.outputs.labels }} load: false github-token: '' - sbom: true - provenance: mode=max - outputs: type=image,name=docker/docker-agent,push-by-digest=true,name-canonical=true,push=true + sbom: ${{ steps.mode.outputs.publish }} + provenance: ${{ steps.mode.outputs.provenance }} + outputs: ${{ steps.mode.outputs.image-outputs }} build-args: | - GIT_TAG=${{ github.ref_name }} - GIT_COMMIT=${{ github.sha }} - - - name: Export digest - run: | - mkdir -p "${RUNNER_TEMP}/digests" - digest="${{ steps.build.outputs.digest }}" - touch "${RUNNER_TEMP}/digests/${digest#sha256:}" + GIT_TAG=${{ steps.mode.outputs.git-tag }} + GIT_COMMIT=${{ steps.mode.outputs.git-commit }} - - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - # The sandbox template is a stage of the same Dockerfile with identical - # build args, so it reuses the builder-linux layer from the image build - # above and embeds the exact same binary. - - name: Build and push template by digest - id: build-template - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + # A stage of the same Dockerfile with identical build args, so it reuses + # the builder-linux layer above and embeds the exact same binary. + - name: Build template + id: template + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + env: + DOCKER_BUILD_RECORD_UPLOAD: ${{ steps.mode.outputs.record-upload }} + DOCKER_BUILD_SUMMARY: ${{ steps.mode.outputs.record-upload }} with: - builder: ${{ steps.buildx.outputs.name }} + builder: ${{ steps.cloud.outputs.builder || steps.local.outputs.name }} context: . target: template - platforms: ${{ matrix.platform }} + platforms: ${{ steps.mode.outputs.platforms }} load: false github-token: '' - sbom: true - provenance: mode=max - outputs: type=image,name=docker/docker-agent-sbx-templates,push-by-digest=true,name-canonical=true,push=true + sbom: ${{ steps.mode.outputs.publish }} + provenance: ${{ steps.mode.outputs.provenance }} + outputs: ${{ steps.mode.outputs.template-outputs }} build-args: | - GIT_TAG=${{ github.ref_name }} - GIT_COMMIT=${{ github.sha }} + GIT_TAG=${{ steps.mode.outputs.git-tag }} + GIT_COMMIT=${{ steps.mode.outputs.git-commit }} - - name: Export template digest + # Single required status check; see the header comment. + gate: + if: always() + needs: [ changes, lint, test-linux, test-windows, cross-compile, licenses, image ] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check results env: - DIGEST: ${{ steps.build-template.outputs.digest }} + RESULTS: ${{ toJSON(needs) }} run: | - mkdir -p "${RUNNER_TEMP}/template-digests" - touch "${RUNNER_TEMP}/template-digests/${DIGEST#sha256:}" - - - name: Upload template digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: template-digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/template-digests/* - if-no-files-found: error - retention-days: 1 - - merge-and-push-image: - if: >- - github.repository == 'docker/docker-agent' && - github.event.repository.fork == false && ( - (github.event_name == 'push' && github.ref == 'refs/heads/main') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || - (github.event_name == 'workflow_dispatch' && - github.ref == 'refs/heads/main' && - (inputs.image_mode == '' || inputs.image_mode == 'auto')) - ) - needs: [ build-and-push-image ] + { + echo "| job | result |" + echo "|---|---|" + jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"' <<< "$RESULTS" + } | tee -a "$GITHUB_STEP_SUMMARY" + jq -e '[.[] | .result] | all(. == "success" or . == "skipped")' <<< "$RESULTS" >/dev/null + + # Moves tags onto the digests pushed by `image` once the gate is green. On + # main only edge moves; on a v* tag the version tag and the floating latest + # tag that sandboxes pull by default. + publish: + if: needs.image.outputs.publish == 'true' + needs: [ gate, image ] permissions: contents: read id-token: write runs-on: ubuntu-latest + timeout-minutes: 15 + env: + IMAGE: docker/docker-agent + TEMPLATE: docker/docker-agent-sbx-templates + IMAGE_DIGEST: ${{ needs.image.outputs.image-digest }} + TEMPLATE_DIGEST: ${{ needs.image.outputs.template-digest }} steps: - - name: Download digests - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - path: ${{ runner.temp }}/digests - pattern: digests-* - merge-multiple: true - - - name: Get Docker Hub OIDC token - id: docker_oidc - uses: docker/oidc-action@b048fb089ace3e15a5fbdd784f4784d284ce5e8d # v1.1.0 + # imagetools talks to the registry directly; no builder needed. + - uses: $/.github/actions/setup-buildx with: connection-id: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} - expires-in: 3600 - - - name: Hub login - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: docker - password: ${{ steps.docker_oidc.outputs.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + builder: 'false' - name: Docker metadata id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: - images: | - docker/docker-agent + images: ${{ env.IMAGE }} tags: | type=semver,pattern={{version}} type=edge - type=ref,event=pr - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests + - name: Tag image + env: + VERSION: ${{ steps.meta.outputs.version }} run: | + : "${IMAGE_DIGEST:?image produced no digest}" mapfile -t tags < <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") args=() for tag in "${tags[@]}"; do args+=(-t "$tag"); done - for digest in *; do args+=("docker/docker-agent@sha256:${digest}"); done - docker buildx imagetools create "${args[@]}" + docker buildx imagetools create "${args[@]}" "${IMAGE}@${IMAGE_DIGEST}" + docker buildx imagetools inspect "${IMAGE}:${VERSION}" - - name: Inspect image - run: | - docker buildx imagetools inspect docker/docker-agent:${{ steps.meta.outputs.version }} - - merge-and-push-template: - if: >- - github.repository == 'docker/docker-agent' && - github.event.repository.fork == false && ( - (github.event_name == 'push' && github.ref == 'refs/heads/main') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || - (github.event_name == 'workflow_dispatch' && - github.ref == 'refs/heads/main' && - (inputs.image_mode == '' || inputs.image_mode == 'auto')) - ) - needs: [ build-and-push-image ] - permissions: - contents: read - id-token: write - runs-on: ubuntu-latest - steps: - - name: Download template digests - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - path: ${{ runner.temp }}/template-digests - pattern: template-digests-* - merge-multiple: true - - - name: Get Docker Hub OIDC token - id: docker_oidc - uses: docker/oidc-action@b048fb089ace3e15a5fbdd784f4784d284ce5e8d # v1.1.0 - with: - connection-id: ${{ vars.DOCKERHUB_OIDC_CONNECTION_ID }} - expires-in: 3600 - - - name: Hub login - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: docker - password: ${{ steps.docker_oidc.outputs.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - # Assemble the multi-arch manifest list from the per-arch digests pushed - # by build-and-push-image. On main only the edge tag moves; on a v* tag - # both the version tag and the floating latest tag that sandboxes pulls - # by default. - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/template-digests + - name: Tag template run: | + : "${TEMPLATE_DIGEST:?image produced no template digest}" if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - tags=(-t "docker/docker-agent-sbx-templates:${GITHUB_REF_NAME#v}" -t "docker/docker-agent-sbx-templates:latest") + version="${GITHUB_REF_NAME#v}" + tags=(-t "${TEMPLATE}:${version}" -t "${TEMPLATE}:latest") else - tags=(-t "docker/docker-agent-sbx-templates:edge") + version=edge + tags=(-t "${TEMPLATE}:edge") fi - args=() - for digest in *; do args+=("docker/docker-agent-sbx-templates@sha256:${digest}"); done - docker buildx imagetools create "${tags[@]}" "${args[@]}" - - - name: Inspect template - run: | - if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then tag="${GITHUB_REF_NAME#v}"; else tag="edge"; fi - docker buildx imagetools inspect "docker/docker-agent-sbx-templates:${tag}" + docker buildx imagetools create "${tags[@]}" "${TEMPLATE}@${TEMPLATE_DIGEST}" + docker buildx imagetools inspect "${TEMPLATE}:${version}" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 23945626dd..21d982fbbf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,13 +1,15 @@ -name: CodeQL +name: codeql permissions: contents: read security-events: write actions: read +# Main and tag runs get a group of their own so they are never queued behind +# or cancelled by another run; PR runs supersede the previous run on the ref. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + group: ${{ github.workflow }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) && github.run_id || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }} on: push: @@ -18,9 +20,9 @@ on: jobs: analyze: - name: Analyze (${{ matrix.language }}) + timeout-minutes: 30 + name: analyze (${{ matrix.language }}) runs-on: ubuntu-latest - strategy: fail-fast: false matrix: @@ -31,29 +33,29 @@ jobs: build-mode: none - language: javascript-typescript build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Set up Go - if: matrix.language == 'go' - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - if: matrix.language == 'go' + uses: $/.github/actions/setup-go with: - go-version-file: go.mod - cache-dependency-path: go.sum + cache-name: test-linux + save: 'false' + task: 'false' - name: Initialize CodeQL - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Autobuild if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + - name: Analyze + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docs-a11y.yml b/.github/workflows/docs-a11y.yml deleted file mode 100644 index 242694f59c..0000000000 --- a/.github/workflows/docs-a11y.yml +++ /dev/null @@ -1,142 +0,0 @@ -# Scans the built docs site for WCAG 2 AA accessibility violations with -# pa11y-ci, using the same "hugo server" flow contributors run locally -# (see docs/Dockerfile). Originally landed as the last step of the -# docs-a11y-audit remediation (PR 9); the docs-a11y-smart-gate plan later -# evolved it from a fixed URL list into a path-aware, two-tier scan: -# -# Tier 1 (always): a small static list of layout-archetype pages in -# docs/.pa11yci.json, each scanned in both themes — guards against -# systemic regressions (shared CSS/JS/templates/SVG) on every run. -# -# Tier 2 (pull_request only): the content pages the PR actually -# modifies, mapped from changed Markdown to rendered URLs by -# scripts/docs-a11y-urls.sh (deterministically capped, both themes) — -# guards against authored-content issues on exactly what changed. A -# generated config (static + changed) is used when there's anything to -# add; push-to-main and content-free PRs fall back to the static-only -# config, since there's no PR diff to compute Tier 2 from. -# -# Each URL is listed once with ?theme=light and once with ?theme=dark: -# the headless browser's default color-scheme is otherwise indeterminate -# (observed as "light" locally), so without the query param the gate -# would only ever exercise one theme. js/app.js's initTheme() honors -# ?theme= ahead of localStorage/prefers-color-scheme for exactly this -# reason. -# -# The changed-file diff is git-native (`git diff HEAD^1 HEAD` against the -# pull_request test-merge commit, fetch-depth: 2) rather than a -# changed-files action, keeping the SHA-pinned, minimal-supply-chain -# convention (see scripts/docs-a11y-urls.sh's header for the mapping -# rules and env vars, and docs/STYLE.md for how to reproduce a scan -# locally). -# -# pa11y only treats HTML_CodeSniffer "error"-level results as failures -# (warnings/notices are informational and excluded by default), so this -# gates on real violations rather than every possible nit. -name: docs-a11y - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -on: - push: - branches: [main] - paths: - - ".github/workflows/docs-a11y.yml" - - "docs/**" - - "scripts/docs-a11y-urls.sh" - pull_request: - paths: - - ".github/workflows/docs-a11y.yml" - - "docs/**" - - "scripts/docs-a11y-urls.sh" - -env: - HUGO_VERSION: 0.163.0 - -jobs: - pa11y: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - # Need the merge commit's parent too, so `git diff HEAD^1 HEAD` - # (below) can compute the PR's changed files without a - # changed-files action or extra token scope. - fetch-depth: 2 - - # pa11y-ci@3.1.0 bundles an old Puppeteer whose postinstall Chromium - # download doesn't happen under `npx --yes` ("Could not find expected - # browser (chrome) locally"). Installing a pinned Chrome and pointing - # Puppeteer at it via PUPPETEER_EXECUTABLE_PATH (below) sidesteps that - # download entirely, per pa11y-ci's own guidance for Ubuntu > 20.04. - - name: Install Chrome - id: setup-chrome - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 - with: - chrome-version: stable - - - name: Install Hugo - run: | - curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ - | sudo tar -xz -C /usr/local/bin hugo - hugo version - - - name: Start Hugo server - working-directory: docs - run: nohup hugo server --bind 127.0.0.1 --port 1313 > hugo-server.log 2>&1 & - - - name: Wait for server - run: npx --yes wait-on@7.2.0 http://127.0.0.1:1313/docker-agent/ --timeout 60000 - - # Tier 2: map this PR's changed content Markdown to rendered URLs. - # `actions/checkout` puts the pull_request test-merge commit at - # HEAD, whose first parent (HEAD^1) is the base branch tip, so this - # diff is exactly the PR's changed files — token-free and identical - # for fork PRs. Skipped on push (no PR diff to compute from). - - name: Compute changed-page URLs - id: changed-urls - if: github.event_name == 'pull_request' - env: - A11Y_BASE_URL: http://127.0.0.1:1313 - run: | - git diff --name-only --diff-filter=d HEAD^1 HEAD \ - | ./scripts/docs-a11y-urls.sh > /tmp/changed-urls.txt - count=$(wc -l < /tmp/changed-urls.txt | tr -d ' ') - echo "count=$count" >> "$GITHUB_OUTPUT" - echo "Tier 2: $count changed-page URL(s):" - cat /tmp/changed-urls.txt - - # Merge Tier 2's URLs into the static (Tier 1) config from - # docs/.pa11yci.json. Only runs when there's something to add; - # otherwise the run step below falls back to the static-only config. - - name: Assemble pa11y config - if: github.event_name == 'pull_request' && steps.changed-urls.outputs.count != '0' - run: | - jq -Rn '[inputs]' /tmp/changed-urls.txt > /tmp/changed-urls.json - jq --slurpfile extra /tmp/changed-urls.json '.urls += $extra[0]' \ - docs/.pa11yci.json > /tmp/pa11yci.generated.json - echo "Merged pa11y-ci config (static + changed pages):" - jq -r '.urls[]' /tmp/pa11yci.generated.json - - - name: Run pa11y-ci - working-directory: docs - env: - PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} - run: | - config=.pa11yci.json - if [ -f /tmp/pa11yci.generated.json ]; then - config=/tmp/pa11yci.generated.json - fi - echo "Using pa11y-ci config: $config" - npx --yes pa11y-ci@3.1.0 --config "$config" - - - name: Show Hugo server log on failure - if: failure() - working-directory: docs - run: cat hugo-server.log || true diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 973077311e..98bd122bdf 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,13 +1,10 @@ # Builds the documentation site in docs/ with Hugo and publishes it to -# GitHub Pages (docker.github.io/docker-agent). Replaces the legacy -# branch-based Jekyll build; docs.docker.com renders the same Markdown -# source through its Hugo module mount (see docs-upstream.yml). +# GitHub Pages (docker.github.io/docker-agent). docs.docker.com renders the +# same Markdown source through its Hugo module mount (see docs.yml, upstream). name: docs-deploy permissions: contents: read - pages: write - id-token: write concurrency: group: pages @@ -18,26 +15,20 @@ on: branches: [main] paths: - ".github/workflows/docs-deploy.yml" + - ".github/actions/setup-hugo/**" - "docs/**" workflow_dispatch: -env: - # Kept in sync with the docker/docs HUGO_VERSION pin so both surfaces - # render with the same Hugo release. - HUGO_VERSION: 0.163.0 - jobs: build: + timeout-minutes: 10 runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Install Hugo - run: | - curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ - | sudo tar -xz -C /usr/local/bin hugo - hugo version + - uses: $/.github/actions/setup-hugo - name: Configure Pages id: pages @@ -45,7 +36,9 @@ jobs: - name: Build site working-directory: docs - run: hugo --gc --baseURL "${{ steps.pages.outputs.base_url }}/" + env: + BASE_URL: ${{ steps.pages.outputs.base_url }} + run: hugo --gc --baseURL "${BASE_URL}/" - name: Upload Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 @@ -53,7 +46,11 @@ jobs: path: docs/public deploy: + timeout-minutes: 10 needs: build + permissions: + pages: write + id-token: write runs-on: ubuntu-latest environment: name: github-pages @@ -61,4 +58,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml deleted file mode 100644 index e48e8bb67a..0000000000 --- a/.github/workflows/docs-lint.yml +++ /dev/null @@ -1,97 +0,0 @@ -# Lints the documentation source in docs/**: markdownlint for style, -# lychee (offline) to verify that every relative Markdown link resolves -# to a real file, and a semantic check of the generated /llms.txt -# (https://llmstxt.org/) against its data/nav.yml source of truth. -# Portable relative links are what the Hugo module mount on -# docs.docker.com resolves, so broken ones would break the downstream -# build (see docs-upstream.yml). -name: docs-lint - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -on: - push: - branches: [main] - paths: - - ".github/workflows/docs-lint.yml" - - "docs/**" - - "scripts/docs-check-canonical.sh" - - "scripts/docs-check-llms-txt.sh" - pull_request: - paths: - - ".github/workflows/docs-lint.yml" - - "docs/**" - - "scripts/docs-check-canonical.sh" - - "scripts/docs-check-llms-txt.sh" - -env: - # Kept in sync with docs-deploy.yml / docs-a11y.yml so every docs - # workflow builds with the same Hugo release. - HUGO_VERSION: 0.163.0 - -jobs: - markdownlint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - # Run from docs/ so the globs and ignores in .markdownlint-cli2.yaml - # resolve the same way as a local `npx markdownlint-cli2` run. - - name: Lint Markdown - run: npx --yes markdownlint-cli2@0.22.1 - working-directory: docs - - canonical-check: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - # Mounted pages must canonicalize to their docs.docker.com URL - # (STYLE.md "Canonical URLs"); catches pages added without one. - - name: Check canonical front matter - run: ./scripts/docs-check-canonical.sh - - link-check: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Check relative links - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 - with: - # Verbatim (code block) links are excluded by lychee's defaults. - # index.md and 404.md are github.io-only landing pages whose - # links are HTML anchors resolved on the rendered site; the - # muffet check (task docs-check-links) covers them. - args: >- - --offline --no-progress - --exclude-path docs/index.md --exclude-path docs/404.md - "docs/**/*.md" - fail: true - - llms-txt-check: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Install Hugo - run: | - curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ - | sudo tar -xz -C /usr/local/bin hugo - hugo version - - # llms.txt is generated at build time from data/nav.yml (STYLE.md - # "llms.txt"); a broken groups[].items traversal or similar template - # regression can silently drop entries while the build stays green, - # so this asserts the built output's content, not just build success. - - name: Check llms.txt - run: ./scripts/docs-check-llms-txt.sh diff --git a/.github/workflows/docs-upstream.yml b/.github/workflows/docs-upstream.yml deleted file mode 100644 index be8f1cf7bc..0000000000 --- a/.github/workflows/docs-upstream.yml +++ /dev/null @@ -1,34 +0,0 @@ -# Validates that changes under docs/** still build on docs.docker.com, -# which mounts this repo's docs as a Hugo module pinned to a release -# tag (docker/docs go.mod + hugo.yaml module.imports). The reusable -# workflow builds docker/docs with the module replaced by this commit -# and runs htmltest on the output, so docs PRs that would break -# docs.docker.com fail here before merging. -# -# The check is a no-op until docker/docs imports the -# github.com/docker/docker-agent module (issue #3371, Phase 2.1). -name: docs-upstream - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -on: - push: - branches: [main] - paths: - - ".github/workflows/docs-upstream.yml" - - "docs/**" - pull_request: - paths: - - ".github/workflows/docs-upstream.yml" - - "docs/**" - -jobs: - validate-upstream: - uses: docker/docs/.github/workflows/validate-upstream.yml@9955a341f1720b8923172e2b099f71d75c3e8570 # main - with: - module-name: docker/docker-agent diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..99c1bef202 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,182 @@ +# Checks for the documentation source in docs/**. Every job runs in parallel: +# +# markdownlint style, via docs/.markdownlint-cli2.yaml +# canonical every mounted page canonicalizes to its docs.docker.com URL +# links relative Markdown links resolve to a real file (lychee, +# offline); these are what the docs.docker.com module mount +# resolves, so a broken one breaks the downstream build +# llms-txt the generated /llms.txt matches data/nav.yml +# a11y WCAG 2 AA scan of the built site with pa11y-ci +# upstream docs.docker.com still builds with docs/ mounted at this commit +# +# Path-filtered, so it only runs when docs change and cannot be a required +# check as is; `ci / gate` is the required one. +name: docs + +permissions: + contents: read + +# Main and tag runs get a group of their own so they are never queued behind +# or cancelled by another run; PR runs supersede the previous run on the ref. +concurrency: + group: ${{ github.workflow }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) && github.run_id || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }} + +on: + push: + branches: [main] + paths: + - ".github/workflows/docs.yml" + - ".github/actions/setup-hugo/**" + - "docs/**" + - "scripts/docs-*.sh" + pull_request: + paths: + - ".github/workflows/docs.yml" + - ".github/actions/setup-hugo/**" + - "docs/**" + - "scripts/docs-*.sh" + +jobs: + markdownlint: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # From docs/ so the globs in .markdownlint-cli2.yaml resolve as locally. + - name: Lint Markdown + working-directory: docs + run: npx --yes markdownlint-cli2@0.22.1 + + canonical: + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check canonical front matter + run: ./scripts/docs-check-canonical.sh + + links: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check relative links + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + # index.md and 404.md are github.io-only landing pages whose links + # are HTML anchors resolved on the rendered site (task docs-check-links). + args: >- + --offline --no-progress + --exclude-path docs/index.md --exclude-path docs/404.md + "docs/**/*.md" + fail: true + + llms-txt: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: $/.github/actions/setup-hugo + + # Asserts the built output's content, not just build success: a broken + # template traversal can silently drop entries while the build stays green. + - name: Check llms.txt + run: ./scripts/docs-check-llms-txt.sh + + # Two tiers, each URL scanned in both themes (?theme= is honored by + # js/app.js ahead of localStorage / prefers-color-scheme, since the headless + # browser's default color scheme is otherwise indeterminate): + # 1. always: the layout-archetype pages listed in docs/.pa11yci.json, + # guarding shared CSS/JS/templates/SVG; + # 2. pull_request only: the content pages the PR modifies, mapped from the + # changed Markdown by scripts/docs-a11y-urls.sh. + # pa11y fails only on HTML_CodeSniffer "error" results. See docs/STYLE.md to + # reproduce a scan locally. + a11y: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # HEAD^1 of the pull_request merge commit is the base tip, so + # `git diff HEAD^1 HEAD` is the PR's changed files, token-free. + fetch-depth: 2 + + # pa11y-ci@3.1.0 bundles a Puppeteer whose Chromium download fails under + # `npx --yes`; a pinned Chrome via PUPPETEER_EXECUTABLE_PATH sidesteps it. + - name: Install Chrome + id: chrome + uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 + with: + chrome-version: stable + + - uses: $/.github/actions/setup-hugo + + - name: Start Hugo server + working-directory: docs + run: nohup hugo server --bind 127.0.0.1 --port 1313 > hugo-server.log 2>&1 & + + - name: Wait for server + run: npx --yes wait-on@7.2.0 http://127.0.0.1:1313/docker-agent/ --timeout 60000 + + - name: Compute changed-page URLs + id: changed + if: github.event_name == 'pull_request' + env: + A11Y_BASE_URL: http://127.0.0.1:1313 + run: | + git diff --name-only --diff-filter=d HEAD^1 HEAD \ + | ./scripts/docs-a11y-urls.sh > /tmp/changed-urls.txt + count=$(wc -l < /tmp/changed-urls.txt | tr -d ' ') + echo "count=$count" >> "$GITHUB_OUTPUT" + echo "Tier 2: $count changed-page URL(s):" + cat /tmp/changed-urls.txt + + - name: Assemble pa11y config + if: github.event_name == 'pull_request' && steps.changed.outputs.count != '0' + run: | + jq -Rn '[inputs]' /tmp/changed-urls.txt > /tmp/changed-urls.json + jq --slurpfile extra /tmp/changed-urls.json '.urls += $extra[0]' \ + docs/.pa11yci.json > /tmp/pa11yci.generated.json + echo "Merged pa11y-ci config (static + changed pages):" + jq -r '.urls[]' /tmp/pa11yci.generated.json + + - name: Run pa11y-ci + working-directory: docs + env: + PUPPETEER_EXECUTABLE_PATH: ${{ steps.chrome.outputs.chrome-path }} + run: | + config=.pa11yci.json + if [ -f /tmp/pa11yci.generated.json ]; then + config=/tmp/pa11yci.generated.json + fi + echo "Using pa11y-ci config: $config" + npx --yes pa11y-ci@3.1.0 --config "$config" + + - name: Show Hugo server log on failure + if: failure() + working-directory: docs + run: cat hugo-server.log || true + + # docker/docs mounts this repo's docs as a Hugo module pinned to a release + # tag; the reusable workflow builds docker/docs with the module replaced by + # this commit and runs htmltest on the output. A no-op until docker/docs + # imports github.com/docker/docker-agent (issue #3371, Phase 2.1). + upstream: + uses: docker/docs/.github/workflows/validate-upstream.yml@9955a341f1720b8923172e2b099f71d75c3e8570 # main + with: + module-name: docker/docker-agent diff --git a/.github/workflows/models-delta-lint.yml b/.github/workflows/models-delta-lint.yml deleted file mode 100644 index 1ba03cbb69..0000000000 --- a/.github/workflows/models-delta-lint.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Lints and tests the models-delta scripts (.github/scripts/models-delta*.jq, -# scripts/models-delta*.sh) used by update-models.yml to compute a semantic -# models.dev catalog diff. Split out from ci.yml's blanket lint job — which -# runs on every PR/commit across the whole repo — since this only needs to -# run when these scripts or their fixtures actually change. -name: models-delta-lint - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -on: - push: - branches: [main] - paths: - - ".github/workflows/models-delta-lint.yml" - - ".github/scripts/models-delta.jq" - - ".github/scripts/models-delta-render.jq" - - ".github/scripts/testdata/**" - - "scripts/models-delta.sh" - - "scripts/models-delta-test.sh" - - "Taskfile.yml" - pull_request: - paths: - - ".github/workflows/models-delta-lint.yml" - - ".github/scripts/models-delta.jq" - - ".github/scripts/models-delta-render.jq" - - ".github/scripts/testdata/**" - - "scripts/models-delta.sh" - - "scripts/models-delta-test.sh" - - "Taskfile.yml" - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Lint models-delta scripts - run: | - shellcheck scripts/models-delta.sh scripts/models-delta-test.sh - ./scripts/models-delta-test.sh diff --git a/.github/workflows/models-live-check.yml b/.github/workflows/models-live-check.yml index 6ac0023974..e8ccb98ee8 100644 --- a/.github/workflows/models-live-check.yml +++ b/.github/workflows/models-live-check.yml @@ -18,27 +18,23 @@ concurrency: on: workflow_dispatch: schedule: - # Daily, 07:00 UTC (an hour after update-models' Monday 06:00 refresh). + # Daily, 07:00 UTC (an hour after models-update's Monday 06:00 refresh). - cron: "0 7 * * *" jobs: check: + timeout-minutes: 15 if: github.repository == 'docker/docker-agent' runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - go-version-file: go.mod - cache-dependency-path: go.sum + persist-credentials: false - - name: Install Task - uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + # Restore-only: reuses the cache ci's test-linux job saves on main. + - uses: $/.github/actions/setup-go with: - version: 3.51.1 + cache-name: test-linux - name: Check examples against the live models.dev catalog run: task check-models-live diff --git a/.github/workflows/update-models.yml b/.github/workflows/models-update.yml similarity index 88% rename from .github/workflows/update-models.yml rename to .github/workflows/models-update.yml index 1d6fe2555a..b495527f04 100644 --- a/.github/workflows/update-models.yml +++ b/.github/workflows/models-update.yml @@ -1,4 +1,4 @@ -name: update-models +name: models-update permissions: contents: read @@ -15,27 +15,21 @@ on: jobs: refresh-snapshot: + timeout-minutes: 20 if: github.repository == 'docker/docker-agent' runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + # Restore-only: reuses the cache ci's test-linux job saves on main. + - uses: $/.github/actions/setup-go with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Install Task - uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - with: - version: 3.51.1 + cache-name: test-linux - name: Refresh models.dev snapshot run: task update-models @@ -100,7 +94,7 @@ jobs: } > "${RUNNER_TEMP}/pr-body.md" - name: Create pull request - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: # sign-commits builds the commit through GitHub's API instead of a # local `git push`, so it's Verified regardless of which token is diff --git a/.github/workflows/pr-review-trigger.yml b/.github/workflows/pr-review-trigger.yml index 827013152f..999b5c2898 100644 --- a/.github/workflows/pr-review-trigger.yml +++ b/.github/workflows/pr-review-trigger.yml @@ -1,4 +1,4 @@ -name: PR Review - Trigger +name: pr-review-trigger on: pull_request: types: [ready_for_review, opened, review_requested] @@ -17,6 +17,7 @@ concurrency: jobs: save-context: + timeout-minutes: 5 # Only run on fork PRs; skip GitHub App bot accounts (Dependabot, Renovate, etc.) early. if: github.event.pull_request.head.repo.fork && github.event.sender.type != 'Bot' runs-on: ubuntu-latest diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index f7e64e36e1..7f35bcafc0 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -1,9 +1,9 @@ -name: PR Review +name: pr-review on: issue_comment: types: [created] workflow_run: - workflows: ["PR Review - Trigger"] + workflows: [pr-review-trigger] types: [completed] concurrency: diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index af6a253e34..3d4973e6c9 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -18,6 +18,7 @@ permissions: {} jobs: publish: + timeout-minutes: 30 runs-on: windows-latest steps: - name: publish diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..2a7448694e --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,6 @@ +rules: + dangerous-triggers: + # pr-review reacts to pr-review-trigger via workflow_run by design: the + # trigger workflow runs with no permissions and only uploads event context. + ignore: + - pr-review.yml diff --git a/AGENTS.md b/AGENTS.md index 70083d8412..76234479af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,3 +115,25 @@ Before marking work as complete: - Keep branches focused on single features or fixes - Ensure your branch is up-to-date before submitting - Sign commits with a GPG or SSH key (`git commit -S`) + +# GitHub Actions + +- `ci / gate` is the single required status check on `main`. It needs every + blocking job; to make a new job blocking, add it to `gate.needs` in + `.github/workflows/ci.yml` — the ruleset does not change. Docs-only PRs + (only `docs/**` changed) skip the Go jobs; `gate` treats skipped as passed +- Every job sets `timeout-minutes`; every checkout sets + `persist-credentials: false`. zizmor (`.github/zizmor.yml`) and Dependabot + (`.github/dependabot.yml`, GitHub Actions only) keep both honest +- Pin every third-party action by 40-character SHA with a `# vX.Y.Z` comment, + and only to versions listed in the org allow list + (`docker/infra-github-allow-list`, `terraform/docker/main.tf`); `actions/*`, + `github/*` and `docker/*` are allowed at any version +- Every PR-triggered workflow declares a `concurrency` group. Runs on `main` + and on tags are never queued or cancelled: their group includes + `github.run_id` +- Shared setup lives in `.github/actions/`, referenced as `$/.github/actions/` + (no checkout needed): `setup-go` (Go, Task, per-job + cache), `setup-hugo`, `setup-buildx` (Hub OIDC login and builder). Pin tool + versions there, not in workflows +- `scripts/workflow-lint.sh` enforces the above and runs in the `lint` job diff --git a/Taskfile.yml b/Taskfile.yml index 7482c2f227..b777cf4329 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -107,12 +107,25 @@ tasks: cmds: - go test ./... + test-race: + desc: Run tests with the race detector and shuffled order (slow; CI runs it on main only) + dotenv: [.env.test] + cmds: + - go test -race -shuffle=on ./... + test-wasm-providers: desc: Test explicit provider registration in the WASM demo (requires Node) + cmds: + - task: test-wasm-js + - go test ./e2e -run '^TestWasmProviderDependencies$' + + # The GOOS=js half of test-wasm-providers; the e2e check above already runs + # under `task test`, so CI calls this to avoid recompiling the e2e package. + test-wasm-js: + desc: Run the GOOS=js provider registration tests (requires Node) cmds: - GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./cmd/wasm - GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" pkg/model/provider/factory_js_openai_vendor_test.go - - go test ./e2e -run '^TestWasmProviderDependencies$' test-binary: desc: Run tests on build binary diff --git a/docs/STYLE.md b/docs/STYLE.md index 9e863439cf..3914f78ebd 100644 --- a/docs/STYLE.md +++ b/docs/STYLE.md @@ -87,7 +87,7 @@ canonical: https://docs.docker.com/ai/docker-agent/
// The github.io layout renders it as the page's `rel=canonical` link; docs.docker.com ignores the value and self-canonicalizes. CI -(`docs-lint` / `scripts/docs-check-canonical.sh`) fails when the +(`docs / canonical`, `scripts/docs-check-canonical.sh`) fails when the value is missing or doesn't match the page path — mind it when scaffolding a new page from an existing one. The homepage, `404.md` and section `_index.md` files are not mirrored pages and don't set @@ -102,7 +102,7 @@ Every nav entry must resolve to a real page with a non-empty `description:` in its front matter (whitespace-only counts as empty) — the build fails with an `errorf` naming the offending title/url otherwise, since the spec's `- [title](url): note` shape requires a -note. CI (`docs-lint` / `scripts/docs-check-llms-txt.sh`) additionally +note. CI (`docs / llms-txt`, `scripts/docs-check-llms-txt.sh`) additionally rebuilds the site and asserts the generated `llms.txt` matches `nav.yml`'s sections, titles, order, count **and per-entry URL** (each rendered link must match the nav url at the same position, not diff --git a/scripts/docs-a11y-urls.sh b/scripts/docs-a11y-urls.sh index 274ad1c563..6fe20e523b 100755 --- a/scripts/docs-a11y-urls.sh +++ b/scripts/docs-a11y-urls.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # scripts/docs-a11y-urls.sh — map changed docs Markdown files to rendered # URLs for Tier 2 of the docs-a11y gate (see docs-a11y-smart-gate plan and -# .github/workflows/docs-a11y.yml). Reads changed repo-root-relative paths +# .github/workflows/docs.yml, a11y job). Reads changed repo-root-relative paths # on stdin (one per line, e.g. from `git diff --name-only`), applies the # mapping rules below, de-dupes against the static archetype list already # in docs/.pa11yci.json, optionally live-probes the mapped pages, caps the diff --git a/scripts/docs-check-canonical.sh b/scripts/docs-check-canonical.sh index d357f7d122..78e12abd22 100755 --- a/scripts/docs-check-canonical.sh +++ b/scripts/docs-check-canonical.sh @@ -3,7 +3,7 @@ # docs.docker.com must declare the canonical: front matter value # derived from its path, so the github.io page defers to the stable # docs (issue #3371, phase 3.2). Missing or stale values (e.g. a page -# scaffolded by copying another one) fail here and in docs-lint CI. +# scaffolded by copying another one) fail here and in the docs CI workflow. # # Scope: docs/
//index.md and deeper. The homepage, # 404.md and section _index.md files are not github.io content pages diff --git a/scripts/docs-check-links.sh b/scripts/docs-check-links.sh index 87611b4135..37a10a3b2d 100755 --- a/scripts/docs-check-links.sh +++ b/scripts/docs-check-links.sh @@ -20,7 +20,7 @@ docker run -d --rm \ hugo server --bind 0.0.0.0 --baseURL http://docs-linkcheck:1313/ echo 'Waiting for Hugo to start...' -for i in $(seq 1 30); do +for _ in $(seq 1 30); do docker run --rm --network docs-linkcheck-net curlimages/curl -sf http://docs-linkcheck:1313/ > /dev/null 2>&1 && break sleep 2 done diff --git a/scripts/workflow-lint.sh b/scripts/workflow-lint.sh index ff1ca9ee9e..d999bc67fd 100755 --- a/scripts/workflow-lint.sh +++ b/scripts/workflow-lint.sh @@ -10,8 +10,9 @@ # which intentionally runs all events to completion # (see PR #2789); # -# 2. pinned-by-sha: every third-party `uses:` reference is -# pinned by a 40-char SHA, not a tag/branch +# 2. pinned-by-sha: every third-party `uses:` reference, in +# workflows and in .github/actions/*/action.yml, +# is pinned by a 40-char SHA, not a tag/branch # (AGENTS.md § GitHub Actions); # # 3. payload-field deny: no `github.event.X.Y` reference on the @@ -39,6 +40,7 @@ else fi WORKFLOWS_DIR=".github/workflows" +ACTIONS_DIR=".github/actions" errors=0 note() { @@ -87,7 +89,7 @@ done # Check 2: third-party `uses:` references pinned by 40-char SHA. # -# Local references (`./...`, `../...`) and re-usable workflow refs +# Local references (`$/...`, `./...`, `../...`) and re-usable workflow refs # without an `@` (handled by the regex below) are exempt; everything # else, including the `docker/` namespace, must look like # `owner/repo@<40hex>`. The trailing comment with the human-readable @@ -111,7 +113,7 @@ while IFS= read -r line; do # Skip empty / local / re-usable workflow refs. case "$ref" in - '' | './'* | '../'*) + '' | './'* | '../'* | '$/'*) continue ;; esac @@ -119,7 +121,7 @@ while IFS= read -r line; do if [[ ! "$ref" =~ @[0-9a-f]{40}$ ]]; then note "$file:$lineno" "third-party action $ref is not pinned by a 40-char SHA" fi -done < <(grep -nE '^\s*-?\s*uses:' "$WORKFLOWS_DIR"/*.yml "$WORKFLOWS_DIR"/*.yaml 2>/dev/null || true) +done < <(grep -nE '^\s*-?\s*uses:' "$WORKFLOWS_DIR"/*.yml "$WORKFLOWS_DIR"/*.yaml "$ACTIONS_DIR"/*/action.yml "$ACTIONS_DIR"/*/action.yaml 2>/dev/null || true) # Check 3: known-broken event-payload field references. #