Build the claim-impound fields with UI::Forms::Group #7576
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| permissions: | |
| actions: write | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| # Dispatches review-app deploys from CI's single `on: push` run — a | |
| # `pull_request: synchronize` trigger + label gate in review-app.yml would | |
| # leave skipped check runs on every push to every unlabeled PR. | |
| # GITHUB_TOKEN can't fire event-driven triggers, but explicit | |
| # workflow_dispatch API calls are allowed (`actions: write` above). | |
| dispatch: | |
| name: "Dispatch - review app conditional triggering" | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| # retry: gh has no built-in retry, and a sporadic API failure shouldn't | |
| # fail the job. Progress goes to stderr — stdout would be captured by | |
| # the $(command substitution) below. | |
| - name: Dispatch review-app deploy (labeled PRs only) | |
| run: | | |
| retry() { local n=1; until "$@"; do [ $n -ge 3 ] && return 1; echo "attempt $n failed; retrying in 15s..." >&2; n=$((n+1)); sleep 15; done; } | |
| pr=$(retry gh pr list --repo "$GITHUB_REPOSITORY" --head "$GITHUB_REF_NAME" \ | |
| --state open --label review-app --json number --jq '.[0].number') | |
| if [ -n "$pr" ]; then | |
| echo "PR #$pr is open with the 'review-app' label -> dispatching review-app deploy" | |
| retry gh workflow run review-app.yml --repo "$GITHUB_REPOSITORY" \ | |
| --ref "$GITHUB_REF_NAME" -f pr_number="$pr" -f action=deploy | |
| else | |
| echo "No open PR with the 'review-app' label for branch '$GITHUB_REF_NAME' -> NOT dispatching review-app deploy" | |
| fi | |
| # Sandbox tracks main: redeploy the persistent sandbox app on every push to | |
| # main. Dispatched here (rather than via a workflow_run trigger in | |
| # sandbox.yml) so both deploys fire from CI's single push run. | |
| # NOTE: this runs in parallel with the `test` job, so sandbox does not wait | |
| # for the test suite to pass. | |
| - name: Dispatch sandbox deploy (main only) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| retry() { local n=1; until "$@"; do [ $n -ge 3 ] && return 1; echo "attempt $n failed; retrying in 15s..." >&2; n=$((n+1)); sleep 15; done; } | |
| echo "Push to main -> dispatching sandbox deploy" | |
| retry gh workflow run sandbox.yml --repo "$GITHUB_REPOSITORY" --ref main | |
| lint_and_scan: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| bundler-cache: true | |
| - run: bundle install | |
| - name: Set up Node | |
| uses: actions/setup-node@v7 | |
| with: | |
| cache: 'npm' | |
| - run: npm install | |
| - name: Lint Ruby code with standard | |
| run: bundle exec standardrb --no-fix | |
| - name: Lint Ruby code with rubocops | |
| run: bundle exec rubocop -c .rubocop_custom.yml | |
| - name: Lint JavaScript code | |
| if: success() || failure() | |
| run: npm run js:lint_check | |
| - name: Lint HTML code | |
| if: success() || failure() | |
| run: npm run html:lint_check | |
| - name: Check HTML formatting | |
| if: success() || failure() | |
| run: npm run html:format_check | |
| - name: Scan for common Rails security vulnerabilities using static analysis | |
| if: success() || failure() | |
| run: bin/brakeman | |
| # Eventually this should be added: | |
| # - name: Scan for security vulnerabilities in JavaScript dependencies | |
| # run: bin/importmap audit | |
| # Builds (or reuses, via GHA layer cache) the tooling image the `test` job | |
| # runs in — see .github/ci/Dockerfile for what it bakes in and why. | |
| build_ci_image: | |
| name: "Build CI image" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| image: ${{ steps.image.outputs.image }} | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }}-ci | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| # Tag + build-args are both derived from the same tracked files, so a | |
| # version bump always produces a new tag AND a matching rebuild — | |
| # nothing to manually keep in sync. | |
| - name: Resolve build args and image tag | |
| id: image | |
| run: | | |
| ruby_version=$(awk '$1=="ruby"{print $2}' .tool-versions) | |
| node_version=$(awk '$1=="nodejs"{print $2}' .tool-versions) | |
| playwright_version=$(node -p "require('./package.json').devDependencies.playwright") | |
| tag=${{ hashFiles('.github/ci/Dockerfile', '.tool-versions', 'package.json') }} | |
| echo "ruby_version=$ruby_version" >> "$GITHUB_OUTPUT" | |
| echo "node_version=$node_version" >> "$GITHUB_OUTPUT" | |
| echo "playwright_version=$playwright_version" >> "$GITHUB_OUTPUT" | |
| echo "image=$REGISTRY/$IMAGE_NAME:$tag" >> "$GITHUB_OUTPUT" | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4.5.2 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # The tag is a content hash (see above) — if it already exists in GHCR | |
| # it's byte-identical to what we'd build, so skip buildx/build/push | |
| # entirely instead of paying their setup cost for a guaranteed no-op. | |
| - name: Check whether this image tag already exists | |
| id: existing | |
| env: | |
| DOCKER_CLI_EXPERIMENTAL: enabled | |
| run: | | |
| if docker manifest inspect ${{ steps.image.outputs.image }} > /dev/null 2>&1; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set up Docker Buildx | |
| if: steps.existing.outputs.found != 'true' | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build and push CI image | |
| if: steps.existing.outputs.found != 'true' | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: .github/ci/Dockerfile | |
| push: true | |
| tags: ${{ steps.image.outputs.image }} | |
| build-args: | | |
| RUBY_VERSION=${{ steps.image.outputs.ruby_version }} | |
| NODE_VERSION=${{ steps.image.outputs.node_version }} | |
| PLAYWRIGHT_VERSION=${{ steps.image.outputs.playwright_version }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| test: | |
| name: "Run tests" | |
| needs: build_ci_image | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # translations-sync push fallback (see below) | |
| pull-requests: write # translations-sync PR fallback (see below) | |
| packages: read # pull the CI image built by build_ci_image | |
| container: | |
| image: ${{ needs.build_ci_image.outputs.image }} | |
| credentials: | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| services: | |
| # Service images pull from Google's pull-through cache of Docker Hub's | |
| # official images. Anonymous pulls from Docker Hub (and from AWS's | |
| # public.ecr.aws mirror) are rate-limited per source IP; Actions runners | |
| # share egress IPs, so on a busy push the parallel shards exhaust the | |
| # quota and the job fails at "Initialize containers" with | |
| # "toomanyrequests: Rate exceeded". mirror.gcr.io serves the identical | |
| # official images without imposing that limit, and needs no credentials. | |
| postgres: | |
| image: mirror.gcr.io/library/postgres:16-alpine | |
| ports: | |
| - 5432:5432 | |
| env: | |
| POSTGRES_USER: rails | |
| POSTGRES_PASSWORD: password | |
| # needed because the postgres container does not provide a healthcheck | |
| # tmpfs makes DB faster by using RAM | |
| # | |
| # health-interval 1s, not 10s: docker runs the FIRST check only after the | |
| # interval elapses, so 10s meant every job idled ~10s+ waiting on a database | |
| # that was ready in ~3s. pg_isready is cheap enough to poll every second. | |
| # `-h 127.0.0.1` forces TCP — the entrypoint's temporary initdb-phase server | |
| # listens only on the unix socket, so a plain pg_isready reports ready | |
| # before the real server is up. | |
| options: >- | |
| --mount type=tmpfs,destination=/var/lib/postgresql/data | |
| --health-cmd "pg_isready -h 127.0.0.1" | |
| --health-interval 1s | |
| --health-retries 60 | |
| redis: | |
| # 7-alpine: pinned to match the review-app host's redis:7 accessory, and | |
| # the alpine variant is a much smaller pull than debian-based latest. | |
| image: mirror.gcr.io/library/redis:7-alpine | |
| ports: | |
| - 6379:6379 | |
| options: --entrypoint redis-server | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| ci_node_total: [5] | |
| ci_node_index: [0, 1, 2, 3, 4] | |
| env: | |
| # `test` now runs inside the CI image (container:), so services are | |
| # sibling containers reachable by name, not localhost. | |
| REDIS_URL: redis://redis:6379 | |
| PGHOST: postgres | |
| RAILS_ENV: test | |
| TZ: "America/Chicago" | |
| COVERAGE: false # additional configuration is needed for parallelized tests | |
| CC_TEST_REPORTER_ID: 04daa6564351115dc1515504790cd379ad8dc25e7778f0641e0f8c63185f887c | |
| TRANSLATION_BRANCH: main | |
| TRANSLATIONS_SYNC_APP_ID: ${{ secrets.TRANSLATIONS_SYNC_APP_ID }} | |
| RETRY_FLAKY: true | |
| # spec/integration/register_spec.rb uploads to the bikeindex-test bucket for real - the | |
| # only way to exercise a presigned cross-origin PUT. Absent, that one example skips. | |
| R2_TEST_ACCESS_KEY: ${{ secrets.R2_TEST_ACCESS_KEY }} | |
| R2_TEST_ACCESS_KEY_SECRET: ${{ secrets.R2_TEST_ACCESS_KEY_SECRET }} | |
| R2_TEST_ENDPOINT: ${{ secrets.R2_TEST_ENDPOINT }} | |
| KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: ${{ secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC }} | |
| KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }} | |
| KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }} | |
| KNAPSACK_PRO_LOG_LEVEL: warn | |
| # Capybara binds to this port in `:js` specs (spec/support/capybara.rb). | |
| # Set explicitly so the assets:precompile step below can bake a matching | |
| # ENV['BASE_URL'] into any ERB-templated asset. | |
| CAPYBARA_PORT: 5042 | |
| steps: | |
| - name: Get CPU cores | |
| id: cpu-info | |
| run: echo "cpu-cores=$(nproc)" >> $GITHUB_OUTPUT | |
| # Checkout persists this App token (below) as the git creds so the sync | |
| # branch is pushed as the App — only a non-default-token push fires | |
| # `on: push`, which is what attaches the required checks to the PR. | |
| # Falls back to default creds until the App secrets exist. | |
| - name: Mint translations-sync token | |
| id: translations_token | |
| if: matrix.ci_node_index == 1 && github.ref == 'refs/heads/main' && env.TRANSLATIONS_SYNC_APP_ID != '' | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.TRANSLATIONS_SYNC_APP_ID }} | |
| private-key: ${{ secrets.TRANSLATIONS_SYNC_APP_PRIVATE_KEY }} | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| token: ${{ steps.translations_token.outputs.token || github.token }} | |
| # Ruby itself is baked into the CI image (.github/ci/Dockerfile) — | |
| # ruby/setup-ruby's own toolcache binary can't dynamically link inside | |
| # a minimal container. Gems still need their own cache: BUNDLE_PATH is | |
| # baked as /usr/local/bundle so this restores/saves the same directory | |
| # bundle install writes to. | |
| - name: Restore gem cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: /usr/local/bundle | |
| key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-gems- | |
| - run: bundle install --jobs 4 | |
| # :js system specs run through capybara-playwright-driver, which drives the | |
| # `playwright` npm package against the Chromium baked into the CI image | |
| # (.github/ci/Dockerfile) at PLAYWRIGHT_BROWSERS_PATH. | |
| - name: Set up Node | |
| uses: actions/setup-node@v7 | |
| with: | |
| cache: 'npm' | |
| - run: npm install | |
| - name: Set up database | |
| run: bin/rails db:create db:schema:load:primary db:schema:load:analytics db:migrate | |
| - name: Sync translations (only on main by default) | |
| if: matrix.ci_node_index == 1 | |
| env: | |
| GH_TOKEN: ${{ steps.translations_token.outputs.token || github.token }} | |
| run: bin/check_translations ${{ secrets.TRANSLATION_IO_API_KEY }} | |
| # Cache compiled assets + sprockets' incremental cache. | |
| # Key hashes every input the three asset tools read: | |
| # - sprockets: app/assets/images, javascripts, config/manifest.js, Gemfile.lock (gem versions) | |
| # - dartsass-rails: app/assets/stylesheets, config/initializers/dartsass.rb | |
| # - tailwindcss-rails: app/assets/tailwind (theme + @source globs live here), | |
| # and everything tailwind scans (views/components/helpers/javascript) | |
| # On a partial-key restore, sprockets' tmp/cache makes the rebuild near-instant | |
| # for unchanged inputs, so the loose restore-keys fallback is worth keeping. | |
| # | |
| # Split into restore/save: all matrix jobs restore, but only ci_node_index == 0 | |
| # writes the cache. With the combined actions/cache@v4 action, parallel jobs race | |
| # on reserveCache and *all* of them lose ("another job may be creating this cache"), | |
| # so the cache never gets populated. | |
| - name: Restore precompiled assets cache | |
| id: assets-cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: | | |
| tmp/cache/assets | |
| app/assets/builds | |
| public/assets | |
| # CAPYBARA_PORT is in the key because the precompile step bakes | |
| # `ENV['BASE_URL']` (derived from it) into ERB-templated assets; | |
| # a cached bundle from a different port would serve a stale URL. | |
| key: ${{ runner.os }}-assets-port${{ env.CAPYBARA_PORT }}-${{ hashFiles('app/assets/**', 'app/views/**', 'app/components/**', 'app/helpers/**/*.rb', 'app/javascript/**', 'config/initializers/dartsass.rb', 'config/initializers/assets.rb', 'Gemfile.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-assets-port${{ env.CAPYBARA_PORT }}- | |
| - name: build assets | |
| if: steps.assets-cache.outputs.cache-hit != 'true' | |
| # BASE_URL must match the host:port that Capybara binds to in tests | |
| # so any ERB-baked asset URL resolves to the same host the browser | |
| # will hit (see spec/support/capybara.rb). | |
| env: | |
| BASE_URL: http://localhost:${{ env.CAPYBARA_PORT }} | |
| run: bin/rails assets:precompile --trace | |
| - name: Save precompiled assets cache | |
| if: matrix.ci_node_index == 0 && steps.assets-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: | | |
| tmp/cache/assets | |
| app/assets/builds | |
| public/assets | |
| key: ${{ steps.assets-cache.outputs.cache-primary-key }} | |
| # Test bin/setup (verifies e.g. seeds work) - only run on 1 node, knapsack should balance | |
| # Done after asset compilation and caching to save time on that | |
| - name: Test bin/setup | |
| if: matrix.ci_node_index == 0 | |
| run: bin/setup | |
| # Run tests | |
| - name: Run tests | |
| env: | |
| SKIP_CSS_BUILD: true | |
| run: bin/knapsack_pro_tests | |
| # Until parallel reporting is set up, skip sending code coverage reports | |
| # - name: publish code coverage | |
| # uses: paambaati/codeclimate-action@v9.0.0 | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-${{ env.KNAPSACK_PRO_CI_NODE_INDEX }} | |
| # tmp/capybara holds the failure screenshots system specs write, which | |
| # are usually the only evidence of what a flaky :js spec actually saw | |
| path: | | |
| tmp/rspec-${{ env.KNAPSACK_PRO_CI_NODE_INDEX }}.xml | |
| tmp/capybara/ | |
| deploy_production: | |
| name: "Deploy to production" | |
| runs-on: ubuntu-latest | |
| needs: [test, lint_and_scan] | |
| if: github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Deploy to Cloud66 production | |
| run: | | |
| curl --insecure -X POST -d "" https://hooks.cloud66.com/stacks/redeploy/${{ secrets.CLOUD66_REDEPLOYMENT_KEY }} | |
| # Commenting out for now - it doesn't work for forked pull requests, | |
| # Which means forked PRs always fail | |
| # Also - I want to fix the thousands separator | |
| # github.com/EnricoMi/publish-unit-test-result-action/issues/698 | |
| # | |
| # aggregate_test_results: | |
| # name: "Test Report" | |
| # runs-on: ubuntu-latest | |
| # needs: test | |
| # if: always() | |
| # steps: | |
| # - name: Download all test results | |
| # uses: actions/download-artifact@v4 | |
| # with: | |
| # pattern: test-results-* | |
| # path: test-results | |
| # - name: Publish combined test results | |
| # id: test-results | |
| # uses: EnricoMi/publish-unit-test-result-action@v2 | |
| # with: | |
| # files: test-results/**/*.xml | |
| # json_thousands_separator: "," # Can't get this to work :/ | |
| # comment_mode: off |