diff --git a/.github/actions/maven-license-check-action/action.yml b/.github/actions/maven-license-check-action/action.yml index 2ab96714..29227be5 100644 --- a/.github/actions/maven-license-check-action/action.yml +++ b/.github/actions/maven-license-check-action/action.yml @@ -29,20 +29,22 @@ runs: shell: bash {0} # do not fail-fast run: | mvnArgs="-U -B -ntp org.eclipse.dash:license-tool-plugin:license-check -Ddash.fail=true -Dtycho.target.eager=true --settings $GITHUB_ACTION_PATH/licenseCheckMavenSettings.xml" - if [[ ${{ inputs.project-id }} != "" ]]; then + mvnArgs+=" -Ddash.repo=https://github.com/${{ github.repository }}" + + if [[ "${{ inputs.project-id }}" != "" ]]; then mvnArgs+=" -Ddash.projectId=${{ inputs.project-id }}" fi - if [ ${{ inputs.request-review }} ]; then + if [ "${{ inputs.request-review }}" != "" ]; then mvn ${mvnArgs} -Ddash.iplab.token=$GITLAB_API_TOKEN - if [[ $? == 0 ]]; then # All licenses are vetted - echo "build-succeeded=1" >> $GITHUB_OUTPUT - else - echo "build-succeeded=0" >> $GITHUB_OUTPUT - fi else mvn ${mvnArgs} - if [[ $? != 0 ]]; then + fi + + if [[ $? == 0 ]]; then # All licenses are vetted + echo "build-succeeded=1" >> $GITHUB_OUTPUT + else + if [[ "${{ inputs.request-review }}" = "" ]]; then echo "Committers can request a review by commenting '/request-license-review'" - exit 1 fi + echo "build-succeeded=0" >> $GITHUB_OUTPUT fi diff --git a/.github/workflows/addLicenseCheckCommentForForks.yml b/.github/workflows/addLicenseCheckCommentForForks.yml new file mode 100644 index 00000000..4c40b342 --- /dev/null +++ b/.github/workflows/addLicenseCheckCommentForForks.yml @@ -0,0 +1,53 @@ +name: Add license check comment for PRs coming from a fork + +on: + workflow_call: + +jobs: + add-pr-comment-for-forks: + runs-on: ubuntu-latest + permissions: + pull-requests: write + if: | + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.head_repository.full_name != github.repository + steps: + - name: 'Download artifact' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr-comment" + })[0]; + var download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr-comment.zip', Buffer.from(download.data)); + + - run: unzip pr-comment.zip + + - name: 'Comment on PR' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var fs = require('fs'); + + const issue_number = Number(fs.readFileSync('./pr.txt')); + const body = fs.readFileSync('./comment.txt', { encoding: 'utf8', flag: 'r' }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: body + }); \ No newline at end of file diff --git a/.github/workflows/mavenLicenseCheck.yml b/.github/workflows/mavenLicenseCheck.yml index 82194e44..a6f21a9e 100644 --- a/.github/workflows/mavenLicenseCheck.yml +++ b/.github/workflows/mavenLicenseCheck.yml @@ -50,29 +50,43 @@ on: required: false jobs: - check-licenses: - if: github.event_name != 'issue_comment' || ( github.event.issue.pull_request != '' && (github.event.comment.body == '/request-license-review') ) + check-request: # Run on all non-comment events specified by the calling workflow and for comments on PRs that have a corresponding body. + if: > + github.event_name != 'issue_comment' || + (github.event.issue.pull_request && + (github.event.comment.body == '/request-license-review' || + github.event.comment.body == '/license-check')) runs-on: ubuntu-latest - permissions: - pull-requests: write + outputs: + request-review: ${{ steps.request-review.outputs.request-review }} + license-check: ${{ steps.license-check.outputs.license-check }} steps: - - - name: Check dependabot PR - run: echo "isDependabotPR=1" >> $GITHUB_ENV + - name: Check dependabot PR if: > - github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened') + github.event_name == 'pull_request' + && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened') && github.actor == 'dependabot[bot]' && github.actor_id == '49699333' - # For 'issue_comment'-events this job only runs if a comment was added to a PR with body specified above + run: echo "isDependabotPR=1" >> $GITHUB_ENV + # For 'issue_comment'-events this job only runs if a comment was added to a PR with body specified above - name: Set review request - run: echo "request-review=1" >> $GITHUB_ENV - if: github.event_name == 'issue_comment' || env.isDependabotPR - # For 'issue_comment'-events this job only runs if a comment was added to a PR with body specified above + id: request-review + if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/request-license-review')) || env.isDependabotPR + run: | + echo "request-review=1" >> "$GITHUB_OUTPUT" + + - name: Set license check + id: license-check + if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/license-check')) || env.isDependabotPR + run: | + echo "license-check=1" >> "$GITHUB_OUTPUT" - name: Process license-vetting request - if: env.request-review && (!env.isDependabotPR) - uses: actions/github-script@v7 + if: | + (steps.request-review.outputs.request-review || steps.license-check.outputs.license-check) + && (!env.isDependabotPR) + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const payload = await github.rest.repos.getCollaboratorPermissionLevel({ @@ -90,27 +104,41 @@ jobs: ...context.repo, comment_id: context.payload?.comment?.id, content: reaction }); - # By default the git-ref checked out for events triggered by comments to PRs is 'refs/heads/master' + + check-licenses: + needs: check-request + if: ${{needs.check-request.outputs.license-check == ''}} + runs-on: ubuntu-latest + permissions: + pull-requests: write + env: + request-review: ${{ needs.check-request.outputs.request-review }} + license-check: ${{ needs.check-request.outputs.license-check }} + comment-header: '' + steps: + # By default, the git-ref checked out for events triggered by comments to PRs is 'refs/heads/master' # and for events triggered by PR creation/updates the ref is 'refs/pull//merge'. # So by default only the master-branch would be considered when requesting license-reviews, but we want the PR's state. # Unless the PR is closed, then we want the master-branch, which allows subsequent license review requests. - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # use default ref 'refs/pull//merge' for PR-events and 'refs/heads/master' for comments if the PR is closed if: github.event.issue.pull_request == '' || github.event.issue.state != 'open' with: submodules: ${{ inputs.submodules }} - - uses: actions/checkout@v4 + + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + if: github.event.issue.pull_request != '' && github.event.issue.state == 'open' with: ref: 'refs/pull/${{ github.event.issue.number }}/merge' submodules: ${{ inputs.submodules }} - if: github.event.issue.pull_request != '' && github.event.issue.state == 'open' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ inputs.javaVersion }} distribution: 'temurin' + - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: ~/.m2/repository # re-cache on changes in the pom and target files @@ -124,12 +152,12 @@ jobs: maven-version: ${{ inputs.mavenVersion }} - name: Prepare for license check - run: ${{ inputs.setupScript }} if: inputs.setupScript !='' + run: ${{ inputs.setupScript }} - name: Check license vetting status (and ask for review if requested) id: check-license-vetting - uses: eclipse-dash/dash-licenses/.github/actions/maven-license-check-action@master + uses: eclipse-dash/dash-licenses/.github/actions/maven-license-check-action@07a2f0e44c1948918f6ba3e5a0aa1b52b1413847 # master with: request-review: ${{ env.request-review }} project-id: ${{ inputs.projectId }} @@ -137,62 +165,138 @@ jobs: GITLAB_API_TOKEN: ${{ secrets.gitlabAPIToken }} - name: Process license check results - if: env.request-review - uses: actions/github-script@v7 + id: process-results + if: always() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: + result-encoding: string script: | const fs = require('fs') - - const licenesVetted = ${{ steps.check-license-vetting.outputs.licenses-vetted }} - let commentBody = '' - // if context.payload.comment is empty, this is an explicit review-request through a comment, if not an automated one, e.g. for dependabot PRs - if ( context.payload.comment ) { - commentBody += '> ' + context.payload.comment.body + '\n\n' - } else if ( licenesVetted ){ - core.info('License review request made automatically but all licenses are already vetted.') - return; // Don't create a comment in this case, the checks in the UI indicate the state already. - } else { - // This run encountered pending reviews, which have been requested automatically, e.g. for dependabot PRs - core.setFailed("Some dependencies must be vetted and their review was requested. Rerun this check once these reviews succeeded.") - } - - if( licenesVetted ) { - commentBody += ':heavy_check_mark: All licenses already successfully vetted.\n' + const reviewRequested = ${{ env.request-review || false }} + const updateRequested = reviewRequested || ${{ env.license-check || false }} + const licensesVetted = ${{ steps.check-license-vetting.outputs.licenses-vetted }} + + let commentBody = "### License summary\n" + if (licensesVetted) { + if (updateRequested) { + commentBody += ':heavy_check_mark: All licenses already successfully vetted.\n' + } else { + // Do not comment if all licenses are vetted and no update was requested + core.info('All licenses are already vetted.') + return; + } } else { - - const reviewSummaryFile = process.env.GITHUB_WORKSPACE + '/target/dash/review-summary' - core.info("Read review summary at " + reviewSummaryFile) + // Print dependency info + const dependencySummaryFile = 'target/dash/summary' + core.info("Read dependency summary at " + dependencySummaryFile) let content = ""; - if ( fs.existsSync( reviewSummaryFile )) { - content = fs.readFileSync( reviewSummaryFile, {encoding: 'utf8'}).trim(); + if (fs.existsSync(dependencySummaryFile)) { + content = fs.readFileSync(dependencySummaryFile, { encoding: 'utf8' }).trim(); } - - if ( content ) { // not empty - commentBody += 'License review requests:\n' + + if (content) { // not empty + commentBody += ":x: Not yet vetted dependencies:\n" + commentBody += "| Dependency | License | Status | Ticket |\n" + commentBody += "|------------|---------|--------|--------|\n" const lines = content.split('\n') - for(var line = 0; line < lines.length; line++){ - commentBody += ('- ' + lines[line] + '\n') + let notVettedDependencies = 0 + for (const line of lines) { + if (line.includes('restricted')) { + depLine = `| ${line.split(", ").join(" | ")} |\n` + commentBody += depLine.replace(/#([0-9]+)/gm, "[#\$1](https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/\$1)") + notVettedDependencies++ + } + } + core.setFailed(`${notVettedDependencies} dependencies are not vetted yet.`) + } else { + commentBody += ':warning: Failed to process summary.\n' + core.setFailed('Failed to process dash summary') + } + + if (reviewRequested) { + const reviewSummaryFile = "target/dash/review-summary" + let reviews = ""; + if (fs.existsSync(reviewSummaryFile)) { + reviews = fs.readFileSync(reviewSummaryFile, { encoding: 'utf8' }).trim(); + } + if (reviews) { // not empty + commentBody += "\n### :rocket: Requested reviews:\n" + const lines = reviews.split('\n') + for (const line of lines) { + commentBody += `- ${line}\n` + } + } else { + core.setFailed("License vetting build failed, but no reviews are created") + commentBody += ':warning: Failed to request review of not vetted licenses.\n' } - commentBody += '\n' - commentBody += 'After all reviews have concluded, re-run the license-vetting check from the Github Actions web-interface to update its status.\n' - } else { - core.setFailed("License vetting build failed, but no reviews are created") - commentBody += ':warning: Failed to request review of not vetted licenses.\n' + commentBody += '\n\n- Committers can request a license review via by commenting `/request-license-review`.\n- After all reviews have concluded, Committers can re-run the license-vetting check by commenting `/license-check`\n' } } - commentBody += '\n' - commentBody += 'Workflow run (with attached summary files):\n' - commentBody += context.serverUrl + "/" + process.env.GITHUB_REPOSITORY + "/actions/runs/" + context.runId - - github.rest.issues.createComment({ - issue_number: context.issue.number, ...context.repo, body: commentBody - }) - - - uses: actions/upload-artifact@v4 - if: always() && env.request-review + + commentBody += `\nWorkflow run (with attached summary files):\n${context.serverUrl}/${process.env.GITHUB_REPOSITORY}/actions/runs/${context.runId}` + return commentBody + + - name: Adding comment to job summary + if: always() + run: | + echo '${{steps.process-results.outputs.result}}' >> $GITHUB_STEP_SUMMARY + + - name: Determine comment header + if: ${{env.request-review}} + run: echo "comment-header=''" >> "$GITHUB_ENV" + + # Add the process result as comment to the PR if an update has been requested + # or if the PR is not coming from a fork (in which case we don't have write tokens) + - name: Adding comment to PR + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0 + if: | + always() + && (github.event_name == 'issue_comment' || github.event.pull_request.head.repo.full_name == github.repository) + with: + header: ${{env.comment-header}} + hide_and_recreate: true + hide_classify: "OUTDATED" + number: ${{github.event.issue.number}} + message: | + ${{steps.process-results.outputs.result}} + + - name: Store PR comment and PR number + if: always() + env: + PR: ${{github.event.issue.number || github.event.pull_request.number}} + run: | + mkdir -p ./pr-comment + echo ${PR} > ./pr-comment/pr.txt + echo '${{steps.process-results.outputs.result}}' > ./pr-comment/comment.txt + + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + if: always() with: name: '${{ inputs.projectId }}-license-vetting-summary' path: | target/dash/review-summary target/dash/summary + + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + if: always() + with: + name: 'pr-comment' + path: | + pr-comment/pr.txt + pr-comment/comment.txt + + rerun-check: + needs: check-request + if: ${{needs.check-request.outputs.license-check == '1'}} + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Rerun license check + run: | + HEAD_SHA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} | jq -r '.head | .sha') + RUNID=$(gh api repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs | jq -r '.check_runs[] | select(.name | endswith("check-licenses")) | .html_url | capture("/runs/(?[0-9]+)/job") | .number' | sed 's/"//g' | head -n 1) + gh run rerun ${RUNID} --repo ${{ github.repository }} + env: + GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index c3c80193..cd06d63a 100644 --- a/README.md +++ b/README.md @@ -624,6 +624,11 @@ on: jobs: call-license-check: uses: eclipse-dash/dash-licenses/.github/workflows/mavenLicenseCheck.yml@master + permissions: + contents: write + issues: write + pull-requests: write + actions: write with: projectId: secrets: @@ -632,17 +637,17 @@ jobs: Projects that have to be set up in advance can use the `setupScript` parameter to pass a script that is executed before the license-check build is started. Projects that want their git submodules to be checked out and processed can use the 'submodule' parameter. -On each pull-reqest event (i.e. a new PR is created or a new commit for it is pushed) the license-status of all project dependencies is checked automatically and in case unvetted licenses are found the check fails. +On each pull-request event (i.e. a new PR is created or a new commit for it is pushed) the license-status of all project dependencies is checked automatically and in case unvetted licenses are found the check fails. Committers of that project can request a review from the IP team, by simply adding a comment with body `/request-license-review`. The github-actions bot reacts with a 'rocket' to indicate the request was understood and is processed. Attempts to request license review by non-committers are rejected with a thumps-down reaction. After the license-review build has terminated the github-action bot will reply with a comment to show the result of the license review request. -Committers can later re-run this license-check workflow from the Github actions web-interface to check for license-status changes. +Committers can later re-run this license-check workflow from the GitHub actions web-interface to check for license-status changes of by adding a comment with body `/license-check`. #### Requirements - Maven based build - Root pom.xml must reside in the repository root -- An [authentication token (scope: api) from gitlab.eclipse.org](README.md#automatic-ip-team-review-requests) has to be stored in the repositories secret store(Settings -> Scrects -> Actions) with name `M2E_GITLAB_API_TOKEN`. +- An [authentication token (scope: api) from gitlab.eclipse.org](README.md#automatic-ip-team-review-requests) has to be stored in the repositories secret store(Settings -> Secrets -> Actions) with name `M2E_GITLAB_API_TOKEN`. ## Advanced Scenarios