Compare commits
3 changed files with 5 additions and 355 deletions
|
|
@ -20,15 +20,13 @@ use forgejo_api::structs::{
|
||||||
AddCollaboratorOption, AddCollaboratorOptionPermission, ChangeFileOperation,
|
AddCollaboratorOption, AddCollaboratorOptionPermission, ChangeFileOperation,
|
||||||
ChangeFileOperationOperation, ChangeFilesOptions, CreateBranchProtectionOption,
|
ChangeFileOperationOperation, ChangeFilesOptions, CreateBranchProtectionOption,
|
||||||
CreateHookOption, CreateHookOptionConfig, CreateHookOptionType, CreateRepoOption,
|
CreateHookOption, CreateHookOptionConfig, CreateHookOptionType, CreateRepoOption,
|
||||||
CreateUserOption, IssueListIssuesQuery, IssueListIssuesQueryState, IssueListIssuesQueryType,
|
CreateUserOption, RepoGetContentsQuery, RepoListPullRequestsQuery,
|
||||||
RepoGetContentsQuery, RepoListPullRequestsQuery, RepoListPullRequestsQueryState,
|
RepoListPullRequestsQueryState,
|
||||||
RepoSearchQuery, StateType,
|
|
||||||
};
|
};
|
||||||
use forgejo_api::{ApiErrorKind, Auth, Forgejo, ForgejoError};
|
use forgejo_api::{ApiErrorKind, Auth, Forgejo, ForgejoError};
|
||||||
use futures_util::{StreamExt as _, TryStreamExt as _};
|
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::BTreeMap;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::webhook::DeliveryKind;
|
use crate::webhook::DeliveryKind;
|
||||||
|
|
@ -43,34 +41,6 @@ pub struct ConfigPrStatus {
|
||||||
pub html_url: Option<String>,
|
pub html_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of [`Client::issue_report`] — an open issue plus the two facts
|
|
||||||
/// derived from its dependency graph that swarm-ui's issue-report page
|
|
||||||
/// sorts/filters on. See that function's doc comment for how both are
|
|
||||||
/// computed.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, ToSchema)]
|
|
||||||
pub struct IssueReportRow {
|
|
||||||
/// `owner/name` — present on every row (not just the cross-repo
|
|
||||||
/// aggregate) so a caller can use the same row type for either
|
|
||||||
/// endpoint without a second shape to branch on.
|
|
||||||
pub repo: String,
|
|
||||||
pub number: i64,
|
|
||||||
pub title: String,
|
|
||||||
pub labels: Vec<String>,
|
|
||||||
/// All assignees, not just one — forgejo's `Issue::assignee` is a
|
|
||||||
/// legacy single-value field; `assignee`**s** (plural) is the real
|
|
||||||
/// multi-assignee list, and this repo's issues do use more than one.
|
|
||||||
pub assignees: Vec<String>,
|
|
||||||
pub html_url: Option<String>,
|
|
||||||
/// True if any of this issue's own dependencies is still open — same
|
|
||||||
/// semantics `hive-forge issue dependency list` already uses (a closed
|
|
||||||
/// blocker doesn't count).
|
|
||||||
pub blocked: bool,
|
|
||||||
/// How many OTHER issues in this same report list this issue as a
|
|
||||||
/// dependency — the reverse of `blocked`. Ranks "fix this one to
|
|
||||||
/// unblock the most other work" highest.
|
|
||||||
pub depended_on_by_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `operators` team, whitelisted for the merge gate on every repo
|
/// The `operators` team, whitelisted for the merge gate on every repo
|
||||||
/// this client protects — provisioned by `hive-c0re::forge::repos`
|
/// this client protects — provisioned by `hive-c0re::forge::repos`
|
||||||
/// already (`ensure_operators_team`), not re-provisioned here. If that
|
/// already (`ensure_operators_team`), not re-provisioned here. If that
|
||||||
|
|
@ -579,182 +549,6 @@ impl Client {
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Repos with at least one open issue, as `owner/name` full names —
|
|
||||||
/// the data source for swarm-ui's repo-filter dropdown, which lists
|
|
||||||
/// only repos that currently have open issues rather than every repo
|
|
||||||
/// the forge hosts. Filtered on `Repository::open_issues_count` from
|
|
||||||
/// the search result itself rather than a follow-up
|
|
||||||
/// `issue_list_issues` call per repo — forgejo's own repo summary
|
|
||||||
/// already carries that count, so this stays one request regardless
|
|
||||||
/// of how many repos exist.
|
|
||||||
///
|
|
||||||
/// One search page (forgejo's own default page size) — this binding's
|
|
||||||
/// `RepoSearchQuery` has no `page`/`limit` field to page through, unlike
|
|
||||||
/// the `(Headers, Vec<T>)`-shaped list endpoints elsewhere in this file
|
|
||||||
/// that `.all()` fully drains. Fine for admin-facing tooling at today's
|
|
||||||
/// repo count; revisit if an instance's repo count ever outgrows one
|
|
||||||
/// page.
|
|
||||||
pub async fn list_repos_with_open_issues(&self) -> Result<Vec<String>> {
|
|
||||||
let results = self
|
|
||||||
.api
|
|
||||||
.repo_search(RepoSearchQuery::default())
|
|
||||||
.await
|
|
||||||
.context("search repos")?;
|
|
||||||
Ok(results
|
|
||||||
.data
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|r| r.open_issues_count.unwrap_or(0) > 0)
|
|
||||||
.filter_map(|r| r.full_name)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every open issue in `owner/repo`, each with its dependency-derived
|
|
||||||
/// `blocked` / `depended_on_by_count` pre-resolved server-side — the
|
|
||||||
/// whole job behind `GET /api/repos/{org}/{repo}/issue-report`. Doing
|
|
||||||
/// this resolution client-side (one dependency lookup per issue, on
|
|
||||||
/// every page load) doesn't scale, which is the reason this endpoint
|
|
||||||
/// exists at all rather than swarm-ui resolving it itself per row.
|
|
||||||
///
|
|
||||||
/// Forgejo has no bulk or reverse dependency query — the same
|
|
||||||
/// limitation `hive-forge issue dependency` hits — so this is still one
|
|
||||||
/// `issue_list_issue_dependencies` call per open issue, just run with
|
|
||||||
/// bounded concurrency (`CONCURRENCY`) rather than one-at-a-time or all
|
|
||||||
/// at once, so a repo with hundreds of open issues doesn't serialize on
|
|
||||||
/// round-trips or fire them all in one burst.
|
|
||||||
///
|
|
||||||
/// `depended_on_by_count` is a reverse index built off that SAME
|
|
||||||
/// resolve pass, not a second query: every open issue's own dependency
|
|
||||||
/// list is already in hand to compute `blocked`, so counting how often
|
|
||||||
/// each open issue's number appears across all of them is pure
|
|
||||||
/// in-memory aggregation. A dependency on a closed issue, or one
|
|
||||||
/// outside this open-issue set, can never accrue a count here — it
|
|
||||||
/// isn't a row in the report either, so "how many rows depend on this
|
|
||||||
/// row" would have nothing to point at.
|
|
||||||
pub async fn issue_report(&self, owner: &str, repo: &str) -> Result<Vec<IssueReportRow>> {
|
|
||||||
const CONCURRENCY: usize = 8;
|
|
||||||
|
|
||||||
let issues = self
|
|
||||||
.api
|
|
||||||
.issue_list_issues(
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
IssueListIssuesQuery {
|
|
||||||
state: Some(IssueListIssuesQueryState::Open),
|
|
||||||
r#type: Some(IssueListIssuesQueryType::Issues),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("list open issues in {owner}/{repo}"))?;
|
|
||||||
|
|
||||||
// Numbers collected into an owned `Vec` first, rather than handing
|
|
||||||
// `stream::iter` a lazy `filter_map` over borrowed `issues`
|
|
||||||
// directly — the latter shape trips a "closure implementation of
|
|
||||||
// `FnOnce` is not general enough" HRTB inference error where this
|
|
||||||
// function is used as an axum handler (the `routes!` macro checks
|
|
||||||
// `Handler` for arbitrary request lifetimes), even though the
|
|
||||||
// closure itself is unremarkable.
|
|
||||||
let numbers: Vec<i64> = issues.iter().filter_map(|i| i.number).collect();
|
|
||||||
let deps: HashMap<i64, Vec<forgejo_api::structs::Issue>> =
|
|
||||||
futures_util::stream::iter(numbers)
|
|
||||||
.map(|number| async move {
|
|
||||||
let blockers = self
|
|
||||||
.api
|
|
||||||
.issue_list_issue_dependencies(owner, repo, number)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("list dependencies of {owner}/{repo}#{number}"))?;
|
|
||||||
Ok::<_, anyhow::Error>((number, blockers))
|
|
||||||
})
|
|
||||||
.buffer_unordered(CONCURRENCY)
|
|
||||||
.try_collect()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let open_numbers: HashSet<i64> = issues.iter().filter_map(|i| i.number).collect();
|
|
||||||
let mut dependent_counts: HashMap<i64, u32> = HashMap::new();
|
|
||||||
for blockers in deps.values() {
|
|
||||||
for blocker in blockers {
|
|
||||||
if let Some(n) = blocker.number
|
|
||||||
&& open_numbers.contains(&n)
|
|
||||||
{
|
|
||||||
*dependent_counts.entry(n).or_insert(0) += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo_full_name = format!("{owner}/{repo}");
|
|
||||||
Ok(issues
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|issue| {
|
|
||||||
let number = issue.number?;
|
|
||||||
let blocked = deps
|
|
||||||
.get(&number)
|
|
||||||
.is_some_and(|b| b.iter().any(|d| d.state == Some(StateType::Open)));
|
|
||||||
Some(IssueReportRow {
|
|
||||||
repo: repo_full_name.clone(),
|
|
||||||
number,
|
|
||||||
title: issue.title.unwrap_or_default(),
|
|
||||||
labels: issue
|
|
||||||
.labels
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|l| l.name)
|
|
||||||
.collect(),
|
|
||||||
assignees: issue
|
|
||||||
.assignees
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|a| a.login)
|
|
||||||
.collect(),
|
|
||||||
html_url: issue.html_url.map(|u| u.to_string()),
|
|
||||||
blocked,
|
|
||||||
depended_on_by_count: dependent_counts.get(&number).copied().unwrap_or(0),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`Self::issue_report`], fanned out over every repo
|
|
||||||
/// [`Self::list_repos_with_open_issues`] finds — the default,
|
|
||||||
/// no-repo-filter view. Each repo's report is resolved independently and
|
|
||||||
/// concurrently (same `CONCURRENCY` bound as the per-issue dependency
|
|
||||||
/// resolution inside `issue_report`, since this is the same
|
|
||||||
/// "N independent forge round-trips" shape one level up), then
|
|
||||||
/// flattened into one row set — `IssueReportRow::repo` is what lets a
|
|
||||||
/// caller tell which repo a given row came from.
|
|
||||||
///
|
|
||||||
/// One repo's report failing does not fail the whole scan — logged and
|
|
||||||
/// skipped, same best-effort shape [`Self::list_open_config_prs`] uses:
|
|
||||||
/// a flaky repo should not blank out every other repo's rows.
|
|
||||||
pub async fn issue_report_all(&self) -> Result<Vec<IssueReportRow>> {
|
|
||||||
const CONCURRENCY: usize = 4;
|
|
||||||
|
|
||||||
let repos = self.list_repos_with_open_issues().await?;
|
|
||||||
let rows: Vec<Vec<IssueReportRow>> = futures_util::stream::iter(repos)
|
|
||||||
.map(|full_name| async move {
|
|
||||||
let Some((owner, repo)) = full_name.split_once('/') else {
|
|
||||||
tracing::warn!(%full_name, "issue_report_all: repo full_name has no '/'");
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
match self.issue_report(owner, repo).await {
|
|
||||||
Ok(rows) => rows,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(
|
|
||||||
error = %format!("{e:#}"),
|
|
||||||
%full_name,
|
|
||||||
"issue_report_all: one repo's report failed, skipping it"
|
|
||||||
);
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.buffer_unordered(CONCURRENCY)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
Ok(rows.into_iter().flatten().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register the swarm-wide hooks against this controller, so a real
|
/// Register the swarm-wide hooks against this controller, so a real
|
||||||
/// forge event reaches [`crate::webhook`] instead of the endpoint only
|
/// forge event reaches [`crate::webhook`] instead of the endpoint only
|
||||||
/// being reachable by hand.
|
/// being reachable by hand.
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
//! The swarm-ui issue-report page's data source — repo listing plus the
|
|
||||||
//! two shapes of the report itself (all-repos default, single-repo
|
|
||||||
//! filtered). See [`crate::forge::Client::issue_report`] and
|
|
||||||
//! [`crate::forge::Client::issue_report_all`] for how a row's `blocked` /
|
|
||||||
//! `depended_on_by_count` fields are actually computed; this module is
|
|
||||||
//! just the HTTP surface over that logic, split out into its own file —
|
|
||||||
//! same shape [`crate::webhook`] already uses: business logic in
|
|
||||||
//! `crate::forge::Client`, the axum handlers next to their own routes.
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
Json,
|
|
||||||
extract::{Path, State},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{AppState, StatusUnavailable};
|
|
||||||
use crate::forge;
|
|
||||||
|
|
||||||
/// Repos with at least one open issue, as `owner/name` full names — the
|
|
||||||
/// data source for swarm-ui's repo-filter dropdown. Not scoped to
|
|
||||||
/// `forge::CONFIG_ORG` the way `main.rs`'s `get_config_prs` is: this
|
|
||||||
/// report deliberately covers the whole forge instance, not one org, since
|
|
||||||
/// the report itself is a general-purpose browsing tool rather than
|
|
||||||
/// something agent-config-specific.
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/api/repos",
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "repos with at least one open issue, as owner/name", body = Vec<String>),
|
|
||||||
(status = 503, description = "no forge is configured on this host", body = String),
|
|
||||||
),
|
|
||||||
tag = "repos"
|
|
||||||
)]
|
|
||||||
pub async fn get_repos(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<Json<Vec<String>>, StatusUnavailable> {
|
|
||||||
let Some(client) = state.forge.as_ref() else {
|
|
||||||
return Err(StatusUnavailable(
|
|
||||||
"no forge is configured on this host".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
match client.list_repos_with_open_issues().await {
|
|
||||||
Ok(repos) => Ok(Json(repos)),
|
|
||||||
Err(e) => {
|
|
||||||
let detail = format!("{e:#}");
|
|
||||||
tracing::warn!(error = %detail, "listing repos failed");
|
|
||||||
Err(StatusUnavailable(detail))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every open issue across every repo with at least one — the default,
|
|
||||||
/// no-repo-filter view. See [`get_issue_report`] for the single-repo
|
|
||||||
/// counterpart (used once the dropdown's repo filter is set) and
|
|
||||||
/// [`forge::Client::issue_report_all`] for how the fan-out works.
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/api/issue-report",
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "every open issue across every repo with one, blocked/dependent-count pre-resolved", body = Vec<forge::IssueReportRow>),
|
|
||||||
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
|
|
||||||
),
|
|
||||||
tag = "repos"
|
|
||||||
)]
|
|
||||||
pub async fn get_issue_report_all(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<Json<Vec<forge::IssueReportRow>>, StatusUnavailable> {
|
|
||||||
let Some(client) = state.forge.as_ref() else {
|
|
||||||
return Err(StatusUnavailable(
|
|
||||||
"no forge is configured on this host".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
match client.issue_report_all().await {
|
|
||||||
Ok(rows) => Ok(Json(rows)),
|
|
||||||
Err(e) => {
|
|
||||||
let detail = format!("{e:#}");
|
|
||||||
tracing::warn!(error = %detail, "building the all-repos issue report failed");
|
|
||||||
Err(StatusUnavailable(detail))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every open issue in `{org}/{repo}` specifically — the filtered view once
|
|
||||||
/// swarm-ui's repo dropdown (populated by [`get_repos`]) has a selection.
|
|
||||||
/// See [`forge::Client::issue_report`] for how `blocked` /
|
|
||||||
/// `depended_on_by_count` are computed and why this has to be a
|
|
||||||
/// server-side pass rather than something swarm-ui resolves itself per row.
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/api/repos/{org}/{repo}/issue-report",
|
|
||||||
params(
|
|
||||||
("org" = String, Path, description = "repo owner (user or org)"),
|
|
||||||
("repo" = String, Path, description = "repo name"),
|
|
||||||
),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "every open issue in this repo, blocked/dependent-count pre-resolved", body = Vec<forge::IssueReportRow>),
|
|
||||||
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
|
|
||||||
),
|
|
||||||
tag = "repos"
|
|
||||||
)]
|
|
||||||
pub async fn get_issue_report(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Path((org, repo)): Path<(String, String)>,
|
|
||||||
) -> Result<Json<Vec<forge::IssueReportRow>>, StatusUnavailable> {
|
|
||||||
let Some(client) = state.forge.as_ref() else {
|
|
||||||
return Err(StatusUnavailable(
|
|
||||||
"no forge is configured on this host".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
match client.issue_report(&org, &repo).await {
|
|
||||||
Ok(rows) => Ok(Json(rows)),
|
|
||||||
Err(e) => {
|
|
||||||
let detail = format!("{e:#}");
|
|
||||||
tracing::warn!(error = %detail, %org, %repo, "building issue report failed");
|
|
||||||
Err(StatusUnavailable(detail))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -43,7 +43,6 @@ use utoipa_axum::{router::OpenApiRouter, routes};
|
||||||
mod auth;
|
mod auth;
|
||||||
mod config_pr;
|
mod config_pr;
|
||||||
mod forge;
|
mod forge;
|
||||||
mod issue_report;
|
|
||||||
mod otel_http_client;
|
mod otel_http_client;
|
||||||
mod status;
|
mod status;
|
||||||
mod vcs_metrics;
|
mod vcs_metrics;
|
||||||
|
|
@ -388,7 +387,6 @@ fn socket_path() -> PathBuf {
|
||||||
(name = "jobq", description = "the swarm-level job graph"),
|
(name = "jobq", description = "the swarm-level job graph"),
|
||||||
(name = "agents", description = "creating agent identities at swarm level"),
|
(name = "agents", description = "creating agent identities at swarm level"),
|
||||||
(name = "webhook", description = "swarm-wide forge webhook receipt"),
|
(name = "webhook", description = "swarm-wide forge webhook receipt"),
|
||||||
(name = "repos", description = "forge repo browsing (swarm-ui's issue-report page)"),
|
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
|
|
@ -476,13 +474,6 @@ struct AppState {
|
||||||
/// `Arc<str>` rationale as `webhook_secret`: never mutated, so a
|
/// `Arc<str>` rationale as `webhook_secret`: never mutated, so a
|
||||||
/// clone per request is just a refcount bump.
|
/// clone per request is just a refcount bump.
|
||||||
swarm_name: Option<Arc<str>>,
|
swarm_name: Option<Arc<str>>,
|
||||||
/// The forge client itself, for the read-only repo/issue-report
|
|
||||||
/// routes (`GET /api/repos`, `GET /api/repos/{org}/{repo}/issue-report`)
|
|
||||||
/// — distinct from `config_prs`, which holds a *cache* built off this
|
|
||||||
/// same client rather than the client. `None` under the same
|
|
||||||
/// "no forge configured on this host" shape every other
|
|
||||||
/// forge-backed field here uses.
|
|
||||||
forge: Option<Arc<forge::Client>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Env var the controller's NixOS module sets from
|
/// Env var the controller's NixOS module sets from
|
||||||
|
|
@ -1197,20 +1188,6 @@ async fn connect_status_reader() -> Result<Option<Arc<status::StatusReader>>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clones `forge_client` for [`AppState::forge`] before handing the
|
|
||||||
/// original off to [`register_swarm_webhooks`], which takes it by value —
|
|
||||||
/// split out of `main` purely to keep that function under clippy's
|
|
||||||
/// line-count lint (same rationale as [`connect_status_reader`]'s own doc
|
|
||||||
/// comment), no behavior change.
|
|
||||||
fn keep_forge_for_state(
|
|
||||||
forge_client: Option<Arc<forge::Client>>,
|
|
||||||
webhook_secret: Option<Arc<str>>,
|
|
||||||
) -> Option<Arc<forge::Client>> {
|
|
||||||
let state_forge = forge_client.clone();
|
|
||||||
register_swarm_webhooks(forge_client, webhook_secret);
|
|
||||||
state_forge
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
|
|
@ -1323,7 +1300,8 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_prs = forge_client.clone().map(config_pr::spawn);
|
let config_prs = forge_client.clone().map(config_pr::spawn);
|
||||||
let state_forge = keep_forge_for_state(forge_client, webhook_secret.clone());
|
|
||||||
|
register_swarm_webhooks(forge_client, webhook_secret.clone());
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
hives: Arc::new(load_hives()),
|
hives: Arc::new(load_hives()),
|
||||||
|
|
@ -1334,7 +1312,6 @@ async fn main() -> Result<()> {
|
||||||
config_prs,
|
config_prs,
|
||||||
swarm_name: load_swarm_name().map(Arc::from),
|
swarm_name: load_swarm_name().map(Arc::from),
|
||||||
auth,
|
auth,
|
||||||
forge: state_forge,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (router, api) = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
|
let (router, api) = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
|
||||||
|
|
@ -1349,9 +1326,6 @@ async fn main() -> Result<()> {
|
||||||
.routes(routes!(get_config_prs))
|
.routes(routes!(get_config_prs))
|
||||||
.routes(routes!(create_agent))
|
.routes(routes!(create_agent))
|
||||||
.routes(routes!(get_agents))
|
.routes(routes!(get_agents))
|
||||||
.routes(routes!(issue_report::get_repos))
|
|
||||||
.routes(routes!(issue_report::get_issue_report_all))
|
|
||||||
.routes(routes!(issue_report::get_issue_report))
|
|
||||||
.routes(routes!(webhook::post_webhook_forge))
|
.routes(routes!(webhook::post_webhook_forge))
|
||||||
.split_for_parts();
|
.split_for_parts();
|
||||||
// Just the JSON, not the UI — Swagger UI itself is nginx-hosted from
|
// Just the JSON, not the UI — Swagger UI itself is nginx-hosted from
|
||||||
|
|
@ -1463,7 +1437,6 @@ mod tests {
|
||||||
// job and never consults one. The roster read is the verb that
|
// job and never consults one. The roster read is the verb that
|
||||||
// needs it, and it has its own test below.
|
// needs it, and it has its own test below.
|
||||||
auth: None,
|
auth: None,
|
||||||
forge: None,
|
|
||||||
};
|
};
|
||||||
(state, sched)
|
(state, sched)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue