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
10 changes: 10 additions & 0 deletions .github/workflows/container.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ jobs:
name: Checkout Repository
uses: actions/checkout@v7

- id: hadolint
name: Lint Containerfile with hadolint
run: >
docker run --rm -i
-v "${{ github.workspace }}/.hadolint.yaml:/.hadolint.yaml"
--entrypoint hadolint
hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e
--config /.hadolint.yaml
- < ./Containerfile

Comment thread
josecelano marked this conversation as resolved.
- id: setup-buildx
name: Setup Buildx
uses: docker/setup-buildx-action@v4
Expand Down
69 changes: 69 additions & 0 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ----- hadolint global ignore configuration -----
#
# Rationale for each globally ignored rule is documented below.
# When adding a new inline `# hadolint ignore=` comment, also add rationale
# alongside it explaining why it's safe to ignore.
#
# Global ignores keep the Containerfile clean by avoiding repetitive
# `# hadolint ignore=` comments for rules that are systematically
# inapplicable to this project's build strategy.

ignored:
# DL3008: Pin versions in apt-get install.
#
# We do not pin package versions in intermediate build stages (chef, tester,
# gcc) because:
# - These stages are development/build-time only, not production runtime images
# - Pinning would require constant manual maintenance as base images update
# - The base image tag (e.g. `slim-trixie`) tracks the latest Debian trixie
# point release. Tags are not immutable — upstream can publish security
# rebuilds under the same tag. We accept this tag drift and rely on the
# CI rebuild cycle to pick up fixes.
- DL3008

# DL3059: Multiple consecutive RUN instructions.
#
# We intentionally use separate RUN instructions for Docker layer caching.
# Each RUN creates a cacheable layer, which speeds up rebuilds when only
# specific steps change. Consolidating them would reduce cache efficiency
# and increase rebuild times during development.
- DL3059

# DL4006: set -o pipefail is not available.
#
# Debian-based images use /bin/sh symlinked to /bin/dash, which does not
# support the `pipefail` option. Switching to `SHELL ["/bin/bash", "-o",
# "pipefail", "-c"]` would require installing bash in every build stage,
# adding unnecessary image size and build time.
#
# The pipe operations in this Containerfile are:
# - `curl -L --proto '=https' --tlsv1.2 -sSf https://... | bash`: downloads
# the cargo-binstall installer script from a GitHub raw URL (`/main/` branch).
# The URL points to a branch, not a pinned commit. The risk is that an
# upstream compromise could inject malicious content. However, the `-sSf`
# flags already make curl return a non-zero exit code on HTTP/download
# failures, and the downstream `cargo binstall` step will fail if the
# script produced no binary. This is a known trade-off accepted by the
# project: pinning to a specific commit would require manual updates on
# every upstream release and the upstream is a trusted dependency.
# - `ldd ... | grep ... | awk ...`: simple text processing for single-file
# library discovery. If the pipe fails, the `cp` target is empty and the
# subsequent build step (or runtime) will fail immediately.
- DL4006

# SC2046: Quote to prevent word splitting.
#
# The unquoted `$(realpath ...)` expansion is used as the source argument
# for `cp` in a specific pattern where word splitting is intentional and
# safe: the output of `realpath` is a single path, and the `ldd | grep`
# pipeline it wraps also produces a single path. The ShellCheck warning
# is a false positive in this context.
#
# This is kept as a global ignore rather than inline because:
# - The pattern is identical in both debug and release stages (same
# `$(realpath $(ldd ... | grep ... | awk ...))` expression)
# - Inline `# hadolint ignore=SC2046` comments for ShellCheck rules in
# Dockerfiles have inconsistent behavior across hadolint versions
# - A global rule with documented rationale is cleaner and avoids
# duplicating the same inline comment with rationale in two places
- SC2046
Comment thread
josecelano marked this conversation as resolved.
4 changes: 4 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# syntax=docker/dockerfile:latest
#
# semantic-links:
# related-artifacts:
# - .hadolint.yaml # hadolint global linting rules and ignore policies with rationale

# Torrust Tracker

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/usr/bin/env bash
# Format the repository cspell dictionary with deterministic ordering and exact de-duplication.
#
# Tests: tests/test-format-project-words.sh
#
# NOTE: These tests are NOT automatically run by the pre-commit hook or CI.
# If you modify this script, run the tests manually:
# bash contrib/dev-tools/checks/tests/test-format-project-words.sh
# This will be addressed by the AI harness redesign (EPIC #2003).

set -uo pipefail

Expand Down Expand Up @@ -41,4 +48,4 @@ fi

printf 'Formatted project-words.txt with LC_ALL=C sort -u.\n'
printf "Stage 'project-words.txt' and retry the commit.\n"
exit 1
exit 1
55 changes: 55 additions & 0 deletions contrib/dev-tools/checks/lint-containerfile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Lint the Containerfile with hadolint.
#
# Tests: (no automated tests yet — EPIC #2003)
#
# This sensor is a standalone check: it can be triggered by any orchestrator
# (pre-commit hook, CI, Copilot file hooks, manual invocation). It only runs
# hadolint when the Containerfile has been staged for commit (git diff check).
# See EPIC #2003 for the long-term harness/sensor architecture design.
#
# Usage:
# ./contrib/dev-tools/checks/lint-containerfile.sh

set -uo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
PROJECT_ROOT=$(cd -- "${SCRIPT_DIR}/../../.." && pwd)
CONTAINERFILE="${PROJECT_ROOT}/Containerfile"
CONFIG="${PROJECT_ROOT}/.hadolint.yaml"
HADOLINT_IMAGE="hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e"

# Skip if Containerfile wasn't changed (staged).
# Use a separate check so that a non-zero exit from `git diff` (e.g. running
# outside a git work tree) is not silently swallowed by `!`.
if git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -q '^Containerfile$'; then
: # Containerfile is staged — proceed
elif [[ $? -eq 1 ]]; then
# grep exited 1: Containerfile not found in staged changes
echo "Containerfile unchanged, skipping hadolint"
exit 0
else
# git diff or grep failed (e.g. not a git repository)
echo "Error: cannot check staged changes (not a git repository?)." >&2
exit 2
fi

# Lint the staged version of the Containerfile to avoid false positives
# from unstaged working-tree changes. This ensures the sensor checks exactly
# what will be committed, not the current working tree.
# Use `git show` piped directly to avoid shell mangling from `echo`.
if [[ ! -f "${CONFIG}" ]]; then
echo "Warning: hadolint config '${CONFIG}' not found, running without." >&2
git show :./"${CONTAINERFILE##*/}" 2>/dev/null | docker run --rm -i --entrypoint hadolint "${HADOLINT_IMAGE}" -
exit $?
fi

git show :./"${CONTAINERFILE##*/}" 2>/dev/null | docker run --rm -i \
-v "${CONFIG}:/.hadolint.yaml" \
--entrypoint hadolint \
"${HADOLINT_IMAGE}" \
--config /.hadolint.yaml \
-

# Capture the exit code from the pipeline (last command: hadolint)
exit "${PIPESTATUS[0]}"
Comment thread
josecelano marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
#!/usr/bin/env bash
# Integration tests for the project dictionary formatter and pre-commit orchestration.
# Integration tests for the project dictionary formatter sensor and pre-commit orchestration.
#
# Sensor: ../format-project-words.sh
#
# NOTE: These tests are NOT automatically run by the pre-commit hook or CI.
# Run them manually after modifying the sensor:
# bash contrib/dev-tools/checks/tests/test-format-project-words.sh
# This will be addressed by the AI harness redesign (EPIC #2003).

set -euo pipefail

PROJECT_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../../.." && pwd)
PROJECT_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)
TEST_DIRECTORY=$(mktemp -d "${TMPDIR:-/tmp}/test-format-project-words.XXXXXX")
trap 'rm -rf "${TEST_DIRECTORY}"' EXIT

create_fixture() {
local fixture_name=$1
local fixture_root="${TEST_DIRECTORY}/${fixture_name}"

mkdir -p "${fixture_root}/contrib/dev-tools/git/hooks" "${fixture_root}/bin" "${fixture_root}/logs"
cp "${PROJECT_ROOT}/contrib/dev-tools/git/format-project-words.sh" "${fixture_root}/contrib/dev-tools/git/"
mkdir -p \
"${fixture_root}/contrib/dev-tools/checks" \
"${fixture_root}/contrib/dev-tools/git/hooks" \
"${fixture_root}/bin" \
"${fixture_root}/logs"
cp "${PROJECT_ROOT}/contrib/dev-tools/checks/format-project-words.sh" "${fixture_root}/contrib/dev-tools/checks/"
cp "${PROJECT_ROOT}/contrib/dev-tools/git/hooks/pre-commit.sh" "${fixture_root}/contrib/dev-tools/git/hooks/"
chmod +x \
"${fixture_root}/contrib/dev-tools/git/format-project-words.sh" \
"${fixture_root}/contrib/dev-tools/checks/format-project-words.sh" \
"${fixture_root}/contrib/dev-tools/git/hooks/pre-commit.sh"

printf '%s\n' "${fixture_root}"
Expand All @@ -42,7 +53,7 @@ it_should_sort_and_remove_exact_duplicates_when_dictionary_requires_formatting()
printf 'zebra\nAlpha\nalpha\nAlpha\n' >"${fixture_root}/project-words.txt"

# Act
if "${fixture_root}/contrib/dev-tools/git/format-project-words.sh" >"${fixture_root}/formatter-output.txt" 2>&1; then
if "${fixture_root}/contrib/dev-tools/checks/format-project-words.sh" >"${fixture_root}/formatter-output.txt" 2>&1; then
printf 'Expected formatter to report a changed dictionary.\n' >&2
return 1
fi
Expand All @@ -59,7 +70,7 @@ it_should_report_success_when_dictionary_is_already_formatted() {
printf 'Alpha\nalpha\nzebra\n' >"${fixture_root}/project-words.txt"

# Act
"${fixture_root}/contrib/dev-tools/git/format-project-words.sh" >"${fixture_root}/formatter-output.txt"
"${fixture_root}/contrib/dev-tools/checks/format-project-words.sh" >"${fixture_root}/formatter-output.txt"

# Assert
grep -F -q 'project-words.txt is already formatted.' "${fixture_root}/formatter-output.txt"
Expand All @@ -77,7 +88,7 @@ EOF
chmod +x "${fixture_root}/bin/mktemp"

# Act
if PATH="${fixture_root}/bin:${PATH}" "${fixture_root}/contrib/dev-tools/git/format-project-words.sh" >"${fixture_root}/formatter-output.txt" 2>&1; then
if PATH="${fixture_root}/bin:${PATH}" "${fixture_root}/contrib/dev-tools/checks/format-project-words.sh" >"${fixture_root}/formatter-output.txt" 2>&1; then
printf 'Expected formatter to fail when it cannot create its temporary dictionary.\n' >&2
return 1
fi
Expand Down
3 changes: 2 additions & 1 deletion contrib/dev-tools/git/hooks/pre-commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ set -uo pipefail
# Each step: "description|command"

declare -a STEPS=(
"Formatting project dictionary|./contrib/dev-tools/git/format-project-words.sh"
"Formatting project dictionary|./contrib/dev-tools/checks/format-project-words.sh"
"Checking for unused dependencies (cargo machete --with-metadata)|cargo machete --with-metadata"
"Checking workspace layer boundary bans (cargo deny check bans)|cargo deny check bans"
"Running all linters|linter all"
"Linting Containerfile with hadolint|./contrib/dev-tools/checks/lint-containerfile.sh"
Comment thread
josecelano marked this conversation as resolved.
"Running documentation tests|cargo test --doc --workspace"
)

Expand Down
Loading
Loading