mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
Reuse cached GitHub installation tokens for run fetches and pushes
The worker minted one read token at launch and a fresh push token for every checkpoint. The read token expired an hour into a run, so a workspace acquired later fetched with a dead credential. Minting per push put every push in GitHub's token-replication window, where a token minted moments earlier is rejected with 404 "Repository not found". The worker now keeps two InstallationTokenSource caches for the run, a read-only one for fetches and a contents: write one for pushes. Each fetch and push resolves through its source, which reuses one token until it nears expiry and then mints the next. Petri's RunSource asks a SourceCredentials provider on every fetch instead of holding a fixed credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
aad954693e
commit
23591d550f
7 changed files with 190 additions and 80 deletions
|
|
@ -318,7 +318,7 @@ Every commit a run creates is authored and committed by the run's GitHub credent
|
|||
|
||||
### Checkpoint pushing
|
||||
|
||||
After each workflow stage, Fabro [checkpoints](/execution/checkpoints) the workspace on the run branch, `fabro/run/<run-id>`, and pushes it directly from that workspace. Fabro keeps no second Git repository or checkpoint bundles on the server. In App mode, the worker mints a fresh Installation Access Token with `contents: write` for each checkpoint push and passes it only to the sandbox Git command. The token is not saved in the repository's remote URL or configuration. `[run.run_branch] push = false` keeps the branch only in the run workspace.
|
||||
After each workflow stage, Fabro [checkpoints](/execution/checkpoints) the workspace on the run branch, `fabro/run/<run-id>`, and pushes it directly from that workspace. Fabro keeps no second Git repository or checkpoint bundles on the server. In App mode, the worker pushes with an Installation Access Token with `contents: write`, reusing one token until it nears expiry and then minting the next, and passes it only to the sandbox Git command. The token is not saved in the repository's remote URL or configuration. `[run.run_branch] push = false` keeps the branch only in the run workspace.
|
||||
|
||||
An intermediate push failure logs a warning; later checkpoints and final publication retry the push. After a successful run's last stage and before the run finishes, Fabro awaits one final push.
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ walkdir.workspace = true
|
|||
rmcp = { workspace = true, features = ["client", "transport-child-process"] }
|
||||
fabro-build-support = { path = "../../foundation/build-support" }
|
||||
fabro-server = { path = "../fabro-server", features = ["test-support"] }
|
||||
fabro-github = { path = "../../components/fabro-github", features = ["test-support"] }
|
||||
fabro-petri = { path = "../../components/fabro-petri", features = ["test-support"] }
|
||||
fabro-workflow = { path = "../../components/fabro-workflow", features = ["test-support"] }
|
||||
fabro-types = { path = "../../foundation/fabro-types", features = ["clap", "test-support"] }
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ pub(super) async fn execute(worker: PetriWorker<'_>) -> Result<()> {
|
|||
runner::set_worker_title(&run_id, WorkerTitlePhase::Running);
|
||||
|
||||
// A GitHub target is fetched into its sandbox with a read-only token and
|
||||
// published with a push token, both minted from the server's
|
||||
// credentials here.
|
||||
// published with a push token, each from a token source over the
|
||||
// server's credentials that the run keeps for its whole life.
|
||||
let github = match publish::github_credentials(&*vault.read().await) {
|
||||
Ok(credentials) => credentials,
|
||||
Err(err) => {
|
||||
|
|
@ -221,8 +221,7 @@ pub(super) async fn execute(worker: PetriWorker<'_>) -> Result<()> {
|
|||
None,
|
||||
);
|
||||
if let Some(source) = &mut source {
|
||||
source.credential =
|
||||
publish::source_credential(&worker.run_state.spec, github.as_ref()).await;
|
||||
source.credentials = publish::source_credentials(&worker.run_state.spec, github.as_ref());
|
||||
}
|
||||
let publisher = publish::GitHubPublisher::for_run(
|
||||
run_id,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
//! The worker resolves the server's GitHub credentials itself, as the
|
||||
//! legacy worker did: the strategy and App id from the server settings the
|
||||
//! server named (`FABRO_CONFIG`), the App key the server hands the worker,
|
||||
//! or `GITHUB_TOKEN` from the worker's vault snapshot. From them it mints a
|
||||
//! read-only token for the checkout inside the sandbox when the run starts,
|
||||
//! and a fresh push token for each checkpoint, so a long run never pushes with
|
||||
//! an expired token.
|
||||
//! or `GITHUB_TOKEN` from the worker's vault snapshot. From them it keeps two
|
||||
//! cached token sources for the run: a read-only one for fetches inside the
|
||||
//! sandbox and a `contents: write` one for pushes. Each fetch and push asks
|
||||
//! its source, which reuses one installation token until it nears expiry and
|
||||
//! then mints the next. Reuse matters: GitHub can reject a token minted
|
||||
//! moments earlier, before it has replicated, so minting per push turns a
|
||||
//! long run's pushes into repeated failures. Re-minting near expiry keeps a
|
||||
//! run of any length on a live token.
|
||||
//!
|
||||
//! Publication runs in Fabro's `run_finished` hook, after the last stage and
|
||||
//! before the run's terminal record, as the legacy publish step did: the
|
||||
|
|
@ -23,13 +27,14 @@ use anyhow::{Context, Result};
|
|||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use fabro_config::ServerSettingsBuilder;
|
||||
use fabro_github::{GitCloneCredentials, GitHubContext, GitHubCredentials};
|
||||
use fabro_llm::credentials::{CredentialProvider, readiness};
|
||||
use fabro_github::token_source::{InstallationTokenSource, SecretString};
|
||||
use fabro_github::{GitHubContext, GitHubCredentials};
|
||||
use fabro_llm::credentials::{self, CredentialProvider};
|
||||
use fabro_llm::lithos_catalog::Catalog;
|
||||
use fabro_petri::checkpoint::Site;
|
||||
use fabro_petri::hooks::{Publication, RunPublisher};
|
||||
use fabro_petri::platform_records::PlatformRecords;
|
||||
use fabro_petri::source::SourceCredential;
|
||||
use fabro_petri::source::{SourceCredential, SourceCredentials};
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_store::platform_records::{PlatformRecord, PullRequestCreatedRecord};
|
||||
use fabro_types::settings::run::{PullRequestSettings, RunMode};
|
||||
|
|
@ -42,8 +47,8 @@ use tracing::warn;
|
|||
|
||||
/// How long one push to the repository may take.
|
||||
const PUSH_TIMEOUT: Duration = Duration::from_mins(5);
|
||||
/// Attempts at the push: a freshly minted token can take a moment to reach
|
||||
/// every GitHub replica.
|
||||
/// Attempts at the push, all with the one token resolved for it: a freshly
|
||||
/// minted token can take a moment to reach every GitHub replica.
|
||||
const PUSH_ATTEMPTS: u32 = 3;
|
||||
const PUSH_RETRY_DELAY: Duration = Duration::from_secs(2);
|
||||
|
||||
|
|
@ -79,38 +84,62 @@ fn repository(spec: &RunSpec) -> Option<GitHubRepositorySlug> {
|
|||
Some(target.clone().validate().ok()?.repository().clone())
|
||||
}
|
||||
|
||||
/// The read-only credential the run's workspaces are fetched with. `None`
|
||||
/// when the run has no GitHub target or no credentials resolve; a public
|
||||
/// repository is then fetched anonymously.
|
||||
pub(super) async fn source_credential(
|
||||
/// A cached token source for the run's repository with `permissions`, or
|
||||
/// `None` when the run has no GitHub target or no credentials resolve.
|
||||
fn token_source(
|
||||
spec: &RunSpec,
|
||||
credentials: Option<&GitHubCredentials>,
|
||||
) -> Option<SourceCredential> {
|
||||
permissions: serde_json::Value,
|
||||
) -> Option<Arc<InstallationTokenSource>> {
|
||||
let repository = repository(spec)?;
|
||||
let credentials = credentials?;
|
||||
let base_url = fabro_github::github_api_base_url();
|
||||
let context = GitHubContext::new(credentials, &base_url);
|
||||
match fabro_github::resolve_read_only_clone_credentials(
|
||||
&context,
|
||||
repository.owner(),
|
||||
repository.repo(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(credentials) => encode(&credentials),
|
||||
match InstallationTokenSource::for_repository(
|
||||
credentials,
|
||||
repository.owner().to_string(),
|
||||
repository.repo().to_string(),
|
||||
permissions,
|
||||
) {
|
||||
Ok(source) => Some(source),
|
||||
Err(err) => {
|
||||
warn!(repository = %repository, error = %err, "no read credential for the run's repository; it is fetched anonymously");
|
||||
warn!(repository = %repository, error = %format!("{err:#}"), "no GitHub token source for the run's repository");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(credentials: &GitCloneCredentials) -> Option<SourceCredential> {
|
||||
SourceCredential::from_encoded(BASE64_STANDARD.encode(format!(
|
||||
"{}:{}",
|
||||
credentials.username(),
|
||||
credentials.password()
|
||||
)))
|
||||
/// The read-only credentials the run's workspaces are fetched with. `None`
|
||||
/// when the run has no GitHub target or no credentials resolve; a public
|
||||
/// repository is then fetched anonymously.
|
||||
pub(super) fn source_credentials(
|
||||
spec: &RunSpec,
|
||||
credentials: Option<&GitHubCredentials>,
|
||||
) -> Option<Arc<dyn SourceCredentials>> {
|
||||
let tokens = token_source(spec, credentials, serde_json::json!({ "contents": "read" }))?;
|
||||
Some(Arc::new(ReadCredentials(tokens)))
|
||||
}
|
||||
|
||||
/// Fetch credentials resolved from the run's read-only token source.
|
||||
struct ReadCredentials(Arc<InstallationTokenSource>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SourceCredentials for ReadCredentials {
|
||||
async fn credential(&self) -> Option<SourceCredential> {
|
||||
match self.0.resolve().await {
|
||||
Ok(resolved) => basic(&resolved.token),
|
||||
Err(err) => {
|
||||
warn!(error = %format!("{err:#}"), "no read credential for the run's repository; it is fetched anonymously");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The HTTP basic credential `git` presents for an installation token or
|
||||
/// personal access token.
|
||||
fn basic(token: &SecretString) -> Option<SourceCredential> {
|
||||
SourceCredential::from_encoded(
|
||||
BASE64_STANDARD.encode(format!("x-access-token:{}", token.expose())),
|
||||
)
|
||||
}
|
||||
|
||||
/// A successful GitHub-target run's publication: the run branch pushed, and
|
||||
|
|
@ -124,6 +153,8 @@ pub(super) struct GitHubPublisher {
|
|||
/// The run's model, when its settings name one.
|
||||
model: Option<String>,
|
||||
credentials: Option<GitHubCredentials>,
|
||||
/// The run's `contents: write` token source; `None` without credentials.
|
||||
push_tokens: Option<Arc<InstallationTokenSource>>,
|
||||
pull_request: Option<PullRequestSettings>,
|
||||
llm_source: Arc<dyn CredentialProvider>,
|
||||
catalog: Arc<Catalog>,
|
||||
|
|
@ -155,6 +186,11 @@ impl GitHubPublisher {
|
|||
return None;
|
||||
};
|
||||
let repository = repository(spec)?;
|
||||
let push_tokens = token_source(
|
||||
spec,
|
||||
credentials.as_ref(),
|
||||
serde_json::json!({ "contents": "write" }),
|
||||
);
|
||||
Some(Self {
|
||||
run_id,
|
||||
repository,
|
||||
|
|
@ -162,6 +198,7 @@ impl GitHubPublisher {
|
|||
goal: spec.graph.goal.clone(),
|
||||
model: settings.model.name.clone(),
|
||||
credentials,
|
||||
push_tokens,
|
||||
pull_request: settings
|
||||
.pull_request
|
||||
.clone()
|
||||
|
|
@ -179,7 +216,9 @@ impl GitHubPublisher {
|
|||
if let Some(model) = &self.model {
|
||||
return Some(model.clone());
|
||||
}
|
||||
let ready = readiness(self.catalog.enabled_providers(), self.llm_source.as_ref()).await;
|
||||
let ready =
|
||||
credentials::readiness(self.catalog.enabled_providers(), self.llm_source.as_ref())
|
||||
.await;
|
||||
self.catalog
|
||||
.default_offering_for(&ready.ready)
|
||||
.map(|entry| entry.model.id().to_string())
|
||||
|
|
@ -239,19 +278,14 @@ impl GitHubPublisher {
|
|||
#[async_trait::async_trait]
|
||||
impl RunPublisher for GitHubPublisher {
|
||||
async fn push(&self, site: &Site, branch: &str, sha: &str) -> Result<(), String> {
|
||||
let credentials = self.credentials.as_ref().ok_or_else(|| {
|
||||
let tokens = self.push_tokens.as_ref().ok_or_else(|| {
|
||||
"pushing the run branch requires the server's GitHub credentials".to_string()
|
||||
})?;
|
||||
let base_url = fabro_github::github_api_base_url();
|
||||
let context = GitHubContext::new(credentials, &base_url);
|
||||
let push_credentials = fabro_github::resolve_clone_credentials(
|
||||
&context,
|
||||
self.repository.owner(),
|
||||
self.repository.repo(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("no push credential for {}: {err:#}", self.repository))?;
|
||||
push(&self.repository, &push_credentials, site, branch, sha).await
|
||||
let resolved = tokens
|
||||
.resolve()
|
||||
.await
|
||||
.map_err(|err| format!("no push credential for {}: {err:#}", self.repository))?;
|
||||
push(&self.repository, &resolved.token, site, branch, sha).await
|
||||
}
|
||||
|
||||
async fn publish(&self, publication: &Publication) -> Result<(), String> {
|
||||
|
|
@ -279,18 +313,20 @@ impl RunPublisher for GitHubPublisher {
|
|||
|
||||
/// Push a checkpoint from inside its workspace to the run
|
||||
/// branch on GitHub, retrying a failure that may be a token still
|
||||
/// replicating. The credential reaches `git` as an HTTP header for the
|
||||
/// replicating. The token reaches `git` as an HTTP header for the
|
||||
/// repository alone and never appears in the error.
|
||||
async fn push(
|
||||
repository: &GitHubRepositorySlug,
|
||||
credentials: &GitCloneCredentials,
|
||||
token: &SecretString,
|
||||
site: &Site,
|
||||
branch: &str,
|
||||
sha: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut url = repository.https_url();
|
||||
url.push_str(".git");
|
||||
let env = encode(credentials)
|
||||
let credential = basic(token);
|
||||
let env = credential
|
||||
.as_ref()
|
||||
.map(|credential| credential.header_env(&url))
|
||||
.unwrap_or_default();
|
||||
let refspec = format!("{sha}:refs/heads/{branch}");
|
||||
|
|
@ -299,9 +335,9 @@ async fn push(
|
|||
match site.push(&url, &refspec, &env, PUSH_TIMEOUT).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => {
|
||||
last = error.to_string().replace(credentials.password(), "***");
|
||||
if let Some(encoded) = encode(credentials) {
|
||||
last = last.replace(encoded.encoded(), "***");
|
||||
last = error.to_string().replace(token.expose(), "***");
|
||||
if let Some(credential) = &credential {
|
||||
last = last.replace(credential.encoded(), "***");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -322,6 +358,11 @@ async fn push(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_github::InstallationToken;
|
||||
use fabro_github::test_support::{InstallationTokenMinter, installation_token_source};
|
||||
use fabro_llm::credentials::NoCredentials;
|
||||
use fabro_llm::test_support;
|
||||
use fabro_petri::test_support::MemoryPlatformRecords;
|
||||
|
|
@ -371,4 +412,36 @@ mod tests {
|
|||
empty.target = Some(RunTarget::None {});
|
||||
assert!(!publishes(&empty));
|
||||
}
|
||||
|
||||
/// Mints `token-<n>` for its n-th mint, valid for an hour.
|
||||
#[derive(Default)]
|
||||
struct CountingMinter(AtomicUsize);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstallationTokenMinter for CountingMinter {
|
||||
async fn mint(&self) -> anyhow::Result<InstallationToken> {
|
||||
let n = self.0.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
Ok(InstallationToken {
|
||||
token: format!("token-{n}"),
|
||||
expires_at: Utc::now() + chrono::Duration::hours(1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetches_reuse_one_cached_read_token() {
|
||||
let minter = Arc::new(CountingMinter::default());
|
||||
let credentials = ReadCredentials(installation_token_source(
|
||||
"acme/widgets",
|
||||
Arc::clone(&minter) as Arc<dyn InstallationTokenMinter>,
|
||||
));
|
||||
let first = credentials.credential().await.unwrap();
|
||||
let second = credentials.credential().await.unwrap();
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(minter.0.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
first.encoded(),
|
||||
BASE64_STANDARD.encode("x-access-token:token-1")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ impl RunWorkspaces {
|
|||
site,
|
||||
"fetch",
|
||||
&args,
|
||||
&source.fetch_env(),
|
||||
&source.fetch_env().await,
|
||||
SOURCE_FETCH_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@
|
|||
//!
|
||||
//! The credential reaches one `git` command at a time through its
|
||||
//! environment, as an HTTP header scoped to the origin. It is never written
|
||||
//! into the repository, its configuration, or its remote URL.
|
||||
//! into the repository, its configuration, or its remote URL. Each fetch asks
|
||||
//! the run's [`SourceCredentials`] for it, so a workspace first acquired hours
|
||||
//! into a run still presents a live token.
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_types::settings::run::{RunCloneSettings, RunMode, RunNamespace};
|
||||
use fabro_types::{GitRunTarget, RunTarget};
|
||||
|
|
@ -86,18 +89,48 @@ impl fmt::Debug for SourceCredential {
|
|||
}
|
||||
}
|
||||
|
||||
/// Where a run's fetches get their credential, asked once per fetch.
|
||||
#[async_trait::async_trait]
|
||||
pub trait SourceCredentials: Send + Sync {
|
||||
/// The credential the next fetch presents; `None` fetches anonymously.
|
||||
async fn credential(&self) -> Option<SourceCredential>;
|
||||
}
|
||||
|
||||
/// A fixed credential, presented by every fetch.
|
||||
#[async_trait::async_trait]
|
||||
impl SourceCredentials for SourceCredential {
|
||||
async fn credential(&self) -> Option<Self> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A run's GitHub repository as its workspaces check it out.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone)]
|
||||
pub struct RunSource {
|
||||
/// The repository's HTTPS URL: the workspace's `origin`.
|
||||
pub origin: String,
|
||||
pub revision: SourceRevision,
|
||||
pub origin: String,
|
||||
pub revision: SourceRevision,
|
||||
/// The branch the workspace stands on before the run branch is created
|
||||
/// from it.
|
||||
pub branch: String,
|
||||
pub branch: String,
|
||||
/// Commits of history to fetch; `None` is the whole history.
|
||||
pub depth: Option<u32>,
|
||||
pub credential: Option<SourceCredential>,
|
||||
pub depth: Option<u32>,
|
||||
pub credentials: Option<Arc<dyn SourceCredentials>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RunSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RunSource")
|
||||
.field("origin", &self.origin)
|
||||
.field("revision", &self.revision)
|
||||
.field("branch", &self.branch)
|
||||
.field("depth", &self.depth)
|
||||
.field(
|
||||
"credentials",
|
||||
&self.credentials.as_ref().map(|_| "<redacted>"),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RunSource {
|
||||
|
|
@ -108,7 +141,7 @@ impl RunSource {
|
|||
target: &GitRunTarget,
|
||||
origin: String,
|
||||
clone: &RunCloneSettings,
|
||||
credential: Option<SourceCredential>,
|
||||
credentials: Option<Arc<dyn SourceCredentials>>,
|
||||
) -> Option<Self> {
|
||||
if !clone.enabled {
|
||||
return None;
|
||||
|
|
@ -127,7 +160,7 @@ impl RunSource {
|
|||
depth: clone
|
||||
.depth_limit()
|
||||
.and_then(|depth| u32::try_from(depth).ok()),
|
||||
credential,
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +172,7 @@ impl RunSource {
|
|||
pub fn for_run(
|
||||
target: Option<&RunTarget>,
|
||||
settings: &RunNamespace,
|
||||
credential: Option<SourceCredential>,
|
||||
credentials: Option<Arc<dyn SourceCredentials>>,
|
||||
) -> Option<Self> {
|
||||
let Some(RunTarget::Git(target)) = target else {
|
||||
return None;
|
||||
|
|
@ -149,15 +182,18 @@ impl RunSource {
|
|||
}
|
||||
let validated = target.clone().validate().ok()?;
|
||||
let origin = validated.repository().https_url();
|
||||
Self::for_target(validated.target(), origin, &settings.clone, credential)
|
||||
Self::for_target(validated.target(), origin, &settings.clone, credentials)
|
||||
}
|
||||
|
||||
/// The environment a `git` command that talks to the origin runs with:
|
||||
/// the credential as an `Authorization` header for the origin alone.
|
||||
#[must_use]
|
||||
pub fn fetch_env(&self) -> Vec<(String, String)> {
|
||||
self.credential
|
||||
.as_ref()
|
||||
pub async fn fetch_env(&self) -> Vec<(String, String)> {
|
||||
let Some(credentials) = &self.credentials else {
|
||||
return Vec::new();
|
||||
};
|
||||
credentials
|
||||
.credential()
|
||||
.await
|
||||
.map(|credential| credential.header_env(&self.origin))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
@ -221,16 +257,17 @@ mod tests {
|
|||
assert_eq!(full.depth_arg(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_credential_is_scoped_to_the_origin_and_never_printed() {
|
||||
#[tokio::test]
|
||||
async fn the_credential_is_scoped_to_the_origin_and_never_printed() {
|
||||
let credential = SourceCredential::from_encoded("c2VjcmV0").unwrap();
|
||||
let source = RunSource::for_target(
|
||||
&target(None, None),
|
||||
"https://github.com/acme/widgets".to_string(),
|
||||
&clone(true, 1),
|
||||
SourceCredential::from_encoded("c2VjcmV0"),
|
||||
Some(Arc::new(credential)),
|
||||
)
|
||||
.unwrap();
|
||||
let env = source.fetch_env();
|
||||
let env = source.fetch_env().await;
|
||||
assert!(env.contains(&(
|
||||
"GIT_CONFIG_KEY_0".to_string(),
|
||||
"http.https://github.com/acme/widgets.extraheader".to_string()
|
||||
|
|
|
|||
|
|
@ -1019,11 +1019,11 @@ async fn a_daytona_run_commits_inside_the_sandbox_and_publishes_every_checkpoint
|
|||
async fn assert_sandbox_run_publishes_every_checkpoint(provider: SandboxProviderKind) {
|
||||
let mut harness = Harness::new().await;
|
||||
harness.source = Some(RunSource {
|
||||
origin: "https://github.com/octocat/Hello-World.git".to_string(),
|
||||
revision: SourceRevision::Branch("master".to_string()),
|
||||
branch: "master".to_string(),
|
||||
depth: Some(1),
|
||||
credential: None,
|
||||
origin: "https://github.com/octocat/Hello-World.git".to_string(),
|
||||
revision: SourceRevision::Branch("master".to_string()),
|
||||
branch: "master".to_string(),
|
||||
depth: Some(1),
|
||||
credentials: None,
|
||||
});
|
||||
let publisher = RecordingPublisher::new(None);
|
||||
harness.publisher = Some(publisher.clone());
|
||||
|
|
@ -1227,7 +1227,7 @@ fn file_source(origin: &Path, branch: &str, depth: Option<u32>) -> RunSource {
|
|||
revision: SourceRevision::Branch(branch.to_string()),
|
||||
branch: branch.to_string(),
|
||||
depth,
|
||||
credential: None,
|
||||
credentials: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue