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