From 1601a4aea6c2105c4fab25ee778190c95ff01a5b Mon Sep 17 00:00:00 2001 From: Gil Desmarais Date: Sat, 31 Oct 2026 11:20:21 +0100 Subject: [PATCH 1/5] build: single root Dockerfile as toolchain SSOT; run CI in the compose dev image Co-Authored-By: Claude Sonnet 5.5 --- .devcontainer/Dockerfile | 43 ----------- .devcontainer/README.md | 49 ------------- .devcontainer/devcontainer.json | 44 ------------ .devcontainer/docker-compose.yml | 22 ------ .dockerignore | 5 -- .github/actions/dev-image/action.yml | 22 ++++++ .github/dependabot.yml | 4 ++ .github/workflows/bump-version.yml | 33 +++------ .github/workflows/ci.yml | 104 +++++++-------------------- .rubocop.yml | 1 - .tool-versions | 2 - .yardopts | 1 - AGENTS.md | 6 +- Dockerfile | 83 ++++++++++++++++++--- README.md | 11 +-- bin/compose-contract-verify | 47 ++++++++++++ bin/dev-ruby | 7 +- docker-compose.dev.yml | 25 +++++++ docs/README.md | 20 ++++-- frontend/playwright.config.ts | 6 +- 20 files changed, 232 insertions(+), 303 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/README.md delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/docker-compose.yml create mode 100644 .github/actions/dev-image/action.yml delete mode 100644 .tool-versions create mode 100644 docker-compose.dev.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 28726b28e..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM ruby:4.0.9-alpine3.24 - -SHELL ["/bin/sh", "-o", "pipefail", "-c"] - -RUN apk add --no-cache \ - bash \ - build-base \ - chromium \ - curl \ - git \ - harfbuzz \ - libxml2-dev \ - libxslt-dev \ - nodejs \ - nss \ - npm \ - openssl-dev \ - python3 \ - ttf-freefont \ - yaml-dev \ - tzdata - -RUN if ! command -v corepack >/dev/null 2>&1; then npm install -g corepack; fi \ - && corepack enable \ - && corepack prepare "pnpm@latest" --activate - -ARG USER=vscode -ARG UID=1000 -ARG GID=1000 - -RUN addgroup -g "$GID" "$USER" \ - && adduser -D -G "$USER" -u "$UID" "$USER" - -WORKDIR /workspace -RUN chown -R "$USER":"$USER" /workspace - -ENV BUNDLE_PATH=/usr/local/bundle - -USER "$USER" - -EXPOSE 4000 - -CMD ["sleep", "infinity"] diff --git a/.devcontainer/README.md b/.devcontainer/README.md deleted file mode 100644 index 60e5d7a46..000000000 --- a/.devcontainer/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Dev Container Workflow - -This repository ships a single Dev Container for development. Open the project in VS Code (Dev -Containers extension) or GitHub Codespaces and use that environment for all work. - -## What gets created - -The devcontainer starts one service named `app` and exposes: - -- **Port 4000:** Ruby app -- **Port 4001:** Vite dev server - -The repo is mounted at `/workspace`. Bundler gems are cached in a Docker volume to speed up -future launches. - -## Bootstrap - -On first open, the Dev Container runs: - -``` -make setup -``` - -This installs Ruby and frontend dependencies inside the container. - -If setup fails due to missing network access (e.g., GitHub DNS), rerun `make setup` once -network access is available. - -## Common commands (run inside the container) - -``` -make dev # Ruby + Vite -make dev-ruby # Ruby only -make dev-frontend # frontend only -make test # Ruby + frontend tests -make ready # RuboCop + RSpec (pre-commit gate) -``` - -## Lint and tests - -``` -bundle exec rubocop -F -bundle exec rspec -``` - -## Notes - -- All commands are expected to run inside the Dev Container. -- The default service command is `sleep infinity`; use the Make targets above to start servers. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 1be83e9c7..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "html2rss-web", - "dockerComposeFile": "docker-compose.yml", - "service": "app", - "workspaceFolder": "/workspace", - "shutdownAction": "stopCompose", - "customizations": { - "vscode": { - "extensions": [ - "rebornix.ruby", - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" - ], - "settings": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], - "prettier.configPath": "./frontend/prettier.config.js", - "ruby.format": "rubocop", - "ruby.lint": { "rubocop": true }, - "editor.tabSize": 2, - "editor.insertSpaces": true, - "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true - } - } - }, - "forwardPorts": [4000, 4001], - "portsAttributes": { - "4000": { - "label": "Ruby App", - "onAutoForward": "notify" - }, - "4001": { - "label": "Vite Dev Server", - "onAutoForward": "silent" - } - }, - "postCreateCommand": "bash -lc \"make setup || (echo 'make setup failed; rerun once network access is available.' && exit 0)\"", - "remoteUser": "vscode" -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 2e23f37f4..000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - app: - build: - context: .. - dockerfile: .devcontainer/Dockerfile - volumes: - - ../:/workspace:cached - - bundle-cache:/usr/local/bundle - ports: - - "4000:4000" - - "4001:4001" - environment: - - RACK_ENV=development - - BUNDLE_PATH=/usr/local/bundle - - PORT=4000 - - BOTASAURUS_SCRAPER_URL=${BOTASAURUS_SCRAPER_URL:-http://host.docker.internal:4010} - command: sleep infinity - user: vscode - working_dir: /workspace - -volumes: - bundle-cache: diff --git a/.dockerignore b/.dockerignore index 46755b10a..16dc7d159 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,18 +6,14 @@ .mergify.yml .rspec .rubocop.yml -.ruby-version .ruby-lsp/ .yardoc/ -app.json bin/ !bin/docker-healthcheck coverage/ docs/ Dockerfile -Procfile Rakefile -renovate.json spec/ tmp/ work/ @@ -25,4 +21,3 @@ frontend/dist/ frontend/node_modules/ frontend/test-results/ frontend/.astro/ - diff --git a/.github/actions/dev-image/action.yml b/.github/actions/dev-image/action.yml new file mode 100644 index 000000000..d9de526d4 --- /dev/null +++ b/.github/actions/dev-image/action.yml @@ -0,0 +1,22 @@ +name: dev image +description: Build the `dev` stage of the root Dockerfile (layer-cached) as html2rss/web:dev for docker-compose.dev.yml + +runs: + using: composite + steps: + - name: Match container user to runner user + shell: bash + run: | + echo "DEV_UID=$(id -u)" >> "$GITHUB_ENV" + echo "DEV_GID=$(id -g)" >> "$GITHUB_ENV" + + - uses: docker/setup-buildx-action@v4 + + - uses: docker/bake-action@v7 + with: + files: docker-compose.dev.yml + targets: dev + load: true + set: | + dev.cache-from=type=gha,scope=dev + dev.cache-to=type=gha,mode=max,scope=dev diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 986e9027d..0db28a75d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,6 +21,10 @@ updates: interval: daily time: "09:00" open-pull-requests-limit: 10 + groups: + docker: + patterns: + - "*" - package-ecosystem: github-actions directory: "/" diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index dfe599fa7..f83ec41f1 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -12,6 +12,9 @@ permissions: contents: write pull-requests: write +env: + DEV: docker compose -f docker-compose.dev.yml run --rm -T dev + jobs: bump_version: runs-on: ubuntu-latest @@ -42,38 +45,20 @@ jobs: fi echo "TARGET_VERSION=${version}" >> "$GITHUB_ENV" - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Setup pnpm - uses: pnpm/action-setup@v6.1.0 - with: - cache: true - cache_dependency_path: frontend/pnpm-lock.yaml - package_json_file: frontend/package.json - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".tool-versions" - - - name: Install frontend dependencies - run: pnpm install --frozen-lockfile - working-directory: frontend + - uses: ./.github/actions/dev-image - name: Perform Version Bump id: perform_bump run: | - bundle exec ruby bin/bump-version "$TARGET_VERSION" - NEW_VERSION=$(ruby -I. -e "require 'config/version'; puts Html2rss::Web::VERSION") + $DEV bundle exec ruby bin/bump-version "$TARGET_VERSION" + NEW_VERSION=$($DEV ruby -I. -e "require 'config/version'; puts Html2rss::Web::VERSION") echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" - name: Regenerate OpenAPI spec and client run: | - make openapi - make openapi-client + $DEV make frontend-setup + $DEV make openapi + $DEV make openapi-client - name: Create Pull Request uses: peter-evans/create-pull-request@v8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4257a73a1..bf2d9106e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,9 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +env: + DEV: docker compose -f docker-compose.dev.yml run --rm -T dev + jobs: hadolint: runs-on: ubuntu-latest @@ -25,8 +28,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Make compose contract verifier executable - run: chmod +x bin/compose-contract-verify - name: Verify compose contracts run: bin/compose-contract-verify @@ -34,19 +35,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true + - uses: ./.github/actions/dev-image - name: Run RuboCop - run: bundle exec rubocop -F + run: $DEV bundle exec rubocop -F - name: Verify Yard documentation - run: bundle exec yard doc --fail-on-warning --no-output + run: $DEV bundle exec yard doc --fail-on-warning --no-output - name: Run RSpec - run: bundle exec rspec + run: $DEV bundle exec rspec - name: Upload coverage report uses: actions/upload-artifact@v7 @@ -60,99 +58,49 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Setup pnpm - uses: pnpm/action-setup@v6.1.0 - with: - cache: true - cache_dependency_path: frontend/pnpm-lock.yaml - package_json_file: frontend/package.json - - - name: Setup Node.js for OpenAPI lint tooling - uses: actions/setup-node@v7 - with: - node-version-file: ".tool-versions" + - uses: ./.github/actions/dev-image - name: Install frontend dependencies for OpenAPI client verification - run: pnpm install --frozen-lockfile - working-directory: frontend + run: $DEV make frontend-setup - name: Verify generated OpenAPI spec and client are up to date - run: make openapi-verify + run: $DEV make openapi-verify - name: Lint OpenAPI contract - run: make openapi-lint + run: $DEV make openapi-lint frontend-audit: runs-on: ubuntu-latest - defaults: - run: - working-directory: frontend steps: - uses: actions/checkout@v7 - - - name: Setup pnpm - uses: pnpm/action-setup@v6.1.0 - with: - cache: true - cache_dependency_path: frontend/pnpm-lock.yaml - package_json_file: frontend/package.json - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".tool-versions" - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/dev-image - name: Audit dependencies - run: pnpm audit --audit-level=moderate + run: $DEV pnpm --dir frontend audit --audit-level=moderate frontend: runs-on: ubuntu-latest - defaults: - run: - working-directory: frontend steps: - uses: actions/checkout@v7 - - - name: Setup pnpm - uses: pnpm/action-setup@v6.1.0 - with: - cache: true - cache_dependency_path: frontend/pnpm-lock.yaml - package_json_file: frontend/package.json - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".tool-versions" + - uses: ./.github/actions/dev-image - name: Install dependencies - run: pnpm install --frozen-lockfile + run: $DEV make frontend-setup - name: Typecheck frontend - run: pnpm run typecheck + run: $DEV pnpm --dir frontend run typecheck - name: Lint CSS with Stylelint - run: pnpm exec stylelint "**/*.css" + run: $DEV pnpm --dir frontend exec stylelint "**/*.css" - name: Check formatting - run: pnpm run format:check + run: $DEV pnpm --dir frontend run format:check - name: Run frontend tests - run: pnpm run test:ci - - - name: Install Playwright Chromium - run: pnpm exec playwright install --with-deps chromium + run: $DEV pnpm --dir frontend run test:ci - name: Run frontend smoke test - run: pnpm run test:e2e + run: $DEV pnpm --dir frontend run test:e2e docker-build-smoke-image: needs: @@ -170,8 +118,8 @@ jobs: context: . load: true tags: html2rss/web - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=runtime + cache-to: type=gha,mode=max,scope=runtime docker-test: needs: @@ -184,10 +132,6 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - uses: docker/setup-buildx-action@v4 - name: Load Docker smoke image from cache @@ -196,10 +140,10 @@ jobs: context: . load: true tags: html2rss/web - cache-from: type=gha + cache-from: type=gha,scope=runtime - name: Run Docker smoke test env: DOCKER_SMOKE_SKIP_BUILD: "true" SMOKE_AUTO_SOURCE_ENABLED: ${{ matrix.smoke_auto_source_enabled }} - run: bundle exec rake + run: rake diff --git a/.rubocop.yml b/.rubocop.yml index 1f326cc6f..9618e1015 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,7 +11,6 @@ AllCops: Exclude: - '**/*.yml' - '**/*.yaml' - - '**/.tool-versions' - 'coverage/**/*' - 'frontend/node_modules/**/*' - 'frontend/test-results/**/*' diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index df7c4c12a..000000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -ruby 4.0.9 -nodejs 22.22.0 diff --git a/.yardopts b/.yardopts index f3c98a332..359a47531 100644 --- a/.yardopts +++ b/.yardopts @@ -6,5 +6,4 @@ --exclude public/ --exclude config/ --exclude bin/ ---exclude .devcontainer/ --exclude .github/ diff --git a/AGENTS.md b/AGENTS.md index 2f0ef81fe..b162b7ee1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ Canonical references: ## Execution & Coding Contracts - **Posture:** Time matters. Reduce LOC when possible. No legacy code, no backward compatibility, no shims. -- **Dev Container only:** Run all commands inside the Dev Container via `make` or `bundle exec`. Never execute on the host. -- **Quality gate:** Never push or open a PR without a green Dev Container gate (`make ready` minimum, which includes `pnpm run format:check` via `lint-js`; `make ci-ready` before push/PR for frontend or API/OpenAPI contract changes). Never mark validation done or open a PR with "gate skipped / unavailable" — start the Dev Container and run the gate, or stop and report the blocker plus next command. +- **Docker-only:** Run all commands inside the `dev` image (the `dev` stage of the root `Dockerfile`) via `docker compose -f docker-compose.dev.yml run --rm …`. For the app: `docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev`. Never execute `make`/`bundle`/`pnpm` gates on the host. +- **Quality gate:** Never push or open a PR without a green `dev`-image gate (`make ready` minimum, which includes `pnpm run format:check` via `lint-js`; `make ci-ready` before push/PR for frontend or API/OpenAPI contract changes). Never mark validation done or open a PR with "gate skipped / unavailable" — start the `dev` service and run the gate, or stop and report the blocker plus next command. - **Frontend verification:** With `make dev` running, verify touched paths in `chrome-devtools` MCP at `http://127.0.0.1:4001/`. Confirm affected states (guest/member/result) share the same frame, keep one primary task and one canonical control per outcome, and preserve autofocus/return-focus on load and transitions ([docs/design-system.md](docs/design-system.md)). - **Security & YARD:** No leaking secrets or insecure patterns ([docs/README.md#security--safety-rules](docs/README.md#security--safety-rules)). Every public Ruby method in `app/` requires a YARD docstring with typed `@param` and `@return` ([docs/README.md#architectural-constraints](docs/README.md#architectural-constraints)). - **Response format:** `Changes → Commands → Results → Next steps`, ending with a one-line summary. @@ -22,7 +22,7 @@ When a task that should ship merges to `main`, ask whether to bump the app versi 1. Wait until `html2rss/html2rss`'s `release gem` workflow for the tag is `success` (a merged version PR is not on RubyGems yet; `bundle update html2rss` keeps the old pin). 2. `git fetch origin main` and branch from `origin/main`. Never continue a feature branch or commit the lockfile onto `main`. -3. In the Dev Container, run `bundle update html2rss`, then `make ready`. Do not run a full `bundle update` unless asked. Keep the Gemfile constraint `~> 0.30` unless resolution fails. +3. In the `dev` image, run `bundle update html2rss`, then `make ready`. Do not run a full `bundle update` unless asked. Keep the Gemfile constraint `~> 0.30` unless resolution fails. 4. Open a lockfile PR and watch required checks (`frontend`, `ruby`, `docker-test (false)`, `docker-test (true)`) with `gh pr checks --watch`. 5. Mergify queues only Dependabot. When asked to merge and checks are green, run `gh pr merge --squash --delete-branch` (merge commits disabled). 6. Stop and ask whether to bump the app version. Dispatch the version workflow only after the gem PR is on `main`. diff --git a/Dockerfile b/Dockerfile index 53b7e5ec1..0fd2cdabd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,12 @@ -ARG RUBY_BASE_IMAGE=ruby:4.0.9-alpine3.24@sha256:50d04650231808a465271b4eedcb78634c2306afb0157b10e9d628373bd5d002 -ARG NODE_BASE_IMAGE=node:22-alpine@sha256:8094c002d08262dba12645a3b4a15cd6cd627d30bc782f53229a2ec13ee22a00 +# Single source of truth for toolchain versions: Dependabot bumps these two digest-pinned bases. +FROM node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS node -# Stage 1: Frontend Build (arch-independent assets; native builder, not QEMU) -FROM --platform=$BUILDPLATFORM ${NODE_BASE_IMAGE} AS frontend-builder +FROM ruby:4.0.9-alpine3.24@sha256:50d04650231808a465271b4eedcb78634c2306afb0157b10e9d628373bd5d002 AS ruby + +LABEL maintainer="Gil Desmarais " + +# Frontend Build (arch-independent assets; native builder, not QEMU) +FROM --platform=$BUILDPLATFORM node AS frontend-builder WORKDIR /app/frontend COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./ @@ -10,10 +14,69 @@ RUN corepack enable && pnpm install --frozen-lockfile COPY frontend/ ./ RUN pnpm run build -# Stage 2: Ruby Build -FROM ${RUBY_BASE_IMAGE} AS builder +# Local + CI toolchain (docker-compose.dev.yml) +FROM ruby AS dev -LABEL maintainer="Gil Desmarais " +SHELL ["/bin/ash", "-o", "pipefail", "-c"] + +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + bash \ + build-base \ + chromium \ + curl \ + git \ + harfbuzz \ + libxml2-dev \ + libxslt-dev \ + nss \ + openssl-dev \ + python3 \ + ttf-freefont \ + yaml-dev \ + tzdata + +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +# COPY materializes binaries as files; restore upstream symlinks so dist/lib resolves. +RUN ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack \ + && ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ + && ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx + +ENV BUNDLE_PATH=/usr/local/bundle \ + COREPACK_HOME=/usr/local/share/corepack + +COPY frontend/package.json /tmp/package.json +RUN corepack enable \ + && corepack prepare "$(node -p "require('/tmp/package.json').packageManager")" --activate \ + && rm /tmp/package.json + +ARG USER=dev +ARG UID=1000 +ARG GID=1000 + +RUN addgroup -g "$GID" "$USER" \ + && adduser -D -G "$USER" -u "$UID" "$USER" \ + && mkdir -p /workspace /usr/local/bundle /usr/local/share/corepack + +WORKDIR /workspace + +COPY Gemfile Gemfile.lock ./ + +# hadolint ignore=SC2046 +RUN gem install bundler:$(tail -1 Gemfile.lock | tr -d ' ') \ + && bundle install --retry=5 --jobs=$(nproc) \ + && rm -rf /usr/local/bundle/cache \ + && chown -R "$USER":"$USER" /workspace /usr/local/bundle /usr/local/share/corepack + +USER $UID + +EXPOSE 4000 4001 + +CMD ["sleep", "infinity"] + +# Ruby Build +FROM ruby AS builder SHELL ["/bin/ash", "-o", "pipefail", "-c"] @@ -39,10 +102,8 @@ RUN apk add --no-cache \ /usr/local/bundle/bundler/gems/*/.git \ /usr/local/bundle/cache/bundler/git -# Stage 3: Runtime -FROM ${RUBY_BASE_IMAGE} - -LABEL maintainer="Gil Desmarais " +# Runtime +FROM ruby AS runtime SHELL ["/bin/ash", "-o", "pipefail", "-c"] diff --git a/README.md b/README.md index 8feabe258..f03a0c142 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,15 @@ volumes: See [Creating Custom Feeds](https://html2rss.github.io/creating-custom-feeds/). -## Development (Dev Container) +## Development (Docker) -Run setup, lint, and tests inside the Dev Container: +Run setup, lint, and tests inside the `dev` compose service built from the `dev` stage of the root `Dockerfile`: ```bash -make setup -make dev -make ready +docker compose -f docker-compose.dev.yml build +docker compose -f docker-compose.dev.yml run --rm dev make setup +docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev +docker compose -f docker-compose.dev.yml run --rm dev make ready ``` Feature branches that depend on an unreleased `html2rss` API may pin the gem from GitHub in `Gemfile`. Revert to the released RubyGems version before merging to `main`. diff --git a/bin/compose-contract-verify b/bin/compose-contract-verify index d230501be..1e5ff8ed2 100755 --- a/bin/compose-contract-verify +++ b/bin/compose-contract-verify @@ -141,6 +141,53 @@ def verify_production! assert_botasaurus_loopback_only!(bot['ports']) end +def volume_target?(volumes, target) + Array(volumes).any? do |volume| + volume.is_a?(Hash) && volume['target'].to_s == target + end +end + +def assert_dev_ports!(ports) + published = Array(ports) + [4000, 4001].each do |port_number| + match = published.find { |port| published_port?(port, published: port_number, target: port_number) } + fail!("dev must publish #{port_number}:#{port_number}") unless match + end +end + +def assert_dev_volumes!(volumes) + fail!('dev must bind-mount /workspace') unless volume_target?(volumes, '/workspace') + fail!('dev must mount /usr/local/bundle volume') unless volume_target?(volumes, '/usr/local/bundle') +end + +def assert_dev_command!(command) + joined = Array(command).join(' ') + return if joined.include?('sleep') && joined.include?('infinity') + + fail!("dev command must be sleep infinity, got #{command.inspect}") +end + +def assert_dev_build!(dev) + assert_equal!(dev['image'], 'html2rss/web:dev', 'dev image') + + build = dev.fetch('build') { fail!('dev missing build') } + assert_equal!(build['target'], 'dev', 'dev build.target') + assert_equal!(File.basename(build.fetch('dockerfile', 'Dockerfile')), 'Dockerfile', 'dev build.dockerfile') +end + +def verify_dev! + config = compose_config(File.join(REPO_ROOT, 'docker-compose.dev.yml')) + services = config.fetch('services') { fail!('dev compose missing top-level services map') } + assert_equal!(services.keys.sort, ['dev'], 'dev compose services') + + dev = fetch_service!(services, 'dev') + assert_dev_build!(dev) + assert_dev_volumes!(dev['volumes']) + assert_dev_ports!(dev['ports']) + assert_dev_command!(dev['command']) +end + verify_quickstart! verify_production! +verify_dev! puts 'compose contract verify passed' diff --git a/bin/dev-ruby b/bin/dev-ruby index 0fe5bd903..72fbcb74d 100755 --- a/bin/dev-ruby +++ b/bin/dev-ruby @@ -10,8 +10,13 @@ fi export RACK_ENV=${RACK_ENV:-development} +# BusyBox lsof in this image reports internal FDs and exits 0, so prefer netstat. +port_in_use() { + netstat -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]$1$" +} + # Prevent silent failures from port conflicts -if lsof -Pi :${PORT:-4000} -sTCP:LISTEN -t >/dev/null 2>&1; then +if port_in_use "${PORT:-4000}"; then echo "❌ Port ${PORT:-4000} is already in use. Run: pkill -f 'falcon.*html2rss-web'" exit 1 fi diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..99bd74183 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,25 @@ +services: + dev: + image: html2rss/web:dev + build: + context: . + target: dev + args: + UID: ${DEV_UID:-1000} + GID: ${DEV_GID:-1000} + volumes: + - .:/workspace:cached + - bundle-cache:/usr/local/bundle + ports: + - "4000:4000" + - "4001:4001" + environment: + RACK_ENV: development + BUNDLE_PATH: /usr/local/bundle + PORT: "4000" + BOTASAURUS_SCRAPER_URL: ${BOTASAURUS_SCRAPER_URL:-http://host.docker.internal:4010} + working_dir: /workspace + command: sleep infinity + +volumes: + bundle-cache: diff --git a/docs/README.md b/docs/README.md index e238b3928..117448863 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,7 +20,7 @@ Welcome! This is the canonical source of truth for contributing to `html2rss-web - **Backend**: Ruby + Falcon + Roda under the `Html2rss::Web` namespace. - **Frontend**: Preact + Vite, built into `frontend/dist` and served at `/` in production. - **Feed extraction**: Delegated to the `html2rss` gem (HTTPX non-blocking transport layer). -- **Distribution**: Docker Compose / Dev Container first. +- **Distribution**: Docker Compose first (`dev` target for local work). ### Source Of Truth @@ -31,12 +31,18 @@ Welcome! This is the canonical source of truth for contributing to `html2rss-web --- -## Development Setup (Dev Container) +## Development Setup (Docker) -Use the repository's [Dev Container](../.devcontainer/README.md) for all local development and tests. +Use the `dev` stage of the root `Dockerfile` via `docker-compose.dev.yml` for all local development and tests. Running the app directly on the host is not supported. -### Common Commands (Inside Dev Container) +```bash +docker compose -f docker-compose.dev.yml build +docker compose -f docker-compose.dev.yml run --rm dev make setup +docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev +``` + +### Common Commands (Inside `dev` image) | Command | Purpose | | ------------------------------ | ---------------------------------------------------------- | @@ -74,7 +80,7 @@ Development routing defaults: To change or add API endpoints, follow this sequence: 1. **Ruby Request Spec**: Define the new behavior or endpoint in `spec/html2rss/web/app_integration_spec.rb` or a dedicated request spec. -2. **OpenAPI Generation**: Run `make openapi` inside the Dev Container to regenerate `public/openapi.yaml` from the spec metadata. +2. **OpenAPI Generation**: Run `make openapi` inside the `dev` image to regenerate `public/openapi.yaml` from the spec metadata. 3. **Verify Contract**: Run `make openapi-verify` and `make openapi-lint` to ensure the generated file matches the specs and is valid. 4. **Frontend Client**: Keep generated client artifacts in `frontend/src/api/generated` aligned with `public/openapi.yaml`. @@ -89,13 +95,13 @@ Always verify the contract before committing API changes. Always run this before pushing or committing: ```bash -make ready +docker compose -f docker-compose.dev.yml run --rm dev make ready ``` For frontend changes and API contract/OpenAPI changes, run the CI-parity gate: ```bash -make ci-ready +docker compose -f docker-compose.dev.yml run --rm dev make ci-ready ``` ### Testing Layers diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 81da0b8e2..52b01cc29 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -5,11 +5,7 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || resolveChromiumExecutablePath(); function resolveChromiumExecutablePath(): string | undefined { - const candidates = [ - '/usr/bin/chromium-browser', - '/usr/bin/chromium', - '/home/vscode/.cache/ms-playwright/chromium-1217/chrome-linux/chrome', - ]; + const candidates = ['/usr/bin/chromium-browser', '/usr/bin/chromium']; return candidates.find((candidate) => existsSync(candidate)); } From 31d2b76c8902a6554fcfe300dc1bd2262dbd2fe3 Mon Sep 17 00:00:00 2001 From: Gil Desmarais Date: Sat, 31 Oct 2026 11:33:29 +0100 Subject: [PATCH 2/5] ci: build smoke image in parallel with gates; fix docs drift Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 3 --- docs/README.md | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf2d9106e..0ae7f90f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,9 +105,6 @@ jobs: docker-build-smoke-image: needs: - hadolint - - ruby - - openapi - - frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/docs/README.md b/docs/README.md index 117448863..9b0ea0157 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ Welcome! This is the canonical source of truth for contributing to `html2rss-web ## Development Setup (Docker) Use the `dev` stage of the root `Dockerfile` via `docker-compose.dev.yml` for all local development and tests. -Running the app directly on the host is not supported. +Running the app directly on the host is not supported. On Linux, export `DEV_UID=$(id -u) DEV_GID=$(id -g)` before `build` so bind-mounted files stay owned by you. CI runs the same image via `.github/actions/dev-image`. ```bash docker compose -f docker-compose.dev.yml build @@ -111,7 +111,7 @@ docker compose -f docker-compose.dev.yml run --rm dev make ci-ready | Ruby API | RSpec + Rack::Test | Feed creation, retrieval, auth paths. | | Frontend unit | Vitest + Testing Library | Component rendering and hooks with mocked fetch. | | Frontend contract | Vitest + MSW | End-to-end fetch flows against mocked API responses. | -| Docker smoke | RSpec (`:docker`) | Net::HTTP probes against the containerised service. | +| Docker smoke | `rake` (Rakefile) | HTTP probes against the built runtime image. | --- From 9b44c4144580900335006ad7c80a092c3e319af0 Mon Sep 17 00:00:00 2001 From: Gil Desmarais Date: Sat, 31 Oct 2026 11:36:54 +0100 Subject: [PATCH 3/5] build: host make delegates to dev container; CI uses make targets, pnpm store cache, audit off the dev image Co-Authored-By: Claude Sonnet 5.5 --- .dockerignore | 1 + .github/actions/dev-image/action.yml | 8 ++++- .github/workflows/bump-version.yml | 12 +++---- .github/workflows/ci.yml | 51 ++++++++++++---------------- AGENTS.md | 2 +- Makefile | 16 ++++++++- README.md | 7 ++-- docker-compose.dev.yml | 1 + docs/README.md | 14 ++++---- 9 files changed, 61 insertions(+), 51 deletions(-) diff --git a/.dockerignore b/.dockerignore index 16dc7d159..16484a606 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,3 +21,4 @@ frontend/dist/ frontend/node_modules/ frontend/test-results/ frontend/.astro/ +.pnpm-store/ diff --git a/.github/actions/dev-image/action.yml b/.github/actions/dev-image/action.yml index d9de526d4..ac33540fe 100644 --- a/.github/actions/dev-image/action.yml +++ b/.github/actions/dev-image/action.yml @@ -1,5 +1,5 @@ name: dev image -description: Build the `dev` stage of the root Dockerfile (layer-cached) as html2rss/web:dev for docker-compose.dev.yml +description: Build the `dev` stage of the root Dockerfile (layer-cached) as html2rss/web:dev and restore the pnpm store; run gates afterwards with `make ` runs: using: composite @@ -10,6 +10,12 @@ runs: echo "DEV_UID=$(id -u)" >> "$GITHUB_ENV" echo "DEV_GID=$(id -g)" >> "$GITHUB_ENV" + - uses: actions/cache@v6 + with: + path: .pnpm-store + key: pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }} + restore-keys: pnpm- + - uses: docker/setup-buildx-action@v4 - uses: docker/bake-action@v7 diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index f83ec41f1..1ff54df22 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -13,7 +13,7 @@ permissions: pull-requests: write env: - DEV: docker compose -f docker-compose.dev.yml run --rm -T dev + CI: "true" jobs: bump_version: @@ -50,15 +50,15 @@ jobs: - name: Perform Version Bump id: perform_bump run: | - $DEV bundle exec ruby bin/bump-version "$TARGET_VERSION" - NEW_VERSION=$($DEV ruby -I. -e "require 'config/version'; puts Html2rss::Web::VERSION") + docker compose -f docker-compose.dev.yml run --rm -T dev bundle exec ruby bin/bump-version "$TARGET_VERSION" + NEW_VERSION=$(docker compose -f docker-compose.dev.yml run --rm -T dev ruby -I. -e "require 'config/version'; puts Html2rss::Web::VERSION") echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" - name: Regenerate OpenAPI spec and client run: | - $DEV make frontend-setup - $DEV make openapi - $DEV make openapi-client + make frontend-setup + make openapi + make openapi-client - name: Create Pull Request uses: peter-evans/create-pull-request@v8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ae7f90f2..2de56b955 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - DEV: docker compose -f docker-compose.dev.yml run --rm -T dev + CI: "true" # makes `make` on the runner delegate to the dev container without a TTY jobs: hadolint: @@ -37,14 +37,11 @@ jobs: - uses: actions/checkout@v7 - uses: ./.github/actions/dev-image - - name: Run RuboCop - run: $DEV bundle exec rubocop -F - - - name: Verify Yard documentation - run: $DEV bundle exec yard doc --fail-on-warning --no-output + - name: Lint (RuboCop, Zeitwerk, YARD) + run: make lint-ruby - name: Run RSpec - run: $DEV bundle exec rspec + run: make test-ruby - name: Upload coverage report uses: actions/upload-artifact@v7 @@ -60,23 +57,27 @@ jobs: - uses: actions/checkout@v7 - uses: ./.github/actions/dev-image - - name: Install frontend dependencies for OpenAPI client verification - run: $DEV make frontend-setup - + - run: make frontend-setup - name: Verify generated OpenAPI spec and client are up to date - run: $DEV make openapi-verify - + run: make openapi-verify - name: Lint OpenAPI contract - run: $DEV make openapi-lint + run: make openapi-lint frontend-audit: runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend steps: - uses: actions/checkout@v7 - - uses: ./.github/actions/dev-image + + - name: Setup pnpm + uses: pnpm/action-setup@v6.1.0 + with: + package_json_file: frontend/package.json - name: Audit dependencies - run: $DEV pnpm --dir frontend audit --audit-level=moderate + run: pnpm audit --audit-level=moderate frontend: runs-on: ubuntu-latest @@ -84,23 +85,13 @@ jobs: - uses: actions/checkout@v7 - uses: ./.github/actions/dev-image - - name: Install dependencies - run: $DEV make frontend-setup - - - name: Typecheck frontend - run: $DEV pnpm --dir frontend run typecheck - - - name: Lint CSS with Stylelint - run: $DEV pnpm --dir frontend exec stylelint "**/*.css" - - - name: Check formatting - run: $DEV pnpm --dir frontend run format:check - + - run: make frontend-setup + - name: Typecheck, lint, format + run: make lint-js - name: Run frontend tests - run: $DEV pnpm --dir frontend run test:ci - + run: make test-frontend - name: Run frontend smoke test - run: $DEV pnpm --dir frontend run test:e2e + run: make test-frontend-e2e docker-build-smoke-image: needs: diff --git a/AGENTS.md b/AGENTS.md index b162b7ee1..9e5758012 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Canonical references: ## Execution & Coding Contracts - **Posture:** Time matters. Reduce LOC when possible. No legacy code, no backward compatibility, no shims. -- **Docker-only:** Run all commands inside the `dev` image (the `dev` stage of the root `Dockerfile`) via `docker compose -f docker-compose.dev.yml run --rm …`. For the app: `docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev`. Never execute `make`/`bundle`/`pnpm` gates on the host. +- **Docker-only:** `make ` from the host re-runs the target in the `dev` compose service (the `dev` stage of the root `Dockerfile`); inside the container it runs directly. One-off commands: `docker compose -f docker-compose.dev.yml run --rm dev `. Never run bare `bundle`/`pnpm`/`rspec` on the host. - **Quality gate:** Never push or open a PR without a green `dev`-image gate (`make ready` minimum, which includes `pnpm run format:check` via `lint-js`; `make ci-ready` before push/PR for frontend or API/OpenAPI contract changes). Never mark validation done or open a PR with "gate skipped / unavailable" — start the `dev` service and run the gate, or stop and report the blocker plus next command. - **Frontend verification:** With `make dev` running, verify touched paths in `chrome-devtools` MCP at `http://127.0.0.1:4001/`. Confirm affected states (guest/member/result) share the same frame, keep one primary task and one canonical control per outcome, and preserve autofocus/return-focus on load and transitions ([docs/design-system.md](docs/design-system.md)). - **Security & YARD:** No leaking secrets or insecure patterns ([docs/README.md#security--safety-rules](docs/README.md#security--safety-rules)). Every public Ruby method in `app/` requires a YARD docstring with typed `@param` and `@return` ([docs/README.md#architectural-constraints](docs/README.md#architectural-constraints)). diff --git a/Makefile b/Makefile index c4444eb77..3915c3315 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,14 @@ # frozen_string_literal: true -.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready ci-ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral test-frontend-e2e lint-css-primitives +# On the host, every target re-runs inside the `dev` compose service; below `else` is the in-container Makefile. +ifeq ($(wildcard /.dockerenv),) +.DEFAULT_GOAL := help +DEV_RUN = docker compose -f docker-compose.dev.yml run --rm $(if $(CI),-T) $(if $(filter dev%,$(MAKECMDGOALS)),--service-ports) dev + +%: + @$(DEV_RUN) make $@ +else +.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready ci-ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral test-frontend-e2e lint-css-primitives frontend-audit RUBOCOP_FLAGS ?= --cache false @@ -66,6 +74,7 @@ lint-ruby: ## Run Ruby linter (RuboCop) - errors when issues found bundle exec rake zeitwerk:verify @echo "Running YARD public-method docs check..." bundle exec rake yard:verify_public_docs + bundle exec yard doc --fail-on-warning --no-output @echo "Ruby linting complete!" lint-js: ## Run JavaScript/Frontend linting (TypeScript + ESLint + Stylelint + Prettier) - errors when issues found @@ -149,7 +158,12 @@ clean: ## Clean temporary files @cd frontend && rm -rf dist/ node_modules/ @echo "Clean complete!" +frontend-audit: ## Fail on moderate+ advisories in the pnpm lockfile + @cd frontend && pnpm audit --audit-level=moderate + frontend-setup: ## Setup frontend dependencies @echo "Setting up frontend dependencies..." @cd frontend && CI=1 pnpm install --frozen-lockfile @echo "Frontend setup complete!" + +endif diff --git a/README.md b/README.md index f03a0c142..b2a93428f 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,9 @@ See [Creating Custom Feeds](https://html2rss.github.io/creating-custom-feeds/). Run setup, lint, and tests inside the `dev` compose service built from the `dev` stage of the root `Dockerfile`: ```bash -docker compose -f docker-compose.dev.yml build -docker compose -f docker-compose.dev.yml run --rm dev make setup -docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev -docker compose -f docker-compose.dev.yml run --rm dev make ready +make setup +make dev +make ready ``` Feature branches that depend on an unreleased `html2rss` API may pin the gem from GitHub in `Gemfile`. Revert to the released RubyGems version before merging to `main`. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 99bd74183..f91be93d0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,6 +16,7 @@ services: environment: RACK_ENV: development BUNDLE_PATH: /usr/local/bundle + npm_config_store_dir: /workspace/.pnpm-store # same filesystem as node_modules, so installs hardlink PORT: "4000" BOTASAURUS_SCRAPER_URL: ${BOTASAURUS_SCRAPER_URL:-http://host.docker.internal:4010} working_dir: /workspace diff --git a/docs/README.md b/docs/README.md index 9b0ea0157..1b638410b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,16 +33,14 @@ Welcome! This is the canonical source of truth for contributing to `html2rss-web ## Development Setup (Docker) -Use the `dev` stage of the root `Dockerfile` via `docker-compose.dev.yml` for all local development and tests. -Running the app directly on the host is not supported. On Linux, export `DEV_UID=$(id -u) DEV_GID=$(id -g)` before `build` so bind-mounted files stay owned by you. CI runs the same image via `.github/actions/dev-image`. +Run `make ` on the host: it delegates to the `dev` stage of the root `Dockerfile` via `docker-compose.dev.yml` (built on first use). Running the app directly on the host is not supported. On Linux, export `DEV_UID=$(id -u) DEV_GID=$(id -g)` before the first `make` so bind-mounted files stay owned by you. CI runs the same image via `.github/actions/dev-image`. ```bash -docker compose -f docker-compose.dev.yml build -docker compose -f docker-compose.dev.yml run --rm dev make setup -docker compose -f docker-compose.dev.yml run --rm --service-ports dev make dev +make setup +make dev ``` -### Common Commands (Inside `dev` image) +### Common Commands | Command | Purpose | | ------------------------------ | ---------------------------------------------------------- | @@ -95,13 +93,13 @@ Always verify the contract before committing API changes. Always run this before pushing or committing: ```bash -docker compose -f docker-compose.dev.yml run --rm dev make ready +make ready ``` For frontend changes and API contract/OpenAPI changes, run the CI-parity gate: ```bash -docker compose -f docker-compose.dev.yml run --rm dev make ci-ready +make ci-ready ``` ### Testing Layers From 2d8b4b51f1d077045aa4df26e5993f760314f7c7 Mon Sep 17 00:00:00 2001 From: Gil Desmarais Date: Sat, 31 Oct 2026 11:41:04 +0100 Subject: [PATCH 4/5] build: detect Podman, preflight Docker, add make reset Co-Authored-By: Claude Sonnet 5.5 --- Makefile | 6 +++++- README.md | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3915c3315..14ed6dc6e 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ # frozen_string_literal: true # On the host, every target re-runs inside the `dev` compose service; below `else` is the in-container Makefile. -ifeq ($(wildcard /.dockerenv),) +ifeq ($(wildcard /.dockerenv /run/.containerenv),) .DEFAULT_GOAL := help DEV_RUN = docker compose -f docker-compose.dev.yml run --rm $(if $(CI),-T) $(if $(filter dev%,$(MAKECMDGOALS)),--service-ports) dev %: + @docker info >/dev/null 2>&1 || { echo "Docker is not running. Start Docker Desktop, OrbStack, Colima or Podman." >&2; exit 1; } @$(DEV_RUN) make $@ + +reset: ## Drop the dev containers and volumes (bundle cache); the image rebuilds on next use + docker compose -f docker-compose.dev.yml down -v else .PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready ci-ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral test-frontend-e2e lint-css-primitives frontend-audit diff --git a/README.md b/README.md index b2a93428f..f0bab7056 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,13 @@ See [Creating Custom Feeds](https://html2rss.github.io/creating-custom-feeds/). ## Development (Docker) -Run setup, lint, and tests inside the `dev` compose service built from the `dev` stage of the root `Dockerfile`: +Needs any Docker-compatible engine (Docker Desktop, OrbStack, Colima, Podman); the first `make` builds the image. Run setup, lint, and tests inside the `dev` compose service built from the `dev` stage of the root `Dockerfile`: ```bash make setup make dev make ready +make reset # drop dev volumes after Gemfile.lock changes misbehave ``` Feature branches that depend on an unreleased `html2rss` API may pin the gem from GitHub in `Gemfile`. Revert to the released RubyGems version before merging to `main`. From 0e950461b47519eb923828822c306c801c158d9d Mon Sep 17 00:00:00 2001 From: Gil Desmarais Date: Sat, 31 Oct 2026 11:46:35 +0100 Subject: [PATCH 5/5] fix(ci): run docker smoke specs in the dev image on the app container's network Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 3 +-- Rakefile | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de56b955..efa7d7a4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,8 +119,7 @@ jobs: smoke_auto_source_enabled: ["false", "true"] steps: - uses: actions/checkout@v7 - - - uses: docker/setup-buildx-action@v4 + - uses: ./.github/actions/dev-image - name: Load Docker smoke image from cache uses: docker/build-push-action@v7 diff --git a/Rakefile b/Rakefile index c62c9361c..44a9733b2 100644 --- a/Rakefile +++ b/Rakefile @@ -89,7 +89,15 @@ task :test do 'SMOKE_AUTO_SOURCE_ENABLED' => smoke_auto_source_enabled, 'RUN_DOCKER_SPECS' => 'true' } - sh smoke_env, 'bundle exec rspec --tag docker' + # Run the suite in the dev image, inside the app container's network namespace (so 127.0.0.1:4000 is the app). + sh smoke_env, + ['docker run --rm', + '--network container:html2rss-web-test', + *smoke_env.keys.map { |key| "-e #{key}" }, + "-v #{current_dir}:/workspace", + '-w /workspace', + 'html2rss/web:dev', + 'bundle exec rspec --tag docker'].join(' ') ensure if test_container_exists?('html2rss-web-test') Output.describe 'Cleaning up test container'