diff --git a/blog/lychee.toml b/blog/lychee.toml index 0c91301b3..ff6306198 100644 --- a/blog/lychee.toml +++ b/blog/lychee.toml @@ -9,4 +9,6 @@ accept = ["200..=299", "429"] exclude = [ # refuses requests without a browser session '^https://bsky\.app/', + # We don't have anything on the bare host endpoint right now + '^https://api\.tangled\.org/?$', ] diff --git a/blog/posts/6-months.md b/blog/posts/6-months.md index 837e69405..2f64b629f 100644 --- a/blog/posts/6-months.md +++ b/blog/posts/6-months.md @@ -29,7 +29,7 @@ architecture [here](/blog/intro). ## new logo and mascot: dolly! Tangled finally has a logo! Designed by Akshay himself, Dolly is in -reference to the first ever *cloned* mammal. For a full set of brand assets and guidelines, see our new [branding page](https://tangled.org/brand). +reference to the first ever *cloned* mammal. ![logo with text](https://assets.tangled.network/blog/logo_with_text.jpeg) @@ -156,6 +156,6 @@ announcements to come. Be on the lookout for: reproducible * achieve complete independence from Bluesky PBC by hosting our own relay -That's all for now; we'll see you in the atmosphere! Meanwhile, if you'd like to contribute to projects on Tangled, make sure to check out the [good first issues page](https://tangled.org/goodfirstissues) to get started! +That's all for now; we'll see you in the atmosphere! Meanwhile, if you'd like to contribute to projects on Tangled, look for "good first issue" labels on the repositories you use. [^1]: atprotation implies a two-way sync between the PDS and appview. Currently, pull requests and repositories are not ingested -- so writing/updating either records on your PDS will not show up on the appview. diff --git a/blog/posts/ci.md b/blog/posts/ci.md index 79bd3f865..80e4abb34 100644 --- a/blog/posts/ci.md +++ b/blog/posts/ci.md @@ -191,7 +191,7 @@ fetching the public key from the DID document. The secrets themselves are stored in a secret manager. By default, this is the same sqlite database that spindle uses. This is *fine* for self-hosters. The hosted, flagship -instance at https://spindle.tangled.sh however uses +instance at https://spindle.tangled.network however uses [OpenBao](https://openbao.org), an OSS fork of HashiCorp Vault. diff --git a/blog/posts/docs.md b/blog/posts/docs.md index bca5eaaf2..dfe0eafbb 100644 --- a/blog/posts/docs.md +++ b/blog/posts/docs.md @@ -71,13 +71,11 @@ input](https://pandoc.org/demo/MANUAL.txt). Massaging pandoc to work for us was quite straightforward: - I first combined all our individual markdown files into - [one big - `DOCS.md`](https://tangled.org/tangled.org/core/blob/master/docs/DOCS.md) - file. + one big `DOCS.md` file. - Modified the [default template](https://github.com/jgm/pandoc-templates/blob/master/default.chunkedhtml) to put the TOC on every page, to form a "sidebar", see - [`docs/template.html`](https://tangled.org/tangled.org/core/blob/master/docs/template.html) + `docs/template.html` - Inserted tailwind `prose` classes where necessary, such that markdown content is rendered the same way between `tangled.org` and `docs.tangled.org` @@ -155,8 +153,8 @@ bar also simply redirects to Google: I mentioned earlier that Ctrl+F has typically worked better for me than, say, the search engine provided by Docusaurus. -To that end, the same docs have been exported to a ["single -page" format](https://docs.tangled.org/single-page.html), by +To that end, the same docs have been exported to a "single +page" format, by just removing the `chunkedhtml` related options: ```diff diff --git a/blog/posts/forge.mdx b/blog/posts/forge.mdx index abd259ad9..3605d5a77 100644 --- a/blog/posts/forge.mdx +++ b/blog/posts/forge.mdx @@ -87,7 +87,7 @@ single SQLite database on one medium-sized server in Sweden. This was great as long as you were ~50–100 ms away from Sweden, but most of our users aren't! We rearchitected: we built a new standalone frontend app in Svelte, deployed at the edge. It is populated by our new -diskless, entirely in-memory indexer service written in Rust: [bobbin](https://tangled.org/docs/components/bobbin). +diskless, entirely in-memory indexer service written in Rust: [bobbin](https://tangled.org/docs/api/bobbin/). Bobbins are deployed globally in over 11+ regions, ensuring users around the world get the best API and UI performance. diff --git a/blog/posts/newsletter-01.md b/blog/posts/newsletter-01.md index 9ecd9bfa9..d58187dcb 100644 --- a/blog/posts/newsletter-01.md +++ b/blog/posts/newsletter-01.md @@ -22,7 +22,7 @@ newsletter. We haven't settled on a cutesy name for it yet -- If you're new here (how'd you end up on our list?!), Tangled is a new social coding platform. You can host Git repositories, collaborate with friends, and self-host your git remotes -([knots](https://docs.tangled.org/knot-self-hosting-guide.html)) and CI +([knots](https://tangled.org/docs/knots/self-hosting)) and CI runners (spindles). We recently raised €3,8M ($4.5M) in [seed financing](/blog/seed) and have been heads-down building. diff --git a/knotmirror/crates/knotmirror-types/src/lib.rs b/knotmirror/crates/knotmirror-types/src/lib.rs index 6eced00d9..1cf4a2ab8 100644 --- a/knotmirror/crates/knotmirror-types/src/lib.rs +++ b/knotmirror/crates/knotmirror-types/src/lib.rs @@ -55,8 +55,8 @@ pub struct OpsEndpoint { impl OpsEndpoint { pub fn parse(input: &str) -> Result { - let url = url::Url::parse(input) - .map_err(|e| format!("{input:?} isn't a valid base URL: {e}"))?; + let url = + url::Url::parse(input).map_err(|e| format!("{input:?} isn't a valid base URL: {e}"))?; if !matches!(url.scheme(), "http" | "https") { return Err(format!("{input:?} must use http/https")); } @@ -77,7 +77,11 @@ impl OpsEndpoint { fn joined(&self, suffix: &str) -> String { let mut url = self.url.clone(); - url.set_path(&format!("{}/{}", self.url.path().trim_end_matches('/'), suffix)); + url.set_path(&format!( + "{}/{}", + self.url.path().trim_end_matches('/'), + suffix + )); url.to_string() } @@ -104,7 +108,9 @@ mod ops_endpoint_tests { #[test] fn ops_endpoint_joins_paths_onto_good_base() { assert_eq!( - OpsEndpoint::parse("https://ops.example").unwrap().xrpc("a.b.c"), + OpsEndpoint::parse("https://ops.example") + .unwrap() + .xrpc("a.b.c"), "https://ops.example/xrpc/a.b.c" ); assert_eq!( diff --git a/knotmirror/crates/knotmirror-xrpc/src/routes/activity.rs b/knotmirror/crates/knotmirror-xrpc/src/routes/activity.rs index ed2eeea84..c2269c136 100644 --- a/knotmirror/crates/knotmirror-xrpc/src/routes/activity.rs +++ b/knotmirror/crates/knotmirror-xrpc/src/routes/activity.rs @@ -59,7 +59,7 @@ pub(crate) async fn did_document(State(state): State) -> Response { mod tests { use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::{SystemTime, UNIX_EPOCH}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; @@ -70,7 +70,7 @@ mod tests { use jacquard_common::service_auth::{PublicKey, parse_jwt, verify_signature}; use jacquard_common::types::did::Did; use jacquard_identity::JacquardResolver; - use knotmirror_db::{ACTIVITY_MONTHS, CommitActivity, Db, MonthWindow}; + use knotmirror_db::{ACTIVITY_MONTHS, CommitActivity, Db, DbError, MonthWindow}; use serde_json::{Value, json}; use crate::{AppState, Deliberi, KnotPolicy, ServiceSigner}; @@ -99,18 +99,34 @@ mod tests { .as_micros(), NEXT.fetch_add(1, Ordering::Relaxed) ); - let admin = Db::connect(&url, 1).await.unwrap(); + let admin = connect_with_retry(&url, 1).await; sqlx::query(sqlx::AssertSqlSafe(format!("create database {name}"))) .execute(admin.pool()) .await .unwrap(); let mut url = reqwest::Url::parse(&url).unwrap(); url.set_path(&name); - let db = Db::connect(url.as_str(), 4).await.unwrap(); + let db = connect_with_retry(url.as_str(), 4).await; db.migrate().await.unwrap(); Some(db) } + async fn connect_with_retry(url: &str, max_connections: u32) -> Db { + let mut backoff = Duration::from_millis(250); + for _ in 0..5 { + match Db::connect(url, max_connections).await { + Ok(db) => return db, + Err(DbError::Connection(err)) => { + eprintln!("connect to {url} timed out ({err}), retrying"); + tokio::time::sleep(backoff).await; + } + Err(err) => panic!("connect to {url}: {err}"), + } + backoff *= 2; + } + Db::connect(url, max_connections).await.unwrap() + } + #[derive(Clone)] struct FakeDeliberi { public_key: String, diff --git a/knotmirror/crates/knotmirror-xrpc/src/routes/git.rs b/knotmirror/crates/knotmirror-xrpc/src/routes/git.rs index d78e9e615..ab461a360 100644 --- a/knotmirror/crates/knotmirror-xrpc/src/routes/git.rs +++ b/knotmirror/crates/knotmirror-xrpc/src/routes/git.rs @@ -1203,9 +1203,12 @@ async fn push_remote( // TODO(post-1.0): remove legacy `#atproto_pds` fallback .or_else(|| document.pds_endpoint()) .ok_or_else(|| XrpcError::InvalidRequest(format!("{did} declares no knot service")))?; - let knot = state.knot_policy.safe_knot_host(endpoint.as_str()).map_err(|_reason| { - XrpcError::InvalidRequest(format!("{did} declares invalid knot service endpoint")) - })?; + let knot = state + .knot_policy + .safe_knot_host(endpoint.as_str()) + .map_err(|_reason| { + XrpcError::InvalidRequest(format!("{did} declares invalid knot service endpoint")) + })?; let audience = knot_service_did(&knot); if auth.aud.audience().as_str() != audience { @@ -1218,7 +1221,6 @@ async fn push_remote( Ok(knot.repo_url(did)) } - fn knot_service_did(knot: &KnotHost) -> String { let url = knot.url(); match url.port() { @@ -2125,7 +2127,11 @@ mod merge_commit_tests { allow_private: false, require_https: true, }; - let base = |endpoint| deployed.safe_knot_host(endpoint).map(|host| host.url().to_string()); + let base = |endpoint| { + deployed + .safe_knot_host(endpoint) + .map(|host| host.url().to_string()) + }; assert_eq!( base("https://knot.example/repo/abc").ok().as_deref(), Some("https://knot.example/"), @@ -2147,7 +2153,10 @@ mod merge_commit_tests { allow_private: true, require_https: false, }; - let base = |endpoint| dev.safe_knot_host(endpoint).map(|host| host.url().to_string()); + let base = |endpoint| { + dev.safe_knot_host(endpoint) + .map(|host| host.url().to_string()) + }; assert_eq!( base("http://127.0.0.1:5555/repo/abc").ok().as_deref(), Some("http://127.0.0.1:5555/") diff --git a/knotmirror/crates/knotmirror-xrpc/src/routes/reads.rs b/knotmirror/crates/knotmirror-xrpc/src/routes/reads.rs index 64e448658..f0d76e19c 100644 --- a/knotmirror/crates/knotmirror-xrpc/src/routes/reads.rs +++ b/knotmirror/crates/knotmirror-xrpc/src/routes/reads.rs @@ -1434,7 +1434,10 @@ mod format_patch_tests { let response = mirror.blob("logo.svg", None).await; assert_eq!(response.status(), 200); assert_eq!(response.headers()["content-type"], "image/svg+xml"); - assert_eq!(response.headers()["content-security-policy"], super::SANDBOX_CSP); + assert_eq!( + response.headers()["content-security-policy"], + super::SANDBOX_CSP + ); assert_eq!(response.headers()["x-content-type-options"], "nosniff"); } diff --git a/nix/Cargo.nix b/nix/Cargo.nix index e615e1615..f48de2610 100644 --- a/nix/Cargo.nix +++ b/nix/Cargo.nix @@ -21045,6 +21045,10 @@ rec { name = "knot-pack"; packageId = "knot-pack"; } + { + name = "knot-runtime"; + packageId = "knot-runtime"; + } { name = "knot-types"; packageId = "knot-types"; @@ -21242,11 +21246,6 @@ rec { name = "bobbin-runtime"; packageId = "bobbin-runtime"; } - { - name = "chrono"; - packageId = "chrono"; - features = [ "serde" ]; - } { name = "gix"; packageId = "gix"; @@ -21315,6 +21314,11 @@ rec { } ]; devDependencies = [ + { + name = "chrono"; + packageId = "chrono"; + features = [ "serde" ]; + } { name = "tempfile"; packageId = "tempfile"; @@ -21580,6 +21584,16 @@ rec { name = "jacquard-common"; packageId = "jacquard-common"; } + { + name = "serde"; + packageId = "serde"; + features = [ "derive" ]; + } + { + name = "url"; + packageId = "url"; + features = [ "serde" ]; + } ]; }; diff --git a/sitesd/sitefeed/feed_test.go b/sitesd/sitefeed/feed_test.go index 94c1558fc..a629e263f 100644 --- a/sitesd/sitefeed/feed_test.go +++ b/sitesd/sitefeed/feed_test.go @@ -387,8 +387,13 @@ func TestFeedTrigger_LastPushWinsOverARacingDeploy(t *testing.T) { return ctxFor(), nil } + unlock, err := f.locks.Lock(context.Background(), feedTestRepoDid) + if err != nil { + t.Fatal(err) + } f.maybeDeploy(context.Background(), ctxFor(), "main", feedTestSha) f.maybeDeploy(context.Background(), ctxFor(), "main", feedTestOtherSha) + unlock() var winner *models.SiteDeploy pollFor(t, "a successful deploy record for the later push", func() bool {