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
77 changes: 58 additions & 19 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ FROM golang:1.26.4-bookworm AS ci
# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive

# Run RUN steps under bash with pipefail so a failure anywhere in a
# `curl ... | tar` download pipeline aborts the build instead of silently
# installing a truncated or empty binary.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Install essential packages + Docker CLI (for DooD via socket mount in CI)
RUN apt-get update \
&& apt-get -y install --no-install-recommends \
Expand Down Expand Up @@ -48,27 +53,24 @@ RUN apt-get update \
# FLUX_VERSION -> https://github.com/fluxcd/flux2
# FLUX_OPERATOR_VERSION -> https://github.com/controlplaneio-fluxcd/flux-operator
# TASK_VERSION -> https://github.com/go-task/task/releases
# TILT_VERSION -> https://github.com/tilt-dev/tilt/releases
# ACTIONLINT_VERSION -> https://github.com/rhysd/actionlint/releases
# HADOLINT_VERSION -> https://github.com/hadolint/hadolint/releases
# VALKEY_VERSION -> https://github.com/valkey-io/valkey/releases

ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" \
KUBECTL_VERSION=v1.36.1 \
KUBECTL_VERSION=v1.36.2 \
KUSTOMIZE_VERSION=5.8.1 \
KUBEBUILDER_VERSION=4.14.1 \
KUBEBUILDER_VERSION=4.15.0 \
GOLANGCI_LINT_VERSION=v2.12.2 \
HELM_VERSION=v4.2.0 \
K3D_VERSION=v5.9.0 \
FLUX_VERSION=2.8.8 \
FLUX_OPERATOR_VERSION=0.50.0 \
FLUX_VERSION=2.9.0 \
FLUX_OPERATOR_VERSION=0.53.0 \
TASK_VERSION=v3.51.1 \
TILT_VERSION=v0.37.4 \
ACTIONLINT_VERSION=1.7.12 \
HADOLINT_VERSION=2.14.0 \
VALKEY_VERSION=9.1.0

# https://github.com/fluxcd/flux2/releases
# https://fluxoperator.dev/

# Fail early on unsupported architectures instead of producing a partial image.
RUN test "$(dpkg --print-architecture)" = "amd64" \
|| (echo "This devcontainer currently supports amd64 only." && exit 1)
Expand Down Expand Up @@ -135,6 +137,17 @@ RUN asset="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
&& install -m 0755 actionlint /usr/local/bin/actionlint \
&& rm -rf "${tmpdir}"

# Install hadolint (static linter for Dockerfiles)
RUN asset="hadolint-linux-x86_64" \
&& base="https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}" \
&& tmpdir="$(mktemp -d)" \
&& curl -fsSL "${base}/${asset}" -o "${tmpdir}/${asset}" \
&& curl -fsSL "${base}/${asset}.sha256" -o "${tmpdir}/${asset}.sha256" \
&& cd "${tmpdir}" \
&& sha256sum -c "${asset}.sha256" \
&& install -m 0755 "${asset}" /usr/local/bin/hadolint \
&& rm -rf "${tmpdir}"

# Install valkey-cli
# Valkey only ships prebuilt binaries for Ubuntu jammy/noble; the jammy build
# (glibc 2.35) is compatible with this bookworm image (glibc 2.36). Extract only
Expand Down Expand Up @@ -170,13 +183,16 @@ RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 \
# This downloads linter dependencies without needing source code
RUN mkdir -p /tmp/golangci-init && cd /tmp/golangci-init \
&& go mod init example.com/init \
&& echo 'package main\n\nfunc main() {}' > main.go \
&& printf 'package main\n\nfunc main() {}\n' > main.go \
&& golangci-lint run --timeout=5m || true \
&& cd / && rm -rf /tmp/golangci-init

# Pre-download Go modules for caching - placed AFTER tool installation
# This layer will be cached and only rebuilt when go.mod/go.sum changes
# Moving this down prevents tool reinstallation when dependencies change
# The single-quoted lines below are shell written verbatim into the profile
# script; build-time expansion must NOT happen, so single quotes are intended.
# hadolint ignore=SC2016
RUN printf '%s\n' \
'# Silently load the optional repo-root .env into login shells.' \
'workspace_dir="${PROJECT_PATH:-}"' \
Expand All @@ -197,14 +213,33 @@ ENV DEBIAN_FRONTEND=dialog
# Default command
CMD ["/bin/bash"]

# Stage 2: Development container with Kind and debugging tools
# Stage 2: Development container, used for local development (https://github.com/devcontainers/spec)
FROM ci AS dev

USER root

# Switch to noninteractive for package installation
ENV DEBIAN_FRONTEND=noninteractive

# Same pipefail hardening as the ci stage (SHELL is not inherited across stages).
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Dev-only tool versions - centralized for easy updates.
# These tools are only used inside the devcontainer (local development, IDE
# tooling, debugging), never in CI, so they live in the dev stage.
#
# Finding the latest versions:
# NODE_MAJOR -> https://github.com/nodejs/node/releases (track a current LTS line)
# TILT_VERSION -> https://github.com/tilt-dev/tilt/releases
# DLV_VERSION -> https://github.com/go-delve/delve/releases
# GOPLS_VERSION -> https://github.com/golang/tools/releases
# STATICCHECK_VERSION -> https://github.com/dominikh/go-tools/releases
ENV NODE_MAJOR=22 \
TILT_VERSION=v0.37.4 \
DLV_VERSION=v1.27.0 \
GOPLS_VERSION=v0.22.0 \
STATICCHECK_VERSION=v0.7.0

# Kind is already installed in the ci stage above.
RUN apt-get update \
&& apt-get -y install --no-install-recommends bash-completion \
Expand All @@ -213,6 +248,8 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*

# Enable bash completion and auto-load Task completions for interactive shells in the dev image only.
# Single-quoted lines are written verbatim into bash.bashrc; no build-time expansion intended.
# hadolint ignore=SC2016
RUN printf '%s\n' \
'' \
'# Enable bash completion and Task completions in interactive shells.' \
Expand All @@ -229,12 +266,10 @@ RUN printf '%s\n' \
>> /etc/bash.bashrc

# Install Node.js (provides npm + npx, e.g. for installing Claude Code skills).
# Dev stage only; CI does not need a JS runtime.
# NODE_MAJOR -> https://github.com/nodejs/node/releases (track a current LTS line)
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& chmod a+r /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends nodejs \
Expand All @@ -259,11 +294,15 @@ RUN arch="$(dpkg --print-architecture)" && \

# Install Delve debugger for Go debugging in VSCode
# ACLs from CI stage should be preserved, so no need to re-apply
RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.1

# Install VSCode Go extension tools (gopls and staticcheck)
RUN go install golang.org/x/tools/gopls@v0.21.1 \
&& go install honnef.co/go/tools/cmd/staticcheck@v0.7.0
RUN go install github.com/go-delve/delve/cmd/dlv@${DLV_VERSION}

# Install VSCode Go extension tools (gopls and staticcheck). Both are editor-only:
# gopls is the language server, and this standalone staticcheck powers the editor's
# live (as-you-type) diagnostics. Neither is a lint step -- `task lint` already runs
# staticcheck's analyzers via golangci-lint's bundled staticcheck linter (configured in
# .golangci.yml), so this binary is for the IDE, not a second lint pass.
RUN go install golang.org/x/tools/gopls@${GOPLS_VERSION} \
&& go install honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION}

# Create vscode user for non-root development and add to godev group
RUN groupadd --gid 1000 vscode \
Expand Down
60 changes: 45 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:
"

lint-helm:
name: Lint and build Helm Chart (and generate single-file installer)
name: Build Helm chart and installer bundle
runs-on: ubuntu-latest
needs: build-ci-container
container:
Expand All @@ -153,9 +153,6 @@ jobs:
- name: Copy generated things from /config
run: task helm-sync

- name: Helm lint
run: helm lint charts/gitops-reverser

- name: Helm template (dry-run)
run: |
helm template gitops-reverser charts/gitops-reverser \
Expand All @@ -181,7 +178,7 @@ jobs:
if-no-files-found: error

lint:
name: Lint Go Code
name: Lint
runs-on: ubuntu-latest
needs: build-ci-container
container:
Expand All @@ -196,19 +193,48 @@ jobs:
- name: Configure Git safe directory (for now needed as workarround https://github.com/actions/checkout/issues/2031)
run: git config --global --add safe.directory /__w/gitops-reverser/gitops-reverser

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
install-mode: none
skip-cache: false
skip-save-cache: false
only-new-issues: ${{ github.event_name == 'pull_request' }}
args: --timeout=5m --concurrency=4
# Runs golangci-lint, hadolint, actionlint, and helm lint (see `task lint`).
# No PR annotations (unlike golangci-lint-action) -- run `task lint` locally.
- name: task lint
run: task lint

- name: task lint cache check
# Container jobs default to `sh`, which rejects `set -o pipefail` below
# ("Illegal option -o pipefail"). Force bash for this bashism-using step.
shell: bash
run: |
set -euo pipefail
cache_log="$(mktemp)"
task --dry --verbose lint 2>&1 | tee "${cache_log}"

for task_name in \
generate \
manifests \
helm-sync \
lint-golang \
lint-dockerfiles \
lint-actions \
lint-helm
do
if ! grep -Fq "Task \"${task_name}\" is up to date" "${cache_log}"; then
echo "Expected ${task_name} to be cached after task lint" >&2
exit 1
fi
done

if grep -Eq '^task: \[[^]]+\]' "${cache_log}"; then
echo "Expected second task lint dry-run to be fully cached, but a command would run" >&2
exit 1
fi

test:
name: Unit tests
runs-on: ubuntu-latest
needs: build-ci-container
permissions:
contents: read # checkout
packages: read # pull the CI base container from GHCR
id-token: write # OIDC token for tokenless Codecov uploads (public repo)
container:
image: ${{ needs.build-ci-container.outputs.image }}
credentials:
Expand All @@ -229,7 +255,7 @@ jobs:
with:
files: cover.out
flags: unit
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: true
fail_ci_if_error: false

docker-build:
Expand Down Expand Up @@ -271,6 +297,10 @@ jobs:
name: E2E (${{ matrix.name }})
runs-on: ubuntu-latest
needs: [build-ci-container, docker-build, lint-helm]
permissions:
contents: read # checkout
packages: read # pull the CI container + project image from GHCR
id-token: write # OIDC token for tokenless Codecov uploads (public repo)
strategy:
matrix:
include:
Expand Down Expand Up @@ -404,7 +434,7 @@ jobs:
with:
files: e2e-cover.out
flags: e2e
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: true
fail_ci_if_error: false

- name: Report disk usage (peak + final)
Expand Down
8 changes: 0 additions & 8 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,3 @@ linters:
# Relax godot for test helpers and utility functions
- path: '(test/|helpers\.go)'
linters: [godot, godoclint]
# Allow utils package name (standard pattern)
- text: "var-naming: avoid meaningless package names"
path: 'test/utils/.*\.go'
linters: [revive]
# Allow types package name (common Go pattern for shared types)
- text: "var-naming: avoid meaningless package names"
path: 'internal/types/.*\.go'
linters: [revive]
20 changes: 20 additions & 0 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# hadolint configuration - https://github.com/hadolint/hadolint#configure
#
# Rules disabled below are deliberate patterns in this repo's tooling/build
# Dockerfiles (.devcontainer/Dockerfile and ./Dockerfile), not defects. Every
# other check stays active. Lint locally with `task lint-dockerfiles`.
ignored:
# Distro package versions are intentionally unpinned in tooling/build images;
# we pin the tool versions we care about via ENV blocks instead.
- DL3008 # apt-get install without a pinned version
- DL3018 # apk add without a pinned version
# Installers `cd` into an ephemeral `mktemp -d` and remove it in the same
# RUN; WORKDIR would persist a path that no longer exists.
- DL3003 # use WORKDIR instead of `cd`

# SC2016 (single-quoted lines that intentionally aren't expanded) is suppressed
# inline with `# hadolint ignore=SC2016` at the two profile-script RUN blocks.

# Fail the build on warnings and errors, but let info/style suggestions through
# so they surface without blocking `task lint`.
failure-threshold: warning
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ task test # Must pass all unit tests + the coverage ratchet (see TESTING RE
task test-e2e # Must pass end-to-end tests
```

If you change a GitHub Actions workflow, also run `task lint-actions`, which lints
`.github/workflows/ci.yml` with `actionlint`. Both `actionlint` and `golangci-lint`
ship in the devcontainer image.
`task lint` also runs `actionlint` on every workflow under `.github/workflows/` (via the
`lint-actions` task) and `hadolint` on the Dockerfiles (via `lint-dockerfiles`), so a
workflow or Dockerfile change is covered by the normal lint gate; you can also run
`task lint-actions` or `task lint-dockerfiles` directly. `actionlint`, `hadolint`, and
`golangci-lint` all ship in the devcontainer image.

## PRE-IMPLEMENTATION BEHAVIOR

Expand Down
Loading