diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 2187e48..13e20a3 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -142,12 +142,20 @@ jobs: if: github.event_name != 'workflow_dispatch' || inputs.dep-suggests-matrix uses: ./.github/workflows/dep-suggests-matrix + - id: repo-state + name: Determine repository state + uses: ./.github/workflows/repo-state + + # Styling on foreign PRs works through the format-suggest workflow + - uses: ./.github/workflows/style + if: steps.repo-state.outputs.foreign != 'true' || steps.repo-state.outputs.is_pr != 'true' + + # Snapshot tests can't work in a way similar to format-suggests + # because it requires running code which is a security risk on pull_request_target workflows - uses: ./.github/workflows/update-snapshots with: base: ${{ inputs.ref || github.head_ref }} - - uses: ./.github/workflows/style - - uses: ./.github/workflows/roxygenize - name: Remove config files from previous iteration @@ -175,7 +183,7 @@ jobs: echo -n "${{ steps.commit.outputs.sha }}" > rcc-smoke-sha.txt shell: bash - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: rcc-smoke-sha path: rcc-smoke-sha.txt diff --git a/.github/workflows/commit-suggest.yaml b/.github/workflows/commit-suggest.yaml new file mode 100644 index 0000000..147b307 --- /dev/null +++ b/.github/workflows/commit-suggest.yaml @@ -0,0 +1,100 @@ +name: commit-suggest.yaml + +on: + workflow_run: + workflows: ["rcc"] + types: + - completed + +permissions: + contents: write + pull-requests: write + +jobs: + commit-suggest: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + + steps: + - name: Show event payload + run: | + echo '${{ toJson(github.event) }}' | jq . + shell: bash + + - name: Checkout PR + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Download artifact + uses: actions/download-artifact@v6 + with: + name: changes-patch + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + continue-on-error: true + + - name: Check if artifact exists + id: check-artifact + run: | + if [ -f changes.patch ]; then + echo "has_diff=true" >> $GITHUB_OUTPUT + else + echo "has_diff=false" >> $GITHUB_OUTPUT + echo "No changes-patch artifact found" + fi + shell: bash + + - name: Find PR number for branch from correct head repository + id: find-pr + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + PR_NUMBER=$(gh pr list --head ${{ github.event.workflow_run.head_branch }} --state open --json number,headRepositoryOwner --jq '.[] | select(.headRepositoryOwner.login == "${{ github.event.workflow_run.head_repository.owner.login }}") | .number' || echo "") + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + shell: bash + + - name: Generate comment body + if: steps.check-artifact.outputs.has_diff == 'true' + id: comment-body + run: | + cat << 'EOF' > comment.md + ## Formatting suggestions available + + A patch file with formatting suggestions has been generated. You can apply it using one of these methods: + + ### Method 1: Apply via gh CLI + + ```bash + # Download and apply the patch directly + gh run download ${{ github.event.workflow_run.id }} --repo ${{ github.repository }} --name changes-patch && patch -p1 < changes.patch && rm changes.patch + ``` + + ### Method 2: View the patch + +
+ Click to see the patch contents + + ```diff + EOF + + cat changes.patch >> comment.md + + cat << 'EOF' >> comment.md + ``` + +
+ + --- + *This comment was automatically generated by the commit-suggester workflow.* + EOF + shell: bash + + - name: Post or update comment + if: steps.check-artifact.outputs.has_diff == 'true' + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ steps.find-pr.outputs.pr_number }} + file-path: comment.md + comment-tag: formatting-suggestions + mode: recreate diff --git a/.github/workflows/commit/action.yml b/.github/workflows/commit/action.yml index e385cd8..94ccfad 100644 --- a/.github/workflows/commit/action.yml +++ b/.github/workflows/commit/action.yml @@ -22,65 +22,121 @@ runs: fi shell: bash - - name: Commit if changed, create a PR if protected - id: commit + - name: Determine repository state if: steps.check.outputs.has_changes == 'true' + id: repo-state + uses: ./.github/workflows/repo-state + + - name: Commit and create PR on protected branch + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'false' && steps.repo-state.outputs.protected == 'true' env: GITHUB_TOKEN: ${{ inputs.token }} run: | set -x - protected=${{ github.ref_protected }} - foreign=${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} - is_pr=${{ github.event_name == 'pull_request' }} - if [ "${is_pr}" = "true" ] && [ "${foreign}" = "true" ]; then - # Running on a PR - will use reviewdog in next step - echo "Code changes detected on PR, will suggest changes via reviewdog" - echo "use_reviewdog=true" | tee -a $GITHUB_OUTPUT - git reset HEAD - git status - elif [ "${foreign}" = "true" ]; then - # https://github.com/krlmlr/actions-sync/issues/44 - echo "Can't push to foreign branch" - elif [ "${protected}" = "true" ]; then - current_branch=$(git branch --show-current) - new_branch=gha-commit-$(git rev-parse --short HEAD) - git checkout -b ${new_branch} - git add . - git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - # Force-push, used in only one place - # Alternative: separate branch names for each usage - git push -u origin HEAD -f - - existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') - if [ -n "${existing_pr}" ]; then - echo "Existing PR: ${existing_pr}" - else - gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - fi - - gh workflow run rcc -f ref=$(git rev-parse HEAD) - gh pr merge --merge --auto + current_branch=$(git branch --show-current) + new_branch=gha-commit-$(git rev-parse --short HEAD) + git checkout -b ${new_branch} + git add . + git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + # Force-push, used in only one place + # Alternative: separate branch names for each usage + git push -u origin HEAD -f + + existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') + if [ -n "${existing_pr}" ]; then + echo "Existing PR: ${existing_pr}" else - git fetch - if [ -n "${GITHUB_HEAD_REF}" ]; then - git add . - git stash save - git switch ${GITHUB_HEAD_REF} - git merge origin/${GITHUB_BASE_REF} --no-edit - git stash pop - fi - git add . - git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - git push -u origin HEAD + gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + fi + + gh workflow run rcc -f ref=$(git rev-parse HEAD) + gh pr merge --merge --auto + shell: bash - # Only set output if changed - echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + - name: Commit and push on unprotected branch + id: commit + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'false' && steps.repo-state.outputs.protected == 'false' + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: | + set -x + git fetch + if [ -n "${GITHUB_HEAD_REF}" ]; then + git add . + git stash save + git switch ${GITHUB_HEAD_REF} + git merge origin/${GITHUB_BASE_REF} --no-edit + git stash pop fi + git add . + git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + git push -u origin HEAD + + # Only set output if changed + echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + shell: bash + + - name: Create patch file for foreign branch + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' + run: | + set -x + git diff > changes.patch + echo "Patch file created with uncommitted changes" + cat changes.patch shell: bash - - name: Suggest changes via reviewdog - if: steps.commit.outputs.use_reviewdog == 'true' - uses: krlmlr/action-suggester@main + - name: Upload patch artifact + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' + uses: actions/upload-artifact@v5 with: - github_token: ${{ inputs.token }} - tool_name: "rcc" + name: changes-patch + path: changes.patch + + - name: Add patch summary on foreign branch + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' + run: | + cat << 'EOF' >> $GITHUB_STEP_SUMMARY + ## Formatting suggestions available + + A patch file with formatting suggestions has been generated. Since this PR is from a forked repository, the changes cannot be pushed automatically. + + You can apply the patch using one of these methods: + + ### Method 1: Apply via gh CLI + + ```bash + # Download and apply the patch directly + gh run download ${{ github.run_id }} --repo ${{ github.repository }} --name changes-patch && git apply changes.patch && rm changes.patch + ``` + + ### Method 2: Download from workflow artifacts + + 1. Download the `changes-patch` artifact from this workflow run + 2. Extract and apply it: + ```bash + git apply changes.patch + ``` + + ### Method 3: View the patch + +
+ Click to see the patch contents + + ```diff + EOF + + cat changes.patch >> $GITHUB_STEP_SUMMARY + + cat << 'EOF' >> $GITHUB_STEP_SUMMARY + ``` + +
+ EOF + shell: bash + + - name: Fail on foreign branch + if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' + run: | + echo "Exiting with failure due to foreign branch. Please apply the patch suggested in the action or PR comment." + exit 1 + shell: bash diff --git a/.github/workflows/covr/action.yml b/.github/workflows/covr/action.yml index 76f593a..a35327f 100644 --- a/.github/workflows/covr/action.yml +++ b/.github/workflows/covr/action.yml @@ -40,7 +40,7 @@ runs: - name: Upload test results if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-test-failures path: ${{ runner.temp }}/package diff --git a/.github/workflows/format-suggest.yaml b/.github/workflows/format-suggest.yaml new file mode 100644 index 0000000..d31a1fa --- /dev/null +++ b/.github/workflows/format-suggest.yaml @@ -0,0 +1,45 @@ +# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples + +on: + # Using `pull_request_target` over `pull_request` for elevated `GITHUB_TOKEN` + # privileges, otherwise we can't set `pull-requests: write` when the pull + # request comes from a fork, which is our main use case (external contributors). + # + # `pull_request_target` runs in the context of the target branch (`main`, usually), + # rather than in the context of the pull request like `pull_request` does. Due + # to this, we must explicitly checkout `ref: ${{ github.event.pull_request.head.sha }}`. + # This is typically frowned upon by GitHub, as it exposes you to potentially running + # untrusted code in a context where you have elevated privileges, but they explicitly + # call out the use case of reformatting and committing back / commenting on the PR + # as a situation that should be safe (because we aren't actually running the untrusted + # code, we are just treating it as passive data). + # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + pull_request_target: + +name: format-suggest.yaml + +jobs: + format-suggest: + name: format-suggest + runs-on: ubuntu-latest + # Only run this job if changes come from a fork. + # We commit changes directly on the main repository. + if: github.event.pull_request.head.repo.full_name != github.repository + + permissions: + # Required to push suggestion comments to the PR + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: ./.github/workflows/style + + - name: Suggest + uses: reviewdog/action-suggester@v1 + with: + level: error + fail_level: error + tool_name: air-and-clang-format diff --git a/.github/workflows/repo-state/action.yml b/.github/workflows/repo-state/action.yml new file mode 100644 index 0000000..be7bb80 --- /dev/null +++ b/.github/workflows/repo-state/action.yml @@ -0,0 +1,28 @@ +name: "Determine repository state" +description: "Expose protected, foreign, and is_pr context variables" +outputs: + protected: + description: "Whether the current branch is protected" + value: ${{ steps.state.outputs.protected }} + foreign: + description: "Whether the PR is from a foreign repository" + value: ${{ steps.state.outputs.foreign }} + is_pr: + description: "Whether the event is a pull request" + value: ${{ steps.state.outputs.is_pr }} + +runs: + using: "composite" + steps: + - name: Determine repository state + id: state + run: | + set -x + protected=${{ github.ref_protected }} + foreign=${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} + is_pr=${{ github.event_name == 'pull_request' }} + + echo "protected=${protected}" | tee -a $GITHUB_OUTPUT + echo "foreign=${foreign}" | tee -a $GITHUB_OUTPUT + echo "is_pr=${is_pr}" | tee -a $GITHUB_OUTPUT + shell: bash diff --git a/.github/workflows/update-snapshots/action.yml b/.github/workflows/update-snapshots/action.yml index 6a54cb0..f4a84b5 100644 --- a/.github/workflows/update-snapshots/action.yml +++ b/.github/workflows/update-snapshots/action.yml @@ -92,3 +92,8 @@ runs: run: | false shell: bash + + - name: Reset Git changes + run: | + git reset -- tests/testthat/_snaps + shell: bash