Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Features

- Add `--git-metadata` flag with CI auto-detection to `sentry-cli build upload` command ([#2974](https://github.com/getsentry/sentry-cli/pull/2974))
- Git metadata is now automatically collected only when running in CI environments (GitHub Actions, GitLab CI, Jenkins, etc.)
- Local development builds no longer trigger GitHub status checks by default
- Users can force enable with `--git-metadata` or disable with `--git-metadata=false`

### Improvements

- The `sentry-cli build upload` command now automatically detects the correct branch or tag reference in non-PR GitHub Actions workflows ([#2976](https://github.com/getsentry/sentry-cli/pull/2976)). Previously, `--head-ref` was only auto-detected for pull request workflows. Now it works for push, release, and other workflow types by using the `GITHUB_REF_NAME` environment variable.
Expand Down
247 changes: 152 additions & 95 deletions src/commands/build/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::utils::build::{
is_aab_file, is_apk_file, is_zip_file, normalize_directory, write_version_metadata,
};
use crate::utils::chunks::{upload_chunks, Chunk};
use crate::utils::ci::is_ci;
use crate::utils::fs::get_sha1_checksums;
use crate::utils::fs::TempDir;
use crate::utils::fs::TempFile;
Expand All @@ -32,98 +33,29 @@ use crate::utils::vcs::{
git_repo_head_ref, git_repo_remote_url,
};

pub fn make_command(command: Command) -> Command {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const HELP_TEXT: &str =
"The path to the build to upload. Supported files include Apk, Aab, XCArchive, and IPA.";
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
const HELP_TEXT: &str =
"The path to the build to upload. Supported files include Apk, and Aab.";
command
.about("Upload builds to a project.")
.org_arg()
.project_arg(false)
.arg(
Arg::new("paths")
.value_name("PATH")
.help(HELP_TEXT)
.num_args(1..)
.action(ArgAction::Append)
.required(true),
)
.arg(
Arg::new("head_sha")
.long("head-sha")
.value_parser(parse_sha_allow_empty)
.help("The VCS commit sha to use for the upload. If not provided, the current commit sha will be used.")
)
.arg(
Arg::new("base_sha")
.long("base-sha")
.value_parser(parse_sha_allow_empty)
.help("The VCS commit's base sha to use for the upload. If not provided, the merge-base of the current and remote branch will be used.")
)
.arg(
Arg::new("vcs_provider")
.long("vcs-provider")
.help("The VCS provider to use for the upload. If not provided, the current provider will be used.")
)
.arg(
Arg::new("head_repo_name")
.long("head-repo-name")
.help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.")
)
.arg(
Arg::new("base_repo_name")
.long("base-repo-name")
.help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.")
)
.arg(
Arg::new("head_ref")
.long("head-ref")
.help("The reference (branch) to use for the upload. If not provided, the current reference will be used.")
)
.arg(
Arg::new("base_ref")
.long("base-ref")
.help("The base reference (branch) to use for the upload. If not provided, the merge-base with the remote tracking branch will be used.")
)
.arg(
Arg::new("pr_number")
.long("pr-number")
.value_parser(clap::value_parser!(u32))
.help("The pull request number to use for the upload. If not provided and running \
in a pull_request-triggered GitHub Actions workflow, the PR number will be automatically \
detected from GitHub Actions environment variables.")
)
.arg(
Arg::new("build_configuration")
.long("build-configuration")
.help("The build configuration to use for the upload. If not provided, the current version will be used.")
)
.arg(
Arg::new("release_notes")
.long("release-notes")
.help("The release notes to use for the upload.")
)
/// Holds git metadata collected for build uploads.
#[derive(Debug, Default)]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is pretty similar to the VcsInfo object but the difference is that GitMetadata uses owned String types while VcsInfo uses borrowed &str.

So we could potentially remove this but we need some other mechanism to keep the values alive. I think another option is that we could also have the collect_git_metadata return a Tuple.

What are your thoughts here?

struct GitMetadata {
head_sha: Option<Digest>,
vcs_provider: String,
head_repo_name: String,
head_ref: String,
base_ref: String,
base_repo_name: String,
base_sha: Option<Digest>,
pr_number: Option<u32>,
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let path_strings = matches
.get_many::<String>("paths")
.expect("paths argument is required");

/// Collects git metadata from arguments and VCS introspection.
fn collect_git_metadata(matches: &ArgMatches, config: &Config) -> GitMetadata {
let head_sha = matches
.get_one::<Option<Digest>>("head_sha")
.map(|d| d.as_ref().cloned())
.or_else(|| Some(vcs::find_head_sha().ok()))
.flatten();

let cached_remote = config.get_cached_vcs_remote();
// Try to open the git repository and find the remote, but handle errors gracefully.
let (vcs_provider, head_repo_name, head_ref, base_ref, base_repo_name) = {
// Try to open the repo and get the remote URL, but don't fail if not in a repo.
let repo = git2::Repository::open_from_env().ok();
let repo_ref = repo.as_ref();
let remote_url = repo_ref.and_then(|repo| git_repo_remote_url(repo, &cached_remote).ok());
Expand Down Expand Up @@ -162,9 +94,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
})
.or_else(|| {
// Fallback to git repository introspection
// Note: git_repo_head_ref will return an error for detached HEAD states,
// which the error handling converts to None - this prevents sending "HEAD" as a branch name
// In that case, the user will need to provide a valid branch name.
repo_ref
.and_then(|r| match git_repo_head_ref(r) {
Ok(ref_name) => {
Expand Down Expand Up @@ -239,7 +168,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
)
};

// Track whether base_sha and base_ref were explicitly provided by the user
let base_sha_from_user = matches.get_one::<Option<Digest>>("base_sha").is_some();
let base_ref_from_user = matches.get_one::<String>("base_ref").is_some();

Expand All @@ -259,7 +187,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
let mut base_ref = base_ref;

// If base_sha equals head_sha and both were auto-inferred, skip setting base_sha and base_ref
// but keep head_sha (since comparing a commit to itself provides no meaningful baseline)
if !base_sha_from_user
&& !base_ref_from_user
&& base_sha.is_some()
Expand All @@ -273,11 +200,141 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
base_sha = None;
base_ref = "".into();
}

let pr_number = matches
.get_one("pr_number")
.copied()
.or_else(get_github_pr_number);

GitMetadata {
head_sha,
vcs_provider: vcs_provider.into_owned(),
head_repo_name: head_repo_name.into_owned(),
head_ref: head_ref.into_owned(),
base_ref: base_ref.into_owned(),
base_repo_name: base_repo_name.into_owned(),
base_sha,
pr_number,
}
}

pub fn make_command(command: Command) -> Command {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const HELP_TEXT: &str =
"The path to the build to upload. Supported files include Apk, Aab, XCArchive, and IPA.";
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
const HELP_TEXT: &str =
"The path to the build to upload. Supported files include Apk, and Aab.";
command
.about("Upload builds to a project.")
.org_arg()
.project_arg(false)
.arg(
Arg::new("paths")
.value_name("PATH")
.help(HELP_TEXT)
.num_args(1..)
.action(ArgAction::Append)
.required(true),
)
.arg(
Arg::new("head_sha")
.long("head-sha")
.value_parser(parse_sha_allow_empty)
.help("The VCS commit sha to use for the upload. If not provided, the current commit sha will be used.")
)
.arg(
Arg::new("base_sha")
.long("base-sha")
.value_parser(parse_sha_allow_empty)
.help("The VCS commit's base sha to use for the upload. If not provided, the merge-base of the current and remote branch will be used.")
)
.arg(
Arg::new("vcs_provider")
.long("vcs-provider")
.help("The VCS provider to use for the upload. If not provided, the current provider will be used.")
)
.arg(
Arg::new("head_repo_name")
.long("head-repo-name")
.help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.")
)
.arg(
Arg::new("base_repo_name")
.long("base-repo-name")
.help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.")
)
.arg(
Arg::new("head_ref")
.long("head-ref")
.help("The reference (branch) to use for the upload. If not provided, the current reference will be used.")
)
.arg(
Arg::new("base_ref")
.long("base-ref")
.help("The base reference (branch) to use for the upload. If not provided, the merge-base with the remote tracking branch will be used.")
)
.arg(
Arg::new("pr_number")
.long("pr-number")
.value_parser(clap::value_parser!(u32))
.help("The pull request number to use for the upload. If not provided and running \
in a pull_request-triggered GitHub Actions workflow, the PR number will be automatically \
detected from GitHub Actions environment variables.")
)
.arg(
Arg::new("build_configuration")
.long("build-configuration")
.help("The build configuration to use for the upload. If not provided, the current version will be used.")
)
.arg(
Arg::new("release_notes")
.long("release-notes")
.help("The release notes to use for the upload.")
)
.arg(
Arg::new("git_metadata")
.long("git-metadata")
.num_args(0..=1)
.default_missing_value("true")
.value_parser(clap::value_parser!(bool))
.help("Controls whether to collect and send git metadata (branch, commit, etc.). \
Use --git-metadata to force enable, --git-metadata=false to force disable. \
If not specified, git metadata is automatically collected only when running in a CI environment.")
)
Comment on lines +110 to +119
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm confused what clap is but I think that is the behavior that we want.

}

pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let path_strings = matches
.get_many::<String>("paths")
.expect("paths argument is required");

// Determine if we should collect git metadata
let should_collect_git_metadata =
if let Some(&git_metadata) = matches.get_one::<bool>("git_metadata") {
// --git-metadata or --git-metadata=true/false was specified
git_metadata
} else {
// Default behavior: auto-detect CI
is_ci()
};

debug!(
"Git metadata collection: {}",
if should_collect_git_metadata {
"enabled"
} else {
"disabled (use --git-metadata to enable)"
}
);

let git_metadata = if should_collect_git_metadata {
collect_git_metadata(matches, &config)
} else {
GitMetadata::default()
};

let build_configuration = matches.get_one("build_configuration").map(String::as_str);
let release_notes = matches.get_one("release_notes").map(String::as_str);

Expand Down Expand Up @@ -334,14 +391,14 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
info!("Uploading file: {}", path.display());
let bytes = ByteView::open(zip.path())?;
let vcs_info = VcsInfo {
head_sha,
base_sha,
vcs_provider: &vcs_provider,
head_repo_name: &head_repo_name,
base_repo_name: &base_repo_name,
head_ref: &head_ref,
base_ref: &base_ref,
pr_number: pr_number.as_ref(),
head_sha: git_metadata.head_sha,
base_sha: git_metadata.base_sha,
vcs_provider: &git_metadata.vcs_provider,
head_repo_name: &git_metadata.head_repo_name,
base_repo_name: &git_metadata.base_repo_name,
head_ref: &git_metadata.head_ref,
base_ref: &git_metadata.base_ref,
pr_number: git_metadata.pr_number.as_ref(),
};
match upload_file(
&authenticated_api,
Expand Down
Loading
Loading