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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The image build only needs go.mod, go.sum, cmd/ and internal/.
*
!go.mod
!go.sum
!cmd/
!internal/
**/*_test.go
100 changes: 100 additions & 0 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Image

# Builds and tests the container image on pull requests, and publishes it to
# GitHub Container Registry when a release is published. Publishing waits for
# the release (not the tag), so nothing becomes public before the release does.
on:
pull_request:
branches: [main]
release:
types: [published]

permissions:
contents: read

jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
# On a release: 0.1.1 and 0.1, plus latest unless it is a prerelease.
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- name: Build information
id: build
env:
RELEASE_VERSION: ${{ github.event_name == 'release' && steps.meta.outputs.version || 'dev' }}
run: |
echo "commit=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "date=$(git log -1 --format=%cI)" >> "$GITHUB_OUTPUT"
echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"

- name: Build the image for testing
uses: docker/build-push-action@v7
with:
context: .
load: true
tags: docker-lint:test
build-args: |
VERSION=${{ steps.build.outputs.version }}
COMMIT=${{ steps.build.outputs.commit }}
DATE=${{ steps.build.outputs.date }}

- name: Test the image
run: |
set -euo pipefail
docker run --rm docker-lint:test --version

# Standard input, and a mounted directory as the working directory.
docker run --rm -i docker-lint:test < testdata/valid/minimal.Dockerfile
code=0
docker run --rm -v "$PWD/testdata:/work:ro" docker-lint:test invalid/no-from.Dockerfile || code=$?
test "$code" = 1

# The image's own Dockerfile passes its own linter.
docker run --rm -v "$PWD:/work:ro" docker-lint:test --strict Dockerfile

# Cross-compiles, so it is cheap; catches an arm64 failure before release.
- name: Build every platform
if: github.event_name != 'release'
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64

- name: Log in to GitHub Container Registry
if: github.event_name == 'release'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Publish the image
if: github.event_name == 'release'
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.build.outputs.version }}
COMMIT=${{ steps.build.outputs.commit }}
DATE=${{ steps.build.outputs.date }}
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ jobs:
- name: Run tests
run: go test ./...

# The docker-lint-docker pre-commit hook names the image by version, so a
# release must ship a hook that points at its own image.
- name: Check the pre-commit hook uses this version's image
run: |
image="ghcr.io/devblac/docker-lint:${GITHUB_REF_NAME#v}"
if ! grep -q "entry: $image\$" .pre-commit-hooks.yaml; then
echo "::error file=.pre-commit-hooks.yaml::the docker-lint-docker hook must use $image"
exit 1
fi

- name: Extract release notes from CHANGELOG.md
run: sh scripts/release-notes.sh "${GITHUB_REF_NAME#v}" > "$RUNNER_TEMP/release-notes.md"

Expand Down
31 changes: 31 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# pre-commit hooks for docker-lint. In a project's .pre-commit-config.yaml:
#
# repos:
# - repo: https://github.com/devblac/docker-lint
# rev: v0.1.1
# hooks:
# - id: docker-lint # builds docker-lint with Go
# # or
# - id: docker-lint-docker # runs the container image instead
#
# Both pass the staged Dockerfiles to docker-lint, which also reads the
# project's .docker-lint.yaml. Templates such as Dockerfile.template are left
# out, as in a directory search.

- id: docker-lint
name: docker-lint
description: Lint Dockerfiles for inefficiencies, anti-patterns and security issues.
entry: docker-lint
language: golang
files: &dockerfiles (?i)(^|/)(dockerfile|containerfile)(\.[^/]*)?$|\.(dockerfile|containerfile)$
exclude: &not-dockerfiles (?i)\.(dockerignore|template|tmpl|tpl|j2|jinja2?|erb|mustache|in|bak|orig|old|swp|rej|md|txt|sample|example)$
require_serial: true

- id: docker-lint-docker
name: docker-lint (Docker)
description: Lint Dockerfiles with the docker-lint container image; needs Docker but not Go.
entry: ghcr.io/devblac/docker-lint:0.1.1
language: docker_image
files: *dockerfiles
exclude: *not-dockerfiles
require_serial: true
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ source. Version 0.1.0 was tagged but never published.
at (`@v0.1.1` installs v0.1.1), so pinning the action pins the linter
- Tests that every rule reports a real-world example of its problem, alongside
the corpus that guards against false positives
- Container image `ghcr.io/devblac/docker-lint` for linux/amd64 and linux/arm64,
a static binary on distroless running as a non-root user, published when a
release is published
- pre-commit hooks: `docker-lint` (built with Go) and `docker-lint-docker` (runs
the container image)

### Changed
- **Breaking:** rule IDs move from `DLxxxx`, which collide with hadolint's IDs for
Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ release, so nothing is public until a maintainer publishes it.
1. Make sure `main` is green and `goreleaser release --snapshot --clean` builds locally
(CI also runs it on every pull request).
2. In `CHANGELOG.md`, rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, add a new
empty `## [Unreleased]` above it, update the comparison links at the bottom, and
merge that change to `main`.
empty `## [Unreleased]` above it, update the comparison links at the bottom, point
the `docker-lint-docker` hook in `.pre-commit-hooks.yaml` and the version examples
in `README.md` at the new version, and merge that change to `main`. The Release
workflow fails if the hook names another version's image.
3. Tag the merge commit and push the tag:

```bash
Expand All @@ -117,6 +119,9 @@ release, so nothing is public until a maintainer publishes it.
verify it against `checksums.txt`, then publish it. Avoid `go install ...@vX.Y.Z`
before publishing: the Go module proxy caches a tag's contents permanently, so the
tag can no longer be moved if the release needs fixing.
6. Publishing the release runs the Image workflow, which pushes
`ghcr.io/devblac/docker-lint` for linux/amd64 and linux/arm64. The first time,
make the package public in its settings on GitHub; packages start out private.

## Questions?

Expand Down
43 changes: 43 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# docker-lint container image.
#
# docker run --rm -i ghcr.io/devblac/docker-lint < Dockerfile
# docker run --rm -v "$PWD:/work" ghcr.io/devblac/docker-lint .
#
# Built for linux/amd64 and linux/arm64 by .github/workflows/image.yml when a
# release is published. The build stage runs on the build machine's platform
# and cross-compiles, so no emulation is needed.

FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build

ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=none
ARG DATE=unknown

WORKDIR /src

# Download modules in their own layer, so source changes reuse it.
COPY go.mod go.sum ./
RUN go mod download

COPY cmd/ cmd/
COPY internal/ internal/
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \
-o /out/docker-lint ./cmd/docker-lint

# A static binary needs no libc or shell; the nonroot variant runs as UID 65532.
FROM gcr.io/distroless/static-debian12:nonroot

LABEL org.opencontainers.image.title="docker-lint" \
org.opencontainers.image.description="Static analyzer for Dockerfiles" \
org.opencontainers.image.source="https://github.com/devblac/docker-lint" \
org.opencontainers.image.licenses="MIT"

COPY --from=build /out/docker-lint /usr/local/bin/docker-lint

WORKDIR /work

# docker-lint ignore: DKL404
ENTRYPOINT ["docker-lint"]
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ sudo mv docker-lint /usr/local/bin/
Archives are named `docker-lint_<version>_<os>_<arch>`, with `.zip` for Windows and
`.tar.gz` elsewhere.

### Docker

A 2.5 MB image (static binary on distroless, running as a non-root user) for linux/amd64 and linux/arm64:

```bash
# Lint from standard input
docker run --rm -i ghcr.io/devblac/docker-lint:0.1.1 < Dockerfile

# Lint every Dockerfile in the current directory
docker run --rm -v "$PWD:/work" ghcr.io/devblac/docker-lint:0.1.1 .
```

### pre-commit

Add to `.pre-commit-config.yaml` to lint staged Dockerfiles before each commit:

```yaml
repos:
- repo: https://github.com/devblac/docker-lint
rev: v0.1.1
hooks:
- id: docker-lint # builds docker-lint with Go
# - id: docker-lint-docker # or runs the container image (needs Docker, not Go)
```

Both hooks read the project's `.docker-lint.yaml` and skip Dockerfile templates.

### From Source

Requires Go 1.22 or later (as specified in `go.mod`).
Expand Down Expand Up @@ -377,8 +404,8 @@ go test ./...
# Build
go build -o docker-lint ./cmd/docker-lint

# Run linter on itself
./docker-lint testdata/valid/minimal.Dockerfile
# Run linter on itself (the repository's own Dockerfile builds the image)
./docker-lint Dockerfile
```

## License
Expand Down
Loading