Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
126 changes: 126 additions & 0 deletions .github/actions/ensure-master-docs-safety/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Ensure master docs-only skips are safe
description: Fails if the previous master commit still has failing workflow runs when current push is docs-only
inputs:
docs-only:
description: 'String output from ci-changes-detector ("true" or "false")'
required: true
previous-sha:
description: 'SHA of the previous commit on master (github.event.before)'
required: true
runs:
using: composite
steps:
- name: Check previous master commit status
if: ${{ inputs.docs-only == 'true' && inputs.previous-sha != '' && inputs.previous-sha != '0000000000000000000000000000000000000000' }}
uses: actions/github-script@v7
env:
PREVIOUS_SHA: ${{ inputs.previous-sha }}
with:
script: |
const previousSha = process.env.PREVIOUS_SHA;

// Query workflow runs from the last 7 days to avoid excessive API calls.
// Why 7 days? This balances API efficiency with practical needs:
// - Most master commits trigger CI within hours, not days
// - Commits older than 7 days are likely stale; better to run full CI anyway
// - Reduces pagination load on high-velocity repos
// For commits outside this window, we skip the check and allow the docs-only skip.
const createdAfter = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();

// Optimize pagination: use lower per_page and collect only what we need
const workflowRuns = [];
let foundAllRelevant = false;

for await (const response of github.paginate.iterator(
github.rest.actions.listWorkflowRunsForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
branch: 'master',
event: 'push',
per_page: 30,
created: `>${createdAfter}`
}
)) {
const pageRuns = response.data;
const relevantInPage = pageRuns.filter((run) => run.head_sha === previousSha);

if (relevantInPage.length > 0) {
workflowRuns.push(...relevantInPage);
}

// Early exit: if we found relevant runs and now seeing different SHAs,
// we've likely collected all runs for the previous commit
if (workflowRuns.length > 0 && relevantInPage.length === 0) {
foundAllRelevant = true;
break;
}
}

if (workflowRuns.length === 0) {
core.info(`No workflow runs found for ${previousSha} in the last 7 days. Allowing docs-only skip.`);
return;
}

// Deduplicate workflow runs by keeping only the latest run for each workflow_id.
// This handles cases where workflows are re-run manually.
// Use run_number as tiebreaker since created_at might be identical for rapid reruns.
const latestByWorkflow = new Map();
for (const run of workflowRuns) {
const existing = latestByWorkflow.get(run.workflow_id);
if (!existing || run.run_number > existing.run_number) {
latestByWorkflow.set(run.workflow_id, run);
}
}

// Check for workflows that are still running
// We require all workflows to complete before allowing docs-only skip
// This prevents skipping CI when the previous commit hasn't been fully validated
const incompleteRuns = Array.from(latestByWorkflow.values()).filter(
(run) => run.status !== 'completed'
);

if (incompleteRuns.length > 0) {
const details = incompleteRuns
.map((run) => `- [${run.name} #${run.run_number}](${run.html_url}) is still ${run.status}`)
.join('\n');
core.setFailed(
[
`Cannot skip CI for docs-only commit because previous master commit ${previousSha} still has running workflows:`,
details,
'',
'Wait for these workflows to complete before pushing docs-only changes.'
].join('\n')
);
return;
}

// Check for workflows that failed on the previous commit.
// We treat these conclusions as failures:
// - 'failure': Obvious failure case
// - 'timed_out': Infrastructure or performance issue that should be investigated
// - 'cancelled': Conservative - might indicate timeout or manual intervention needed
// - 'action_required': Requires manual intervention
// We treat 'skipped' and 'neutral' as non-blocking since they indicate
// intentional skips or informational-only workflows.
const failingRuns = Array.from(latestByWorkflow.values()).filter((run) => {
return ['failure', 'timed_out', 'cancelled', 'action_required'].includes(run.conclusion);
});

if (failingRuns.length === 0) {
core.info(`Previous master commit ${previousSha} completed without failures. Docs-only skip allowed.`);
return;
}

const details = failingRuns
.map((run) => `- [${run.name} #${run.run_number}](${run.html_url}) concluded ${run.conclusion}`)
.join('\n');

core.setFailed(
[
`Cannot skip CI for docs-only commit because previous master commit ${previousSha} still has failing workflows:`,
details,
'',
'Fix these failures before pushing docs-only changes, or push non-docs changes to trigger full CI.'
].join('\n')
);
23 changes: 10 additions & 13 deletions .github/workflows/detect-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ on:
jobs:
detect:
runs-on: ubuntu-22.04
permissions:
contents: read
actions: read
outputs:
docs_only: ${{ steps.changes.outputs.docs_only }}
run_lint: ${{ steps.changes.outputs.run_lint }}
Expand All @@ -41,17 +44,11 @@ jobs:
- name: Detect changes
id: changes
run: |
# For master branch, always run everything
if [ "${{ github.ref }}" = "refs/heads/master" ]; then
echo "docs_only=false" >> "$GITHUB_OUTPUT"
echo "run_lint=true" >> "$GITHUB_OUTPUT"
echo "run_ruby_tests=true" >> "$GITHUB_OUTPUT"
echo "run_js_tests=true" >> "$GITHUB_OUTPUT"
echo "run_dummy_tests=true" >> "$GITHUB_OUTPUT"
echo "run_generators=true" >> "$GITHUB_OUTPUT"
exit 0
fi

# For PRs, analyze changes
BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before }}"
BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before || 'origin/master' }}"
script/ci-changes-detector "$BASE_SHA"
- name: Guard docs-only master pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: ./.github/actions/ensure-master-docs-safety
with:
docs-only: ${{ steps.changes.outputs.docs_only }}
previous-sha: ${{ github.event.before }}
21 changes: 19 additions & 2 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- 'master'
# Never skip on master - always run full test suite
# Always trigger on master; docs-only detection handles skipping heavy jobs
pull_request:
paths-ignore:
- '**.md'
Expand All @@ -19,6 +19,9 @@ on:

jobs:
detect-changes:
permissions:
contents: read
actions: read
runs-on: ubuntu-22.04
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
Expand Down Expand Up @@ -54,6 +57,12 @@ jobs:
BASE_REF="${{ github.event.pull_request.base.sha || github.event.before || 'origin/master' }}"
script/ci-changes-detector "$BASE_REF"
shell: bash
- name: Guard docs-only master pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: ./.github/actions/ensure-master-docs-safety
with:
docs-only: ${{ steps.detect.outputs.docs_only }}
previous-sha: ${{ github.event.before }}

setup-matrix:
needs: detect-changes
Expand All @@ -79,7 +88,15 @@ jobs:
needs: [detect-changes, setup-matrix]
# Run on master, workflow_dispatch, OR when generators needed
if: |
github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.run_generators == 'true'
!(
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
needs.detect-changes.outputs.docs_only == 'true'
) && (
github.ref == 'refs/heads/master' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.run_generators == 'true'
)
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
Expand Down
24 changes: 21 additions & 3 deletions .github/workflows/gem-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- 'master'
# Never skip on master - always run full test suite
# Always trigger on master; docs-only detection handles skipping heavy jobs
pull_request:
paths-ignore:
- '**.md'
Expand All @@ -21,6 +21,9 @@ on:

jobs:
detect-changes:
permissions:
contents: read
actions: read
runs-on: ubuntu-22.04
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
Expand Down Expand Up @@ -56,6 +59,12 @@ jobs:
BASE_REF="${{ github.event.pull_request.base.sha || github.event.before || 'origin/master' }}"
script/ci-changes-detector "$BASE_REF"
shell: bash
- name: Guard docs-only master pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: ./.github/actions/ensure-master-docs-safety
with:
docs-only: ${{ steps.detect.outputs.docs_only }}
previous-sha: ${{ github.event.before }}

setup-gem-tests-matrix:
needs: detect-changes
Expand All @@ -77,9 +86,18 @@ jobs:

rspec-package-tests:
needs: [detect-changes, setup-gem-tests-matrix]
# Run on master OR when Ruby tests needed on PR
# Skip only if: master push AND docs-only changes
# Otherwise run if: on master OR Ruby tests needed
# This allows docs-only commits to skip heavy jobs while ensuring full CI on master for code changes
if: |
(github.ref == 'refs/heads/master' || needs.detect-changes.outputs.run_ruby_tests == 'true')
!(
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
needs.detect-changes.outputs.docs_only == 'true'
) && (
github.ref == 'refs/heads/master' ||
needs.detect-changes.outputs.run_ruby_tests == 'true'
)
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup-gem-tests-matrix.outputs.matrix) }}
Expand Down
37 changes: 32 additions & 5 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- 'master'
# Never skip on master - always run full test suite
# Always trigger on master; docs-only detection handles skipping heavy jobs
pull_request:
paths-ignore:
- '**.md'
Expand All @@ -20,6 +20,9 @@ on:

jobs:
detect-changes:
permissions:
contents: read
actions: read
runs-on: ubuntu-22.04
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
Expand Down Expand Up @@ -51,9 +54,15 @@ jobs:
echo "docs_only=false" >> "$GITHUB_OUTPUT"
else
BASE_REF="${{ github.event.pull_request.base.sha || github.event.before || 'origin/master' }}"
script/ci-changes-detector "$BASE_REF"
script/ci-changes-detector "$BASE_REF"
fi
shell: bash
- name: Guard docs-only master pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: ./.github/actions/ensure-master-docs-safety
with:
docs-only: ${{ steps.detect.outputs.docs_only }}
previous-sha: ${{ github.event.before }}

setup-integration-matrix:
needs: detect-changes
Expand All @@ -77,9 +86,19 @@ jobs:

build-dummy-app-webpack-test-bundles:
needs: [detect-changes, setup-integration-matrix]
# Run on master, workflow_dispatch, OR when tests needed on PR
# Skip only if: master push AND docs-only changes
# Otherwise run if: on master OR workflow_dispatch OR dummy tests needed
# This allows docs-only commits to skip heavy jobs while ensuring full CI on master for code changes
if: |
github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.run_dummy_tests == 'true'
!(
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
needs.detect-changes.outputs.docs_only == 'true'
) && (
github.ref == 'refs/heads/master' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.run_dummy_tests == 'true'
)
strategy:
matrix: ${{ fromJson(needs.setup-integration-matrix.outputs.matrix) }}
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -154,7 +173,15 @@ jobs:
needs: [detect-changes, setup-integration-matrix, build-dummy-app-webpack-test-bundles]
# Run on master, workflow_dispatch, OR when tests needed on PR
if: |
github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.run_dummy_tests == 'true'
!(
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
needs.detect-changes.outputs.docs_only == 'true'
) && (
github.ref == 'refs/heads/master' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.run_dummy_tests == 'true'
)
strategy:
matrix: ${{ fromJson(needs.setup-integration-matrix.outputs.matrix) }}
runs-on: ubuntu-22.04
Expand Down
24 changes: 22 additions & 2 deletions .github/workflows/lint-js-and-ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- 'master'
# Never skip on master - always run full test suite
# Always trigger on master; docs-only detection handles skipping heavy jobs
pull_request:
paths-ignore:
- '**.md'
Expand All @@ -20,6 +20,9 @@ on:

jobs:
detect-changes:
permissions:
contents: read
actions: read
runs-on: ubuntu-22.04
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
Expand Down Expand Up @@ -54,10 +57,27 @@ jobs:
BASE_REF="${{ github.event.pull_request.base.sha || github.event.before || 'origin/master' }}"
script/ci-changes-detector "$BASE_REF"
shell: bash
- name: Guard docs-only master pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: ./.github/actions/ensure-master-docs-safety
with:
docs-only: ${{ steps.detect.outputs.docs_only }}
previous-sha: ${{ github.event.before }}

build:
needs: detect-changes
if: github.ref == 'refs/heads/master' || needs.detect-changes.outputs.run_lint == 'true'
# Skip only if: master push AND docs-only changes
# Otherwise run if: on master OR lint needed
# This allows docs-only commits to skip linting while ensuring full CI on master for code changes
if: |
!(
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
needs.detect-changes.outputs.docs_only == 'true'
) && (
github.ref == 'refs/heads/master' ||
needs.detect-changes.outputs.run_lint == 'true'
)
env:
BUNDLE_FROZEN: true
runs-on: ubuntu-22.04
Expand Down
Loading
Loading