Skip to content
Merged
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
16 changes: 16 additions & 0 deletions apps/docs/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ In this example, if only `README.md` changed in a package, `build` would not run
selected, regardless of individual task `inputs`.
</Callout>

#### `githubActionsRemoteBaseRefFallback`

Default: `false`

When GitHub Actions reports a pull request's base branch but that branch is not available as a local ref, fall back to `origin/<branch>`. This supports detached checkouts created by `actions/checkout`, where the base branch may only exist as a remote-tracking ref.

Local refs continue to take precedence. This flag only affects base refs inferred from GitHub Actions. Explicit refs provided through `TURBO_SCM_BASE` and default `main` or `master` resolution are unchanged.

```jsonc title="./turbo.json"
{
"futureFlags": {
"githubActionsRemoteBaseRefFallback": true,
},
}
```

#### `watchUsingTaskInputs`

Default: `false`
Expand Down
9 changes: 8 additions & 1 deletion crates/turborepo-lib/src/run/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,11 +479,18 @@ impl RunBuilder {
let repo_index_task = {
let repo_root = self.repo_root.clone();
let git_root = self.opts.git_root.clone();
let github_actions_remote_base_ref_fallback = self
.opts
.future_flags
.github_actions_remote_base_ref_fallback;
tokio::task::spawn_blocking(move || {
let scm = match git_root {
Some(root) => SCM::new_with_git_root(&repo_root, root),
None => SCM::new(&repo_root),
};
}
.with_github_actions_remote_base_ref_fallback(
github_actions_remote_base_ref_fallback,
);
// The tracked half of the repo index only needs `.git/index`.
let tracked_index = {
let _span = tracing::info_span!("build_tracked_repo_index_gix").entered();
Expand Down
8 changes: 8 additions & 0 deletions crates/turborepo-schema-gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,14 @@ export interface FutureFlags {
* @defaultValue `false`
*/
affectedUsingTaskInputs?: boolean;
/**
* When GitHub Actions reports a base branch that is not available as a
* local ref, fall back to `origin/<branch>`. This supports detached
* checkouts where only remote-tracking refs are present.
*
* @defaultValue `false`
*/
githubActionsRemoteBaseRefFallback?: boolean;
/**
* Use task-level `inputs` globs to determine which tasks to re-run when
* files change in `turbo watch`. When enabled, only tasks whose declared
Expand Down
92 changes: 84 additions & 8 deletions crates/turborepo-scm/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,21 +491,36 @@ impl GitRepo {
// because at this point we know we're in a GITHUB CI environment
// and we should really know by now what the base ref is
// so it's better to just error if something went wrong
return match self
.execute_git_command(&["rev-parse", "--end-of-options", &github_base_ref], "")
{
let local_result =
self.execute_git_command(&["rev-parse", "--end-of-options", &github_base_ref], "");
match local_result {
Ok(_) => {
eprintln!("Resolved base ref from GitHub Actions event: {github_base_ref}");
Ok(github_base_ref)
return Ok(github_base_ref);
}
Err(e) => {
Err(local_error) if self.github_actions_remote_base_ref_fallback => {
let remote_ref = format!("origin/{github_base_ref}");
match self
.execute_git_command(&["rev-parse", "--end-of-options", &remote_ref], "")
{
Ok(_) => {
eprintln!("Resolved base ref from GitHub Actions event: {remote_ref}");
return Ok(remote_ref);
}
Err(remote_error) => eprintln!(
"Failed to resolve base ref '{github_base_ref}' ({local_error}) or \
'{remote_ref}' ({remote_error}) from GitHub Actions event"
),
}
}
Err(error) => {
eprintln!(
"Failed to resolve base ref '{github_base_ref}' from GitHub Actions \
event: {e}"
event: {error}"
);
Err(Error::UnableToResolveRef)
}
};
}
return Err(Error::UnableToResolveRef);
}

default_base_ref(|branch| self.execute_git_command(&["rev-parse", branch], "").is_ok())
Expand Down Expand Up @@ -1252,6 +1267,7 @@ mod tests {
for (branches, expected) in [
(vec!["main"], Some("main")),
(vec!["master"], Some("master")),
(vec!["origin/main"], None),
(vec!["ziltoid"], None),
(vec!["ziltoid", "main"], Some("main")),
(vec!["ziltoid", "master"], Some("master")),
Expand All @@ -1264,6 +1280,65 @@ mod tests {
}
}

#[test]
fn test_github_base_ref_remote_fallback_respects_flag() -> Result<(), Error> {
let (repo_root, repo_path) = setup_repository(Some("main"))?;
let root = AbsoluteSystemPathBuf::try_from(repo_root.path()).unwrap();
let file = root.join_component("todo.txt");
file.create_with_contents("test remote base ref fallback")?;
let commit = commit_file(&repo_path, Path::new("todo.txt"), None);

run_git(&repo_path, &["checkout", "--detach", &commit]);
run_git(&repo_path, &["branch", "-D", "main"]);
run_git(
&repo_path,
&["update-ref", "refs/remotes/origin/main", &commit],
);

let github_env = || CIEnv {
is_github_actions: true,
github_base_ref: Ok("main".to_string()),
github_event_path: Err(VarError::NotPresent),
};
let mut git = GitRepo::find(&root).unwrap();

assert_matches!(
git.resolve_base(None, github_env()),
Err(Error::UnableToResolveRef)
);
git.github_actions_remote_base_ref_fallback = true;
assert_matches!(
git.resolve_base(None, CIEnv::none()),
Err(Error::UnableToResolveRef)
);
assert_eq!(
git.resolve_base(None, github_env())?,
"origin/main".to_string()
);

Ok(())
}

#[test]
fn test_github_base_ref_remote_fallback_errors_when_refs_are_missing() -> Result<(), Error> {
let (repo_root, repo_path) = setup_repository(Some("main"))?;
let root = AbsoluteSystemPathBuf::try_from(repo_root.path()).unwrap();
let file = root.join_component("todo.txt");
file.create_with_contents("test missing base refs")?;
commit_file(&repo_path, Path::new("todo.txt"), None);

let mut git = GitRepo::find(&root).unwrap();
git.github_actions_remote_base_ref_fallback = true;
let env = CIEnv {
is_github_actions: true,
github_base_ref: Ok("missing".to_string()),
github_event_path: Err(VarError::NotPresent),
};

assert_matches!(git.resolve_base(None, env), Err(Error::UnableToResolveRef));
Ok(())
}

#[test]
fn test_base_resolution_uses_override_before_default_branches() -> Result<(), Error> {
let (repo_root, repo_path) = setup_repository(Some("main"))?;
Expand Down Expand Up @@ -2351,6 +2426,7 @@ mod tests {
root: root.to_owned(),
bin,
attrs: std::sync::OnceLock::new(),
github_actions_remote_base_ref_fallback: false,
slowest_files: None,
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/turborepo-scm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ pub struct GitRepo {
root: AbsoluteSystemPathBuf,
bin: AbsoluteSystemPathBuf,
attrs: OnceLock<Option<crlf::GitAttrs>>,
github_actions_remote_base_ref_fallback: bool,
/// Optional recorder for the slowest-to-hash files. Set by long-running
/// consumers (the file watcher) so they can diagnose a stalled startup.
slowest_files: Option<std::sync::Arc<SlowestFiles>>,
Expand All @@ -250,6 +251,7 @@ impl Clone for GitRepo {
root: self.root.clone(),
bin: self.bin.clone(),
attrs: OnceLock::new(),
github_actions_remote_base_ref_fallback: self.github_actions_remote_base_ref_fallback,
slowest_files: self.slowest_files.clone(),
}
}
Expand Down Expand Up @@ -284,6 +286,7 @@ impl GitRepo {
root,
bin,
attrs: OnceLock::new(),
github_actions_remote_base_ref_fallback: false,
slowest_files: None,
})
}
Expand Down Expand Up @@ -355,6 +358,7 @@ impl SCM {
root: git_root,
bin,
attrs: OnceLock::new(),
github_actions_remote_base_ref_fallback: false,
slowest_files: None,
}),
Err(e) => {
Expand All @@ -367,6 +371,13 @@ impl SCM {
}
}

pub fn with_github_actions_remote_base_ref_fallback(mut self, enabled: bool) -> Self {
if let SCM::Git(git) = &mut self {
git.github_actions_remote_base_ref_fallback = enabled;
}
self
}

/// Attach a recorder that tracks the slowest-to-hash files. Long-running
/// consumers (the file watcher) use this to diagnose a stalled startup
/// caused by hashing a very large file. No-op for `SCM::Manual`.
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-scm/src/repo_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,7 @@ mod tests {
root: root.clone(),
bin: root,
attrs: OnceLock::new(),
github_actions_remote_base_ref_fallback: false,
slowest_files: None,
}
}
Expand Down
31 changes: 19 additions & 12 deletions crates/turborepo-turbo-json/src/future_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ pub struct FutureFlags {
/// selecting all tasks in changed packages.
#[serde(default)]
pub affected_using_task_inputs: bool,
/// When GitHub Actions reports a base branch that is not available as a
/// local ref, fall back to `origin/<branch>`. This supports detached
/// checkouts where only remote-tracking refs are present.
#[serde(default)]
pub github_actions_remote_base_ref_fallback: bool,
/// Use task-level `inputs` globs to determine which tasks to re-run when
/// files change in `turbo watch`. When enabled, only tasks whose declared
/// inputs match the changed files are re-executed, rather than re-running
Expand Down Expand Up @@ -158,37 +163,39 @@ impl TS for FutureFlags {

fn inline() -> String {
"{ errorsOnlyShowHash?: boolean, experimentalObservability?: boolean, longerSignatureKey?: \
boolean, affectedUsingTaskInputs?: boolean, watchUsingTaskInputs?: boolean, \
pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: boolean, \
strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
boolean, affectedUsingTaskInputs?: boolean, githubActionsRemoteBaseRefFallback?: boolean, \
watchUsingTaskInputs?: boolean, pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: \
boolean, strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
experimentalCargoWorkspaces?: boolean, experimentalTaskCommand?: boolean }"
.to_string()
}

fn inline_flattened() -> String {
"{ errorsOnlyShowHash?: boolean, experimentalObservability?: boolean, longerSignatureKey?: \
boolean, affectedUsingTaskInputs?: boolean, watchUsingTaskInputs?: boolean, \
pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: boolean, \
strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
boolean, affectedUsingTaskInputs?: boolean, githubActionsRemoteBaseRefFallback?: boolean, \
watchUsingTaskInputs?: boolean, pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: \
boolean, strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
experimentalCargoWorkspaces?: boolean, experimentalTaskCommand?: boolean }"
.to_string()
}

fn decl() -> String {
"type FutureFlags = { errorsOnlyShowHash?: boolean, experimentalObservability?: boolean, \
longerSignatureKey?: boolean, affectedUsingTaskInputs?: boolean, watchUsingTaskInputs?: \
boolean, pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: boolean, \
strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
experimentalCargoWorkspaces?: boolean, experimentalTaskCommand?: boolean };"
boolean, githubActionsRemoteBaseRefFallback?: boolean, pruneIncludesGlobalFiles?: \
boolean, filterUsingTasks?: boolean, strictTaskEntrypointSelection?: boolean, \
globalConfiguration?: boolean, experimentalCargoWorkspaces?: boolean, \
experimentalTaskCommand?: boolean };"
.to_string()
}

fn decl_concrete() -> String {
"type FutureFlags = { errorsOnlyShowHash?: boolean, experimentalObservability?: boolean, \
longerSignatureKey?: boolean, affectedUsingTaskInputs?: boolean, watchUsingTaskInputs?: \
boolean, pruneIncludesGlobalFiles?: boolean, filterUsingTasks?: boolean, \
strictTaskEntrypointSelection?: boolean, globalConfiguration?: boolean, \
experimentalCargoWorkspaces?: boolean, experimentalTaskCommand?: boolean };"
boolean, githubActionsRemoteBaseRefFallback?: boolean, pruneIncludesGlobalFiles?: \
boolean, filterUsingTasks?: boolean, strictTaskEntrypointSelection?: boolean, \
globalConfiguration?: boolean, experimentalCargoWorkspaces?: boolean, \
experimentalTaskCommand?: boolean };"
.to_string()
}

Expand Down
33 changes: 33 additions & 0 deletions crates/turborepo-turbo-json/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,39 @@ mod tests {
assert!(turbo_json.unwrap().future_flags.longer_signature_key);
}

#[test]
fn test_deserialize_future_flags_github_actions_remote_base_ref_fallback() {
let json = r#"{
"tasks": {},
"futureFlags": {
"githubActionsRemoteBaseRefFallback": true
}
}"#;

let (deserialized, diagnostics) = deserialize_from_json_str(
json,
JsonParserOptions::default().with_allow_comments(),
"turbo.json",
);
assert!(diagnostics.is_empty());
let raw_turbo_json: RawTurboJson = deserialized.unwrap();
assert!(
raw_turbo_json
.future_flags
.as_ref()
.unwrap()
.as_inner()
.github_actions_remote_base_ref_fallback
);

let turbo_json = TurboJson::try_from(raw_turbo_json).unwrap();
assert!(
turbo_json
.future_flags
.github_actions_remote_base_ref_fallback
);
}

#[test]
fn test_is_root_config_with_root_path() {
let turbo_json = TurboJson {
Expand Down
5 changes: 5 additions & 0 deletions packages/turbo-types/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@
"default": false,
"type": "boolean"
},
"githubActionsRemoteBaseRefFallback": {
"description": "When GitHub Actions reports a base branch that is not available as a local ref, fall back to `origin/<branch>`. This supports detached checkouts where only remote-tracking refs are present.",
"default": false,
"type": "boolean"
},
"strictTaskEntrypointSelection": {
"description": "Select requested task entrypoints according to whether the task resolves a command in the repository. When any package can run a requested task, packages without a command are not used as entrypoints. Tasks with no command anywhere remain available for graph-only orchestration, and missing tasks reached as dependencies remain in the Task Graph.",
"default": false,
Expand Down
5 changes: 5 additions & 0 deletions packages/turbo-types/schemas/schema.v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@
"default": false,
"type": "boolean"
},
"githubActionsRemoteBaseRefFallback": {
"description": "When GitHub Actions reports a base branch that is not available as a local ref, fall back to `origin/<branch>`. This supports detached checkouts where only remote-tracking refs are present.",
"default": false,
"type": "boolean"
},
"errorsOnlyShowHash": {
"description": "When using `outputLogs: \"errors-only\"`, show task hashes when tasks complete successfully. This provides visibility into which tasks are running without showing full output logs.",
"default": false,
Expand Down
8 changes: 8 additions & 0 deletions packages/turbo-types/src/types/config-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ export interface FutureFlags {
* @defaultValue `false`
*/
affectedUsingTaskInputs?: boolean;
/**
* When GitHub Actions reports a base branch that is not available as a
* local ref, fall back to `origin/<branch>`. This supports detached
* checkouts where only remote-tracking refs are present.
*
* @defaultValue `false`
*/
githubActionsRemoteBaseRefFallback?: boolean;
/**
* Use task-level `inputs` globs to determine which tasks to re-run when
* files change in `turbo watch`. When enabled, only tasks whose declared
Expand Down
Loading