on deploy, one agent whose agent-configs repo has no commit on main is skipped and logged instead of failing the whole internal/meta render, so the other agents' pin moves go through. the controller reads each config head from main, the branch hive-c0re compares against, not the repo's default branch.
166 lines
5.6 KiB
Rust
166 lines
5.6 KiB
Rust
//! `internal/meta` on the forge: the repo the swarm meta flake is committed to
|
|
//! (`crate::meta_flake`), and the read grant each hive's account holds on it.
|
|
|
|
use anyhow::{Context, Result};
|
|
use base64::Engine as _;
|
|
use forgejo_api::structs::{
|
|
AddCollaboratorOptionPermission, ChangeFileOperation, ChangeFileOperationOperation,
|
|
ChangeFilesOptions,
|
|
};
|
|
|
|
use super::objects::SHARED_ORG;
|
|
use super::{Client, DefaultBranchFiles, is_repo_not_found};
|
|
use crate::meta_flake::{META_REPO, MetaFile, MetaForge};
|
|
|
|
impl MetaForge for Client {
|
|
async fn head_files(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
branch: Option<&str>,
|
|
paths: &[&str],
|
|
) -> Result<Option<DefaultBranchFiles>> {
|
|
self.branch_files(owner, repo, branch, paths).await
|
|
}
|
|
|
|
async fn ensure_meta_repo(&self) -> Result<()> {
|
|
self.create_shared_repo(SHARED_ORG, META_REPO, true).await
|
|
}
|
|
|
|
async fn grant_meta_read(&self, login: &str) -> Result<()> {
|
|
self.add_collaborator_to(
|
|
SHARED_ORG,
|
|
META_REPO,
|
|
login,
|
|
AddCollaboratorOptionPermission::Read,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn commits_ahead(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
base: &str,
|
|
head: &str,
|
|
) -> Result<Option<u64>> {
|
|
let basehead = format!("{base}...{head}");
|
|
match self
|
|
.api
|
|
.call(|api| api.repo_compare_diff(owner, repo, &basehead).into_future())
|
|
.await
|
|
{
|
|
Ok(compare) => Ok(Some(
|
|
u64::try_from(compare.total_commits.unwrap_or(0)).unwrap_or(0),
|
|
)),
|
|
Err(e) if is_repo_not_found(&e) => Ok(None),
|
|
Err(e) => Err(e).with_context(|| format!("compare {basehead} in {owner}/{repo}")),
|
|
}
|
|
}
|
|
|
|
async fn commit_meta(&self, message: &str, files: Vec<MetaFile>) -> Result<()> {
|
|
let ops: Vec<ChangeFileOperation> = files
|
|
.into_iter()
|
|
.map(|f| ChangeFileOperation {
|
|
content: Some(base64::engine::general_purpose::STANDARD.encode(f.bytes)),
|
|
from_path: None,
|
|
operation: if f.sha.is_some() {
|
|
ChangeFileOperationOperation::Update
|
|
} else {
|
|
ChangeFileOperationOperation::Create
|
|
},
|
|
path: f.path.to_owned(),
|
|
sha: f.sha,
|
|
})
|
|
.collect();
|
|
self.api
|
|
.call(|api| {
|
|
api.repo_change_files(
|
|
SHARED_ORG,
|
|
META_REPO,
|
|
ChangeFilesOptions {
|
|
author: None,
|
|
branch: None,
|
|
committer: None,
|
|
dates: None,
|
|
files: ops.clone(),
|
|
force_overwrite_new_branch: None,
|
|
message: Some(message.to_owned()),
|
|
new_branch: None,
|
|
signoff: None,
|
|
},
|
|
)
|
|
.into_future()
|
|
})
|
|
.await
|
|
.with_context(|| format!("commit to {SHARED_ORG}/{META_REPO}"))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use super::*;
|
|
|
|
/// A stand-in forge answering Forgejo's compare endpoint: `total_commits`
|
|
/// of 3 for `aaa...bbb`, 0 for `bbb...aaa`, 404 for anything else.
|
|
/// Records every request's path.
|
|
async fn compare_forge() -> (Client, Arc<Mutex<Vec<String>>>) {
|
|
use axum::http::{StatusCode, Uri};
|
|
|
|
let seen = Arc::new(Mutex::new(Vec::new()));
|
|
let recorded = Arc::clone(&seen);
|
|
let app = axum::Router::new().fallback(move |uri: Uri| {
|
|
let recorded = Arc::clone(&recorded);
|
|
async move {
|
|
recorded.lock().unwrap().push(uri.path().to_owned());
|
|
match uri.path() {
|
|
"/api/v1/repos/o/r/compare/aaa...bbb" => (
|
|
StatusCode::OK,
|
|
r#"{"total_commits":3,"commits":[],"files":[]}"#,
|
|
),
|
|
"/api/v1/repos/o/r/compare/bbb...aaa" => {
|
|
(StatusCode::OK, r#"{"total_commits":0,"commits":[]}"#)
|
|
}
|
|
_ => (StatusCode::NOT_FOUND, r#"{"message":"not found"}"#),
|
|
}
|
|
}
|
|
});
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap();
|
|
tokio::spawn(async move { axum::serve(listener, app).await });
|
|
let api = crate::forge::api::tests::fixed(url.clone(), "stub-token");
|
|
(Client { api, url }, seen)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn commits_ahead_reads_the_compare_endpoint_as_base_dots_head() {
|
|
let (client, seen) = compare_forge().await;
|
|
assert_eq!(
|
|
client.commits_ahead("o", "r", "aaa", "bbb").await.unwrap(),
|
|
Some(3)
|
|
);
|
|
assert_eq!(
|
|
client.commits_ahead("o", "r", "bbb", "aaa").await.unwrap(),
|
|
Some(0)
|
|
);
|
|
assert_eq!(
|
|
*seen.lock().unwrap(),
|
|
[
|
|
"/api/v1/repos/o/r/compare/aaa...bbb",
|
|
"/api/v1/repos/o/r/compare/bbb...aaa"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_commit_the_forge_lacks_is_no_count_not_an_error() {
|
|
let (client, _) = compare_forge().await;
|
|
assert_eq!(
|
|
client.commits_ahead("o", "r", "aaa", "zzz").await.unwrap(),
|
|
None
|
|
);
|
|
}
|
|
}
|