diff --git a/.github/workflows/pull-request-helper.yml b/.github/workflows/pull-request-helper.yml index b26be60ece..7b1c452cfe 100644 --- a/.github/workflows/pull-request-helper.yml +++ b/.github/workflows/pull-request-helper.yml @@ -4,68 +4,14 @@ on: permissions: contents: write + pull-requests: write jobs: pull-request-helper: if: github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # this is the personal access token used for "git push" below - # which is needed in order to trigger workflows - token: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - with: - cache-read-only: true - - - name: Set git user - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - # - name: Spotless - # env: - # NUMBER: ${{ github.event.issue.number }} - # run: | - # ./gradlew spotlessApply - # if git diff --quiet; then - # exit 0 # success - # fi - # git commit -a -m "./gradlew spotlessApply" - - - name: Update license report - env: - NUMBER: ${{ github.event.issue.number }} - run: | - ./gradlew generateLicenseReport - git add licenses - # there's always going to one line difference due to the timestamp included in the report - if [[ $(git diff --cached --shortstat licenses) == " 1 file changed, 1 insertion(+), 1 deletion(-)" ]] - then - git reset HEAD licenses - git checkout -- licenses - exit 0 # success - fi - git commit -m "./gradlew generateLicenseReport" - - - name: Update lock files - env: - NUMBER: ${{ github.event.issue.number }} - run: | - ./gradlew resolveAndLockAll --write-locks - git add "*.lockfile" - if git diff --cached --quiet; then - exit 0 # success - fi - git commit -m "./gradlew resolveAndLockAll --write-locks" - - - name: Push - env: - GH_TOKEN: ${{ github.token }} - run: | - git push + uses: ./.github/workflows/reusable-regenerate-generated-files.yml + with: + pr_number: ${{ github.event.pull_request.number }} + skip_if_contains_main: false + secrets: + PR_HELPER_GITHUB_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} diff --git a/.github/workflows/regenerate-generated-files-runner.yml b/.github/workflows/regenerate-generated-files-runner.yml new file mode 100644 index 0000000000..6b652d54e3 --- /dev/null +++ b/.github/workflows/regenerate-generated-files-runner.yml @@ -0,0 +1,22 @@ +name: Regenerate generated files runner + +on: + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to process + required: true + type: number + +permissions: + contents: write + pull-requests: write + +jobs: + regenerate-generated-files: + uses: ./.github/workflows/reusable-regenerate-generated-files.yml + with: + pr_number: ${{ inputs.pr_number }} + skip_if_contains_main: true + secrets: + PR_HELPER_GITHUB_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} diff --git a/.github/workflows/regenerate-generated-files.yml b/.github/workflows/regenerate-generated-files.yml new file mode 100644 index 0000000000..d8668148f8 --- /dev/null +++ b/.github/workflows/regenerate-generated-files.yml @@ -0,0 +1,93 @@ +name: Regenerate generated files + +on: + push: + branches: + - main + issue_comment: + types: + - created + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to process + required: false + type: string + +permissions: + contents: read + pull-requests: read + +jobs: + dispatch-regeneration-request: + if: github.event_name == 'issue_comment' && github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/regen') && github.event.issue.user.login == 'dependabot[bot]' && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') + runs-on: ubuntu-latest + steps: + - name: Trigger workflow_dispatch for target pull request + env: + GH_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + run: | + gh workflow run regenerate-generated-files-runner.yml \ + --repo "$REPO" \ + --ref main \ + -f pr_number="$PR_NUMBER" + + prepare-targets: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.targets.outputs.matrix }} + steps: + - name: Determine target pull requests + id: targets + env: + EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + REPO: ${{ github.repository }} + WORKFLOW_DISPATCH_PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -euo pipefail + + if [[ "$EVENT_NAME" == "push" ]]; then + prs=$(gh pr list --repo "$REPO" --state open --label dependencies --json number,headRepository) + matrix=$(echo "$prs" | jq -c '[.[] | {pr: .number}]') + elif [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ -z "$WORKFLOW_DISPATCH_PR_NUMBER" ]]; then + echo "No workflow_dispatch pr_number provided, skipping" + matrix='[]' + else + matrix=$(jq -cn --argjson pr "$WORKFLOW_DISPATCH_PR_NUMBER" '[{pr:$pr}]') + fi + else + echo "Unsupported event: $EVENT_NAME" + exit 1 + fi + + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + + regenerate-generated-files: + needs: + - prepare-targets + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.prepare-targets.outputs.matrix != '[]' + strategy: + fail-fast: false + max-parallel: 3 + matrix: + include: ${{ fromJson(needs.prepare-targets.outputs.matrix) }} + concurrency: + group: regen-${{ matrix.pr }} + cancel-in-progress: true + runs-on: ubuntu-latest + steps: + - name: Trigger workflow_dispatch for target pull request + env: + GH_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + PR_NUMBER: ${{ matrix.pr }} + REPO: ${{ github.repository }} + run: | + gh workflow run regenerate-generated-files-runner.yml \ + --repo "$REPO" \ + --ref main \ + -f pr_number="$PR_NUMBER" diff --git a/.github/workflows/reusable-regenerate-generated-files.yml b/.github/workflows/reusable-regenerate-generated-files.yml new file mode 100644 index 0000000000..e75b018cef --- /dev/null +++ b/.github/workflows/reusable-regenerate-generated-files.yml @@ -0,0 +1,134 @@ +name: Reusable - Regenerate generated files + +on: + workflow_call: + inputs: + pr_number: + type: number + required: true + skip_if_contains_main: + type: boolean + required: false + default: true + secrets: + PR_HELPER_GITHUB_TOKEN: + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + regenerate-generated-files: + runs-on: ubuntu-latest + steps: + - name: Resolve PR head branch + id: pr + env: + GH_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + PR_NUMBER: ${{ inputs.pr_number }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + pr_json=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName,headRefOid,headRepository) + head_repo=$(echo "$pr_json" | jq -r '.headRepository.nameWithOwner // ""') + head_ref=$(echo "$pr_json" | jq -r '.headRefName') + head_sha=$(echo "$pr_json" | jq -r '.headRefOid') + + echo "head_repo=$head_repo" >> "$GITHUB_OUTPUT" + echo "head_ref=$head_ref" >> "$GITHUB_OUTPUT" + echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" + + if [[ "$head_repo" != "$REPO" ]]; then + echo "eligible=false" >> "$GITHUB_OUTPUT" + echo "Skipping PR #$PR_NUMBER from fork repository: $head_repo" + else + echo "eligible=true" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout PR head + if: steps.pr.outputs.eligible == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # this personal access token is required so pushes retrigger workflows + token: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + repository: ${{ steps.pr.outputs.head_repo }} + ref: ${{ steps.pr.outputs.head_sha }} + fetch-depth: 0 + + - name: Setup Gradle + if: steps.pr.outputs.eligible == 'true' + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 + with: + cache-read-only: true + + - name: Set git user + if: steps.pr.outputs.eligible == 'true' + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + + - name: Merge main and regenerate generated files + if: steps.pr.outputs.eligible == 'true' + env: + GH_TOKEN: ${{ secrets.PR_HELPER_GITHUB_TOKEN }} + PR_NUMBER: ${{ inputs.pr_number }} + REPO: ${{ github.repository }} + SKIP_IF_CONTAINS_MAIN: ${{ inputs.skip_if_contains_main }} + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + run: | + set -euo pipefail + + git fetch origin main + + if [[ "$SKIP_IF_CONTAINS_MAIN" == "true" ]] && git merge-base --is-ancestor origin/main HEAD; then + echo "PR #$PR_NUMBER already contains origin/main, skipping" + exit 0 + fi + + git merge --no-edit origin/main || true + + mapfile -t conflicted < <(git diff --name-only --diff-filter=U) + + if [ ${#conflicted[@]} -gt 0 ]; then + generated=() + real=() + + for f in "${conflicted[@]}"; do + case "$f" in + *gradle.lockfile|licenses/more-licenses.md) generated+=("$f") ;; + *) real+=("$f") ;; + esac + done + + if [ ${#real[@]} -gt 0 ]; then + printf 'genuine conflict, aborting: %s\n' "${real[@]}" + + comment=$'Auto-regeneration stopped because this PR has genuine merge conflicts in non-generated files:\n\n' + for f in "${real[@]}"; do + comment+="- \`$f\`"$'\n' + done + comment+=$'\nPlease resolve these conflicts manually, then rerun `/regen` if needed.' + + gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$comment" + git merge --abort + exit 0 + fi + + for f in "${generated[@]}"; do + git checkout --ours -- "$f" + git add -- "$f" + done + fi + + if [ -f .git/MERGE_HEAD ]; then + git commit --no-edit + fi + + ./gradlew resolveAndLockAll --write-locks + ./gradlew generateLicenseReport --no-build-cache + + git add -A -- '*.lockfile' licenses + git diff --cached --quiet || git commit -m "regenerate lock files and license report" + git push origin "HEAD:$HEAD_REF" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6df87810fe..40d935fe6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,16 @@ After updating dependencies, regenerate the license report to include licenses f This ensures all third-party licenses are properly documented. +### Automatic regeneration for dependency pull requests + +Generated files (`**/gradle.lockfile` and `licenses/more-licenses.md`) are automatically refreshed by GitHub Actions: + +- On each push to `main`, open dependency PRs are merged with `main` and regenerated automatically. +- On an individual Dependabot PR, a repository member can comment `/regen` to run the regeneration workflow on demand. +- `pull-request-helper.yml` continues to refresh generated files for newly opened Dependabot PRs. + +If regeneration detects merge conflicts in non-generated source files (`.kts`, `.java`, `.toml`, workflows, etc.), the bot leaves a PR comment and stops. In that case, resolve those source conflicts manually and rerun `/regen` if needed. + ## IntelliJ setup and troubleshooting See [IntelliJ setup and troubleshooting](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/contributing/intellij-setup-and-troubleshooting.md)