diff --git a/.github/workflows/danger-comment.yml b/.github/workflows/danger-comment.yml new file mode 100644 index 000000000..2dd5037a1 --- /dev/null +++ b/.github/workflows/danger-comment.yml @@ -0,0 +1,92 @@ +--- +name: danger-comment +on: + workflow_run: + workflows: [danger] + types: [completed] + +permissions: + actions: read # Required to download the artifact + contents: read # Standard safe default + issues: write # Required to post comments (PRs are Issues in the API) + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download Danger report + uses: actions/download-artifact@v6 + with: + name: danger-report + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + - name: Post or update PR comment + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const report = JSON.parse(fs.readFileSync('danger_report.json', 'utf8')); + + if (!report.pr_number) { + console.log('No PR number found in report, skipping comment'); + return; + } + + let body = '## Danger Report\n\n'; + + if (report.errors && report.errors.length > 0) { + body += '### Errors\n'; + report.errors.forEach(e => body += `- :no_entry_sign: ${e}\n`); + body += '\n'; + } + + if (report.warnings && report.warnings.length > 0) { + body += '### Warnings\n'; + report.warnings.forEach(w => body += `- :warning: ${w}\n`); + body += '\n'; + } + + if (report.messages && report.messages.length > 0) { + body += '### Messages\n'; + report.messages.forEach(m => body += `- :book: ${m}\n`); + body += '\n'; + } + + if ((!report.errors || report.errors.length === 0) && + (!report.warnings || report.warnings.length === 0) && + (!report.messages || report.messages.length === 0)) { + body += ':white_check_mark: All checks passed!'; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: report.pr_number + }); + + const botComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.includes('## Danger Report') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: report.pr_number, + body: body + }); + } + + // Fail if there are errors + if (report.errors && report.errors.length > 0) { + core.setFailed('Danger found errors'); + } diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 12372470c..dd0eef550 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -1,21 +1,31 @@ --- name: danger -on: pull_request +on: + pull_request: + types: [opened, synchronize, reopened] jobs: danger: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - fetch-depth: 100 + fetch-depth: 0 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true - name: Run Danger - run: | - # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok - TOKEN=$(echo -n Z2hwX2lYb0dPNXNyejYzOFJyaTV3QUxUdkNiS1dtblFwZTFuRXpmMwo= | base64 --decode) - DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + DANGER_REPORT_PATH: danger_report.json + run: bundle exec danger dry_run --base=$BASE_SHA --head=$HEAD_SHA --verbose + - name: Upload Danger report + uses: actions/upload-artifact@v5 + with: + name: danger-report + path: danger_report.json + retention-days: 1 diff --git a/Dangerfile b/Dangerfile index 82881902a..17c4726a3 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,3 +1,41 @@ # frozen_string_literal: true -danger.import_dangerfile(gem: 'ruby-grape-danger') +# Inline checks from ruby-grape-danger (avoids plugins requiring GitHub API token) + +has_app_changes = !git.modified_files.grep(%r{^lib/}).empty? +has_spec_changes = !git.modified_files.grep(%r{^spec/}).empty? + +warn("There're library changes, but not tests. That's OK as long as you're refactoring existing code.", sticky: false) if has_app_changes && !has_spec_changes + +message('We really appreciate pull requests that demonstrate issues, even without a fix. That said, the next step is to try and fix the failing tests!', sticky: false) if !has_app_changes && has_spec_changes + +# Simplified changelog check (replaces danger-changelog plugin which requires github.* methods) +# Note: toc.check! from danger-toc plugin removed (not essential for CI) +has_changelog_changes = git.modified_files.include?('CHANGELOG.md') || git.added_files.include?('CHANGELOG.md') +warn('Please update CHANGELOG.md with a description of your changes.', sticky: false) if has_app_changes && !has_changelog_changes + +(git.modified_files + git.added_files - %w[Dangerfile]).each do |file| + next unless File.file?(file) + + contents = File.read(file) + # rubocop:disable Style/SignalException -- `fail` is Danger's DSL method, not Kernel#fail + if file.start_with?('spec') + fail("`xit` or `fit` left in tests (#{file})") if /^\s*[xf]it\b/.match?(contents) + fail("`fdescribe` left in tests (#{file})") if /^\s*fdescribe\b/.match?(contents) + end + # rubocop:enable Style/SignalException +end + +# Output JSON report for GitHub Actions workflow_run to post as PR comment +if ENV['DANGER_REPORT_PATH'] + require 'json' + + report = { + pr_number: ENV['PR_NUMBER']&.to_i, + errors: violation_report[:errors].map(&:message), + warnings: violation_report[:warnings].map(&:message), + messages: violation_report[:messages].map(&:message) + } + + File.write(ENV['DANGER_REPORT_PATH'], JSON.pretty_generate(report)) +end