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/scripts/build_coverage_comment.sh b/.github/scripts/build_coverage_comment.sh new file mode 100755 index 000000000..f22fe987d --- /dev/null +++ b/.github/scripts/build_coverage_comment.sh @@ -0,0 +1,38 @@ +#!/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 new file mode 100644 index 000000000..8bd26a785 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,125 @@ +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 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 exec rubocop --format progress + + test: + runs-on: ubuntu-latest + timeout-minutes: 45 + 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_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 + 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 + 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 + with: + ruby-version: .tool-versions + bundler-cache: true + + - name: Verify Firefox availability + run: firefox --version + + - 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: .github/scripts/build_coverage_comment.sh + + - name: Comment coverage on PR + if: always() && github.event_name == 'pull_request' + continue-on-error: true + env: + GITHUB_TOKEN: ${{ github.token }} + COVERAGE_MESSAGE: ${{ steps.coverage_comment.outputs.message }} + run: .github/scripts/post_coverage_comment.sh 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`.