diff --git a/.github/actions/check-full-ci-label/action.yml b/.github/actions/check-full-ci-label/action.yml new file mode 100644 index 0000000000..60a37e44c7 --- /dev/null +++ b/.github/actions/check-full-ci-label/action.yml @@ -0,0 +1,29 @@ +name: Check for full-ci label +description: Checks if the PR has the full-ci label to enable full CI mode +outputs: + result: + description: 'Whether the full-ci label is present (string: "true" or "false")' + value: ${{ steps.check.outputs.result }} +runs: + using: composite + steps: + - name: Check for full-ci label + id: check + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + // Only check labels on pull requests + if (!context.payload.pull_request) { + return 'false'; + } + + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number + }); + + const hasLabel = labels.some(label => label.name === 'full-ci'); + console.log(`full-ci label present: ${hasLabel}`); + return hasLabel ? 'true' : 'false'; diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 455d4cdbb3..6a639a2b6b 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -29,17 +29,21 @@ jobs: run_ruby_tests: ${{ steps.detect.outputs.run_ruby_tests }} run_dummy_tests: ${{ steps.detect.outputs.run_dummy_tests }} run_generators: ${{ steps.detect.outputs.run_generators }} + has_full_ci_label: ${{ steps.check-label.outputs.result }} steps: - uses: actions/checkout@v4 with: # Fetch enough history for change detection (50 commits is usually sufficient for PRs) fetch-depth: 50 persist-credentials: false + - name: Check for full-ci label + id: check-label + uses: ./.github/actions/check-full-ci-label - name: Detect relevant changes id: detect run: | - # If force_run is true, run everything - if [ "${{ inputs.force_run }}" = "true" ]; then + # If force_run is true OR full-ci label is present, run everything + if [ "${{ inputs.force_run }}" = "true" ] || [ "${{ steps.check-label.outputs.result }}" = "true" ]; then echo "run_lint=true" >> "$GITHUB_OUTPUT" echo "run_js_tests=true" >> "$GITHUB_OUTPUT" echo "run_ruby_tests=true" >> "$GITHUB_OUTPUT" @@ -69,9 +73,9 @@ jobs: - ruby-version: '3.2' dependency-level: 'minimum' exclude: - # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run) - - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && '3.2' || '' }} - dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && 'minimum' || '' }} + # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run/full-ci label) + - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && '3.2' || '' }} + dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && 'minimum' || '' }} env: SKIP_YARN_COREPACK_CHECK: 0 BUNDLE_FROZEN: ${{ matrix.dependency-level == 'minimum' && 'false' || 'true' }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b02d517ba..8983bbe9fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,17 +29,21 @@ jobs: run_ruby_tests: ${{ steps.detect.outputs.run_ruby_tests }} run_dummy_tests: ${{ steps.detect.outputs.run_dummy_tests }} run_generators: ${{ steps.detect.outputs.run_generators }} + has_full_ci_label: ${{ steps.check-label.outputs.result }} steps: - uses: actions/checkout@v4 with: # Fetch enough history for change detection (50 commits is usually sufficient for PRs) fetch-depth: 50 persist-credentials: false + - name: Check for full-ci label + id: check-label + uses: ./.github/actions/check-full-ci-label - name: Detect relevant changes id: detect run: | - # If force_run is true, run everything - if [ "${{ inputs.force_run }}" = "true" ]; then + # If force_run is true OR full-ci label is present, run everything + if [ "${{ inputs.force_run }}" = "true" ] || [ "${{ steps.check-label.outputs.result }}" = "true" ]; then echo "run_lint=true" >> "$GITHUB_OUTPUT" echo "run_js_tests=true" >> "$GITHUB_OUTPUT" echo "run_ruby_tests=true" >> "$GITHUB_OUTPUT" @@ -70,10 +74,10 @@ jobs: node-version: '20' dependency-level: 'minimum' exclude: - # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run) - - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && '3.2' || '' }} - node-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && '20' || '' }} - dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && 'minimum' || '' }} + # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run/full-ci label) + - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && '3.2' || '' }} + node-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && '20' || '' }} + dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && 'minimum' || '' }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -160,10 +164,10 @@ jobs: node-version: '20' dependency-level: 'minimum' exclude: - # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run) - - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && '3.2' || '' }} - node-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && '20' || '' }} - dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && 'minimum' || '' }} + # Skip minimum dependency matrix on regular PRs (run only on master/workflow_dispatch/force_run/full-ci label) + - ruby-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && '3.2' || '' }} + node-version: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && '20' || '' }} + dependency-level: ${{ github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && inputs.force_run != true && needs.detect-changes.outputs.has_full_ci_label != 'true' && 'minimum' || '' }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pr-welcome-message.yml b/.github/workflows/pr-welcome-message.yml new file mode 100644 index 0000000000..4d00fca5cb --- /dev/null +++ b/.github/workflows/pr-welcome-message.yml @@ -0,0 +1,47 @@ +name: PR Welcome Message + +on: + pull_request: + types: [opened] + +jobs: + welcome: + runs-on: ubuntu-22.04 + permissions: + issues: write + pull-requests: write + steps: + - name: Post welcome message + uses: actions/github-script@v7 + with: + script: | + const welcomeMessage = [ + '👋 **Thanks for your contribution!**', + '', + 'This PR will run CI tests based on the files you changed. If some tests are skipped and you want to run the full test suite (including minimum dependency tests), you can use these commands:', + '', + '### CI Control Commands', + '', + '- **`/run-skipped-ci`** - Runs all skipped CI checks and enables full CI mode for this PR', + ' - Adds the `full-ci` label to ensure future commits also run the full test suite', + ' - Useful when you want comprehensive testing across all configurations', + '', + '- **`/stop-run-skipped-ci`** - Disables full CI mode and returns to standard CI', + ' - Removes the `full-ci` label', + ' - Future commits will only run tests for changed files', + '', + '**Note:**', + '- These commands require write access to the repository', + '- The `full-ci` label is preserved on merged PRs as a historical record', + '', + 'View CI progress in the [Actions tab](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions).' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: welcomeMessage + }); + + console.log('✅ Posted welcome message to PR'); diff --git a/.github/workflows/pro-integration-tests.yml b/.github/workflows/pro-integration-tests.yml index a8c0a149db..05f58fff1a 100644 --- a/.github/workflows/pro-integration-tests.yml +++ b/.github/workflows/pro-integration-tests.yml @@ -24,17 +24,21 @@ jobs: docs_only: ${{ steps.detect.outputs.docs_only }} run_pro_lint: ${{ steps.detect.outputs.run_pro_lint }} run_pro_tests: ${{ steps.detect.outputs.run_pro_tests }} + has_full_ci_label: ${{ steps.check-label.outputs.result }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false + - name: Check for full-ci label + id: check-label + uses: ./.github/actions/check-full-ci-label - name: Detect relevant changes id: detect working-directory: . run: | - # If force_run is true, run everything - if [ "${{ inputs.force_run }}" = "true" ]; then + # If force_run is true OR full-ci label is present, run everything + if [ "${{ inputs.force_run }}" = "true" ] || [ "${{ steps.check-label.outputs.result }}" = "true" ]; then echo "run_pro_lint=true" >> "$GITHUB_OUTPUT" echo "run_pro_tests=true" >> "$GITHUB_OUTPUT" echo "docs_only=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/pro-lint.yml b/.github/workflows/pro-lint.yml index 92be479934..41b10e2f21 100644 --- a/.github/workflows/pro-lint.yml +++ b/.github/workflows/pro-lint.yml @@ -24,17 +24,21 @@ jobs: docs_only: ${{ steps.detect.outputs.docs_only }} run_pro_lint: ${{ steps.detect.outputs.run_pro_lint }} run_pro_tests: ${{ steps.detect.outputs.run_pro_tests }} + has_full_ci_label: ${{ steps.check-label.outputs.result }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false + - name: Check for full-ci label + id: check-label + uses: ./.github/actions/check-full-ci-label - name: Detect relevant changes id: detect working-directory: . run: | - # If force_run is true, run everything - if [ "${{ inputs.force_run }}" = "true" ]; then + # If force_run is true OR full-ci label is present, run everything + if [ "${{ inputs.force_run }}" = "true" ] || [ "${{ steps.check-label.outputs.result }}" = "true" ]; then echo "run_pro_lint=true" >> "$GITHUB_OUTPUT" echo "run_pro_tests=true" >> "$GITHUB_OUTPUT" echo "docs_only=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/pro-package-tests.yml b/.github/workflows/pro-package-tests.yml index 13a3dfe5f4..99afb186d3 100644 --- a/.github/workflows/pro-package-tests.yml +++ b/.github/workflows/pro-package-tests.yml @@ -24,17 +24,21 @@ jobs: docs_only: ${{ steps.detect.outputs.docs_only }} run_pro_lint: ${{ steps.detect.outputs.run_pro_lint }} run_pro_tests: ${{ steps.detect.outputs.run_pro_tests }} + has_full_ci_label: ${{ steps.check-label.outputs.result }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false + - name: Check for full-ci label + id: check-label + uses: ./.github/actions/check-full-ci-label - name: Detect relevant changes id: detect working-directory: . run: | - # If force_run is true, run everything - if [ "${{ inputs.force_run }}" = "true" ]; then + # If force_run is true OR full-ci label is present, run everything + if [ "${{ inputs.force_run }}" = "true" ] || [ "${{ steps.check-label.outputs.result }}" = "true" ]; then echo "run_pro_lint=true" >> "$GITHUB_OUTPUT" echo "run_pro_tests=true" >> "$GITHUB_OUTPUT" echo "docs_only=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/run-skipped-ci.yml b/.github/workflows/run-skipped-ci.yml index e589cf9a43..688c62f111 100644 --- a/.github/workflows/run-skipped-ci.yml +++ b/.github/workflows/run-skipped-ci.yml @@ -216,17 +216,34 @@ jobs: const notFoundList = notFound.length > 0 ? `\n\n**Triggered but not yet queued (may still start):**\n${notFound.map(w => `- âŗ ${w.name}`).join('\n')}` : ''; const failedList = failed.length > 0 ? `\n\n**Failed to trigger:**\n${failed.map(f => `- ❌ ${f.workflow}: ${f.error}`).join('\n')}` : ''; - const body = `🚀 **Skipped CI Checks - Trigger Results** + // Add full-ci label only if we actually triggered workflows or if checks are already running + let labelAdded = false; + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['full-ci'] + }); + labelAdded = true; + console.log('✅ Added full-ci label to PR'); + } catch (error) { + console.error('âš ī¸ Failed to add label:', error.message); + } + + const body = `🚀 **Full CI Mode Enabled** ${status} ${skippedChecksList} ${verifiedList}${notFoundList}${failedList} - ${verified.length > 0 ? `\n**Note:** These workflows will run with \`force_run: true\` to bypass detect-changes logic that caused them to skip. + ${labelAdded && verified.length > 0 ? `\n**Note:** Added the \`full-ci\` label to this PR. All future commits will run the full CI suite (including minimum dependency tests) until the label is removed. + + To disable full CI mode, use the \`/stop-run-skipped-ci\` command. View progress in the [Actions tab](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions).` : ''} - ${notApplicable.length > 0 ? `\nAll CI checks are already running on this PR. Use this command when you see skipped checks that you want to run.` : ''}`; + ${labelAdded && notApplicable.length > 0 ? `\nAll CI checks are already running on this PR. Added the \`full-ci\` label - future commits will run the full CI suite.` : ''}`; // Post the comment await github.rest.issues.createComment({ diff --git a/.github/workflows/stop-run-skipped-ci.yml b/.github/workflows/stop-run-skipped-ci.yml new file mode 100644 index 0000000000..ae06fb471c --- /dev/null +++ b/.github/workflows/stop-run-skipped-ci.yml @@ -0,0 +1,126 @@ +name: Stop Full CI Suite + +on: + issue_comment: + types: [created] + +jobs: + stop-full-ci: + # Only run on PR comments that match the command + if: | + github.event.issue.pull_request && + ( + startsWith(github.event.comment.body, '/stop-run-skipped-ci') || + contains(github.event.comment.body, '\n/stop-run-skipped-ci') + ) + runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: write + issues: write + steps: + - name: Check if user has write access + id: check_access + uses: actions/github-script@v7 + with: + script: | + try { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor + }); + + const hasAccess = ['admin', 'write'].includes(permission.permission); + console.log(`User ${context.actor} has permission: ${permission.permission}`); + + if (!hasAccess) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.actor} Sorry, only repository collaborators with write access can stop full CI runs. 🔒` + }); + } + + return hasAccess; + } catch (error) { + console.error('Error checking permissions:', error); + return false; + } + + - name: Exit if no access + if: steps.check_access.outputs.result == 'false' + run: | + echo "User does not have permission to stop full CI" + exit 1 + + - name: Add reaction to comment + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ github.event.comment.id }} + reactions: 'eyes' + + - name: Remove full-ci label + uses: actions/github-script@v7 + with: + script: | + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'full-ci' + }); + console.log('✅ Removed full-ci label from PR'); + + // Post success comment + const successBody = [ + '✅ **Full CI Mode Disabled**', + '', + 'The `full-ci` label has been removed. Future commits will use the standard CI suite (skipping tests for unchanged code).', + '', + 'To re-enable full CI mode, use the `/run-skipped-ci` command.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: successBody + }); + } catch (error) { + if (error.status === 404) { + console.log('â„šī¸ Label not found - already removed or never added'); + + const notFoundBody = [ + 'â„šī¸ **Full CI Mode Already Disabled**', + '', + 'The `full-ci` label is not present on this PR. CI is already running in standard mode.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: notFoundBody + }); + } else { + console.error('❌ Failed to remove label:', error); + + const errorBody = [ + '❌ **Error Removing Label**', + '', + 'Failed to remove the `full-ci` label: ' + error.message + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: errorBody + }); + // Use core.setFailed instead of throw to properly fail the workflow + core.setFailed(`Failed to remove label: ${error.message}`); + } + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d7700c3ee..d7176df925 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -421,6 +421,53 @@ The CI system intelligently skips unnecessary work: For more details, see [`docs/CI_OPTIMIZATION.md`](./docs/CI_OPTIMIZATION.md). +### CI Control Commands + +React on Rails provides PR comment commands to control CI behavior: + +#### `/run-skipped-ci` - Enable Full CI Mode + +Runs all skipped CI checks and enables full CI mode for the PR: + +``` +/run-skipped-ci +``` + +**What it does:** + +- Triggers all CI workflows that were skipped due to unchanged code +- Adds the `full-ci` label to the PR +- **Persists across future commits** - all subsequent pushes will run the full test suite +- Runs minimum dependency tests (Ruby 3.2, Node 20, Shakapacker 8.2.0, React 18) + +**When to use:** + +- You want comprehensive testing across all configurations +- Testing changes that might affect minimum supported versions +- Validating generator changes or core functionality +- Before merging PRs that touch critical paths + +#### `/stop-run-skipped-ci` - Disable Full CI Mode + +Removes the `full-ci` label and returns to standard CI behavior: + +``` +/stop-run-skipped-ci +``` + +**What it does:** + +- Removes the `full-ci` label from the PR +- Future commits will use the optimized CI suite (tests only changed code) +- Does not stop currently running workflows + +**When to use:** + +- You've validated changes with full CI and want to return to faster feedback +- Reducing CI time during rapid iteration on a PR + +**Note:** The `full-ci` label is preserved on merged PRs as a historical record of which PRs ran with comprehensive testing. + ### Install Generator In your Rails app add this gem with a path to your fork.