Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ target/
.env
.DS_Store
__pycache__/
*.review.json
*.try.json
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ required.
| `--client-secret` | `OAUTH_CLIENT_SECRET`| | GitHub OAuth client secret for rollup UI (optional). |
| `--db` | `DATABASE_URL` | | Database connection string. Only PostgreSQL is supported. |
| `--cmd-prefix` | `CMD_PREFIX` | @bors | Prefix used to invoke bors commands in PR comments. |
| `--web_url` | `WEB_URL` | http://localhost:8080| Web URL where the bot's website is deployed (optional).|
| `--permissions` | `PERMISSIONS` | Rust Team API url| List of users with permissions to perform try/review (optional).|

### Special branches
The bot uses the following branch names for its operations.
Expand Down
3 changes: 3 additions & 0 deletions data/permissions/bors.review.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"github_ids": []
}
3 changes: 3 additions & 0 deletions data/permissions/bors.try.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"github_ids": []
}
6 changes: 6 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ One-time setup:
- Subscribe it to webhook events `Issue comment`, `Pull request`, `Pull request review`, `Pull request review comment` and `Workflow run`.
- Install your GitHub app on some test repository where you want to test bors.
- Don't forget to configure `rust-bors.toml` in the root of the repository, and also add some example CI workflows.
- Create try/review permissions for Github users
- Copy a review json file `cp data/permissions/bors.review.json.example data/permissions/bors.review.json`
- Copy a try json file `cp data/permissions/bors.try.json.example data/permissions/bors.try.json`
- Get your Github user `ID` `https://api.github.com/users/<your_github_user_name>`
- Edit both `bors.review.json` and `bors.try.json` files to include your GitHub `ID`: `{ "github_ids": [12345678] }`

Everytime you want to run bors:
- Run bors locally.
Expand All @@ -65,6 +70,7 @@ Everytime you want to run bors:
- Set `PRIVATE_KEY` to the private key of the app.
- (optional) Set `WEB_URL` to the public URL of the website of the app.
- (optional) Set `CMD_PREFIX` to the command prefix used to control the bot (e.g. `@bors`).
- (optional) Set `PERMISSIONS` `"data/permissions"` directory path to list users with permissions to perform try/review.
- Set up some globally reachable URL/IP address for your computer, e.g. using [ngrok](https://ngrok.com/).
- Configure the webhook URL for your app to point to `<address>/github`. You can use [gh webhook](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/using-the-github-cli-to-forward-webhooks-for-testing) for that.
- Try `@bors ping` on some PR on the test repository :)
Expand Down
11 changes: 10 additions & 1 deletion src/bin/bors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ struct Opts {
/// Web URL where the bot's website is deployed.
#[arg(long, env = "WEB_URL", default_value = "http://localhost:8080")]
web_url: String,

/// Source of list of users with permissions to perform try/review.
#[arg(
long,
env = "PERMISSIONS",
default_value = "https://team-api.infra.rust-lang.org"
)]
permissions: Option<String>,
}

/// Starts a server that receives GitHub webhooks and generates events into a queue
Expand Down Expand Up @@ -117,7 +125,8 @@ fn try_main(opts: Opts) -> anyhow::Result<()> {
let db = runtime
.block_on(initialize_db(&opts.db))
.context("Cannot initialize database")?;
let team_api = TeamApiClient::default();
// Unwrap will not panic due to default_value for the 'permissions' argument
let team_api = TeamApiClient::new(opts.permissions.as_deref().unwrap());
let (client, loaded_repos) = runtime.block_on(async {
let client = create_github_client(
opts.app_id.into(),
Expand Down
71 changes: 44 additions & 27 deletions src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,24 @@ pub(crate) struct UserPermissionsResponse {
github_ids: HashSet<UserId>,
}

enum TeamSource {
Url(String),
Directory(String),
}

pub struct TeamApiClient {
base_url: String,
team_source: TeamSource,
}

impl TeamApiClient {
pub(crate) fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
}
pub fn new(source: impl Into<String>) -> Self {
let source_str = source.into();
let team_source = if source_str.starts_with("http") {
TeamSource::Url(source_str)
} else {
TeamSource::Directory(source_str)
};
Self { team_source }
}

pub(crate) async fn load_permissions(
Expand All @@ -81,7 +90,8 @@ impl TeamApiClient {
})
}

/// Loads users that are allowed to perform try/review from the Rust Team API.
/// Loads users that are allowed to perform try/review from the Rust Team API
/// or from directory for local environment.
async fn load_users(
&self,
repository_name: &str,
Expand All @@ -92,26 +102,33 @@ impl TeamApiClient {
PermissionType::Try => "try",
};

let normalized_name = repository_name.replace('-', "_");
let url = format!(
"{}/v1/permissions/bors.{normalized_name}.{permission}.json",
self.base_url
);
let users = reqwest::get(url)
.await
.and_then(|res| res.error_for_status())
.map_err(|error| anyhow::anyhow!("Cannot load users from team API: {error:?}"))?
.json::<UserPermissionsResponse>()
.await
.map_err(|error| {
anyhow::anyhow!("Cannot deserialize users from team API: {error:?}")
})?;
Ok(users.github_ids)
}
}

impl Default for TeamApiClient {
fn default() -> Self {
Self::new("https://team-api.infra.rust-lang.org")
match &self.team_source {
TeamSource::Url(base_url) => {
let normalized_name = repository_name.replace('-', "_");
let url =
format!("{base_url}/v1/permissions/bors.{normalized_name}.{permission}.json",);
let users = reqwest::get(url)
.await
.and_then(|res| res.error_for_status())
.map_err(|error| anyhow::anyhow!("Cannot load users from team API: {error:?}"))?
.json::<UserPermissionsResponse>()
.await
.map_err(|error| {
anyhow::anyhow!("Cannot deserialize users from team API: {error:?}")
})?;
Ok(users.github_ids)
}
TeamSource::Directory(base_path) => {
let path = format!("{base_path}/bors.{permission}.json");
let data = std::fs::read_to_string(&path).map_err(|error| {
anyhow::anyhow!("Could not read users from a file '{path}': {error:?}")
})?;
let users: UserPermissionsResponse =
serde_json::from_str(&data).map_err(|error| {
anyhow::anyhow!("Cannot deserialize users from a file '{path}': {error:?}")
})?;
Ok(users.github_ids)
}
}
}
}