From ae97d06697ce67c4bbf7f8881d0d49853c3419e8 Mon Sep 17 00:00:00 2001 From: mmeest Date: Tue, 8 Sep 2026 16:06:51 +0300 Subject: [PATCH 1/2] Ensure PR tests use Gemfile.lock gems and actually auto-merge patches. Automerge waited for checks then called gh pr merge --auto and swallowed failures, so ready patch PRs never merged. Test images and CI now freeze Bundler to the PR lockfile instead of a stale gems-latest snapshot. Co-authored-by: Cursor --- .github/workflows/auto-merge.yml | 34 +++++++++++++++++--------------- .github/workflows/ruby.yml | 21 ++++++++++++++++---- Dockerfile.gems | 5 ++++- Dockerfile.preinstalled_gems | 6 ++++++ Dockerfile.test | 12 ++++------- renovate.json | 2 +- 6 files changed, 50 insertions(+), 30 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 8e61af9fc3..553038d343 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -8,13 +8,15 @@ on: permissions: contents: write pull-requests: write + checks: read + statuses: read jobs: auto-approve-merge: + if: github.repository == 'internetee/registry' runs-on: ubuntu-latest steps: - name: Checkout repository - if: github.repository == 'internetee/registry' uses: actions/checkout@v6 - name: Install GitHub CLI @@ -24,14 +26,14 @@ jobs: - name: Auto approve PR if: | - github.actor == 'dependabot[bot]' || - github.actor == 'renovate[bot]' + github.event.pull_request.user.login == 'dependabot[bot]' || + github.event.pull_request.user.login == 'renovate[bot]' uses: hmarr/auto-approve-action@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Fetch Dependabot metadata - if: github.actor == 'dependabot[bot]' + if: github.event.pull_request.user.login == 'dependabot[bot]' id: metadata uses: dependabot/fetch-metadata@v1 with: @@ -40,10 +42,10 @@ jobs: - name: Check if PR should be auto-merged id: check_auto_merge run: | - # Set default UPDATE_TYPE to avoid unbound variable errors + PR_AUTHOR="${{ github.event.pull_request.user.login }}" UPDATE_TYPE="${{ steps.metadata.outputs.update-type || 'unknown' }}" - - if [ "${{ github.actor }}" == "dependabot[bot]" ]; then + + if [ "$PR_AUTHOR" == "dependabot[bot]" ]; then if [[ "$UPDATE_TYPE" == "version-update:semver-patch" ]]; then echo "auto_merge=true" >> $GITHUB_OUTPUT echo "update_type=${UPDATE_TYPE}" >> $GITHUB_OUTPUT @@ -53,8 +55,7 @@ jobs: echo "update_type=${UPDATE_TYPE}" >> $GITHUB_OUTPUT echo "Auto-merge: Dependabot non-patch update, skipping" fi - elif [ "${{ github.actor }}" == "renovate[bot]" ]; then - # Check if the PR has the 'patch' label but NOT 'minor' or 'major' labels + elif [ "$PR_AUTHOR" == "renovate[bot]" ]; then LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels -q '.labels[].name' | tr '\n' ' ') if [[ "$LABELS" == *"patch"* ]] && [[ "$LABELS" != *"minor"* ]] && [[ "$LABELS" != *"major"* ]]; then echo "auto_merge=true" >> $GITHUB_OUTPUT @@ -66,10 +67,10 @@ jobs: else echo "auto_merge=false" >> $GITHUB_OUTPUT echo "update_type=${UPDATE_TYPE}" >> $GITHUB_OUTPUT - echo "Auto-merge: Unknown actor, skipping" + echo "Auto-merge: Unknown author ${PR_AUTHOR}, skipping" fi env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash - name: Wait for CI checks @@ -79,15 +80,16 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 30 + running-workflow-name: Auto approve & merge Dependabot and Renovate PRs + check-regexp: '^test' + checks-discovery-timeout: 300 - name: Auto-merge PR if: steps.check_auto_merge.outputs.auto_merge == 'true' run: | echo "Attempting to auto-merge PR #${{ github.event.pull_request.number }}" - gh pr merge --auto --merge ${{ github.event.pull_request.number }} || { - echo "Auto-merge failed, but continuing..." - exit 0 - } + if ! gh pr merge --merge --auto ${{ github.event.pull_request.number }}; then + gh pr merge --merge ${{ github.event.pull_request.number }} + fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - \ No newline at end of file diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index c2154e0f2b..1a159c82f1 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -20,20 +20,33 @@ jobs: ruby: ['3.0.3'] runs-on: ${{ matrix.os }} continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }} + env: + BUNDLE_WITHOUT: "development:staging:production" + BUNDLE_FROZEN: "true" steps: - uses: actions/checkout@v6 + - name: Assert Ruby matches .ruby-version + run: | + expected=$(tr -d '[:space:]' < .ruby-version) + if [ "${{ matrix.ruby }}" != "$expected" ]; then + echo "CI ruby ${{ matrix.ruby }} does not match .ruby-version ($expected)" + exit 1 + fi + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - bundler-cache: true # runs 'bundle install' and caches installed gems automatically + bundler: Gemfile.lock + bundler-cache: true - - name: Config bundler + - name: Verify gems match Gemfile.lock run: | bundle config set without 'development staging production' - bundle config set deployment '[secure]' + bundle config set frozen true + bundle check bundle env - head -n1 $(which bundle) + bundle list - name: Run Tests env: diff --git a/Dockerfile.gems b/Dockerfile.gems index 86068c56f3..0a121a26bc 100644 --- a/Dockerfile.gems +++ b/Dockerfile.gems @@ -2,4 +2,7 @@ FROM internetee/ruby_base:3.0 LABEL org.opencontainers.image.source=https://github.com/internetee/registry COPY Gemfile Gemfile.lock ./ -RUN gem install bundler && bundle config set without 'development test' && bundle install --jobs 20 --retry 5 +RUN gem install bundler && \ + bundle config set without 'development test' && \ + bundle config set frozen true && \ + bundle install --jobs 20 --retry 5 diff --git a/Dockerfile.preinstalled_gems b/Dockerfile.preinstalled_gems index 0b86334220..c6f216df14 100644 --- a/Dockerfile.preinstalled_gems +++ b/Dockerfile.preinstalled_gems @@ -18,6 +18,12 @@ RUN touch /opt/ca/index.txt RUN chmod 776 /opt/ca/index.txt WORKDIR /opt/webapps/app +COPY Gemfile Gemfile.lock ./ +RUN gem install bundler && \ + bundle config set without 'development test' && \ + bundle config set frozen true && \ + bundle install --jobs 20 --retry 5 + COPY . . RUN bundle exec rails assets:precompile diff --git a/Dockerfile.test b/Dockerfile.test index c1509dd357..a5ff85a53e 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -5,8 +5,7 @@ FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /opt/webapps/app -# Set staging environment -ENV RAILS_ENV="staging" \ +ENV RAILS_ENV="test" \ RAILS_LOG_TO_STDOUT=true \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" @@ -52,10 +51,10 @@ ENV LANG=et_EE.UTF-8 # Install application gems COPY Gemfile Gemfile.lock ./ RUN gem install bundler && \ - bundle config set --local without 'development test' && \ + bundle config set --local without 'development staging production' && \ + bundle config set --local frozen true && \ bundle install && \ bundle clean --force && \ - # Remove gem cache to save space in final image (~50-100MB saved) rm -rf ${BUNDLE_PATH}/cache/*.gem # Copy application code @@ -66,13 +65,10 @@ RUN cp config/application.yml.sample config/application.yml && \ cp config/database.yml.sample config/database.yml # Precompile assets -RUN RAILS_ENV=staging SECRET_KEY_BASE=dummy_for_assets \ +RUN RAILS_ENV=test SECRET_KEY_BASE=dummy_for_assets \ ACTION_MAILER_DEFAULT_HOST=dummy.host \ ACTION_MAILER_DEFAULT_FROM=dummy@example.com \ bundle exec rails assets:precompile && \ - echo "Assets precompiled successfully for staging" && \ - ls -la public/assets/ | head -20 && \ - # Remove temporary cache generated by assets:precompile (~100MB+ saved) rm -rf tmp/cache/assets # Final stage for app image diff --git a/renovate.json b/renovate.json index c66fa35834..a0d11ef559 100644 --- a/renovate.json +++ b/renovate.json @@ -23,7 +23,7 @@ { "matchDepTypes": ["ruby", "bundler", "Gemfile", "Gemfile.lock"], "matchUpdateTypes": ["patch"], - "addLabels": ["bundler", "dependencies"], + "addLabels": ["bundler", "dependencies", "patch"], "automerge": false, "automergeType": "pr", "requiredStatusChecks": null From 15194b9b2f378ebd6a76c786a1f39880c918e539 Mon Sep 17 00:00:00 2001 From: mmeest Date: Tue, 8 Sep 2026 16:28:27 +0300 Subject: [PATCH 2/2] Send Dependabot auto-merge notifications to Mattermost like registrar_center2. The old workflow_run filter on master never fired because auto-merge runs on the PR branch. Notify on the actual merged bot PR with title, link and changelog, and only after a real merge. Co-authored-by: Cursor --- .github/workflows/auto-merge.yml | 39 ++++++++- .github/workflows/pr-merged-mattermost.yml | 98 ++++++++++++++++++++-- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 553038d343..fddd992f62 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -84,12 +84,43 @@ jobs: check-regexp: '^test' checks-discovery-timeout: 300 - - name: Auto-merge PR + - name: Check CI status if: steps.check_auto_merge.outputs.auto_merge == 'true' + id: ci_status + run: | + PR_NUMBER='${{ github.event.pull_request.number }}' + NON_SUCCESS=$(gh pr checks "$PR_NUMBER" --required --json name,state -q '.[] | select(.state != "SUCCESS") | "\(.name):\(.state)"' || true) + if [ -z "$NON_SUCCESS" ]; then + echo "ci_passed=true" >> $GITHUB_OUTPUT + else + echo "ci_passed=false" >> $GITHUB_OUTPUT + echo "CI checks failed or pending, skipping auto-merge" + echo "$NON_SUCCESS" + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-merge PR + if: steps.check_auto_merge.outputs.auto_merge == 'true' && steps.ci_status.outputs.ci_passed == 'true' run: | - echo "Attempting to auto-merge PR #${{ github.event.pull_request.number }}" - if ! gh pr merge --merge --auto ${{ github.event.pull_request.number }}; then - gh pr merge --merge ${{ github.event.pull_request.number }} + PR_NUMBER='${{ github.event.pull_request.number }}' + if gh pr merge --merge "$PR_NUMBER"; then + echo "Merged PR #$PR_NUMBER" + exit 0 + fi + STATE=$(gh pr view "$PR_NUMBER" --json state -q .state) + if [ "$STATE" = "MERGED" ]; then + echo "PR #$PR_NUMBER was already merged" + exit 0 + fi + gh pr merge --auto --merge "$PR_NUMBER" + STATE=$(gh pr view "$PR_NUMBER" --json state -q .state) + if [ "$STATE" = "MERGED" ]; then + echo "Merged PR #$PR_NUMBER via auto-merge" + exit 0 fi + echo "Failed to merge PR #$PR_NUMBER (state=$STATE)" >&2 + exit 1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-merged-mattermost.yml b/.github/workflows/pr-merged-mattermost.yml index ba8ca2fe3a..38088c52d7 100644 --- a/.github/workflows/pr-merged-mattermost.yml +++ b/.github/workflows/pr-merged-mattermost.yml @@ -1,37 +1,119 @@ -name: Notify Auto-Merged PRs +name: Notify Mattermost on merged PRs on: workflow_run: workflows: ["Auto approve & merge Dependabot and Renovate PRs"] types: [completed] + push: branches: [master] permissions: contents: read pull-requests: read + actions: read jobs: notify: - if: github.event.workflow_run.conclusion == 'success' + if: github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest + env: + GH_REPO: ${{ github.repository }} steps: + - name: Find merged bot PR + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + EVENT_NAME: ${{ github.event_name }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + RUN_STARTED: ${{ github.event.workflow_run.run_started_at }} + run: | + if [ "$EVENT_NAME" = "workflow_run" ]; then + PR=$(gh pr list --repo "$REPO" --head "$HEAD_BRANCH" --base master --state merged --json number,title,url,author,labels,mergedAt --jq '.[0] // empty') + if [ -n "$PR" ] && [ -n "$RUN_STARTED" ]; then + MERGED_AT=$(echo "$PR" | jq -r '.mergedAt // .merged_at') + MERGED_EPOCH=$(date -d "$MERGED_AT" +%s) + STARTED_EPOCH=$(date -d "$RUN_STARTED" +%s) + if [ "$MERGED_EPOCH" -lt $((STARTED_EPOCH - 60)) ]; then + echo "notify=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + else + if [ "${{ github.actor }}" = "github-actions[bot]" ]; then + echo "notify=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + PR=$(gh api "repos/${REPO}/commits/${SHA}/pulls" --jq '[.[] | select(.base.ref == "master" and .merged_at != null)] | .[0] // empty') + fi + if [ -z "$PR" ]; then + echo "notify=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + AUTHOR=$(echo "$PR" | jq -r '.user.login // .author.login') + IS_BOT=$(echo "$PR" | jq -r '.author.is_bot // (.user.type == "Bot") // false') + LABELS=$(echo "$PR" | jq -r '[.labels[].name] | join(" ")') + echo "author=$AUTHOR is_bot=$IS_BOT labels=$LABELS" + if [[ "$IS_BOT" == "true" ]] || [[ "$AUTHOR" == "dependabot" || "$AUTHOR" == "dependabot[bot]" || "$AUTHOR" == "app/dependabot" || "$AUTHOR" == "renovate" || "$AUTHOR" == "renovate[bot]" || "$AUTHOR" == "app/renovate" || "$AUTHOR" == *"[bot]" || "$AUTHOR" == app/* ]] || [[ "$LABELS" == *"auto-merge"* ]] || [[ "$LABELS" == *"patch"* ]] || [[ "$LABELS" == *"minor"* ]]; then + echo "notify=true" >> "$GITHUB_OUTPUT" + echo "author=$AUTHOR" >> "$GITHUB_OUTPUT" + echo "number=$(echo "$PR" | jq -r '.number')" >> "$GITHUB_OUTPUT" + echo "title=$(echo "$PR" | jq -r '.title')" >> "$GITHUB_OUTPUT" + echo "url=$(echo "$PR" | jq -r '.html_url // .url')" >> "$GITHUB_OUTPUT" + echo "merged_at=$(echo "$PR" | jq -r '.merged_at // .mergedAt')" >> "$GITHUB_OUTPUT" + else + echo "notify=false" >> "$GITHUB_OUTPUT" + fi + + - name: Build short changelog + if: steps.pr.outputs.notify == 'true' + id: changelog + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + run: | + TEXT=$(gh pr view "$PR_NUMBER" --repo "$GH_REPO" --json commits --jq '.commits[:10] | map("- " + .messageHeadline) | join("\n")') + if [ -z "$TEXT" ]; then + TEXT="No commit messages found" + fi + { + echo "text<> "$GITHUB_OUTPUT" + - name: Send Mattermost notification + if: steps.pr.outputs.notify == 'true' env: MATTERMOST_BOT_TOKEN: ${{ secrets.MATTERMOST_BOT_TOKEN }} MATTERMOST_CHANNEL_ID: ${{ secrets.MATTERMOST_CHANNEL_ID }} MATTERMOST_BASE_URL: ${{ secrets.MATTERMOST_BASE_URL }} REPO: ${{ github.repository }} - WORKFLOW_NAME: ${{ github.event.workflow_run.name }} - WORKFLOW_URL: ${{ github.event.workflow_run.html_url }} - COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} - BRANCH: ${{ github.event.workflow_run.head_branch }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_TITLE: ${{ steps.pr.outputs.title }} + PR_URL: ${{ steps.pr.outputs.url }} + MERGED_AT: ${{ steps.pr.outputs.merged_at }} + CHANGELOG: ${{ steps.changelog.outputs.text }} + MERGE_TYPE: ${{ steps.pr.outputs.author }} run: | if [ -z "$MATTERMOST_BOT_TOKEN" ] || [ -z "$MATTERMOST_CHANNEL_ID" ] || [ -z "$MATTERMOST_BASE_URL" ]; then echo "Missing Mattermost secrets (MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID, MATTERMOST_BASE_URL)" >&2 exit 1 fi - curl -sS -X POST \ + jq -n \ + --arg channel "$MATTERMOST_CHANNEL_ID" \ + --arg repo "$REPO" \ + --arg number "$PR_NUMBER" \ + --arg title "$PR_TITLE" \ + --arg merge_type "$MERGE_TYPE" \ + --arg merged_at "$MERGED_AT" \ + --arg url "$PR_URL" \ + --arg changelog "$CHANGELOG" \ + '{channel_id:$channel, message:("[" + $repo + "] PR #" + $number + ": \"" + $title + "\" was auto-merged by " + $merge_type + ".\nMerged at: " + $merged_at + "\nLink: " + $url + "\nChangelog:\n" + $changelog)}' \ + | curl -sS -X POST \ -H "Authorization: Bearer $MATTERMOST_BOT_TOKEN" \ -H "Content-Type: application/json" \ - -d "{\"channel_id\":\"$MATTERMOST_CHANNEL_ID\",\"message\":\"🤖 [$REPO] Auto-merge workflow completed successfully!\\nWorkflow: $WORKFLOW_NAME\\nBranch: $BRANCH\\nCommit: $COMMIT_SHA\\nLink: $WORKFLOW_URL\"}" \ + -d @- \ "$MATTERMOST_BASE_URL/api/v4/posts"