Skip to content

Commit 7fc1b17

Browse files
committed
fix: resolve hadolint findings and add local CI runner
1 parent a0c87c8 commit 7fc1b17

4 files changed

Lines changed: 245 additions & 3 deletions

File tree

deploy/Dockerfile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ COPY go.mod go.sum ./
1313
RUN go mod download
1414

1515
# Copy source and build.
16+
# GOOS/GOARCH intentionally unset: buildx + QEMU runs this stage on the
17+
# TARGETPLATFORM for each entry in `platforms:` (amd64, arm64), so Go picks
18+
# up the correct target arch natively. The playwright-go driver install
19+
# below also relies on this — it downloads /driver/node matching the arch
20+
# of the builder process.
1621
COPY . .
17-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
22+
RUN CGO_ENABLED=0 GOOS=linux \
1823
go build -ldflags="-s -w" -trimpath -o /palena ./cmd/palena
1924

2025
# Download the Playwright driver into /driver via playwright-go's own CLI.
2126
# The driver zip from Microsoft's CDN ships its own `node` binary, so the
2227
# runtime image does not need Node.js installed.
2328
ENV PLAYWRIGHT_DRIVER_PATH=/driver
2429
RUN mkdir -p /driver \
25-
&& go run github.com/playwright-community/playwright-go/cmd/playwright install --no-browsers \
30+
&& go run github.com/playwright-community/playwright-go/cmd/playwright@v0.5700.1 install --no-browsers \
2631
|| true
2732
# The install command returns non-zero when browsers are skipped with no args.
2833
# Verify the driver binary exists; fail the build if it does not.
@@ -39,6 +44,10 @@ RUN chmod -R a+rX /driver
3944
FROM debian:bookworm-slim
4045

4146
# ca-certificates so the Go binary can talk HTTPS to SearXNG, reranker, etc.
47+
# (debian:bookworm-slim pins a single ca-certificates version per release;
48+
# pinning inline here would have to be updated every time the base image
49+
# bumps, which Renovate already handles at the FROM level.)
50+
# hadolint ignore=DL3008
4251
RUN apt-get update \
4352
&& apt-get install -y --no-install-recommends ca-certificates \
4453
&& rm -rf /var/lib/apt/lists/* \

deploy/Dockerfile.flashrank

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ FROM python:3.11-slim
55

66
WORKDIR /app
77

8-
RUN pip install --no-cache-dir flask flashrank
8+
COPY flashrank-requirements.txt .
9+
RUN pip install --no-cache-dir --require-hashes=false -r flashrank-requirements.txt
910

1011
COPY flashrank_server.py .
1112

deploy/flashrank-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
flask==3.1.0
2+
flashrank==0.2.10

scripts/ci-local.sh

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2026 bitkaio LLC. All rights reserved.
3+
# Licensed under the Apache License, Version 2.0. See LICENSE for details.
4+
#
5+
# Run the same checks GitHub Actions CI runs, locally, in Docker.
6+
# Mirrors .github/workflows/ci.yml so you can get a green-or-red signal
7+
# before pushing.
8+
#
9+
# Usage:
10+
# scripts/ci-local.sh # run all checks
11+
# scripts/ci-local.sh lint test # run only selected checks
12+
# SKIP_CONTAINER_SCAN=1 scripts/ci-local.sh # skip the slow container build+trivy
13+
#
14+
# Checks (names match the CI job names as closely as possible):
15+
# lint golangci-lint (Docker)
16+
# test go test -race ./...
17+
# vulncheck govulncheck ./...
18+
# semgrep Semgrep with the same config set as CI (Docker)
19+
# hadolint Hadolint on both Dockerfiles (Docker)
20+
# zizmor Zizmor on .github/workflows (Docker)
21+
# dockerfile Build deploy/Dockerfile (no push)
22+
# trivy Trivy HIGH/CRITICAL scan of the built image (Docker)
23+
# markdown markdownlint-cli2 (Docker)
24+
#
25+
# Exit code is non-zero if any selected check fails.
26+
27+
set -euo pipefail
28+
29+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
30+
cd "$REPO_ROOT"
31+
32+
# --- Pinned tool versions (keep in sync with .github/workflows/ci.yml) -------
33+
SEMGREP_IMAGE="semgrep/semgrep@sha256:d8159ff400a103b21d231a9646452025769552e631df786f508448d2e4eacf86"
34+
HADOLINT_IMAGE="ghcr.io/hadolint/hadolint:v2.14.0-debian"
35+
GOLANGCI_IMAGE="golangci/golangci-lint:latest-alpine"
36+
ZIZMOR_VERSION="1.17.0"
37+
TRIVY_IMAGE="aquasec/trivy:0.70.0"
38+
MARKDOWNLINT_IMAGE="davidanson/markdownlint-cli2:v0.18.1"
39+
LOCAL_IMAGE_TAG="palena:ci-local"
40+
41+
# --- Pretty output ------------------------------------------------------------
42+
C_GREEN=$'\033[32m'; C_RED=$'\033[31m'; C_YELLOW=$'\033[33m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
43+
44+
FAILED=()
45+
PASSED=()
46+
47+
step() { printf '\n%s==> %s%s\n' "$C_BOLD" "$1" "$C_RESET"; }
48+
ok() { printf '%s[PASS]%s %s\n' "$C_GREEN" "$C_RESET" "$1"; PASSED+=("$1"); }
49+
fail() { printf '%s[FAIL]%s %s\n' "$C_RED" "$C_RESET" "$1"; FAILED+=("$1"); }
50+
skip() { printf '%s[SKIP]%s %s\n' "$C_YELLOW" "$C_RESET" "$1"; }
51+
52+
need_docker() {
53+
if ! command -v docker >/dev/null 2>&1; then
54+
echo "docker is required but not installed" >&2
55+
exit 2
56+
fi
57+
}
58+
59+
# --- Individual checks --------------------------------------------------------
60+
61+
run_lint() {
62+
step "golangci-lint"
63+
need_docker
64+
if docker run --rm -v "$REPO_ROOT:/app" -w /app "$GOLANGCI_IMAGE" \
65+
golangci-lint run --timeout=5m; then
66+
ok "lint"
67+
else
68+
fail "lint"
69+
fi
70+
}
71+
72+
run_test() {
73+
step "go test -race ./..."
74+
if ! command -v go >/dev/null 2>&1; then
75+
skip "test (go not installed locally)"
76+
return
77+
fi
78+
if go test -race -coverprofile=coverage.out -covermode=atomic ./...; then
79+
ok "test"
80+
else
81+
fail "test"
82+
fi
83+
}
84+
85+
run_vulncheck() {
86+
step "govulncheck"
87+
if ! command -v go >/dev/null 2>&1; then
88+
skip "vulncheck (go not installed locally)"
89+
return
90+
fi
91+
if ! command -v govulncheck >/dev/null 2>&1; then
92+
go install golang.org/x/vuln/cmd/govulncheck@latest
93+
fi
94+
if govulncheck ./...; then
95+
ok "vulncheck"
96+
else
97+
fail "vulncheck"
98+
fi
99+
}
100+
101+
run_semgrep() {
102+
step "semgrep (same config + severity as CI)"
103+
need_docker
104+
if docker run --rm -v "$REPO_ROOT:/src" -w /src "$SEMGREP_IMAGE" \
105+
semgrep scan \
106+
--config p/golang \
107+
--config p/security-audit \
108+
--config p/owasp-top-ten \
109+
--config p/dockerfile \
110+
--severity ERROR \
111+
--error \
112+
--metrics=off; then
113+
ok "semgrep"
114+
else
115+
fail "semgrep"
116+
fi
117+
}
118+
119+
run_hadolint() {
120+
step "hadolint (deploy/Dockerfile + deploy/Dockerfile.flashrank)"
121+
need_docker
122+
local rc=0
123+
for df in deploy/Dockerfile deploy/Dockerfile.flashrank; do
124+
printf -- '--- %s ---\n' "$df"
125+
if ! docker run --rm -i "$HADOLINT_IMAGE" hadolint - < "$df"; then
126+
rc=1
127+
fi
128+
done
129+
if [ "$rc" -eq 0 ]; then
130+
ok "hadolint"
131+
else
132+
fail "hadolint"
133+
fi
134+
}
135+
136+
run_zizmor() {
137+
step "zizmor (fail on HIGH)"
138+
need_docker
139+
# zizmor ships an official pip package; run via python:slim to avoid
140+
# polluting the host Python env.
141+
if docker run --rm -v "$REPO_ROOT:/src" -w /src python:3.12-slim \
142+
sh -c "pip install --quiet --disable-pip-version-check 'zizmor==$ZIZMOR_VERSION' \
143+
&& zizmor --min-severity high --format plain .github/workflows/"; then
144+
ok "zizmor"
145+
else
146+
fail "zizmor"
147+
fi
148+
}
149+
150+
run_dockerfile() {
151+
step "docker build deploy/Dockerfile"
152+
need_docker
153+
if docker build -f deploy/Dockerfile -t "$LOCAL_IMAGE_TAG" .; then
154+
ok "dockerfile"
155+
else
156+
fail "dockerfile"
157+
fi
158+
}
159+
160+
run_trivy() {
161+
step "trivy image scan (HIGH/CRITICAL, --ignore-unfixed)"
162+
need_docker
163+
if ! docker image inspect "$LOCAL_IMAGE_TAG" >/dev/null 2>&1; then
164+
echo "$LOCAL_IMAGE_TAG not built; running 'dockerfile' first"
165+
run_dockerfile
166+
fi
167+
if docker run --rm \
168+
-v /var/run/docker.sock:/var/run/docker.sock \
169+
"$TRIVY_IMAGE" image \
170+
--exit-code 1 \
171+
--severity HIGH,CRITICAL \
172+
--ignore-unfixed \
173+
"$LOCAL_IMAGE_TAG"; then
174+
ok "trivy"
175+
else
176+
fail "trivy"
177+
fi
178+
}
179+
180+
run_markdown() {
181+
step "markdownlint-cli2"
182+
need_docker
183+
if docker run --rm -v "$REPO_ROOT:/workdir" "$MARKDOWNLINT_IMAGE" \
184+
"**/*.md" "#node_modules" "#tmp" "#vendor"; then
185+
ok "markdown"
186+
else
187+
fail "markdown"
188+
fi
189+
}
190+
191+
# --- Dispatch -----------------------------------------------------------------
192+
193+
ALL_CHECKS=(lint test vulncheck semgrep hadolint zizmor dockerfile trivy markdown)
194+
195+
if [ "$#" -gt 0 ]; then
196+
SELECTED=("$@")
197+
else
198+
SELECTED=("${ALL_CHECKS[@]}")
199+
fi
200+
201+
for check in "${SELECTED[@]}"; do
202+
case "$check" in
203+
lint) run_lint ;;
204+
test) run_test ;;
205+
vulncheck) run_vulncheck ;;
206+
semgrep) run_semgrep ;;
207+
hadolint) run_hadolint ;;
208+
zizmor) run_zizmor ;;
209+
dockerfile) run_dockerfile ;;
210+
trivy)
211+
if [ "${SKIP_CONTAINER_SCAN:-0}" = "1" ]; then
212+
skip "trivy (SKIP_CONTAINER_SCAN=1)"
213+
else
214+
run_trivy
215+
fi
216+
;;
217+
markdown) run_markdown ;;
218+
*) echo "unknown check: $check" >&2; exit 2 ;;
219+
esac
220+
done
221+
222+
printf '\n%s=== Summary ===%s\n' "$C_BOLD" "$C_RESET"
223+
for c in ${PASSED[@]+"${PASSED[@]}"}; do printf '%s[PASS]%s %s\n' "$C_GREEN" "$C_RESET" "$c"; done
224+
for c in ${FAILED[@]+"${FAILED[@]}"}; do printf '%s[FAIL]%s %s\n' "$C_RED" "$C_RESET" "$c"; done
225+
226+
if [ "${#FAILED[@]}" -gt 0 ]; then
227+
printf '\n%s%d check(s) failed.%s\n' "$C_RED" "${#FAILED[@]}" "$C_RESET"
228+
exit 1
229+
fi
230+
printf '\n%sAll checks passed.%s\n' "$C_GREEN" "$C_RESET"

0 commit comments

Comments
 (0)