Skip to content

Commit 2f43302

Browse files
Simplify CLA gate workflow
1 parent d93b381 commit 2f43302

3 files changed

Lines changed: 67 additions & 179 deletions

File tree

.github/workflows/cla-gate.yml

Lines changed: 64 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,81 @@
11
name: CLA Gate
22

3-
# This workflow publishes a repository-owned commit status named `CLA Gate`.
4-
# Make `CLA Gate` required instead of requiring CLA Assistant's raw `license/cla`
5-
# status directly. That lets merge queue entries pass without waiting for CLA
6-
# Assistant to report on the synthetic merge-group SHA, while pull requests still
7-
# mirror the real CLA Assistant result.
3+
# Make this Actions check required instead of requiring CLA Assistant's raw
4+
# `license/cla` status directly. Merge queue entries pass without waiting for
5+
# CLA Assistant on the synthetic merge-group SHA, while pull requests wait for
6+
# the real CLA Assistant result on the PR head SHA.
87
#
9-
# SECURITY: This workflow uses pull_request_target so it can publish commit
10-
# statuses on external PRs. It must never check out, build, or execute PR code.
8+
# SECURITY: This workflow uses pull_request_target so it can read statuses on
9+
# external PRs. It must never check out, build, or execute PR code.
1110

1211
on:
1312
pull_request_target:
14-
types: [opened, reopened]
15-
status:
13+
types: [opened, reopened, synchronize, ready_for_review]
1614
merge_group:
1715

1816
permissions:
1917
contents: read
20-
pull-requests: read
21-
statuses: write
18+
statuses: read
19+
20+
concurrency:
21+
group: cla-gate-${{ github.event.pull_request.number || github.sha }}
22+
cancel-in-progress: true
2223

2324
jobs:
24-
publish-cla-gate-status:
25-
name: CLA status
25+
cla-gate:
26+
name: CLA Gate
2627
runs-on: ubuntu-latest
27-
if: github.event_name != 'status' || github.event.context == 'license/cla'
2828

2929
steps:
30-
- name: Check out trusted base code
31-
uses: actions/checkout@v4
32-
with:
33-
ref: ${{ github.event.repository.default_branch }}
34-
35-
- uses: dsherret/rust-toolchain-file@v1
30+
- name: Skip CLA check for merge queue
31+
if: ${{ github.event_name == 'merge_group' }}
32+
run: echo "Merge group entry; CLA was checked before entering the queue."
3633

37-
- name: Publish CLA Gate status
38-
uses: actions/github-script@v7
34+
- name: Wait for CLA Assistant
35+
if: ${{ github.event_name == 'pull_request_target' }}
3936
env:
40-
GITHUB_TOKEN: ${{ github.token }}
41-
with:
42-
script: |
43-
const { execFileSync } = require("child_process");
44-
45-
function claStatus(args) {
46-
const output = execFileSync("cargo", ["ci", "cla-assistant", "status", ...args], {
47-
encoding: "utf8",
48-
env: process.env,
49-
});
50-
return JSON.parse(output);
51-
}
52-
53-
async function postStatus({ sha, state, description, targetUrl }) {
54-
const statusContext = "CLA Gate";
55-
core.info(
56-
`Publishing ${statusContext}=${state} for ${sha}: ${description}`
57-
);
58-
59-
await github.rest.repos.createCommitStatus({
60-
owner: context.repo.owner,
61-
repo: context.repo.repo,
62-
sha,
63-
state,
64-
description,
65-
context: statusContext,
66-
target_url: targetUrl,
67-
});
68-
}
69-
70-
let targetSha;
71-
let claStatusArgs;
72-
73-
if (context.eventName === "merge_group") {
74-
await postStatus({
75-
sha: process.env.GITHUB_SHA,
76-
state: "success",
77-
description: "Merge group entry; CLA already checked before queue",
78-
});
79-
return;
80-
}
81-
82-
if (context.eventName === "status") {
83-
targetSha = context.payload.sha;
84-
claStatusArgs = ["--sha", targetSha];
85-
} else if (context.eventName === "pull_request_target") {
86-
const pr = context.payload.pull_request;
87-
targetSha = pr.head.sha;
88-
claStatusArgs = ["--pr", String(pr.number)];
89-
} else {
90-
core.setFailed(`Unsupported event type: ${context.eventName}`);
91-
return;
92-
}
93-
94-
const status = claStatus(claStatusArgs);
95-
96-
if (!status.state) {
97-
core.info(`No CLA Gate status to publish for ${targetSha}`);
98-
return;
99-
}
100-
101-
await postStatus({
102-
sha: targetSha,
103-
state: status.state,
104-
description: status.description || undefined,
105-
targetUrl: status.target_url || undefined,
106-
});
37+
GH_TOKEN: ${{ github.token }}
38+
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
39+
run: |
40+
set -euo pipefail
41+
42+
for attempt in $(seq 1 30); do
43+
status_json="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${PR_HEAD_SHA}/status")"
44+
cla_status="$(jq -c '.statuses | map(select(.context == "license/cla")) | first // empty' <<<"${status_json}")"
45+
46+
if [ -z "${cla_status}" ]; then
47+
echo "license/cla is not present on ${PR_HEAD_SHA} yet."
48+
else
49+
state="$(jq -r '.state' <<<"${cla_status}")"
50+
description="$(jq -r '.description // ""' <<<"${cla_status}")"
51+
target_url="$(jq -r '.target_url // ""' <<<"${cla_status}")"
52+
53+
echo "license/cla is ${state}: ${description}"
54+
case "${state}" in
55+
success)
56+
exit 0
57+
;;
58+
pending)
59+
;;
60+
failure|error)
61+
if [ -n "${target_url}" ]; then
62+
echo "::error title=license/cla ${state}::${description} (${target_url})"
63+
else
64+
echo "::error title=license/cla ${state}::${description}"
65+
fi
66+
exit 1
67+
;;
68+
*)
69+
echo "::error::Unexpected license/cla state: ${state}"
70+
exit 1
71+
;;
72+
esac
73+
fi
74+
75+
if [ "${attempt}" -lt 30 ]; then
76+
sleep 10
77+
fi
78+
done
79+
80+
echo "::error::Timed out waiting for license/cla to succeed on ${PR_HEAD_SHA}."
81+
exit 1

tools/ci/README.md

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -278,20 +278,6 @@ Usage: retry [OPTIONS] --pr-number <PR_NUMBER>
278278
- `--repo <REPO>`: Repository in `owner/name` form. Defaults to GITHUB_REPOSITORY
279279
- `--help`: Print help
280280

281-
#### `status`
282-
283-
**Usage:**
284-
```bash
285-
Usage: status [OPTIONS] <--pr <PR>|--sha <SHA>>
286-
```
287-
288-
**Options:**
289-
290-
- `--pr <PR>`: Pull request number whose head commit should be checked
291-
- `--sha <SHA>`: Commit SHA to check
292-
- `--repo <REPO>`: Repository in `owner/name` form. Defaults to GITHUB_REPOSITORY
293-
- `--help`: Print help
294-
295281
#### `help`
296282

297283
**Usage:**

tools/ci/src/cla_assistant.rs

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,18 @@ use std::collections::BTreeMap;
22
use std::env;
33

44
use anyhow::{anyhow, bail, Context, Result};
5-
use clap::{ArgGroup, Args, Subcommand};
5+
use clap::{Args, Subcommand};
66
use reqwest::blocking::Client;
77
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT};
88
use serde::de::DeserializeOwned;
9-
use serde::{Deserialize, Serialize};
9+
use serde::Deserialize;
1010

1111
const CLA_CONTEXT: &str = "license/cla";
1212

1313
#[derive(Subcommand)]
1414
pub(crate) enum ClaAssistantCmd {
1515
/// Retries CLA Assistant if `license/cla` is the only remaining PR blocker.
1616
Retry(RetryArgs),
17-
18-
/// Returns the `license/cla` status for a pull request or commit SHA.
19-
Status(StatusArgs),
2017
}
2118

2219
#[derive(Args)]
@@ -30,31 +27,9 @@ pub(crate) struct RetryArgs {
3027
pub(crate) repo: Option<String>,
3128
}
3229

33-
#[derive(Args)]
34-
#[command(group(
35-
ArgGroup::new("target")
36-
.required(true)
37-
.multiple(false)
38-
.args(["pr", "sha"]),
39-
))]
40-
pub(crate) struct StatusArgs {
41-
/// Pull request number whose head commit should be checked.
42-
#[arg(long)]
43-
pub(crate) pr: Option<u64>,
44-
45-
/// Commit SHA to check.
46-
#[arg(long)]
47-
pub(crate) sha: Option<String>,
48-
49-
/// Repository in `owner/name` form. Defaults to GITHUB_REPOSITORY.
50-
#[arg(long)]
51-
pub(crate) repo: Option<String>,
52-
}
53-
5430
pub(crate) fn run(cmd: ClaAssistantCmd) -> Result<()> {
5531
match cmd {
5632
ClaAssistantCmd::Retry(args) => retry(args),
57-
ClaAssistantCmd::Status(args) => status(args),
5833
}
5934
}
6035

@@ -66,33 +41,6 @@ fn retry(args: RetryArgs) -> Result<()> {
6641
retry_for_pr(&client, &repo, args.pr_number)
6742
}
6843

69-
fn status(args: StatusArgs) -> Result<()> {
70-
let repo = Repo::from_arg_or_env(args.repo)?;
71-
let token = env::var("GITHUB_TOKEN").context("GITHUB_TOKEN is required")?;
72-
let client = GithubClient::new(token)?;
73-
74-
let sha = match (args.pr, args.sha) {
75-
(Some(pr_number), None) => client.pull_request(&repo, pr_number)?.head.sha,
76-
(None, Some(sha)) => sha,
77-
_ => unreachable!("clap requires exactly one of --pr or --sha"),
78-
};
79-
80-
let statuses = client.list_statuses(&repo, &sha)?;
81-
let latest_statuses = latest_status_by_context(statuses);
82-
let output = latest_statuses.get(CLA_CONTEXT).map_or_else(
83-
|| ClaStatusOutput::missing(sha.clone()),
84-
|status| ClaStatusOutput {
85-
sha: sha.clone(),
86-
state: Some(status.state.clone()),
87-
description: status.description.clone(),
88-
target_url: status.target_url.clone(),
89-
},
90-
);
91-
92-
println!("{}", serde_json::to_string(&output)?);
93-
Ok(())
94-
}
95-
9644
fn retry_for_pr(client: &GithubClient, repo: &Repo, pr_number: u64) -> Result<()> {
9745
println!("Inspecting PR #{pr_number}");
9846

@@ -217,29 +165,8 @@ struct CombinedStatusResponse {
217165
statuses: Vec<CommitStatus>,
218166
}
219167

220-
#[derive(Clone, Deserialize)]
168+
#[derive(Deserialize)]
221169
struct CommitStatus {
222170
context: String,
223171
state: String,
224-
description: Option<String>,
225-
target_url: Option<String>,
226-
}
227-
228-
#[derive(Serialize)]
229-
struct ClaStatusOutput {
230-
sha: String,
231-
state: Option<String>,
232-
description: Option<String>,
233-
target_url: Option<String>,
234-
}
235-
236-
impl ClaStatusOutput {
237-
fn missing(sha: String) -> Self {
238-
Self {
239-
sha,
240-
state: None,
241-
description: None,
242-
target_url: None,
243-
}
244-
}
245172
}

0 commit comments

Comments
 (0)