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
19 changes: 19 additions & 0 deletions docs/toml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ private-non-synced = false
#### Bots

Here are the available bots that can be added to the `bots` array:

- `datadog`: installs a GitHub App used by DataDog. Used for [CI Visibility](https://www.datadoghq.com/product/ci-cd-monitoring/). It has read access to organization members, and repository
actions, administration, checks, code scanning alerts, commit statuses, contents,
deployments, issues, pull requests, secret scanning alerts and secrets.
Expand Down Expand Up @@ -527,7 +528,25 @@ 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please check my comment on custom property types, we should elaborate better here


[Repository custom properties]: https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization

```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.

```toml
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, String>,
}

#[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, String>,
}

#[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.clone()))
.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>,

@ubiratansoares ubiratansoares Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right now we are modelling propertie values as optional string fields, but actually what we get here in the API response will depend on specifics of custom property settings:

Image

As an example, when we set the property type as multi-select we'll get an array of strings in the API response. Tested with one my repos:

gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  /repos/dotanuki-labs/testbed/properties/values

[
  {
    "property_name": "Category",
    "value": [
      "DevOps"
    ]
  }
]
Image

Perhaps we don't need to support all types of custom properties to get started, but we must clarify that we support only text-field like properties for now

}

#[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
Comment on lines +711 to +712

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since custom properties must be pre-defined at org-level to be applied, I think we should capture whether we are able to apply the custom property in the dry-run

// 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