Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -79,15 +80,47 @@ 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
- name: Check CI status
if: steps.check_auto_merge.outputs.auto_merge == 'true'
id: ci_status
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..."
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: |
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 }}

98 changes: 90 additions & 8 deletions .github/workflows/pr-merged-mattermost.yml
Original file line number Diff line number Diff line change
@@ -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<<CHANGELOG_EOF"
echo "$TEXT"
echo "CHANGELOG_EOF"
} >> "$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"
21 changes: 17 additions & 4 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile.gems
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions Dockerfile.preinstalled_gems
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 4 additions & 8 deletions Dockerfile.test
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading