Skip to content

Commit

Permalink
Make "@bors-servo try" a GitHub Action
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mrobinson committed Jul 20, 2023
1 parent ae3f33b commit 1cf52c2
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 59 deletions.
110 changes: 68 additions & 42 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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";
// 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;
}
return "linux"
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
Expand All @@ -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') }}
Expand Down
121 changes: 121 additions & 0 deletions .github/workflows/try.yml
Original file line number Diff line number Diff line change
@@ -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.",
});
30 changes: 13 additions & 17 deletions etc/ci/report_aggregated_expected_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# 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"])

# 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
return None


def create_check_run(body: str, tag: str = ""):
Expand Down Expand Up @@ -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.")

Expand Down

0 comments on commit 1cf52c2

Please sign in to comment.