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
17 changes: 17 additions & 0 deletions docs/toml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,23 @@ branches = ["develop", "staging"]
# No branch or tag patterns specified - any branch or tag can deploy
```

### Repository custom properties

[Repository custom properties] are values set on a repository to opt it into org-wide tooling. The property must first be defined at the organization level.

Only boolean values are supported.

[Repository custom properties]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/managing-custom-properties-for-a-repository

```toml
# Repository custom properties (optional)
[custom-properties]
# Set a property name to a boolean value
crabwatch = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The property values on GitHub are strings, and the code currently converts bools to a string via its Display impl, which is a bit opaque and potentially fragile. Maybe we could just expose the value as a string?

@Sandijigs Sandijigs Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. The reason I went with bool is that @marcoieni suggested it earlier. TOML supports it natively, and the conversion to string happens on the Rust side.
I can switch to strings throughout if you'd rather, but probably worthchecking with @marcoieni first so we don't end up bouncing between the two.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was wrong on this. The fact that the property crabwatch can be only true or false isn't true for all properties, so it makes sense to use strings. Sorry about this!

```

Properties set on GitHub but not declared here are left unchanged.

### Crates.io crate management
Configure properties of crates.io crates that are deployed using Trusted Publishing from the given repository.

Expand Down
3 changes: 3 additions & 0 deletions repos/rust-lang/crabwatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ pattern = "main"
ci-checks = ["CI"]
required-approvals = 0

[custom-properties]
crabwatch = true

1 change: 1 addition & 0 deletions rust_team_data/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ pub struct Repo {
// Is the GitHub "Auto-merge" option enabled?
// https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request
pub auto_merge_enabled: bool,
pub custom_properties: IndexMap<String, bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
2 changes: 2 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,8 @@ pub(crate) struct Repo {
pub crates_io: Vec<CratesIoConfiguration>,
#[serde(default)]
pub environments: BTreeMap<String, Environment>,
#[serde(default)]
pub custom_properties: BTreeMap<String, bool>,
}

#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
Expand Down
7 changes: 6 additions & 1 deletion src/static_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ impl<'a> Generator<'a> {
},
archived,
auto_merge_enabled: !managed_by_bors,
custom_properties: r
.custom_properties
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect(),
};

self.add(&format!("v1/repos/{}.json", r.name), &repo)?;
Expand Down Expand Up @@ -491,7 +496,7 @@ impl<'a> Generator<'a> {
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
{
info!("writing API object {path}...");
let json = serde_json::to_string_pretty(obj)?;
let json = serde_json::to_string_pretty(obj)? + "\n";
self.write(path, json.as_bytes())?;

let obj2: T =
Expand Down
12 changes: 12 additions & 0 deletions src/sync/github/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,3 +746,15 @@ pub(crate) struct BranchPolicy {
fn default_branch_policy_type() -> String {
"branch".to_string()
}

/// A GitHub repository custom property. Values are strings even for booleans.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct CustomPropertyValue {
pub(crate) property_name: String,
pub(crate) value: Option<String>,
}

#[derive(Debug, serde::Serialize)]
pub(crate) struct SetCustomPropertiesRequest {
pub(crate) properties: Vec<CustomPropertyValue>,
}
29 changes: 28 additions & 1 deletion src/sync/github/api/read.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::sync::github::api;
use crate::sync::github::api::url::TokenType;
use crate::sync::github::api::{BranchPolicy, Ruleset};
use crate::sync::github::api::{BranchPolicy, CustomPropertyValue, Ruleset};
use crate::sync::github::api::{
BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, OrgAppInstallation,
Repo, RepoAppInstallation, RepoTeam, RepoUser, RestPaginatedError, Team, TeamMember, TeamRole,
Expand Down Expand Up @@ -88,6 +88,13 @@ pub(crate) trait GithubRead {
/// Returns a vector of rulesets
async fn repo_rulesets(&self, org: &str, repo: &str) -> anyhow::Result<Vec<Ruleset>>;

/// Get custom property values for a repository
async fn repo_custom_properties(
&self,
org: &str,
repo: &str,
) -> anyhow::Result<Vec<CustomPropertyValue>>;

async fn environment_branch_policies(
&self,
org: &str,
Expand Down Expand Up @@ -712,6 +719,26 @@ impl GithubRead for GitHubApiRead {
Ok(rulesets)
}

async fn repo_custom_properties(
&self,
org: &str,
repo: &str,
) -> anyhow::Result<Vec<CustomPropertyValue>> {
// REST API endpoint for repository custom property values
// https://docs.github.com/en/rest/repos/custom-properties#get-all-custom-property-values-for-a-repository
let values: Vec<CustomPropertyValue> = self
.client
.req(
Method::GET,
&GitHubUrl::repos(org, repo, "properties/values")?,
)?
.send()
.await?
.json_annotated()
.await?;
Ok(values)
}

async fn environment_branch_policies(
&self,
org: &str,
Expand Down
27 changes: 25 additions & 2 deletions src/sync/github/api/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::collections::HashSet;

use crate::sync::github::api::url::GitHubUrl;
use crate::sync::github::api::{
GitHubApiRead, GithubRead, HttpClient, Repo, RepoPermission, RepoSettings, Ruleset, RulesetOp,
Team, TeamPrivacy, TeamRole, allow_not_found,
CustomPropertyValue, GitHubApiRead, GithubRead, HttpClient, Repo, RepoPermission, RepoSettings,
Ruleset, RulesetOp, SetCustomPropertiesRequest, Team, TeamPrivacy, TeamRole, allow_not_found,
};
use crate::sync::utils::ResponseExt;

Expand Down Expand Up @@ -696,4 +696,27 @@ impl GitHubWrite {
}
Ok(())
}

/// Set a custom property value on a repository
pub(crate) async fn set_custom_property(
&self,
org: &str,
repo: &str,
property: &CustomPropertyValue,
) -> anyhow::Result<()> {
debug!(
"Setting custom property '{}' on '{}/{}'",
property.property_name, org, repo
);
if !self.dry_run {
// REST API: PATCH /repos/{owner}/{repo}/properties/values
// https://docs.github.com/en/rest/repos/custom-properties#create-or-update-custom-property-values-for-a-repository
let url = GitHubUrl::repos(org, repo, "properties/values")?;
let body = SetCustomPropertiesRequest {
properties: vec![property.clone()],
};
self.client.send(Method::PATCH, &url, &body).await?;
}
Ok(())
}
}
Loading