Skip to content

Commit 8a23f6c

Browse files
authored
ci: add reusable Docker lint workflow (hadolint + shellcheck) (#61)
* ci: add reusable Docker lint workflow (hadolint + shellcheck) * fix: correct hadolint SHA256 checksum * fix: replace tr newline with xargs to avoid YAML parse error
1 parent 8f81f89 commit 8a23f6c

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Reusable workflow: Dockerfile and shell script linting
2+
#
3+
# Lints container build inputs: Dockerfile(s) via hadolint and shell scripts
4+
# under a configurable directory via shellcheck. These catch regressions that
5+
# behavioral tests cannot — unquoted variable expansions, silently-failing
6+
# RUN commands, missing apt-get cleanup, etc.
7+
#
8+
# Design reference: hermes-agent .github/workflows/docker-lint.yml
9+
#
10+
# Usage example (caller workflow):
11+
#
12+
# on:
13+
# push:
14+
# branches: [main]
15+
# paths:
16+
# - 'Dockerfile*'
17+
# - 'docker/**'
18+
# - '.hadolint.yaml'
19+
# pull_request:
20+
# branches: [main]
21+
# paths:
22+
# - 'Dockerfile*'
23+
# - 'docker/**'
24+
# - '.hadolint.yaml'
25+
#
26+
# jobs:
27+
# docker-lint:
28+
# uses: Mininglamp-OSS/.github/.github/workflows/reusable-docker-lint.yml@main
29+
#
30+
# Callers can override inputs for non-standard layouts:
31+
#
32+
# jobs:
33+
# docker-lint:
34+
# uses: Mininglamp-OSS/.github/.github/workflows/reusable-docker-lint.yml@main
35+
# with:
36+
# dockerfiles: 'Dockerfile.api Dockerfile.worker'
37+
# shell-dir: './scripts'
38+
# hadolint-failure-threshold: 'error'
39+
#
40+
# SECURITY: This workflow only reads source files. No secrets are required
41+
# and no code is executed — hadolint parses Dockerfiles statically and
42+
# shellcheck analyzes shell scripts without running them.
43+
name: Reusable — Docker Lint
44+
45+
on:
46+
workflow_call:
47+
inputs:
48+
dockerfiles:
49+
description: >
50+
Space-separated list of Dockerfile paths to lint.
51+
Default 'auto' discovers all files matching Dockerfile* at the repo root.
52+
type: string
53+
required: false
54+
default: 'auto'
55+
shell-dir:
56+
description: >
57+
Directory to scan for shell scripts (shellcheck).
58+
Set to empty string to skip shellcheck. Default: './docker'
59+
type: string
60+
required: false
61+
default: './docker'
62+
hadolint-failure-threshold:
63+
description: >
64+
Hadolint failure threshold: error, warning, info, style, ignore.
65+
Default: warning (same as hermes-agent).
66+
type: string
67+
required: false
68+
default: 'warning'
69+
shellcheck-severity:
70+
description: >
71+
Shellcheck minimum severity: error, warning, info, style.
72+
Default: error (avoids false positives from missing sourced files).
73+
type: string
74+
required: false
75+
default: 'error'
76+
77+
permissions: {}
78+
79+
jobs:
80+
hadolint:
81+
name: Lint Dockerfiles (hadolint)
82+
runs-on: ubuntu-24.04
83+
timeout-minutes: 5
84+
permissions:
85+
contents: read
86+
steps:
87+
- name: Checkout
88+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
89+
with:
90+
persist-credentials: false
91+
92+
- name: Discover Dockerfiles
93+
id: discover
94+
env:
95+
INPUT_DOCKERFILES: ${{ inputs.dockerfiles }}
96+
run: |
97+
set -euo pipefail
98+
if [ "$INPUT_DOCKERFILES" = "auto" ]; then
99+
# Find all Dockerfile* at repo root (Dockerfile, Dockerfile.api, etc.)
100+
files=$(find . -maxdepth 1 -name 'Dockerfile*' -type f | sort | xargs)
101+
if [ -z "$files" ]; then
102+
echo "No Dockerfile* found at repo root, skipping hadolint."
103+
echo "skip=true" >> "$GITHUB_OUTPUT"
104+
exit 0
105+
fi
106+
echo "Discovered: $files"
107+
echo "files=$files" >> "$GITHUB_OUTPUT"
108+
else
109+
echo "files=$INPUT_DOCKERFILES" >> "$GITHUB_OUTPUT"
110+
fi
111+
echo "skip=false" >> "$GITHUB_OUTPUT"
112+
113+
- name: Run hadolint
114+
if: steps.discover.outputs.skip != 'true'
115+
env:
116+
FILES: ${{ steps.discover.outputs.files }}
117+
THRESHOLD: ${{ inputs.hadolint-failure-threshold }}
118+
run: |
119+
set -euo pipefail
120+
121+
# Install hadolint binary (faster than Docker-based action for
122+
# multi-file scanning, and allows iterating over discovered files)
123+
HADOLINT_VERSION="2.12.0"
124+
HADOLINT_SHA256="56de6d5e5ec427e17b74fa48d51271c7fc0d61244bf5c90e828aab8362d55010"
125+
curl -sSfL -o hadolint \
126+
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64"
127+
echo "${HADOLINT_SHA256} hadolint" | sha256sum -c -
128+
chmod +x hadolint
129+
130+
exit_code=0
131+
for dockerfile in $FILES; do
132+
if [ ! -f "$dockerfile" ]; then
133+
echo "::warning::Dockerfile not found: $dockerfile"
134+
continue
135+
fi
136+
echo "--- Linting $dockerfile ---"
137+
./hadolint --failure-threshold "$THRESHOLD" "$dockerfile" || exit_code=$?
138+
done
139+
exit "$exit_code"
140+
141+
shellcheck:
142+
name: Lint shell scripts (shellcheck)
143+
if: inputs.shell-dir != ''
144+
runs-on: ubuntu-24.04
145+
timeout-minutes: 5
146+
permissions:
147+
contents: read
148+
steps:
149+
- name: Checkout
150+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
151+
with:
152+
persist-credentials: false
153+
154+
- name: Check for shell scripts
155+
id: check
156+
env:
157+
SHELL_DIR: ${{ inputs.shell-dir }}
158+
run: |
159+
set -euo pipefail
160+
if [ ! -d "$SHELL_DIR" ]; then
161+
echo "Directory $SHELL_DIR does not exist, skipping shellcheck."
162+
echo "skip=true" >> "$GITHUB_OUTPUT"
163+
exit 0
164+
fi
165+
# Check if any .sh files exist
166+
count=$(find "$SHELL_DIR" -name '*.sh' -type f | wc -l)
167+
if [ "$count" -eq 0 ]; then
168+
echo "No .sh files found in $SHELL_DIR, skipping shellcheck."
169+
echo "skip=true" >> "$GITHUB_OUTPUT"
170+
exit 0
171+
fi
172+
echo "Found $count shell script(s) in $SHELL_DIR"
173+
echo "skip=false" >> "$GITHUB_OUTPUT"
174+
175+
- name: Run shellcheck
176+
if: steps.check.outputs.skip != 'true'
177+
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0
178+
env:
179+
SHELLCHECK_OPTS: --severity=${{ inputs.shellcheck-severity }}
180+
with:
181+
scandir: ${{ inputs.shell-dir }}

0 commit comments

Comments
 (0)