diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..4d7b470 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,114 @@ +name: Integration tests + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + types: + - opened + - reopened + - synchronize + - converted_to_draft + - ready_for_review + - labeled + - unlabeled + - auto_merge_enabled + - auto_merge_disabled + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pre_check: + uses: ./.github/workflows/pre-check-integration.yml + + matrix-test: + needs: pre_check + if: needs.pre_check.outputs.should_run == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + variation: [one, two] + timeout-minutes: 5 + steps: + - name: Get the behavior from the PR description + id: get-behavior + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + let behavior = '{}'; + if (context.payload.pull_request) { + const { body } = context.payload.pull_request; + const regex = /^\#matrix-test-${{ matrix.variation }}:\s+(\S+)/m; + const result = regex.exec(body); + if (result) { + behavior = result[1]; + } + } + console.log(behavior); + return behavior; + + - name: run matrix test + run: |> + sleep ${{ fromJSON(steps.get-behavior.outputs.result).sleep || 60 }} + exit ${{ fromJSON(steps.get-behavior.outputs.result).exitCode || 0 }} + continue-on-error: ${{ matrix.variation == 'two' }} + + standalone-test: + needs: pre_check + if: >- + needs.pre_check.outputs.should_run == 'true' && + ( + github.event_name != 'pull_request' || + !contains(github.event.pull_request.labels.*.name, 'skip:standalone') + ) + + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Get the behavior from the PR description + id: get-behavior + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + let behavior = '{}'; + if (context.payload.pull_request) { + const { body } = context.payload.pull_request; + const regex = /^\#standalone-test:\s+(\S+)/m; + const result = regex.exec(body); + if (result) { + behavior = result[1]; + } + } + console.log(behavior); + return behavior; + + - name: run standalone test + run: |> + sleep ${{ fromJSON(steps.get-behavior.outputs.result).sleep || 45 }} + exit ${{ fromJSON(steps.get-behavior.outputs.result).exitCode || 0 }} + + integration-test-result: + needs: + - pre_check + - matrix-test + - standalone-test + if: (needs.pre_check.outputs.should_run == 'true' && (success() || failure() || cancelled())) || needs.pre_check.outputs.previous_success == 'true' + runs-on: ubuntu-latest + steps: + - name: Check job results + shell: bash + run: | + cat <- + github.event_name != 'pull_request' || + github.event.pull_request.draft == true || + github.event.pull_request.base.ref != 'main' || ( + contains(github.event.pull_request.labels.*.name, 'automerge:squash') || + contains(github.event.pull_request.labels.*.name, 'automerge:no-update') || + contains(github.event.pull_request.labels.*.name, 'automerge:rebase') || + contains(github.event.pull_request.labels.*.name, 'bypass:automerge') || + github.event.pull_request.auto_merge != null + ) + strategy: + # abuse the matrix feature to create a check which stays pending until + # a merge strategy is chosen + matrix: + merge: [chosen] + steps: + - shell: bash + run: echo "Merge strategy chosen" + + linear-history: + runs-on: ubuntu-latest + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.draft == false && + github.event.pull_request.base.ref == 'main' && ( + contains(github.event.pull_request.labels.*.name, 'automerge:no-update') || + contains(github.event.pull_request.labels.*.name, 'bypass:automerge') + ) && + !contains(github.event.pull_request.labels.*.name, 'bypass:linear-history') + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - shell: bash + run: | + merge_commits=$(git rev-list --merges "origin/$GITHUB_BASE_REF".."origin/$GITHUB_HEAD_REF") + + if [ -n "$merge_commits" ]; then + echo "Error: merge commits found in $GITHUB_BASE_REF..$GITHUB_HEAD_REF" + + for merge_commit in $merge_commits; do + echo "$merge_commit" + done + + exit 1 + fi + + fixup_commits= + for commit in $(git rev-list $GITHUB_BASE_REF..$GITHUB_HEAD_REF); do + case $(git show --pretty=format:%s -s $commit) in fixup\!*|squash\!*) + fixup_commits="$fixup_commits\n$commit" + ;; + esac + done + + if [ -n "$fixup_commits" ]; then + echo "Error: fixup/squash commits found in $GITHUB_BASE_REF..$GITHUB_HEAD_REF" + echo -e "$fixup_commits" + exit 1 + fi diff --git a/.github/workflows/normal.yml b/.github/workflows/normal.yml new file mode 100644 index 0000000..613b528 --- /dev/null +++ b/.github/workflows/normal.yml @@ -0,0 +1,39 @@ +name: Normal tests + +on: + pull_request: + merge_group: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + normal-test: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Get the behavior from the PR description + id: get-behavior + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + let behavior = '{}'; + if (context.payload.pull_request) { + const { body } = context.payload.pull_request; + const regex = /^\#normal-test:\s+(\S+)/m; + const result = regex.exec(body); + if (result) { + behavior = result[1]; + } + } + console.log(behavior); + return behavior; + + - name: run normal test + run: |> + sleep ${{ fromJSON(steps.get-behavior.outputs.result).sleep || 45 }} + exit ${{ fromJSON(steps.get-behavior.outputs.result).exitCode || 0 }} diff --git a/.github/workflows/pre-check-integration.yml b/.github/workflows/pre-check-integration.yml new file mode 100644 index 0000000..e6012a3 --- /dev/null +++ b/.github/workflows/pre-check-integration.yml @@ -0,0 +1,75 @@ +name: Pre-check Integration Test + +on: + workflow_call: + outputs: + should_run: + description: "'true' if the test should run" + value: "${{ jobs.check_and_cancel.outcome != 'skipped' && jobs.check_and_cancel.outputs.should_skip != 'true' }}" + previous_success: + description: "'true' if should not run because of a previous successful run" + value: "${{ jobs.check_and_cancel.outcome != 'skipped' && jobs.check_and_cancel.outputs.previous_success == 'true' }}" + merge_requested: + description: "'true' if pull_request was requested for merge" + value: >- + ${{ + github.event_name != 'pull_request' || ( + github.event.pull_request.base.ref == 'main' && + github.event.pull_request.draft == false && + ( + contains(github.event.pull_request.labels.*.name, 'automerge:squash') || + contains(github.event.pull_request.labels.*.name, 'automerge:no-update') || + contains(github.event.pull_request.labels.*.name, 'automerge:rebase') || + github.event.pull_request.auto_merge != null + ) + ) + }} + +jobs: + check_and_cancel: + name: Check preconditions and cancel previous jobs + if: >- + github.event_name != 'pull_request' || + contains(github.event.pull_request.labels.*.name, 'force:integration') || ( + github.event.pull_request.base.ref == 'main' && + github.event.pull_request.draft == false && + ( + contains(github.event.pull_request.labels.*.name, 'automerge:squash') || + contains(github.event.pull_request.labels.*.name, 'automerge:no-update') || + contains(github.event.pull_request.labels.*.name, 'automerge:rebase') || + github.event.pull_request.auto_merge != null + ) && + !contains(github.event.pull_request.labels.*.name, 'bypass:integration') + ) + runs-on: ubuntu-latest + outputs: + should_skip: ${{ (steps.step2.outcome == 'skipped' || steps.step2.outputs.concurrent_conclusion == 'success') && steps.step1.outputs.should_skip || 'false' }} + previous_success: >- + ${{ + (steps.step2.outcome == 'skipped' && steps.step1.outputs.reason == 'skip_after_successful_duplicate' && 'true') || + (steps.step2.outputs.concurrent_conclusion == 'success' && 'true') || + 'false' + }} + steps: + - id: step1 + uses: fkirc/skip-duplicate-actions@v5 + with: + cancel_others: "${{ github.event_name == 'pull_request' }}" + concurrent_skipping: 'same_content_newer' + - id: step2 + name: Wait for concurrent run conclusion + if: >- + steps.step1.outputs.should_skip == 'true' && + steps.step1.outputs.reason == 'concurrent_skipping' && + fromJSON(steps.step1.outputs.skipped_by).status != 'completed' + run: | + while : ; do + conclusion="$(curl --fail --silent \ + --url https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ fromJSON(steps.step1.outputs.skipped_by).id }} \ + --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'content-type: application/json' \ + | jq -r '.conclusion')" + [ "$conclusion" != "null" ] && break + sleep 10 + done + echo "concurrent_conclusion=$conclusion" >> $GITHUB_OUTPUT diff --git a/README.md b/README.md index b7ff130..323ee3e 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# mergify-experiements \ No newline at end of file +# mergify-experiements + +Repository to experiment with mergify configuration \ No newline at end of file diff --git a/mergify.yml b/mergify.yml new file mode 100644 index 0000000..3f15bf4 --- /dev/null +++ b/mergify.yml @@ -0,0 +1,48 @@ +# Linear queue for the main branch. +queue_rules: + - name: main + conditions: + - base=main + # Require integration tests before merging only + - or: + - label=bypass:integration + - check-success=integration-test-result + +pull_request_rules: + - name: merge to main + conditions: + - base=main + - label=automerge:no-update + - or: + - '#commits-behind=0' + - label=bypass:linear-history + - or: + - check-success=wait-integration-pre-checks + - label=bypass:integration + actions: + queue: + name: main + method: merge + - name: rebase updates then merge to main + conditions: + - base=main + - label=automerge:rebase + - or: + - check-success=wait-integration-pre-checks + - label=bypass:integration + actions: + queue: + name: main + method: merge + update_method: rebase + - name: squash to main + conditions: + - base=main + - label=automerge:squash + - or: + - check-success=wait-integration-pre-checks + - label=bypass:integration + actions: + queue: + name: main + method: squash