From 7508d8321a226970137e098ca12b063f92f468c1 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 24 Jul 2023 17:15:33 +0200 Subject: [PATCH] Make "@bors-servo try" a GitHub Action (#30014) This is the last piece of the puzzle to turning off bors. This makes functionality provided by bors to understand "@bors-servo try" a GitHub Action. For now the syntax is more or less the same, but we can modify it in the future and even add support for custom configuration options (more specific build combinations or even passing compiler flags). The big difference between this and what bors does is that there is no merge commit. GitHub simply runs tests on the version of the branch that is on a pull request. There is always the risk that tests might start failing when a branch is rebased, but this offers a bit more control because you can easily rebase from the PR and the merge queue will check this as well. --- .github/workflows/main.yml | 112 ++++++++++------- .github/workflows/try.yml | 121 +++++++++++++++++++ etc/ci/report_aggregated_expected_results.py | 30 ++--- 3 files changed, 203 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/try.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index acb9c4f05a3..1deafed2378 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,80 +11,106 @@ on: branches: ["**"] merge_group: types: [checks_requested] + workflow_call: + inputs: + platform: + required: true + type: string + layout: + required: true + type: string + unit-tests: + required: true + type: boolean workflow_dispatch: + inputs: + platform: + required: false + type: choice + options: ["none", "linux", "windows", "macos", "all", "sync"] + layout: + required: false + type: choice + options: ["none", "2013", "2020", "all"] + unit-tests: + required: false + type: boolean jobs: decision: name: Decision runs-on: ubuntu-20.04 outputs: - skipped: ${{ steps.skipDecision.outputs.result }} - platforms: ${{ steps.platformDecision.outputs.result }} + configuration: ${{ steps.configuration.outputs.result }} steps: - - name: Skip Decision - id: skipDecision + - name: Configuration + id: configuration uses: actions/github-script@v6 with: - result-encoding: string script: | - // Never skip workflow runs for pull requests or merge groups, which might - // need to actually run / retry WPT tests. - if (context.eventName == "pull_request" || context.eventName == "merge_group") { - return "run"; + // Never skip workflow runs for pull requests, merge groups, or manually triggered + // workfows / try jobs, which might need to actually run / retry WPT tests. + if (!['pull_request', 'merge_group', 'workflow_run', 'workflow_call'].includes(context.eventName)) { + // Skip the run if an identical run already exists. This helps to avoid running + // the workflow over and over again for the same commit hash. + if ((await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "main.yml", + head_sha: context.sha, + status: "success", + })).data.workflow_runs.length > 0) { + console.log("Skipping workflow, because of duplicate job."); + return { platform: "none" }; + } } - // Skip the run if an identical run already exists. This helps to avoid running - // the workflow over and over again for the same commit hash. - if ((await github.rest.actions.listWorkflowRuns({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "main.yml", - head_sha: context.sha, - status: "success", - })).data.workflow_runs.length > 0) { - return "skip" - } else { - return "run" - } - - name: Platform Decision - id: platformDecision - uses: actions/github-script@v6 - with: - result-encoding: string - script: | - if ("${{ steps.skipDecision.outputs.result }}" == "skip") { - return "none"; - } - if (context.eventName == "push" || context.eventName == "merge_group") { - return "all"; - } - return "linux" + // We need to pick defaults if the inputs are not provided. Unprovided inputs + // are empty strings in this template. + let platform = "${{ inputs.platform }}" || "linux"; + let layout = "${{ inputs.layout }}" || "none"; + let unit_tests = Boolean(${{ inputs.unit-tests }}) + + // Merge queue runs and pushes to master should always trigger a full build and test. + if (["push", "merge_group"].includes(context.eventName)) { + platform = "all"; + layout = "all"; + unit_tests = true; + } + + let returnValue = { + platform, + layout, + unit_tests, + }; + console.log("Using configuration: " + JSON.stringify(returnValue)); + return returnValue; build-win: name: Windows needs: ["decision"] - if: ${{ needs.decision.outputs.platforms == 'all' }} + if: ${{ contains(fromJson('["windows", "all"]'), fromJson(needs.decision.outputs.configuration).platform) }} uses: ./.github/workflows/windows.yml with: - unit-tests: true + unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} build-mac: name: Mac needs: ["decision"] - if: ${{ needs.decision.outputs.platforms == 'all' }} + if: ${{ contains(fromJson('["macos", "all"]'), fromJson(needs.decision.outputs.configuration).platform) }} uses: ./.github/workflows/mac.yml with: - unit-tests: true + unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} build-linux: name: Linux needs: ["decision"] - if: ${{ needs.decision.outputs.platforms == 'all' || needs.decision.outputs.platforms == 'linux' }} + if: ${{ contains(fromJson('["linux", "all"]'), fromJson(needs.decision.outputs.configuration).platform) }} uses: ./.github/workflows/linux.yml with: wpt: 'test' - layout: ${{ (github.event_name == 'push' || github.event_name == 'merge_group') && 'all' || 'none' }} - unit-tests: ${{ github.event_name == 'push' || github.event_name == 'merge_group' }} + layout: ${{ fromJson(needs.decision.outputs.configuration).layout }} + unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} build-result: name: Result @@ -99,7 +125,7 @@ jobs: steps: - name: Mark skipped jobs as successful - if: ${{ needs.decision.outputs.skipped == 'skip' }} + if: ${{ fromJson(needs.decision.outputs.configuration).platform == 'none' }} run: exit 0 - name: Mark the job as successful if: ${{ !contains(join(needs.*.result, ','), 'failure') && !contains(join(needs.*.result, ','), 'cancelled') }} diff --git a/.github/workflows/try.yml b/.github/workflows/try.yml new file mode 100644 index 00000000000..336f2935b93 --- /dev/null +++ b/.github/workflows/try.yml @@ -0,0 +1,121 @@ +on: issue_comment +name: Try + +jobs: + parse-comment: + name: Process Comment + if: ${{ github.event.issue.pull_request }} + runs-on: ubuntu-latest + outputs: + configuration: ${{ steps.configuration.outputs.result }} + steps: + - uses: actions/github-script@v6 + id: configuration + with: + script: | + function makeComment(body) { + console.log(body); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }) + } + + let tokens = "${{ github.event.comment.body }}".split(" "); + let tagIndex = tokens.indexOf("@bors-servo"); + if (tagIndex == -1 || tagIndex + 1 >= tokens.length) { + return { try: false }; + } + + let tryString = tokens[tagIndex + 1]; + console.log("Found try string: '" + tryString + "'"); + let returnValue = { try: false }; + if (tryString == "try") { + returnValue = { try: true, platform: 'all', layout: 'all', unit_tests: true, }; + } else if (tryString == "try=wpt") { + returnValue = { try: true, platform: 'linux', layout: '2013', unit_tests: false }; + } else if (tryString == "try=wpt-2020") { + returnValue = { try: true, platform: 'linux', layout: '2020', unit_tests: false }; + } else if (tryString == "try=linux") { + returnValue = { try: true, platform: 'linux', layout: 'none', unit_tests: true }; + } else if (tryString == "try=mac") { + returnValue = { try: true, platform: 'macos', layout: 'none', unit_tests: true }; + } else if (tryString == "try=windows") { + returnValue = { try: true, platform: 'windows', layout: 'none', unit_tests: true }; + } else { + makeComment("🤔 Unknown try string '" + tryString + "'"); + return returnValue; + } + + if (returnValue.try) { + let result = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: "${{ github.event.sender.login }}" + }); + if (!result.data.user.permissions.push) { + makeComment('🔒 User @${{ github.event.sender.login }} does not have permission to trigger try jobs.'); + return { try: false }; + } + } + + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + makeComment("🔨 Triggering try run (" + formattedURL + ") with platform=" + returnValue.platform + " and layout=" + returnValue.layout); + return returnValue; + + run-try: + name: Run Try + needs: ["parse-comment"] + if: ${{ fromJson(needs.parse-comment.outputs.configuration).try}} + uses: ./.github/workflows/main.yml + with: + platform: ${{ fromJson(needs.parse-comment.outputs.configuration).platform }} + layout: ${{ fromJson(needs.parse-comment.outputs.configuration).layout }} + unit-tests: ${{ fromJson(needs.parse-comment.outputs.configuration).unit_tests }} + + results: + name: Results + needs: ["parse-comment", "run-try"] + runs-on: ubuntu-latest + if: ${{ always() && fromJson(needs.parse-comment.outputs.configuration).try}} + steps: + - name: Success + if: ${{ !contains(join(needs.*.result, ','), 'failure') }} + uses: actions/github-script@v6 + with: + script: | + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "✨ Try run (" + formattedURL + ") " + "succeeded.", + }); + - name: Failure + if: ${{ contains(join(needs.*.result, ','), 'failure') }} + uses: actions/github-script@v6 + with: + script: | + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "⚠️ Try run (" + formattedURL + ") " + "failed.", + }); + + diff --git a/etc/ci/report_aggregated_expected_results.py b/etc/ci/report_aggregated_expected_results.py index 177e9266bd6..832fc6f0ba8 100755 --- a/etc/ci/report_aggregated_expected_results.py +++ b/etc/ci/report_aggregated_expected_results.py @@ -156,28 +156,24 @@ def get_github_run_url() -> Optional[str]: return f"[#{run_id}](https://github.com/{repository}/actions/runs/{run_id})" -def is_pr_open(pr_number: str) -> bool: - return b"open" == subprocess.check_output( - ["gh", "api", f"/repos/servo/servo/pulls/{pr_number}", "--template", "{{.state}}"]) - - def get_pr_number() -> Optional[str]: github_context = json.loads(os.environ.get("GITHUB_CONTEXT", "{}")) if "event" not in github_context: return None - if "head_commit" not in github_context["event"]: - return None - commit_title = github_context["event"]["head_commit"]["message"] - match = re.match(r"^Auto merge of #(\d+)", commit_title) - if not match: - return None + # If we have a 'merge_group' in the context, this was triggered by + # the merge queue. + if "merge_group" in github_context["event"]: + commit_title = github_context["event"]["merge_group"]["head_commit"]["message"] + match = re.match(r"\(#(\d+)\)$", commit_title) + return match.group(1) if match else None - # Only return a PR number if the PR is open. bors will often push old merges - # onto the HEAD of try branches and we don't want to return results for these - # old PRs. - number = match.group(1) - return number if is_pr_open(number) else None + # If we have an 'issue' in the context, this was triggered by a try comment + # on a PR. + if "issue" in github_context["event"]: + return str(github_context["event"]["issue"]["number"]) + + return None def create_check_run(body: str, tag: str = ""): @@ -246,7 +242,7 @@ def main(): if pr_number: process = subprocess.Popen( ['gh', 'pr', 'comment', pr_number, '-F', '-'], stdin=subprocess.PIPE) - process.communicate(input=html_string.encode("utf-8"))[0] + print(process.communicate(input=html_string.encode("utf-8"))[0]) else: print("Could not find PR number in environment. Not making GitHub comment.")