From 3332fcbcd047d4656aa0e85801569b89c1f85529 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 11:35:11 +0000 Subject: [PATCH 01/10] Migrate CI from CircleCI to GitHub Actions Co-authored-by: Chris Zetter --- .circleci/config.yml | 76 --------------- .circleci/record_coverage | 103 -------------------- .devcontainer/devcontainer.json | 1 - .github/workflows/ci.yml | 160 ++++++++++++++++++++++++++++++++ AGENTS.md | 2 +- spec/rails_helper.rb | 15 ++- 6 files changed, 173 insertions(+), 184 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100644 .circleci/record_coverage create mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e868a1b5f..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,76 +0,0 @@ -jobs: - rubocop: - docker: - - image: "cimg/ruby:3.4" - steps: - - checkout - - ruby/install-deps - - ruby/rubocop-check: - format: progress - label: Inspecting with Rubocop - - test: - docker: - - image: "cimg/ruby:3.4-browsers" - - image: "circleci/postgres:12.0-alpine-ram" - environment: - POSTGRES_DB: choco_cake_test - POSTGRES_PASSWORD: password - POSTGRES_USER: choco - - image: "circleci/redis:6.2-alpine" - - environment: - BUNDLE_JOBS: "3" - BUNDLE_RETRY: "3" - PAGER: "" - POSTGRES_DB: choco_cake_test - POSTGRES_PASSWORD: password - POSTGRES_USER: choco - POSTGRES_HOST: "127.0.0.1" - RAILS_ENV: test - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt - EDITOR_ENCRYPTION_KEY: a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0 - steps: - - checkout - - browser-tools/install-firefox - - ruby/install-deps: - key: gems-v2- - - run: - command: "dockerize -wait tcp://localhost:5432 -timeout 1m" - name: Wait for DB - - run: - command: "sudo apt-get update && sudo apt-get install --yes --no-install-recommends postgresql-client jq curl imagemagick" - name: Install postgres client, jq, curl, imagemagick - - run: - command: "bin/rails db:setup --trace" - name: Database setup - - ruby/rspec-test - - store_artifacts: - path: coverage - - run: - name: Post test coverage to Github - command: bash -ue .circleci/record_coverage - when: always - -orbs: - browser-tools: circleci/browser-tools@1 - node: circleci/node@4 - ruby: circleci/ruby@1.3 - -version: 2.1 - -workflows: - code_quality: - jobs: - - rubocop: - filters: - branches: - ignore: - - master - - main - test: - jobs: - - test: - context: raspberrypigithubbot diff --git a/.circleci/record_coverage b/.circleci/record_coverage deleted file mode 100644 index 9a60a0d4c..000000000 --- a/.circleci/record_coverage +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -ueo pipefail - -# Record coverage -# -# This script uses the Circle and Github APIs to poke a comment into a PR about test coverage. -# -# To work, the GITHUB_TOKEN and CIRCLE_TOKEN vars must be in the environment, -# with appropriate API tokens from GH and Circle. -# -# Also to get the magic link to your test coverage, you'll want to store the -# `coverage/` directory. -#``` -# - store_artifacts: -# path: coverage -#``` - -CURL_ARGS="-s -S -f" - -function graceful_exit() { - echo "*** Something failed! Exiting gracefully so the build doesn't fail overall" - exit 0 -} - -# -# Wrapper for the Github GraphQL API -# -function gh_query() { - # Build and escape our JSON - json=$(jq -n --arg q "$*" '{query: $q}') - curl $CURL_ARGS -H "Authorization: bearer $GITHUB_TOKEN" -X POST -d "$json" https://api.github.com/graphql -} - - -# Trap any fails, and force a successful exit. -trap graceful_exit ERR - -last_run=coverage/.last_run.json -if ! [ -s $last_run ] ; then - echo "*** No $last_run file found." - exit 0 -fi - -which jq > /dev/null || sudo apt-get install -y jq - -# This is the message that makes it into github -msg="* CircleCI build [#${CIRCLE_BUILD_NUM}](${CIRCLE_BUILD_URL})\n" -msg="$msg* Test coverage: " - - -# Check to see if coverage is under `result.line` or under `result.covered_percent` (older versions) -coverage=$(jq -r 'if .result.line then .result.line else .result.covered_percent end' < coverage/.last_run.json) - -if [ "${coverage}" = "null" ] ; then - echo "*** Failed to determine coverage" - exit 0 -fi - -artifacts_response=$(curl $CURL_ARGS -H "Circle-Token: $CIRCLE_TOKEN" https://circleci.com/api/v1.1/project/gh/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BUILD_NUM}/artifacts) -coverage_url=$(echo ${artifacts_response} | jq -r '. | map(select(.path == "coverage/index.html"))[0].url') - -if ! [ "${coverage_url}" = "null" ] ; then - msg="$msg [$coverage%]($coverage_url)\n\n" -else - msg="$msg $coverage%\n\n" - msg="$msg > CircleCI didn't store the Simplecov index (maybe the store_artifacts step is missing?)" -fi - -# Find associated PR. *NB* we're assuming that the first, open PR is the one -# to comment on. -q="query { - repository(name: \"${CIRCLE_PROJECT_REPONAME}\", owner: \"${CIRCLE_PROJECT_USERNAME}\") { - ref(qualifiedName: \"${CIRCLE_BRANCH}\") { - associatedPullRequests(first: 1) { - nodes { - id - } - } - } - } -}" - -pr_response=$(gh_query $q) -pr_node=$(echo $pr_response | jq -r ".data.repository.ref.associatedPullRequests.nodes[0].id") - -if [ "$pr_node" = "null" ] ; then - echo "*** No PR found" - exit 0 -fi - - -echo ">>> Posting code coverage comment" -m="mutation { - addComment(input: { - subjectId: \"${pr_node}\", - body: \"${msg}\" - }) { - subject { - id - } - } -}" - -gh_query $m diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4373f9fe9..caf25154f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -58,7 +58,6 @@ "mikestead.dotenv", "ms-vscode.remote-repositories", "github.remotehub", - "circleci.circleci", "stylelint.vscode-stylelint", "christian-kohler.path-intellisense", "esbenp.prettier-vscode", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..80a6d8400 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,160 @@ +name: CI + +on: + pull_request: + push: + +permissions: + contents: read + +env: + BUNDLE_JOBS: '3' + BUNDLE_RETRY: '3' + PAGER: '' + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + + - name: Install lint dependencies + run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends libpq-dev + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .tool-versions + bundler-cache: true + + - name: Run RuboCop + run: bundle exec rubocop --format progress + + test: + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + pull-requests: write + env: + RAILS_ENV: test + POSTGRES_DB: choco_cake_test + POSTGRES_PASSWORD: password + POSTGRES_USER: choco + POSTGRES_HOST: 127.0.0.1 + POSTGRES_PORT: '5432' + REDIS_URL: redis://127.0.0.1:6379/1 + ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key + ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key + ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt + EDITOR_ENCRYPTION_KEY: a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0 + services: + postgres: + image: postgres:12 + env: + POSTGRES_DB: choco_cake_test + POSTGRES_PASSWORD: password + POSTGRES_USER: choco + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U choco -d choco_cake_test" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + redis: + image: redis:6.2-alpine + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4 + + - name: Install test dependencies + run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends libpq-dev postgresql-client jq curl imagemagick + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .tool-versions + bundler-cache: true + + - name: Set up Firefox + id: setup_firefox + continue-on-error: true + uses: browser-actions/setup-firefox@v1 + + - name: Set up Chrome fallback + if: steps.setup_firefox.outcome == 'failure' + uses: browser-actions/setup-chrome@v1 + + - name: Select system test browser + run: | + if [ "${{ steps.setup_firefox.outcome }}" = "success" ]; then + echo "SYSTEM_TEST_BROWSER=firefox" >> "$GITHUB_ENV" + echo "Using Firefox for system tests" + else + echo "SYSTEM_TEST_BROWSER=chrome" >> "$GITHUB_ENV" + echo "Falling back to Chrome for system tests" + fi + + - name: Wait for DB + run: | + until pg_isready -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER"; do + sleep 1 + done + + - name: Database setup + run: bin/rails db:setup --trace + + - name: Run test suite + run: bundle exec rspec + + - name: Upload coverage artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage + if-no-files-found: ignore + + - name: Build coverage comment + if: always() && github.event_name == 'pull_request' + id: coverage_comment + run: | + coverage='unavailable' + if [ -s coverage/.last_run.json ]; then + coverage=$(jq -r 'if .result.line then .result.line else .result.covered_percent end' coverage/.last_run.json) + fi + + if [ -z "$coverage" ] || [ "$coverage" = 'null' ]; then + coverage='unavailable' + fi + + run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [ "$coverage" = 'unavailable' ]; then + message="### Test coverage +SimpleCov coverage data was unavailable for this run. +Run: $run_url" + else + message="### Test coverage +$coverage% line coverage reported by SimpleCov. +Run: $run_url" + fi + + { + echo "message<<'EOF'" + echo "$message" + echo 'EOF' + } >> "$GITHUB_OUTPUT" + + - name: Comment coverage on PR + if: always() && github.event_name == 'pull_request' + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: simplecov-coverage + message: ${{ steps.coverage_comment.outputs.message }} diff --git a/AGENTS.md b/AGENTS.md index 7b47a2cff..5135dc981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ docker compose up - Full suite: `docker compose run --rm api rspec` - Single spec: `docker compose run --rm api rspec spec/path/to/spec.rb` - Lint: `docker compose run --rm api bundle exec rubocop` -- CI: CircleCI with Ruby 3.2, Postgres 12, Redis. +- CI: GitHub Actions with Ruby 3.4, Postgres 12, Redis. ## Where to Look First - Routes: `config/routes.rb`. Auth: `config/initializers/omniauth.rb`, `app/helpers/authentication_helper.rb`, `app/controllers/concerns/identifiable.rb`. diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 208a5d28f..36a2f7519 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -121,9 +121,18 @@ # or Capybara to talk to selenium etc. WebMock.allow_net_connect! - # Ensure we update the driver here, while we can connect to the network - Webdrivers::Geckodriver.update - driven_by :selenium_headless, using: :firefox + # Allow CI to fall back to Chrome when Firefox is unavailable. + system_test_browser = ENV.fetch('SYSTEM_TEST_BROWSER', 'firefox') + case system_test_browser + when 'firefox' + Webdrivers::Geckodriver.update + driven_by :selenium_headless, using: :firefox + when 'chrome' + Webdrivers::Chromedriver.update + driven_by :selenium_headless, using: :chrome + else + raise "Unsupported SYSTEM_TEST_BROWSER '#{system_test_browser}'" + end # Need to set the hostname, otherwise it defaults to www.example.com. default_url_options[:host] = Capybara.server_host From 797990d15c1fda8476f83b43dda7eecd7f152db6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 11:39:50 +0000 Subject: [PATCH 02/10] Use GitHub-owned actions and browser fallback scripts Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 93 ++++++++++++++++++++++++---------------- spec/rails_helper.rb | 1 + 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a6d8400..640e7d820 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,36 +16,35 @@ jobs: lint: runs-on: ubuntu-latest timeout-minutes: 20 + container: + image: ruby:3.4 steps: - uses: actions/checkout@v4 - name: Install lint dependencies - run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends libpq-dev - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .tool-versions - bundler-cache: true + run: apt-get update && apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config - name: Run RuboCop - run: bundle exec rubocop --format progress + run: bundle install && bundle exec rubocop --format progress test: runs-on: ubuntu-latest timeout-minutes: 45 + container: + image: ruby:3.4 permissions: contents: read + issues: write pull-requests: write env: RAILS_ENV: test POSTGRES_DB: choco_cake_test POSTGRES_PASSWORD: password POSTGRES_USER: choco - POSTGRES_HOST: 127.0.0.1 + POSTGRES_HOST: postgres POSTGRES_PORT: '5432' - REDIS_URL: redis://127.0.0.1:6379/1 + REDIS_URL: redis://redis:6379/1 ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt @@ -57,8 +56,6 @@ jobs: POSTGRES_DB: choco_cake_test POSTGRES_PASSWORD: password POSTGRES_USER: choco - ports: - - 5432:5432 options: >- --health-cmd="pg_isready -U choco -d choco_cake_test" --health-interval=10s @@ -66,38 +63,29 @@ jobs: --health-retries=5 redis: image: redis:6.2-alpine - ports: - - 6379:6379 steps: - uses: actions/checkout@v4 - name: Install test dependencies - run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends libpq-dev postgresql-client jq curl imagemagick - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .tool-versions - bundler-cache: true - - - name: Set up Firefox - id: setup_firefox - continue-on-error: true - uses: browser-actions/setup-firefox@v1 - - - name: Set up Chrome fallback - if: steps.setup_firefox.outcome == 'failure' - uses: browser-actions/setup-chrome@v1 + run: > + apt-get update && apt-get install --yes --no-install-recommends + build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr chromium - name: Select system test browser run: | - if [ "${{ steps.setup_firefox.outcome }}" = "success" ]; then + if command -v firefox >/dev/null 2>&1; then echo "SYSTEM_TEST_BROWSER=firefox" >> "$GITHUB_ENV" echo "Using Firefox for system tests" else + echo "Firefox unavailable; falling back to Chrome" echo "SYSTEM_TEST_BROWSER=chrome" >> "$GITHUB_ENV" - echo "Falling back to Chrome for system tests" + chrome_bin="$(command -v google-chrome || command -v chromium || command -v chromium-browser || true)" + if [ -z "$chrome_bin" ]; then + echo "Chrome fallback requested, but no Chrome binary is available" + exit 1 + fi + echo "CHROME_BIN=$chrome_bin" >> "$GITHUB_ENV" fi - name: Wait for DB @@ -106,6 +94,9 @@ jobs: sleep 1 done + - name: Install gems + run: bundle install + - name: Database setup run: bin/rails db:setup --trace @@ -153,8 +144,38 @@ Run: $run_url" - name: Comment coverage on PR if: always() && github.event_name == 'pull_request' - continue-on-error: true - uses: marocchino/sticky-pull-request-comment@v2 + uses: actions/github-script@v7 + env: + COVERAGE_MESSAGE: ${{ steps.coverage_comment.outputs.message }} with: - header: simplecov-coverage - message: ${{ steps.coverage_comment.outputs.message }} + script: | + const marker = ''; + const body = `${marker}\n${process.env.COVERAGE_MESSAGE}`; + const issue_number = context.payload.pull_request.number; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + per_page: 100 + }); + + const existing = comments.find((comment) => + comment.user?.type === 'Bot' && comment.body?.includes(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body + }); + } diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 36a2f7519..0f041ad77 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -128,6 +128,7 @@ Webdrivers::Geckodriver.update driven_by :selenium_headless, using: :firefox when 'chrome' + Selenium::WebDriver::Chrome.path = ENV['CHROME_BIN'] if ENV['CHROME_BIN'] Webdrivers::Chromedriver.update driven_by :selenium_headless, using: :chrome else From 1b5f78ebe04cda0a25cd35ebfb758da77c80bfcf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 11:40:47 +0000 Subject: [PATCH 03/10] Fix CI workflow YAML coverage comment block Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 640e7d820..9fc47f398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,13 +127,19 @@ jobs: run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" if [ "$coverage" = 'unavailable' ]; then - message="### Test coverage -SimpleCov coverage data was unavailable for this run. -Run: $run_url" + message=$(cat < Date: Fri, 27 Feb 2026 11:45:22 +0000 Subject: [PATCH 04/10] Do not fail CI when coverage comment cannot be posted Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fc47f398..4bb40d4bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,7 @@ jobs: - name: Comment coverage on PR if: always() && github.event_name == 'pull_request' + continue-on-error: true uses: actions/github-script@v7 env: COVERAGE_MESSAGE: ${{ steps.coverage_comment.outputs.message }} From 049993ae5afc491cb152aa7e878f6e813a902d4f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 11:51:11 +0000 Subject: [PATCH 05/10] Keep system tests Firefox-only in CI Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 19 +++---------------- spec/rails_helper.rb | 16 +++------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bb40d4bb..c6faa8632 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,23 +70,10 @@ jobs: - name: Install test dependencies run: > apt-get update && apt-get install --yes --no-install-recommends - build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr chromium + build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr - - name: Select system test browser - run: | - if command -v firefox >/dev/null 2>&1; then - echo "SYSTEM_TEST_BROWSER=firefox" >> "$GITHUB_ENV" - echo "Using Firefox for system tests" - else - echo "Firefox unavailable; falling back to Chrome" - echo "SYSTEM_TEST_BROWSER=chrome" >> "$GITHUB_ENV" - chrome_bin="$(command -v google-chrome || command -v chromium || command -v chromium-browser || true)" - if [ -z "$chrome_bin" ]; then - echo "Chrome fallback requested, but no Chrome binary is available" - exit 1 - fi - echo "CHROME_BIN=$chrome_bin" >> "$GITHUB_ENV" - fi + - name: Verify Firefox availability + run: firefox --version - name: Wait for DB run: | diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0f041ad77..208a5d28f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -121,19 +121,9 @@ # or Capybara to talk to selenium etc. WebMock.allow_net_connect! - # Allow CI to fall back to Chrome when Firefox is unavailable. - system_test_browser = ENV.fetch('SYSTEM_TEST_BROWSER', 'firefox') - case system_test_browser - when 'firefox' - Webdrivers::Geckodriver.update - driven_by :selenium_headless, using: :firefox - when 'chrome' - Selenium::WebDriver::Chrome.path = ENV['CHROME_BIN'] if ENV['CHROME_BIN'] - Webdrivers::Chromedriver.update - driven_by :selenium_headless, using: :chrome - else - raise "Unsupported SYSTEM_TEST_BROWSER '#{system_test_browser}'" - end + # Ensure we update the driver here, while we can connect to the network + Webdrivers::Geckodriver.update + driven_by :selenium_headless, using: :firefox # Need to set the hostname, otherwise it defaults to www.example.com. default_url_options[:host] = Capybara.server_host From f000d7c94e0f0f08abb02b1cbad33ddbd9c7b82b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 12:02:25 +0000 Subject: [PATCH 06/10] Refactor coverage comment steps into CI scripts Co-authored-by: Chris Zetter --- .github/scripts/build_coverage_comment.sh | 37 ++++++++++ .github/scripts/post_coverage_comment.sh | 49 +++++++++++++ .github/workflows/ci.yml | 86 +++++------------------ 3 files changed, 102 insertions(+), 70 deletions(-) create mode 100755 .github/scripts/build_coverage_comment.sh create mode 100755 .github/scripts/post_coverage_comment.sh diff --git a/.github/scripts/build_coverage_comment.sh b/.github/scripts/build_coverage_comment.sh new file mode 100755 index 000000000..83ee75d47 --- /dev/null +++ b/.github/scripts/build_coverage_comment.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +coverage='unavailable' +if [ -s coverage/.last_run.json ]; then + coverage=$(jq -r 'if .result.line then .result.line else .result.covered_percent end' coverage/.last_run.json) +fi + +if [ -z "${coverage}" ] || [ "${coverage}" = 'null' ]; then + coverage='unavailable' +fi + +run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + +if [ "${coverage}" = 'unavailable' ]; then + message=$( + cat <> "${GITHUB_OUTPUT}" diff --git a/.github/scripts/post_coverage_comment.sh b/.github/scripts/post_coverage_comment.sh new file mode 100755 index 000000000..e4e8181fe --- /dev/null +++ b/.github/scripts/post_coverage_comment.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +marker='' +body="${marker}"$'\n'"${COVERAGE_MESSAGE:-}" + +if [ -z "${COVERAGE_MESSAGE:-}" ]; then + echo 'COVERAGE_MESSAGE is empty; skipping PR comment.' + exit 0 +fi + +pr_number=$(jq -r '.pull_request.number // empty' "${GITHUB_EVENT_PATH}") +if [ -z "${pr_number}" ]; then + echo 'No pull request number in event payload; skipping PR comment.' + exit 0 +fi + +owner_repo="${GITHUB_REPOSITORY}" +owner="${owner_repo%%/*}" +repo="${owner_repo#*/}" +api_base="https://api.github.com/repos/${owner}/${repo}/issues" + +comments_json=$(curl -sS -f \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H 'Accept: application/vnd.github+json' \ + "${api_base}/${pr_number}/comments?per_page=100") + +existing_comment_id=$(echo "${comments_json}" | jq -r --arg marker "${marker}" \ + 'map(select(.user.type == "Bot" and (.body // "" | contains($marker)))) | .[0].id // empty') + +payload=$(jq -n --arg body "${body}" '{body: $body}') + +if [ -n "${existing_comment_id}" ]; then + curl -sS -f \ + -X PATCH \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H 'Accept: application/vnd.github+json' \ + "${api_base}/comments/${existing_comment_id}" \ + -d "${payload}" > /dev/null + echo "Updated coverage comment ${existing_comment_id} on PR #${pr_number}." +else + curl -sS -f \ + -X POST \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H 'Accept: application/vnd.github+json' \ + "${api_base}/${pr_number}/comments" \ + -d "${payload}" > /dev/null + echo "Created coverage comment on PR #${pr_number}." +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6faa8632..649258c5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,8 +25,14 @@ jobs: - name: Install lint dependencies run: apt-get update && apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config + - name: Set up Ruby and bundle cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .tool-versions + bundler-cache: true + - name: Run RuboCop - run: bundle install && bundle exec rubocop --format progress + run: bundle exec rubocop --format progress test: runs-on: ubuntu-latest @@ -72,6 +78,12 @@ jobs: apt-get update && apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr + - name: Set up Ruby and bundle cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .tool-versions + bundler-cache: true + - name: Verify Firefox availability run: firefox --version @@ -81,9 +93,6 @@ jobs: sleep 1 done - - name: Install gems - run: bundle install - - name: Database setup run: bin/rails db:setup --trace @@ -101,75 +110,12 @@ jobs: - name: Build coverage comment if: always() && github.event_name == 'pull_request' id: coverage_comment - run: | - coverage='unavailable' - if [ -s coverage/.last_run.json ]; then - coverage=$(jq -r 'if .result.line then .result.line else .result.covered_percent end' coverage/.last_run.json) - fi - - if [ -z "$coverage" ] || [ "$coverage" = 'null' ]; then - coverage='unavailable' - fi - - run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - if [ "$coverage" = 'unavailable' ]; then - message=$(cat <> "$GITHUB_OUTPUT" + run: .github/scripts/build_coverage_comment.sh - name: Comment coverage on PR if: always() && github.event_name == 'pull_request' continue-on-error: true - uses: actions/github-script@v7 env: + GITHUB_TOKEN: ${{ github.token }} COVERAGE_MESSAGE: ${{ steps.coverage_comment.outputs.message }} - with: - script: | - const marker = ''; - const body = `${marker}\n${process.env.COVERAGE_MESSAGE}`; - const issue_number = context.payload.pull_request.number; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number, - per_page: 100 - }); - - const existing = comments.find((comment) => - comment.user?.type === 'Bot' && comment.body?.includes(marker) - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number, - body - }); - } + run: .github/scripts/post_coverage_comment.sh From 439746b96acc6069c7799b289ddbce58115c5d62 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 12:04:43 +0000 Subject: [PATCH 07/10] Run CI on host runner for setup-ruby cache Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 649258c5a..f11658984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,14 +16,12 @@ jobs: lint: runs-on: ubuntu-latest timeout-minutes: 20 - container: - image: ruby:3.4 steps: - uses: actions/checkout@v4 - name: Install lint dependencies - run: apt-get update && apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config + run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config - name: Set up Ruby and bundle cache uses: ruby/setup-ruby@v1 @@ -37,8 +35,6 @@ jobs: test: runs-on: ubuntu-latest timeout-minutes: 45 - container: - image: ruby:3.4 permissions: contents: read issues: write @@ -48,9 +44,9 @@ jobs: POSTGRES_DB: choco_cake_test POSTGRES_PASSWORD: password POSTGRES_USER: choco - POSTGRES_HOST: postgres + POSTGRES_HOST: 127.0.0.1 POSTGRES_PORT: '5432' - REDIS_URL: redis://redis:6379/1 + REDIS_URL: redis://127.0.0.1:6379/1 ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt @@ -62,6 +58,8 @@ jobs: POSTGRES_DB: choco_cake_test POSTGRES_PASSWORD: password POSTGRES_USER: choco + ports: + - 5432:5432 options: >- --health-cmd="pg_isready -U choco -d choco_cake_test" --health-interval=10s @@ -69,13 +67,15 @@ jobs: --health-retries=5 redis: image: redis:6.2-alpine + ports: + - 6379:6379 steps: - uses: actions/checkout@v4 - name: Install test dependencies run: > - apt-get update && apt-get install --yes --no-install-recommends + sudo apt-get update && sudo apt-get install --yes --no-install-recommends build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr - name: Set up Ruby and bundle cache From 9213a3ac009da74bd45bb5fc27e4ddf5c45341d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 12:07:57 +0000 Subject: [PATCH 08/10] Install Firefox via setup action on Ubuntu runner Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f11658984..1499e4467 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,10 @@ jobs: - name: Install test dependencies run: > sudo apt-get update && sudo apt-get install --yes --no-install-recommends - build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick firefox-esr + build-essential git libpq-dev pkg-config postgresql-client jq curl imagemagick + + - name: Set up Firefox + uses: browser-actions/setup-firefox@v1 - name: Set up Ruby and bundle cache uses: ruby/setup-ruby@v1 From ee1f734946a14c27ce90e742dd77fbfbd2c21d75 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 12:10:29 +0000 Subject: [PATCH 09/10] Set HOSTNAME env for GitHub-hosted test runs Co-authored-by: Chris Zetter --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1499e4467..8bd26a785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: POSTGRES_HOST: 127.0.0.1 POSTGRES_PORT: '5432' REDIS_URL: redis://127.0.0.1:6379/1 + HOSTNAME: 127.0.0.1 ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt From 43e449b41bc1d61cfe49daab20285dbde4ea4c7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 12:27:12 +0000 Subject: [PATCH 10/10] Fix GitHub output delimiter in coverage comment script Co-authored-by: Chris Zetter --- .github/scripts/build_coverage_comment.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/build_coverage_comment.sh b/.github/scripts/build_coverage_comment.sh index 83ee75d47..f22fe987d 100755 --- a/.github/scripts/build_coverage_comment.sh +++ b/.github/scripts/build_coverage_comment.sh @@ -31,7 +31,8 @@ EOF fi { - echo "message<<'EOF'" + delimiter="COVERAGE_COMMENT_${RANDOM}_${RANDOM}" + echo "message<<${delimiter}" echo "${message}" - echo 'EOF' + echo "${delimiter}" } >> "${GITHUB_OUTPUT}"