diff --git a/.github/workflows/check-status.yml b/.github/workflows/check-status.yml new file mode 100644 index 000000000..0e9b9db95 --- /dev/null +++ b/.github/workflows/check-status.yml @@ -0,0 +1,180 @@ +name: check-status + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + branches: + - develop + - master + - hotfix-* + - release-* + +jobs: + check-status: + runs-on: ubuntu-24.04 + steps: + - name: Check workflow statuses and display token usage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "current rest api rate usage:" + curl -s -H "Accept: application/vnd.github+json" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit | jq .rate + echo "" + echo "" + echo "current graphql rate usage:" + curl -s -H "Accept: application/vnd.github+json" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit | jq .resources.graphql + echo "" + echo "" + + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + with: + script: | + await exec.exec("sleep 20s"); + + for (let i = 0; i < 120; i++) { + const failure = []; + const cancelled = []; + const pending = []; + + const result = await github.rest.checks.listSuitesForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "${{ github.head_ref }}" + }); + result.data.check_suites.forEach(({ app: { slug }, conclusion, id}) => { + if (slug === 'github-actions') { + if (conclusion === 'failure' || conclusion === 'cancelled') { + failure.push(id); + } else if (conclusion === null) { + pending.push(id); + } + console.log(`check suite ${id} => ${conclusion === null ? 'pending' : conclusion}`); + } + }); + + if (pending.length === 0) { + core.setFailed("Cannot get pull request check status"); + return; + } + + if (failure.length > 0) { + let failureMessage = ''; + const failedCheckRuns = []; + for await (const suite_id of failure) { + const resultCheckRuns = await github.rest.checks.listForSuite({ + owner: context.repo.owner, + repo: context.repo.repo, + check_suite_id: suite_id + }); + + resultCheckRuns.data.check_runs.forEach(({ conclusion, name, html_url }) => { + if (conclusion === 'failure' || conclusion === 'cancelled') { + failedCheckRuns.push(`${name} (${conclusion})`); + } + }); + } + + core.summary.addRaw(`${failedCheckRuns.length} job(s) failed:`, true) + core.summary.addList(failedCheckRuns); + core.summary.write() + + if (failedCheckRuns.length > 0) { + core.setFailed(`${failedCheckRuns.length} job(s) failed`); + return; + } + } + + if (pending.length === 1) { + core.info("All workflows are ok"); + return; + } + + core.info(`${pending.length} workflows in progress`); + + await exec.exec("sleep 30s"); + } + + core.setFailed("Timeout: some jobs are still in progress"); + + get-environment: + if: | + contains(fromJSON('["pull_request", "pull_request_target"]') , github.event_name) && + (startsWith(github.base_ref, 'release-') || startsWith(github.base_ref, 'hotfix-')) + uses: ./.github/workflows/get-environment.yml + + check-cherry-pick: + needs: [get-environment, check-status] + runs-on: ubuntu-24.04 + if: | + contains(fromJSON('["pull_request", "pull_request_target"]') , github.event_name) && + needs.get-environment.outputs.target_stability == 'testing' && + ! contains(needs.get-environment.outputs.labels, 'skip-cherry-pick') + + steps: + - name: Check if the PR is a cherry-pick from dev branch + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + LINKED_DEV_BRANCH: develop + with: + script: | + let linkedPrs = []; + let errorMessage = `This pull request is not a cherry-pick from ${process.env.LINKED_DEV_BRANCH} or has no reference to a pull request which has been merged on ${process.env.LINKED_DEV_BRANCH}\n`; + + try { + const pull = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + const { title, body } = pull.data; + + [title, body].forEach((text) => { + const linkedPrMatches = text.matchAll(/(?:#|\/pull\/)(\d+)/g); + if (linkedPrMatches) { + [...linkedPrMatches].forEach((match) => { + linkedPrs.push(Number(match[1])); + }); + } + }); + + // remove duplicates + linkedPrs = [...new Set(linkedPrs)]; + console.log(`Linked pull requests found in PR title and body: ${linkedPrs.join(', ')}`); + } catch (e) { + throw new Error(`Failed to get information of pull request #${context.issue.number}: ${e}`); + } + + for await (const prNumber of linkedPrs) { + try { + const pull = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + if (pull.data.base.ref === process.env.LINKED_DEV_BRANCH) { + if (pull.data.state === 'closed' && pull.data.merged === true) { + console.log(`This pull request is a cherry-pick from pull request #${prNumber} on ${process.env.LINKED_DEV_BRANCH}`); + return; + } else { + errorMessage += `This pull request seems to be a cherry-pick from pull request #${prNumber} on ${process.env.LINKED_DEV_BRANCH} but it is not merged yet\n`; + } + } else { + errorMessage += `Pull request #${prNumber} is linked to ${pull.data.base.ref} instead of ${process.env.LINKED_DEV_BRANCH}\n`; + } + } catch (e) { + errorMessage += `Failed to get information on pull request #${prNumber}: ${e}\n`; + } + } + + errorMessage += `\nIf you are sure this PR does not need to be a cherry-pick from ${process.env.LINKED_DEV_BRANCH} or must be merged urgently, `; + errorMessage += `open the pull request on ${process.env.LINKED_DEV_BRANCH} and add label "skip-cherry-pick" to the PR and re-run all jobs of workflow check-status\n`; + + throw new Error(errorMessage); diff --git a/.github/workflows/get-environment.yml b/.github/workflows/get-environment.yml index 14c1eba61..e97a2d0a3 100644 --- a/.github/workflows/get-environment.yml +++ b/.github/workflows/get-environment.yml @@ -26,6 +26,9 @@ on: skip_workflow: description: "if the current workflow should be skipped" value: ${{ jobs.get-environment.outputs.skip_workflow }} + labels: + description: "list of labels on the PR" + value: ${{ jobs.get-environment.outputs.labels }} jobs: get-environment: @@ -38,6 +41,7 @@ jobs: release_type: ${{ steps.get_release_type.outputs.release_type }} is_targeting_feature_branch: ${{ steps.get_stability.outputs.is_targeting_feature_branch }} skip_workflow: ${{ steps.skip_workflow.outputs.result }} + labels: ${{ steps.has_skip_label.outputs.labels }} steps: - name: Check if PR has skip label @@ -46,14 +50,17 @@ jobs: with: script: | let hasSkipLabel = false; + let labels = []; + if (${{ contains(fromJSON('["pull_request", "pull_request_target"]') , github.event_name) }} === true) { try { - const labels = await github.rest.issues.listLabelsOnIssue({ + const fetchedLabels = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); - labels.data.forEach(({ name }) => { + fetchedLabels.data.forEach(({ name }) => { + labels.push(name); if (name === '${{ format('skip-workflow-{0}', github.workflow) }}') { hasSkipLabel = true; } @@ -62,6 +69,9 @@ jobs: core.warning(`failed to list labels: ${e}`); } } + + core.setOutput('labels', labels); + return hasSkipLabel; - name: Checkout sources (current branch) @@ -276,7 +286,8 @@ jobs: ['release_type', '${{ steps.get_release_type.outputs.release_type || 'not defined because this is not a release' }}'], ['is_targeting_feature_branch', '${{ steps.get_stability.outputs.is_targeting_feature_branch }}'], ['target_stability', '${{ steps.get_stability.outputs.target_stability || 'not defined because current run is not triggered by pull request event' }}'], - ['skip_workflow', '${{ steps.skip_workflow.outputs.result }}'] + ['skip_workflow', '${{ steps.skip_workflow.outputs.result }}'], + ['labels', '${{ steps.has_skip_label.outputs.labels }}'], ]; core.summary .addHeading(`${context.workflow} environment outputs`)