diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0ed923e1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,72 @@ +# CodeLens .dockerignore — keep build context lean (Phase 2, issue #54) +# Excludes files that are not needed inside the container image. + +# Version control — never needed in image, and .git is large. +.git +.gitignore +.gitattributes + +# CI / GitHub metadata +.github/ +.gitlab-ci.yml + +# Tests — not run inside the container, excluded to shrink context. +# (The maximal image installs pytest as an extra, but tests themselves +# are not copied in. Run tests on the host or in a dedicated CI job.) +tests/ +pytest.ini +.pytest_cache/ +.coverage +htmlcov/ + +# Benchmarks + intentional vulnerable fixtures (would pollute scan results +# if accidentally scanned from inside the container). +benchmarks/ + +# VS Code extension — separate concern, shipped via the VS Code marketplace. +vscode-codelens/ + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +*.egg +build/ +dist/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# CodeLens own runtime state — must never be baked into the image. +.codelens/ +*.db +*.sqlite +*.sqlite3 + +# Editor / OS noise +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# Docs (installed via pip or viewed on GitHub; not needed in image) +docs/ +*.md +!README.md + +# Docker files themselves — not needed inside the image. +Dockerfile +Dockerfile.maximal +.dockerignore +.docker/ + +# Logs +*.log +logs/ diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..c46e2ce4 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,151 @@ +name: Docker Build & Publish (GHCR) + +# Phase 2 of issue #54 — container image distribution. +# +# Builds two images on every relevant trigger: +# 1. minimal -> ghcr.io/wolfvin/codelens:latest, :v, :sha- +# 2. maximal -> ghcr.io/wolfvin/codelens:maximal-latest, :maximal-v +# +# Both are built multi-arch (linux/amd64 + linux/arm64) via docker buildx + +# QEMU. Images are pushed to GitHub Container Registry (GHCR) using the +# auto-provisioned GITHUB_TOKEN (no extra secret needed). +# +# Triggers: +# - push to main touching Dockerfile / Dockerfile.maximal / .dockerignore / +# the workflow itself -> :latest + :sha- +# - tag push matching v* (release) -> :v + :latest +# - workflow_dispatch (manual) -> :latest + :sha- +# +# The workflow is a no-op on forks (guard at job level) so contributors can +# fork-test without leaking images to the upstream GHCR namespace. + +on: + push: + branches: + - main + paths: + - 'Dockerfile' + - 'Dockerfile.maximal' + - '.dockerignore' + - '.github/workflows/docker-publish.yml' + - 'scripts/**' + - 'pyproject.toml' + tags: + - 'v*' + workflow_dispatch: + inputs: + dry_run: + description: 'Build but do not push to GHCR' + required: false + default: 'false' + type: choice + options: ['false', 'true'] + +permissions: + contents: read + packages: write # needed to push to GHCR (ghcr.io/wolfvin/*) + +jobs: + build-and-push: + name: Build & Push ${{ matrix.variant }} + runs-on: ubuntu-latest + if: github.repository == 'Wolfvin/CodeLens' + strategy: + fail-fast: false + matrix: + include: + - variant: minimal + dockerfile: Dockerfile + image_suffix: '' + tag_latest: latest + - variant: maximal + dockerfile: Dockerfile.maximal + image_suffix: -maximal + tag_latest: maximal-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU (for cross-arch emulation) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'workflow_dispatch' || inputs.dry_run != 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/wolfvin/codelens${{ matrix.image_suffix }} + # Produces: + # - On main push: type=sha,prefix=sha-,format=short + type=raw,value=${{ matrix.tag_latest }} + # - On tag v*: type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}} + # + type=raw,value=${{ matrix.tag_latest }} + tags: | + type=raw,value=${{ matrix.tag_latest }},enable={{is_default_branch}} + type=sha,prefix=sha-,format=short + type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}} + labels: | + org.opencontainers.image.title=CodeLens (${{ matrix.variant }}) + org.opencontainers.image.source=https://github.com/Wolfvin/CodeLens + org.opencontainers.image.licenses=MIT + + - name: Build & push ${{ matrix.variant }} image + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.variant }} + cache-to: type=gha,mode=max,scope=${{ matrix.variant }} + build-args: | + PYTHON_VERSION=3.11 + # No provenance file for now — Phase 5 (release signing) will add + # SBOM + SLSA via Cosign. Keeping this job focused on image build. + + - name: Smoke-test the built image (minimal only, amd64) + # Quick sanity check that the CLI loads and the command registry + # builds. Skipped for maximal (heavier, slower) and for dry-run + # (image not pushed, but buildx can load locally for smoke test). + if: matrix.variant == 'minimal' && github.event_name != 'pull_request' + run: | + # Build a local single-arch copy for smoke testing (does not push). + docker buildx build \ + --platform linux/amd64 \ + --load \ + -t codelens:smoke \ + -f Dockerfile \ + . + echo "--- codelens --command-count ---" + docker run --rm codelens:smoke --command-count + echo "--- codelens --help (first 20 lines) ---" + docker run --rm codelens:smoke --help 2>&1 | head -20 + + - name: Job summary + if: always() + run: | + echo "## Docker Build Summary — ${{ matrix.variant }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| Variant | \`${{ matrix.variant }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Dockerfile | \`${{ matrix.dockerfile }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Platforms | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY + echo "| Image | \`ghcr.io/wolfvin/codelens${{ matrix.image_suffix }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tags | \`${{ steps.meta.outputs.tags }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Pushed | ${{ steps.build.outputs.pushed }} |" >> $GITHUB_STEP_SUMMARY + echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..13746fb6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,102 @@ +# syntax=docker/dockerfile:1.6 +# CodeLens — minimal Docker image (Phase 2, issue #54) +# +# @WHO: Dockerfile +# @WHAT: Minimal container image for CodeLens CLI — python:3.11-slim + core deps + tree-sitter grammars +# @PART: distribution +# @ENTRY: codelens (wrapper -> python3 /opt/codelens/scripts/codelens.py) +# +# Build: +# docker build -t codelens:latest . +# docker build -t codelens:maximal -f Dockerfile.maximal . +# +# Run (acceptance criterion from issue #54): +# docker run --rm -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens scan /workspace +# +# Forward-compat note: Phase 1 (PR #144) will add a `codelens` console script via +# pyproject.toml [project.scripts]. Once merged, replace the wrapper at +# /usr/local/bin/codelens with `pip install .` and drop the manual copy step. +# Until then the code uses sys.path-based imports (see pyproject.toml lines 77-79) +# and must be run via `python3 scripts/codelens.py`. + +ARG PYTHON_VERSION=3.11 + +FROM python:${PYTHON_VERSION}-slim AS base + +# Label the image with OCI-standard metadata so GHCR / tooling can identify it. +LABEL org.opencontainers.image.title="CodeLens" \ + org.opencontainers.image.description="AI-native static code intelligence CLI + MCP server" \ + org.opencontainers.image.source="https://github.com/Wolfvin/CodeLens" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.authors="Wolfvin" + +# Install runtime deps only. tree-sitter + grammar packages ship manylinux/arm64 +# wheels on PyPI, so no build toolchain is required. git is needed by some +# CodeLens commands (ownership/blame, diff-base) and adds ~30MB — acceptable. +# curl is included for HEALTHCHECK debugging and download fallbacks. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user (UID/GID 1000) for least-privilege execution. +# The user owns /workspace (the mount point) so scanned files are readable +# and generated .codelens/ artifacts are writable. +RUN groupadd --gid 1000 codelens \ + && useradd --uid 1000 --gid codelens --create-home --shell /bin/bash codelens + +# Copy the repository into /opt/codelens. .dockerignore excludes tests, +# .git, benchmarks, and other build-context noise to keep layers small. +COPY --chown=codelens:codelens . /opt/codelens/ + +# Install Python dependencies. We install the same set that setup.sh installs +# (tree-sitter core + 6 grammars + watchdog) plus PyYAML for the rules engine. +# --no-cache-dir keeps the layer small. We do NOT `pip install .` here because +# the project has no console-script entry point yet (Phase 1 will add one). +RUN pip install --no-cache-dir \ + "tree-sitter>=0.21.0" \ + tree-sitter-html \ + tree-sitter-css \ + tree-sitter-javascript \ + tree-sitter-typescript \ + tree-sitter-rust \ + tree-sitter-python \ + watchdog \ + "PyYAML>=6.0" + +# Create a wrapper script so the container exposes a `codelens` command. +# This matches the acceptance criterion `docker run ... codelens scan /workspace` +# and is forward-compatible: when Phase 1 merges, `pip install .` will overwrite +# this file with the real console-script entry point. +RUN printf '#!/bin/sh\nexec python3 /opt/codelens/scripts/codelens.py "$@"\n' \ + > /usr/local/bin/codelens \ + && chmod +x /usr/local/bin/codelens + +# Default workspace mount point. Users mount their codebase here: +# docker run -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens scan /workspace +RUN mkdir -p /workspace \ + && chown codelens:codelens /workspace + +USER codelens +WORKDIR /workspace + +# Persist the per-user .codelens/ config dir to a volume so registry/SQLite +# state survives container restarts when the user mounts it. +ENV CODELENS_CONFIG_DIR=/home/codelens/.codelens \ + PYTHONUNBUFFERED=1 \ + PYTHONUTF8=1 \ + PYTHONDONTWRITEBYTECODE=1 + +# HEALTHCHECK: `--command-count` exercises the full command registry import +# path (loads every command module + tree-sitter grammars), so it verifies +# the runtime is functional — a deeper check than a static `--version` print. +# Interval is 30s with 30s timeout, 3 retries before unhealthy. +HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ + CMD codelens --command-count > /dev/null 2>&1 || exit 1 + +# ENTRYPOINT is the codelens wrapper so `docker run image ` works the +# same way `codelens ` works on the host. No CMD — the user must supply +# a command (scan, query, dead-code, etc.) plus the workspace path. +ENTRYPOINT ["codelens"] diff --git a/Dockerfile.maximal b/Dockerfile.maximal new file mode 100644 index 00000000..1096e209 --- /dev/null +++ b/Dockerfile.maximal @@ -0,0 +1,95 @@ +# syntax=docker/dockerfile:1.6 +# CodeLens — maximal Docker image (Phase 2, issue #54) +# +# @WHO: Dockerfile.maximal +# @WHAT: Maximal container image — core + all Python extras + pre-installed LSP servers (Python, JS/TS) +# @PART: distribution +# @ENTRY: codelens (wrapper -> python3 /opt/codelens/scripts/codelens.py) +# +# Build: +# docker build -t codelens:maximal -f Dockerfile.maximal . +# +# Run: +# docker run --rm -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens:maximal-latest scan /workspace +# +# Differences vs minimal Dockerfile: +# - Installs all Python extras (grammars + watch + rules + lsp + dev) +# - Pre-installs LSP servers for Python (python-lsp-server, ruff-lsp) and +# JS/TS (typescript-language-server, pyright) so `--deep` works out of the box +# - Includes Node.js LTS (required by the npm-based LSP servers above) +# +# Note: full LSP coverage (Rust analyzer, Go gopls, etc.) is pending the native +# LSP server issue referenced in #54. This image covers the two most common +# languages (Python, JS/TS) and is extensible. + +ARG PYTHON_VERSION=3.11 + +FROM python:${PYTHON_VERSION}-slim AS base + +LABEL org.opencontainers.image.title="CodeLens (maximal)" \ + org.opencontainers.image.description="CodeLens CLI + MCP server with all extras and pre-installed LSP servers" \ + org.opencontainers.image.source="https://github.com/Wolfvin/CodeLens" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.authors="Wolfvin" + +# Runtime deps: git (ownership/diff commands), ca-certificates, curl. +# Node.js + npm: required by typescript-language-server and pyright (JS/TS LSP). +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --gid 1000 codelens \ + && useradd --uid 1000 --gid codelens --create-home --shell /bin/bash codelens + +COPY --chown=codelens:codelens . /opt/codelens/ + +# Install ALL Python extras matching pyproject.toml [project.optional-dependencies] +# all = [grammars, watch, rules, lsp, dev]. Plus pip-based LSP servers for Python. +RUN pip install --no-cache-dir \ + "tree-sitter>=0.21.0" \ + tree-sitter-html \ + tree-sitter-css \ + tree-sitter-javascript \ + tree-sitter-typescript \ + tree-sitter-rust \ + tree-sitter-python \ + watchdog \ + "PyYAML>=6.0" \ + "pygls>=2.0" \ + "lsprotocol>=2024.0" \ + "pytest>=7.0" \ + "pytest-cov" \ + "python-lsp-server[all]" \ + "ruff-lsp" + +# Install JS/TS LSP servers globally via npm. +RUN npm install -g --no-save \ + pyright \ + typescript-language-server \ + && npm cache clean --force 2>/dev/null || true + +# Wrapper script (same as minimal — see Dockerfile for forward-compat notes). +RUN printf '#!/bin/sh\nexec python3 /opt/codelens/scripts/codelens.py "$@"\n' \ + > /usr/local/bin/codelens \ + && chmod +x /usr/local/bin/codelens + +RUN mkdir -p /workspace \ + && chown codelens:codelens /workspace + +USER codelens +WORKDIR /workspace + +ENV CODELENS_CONFIG_DIR=/home/codelens/.codelens \ + PYTHONUNBUFFERED=1 \ + PYTHONUTF8=1 \ + PYTHONDONTWRITEBYTECODE=1 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=15s --retries=3 \ + CMD codelens --command-count > /dev/null 2>&1 || exit 1 + +ENTRYPOINT ["codelens"] diff --git a/docs/installation/docker.md b/docs/installation/docker.md new file mode 100644 index 00000000..534ac7eb --- /dev/null +++ b/docs/installation/docker.md @@ -0,0 +1,240 @@ +# Docker Installation + +> **Phase 2 of issue #54** — container image distribution for CodeLens. +> Images are published to GitHub Container Registry (GHCR) on every push to +> `main` and on version tags. This page covers install, usage, and the +> differences between the minimal and maximal image variants. + +--- + +## Quick start + +Pull the minimal image and scan the current directory: + +```bash +docker run --rm -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens scan /workspace +``` + +That is the acceptance criterion from issue #54: the container accepts a +CodeLens command (`scan`) plus a workspace path (`/workspace`) and runs +against the mounted host directory. + +--- + +## Image variants + +| Image | Tag | Size (approx.) | Best for | +|---|---|---|---| +| `ghcr.io/wolfvin/codelens` | `:latest`, `:v8.x.y`, `:sha-` | ~180 MB | CI pipelines, automated scans, minimal footprint | +| `ghcr.io/wolfvin/codelens:maximal-latest` | `:maximal-latest`, `:maximal-v8.x.y` | ~450 MB | Local development with `--deep` LSP-enhanced analysis | + +### Minimal image (`:latest`) + +Built from [`Dockerfile`](https://github.com/Wolfvin/CodeLens/blob/main/Dockerfile) +on `python:3.11-slim`. Includes: + +- Python 3.11 runtime +- `tree-sitter` core + 6 grammar packages (HTML, CSS, JS, TS, Rust, Python) +- `watchdog` (file watcher) and `PyYAML` (rules engine) +- `git` (required by `ownership`, `diff-base`, and related commands) +- Runs as non-root user `codelens` (UID 1000) + +The minimal image is the right default for CI: it is small, fast to pull, and +covers every CodeLens command that does not require an LSP server. + +### Maximal image (`:maximal-latest`) + +Built from [`Dockerfile.maximal`](https://github.com/Wolfvin/CodeLens/blob/main/Dockerfile.maximal). +In addition to everything in the minimal image, it pre-installs: + +- All Python extras from `pyproject.toml` (`grammars`, `watch`, `rules`, `lsp`, `dev`) +- **LSP servers**: `python-lsp-server`, `ruff-lsp` (Python), `pyright` and + `typescript-language-server` (JS/TS) — so `codelens --deep` works + out of the box without installing language servers on the host +- Node.js LTS (required by the npm-based LSP servers above) + +Full LSP coverage for Rust, Go, and other languages is pending the native +LSP server issue referenced in #54. The maximal image is designed to be +extended: add more `npm install -g` / `pip install` lines as new language +servers land. + +--- + +## Usage patterns + +### One-off scan (CI style) + +```bash +docker run --rm \ + -v "$(pwd):/workspace" \ + ghcr.io/wolfvin/codelens scan /workspace +``` + +The `--rm` flag removes the container after exit. The `$(pwd):/workspace` +bind-mount makes the host's current directory visible inside the container at +`/workspace`, which is the default `WORKDIR`. + +### Persisting the `.codelens/` registry between runs + +By default the per-user config dir lives at `/home/codelens/.codelens` inside +the container and is lost when the container is removed. To persist the +SQLite registry and scan cache across runs, mount a named volume: + +```bash +docker volume create codelens-data + +docker run --rm \ + -v "$(pwd):/workspace" \ + -v codelens-data:/home/codelens/.codelens \ + ghcr.io/wolfvin/codelens scan /workspace +``` + +Subsequent `query`, `dead-code`, or `trace` commands can reuse the cached +registry without re-scanning: + +```bash +docker run --rm \ + -v "$(pwd):/workspace" \ + -v codelens-data:/home/codelens/.codelens \ + ghcr.io/wolfvin/codelens query 'btn-primary' /workspace +``` + +### Shell alias + +Add to `~/.bashrc` or `~/.zshrc`: + +```bash +alias codelens='docker run --rm -v "$(pwd):/workspace" -v codelens-data:/home/codelens/.codelens ghcr.io/wolfvin/codelens' +``` + +Then use `codelens scan .`, `codelens query 'fn' .`, etc. exactly as if it +were a locally installed binary. + +### Deep analysis with LSP (maximal image only) + +```bash +docker run --rm \ + -v "$(pwd):/workspace" \ + -v codelens-data:/home/codelens/.codelens \ + ghcr.io/wolfvin/codelens:maximal-latest complexity /workspace --deep +``` + +The `--deep` flag enables LSP-enhanced type inference and cross-file resolution. +It requires the relevant language server to be installed — which the maximal +image handles for Python and JS/TS. + +### Running the MCP server + +CodeLens can run as an MCP (Model Context Protocol) server over stdio. To +expose it to an MCP client running on the host: + +```bash +docker run --i \ + -v "$(pwd):/workspace" \ + ghcr.io/wolfvin/codelens mcp-server /workspace +``` + +Note: MCP-over-stdio requires an interactive (`-i`) container. Network modes +and stdio plumbing depend on the host MCP client; consult the client's docs. + +--- + +## Health check + +Both images ship with a `HEALTHCHECK` that runs `codelens --command-count`. +This exercises the full command-registry import path (every command module + +tree-sitter grammars load), so a healthy status confirms the runtime is +functional — not just that the binary exists. + +Inspect health: + +```bash +docker inspect --format='{{.State.Health.Status}}' +``` + +--- + +## Multi-arch support + +Both image variants are built for `linux/amd64` and `linux/arm64` via +`docker buildx` + QEMU. Docker automatically pulls the matching architecture +for your host. To explicitly pull a different architecture: + +```bash +docker pull --platform linux/arm64 ghcr.io/wolfvin/codelens:latest +``` + +--- + +## Building locally + +To build the image yourself (for development or testing): + +```bash +# Minimal +docker build -t codelens:local . + +# Maximal +docker build -t codelens:local-maximal -f Dockerfile.maximal . + +# Test +docker run --rm -v "$(pwd):/workspace" codelens:local --command-count +``` + +The build uses BuildKit (`# syntax=docker/dockerfile:1.6` directive at the top +of each Dockerfile), so set `DOCKER_BUILDKIT=1` if your Docker daemon is older +than 23.0. + +--- + +## Tagging strategy + +| Event | Tags produced | +|---|---| +| Push to `main` (Dockerfile changes) | `:latest`, `:sha-` | +| Tag push `v8.2.1` | `:v8.2.1`, `:v8.2`, `:latest` | +| Manual `workflow_dispatch` | `:latest`, `:sha-` (or build-only if dry-run) | + +The same scheme applies to the maximal image with a `-maximal` suffix on the +image name and `maximal-` prefix on the `latest` tag (i.e. `:maximal-latest`, +`:maximal-v8.2.1`). + +--- + +## Troubleshooting + +### `permission denied` on mounted files + +The container runs as UID 1000 (`codelens`). If your host files are owned by a +different UID, scans may fail to read them. Fix by either: + +- `chmod -R a+r ` on the host, or +- Running with `--user $(id -u):$(id -g)` to match the host user (the + `.codelens/` volume must then be writable by that UID). + +### `codelens: command not found` inside the container + +The entrypoint is the `codelens` wrapper at `/usr/local/bin/codelens`. If you +override `--entrypoint`, you must invoke `python3 /opt/codelens/scripts/codelens.py` +directly. + +### LSP server not detected (minimal image) + +The minimal image does not bundle LSP servers. Use the maximal image, or +install the language server on your host and drop `--deep`. + +--- + +## Related + +- Issue #54 — Distribution & packaging overhaul (this phase) +- PR #144 — Phase 1 (pip-installable package; not yet merged). Once merged, + the Dockerfile entry point switches from the wrapper script to the + `codelens` console script, and `pip install .` replaces the manual + dependency install step. +- [`Dockerfile`](https://github.com/Wolfvin/CodeLens/blob/main/Dockerfile) — + minimal image definition +- [`Dockerfile.maximal`](https://github.com/Wolfvin/CodeLens/blob/main/Dockerfile.maximal) — + maximal image definition +- [`.github/workflows/docker-publish.yml`](https://github.com/Wolfvin/CodeLens/blob/main/.github/workflows/docker-publish.yml) — + CI build & publish pipeline