Developer Guide: Building & Testing

Developer guide for imgcompress: build and test locally with the Makefile, run unit, integration, E2E, and feature-flag tests, set up the VS Code Dev Container, and simulate the full CI pipeline before pushing your changes.

DevelopmentGuide

This guide covers how to run tests and builds locally so your contributions pass the same checks as CI. The Makefile is the canonical entry point for every common task. It also shows how to simulate the GitHub Actions pipeline exactly before you push.


1. VS Code Dev Container#

The easiest way to get a consistent dev environment is the VS Code Dev Container. It gives you a pre-configured Docker environment with all dependencies and extensions already installed.

How to use#

  1. Open the project in VS Code.
  2. Install the Dev Containers extension.
  3. Click "Reopen in Container" when prompted (or run Dev Containers: Reopen in Container from the command palette).

What you get#

  • Pre-installed tools: Python 3.10+, Node.js, pnpm, and Docker CLI
  • Extensions: Python, Docker, ESLint, Playwright, and more
  • Port forwarding: port 3001 (app) and 5000 (backend) are forwarded automatically
  • Docker-in-Docker: you can build and run containers inside the dev container, which is required for integration and E2E tests

2. Make Targets (canonical entry point)#

All everyday dev tasks run through the Makefile at the repo root. Helper scripts live under ./scripts/ and the Makefile wires them up so you don't have to remember paths or flags.

⚠️Docker Hub login required (starting v0.7.0)

Starting with the v0.7.0 release, imgcompress builds on top of Docker Hardened Images (DHI) for a reduced OS attack surface and strict non-root runtime. DHI base images are pulled from a private registry on Docker Hub, so before running any make target that builds an image (build, build-local, scan, deploy-local, deploy-local-dev-mode, simci) you need to be logged in:

bash
docker login

Use a Docker Hub account that has access to the hardened image repository. If you skip this step the build will fail with an unauthorized pull error on the base image.

TargetWhat it does
make lintRuns the Python linter (auto-runs before every other target).
make unitRuns backend unit tests.
make integrationRuns backend integration tests (needs Docker).
make e2eRuns the Playwright end-to-end suite.
make feature-flagsRuns the feature-flag matrix E2E suite.
make buildMulti-arch (linux/amd64,linux/arm64) image build with SBOM and provenance attestations. Used for release validation.
make build-localSingle-platform build loaded into your local Docker daemon. Used by scan and local testing.
make scan (alias: make trivy)Builds locally and runs a Trivy image scan.
make deploy-localRecommended for local development. Builds and runs the full production-style stack on your machine via Docker.
make deploy-local-dev-modeSame as deploy-local, intended for development mode runs.
make simciSimulates the full GitHub Actions CI pipeline locally: builds the devcontainer, runs lint + tests inside it, builds the app image, and runs E2E. If this passes, CI passes.

Common overrides#

The Makefile exposes a few variables you can override on the command line:

bash
make build IMAGE=imgcompress TAG=my-test
make scan REGISTRY=docker.io/karimz1
make build-local LOCAL_PLATFORM=linux/arm64

3. Local Development#

For working on the app end-to-end (frontend + backend, exactly as users see it), use:

bash
make deploy-local

This builds the image with build-local and runs it through ./scripts/runLocalDockerBuildTester.sh, so what you see locally matches the published image. You can pass the same environment overrides the script accepts:

bash
PORT_HOST=8080 \
 DISABLE_LOGO=false \
 DISABLE_STORAGE_MANAGEMENT=true \
 make deploy-local

Once running, open http://localhost:8080 (or whatever PORT_HOST you set, default 80).

💡Why this path is preferred

The Docker path bundles the built frontend assets into the image and runs the production server, so route behaviour, static-asset serving, and runtime config all behave the same as in production. The raw split-process scripts below skip that bundling step and require everything to be set up just right.

Advanced: raw split-process scripts#

If you want hot-reload on the frontend while iterating, you can run the backend and frontend as separate processes. This path needs your local Python venv and Node toolchain to be ready, and is more brittle than make deploy-local.

Prerequisites:

  • Python 3.10+ with the project installed in a venv (pip install -e ".[dev]")
  • Node.js & pnpm for the frontend
  • Docker for integration tests

Run each in its own terminal from the repo root:

  1. Backend: ./scripts/runStartLocalBackend.sh
  2. Frontend: ./scripts/runStartLocalFrontend.sh
  3. E2E (optional): ./scripts/run-e2e.sh
⚠️Known limitation

The split-process flow doesn't bundle the frontend into the backend the way the production image does, so some scenarios (static asset routing, runtime config injection) won't match what users see. If you hit unexpected behaviour, switch to make deploy-local to confirm whether the issue reproduces against the production-style stack.

Targeted test runs#

You can run any test suite directly through Make without standing up the app:

bash
make unit            # backend unit tests
make integration     # backend integration tests
make e2e             # Playwright E2E
make feature-flags   # feature-flag matrix E2E

See the Playwright E2E Testing Guide for debugging tips, targeted runs, and reports.


4. CI Simulation#

bash
make simci

This is the one command that mirrors the full GitHub Actions pipeline end-to-end: builds the devcontainer, runs lint and tests inside it, builds the app image, and runs E2E against it. If this passes locally, CI will pass.

Manual fallback (debugging only)#

If you need to debug a single CI step in isolation (for example, to repro a failing unit-test run inside the devcontainer image), you can invoke the underlying Docker commands directly. Prefer make simci for normal use.

Show manual Docker commands

Build the Dev Container

bash
docker buildx build -t devcontainer:local-test .devcontainer/

Backend Unit Tests

bash
docker run --rm \
  --entrypoint /bin/sh \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$(pwd):/app/" \
  -e IS_RUNNING_IN_GITHUB_ACTIONS=true \
  --name devcontainer \
  devcontainer:local-test /app/scripts/runUnitTests.sh

Backend Integration Tests

bash
docker run --rm \
  --entrypoint /bin/sh \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$(pwd):/app/" \
  -e IS_RUNNING_IN_GITHUB_ACTIONS=true \
  --name devcontainer \
  devcontainer:local-test /app/scripts/runIntegrationTests.sh

End-to-End (E2E) Tests

  1. Build the ImgCompress image:

    bash
    docker buildx build -t karimz1/imgcompress:local-test .
  2. Start the app:

    bash
    docker run --rm -d \
      --network host \
      --name app \
      karimz1/imgcompress:local-test web
  3. Run E2E:

    bash
    docker run --rm \
      --entrypoint /bin/sh \
      --network host \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v "$(pwd):/app/" \
      -e IS_RUNNING_IN_GITHUB_ACTIONS=true \
      -e PLAYWRIGHT_BASE_URL=http://localhost:5000 \
      --name devcontainer_e2e \
      devcontainer:local-test -c "/app/scripts/run-e2e.sh"

Common issue: broken pnpm lockfile after Dependabot merge#

Dependabot PRs that touch dependencies can leave a corrupted pnpm-lock.yaml when multiple lockfile changes are merged without a rebase.

Symptoms#

⚠️Warning

You may see an error like:

bash
broken lockfile at /workspaces/imgcompress/frontend:
The lockfile at "/workspaces/imgcompress/frontend/pnpm-lock.yaml" is broken:
duplicated mapping key (1307:3)

This means the lockfile has invalid YAML, usually from duplicated dependency entries.

Root cause#

Multiple Dependabot lockfile edits merged together without rebasing. pnpm validates YAML structure and fails immediately on duplicates.

Fix#

The fastest path is to ask imgcompress-chan, the repo's helper bot, to handle it for you. Drop a comment on the affected PR with /chan-fix somewhere in it. She loves a polite ask:

Hey chan, can you /chan-fix it please? 💛

She'll refresh the branch from main, regenerate frontend/pnpm-lock.yaml with the pinned pnpm version, and push the repair commit so CI re-runs. Full details, allowed users, and the manual fallback live on her page: imgcompress-chan.

Prevent it next time#

Rebase Dependabot PRs that modify pnpm-lock.yaml before merging, and let CI run pnpm install on the branch to catch issues early.

💡Need help?

Stuck on a step? Contact Support