mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
compliance: Allow Zippy release channel changes (#54572)
This adds support for tracking Zed Zippy channel changes with the compliance automation. Release Notes: - N/A
This commit is contained in:
parent
43d6ab5386
commit
75fe5a3c9b
6 changed files with 338 additions and 87 deletions
4
.github/workflows/bump_zed_version.yml
vendored
4
.github/workflows/bump_zed_version.yml
vendored
|
|
@ -160,7 +160,7 @@ jobs:
|
|||
name: steps::bot_commit
|
||||
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
||||
with:
|
||||
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview
|
||||
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview for @${{ github.actor }}
|
||||
ref: refs/heads/${{ needs.resolve_versions.outputs.preview_branch }}
|
||||
files: crates/zed/RELEASE_CHANNEL
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
|
@ -206,7 +206,7 @@ jobs:
|
|||
name: steps::bot_commit
|
||||
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
||||
with:
|
||||
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable
|
||||
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable for @${{ github.actor }}
|
||||
ref: refs/heads/${{ needs.resolve_versions.outputs.stable_branch }}
|
||||
files: crates/zed/RELEASE_CHANNEL
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,23 @@ use std::{fmt, ops::Not as _, rc::Rc};
|
|||
use itertools::Itertools as _;
|
||||
|
||||
use crate::{
|
||||
git::{CommitDetails, CommitList, ZED_ZIPPY_LOGIN},
|
||||
git::{AutomatedChangeKind, CommitDetails, CommitList, ZED_ZIPPY_LOGIN},
|
||||
github::{
|
||||
Approvable, CommitAuthor, GithubApiClient, GithubLogin, PullRequestComment,
|
||||
PullRequestData, PullRequestReview, Repository, ReviewState,
|
||||
Approvable, CommitAuthor, CommitFileChange, CommitMetadata, GithubApiClient, GithubLogin,
|
||||
PullRequestComment, PullRequestData, PullRequestReview, Repository, ReviewState,
|
||||
},
|
||||
report::Report,
|
||||
};
|
||||
|
||||
const ZED_ZIPPY_COMMENT_APPROVAL_PATTERN: &str = "@zed-zippy approve";
|
||||
const ZED_ZIPPY_GROUP_APPROVAL: &str = "@zed-industries/approved";
|
||||
const EXPECTED_VERSION_BUMP_LOC: u64 = 2;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReviewSuccess {
|
||||
ApprovingComment(Vec<PullRequestComment>),
|
||||
CoAuthored(Vec<CommitAuthor>),
|
||||
PullRequestReviewed(Vec<PullRequestReview>),
|
||||
ZedZippyCommit(GithubLogin),
|
||||
ZedZippyCommit(AutomatedChangeKind, GithubLogin),
|
||||
}
|
||||
|
||||
impl ReviewSuccess {
|
||||
|
|
@ -36,7 +35,7 @@ impl ReviewSuccess {
|
|||
.iter()
|
||||
.map(|comment| format!("@{}", comment.user.login))
|
||||
.collect_vec(),
|
||||
Self::ZedZippyCommit(login) => vec![login.to_string()],
|
||||
Self::ZedZippyCommit(_, login) => vec![login.to_string()],
|
||||
};
|
||||
|
||||
let reviewers = reviewers.into_iter().unique().collect_vec();
|
||||
|
|
@ -59,8 +58,8 @@ impl fmt::Display for ReviewSuccess {
|
|||
Self::ApprovingComment(_) => {
|
||||
formatter.write_str("Approved by an organization approval comment")
|
||||
}
|
||||
Self::ZedZippyCommit(_) => {
|
||||
formatter.write_str("Fully untampered automated version bump commit")
|
||||
Self::ZedZippyCommit(kind, _) => {
|
||||
write!(formatter, "Fully untampered automated {kind}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +70,7 @@ pub enum ReviewFailure {
|
|||
// todo: We could still query the GitHub API here to search for one
|
||||
NoPullRequestFound,
|
||||
Unreviewed,
|
||||
UnexpectedZippyAction(VersionBumpFailure),
|
||||
UnexpectedZippyAction(AutomatedChangeFailure),
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
|
|
@ -90,17 +89,25 @@ impl fmt::Display for ReviewFailure {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VersionBumpFailure {
|
||||
pub enum AutomatedChangeFailure {
|
||||
NoMentionInTitle,
|
||||
MissingCommitData,
|
||||
AuthorMismatch,
|
||||
UnexpectedCoAuthors,
|
||||
NotSigned,
|
||||
InvalidSignature,
|
||||
UnexpectedLineChanges { additions: u64, deletions: u64 },
|
||||
UnexpectedLineChanges {
|
||||
kind: AutomatedChangeKind,
|
||||
additions: u64,
|
||||
deletions: u64,
|
||||
},
|
||||
UnexpectedFiles {
|
||||
kind: AutomatedChangeKind,
|
||||
found: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionBumpFailure {
|
||||
impl fmt::Display for AutomatedChangeFailure {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NoMentionInTitle => formatter.write_str("No @-mention found in commit title"),
|
||||
|
|
@ -112,19 +119,62 @@ impl fmt::Display for VersionBumpFailure {
|
|||
Self::NotSigned => formatter.write_str("Commit is not signed"),
|
||||
Self::InvalidSignature => formatter.write_str("Commit signature is invalid"),
|
||||
Self::UnexpectedLineChanges {
|
||||
kind,
|
||||
additions,
|
||||
deletions,
|
||||
} => {
|
||||
write!(
|
||||
formatter,
|
||||
"Unexpected line changes ({additions} additions, {deletions} deletions, \
|
||||
expected {EXPECTED_VERSION_BUMP_LOC} each)"
|
||||
"Unexpected line changes for {kind} \
|
||||
({additions} additions, {deletions} deletions, \
|
||||
expected {} each)",
|
||||
kind.expected_loc()
|
||||
)
|
||||
}
|
||||
Self::UnexpectedFiles { kind, found } => {
|
||||
let expected = kind.expected_files().join(", ");
|
||||
let actual = found.join(", ");
|
||||
write!(
|
||||
formatter,
|
||||
"Unexpected files changed for {kind} \
|
||||
(expected [{expected}], found [{actual}])"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomatedChangeKind {
|
||||
fn validate_changes(
|
||||
self,
|
||||
metadata: &CommitMetadata,
|
||||
files: &[CommitFileChange],
|
||||
) -> Result<(), AutomatedChangeFailure> {
|
||||
let expected_loc = self.expected_loc();
|
||||
if metadata.additions() != expected_loc || metadata.deletions() != expected_loc {
|
||||
return Err(AutomatedChangeFailure::UnexpectedLineChanges {
|
||||
kind: self,
|
||||
additions: metadata.additions(),
|
||||
deletions: metadata.deletions(),
|
||||
});
|
||||
}
|
||||
|
||||
let files_differ = files.len() != self.expected_files().len()
|
||||
|| files
|
||||
.iter()
|
||||
.any(|f| self.expected_files().contains(&f.filename.as_str()).not());
|
||||
|
||||
if files_differ {
|
||||
return Err(AutomatedChangeFailure::UnexpectedFiles {
|
||||
kind: self,
|
||||
found: files.into_iter().map(|f| f.filename.clone()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type ReviewResult = Result<ReviewSuccess, ReviewFailure>;
|
||||
|
||||
impl<E: Into<anyhow::Error>> From<E> for ReviewFailure {
|
||||
|
|
@ -162,7 +212,7 @@ impl Reporter {
|
|||
) -> Result<ReviewSuccess, ReviewFailure> {
|
||||
let Some(pr_number) = commit.pr_number() else {
|
||||
if commit.author().is_zed_zippy() {
|
||||
return self.check_zippy_version_bump(commit).await;
|
||||
return self.check_zippy_automated_change(commit).await;
|
||||
} else {
|
||||
return Err(ReviewFailure::NoPullRequestFound);
|
||||
}
|
||||
|
|
@ -194,15 +244,15 @@ impl Reporter {
|
|||
Err(ReviewFailure::Unreviewed)
|
||||
}
|
||||
|
||||
async fn check_zippy_version_bump(
|
||||
async fn check_zippy_automated_change(
|
||||
&self,
|
||||
commit: &CommitDetails,
|
||||
) -> Result<ReviewSuccess, ReviewFailure> {
|
||||
let responsible_actor =
|
||||
let (change_kind, responsible_actor) =
|
||||
commit
|
||||
.version_bump_mention()
|
||||
.detect_automated_change()
|
||||
.ok_or(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::NoMentionInTitle,
|
||||
AutomatedChangeFailure::NoMentionInTitle,
|
||||
))?;
|
||||
|
||||
let commit_data = self
|
||||
|
|
@ -210,54 +260,54 @@ impl Reporter {
|
|||
.get_commit_metadata(&Repository::ZED, &[commit.sha()])
|
||||
.await?;
|
||||
|
||||
let authors = commit_data
|
||||
.get(commit.sha())
|
||||
.ok_or(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::MissingCommitData,
|
||||
))?;
|
||||
let metadata =
|
||||
commit_data
|
||||
.get(commit.sha())
|
||||
.ok_or(ReviewFailure::UnexpectedZippyAction(
|
||||
AutomatedChangeFailure::MissingCommitData,
|
||||
))?;
|
||||
|
||||
if !authors
|
||||
if !metadata
|
||||
.primary_author()
|
||||
.user()
|
||||
.is_some_and(|login| login.as_str() == ZED_ZIPPY_LOGIN)
|
||||
{
|
||||
return Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::AuthorMismatch,
|
||||
AutomatedChangeFailure::AuthorMismatch,
|
||||
));
|
||||
}
|
||||
|
||||
if authors.co_authors().is_some() {
|
||||
if metadata.co_authors().is_some() {
|
||||
return Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::UnexpectedCoAuthors,
|
||||
AutomatedChangeFailure::UnexpectedCoAuthors,
|
||||
));
|
||||
}
|
||||
|
||||
let signature = authors
|
||||
let signature = metadata
|
||||
.signature()
|
||||
.ok_or(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::NotSigned,
|
||||
AutomatedChangeFailure::NotSigned,
|
||||
))?;
|
||||
|
||||
if !signature.is_valid() {
|
||||
return Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::InvalidSignature,
|
||||
AutomatedChangeFailure::InvalidSignature,
|
||||
));
|
||||
}
|
||||
|
||||
if authors.additions() != EXPECTED_VERSION_BUMP_LOC
|
||||
|| authors.deletions() != EXPECTED_VERSION_BUMP_LOC
|
||||
{
|
||||
return Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::UnexpectedLineChanges {
|
||||
additions: authors.additions(),
|
||||
deletions: authors.deletions(),
|
||||
},
|
||||
));
|
||||
}
|
||||
let files = self
|
||||
.github_client
|
||||
.get_commit_files(&Repository::ZED, commit.sha())
|
||||
.await?;
|
||||
|
||||
Ok(ReviewSuccess::ZedZippyCommit(GithubLogin::new(
|
||||
responsible_actor.to_owned(),
|
||||
)))
|
||||
change_kind
|
||||
.validate_changes(metadata, &files)
|
||||
.map_err(ReviewFailure::UnexpectedZippyAction)?;
|
||||
|
||||
Ok(ReviewSuccess::ZedZippyCommit(
|
||||
change_kind,
|
||||
GithubLogin::new(responsible_actor.to_owned()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn check_commit_co_authors(
|
||||
|
|
@ -391,19 +441,23 @@ mod tests {
|
|||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::git::{CommitDetails, CommitList, CommitSha, ZED_ZIPPY_EMAIL, ZED_ZIPPY_LOGIN};
|
||||
use crate::git::{
|
||||
AutomatedChangeKind, CommitDetails, CommitList, CommitSha, ZED_ZIPPY_EMAIL, ZED_ZIPPY_LOGIN,
|
||||
};
|
||||
use crate::github::{
|
||||
AuthorAssociation, CommitMetadataBySha, GithubApiClient, GithubLogin, GithubUser,
|
||||
PullRequestComment, PullRequestData, PullRequestReview, Repository, ReviewState,
|
||||
AuthorAssociation, CommitFileChange, CommitMetadataBySha, GithubApiClient, GithubLogin,
|
||||
GithubUser, PullRequestComment, PullRequestData, PullRequestReview, Repository,
|
||||
ReviewState,
|
||||
};
|
||||
|
||||
use super::{Reporter, ReviewFailure, ReviewSuccess, VersionBumpFailure};
|
||||
use super::{AutomatedChangeFailure, Reporter, ReviewFailure, ReviewSuccess};
|
||||
|
||||
struct MockGithubApi {
|
||||
pull_request: PullRequestData,
|
||||
reviews: Vec<PullRequestReview>,
|
||||
comments: Vec<PullRequestComment>,
|
||||
commit_metadata_json: serde_json::Value,
|
||||
commit_files: Vec<CommitFileChange>,
|
||||
org_members: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -441,6 +495,14 @@ mod tests {
|
|||
serde_json::from_value(self.commit_metadata_json.clone()).map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_commit_files(
|
||||
&self,
|
||||
_repo: &Repository<'_>,
|
||||
_sha: &CommitSha,
|
||||
) -> anyhow::Result<Vec<CommitFileChange>> {
|
||||
Ok(self.commit_files.clone())
|
||||
}
|
||||
|
||||
async fn check_repo_write_permission(
|
||||
&self,
|
||||
_repo: &Repository<'_>,
|
||||
|
|
@ -546,6 +608,7 @@ mod tests {
|
|||
reviews: Vec<PullRequestReview>,
|
||||
comments: Vec<PullRequestComment>,
|
||||
commit_metadata_json: serde_json::Value,
|
||||
commit_files: Vec<CommitFileChange>,
|
||||
org_members: Vec<String>,
|
||||
commit: CommitDetails,
|
||||
}
|
||||
|
|
@ -564,6 +627,7 @@ mod tests {
|
|||
reviews: vec![],
|
||||
comments: vec![],
|
||||
commit_metadata_json: serde_json::json!({}),
|
||||
commit_files: vec![],
|
||||
org_members: vec![],
|
||||
commit: make_commit(
|
||||
"abc12345abc12345",
|
||||
|
|
@ -600,6 +664,16 @@ mod tests {
|
|||
self
|
||||
}
|
||||
|
||||
fn with_commit_files(mut self, filenames: Vec<&str>) -> Self {
|
||||
self.commit_files = filenames
|
||||
.into_iter()
|
||||
.map(|f| CommitFileChange {
|
||||
filename: f.to_owned(),
|
||||
})
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
fn zippy_version_bump() -> Self {
|
||||
Self {
|
||||
pull_request: PullRequestData {
|
||||
|
|
@ -622,6 +696,14 @@ mod tests {
|
|||
"deletions": 2
|
||||
}
|
||||
}),
|
||||
commit_files: vec![
|
||||
CommitFileChange {
|
||||
filename: "Cargo.lock".to_owned(),
|
||||
},
|
||||
CommitFileChange {
|
||||
filename: "crates/zed/Cargo.toml".to_owned(),
|
||||
},
|
||||
],
|
||||
org_members: vec![],
|
||||
commit: make_commit(
|
||||
"abc12345abc12345",
|
||||
|
|
@ -633,12 +715,49 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn zippy_release_channel_update() -> Self {
|
||||
Self {
|
||||
pull_request: PullRequestData {
|
||||
number: 0,
|
||||
user: None,
|
||||
merged_by: None,
|
||||
labels: None,
|
||||
},
|
||||
reviews: vec![],
|
||||
comments: vec![],
|
||||
commit_metadata_json: serde_json::json!({
|
||||
"abc12345abc12345": {
|
||||
"author": zippy_author(),
|
||||
"authors": { "nodes": [] },
|
||||
"signature": {
|
||||
"isValid": true,
|
||||
"signer": { "login": ZED_ZIPPY_LOGIN }
|
||||
},
|
||||
"additions": 1,
|
||||
"deletions": 1
|
||||
}
|
||||
}),
|
||||
commit_files: vec![CommitFileChange {
|
||||
filename: "crates/zed/RELEASE_CHANNEL".to_owned(),
|
||||
}],
|
||||
org_members: vec![],
|
||||
commit: make_commit(
|
||||
"abc12345abc12345",
|
||||
"Zed Zippy",
|
||||
ZED_ZIPPY_EMAIL,
|
||||
"v0.233.x stable for @cole-miller",
|
||||
"",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_scenario(self) -> Result<ReviewSuccess, ReviewFailure> {
|
||||
let mock = MockGithubApi {
|
||||
pull_request: self.pull_request,
|
||||
reviews: self.reviews,
|
||||
comments: self.comments,
|
||||
commit_metadata_json: self.commit_metadata_json,
|
||||
commit_files: self.commit_files,
|
||||
org_members: self.org_members,
|
||||
};
|
||||
let client = Rc::new(mock);
|
||||
|
|
@ -901,8 +1020,14 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn zippy_version_bump_with_valid_signature_succeeds() {
|
||||
let result = TestScenario::zippy_version_bump().run_scenario().await;
|
||||
assert!(matches!(result, Ok(ReviewSuccess::ZedZippyCommit(_))));
|
||||
if let Ok(ReviewSuccess::ZedZippyCommit(login)) = &result {
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(ReviewSuccess::ZedZippyCommit(
|
||||
AutomatedChangeKind::VersionBump,
|
||||
_
|
||||
))
|
||||
));
|
||||
if let Ok(ReviewSuccess::ZedZippyCommit(_, login)) = &result {
|
||||
assert_eq!(login.as_str(), "cole-miller");
|
||||
}
|
||||
}
|
||||
|
|
@ -922,7 +1047,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::NoMentionInTitle
|
||||
AutomatedChangeFailure::NoMentionInTitle
|
||||
))
|
||||
));
|
||||
}
|
||||
|
|
@ -943,7 +1068,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::NotSigned
|
||||
AutomatedChangeFailure::NotSigned
|
||||
))
|
||||
));
|
||||
}
|
||||
|
|
@ -968,7 +1093,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::InvalidSignature
|
||||
AutomatedChangeFailure::InvalidSignature
|
||||
))
|
||||
));
|
||||
}
|
||||
|
|
@ -993,7 +1118,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::UnexpectedLineChanges { .. }
|
||||
AutomatedChangeFailure::UnexpectedLineChanges { .. }
|
||||
))
|
||||
));
|
||||
}
|
||||
|
|
@ -1018,7 +1143,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::AuthorMismatch
|
||||
AutomatedChangeFailure::AuthorMismatch
|
||||
))
|
||||
));
|
||||
}
|
||||
|
|
@ -1043,11 +1168,42 @@ mod tests {
|
|||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
VersionBumpFailure::UnexpectedCoAuthors
|
||||
AutomatedChangeFailure::UnexpectedCoAuthors
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn zippy_version_bump_with_wrong_files_fails() {
|
||||
let result = TestScenario::zippy_version_bump()
|
||||
.with_commit_files(vec!["crates/zed/RELEASE_CHANNEL"])
|
||||
.run_scenario()
|
||||
.await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ReviewFailure::UnexpectedZippyAction(
|
||||
AutomatedChangeFailure::UnexpectedFiles { .. }
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn zippy_release_channel_update_succeeds() {
|
||||
let result = TestScenario::zippy_release_channel_update()
|
||||
.run_scenario()
|
||||
.await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(ReviewSuccess::ZedZippyCommit(
|
||||
AutomatedChangeKind::ReleaseChannelUpdate,
|
||||
_
|
||||
))
|
||||
));
|
||||
if let Ok(ReviewSuccess::ZedZippyCommit(_, login)) = &result {
|
||||
assert_eq!(login.as_str(), "cole-miller");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_zippy_commit_without_pr_is_no_pr_found() {
|
||||
let result = TestScenario::single_commit()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,37 @@ use serde::Deserialize;
|
|||
pub(crate) const ZED_ZIPPY_LOGIN: &str = "zed-zippy[bot]";
|
||||
pub(crate) const ZED_ZIPPY_EMAIL: &str = "234243425+zed-zippy[bot]@users.noreply.github.com";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AutomatedChangeKind {
|
||||
VersionBump,
|
||||
ReleaseChannelUpdate,
|
||||
}
|
||||
|
||||
impl AutomatedChangeKind {
|
||||
pub(crate) fn expected_files(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::VersionBump => &["Cargo.lock", "crates/zed/Cargo.toml"],
|
||||
Self::ReleaseChannelUpdate => &["crates/zed/RELEASE_CHANNEL"],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expected_loc(&self) -> u64 {
|
||||
match self {
|
||||
Self::VersionBump => 2,
|
||||
Self::ReleaseChannelUpdate => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AutomatedChangeKind {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::VersionBump => formatter.write_str("version bump"),
|
||||
Self::ReleaseChannelUpdate => formatter.write_str("release channel update"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Subcommand {
|
||||
type ParsedOutput: FromStr<Err = anyhow::Error>;
|
||||
|
||||
|
|
@ -246,15 +277,25 @@ impl CommitDetails {
|
|||
&self.sha
|
||||
}
|
||||
|
||||
pub(crate) fn version_bump_mention(&self) -> Option<&str> {
|
||||
pub(crate) fn detect_automated_change(&self) -> Option<(AutomatedChangeKind, &str)> {
|
||||
static VERSION_BUMP_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^Bump to [0-9]+\.[0-9]+\.[0-9]+ for @([a-zA-Z0-9][a-zA-Z0-9-]*)$").unwrap()
|
||||
});
|
||||
static RELEASE_CHANNEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^v[0-9]+\.[0-9]+\.x (?:stable|preview) for @([a-zA-Z0-9][a-zA-Z0-9-]*)$")
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
VERSION_BUMP_REGEX
|
||||
.captures(&self.title)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| m.as_str())
|
||||
.and_then(|capture| capture.get(1))
|
||||
.map(|r#match| (AutomatedChangeKind::VersionBump, r#match.as_str()))
|
||||
.or_else(|| {
|
||||
RELEASE_CHANNEL_REGEX
|
||||
.captures(&self.title)
|
||||
.and_then(|capture| capture.get(1))
|
||||
.map(|r#match| (AutomatedChangeKind::ReleaseChannelUpdate, r#match.as_str()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -630,53 +671,69 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn version_bump_mention_extracts_username() {
|
||||
fn automated_change_detects_version_bump() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}Bump to 0.230.2 for @cole-miller",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
assert_eq!(commit.version_bump_mention(), Some("cole-miller"));
|
||||
let (kind, actor) = commit.detect_automated_change().unwrap();
|
||||
assert_eq!(kind, AutomatedChangeKind::VersionBump);
|
||||
assert_eq!(actor, "cole-miller");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_bump_mention_returns_none_without_mention() {
|
||||
fn automated_change_detects_stable_release_channel() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}v0.233.x stable for @cole-miller",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
let (kind, actor) = commit.detect_automated_change().unwrap();
|
||||
assert_eq!(kind, AutomatedChangeKind::ReleaseChannelUpdate);
|
||||
assert_eq!(actor, "cole-miller");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automated_change_detects_preview_release_channel() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}v0.234.x preview for @cole-miller",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
let (kind, actor) = commit.detect_automated_change().unwrap();
|
||||
assert_eq!(kind, AutomatedChangeKind::ReleaseChannelUpdate);
|
||||
assert_eq!(actor, "cole-miller");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automated_change_returns_none_for_regular_commit() {
|
||||
let line = format!(
|
||||
"abc123{d}Alice{d}alice@test.com{d}Fix a bug",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
assert!(commit.version_bump_mention().is_none());
|
||||
assert!(commit.detect_automated_change().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_bump_mention_rejects_wrong_prefix() {
|
||||
fn automated_change_rejects_wrong_prefix() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}Fix thing for @cole-miller",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
assert!(commit.version_bump_mention().is_none());
|
||||
assert!(commit.detect_automated_change().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_bump_mention_rejects_bare_mention() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}@cole-miller bumped something",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
assert!(commit.version_bump_mention().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_bump_mention_rejects_trailing_text() {
|
||||
fn automated_change_rejects_trailing_text() {
|
||||
let line = format!(
|
||||
"abc123{d}Zed Zippy{d}bot@test.com{d}Bump to 0.230.2 for @cole-miller extra",
|
||||
d = CommitDetails::FIELD_DELIMITER
|
||||
);
|
||||
let commit = CommitDetails::parse(&line, "").unwrap();
|
||||
assert!(commit.version_bump_mention().is_none());
|
||||
assert!(commit.detect_automated_change().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@ impl CommitSignature {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CommitFileChange {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommitMetadata {
|
||||
#[serde(rename = "author")]
|
||||
|
|
@ -301,6 +306,11 @@ pub trait GithubApiClient {
|
|||
repo: &Repository<'_>,
|
||||
commit_shas: &[&CommitSha],
|
||||
) -> Result<CommitMetadataBySha>;
|
||||
async fn get_commit_files(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
sha: &CommitSha,
|
||||
) -> Result<Vec<CommitFileChange>>;
|
||||
async fn check_repo_write_permission(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
|
|
@ -430,8 +440,8 @@ mod octo_client {
|
|||
};
|
||||
|
||||
use super::{
|
||||
AuthorAssociation, CommitMetadataBySha, GithubApiClient, GithubLogin, GithubUser,
|
||||
PullRequestComment, PullRequestData, PullRequestReview, ReviewState,
|
||||
AuthorAssociation, CommitFileChange, CommitMetadataBySha, GithubApiClient, GithubLogin,
|
||||
GithubUser, PullRequestComment, PullRequestData, PullRequestReview, ReviewState,
|
||||
};
|
||||
|
||||
fn convert_author_association(association: OctocrabAuthorAssociation) -> AuthorAssociation {
|
||||
|
|
@ -612,6 +622,27 @@ mod octo_client {
|
|||
.map(|response| response.repository)
|
||||
}
|
||||
|
||||
async fn get_commit_files(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
sha: &CommitSha,
|
||||
) -> Result<Vec<CommitFileChange>> {
|
||||
let response = self
|
||||
.client
|
||||
.commits(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.get(sha.as_str())
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.files
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|file| CommitFileChange {
|
||||
filename: file.filename,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn check_repo_write_permission(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ mod tests {
|
|||
|
||||
use crate::{
|
||||
checks::{ReviewFailure, ReviewSuccess},
|
||||
git::{CommitDetails, CommitList},
|
||||
git::{AutomatedChangeKind, CommitDetails, CommitList},
|
||||
github::{AuthorAssociation, GithubLogin, GithubUser, PullRequestReview, ReviewState},
|
||||
};
|
||||
|
||||
|
|
@ -382,9 +382,10 @@ mod tests {
|
|||
);
|
||||
report.add(
|
||||
make_commit("ddd", "Dave", "dave@test.com", "Bump Version", ""),
|
||||
Ok(ReviewSuccess::ZedZippyCommit(GithubLogin::new(
|
||||
"dave".to_string(),
|
||||
))),
|
||||
Ok(ReviewSuccess::ZedZippyCommit(
|
||||
AutomatedChangeKind::VersionBump,
|
||||
GithubLogin::new("dave".to_string()),
|
||||
)),
|
||||
);
|
||||
|
||||
let summary = report.summary();
|
||||
|
|
|
|||
|
|
@ -175,7 +175,10 @@ fn create_preview_branch(
|
|||
let main_sha = StepOutput::new(&main_sha_step, "main_sha");
|
||||
|
||||
let commit_step: Step<Use> = steps::BotCommitStep::new(
|
||||
format!("{} preview", outputs.preview_branch),
|
||||
format!(
|
||||
"{} preview for @${{{{ github.actor }}}}",
|
||||
outputs.preview_branch
|
||||
),
|
||||
&outputs.preview_branch,
|
||||
&token,
|
||||
)
|
||||
|
|
@ -229,7 +232,10 @@ fn promote_to_stable(
|
|||
let write_channel = named::bash("echo -n stable > crates/zed/RELEASE_CHANNEL");
|
||||
|
||||
let commit_step: Step<Use> = steps::BotCommitStep::new(
|
||||
format!("{} stable", outputs.stable_branch),
|
||||
format!(
|
||||
"{} stable for @${{{{ github.actor }}}}",
|
||||
outputs.stable_branch
|
||||
),
|
||||
&outputs.stable_branch,
|
||||
&token,
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue