···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+package tangled
4+5+// schema: sh.tangled.repo.tree
6+7+import (
8+ "context"
9+10+ "github.com/bluesky-social/indigo/lex/util"
11+)
12+13+const (
14+ RepoTreeNSID = "sh.tangled.repo.tree"
15+)
16+17+// RepoTree_LastCommit is a "lastCommit" in the sh.tangled.repo.tree schema.
18+type RepoTree_LastCommit struct {
19+ // hash: Commit hash
20+ Hash string `json:"hash" cborgen:"hash"`
21+ // message: Commit message
22+ Message string `json:"message" cborgen:"message"`
23+ // when: Commit timestamp
24+ When string `json:"when" cborgen:"when"`
25+}
26+27+// RepoTree_Output is the output of a sh.tangled.repo.tree call.
28+type RepoTree_Output struct {
29+ // dotdot: Parent directory path
30+ Dotdot *string `json:"dotdot,omitempty" cborgen:"dotdot,omitempty"`
31+ Files []*RepoTree_TreeEntry `json:"files" cborgen:"files"`
32+ // parent: The parent path in the tree
33+ Parent *string `json:"parent,omitempty" cborgen:"parent,omitempty"`
34+ // ref: The git reference used
35+ Ref string `json:"ref" cborgen:"ref"`
36+}
37+38+// RepoTree_TreeEntry is a "treeEntry" in the sh.tangled.repo.tree schema.
39+type RepoTree_TreeEntry struct {
40+ // is_file: Whether this entry is a file
41+ Is_file bool `json:"is_file" cborgen:"is_file"`
42+ // is_subtree: Whether this entry is a directory/subtree
43+ Is_subtree bool `json:"is_subtree" cborgen:"is_subtree"`
44+ Last_commit *RepoTree_LastCommit `json:"last_commit,omitempty" cborgen:"last_commit,omitempty"`
45+ // mode: File mode
46+ Mode string `json:"mode" cborgen:"mode"`
47+ // name: Relative file or directory name
48+ Name string `json:"name" cborgen:"name"`
49+ // size: File size in bytes
50+ Size int64 `json:"size" cborgen:"size"`
51+}
52+53+// RepoTree calls the XRPC method "sh.tangled.repo.tree".
54+//
55+// path: Path within the repository tree
56+// ref: Git reference (branch, tag, or commit SHA)
57+// repo: Repository identifier in format 'did:plc:.../repoName'
58+func RepoTree(ctx context.Context, c util.LexClient, path string, ref string, repo string) (*RepoTree_Output, error) {
59+ var out RepoTree_Output
60+61+ params := map[string]interface{}{}
62+ if path != "" {
63+ params["path"] = path
64+ }
65+ params["ref"] = ref
66+ params["repo"] = repo
67+ if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.tree", params, nil, &out); err != nil {
68+ return nil, err
69+ }
70+71+ return &out, nil
72+}
+30
api/tangled/tangledowner.go
···000000000000000000000000000000
···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+package tangled
4+5+// schema: sh.tangled.owner
6+7+import (
8+ "context"
9+10+ "github.com/bluesky-social/indigo/lex/util"
11+)
12+13+const (
14+ OwnerNSID = "sh.tangled.owner"
15+)
16+17+// Owner_Output is the output of a sh.tangled.owner call.
18+type Owner_Output struct {
19+ Owner string `json:"owner" cborgen:"owner"`
20+}
21+22+// Owner calls the XRPC method "sh.tangled.owner".
23+func Owner(ctx context.Context, c util.LexClient) (*Owner_Output, error) {
24+ var out Owner_Output
25+ if err := c.LexDo(ctx, util.Query, "", "sh.tangled.owner", nil, nil, &out); err != nil {
26+ return nil, err
27+ }
28+29+ return &out, nil
30+}
+1-1
appview/config/config.go
···21 AppPassword string `env:"APP_PASSWORD"`
2223 // uhhhh this is because knot1 is under icy's did
24- TmpAltAppPassword string `env:"ALT_APP_PASSWORD, required"`
25}
2627type OAuthConfig struct {
···21 AppPassword string `env:"APP_PASSWORD"`
2223 // uhhhh this is because knot1 is under icy's did
24+ TmpAltAppPassword string `env:"ALT_APP_PASSWORD"`
25}
2627type OAuthConfig struct {
···703 return err
704 })
705706+ // repurpose the read-only column to "needs-upgrade"
707+ runMigration(conn, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
708+ _, err := tx.Exec(`
709+ alter table registrations rename column read_only to needs_upgrade;
710+ `)
711+ return err
712+ })
713+714+ // require all knots to upgrade after the release of total xrpc
715+ runMigration(conn, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
716+ _, err := tx.Exec(`
717+ update registrations set needs_upgrade = 1;
718+ `)
719+ return err
720+ })
721+722+ // require all knots to upgrade after the release of total xrpc
723+ runMigration(conn, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
724+ _, err := tx.Exec(`
725+ alter table spindles add column needs_upgrade integer not null default 0;
726+ `)
727+ if err != nil {
728+ return err
729+ }
730+731+ _, err = tx.Exec(`
732+ update spindles set needs_upgrade = 1;
733+ `)
734+ return err
735+ })
736+737+ // remove issue_at from issues and replace with generated column
738+ //
739+ // this requires a full table recreation because stored columns
740+ // cannot be added via alter
741+ //
742+ // couple other changes:
743+ // - columns renamed to be more consistent
744+ // - adds edited and deleted fields
745+ //
746+ // disable foreign-keys for the next migration
747+ conn.ExecContext(ctx, "pragma foreign_keys = off;")
748+ runMigration(conn, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
749+ _, err := tx.Exec(`
750+ create table if not exists issues_new (
751+ -- identifiers
752+ id integer primary key autoincrement,
753+ did text not null,
754+ rkey text not null,
755+ at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored,
756+757+ -- at identifiers
758+ repo_at text not null,
759+760+ -- content
761+ issue_id integer not null,
762+ title text not null,
763+ body text not null,
764+ open integer not null default 1,
765+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
766+ edited text, -- timestamp
767+ deleted text, -- timestamp
768+769+ unique(did, rkey),
770+ unique(repo_at, issue_id),
771+ unique(at_uri),
772+ foreign key (repo_at) references repos(at_uri) on delete cascade
773+ );
774+ `)
775+ if err != nil {
776+ return err
777+ }
778+779+ // transfer data
780+ _, err = tx.Exec(`
781+ insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created)
782+ select
783+ i.id,
784+ i.owner_did,
785+ i.rkey,
786+ i.repo_at,
787+ i.issue_id,
788+ i.title,
789+ i.body,
790+ i.open,
791+ i.created
792+ from issues i;
793+ `)
794+ if err != nil {
795+ return err
796+ }
797+798+ // drop old table
799+ _, err = tx.Exec(`drop table issues`)
800+ if err != nil {
801+ return err
802+ }
803+804+ // rename new table
805+ _, err = tx.Exec(`alter table issues_new rename to issues`)
806+ return err
807+ })
808+ conn.ExecContext(ctx, "pragma foreign_keys = on;")
809+810+ // - renames the comments table to 'issue_comments'
811+ // - rework issue comments to update constraints:
812+ // * unique(did, rkey)
813+ // * remove comment-id and just use the global ID
814+ // * foreign key (repo_at, issue_id)
815+ // - new columns
816+ // * column "reply_to" which can be any other comment
817+ // * column "at-uri" which is a generated column
818+ runMigration(conn, "rework-issue-comments", func(tx *sql.Tx) error {
819+ _, err := tx.Exec(`
820+ create table if not exists issue_comments (
821+ -- identifiers
822+ id integer primary key autoincrement,
823+ did text not null,
824+ rkey text,
825+ at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored,
826+827+ -- at identifiers
828+ issue_at text not null,
829+ reply_to text, -- at_uri of parent comment
830+831+ -- content
832+ body text not null,
833+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
834+ edited text,
835+ deleted text,
836+837+ -- constraints
838+ unique(did, rkey),
839+ unique(at_uri),
840+ foreign key (issue_at) references issues(at_uri) on delete cascade
841+ );
842+ `)
843+ if err != nil {
844+ return err
845+ }
846+847+ // transfer data
848+ _, err = tx.Exec(`
849+ insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted)
850+ select
851+ c.id,
852+ c.owner_did,
853+ c.rkey,
854+ i.at_uri, -- get at_uri from issues table
855+ c.body,
856+ c.created,
857+ c.edited,
858+ c.deleted
859+ from comments c
860+ join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id;
861+ `)
862+ if err != nil {
863+ return err
864+ }
865+866+ // drop old table
867+ _, err = tx.Exec(`drop table comments`)
868+ return err
869+ })
870+871 return &DB{db}, nil
872}
873···912 }
913914 return nil
915+}
916+917+func (d *DB) Close() error {
918+ return d.DB.Close()
919}
920921type filter struct {
+4-4
appview/db/follow.go
···56}
5758type FollowStats struct {
59- Followers int
60- Following int
61}
6263func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) {
64- followers, following := 0, 0
65 err := e.QueryRow(
66 `SELECT
67 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
···122123 for rows.Next() {
124 var did string
125- var followers, following int
126 if err := rows.Scan(&did, &followers, &following); err != nil {
127 return nil, err
128 }
···56}
5758type FollowStats struct {
59+ Followers int64
60+ Following int64
61}
6263func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) {
64+ var followers, following int64
65 err := e.QueryRow(
66 `SELECT
67 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
···122123 for rows.Next() {
124 var did string
125+ var followers, following int64
126 if err := rows.Scan(&did, &followers, &following); err != nil {
127 return nil, err
128 }
···3import (
4 "errors"
5 "fmt"
6- "log"
7 "log/slog"
8 "net/http"
9 "slices"
···17 "tangled.sh/tangled.sh/core/appview/oauth"
18 "tangled.sh/tangled.sh/core/appview/pages"
19 "tangled.sh/tangled.sh/core/appview/serververify"
020 "tangled.sh/tangled.sh/core/eventconsumer"
21 "tangled.sh/tangled.sh/core/idresolver"
22 "tangled.sh/tangled.sh/core/rbac"
···49 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
51 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
52-53- r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner)
5455 return r
56}
···399 if err != nil {
400 l.Error("verification failed", "err", err)
401402- if errors.Is(err, serververify.FetchError) {
403- k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
404 return
405 }
406···420 return
421 }
422423- // if this knot was previously read-only, then emit a record too
424 //
425 // this is part of migrating from the old knot system to the new one
426- if registration.ReadOnly {
427 // re-announce by registering under same rkey
428 client, err := k.OAuth.AuthorizedClient(r)
429 if err != nil {
···484 return
485 }
486 updatedRegistration := registrations[0]
487-488- log.Println(updatedRegistration)
489490 w.Header().Set("HX-Reswap", "outerHTML")
491 k.Pages.KnotListing(w, pages.KnotListingParams{
···678 // ok
679 k.Pages.HxRefresh(w)
680}
681-682-func (k *Knots) banner(w http.ResponseWriter, r *http.Request) {
683- user := k.OAuth.GetUser(r)
684- l := k.Logger.With("handler", "removeMember")
685- l = l.With("did", user.Did)
686- l = l.With("handle", user.Handle)
687-688- registrations, err := db.GetRegistrations(
689- k.Db,
690- db.FilterEq("did", user.Did),
691- db.FilterEq("read_only", 1),
692- )
693- if err != nil {
694- l.Error("non-fatal: failed to get registrations")
695- return
696- }
697-698- if registrations == nil {
699- return
700- }
701-702- k.Pages.KnotBanner(w, pages.KnotBannerParams{
703- Registrations: registrations,
704- })
705-}
···3import (
4 "errors"
5 "fmt"
06 "log/slog"
7 "net/http"
8 "slices"
···16 "tangled.sh/tangled.sh/core/appview/oauth"
17 "tangled.sh/tangled.sh/core/appview/pages"
18 "tangled.sh/tangled.sh/core/appview/serververify"
19+ "tangled.sh/tangled.sh/core/appview/xrpcclient"
20 "tangled.sh/tangled.sh/core/eventconsumer"
21 "tangled.sh/tangled.sh/core/idresolver"
22 "tangled.sh/tangled.sh/core/rbac"
···49 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
51 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
005253 return r
54}
···397 if err != nil {
398 l.Error("verification failed", "err", err)
399400+ if errors.Is(err, xrpcclient.ErrXrpcUnsupported) {
401+ k.Pages.Notice(w, noticeId, "Failed to verify knot, XRPC queries are unsupported on this knot, consider upgrading!")
402 return
403 }
404···418 return
419 }
420421+ // if this knot requires upgrade, then emit a record too
422 //
423 // this is part of migrating from the old knot system to the new one
424+ if registration.NeedsUpgrade {
425 // re-announce by registering under same rkey
426 client, err := k.OAuth.AuthorizedClient(r)
427 if err != nil {
···482 return
483 }
484 updatedRegistration := registrations[0]
00485486 w.Header().Set("HX-Reswap", "outerHTML")
487 k.Pages.KnotListing(w, pages.KnotListingParams{
···674 // ok
675 k.Pages.HxRefresh(w)
676}
0000000000000000000000000
+40
appview/middleware/middleware.go
···275 }
276}
2770000000000000000000000000000000000000000278// this should serve the go-import meta tag even if the path is technically
279// a 404 like tangled.sh/oppi.li/go-git/v5
280func (mw Middleware) GoImport() middlewareFunc {
···275 }
276}
277278+// middleware that is tacked on top of /{user}/{repo}/issues/{issue}
279+func (mw Middleware) ResolveIssue() middlewareFunc {
280+ return func(next http.Handler) http.Handler {
281+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
282+ f, err := mw.repoResolver.Resolve(r)
283+ if err != nil {
284+ log.Println("failed to fully resolve repo", err)
285+ mw.pages.ErrorKnot404(w)
286+ return
287+ }
288+289+ issueIdStr := chi.URLParam(r, "issue")
290+ issueId, err := strconv.Atoi(issueIdStr)
291+ if err != nil {
292+ log.Println("failed to fully resolve issue ID", err)
293+ mw.pages.ErrorKnot404(w)
294+ return
295+ }
296+297+ issues, err := db.GetIssues(
298+ mw.db,
299+ db.FilterEq("repo_at", f.RepoAt()),
300+ db.FilterEq("issue_id", issueId),
301+ )
302+ if err != nil {
303+ log.Println("failed to get issues", "err", err)
304+ return
305+ }
306+ if len(issues) != 1 {
307+ log.Println("got incorrect number of issues", "len(issuse)", len(issues))
308+ return
309+ }
310+ issue := issues[0]
311+312+ ctx := context.WithValue(r.Context(), "issue", &issue)
313+ next.ServeHTTP(w, r.WithContext(ctx))
314+ })
315+ }
316+}
317+318// this should serve the go-import meta tag even if the path is technically
319// a 404 like tangled.sh/oppi.li/go-git/v5
320func (mw Middleware) GoImport() middlewareFunc {
···13 FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
14}
1500000000000016func GetFormat(filename string) Format {
17 for format, extensions := range FileTypes {
18 for _, extension := range extensions {
···13 FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
14}
1516+// ReadmeFilenames contains the list of common README filenames to search for,
17+// in order of preference. Only includes well-supported formats.
18+var ReadmeFilenames = []string{
19+ "README.md", "readme.md",
20+ "README",
21+ "readme",
22+ "README.markdown",
23+ "readme.markdown",
24+ "README.txt",
25+ "readme.txt",
26+}
27+28func GetFormat(filename string) Format {
29 for format, extensions := range FileTypes {
30 for _, extension := range extensions {
···1+{{ define "banner" }}
2+<div class="flex items-center justify-center mx-auto w-full bg-red-100 dark:bg-red-900 border border-red-200 dark:border-red-800 rounded-b drop-shadow-sm text-red-800 dark:text-red-200">
3+ <details class="group p-2">
4+ <summary class="list-none cursor-pointer">
5+ <div class="flex gap-4 items-center">
6+ <span class="group-open:hidden inline">{{ i "triangle-alert" "w-4 h-4" }}</span>
7+ <span class="hidden group-open:inline">{{ i "x" "w-4 h-4" }}</span>
8+9+ <span class="group-open:hidden inline">Some services that you administer require an update. Click to show more.</span>
10+ <span class="hidden group-open:inline">Some services that you administer will have to be updated to be compatible with Tangled.</span>
11+ </div>
12+ </summary>
13+14+ {{ if .Registrations }}
15+ <ul class="list-disc mx-12 my-2">
16+ {{range .Registrations}}
17+ <li>Knot: {{ .Domain }}</li>
18+ {{ end }}
19+ </ul>
20+ {{ end }}
21+22+ {{ if .Spindles }}
23+ <ul class="list-disc mx-12 my-2">
24+ {{range .Spindles}}
25+ <li>Spindle: {{ .Instance }}</li>
26+ {{ end }}
27+ </ul>
28+ {{ end }}
29+30+ <div class="mx-6">
31+ These services may not be fully accessible until upgraded.
32+ <a class="underline text-red-800 dark:text-red-200"
33+ href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations.md">
34+ Click to read the upgrade guide</a>.
35+ </div>
36+ </details>
37+</div>
38+{{ end }}
+1-1
appview/pages/templates/errors/404.html
···17 The page you're looking for doesn't exist. It may have been moved, deleted, or you have the wrong URL.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20- <a href="javascript:history.back()" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700">
21 {{ i "arrow-left" "w-4 h-4" }}
22 go back
23 </a>
···17 The page you're looking for doesn't exist. It may have been moved, deleted, or you have the wrong URL.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20+ <a href="javascript:history.back()" class="btn no-underline hover:no-underline gap-2">
21 {{ i "arrow-left" "w-4 h-4" }}
22 go back
23 </a>
+4-4
appview/pages/templates/errors/500.html
···8 {{ i "alert-triangle" "w-8 h-8 text-red-500 dark:text-red-400" }}
9 </div>
10 </div>
11-12 <div class="space-y-4">
13 <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
14 500 — internal server error
···24 <p class="mt-1">Our team has been automatically notified about this error.</p>
25 </div>
26 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
27- <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white">
28 {{ i "refresh-cw" "w-4 h-4" }}
29 try again
30 </button>
31- <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700">
32 {{ i "home" "w-4 h-4" }}
33 back to home
34 </a>
···36 </div>
37 </div>
38</div>
39-{{ end }}
···8 {{ i "alert-triangle" "w-8 h-8 text-red-500 dark:text-red-400" }}
9 </div>
10 </div>
11+12 <div class="space-y-4">
13 <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
14 500 — internal server error
···24 <p class="mt-1">Our team has been automatically notified about this error.</p>
25 </div>
26 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
27+ <button onclick="location.reload()" class="btn-create gap-2">
28 {{ i "refresh-cw" "w-4 h-4" }}
29 try again
30 </button>
31+ <a href="/" class="btn no-underline hover:no-underline gap-2">
32 {{ i "home" "w-4 h-4" }}
33 back to home
34 </a>
···36 </div>
37 </div>
38</div>
39+{{ end }}
+2-2
appview/pages/templates/errors/503.html
···17 We were unable to reach the knot hosting this repository. The service may be temporarily unavailable.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20- <button onclick="location.reload()" class="btn-create px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline hover:text-white">
21 {{ i "refresh-cw" "w-4 h-4" }}
22 try again
23 </button>
24- <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700">
25 {{ i "arrow-left" "w-4 h-4" }}
26 back to timeline
27 </a>
···17 We were unable to reach the knot hosting this repository. The service may be temporarily unavailable.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20+ <button onclick="location.reload()" class="btn-create gap-2">
21 {{ i "refresh-cw" "w-4 h-4" }}
22 try again
23 </button>
24+ <a href="/" class="btn gap-2 no-underline hover:no-underline">
25 {{ i "arrow-left" "w-4 h-4" }}
26 back to timeline
27 </a>
+1-1
appview/pages/templates/errors/knot404.html
···17 The repository you were looking for could not be found. The knot serving the repository may be unavailable.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20- <a href="/" class="btn px-4 py-2 rounded flex items-center gap-2 no-underline hover:no-underline">
21 {{ i "arrow-left" "w-4 h-4" }}
22 back to timeline
23 </a>
···17 The repository you were looking for could not be found. The knot serving the repository may be unavailable.
18 </p>
19 <div class="flex flex-col sm:flex-row gap-3 justify-center items-center mt-6">
20+ <a href="/" class="btn flex items-center gap-2 no-underline hover:no-underline">
21 {{ i "arrow-left" "w-4 h-4" }}
22 back to timeline
23 </a>
···1-{{ define "knots/fragments/banner" }}
2-<div class="w-full px-6 py-2 -z-15 bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm">
3- A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }})
4- that you administer is presently read-only. Consider upgrading this knot to
5- continue creating repositories on it.
6- <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/migrations/knot-1.7.0.md">Click to read the upgrade guide</a>.
7-</div>
8-{{ end }}
9-
···1{{ define "title" }}knots{{ end }}
23{{ define "content" }}
4-<div class="px-6 py-4">
5 <h1 class="text-xl font-bold dark:text-white">Knots</h1>
00006</div>
78<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
···15{{ end }}
1617{{ define "about" }}
18- <section class="rounded flex flex-col gap-2">
19- <p class="dark:text-gray-300">
20- Knots are lightweight headless servers that enable users to host Git repositories with ease.
21- Knots are designed for either single or multi-tenant use which is perfect for self-hosting on a Raspberry Pi at home, or larger โcommunityโ servers.
22- When creating a repository, you can choose a knot to store it on.
23- <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">
24- Checkout the documentation if you're interested in self-hosting.
25- </a>
26 </p>
27- </section>
0028{{ end }}
2930{{ define "list" }}
···1{{ define "title" }}knots{{ end }}
23{{ define "content" }}
4+<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5 <h1 class="text-xl font-bold dark:text-white">Knots</h1>
6+ <span class="flex items-center gap-1">
7+ {{ i "book" "w-3 h-3" }}
8+ <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">docs</a>
9+ </span>
10</div>
1112<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
···19{{ end }}
2021{{ define "about" }}
22+<section class="rounded">
23+ <p class="text-gray-500 dark:text-gray-400">
24+ Knots are lightweight headless servers that enable users to host Git repositories with ease.
25+ When creating a repository, you can choose a knot to store it on.
000026 </p>
27+28+29+</section>
30{{ end }}
3132{{ define "list" }}
···1-{{ define "title" }} privacy policy {{ end }}
02{{ define "content" }}
3<div class="max-w-4xl mx-auto px-4 py-8">
4 <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8">
5 <div class="prose prose-gray dark:prose-invert max-w-none">
6- <h1>Privacy Policy</h1>
7-8- <p><strong>Last updated:</strong> {{ now.Format "January 2, 2006" }}</p>
9-10- <p>This Privacy Policy describes how Tangled ("we," "us," or "our") collects, uses, and shares your personal information when you use our platform and services (the "Service").</p>
11-12- <h2>1. Information We Collect</h2>
13-14- <h3>Account Information</h3>
15- <p>When you create an account, we collect:</p>
16- <ul>
17- <li>Your chosen username</li>
18- <li>Email address</li>
19- <li>Profile information you choose to provide</li>
20- <li>Authentication data</li>
21- </ul>
22-23- <h3>Content and Activity</h3>
24- <p>We store:</p>
25- <ul>
26- <li>Code repositories and associated metadata</li>
27- <li>Issues, pull requests, and comments</li>
28- <li>Activity logs and usage patterns</li>
29- <li>Public keys for authentication</li>
30- </ul>
31-32- <h2>2. Data Location and Hosting</h2>
33- <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 my-6">
34- <h3 class="text-blue-800 dark:text-blue-200 font-semibold mb-2">EU Data Hosting</h3>
35- <p class="text-blue-700 dark:text-blue-300">
36- <strong>All Tangled service data is hosted within the European Union.</strong> Specifically:
37- </p>
38- <ul class="text-blue-700 dark:text-blue-300 mt-2">
39- <li><strong>Personal Data Servers (PDS):</strong> Accounts hosted on Tangled PDS (*.tngl.sh) are located in Finland</li>
40- <li><strong>Application Data:</strong> All other service data is stored on EU-based servers</li>
41- <li><strong>Data Processing:</strong> All data processing occurs within EU jurisdiction</li>
42- </ul>
43- </div>
44-45- <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 my-6">
46- <h3 class="text-yellow-800 dark:text-yellow-200 font-semibold mb-2">External PDS Notice</h3>
47- <p class="text-yellow-700 dark:text-yellow-300">
48- <strong>Important:</strong> If your account is hosted on Bluesky's PDS or other self-hosted Personal Data Servers (not *.tngl.sh), we do not control that data. The data protection, storage location, and privacy practices for such accounts are governed by the respective PDS provider's policies, not this Privacy Policy. We only control data processing within our own services and infrastructure.
49- </p>
50- </div>
51-52- <h2>3. Third-Party Data Processors</h2>
53- <p>We only share your data with the following third-party processors:</p>
54-55- <h3>Resend (Email Services)</h3>
56- <ul>
57- <li><strong>Purpose:</strong> Sending transactional emails (account verification, notifications)</li>
58- <li><strong>Data Shared:</strong> Email address and necessary message content</li>
59- <li><strong>Location:</strong> EU-compliant email delivery service</li>
60- </ul>
61-62- <h3>Cloudflare (Image Caching)</h3>
63- <ul>
64- <li><strong>Purpose:</strong> Caching and optimizing image delivery</li>
65- <li><strong>Data Shared:</strong> Public images and associated metadata for caching purposes</li>
66- <li><strong>Location:</strong> Global CDN with EU data protection compliance</li>
67- </ul>
68-69- <h2>4. How We Use Your Information</h2>
70- <p>We use your information to:</p>
71- <ul>
72- <li>Provide and maintain the Service</li>
73- <li>Process your transactions and requests</li>
74- <li>Send you technical notices and support messages</li>
75- <li>Improve and develop new features</li>
76- <li>Ensure security and prevent fraud</li>
77- <li>Comply with legal obligations</li>
78- </ul>
79-80- <h2>5. Data Sharing and Disclosure</h2>
81- <p>We do not sell, trade, or rent your personal information. We may share your information only in the following circumstances:</p>
82- <ul>
83- <li>With the third-party processors listed above</li>
84- <li>When required by law or legal process</li>
85- <li>To protect our rights, property, or safety, or that of our users</li>
86- <li>In connection with a merger, acquisition, or sale of assets (with appropriate protections)</li>
87- </ul>
88-89- <h2>6. Data Security</h2>
90- <p>We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the Internet is 100% secure.</p>
91-92- <h2>7. Data Retention</h2>
93- <p>We retain your personal information for as long as necessary to provide the Service and fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required by law.</p>
94-95- <h2>8. Your Rights</h2>
96- <p>Under applicable data protection laws, you have the right to:</p>
97- <ul>
98- <li>Access your personal information</li>
99- <li>Correct inaccurate information</li>
100- <li>Request deletion of your information</li>
101- <li>Object to processing of your information</li>
102- <li>Data portability</li>
103- <li>Withdraw consent (where applicable)</li>
104- </ul>
105-106- <h2>9. Cookies and Tracking</h2>
107- <p>We use cookies and similar technologies to:</p>
108- <ul>
109- <li>Maintain your login session</li>
110- <li>Remember your preferences</li>
111- <li>Analyze usage patterns to improve the Service</li>
112- </ul>
113- <p>You can control cookie settings through your browser preferences.</p>
114-115- <h2>10. Children's Privacy</h2>
116- <p>The Service is not intended for children under 16 years of age. We do not knowingly collect personal information from children under 16. If we become aware that we have collected such information, we will take steps to delete it.</p>
117-118- <h2>11. International Data Transfers</h2>
119- <p>While all our primary data processing occurs within the EU, some of our third-party processors may process data outside the EU. When this occurs, we ensure appropriate safeguards are in place, such as Standard Contractual Clauses or adequacy decisions.</p>
120-121- <h2>12. Changes to This Privacy Policy</h2>
122- <p>We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date.</p>
123-124- <h2>13. Contact Information</h2>
125- <p>If you have any questions about this Privacy Policy or wish to exercise your rights, please contact us through our platform or via email.</p>
126-127- <div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-600 dark:text-gray-400">
128- <p>This Privacy Policy complies with the EU General Data Protection Regulation (GDPR) and other applicable data protection laws.</p>
129- </div>
130 </div>
131 </div>
132</div>
133-{{ end }}
···4<div class="max-w-4xl mx-auto px-4 py-8">
5 <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8">
6 <div class="prose prose-gray dark:prose-invert max-w-none">
7- <h1>Terms of Service</h1>
8-9- <p><strong>Last updated:</strong> {{ now.Format "January 2, 2006" }}</p>
10-11- <p>Welcome to Tangled. These Terms of Service ("Terms") govern your access to and use of the Tangled platform and services (the "Service") operated by us ("Tangled," "we," "us," or "our").</p>
12-13- <h2>1. Acceptance of Terms</h2>
14- <p>By accessing or using our Service, you agree to be bound by these Terms. If you disagree with any part of these terms, then you may not access the Service.</p>
15-16- <h2>2. Account Registration</h2>
17- <p>To use certain features of the Service, you must register for an account. You agree to provide accurate, current, and complete information during the registration process and to update such information to keep it accurate, current, and complete.</p>
18-19- <h2>3. Account Termination</h2>
20- <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 my-6">
21- <h3 class="text-red-800 dark:text-red-200 font-semibold mb-2">Important Notice</h3>
22- <p class="text-red-700 dark:text-red-300">
23- <strong>We reserve the right to terminate, suspend, or restrict access to your account at any time, for any reason, or for no reason at all, at our sole discretion.</strong> This includes, but is not limited to, termination for violation of these Terms, inappropriate conduct, spam, abuse, or any other behavior we deem harmful to the Service or other users.
24- </p>
25- <p class="text-red-700 dark:text-red-300 mt-2">
26- Account termination may result in the loss of access to your repositories, data, and other content associated with your account. We are not obligated to provide advance notice of termination, though we may do so in our discretion.
27- </p>
28- </div>
29-30- <h2>4. Acceptable Use</h2>
31- <p>You agree not to use the Service to:</p>
32- <ul>
33- <li>Violate any applicable laws or regulations</li>
34- <li>Infringe upon the rights of others</li>
35- <li>Upload, store, or share content that is illegal, harmful, threatening, abusive, harassing, defamatory, vulgar, obscene, or otherwise objectionable</li>
36- <li>Engage in spam, phishing, or other deceptive practices</li>
37- <li>Attempt to gain unauthorized access to the Service or other users' accounts</li>
38- <li>Interfere with or disrupt the Service or servers connected to the Service</li>
39- </ul>
40-41- <h2>5. Content and Intellectual Property</h2>
42- <p>You retain ownership of the content you upload to the Service. By uploading content, you grant us a non-exclusive, worldwide, royalty-free license to use, reproduce, modify, and distribute your content as necessary to provide the Service.</p>
43-44- <h2>6. Privacy</h2>
45- <p>Your privacy is important to us. Please review our <a href="/privacy" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Privacy Policy</a>, which also governs your use of the Service.</p>
46-47- <h2>7. Disclaimers</h2>
48- <p>The Service is provided on an "AS IS" and "AS AVAILABLE" basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.</p>
49-50- <h2>8. Limitation of Liability</h2>
51- <p>In no event shall Tangled, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential, or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your use of the Service.</p>
52-53- <h2>9. Indemnification</h2>
54- <p>You agree to defend, indemnify, and hold harmless Tangled and its affiliates, officers, directors, employees, and agents from and against any and all claims, damages, obligations, losses, liabilities, costs, or debt, and expenses (including attorney's fees).</p>
55-56- <h2>10. Governing Law</h2>
57- <p>These Terms shall be interpreted and governed by the laws of Finland, without regard to its conflict of law provisions.</p>
58-59- <h2>11. Changes to Terms</h2>
60- <p>We reserve the right to modify or replace these Terms at any time. If a revision is material, we will try to provide at least 30 days notice prior to any new terms taking effect.</p>
61-62- <h2>12. Contact Information</h2>
63- <p>If you have any questions about these Terms of Service, please contact us through our platform or via email.</p>
64-65- <div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-600 dark:text-gray-400">
66- <p>These terms are effective as of the last updated date shown above and will remain in effect except with respect to any changes in their provisions in the future, which will be in effect immediately after being posted on this page.</p>
67- </div>
68 </div>
69 </div>
70</div>
71-{{ end }}
···11### message format
1213```
14-<service/top-level directory>: <affected package/directory>: <short summary of change>
151617Optional longer description can go here, if necessary. Explain what the
···23Here are some examples:
2425```
26-appview: state: fix token expiry check in middleware
2728The previous check did not account for clock drift, leading to premature
29token invalidation.
30```
3132```
33-knotserver: git/service: improve error checking in upload-pack
34```
3536
···11### message format
1213```
14+<service/top-level directory>/<affected package/directory>: <short summary of change>
151617Optional longer description can go here, if necessary. Explain what the
···23Here are some examples:
2425```
26+appview/state: fix token expiry check in middleware
2728The previous check did not account for clock drift, leading to premature
29token invalidation.
30```
3132```
33+knotserver/git/service: improve error checking in upload-pack
34```
3536
+53-12
docs/hacking.md
···48redis-server
49```
5051-## running a knot
5253An end-to-end knot setup requires setting up a machine with
54`sshd`, `AuthorizedKeysCommand`, and git user, which is
55quite cumbersome. So the nix flake provides a
56`nixosConfiguration` to do so.
5758-To begin, grab your DID from http://localhost:3000/settings.
59-Then, set `TANGLED_VM_KNOT_OWNER` and
60-`TANGLED_VM_SPINDLE_OWNER` to your DID.
000000000000000000000000000006162-If you don't want to [set up a spindle](#running-a-spindle),
63-you can use any placeholder value.
0006465-You can now start a lightweight NixOS VM like so:
000006667```bash
68nix run --impure .#vm
···74with `ssh` exposed on port 2222.
7576Once the services are running, head to
77-http://localhost:3000/knots and hit verify (and similarly,
78-http://localhost:3000/spindles to verify your spindle). It
79-should verify the ownership of the services instantly if
80-everything went smoothly.
8182You can push repositories to this VM with this ssh config
83block on your main machine:
···97git push local-dev main
98```
99100-## running a spindle
101102The above VM should already be running a spindle on
103`localhost:6555`. Head to http://localhost:3000/spindles and
···119# litecli has a nicer REPL interface:
120litecli /var/lib/spindle/spindle.db
121```
00000
···48redis-server
49```
5051+## running knots and spindles
5253An end-to-end knot setup requires setting up a machine with
54`sshd`, `AuthorizedKeysCommand`, and git user, which is
55quite cumbersome. So the nix flake provides a
56`nixosConfiguration` to do so.
5758+<details>
59+ <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
60+61+ In order to build Tangled's dev VM on macOS, you will
62+ first need to set up a Linux Nix builder. The recommended
63+ way to do so is to run a [`darwin.linux-builder`
64+ VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder)
65+ and to register it in `nix.conf` as a builder for Linux
66+ with the same architecture as your Mac (`linux-aarch64` if
67+ you are using Apple Silicon).
68+69+ > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
70+ > the tangled repo so that it doesn't conflict with the other VM. For example,
71+ > you can do
72+ >
73+ > ```shell
74+ > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder
75+ > ```
76+ >
77+ > to store the builder VM in a temporary dir.
78+ >
79+ > You should read and follow [all the other intructions][darwin builder vm] to
80+ > avoid subtle problems.
81+82+ Alternatively, you can use any other method to set up a
83+ Linux machine with `nix` installed that you can `sudo ssh`
84+ into (in other words, root user on your Mac has to be able
85+ to ssh into the Linux machine without entering a password)
86+ and that has the same architecture as your Mac. See
87+ [remote builder
88+ instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements)
89+ for how to register such a builder in `nix.conf`.
9091+ > WARNING: If you'd like to use
92+ > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or
93+ > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo
94+ > ssh` works can be tricky. It seems to be [possible with
95+ > Orbstack](https://github.com/orgs/orbstack/discussions/1669).
9697+</details>
98+99+To begin, grab your DID from http://localhost:3000/settings.
100+Then, set `TANGLED_VM_KNOT_OWNER` and
101+`TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a
102+lightweight NixOS VM like so:
103104```bash
105nix run --impure .#vm
···111with `ssh` exposed on port 2222.
112113Once the services are running, head to
114+http://localhost:3000/knots and hit verify. It should
115+verify the ownership of the services instantly if everything
116+went smoothly.
0117118You can push repositories to this VM with this ssh config
119block on your main machine:
···133git push local-dev main
134```
135136+### running a spindle
137138The above VM should already be running a spindle on
139`localhost:6555`. Head to http://localhost:3000/spindles and
···155# litecli has a nicer REPL interface:
156litecli /var/lib/spindle/spindle.db
157```
158+159+If for any reason you wish to disable either one of the
160+services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set
161+`services.tangled-spindle.enable` (or
162+`services.tangled-knot.enable`) to `false`.
-35
docs/migrations/knot-1.7.0.md
···1-# Upgrading from v1.7.0
2-3-After v1.7.0, knot secrets have been deprecated. You no
4-longer need a secret from the appview to run a knot. All
5-authorized commands to knots are managed via [Inter-Service
6-Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
7-Knots will be read-only until upgraded.
8-9-Upgrading is quite easy, in essence:
10-11-- `KNOT_SERVER_SECRET` is no more, you can remove this
12- environment variable entirely
13-- `KNOT_SERVER_OWNER` is now required on boot, set this to
14- your DID. You can find your DID in the
15- [settings](https://tangled.sh/settings) page.
16-- Restart your knot once you have replaced the environment
17- variable
18-- Head to the [knot dashboard](https://tangled.sh/knots) and
19- hit the "retry" button to verify your knot. This simply
20- writes a `sh.tangled.knot` record to your PDS.
21-22-## Nix
23-24-If you use the nix module, simply bump the flake to the
25-latest revision, and change your config block like so:
26-27-```diff
28- services.tangled-knot = {
29- enable = true;
30- server = {
31-- secretFile = /path/to/secret;
32-+ owner = "did:plc:foo";
33- };
34- };
35-```
···1+# Migrations
2+3+This document is laid out in reverse-chronological order.
4+Newer migration guides are listed first, and older guides
5+are further down the page.
6+7+## Upgrading from v1.8.x
8+9+After v1.8.2, the HTTP API for knot and spindles have been
10+deprecated and replaced with XRPC. Repositories on outdated
11+knots will not be viewable from the appview. Upgrading is
12+straightforward however.
13+14+For knots:
15+16+- Upgrade to latest tag (v1.9.0 or above)
17+- Head to the [knot dashboard](https://tangled.sh/knots) and
18+ hit the "retry" button to verify your knot
19+20+For spindles:
21+22+- Upgrade to latest tag (v1.9.0 or above)
23+- Head to the [spindle
24+ dashboard](https://tangled.sh/spindles) and hit the
25+ "retry" button to verify your spindle
26+27+## Upgrading from v1.7.x
28+29+After v1.7.0, knot secrets have been deprecated. You no
30+longer need a secret from the appview to run a knot. All
31+authorized commands to knots are managed via [Inter-Service
32+Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
33+Knots will be read-only until upgraded.
34+35+Upgrading is quite easy, in essence:
36+37+- `KNOT_SERVER_SECRET` is no more, you can remove this
38+ environment variable entirely
39+- `KNOT_SERVER_OWNER` is now required on boot, set this to
40+ your DID. You can find your DID in the
41+ [settings](https://tangled.sh/settings) page.
42+- Restart your knot once you have replaced the environment
43+ variable
44+- Head to the [knot dashboard](https://tangled.sh/knots) and
45+ hit the "retry" button to verify your knot. This simply
46+ writes a `sh.tangled.knot` record to your PDS.
47+48+If you use the nix module, simply bump the flake to the
49+latest revision, and change your config block like so:
50+51+```diff
52+ services.tangled-knot = {
53+ enable = true;
54+ server = {
55+- secretFile = /path/to/secret;
56++ owner = "did:plc:foo";
57+ };
58+ };
59+```
60+
+130-54
docs/spindle/pipeline.md
···1-# spindle pipeline manifest
000023-Spindle pipelines are defined under the `.tangled/workflows` directory in a
4-repo. Generally:
000056-* Pipelines are defined in YAML.
7-* Workflows can run using different *engines*.
89-The most barebones workflow looks like this:
000000001011```yaml
12when:
13- - event: ["push"]
0014 branch: ["main"]
01500000000016engine: "nixery"
0001718-# optional
0000000019clone:
20 skip: false
21- depth: 50
22- submodules: true
23```
2425-The `when` and `engine` fields are required, while every other aspect
26-of how the definition is parsed is up to the engine. Currently, a spindle
27-provides at least one of these built-in engines:
2829-## `nixery`
3031-The Nixery engine uses an instance of [Nixery](https://nixery.dev) to run
32-steps that use dependencies from [Nixpkgs](https://github.com/NixOS/nixpkgs).
33-34-Here's an example that uses all fields:
3536```yaml
37-# build_and_test.yaml
38-when:
39- - event: ["push", "pull_request"]
40- branch: ["main", "develop"]
41- - event: ["manual"]
42-43dependencies:
44- ## from nixpkgs
45 nixpkgs:
46 - nodejs
47- ## custom registry
48- git+https://tangled.sh/@oppi.li/statix:
49- - statix
005051-steps:
52- - name: "Install dependencies"
53- command: "npm install"
54- environment:
55- NODE_ENV: "development"
56- CI: "true"
5758- - name: "Run linter"
59- command: "npm run lint"
000000000006061- - name: "Run tests"
62- command: "npm test"
63- environment:
64- NODE_ENV: "test"
65- JEST_WORKERS: "2"
6667- - name: "Build application"
00000000000000068 command: "npm run build"
69 environment:
70 NODE_ENV: "production"
07172-environment:
73- BUILD_NUMBER: "123"
74- GIT_BRANCH: "main"
7576-## current repository is cloned and checked out at the target ref
77-## by default.
000000000078clone:
79 skip: false
80- depth: 50
81- submodules: true
82-```
8384-## git push options
00000008586-These are push options that can be used with the `--push-option (-o)` flag of git push:
00000000000000008788-- `verbose-ci`, `ci-verbose`: enables diagnostics reporting for the CI pipeline, allowing you to see any issues when you push.
89-- `skip-ci`, `ci-skip`: skips triggering the CI pipeline.
···1+# spindle pipelines
2+3+Spindle workflows allow you to write CI/CD pipelines in a simple format. They're located in the `.tangled/workflows` directory at the root of your repository, and are defined using YAML.
4+5+The fields are:
67+- [Trigger](#trigger): A **required** field that defines when a workflow should be triggered.
8+- [Engine](#engine): A **required** field that defines which engine a workflow should run on.
9+- [Clone options](#clone-options): An **optional** field that defines how the repository should be cloned.
10+- [Dependencies](#dependencies): An **optional** field that allows you to list dependencies you may need.
11+- [Environment](#environment): An **optional** field that allows you to define environment variables.
12+- [Steps](#steps): An **optional** field that allows you to define what steps should run in the workflow.
1314+## Trigger
01516+The first thing to add to a workflow is the trigger, which defines when a workflow runs. This is defined using a `when` field, which takes in a list of conditions. Each condition has the following fields:
17+18+- `event`: This is a **required** field that defines when your workflow should run. It's a list that can take one or more of the following values:
19+ - `push`: The workflow should run every time a commit is pushed to the repository.
20+ - `pull_request`: The workflow should run every time a pull request is made or updated.
21+ - `manual`: The workflow can be triggered manually.
22+- `branch`: This is a **required** field that defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event.
23+24+For example, if you'd like define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
2526```yaml
27when:
28+ - event: ["push", "manual"]
29+ branch: ["main", "develop"]
30+ - event: ["pull_request"]
31 branch: ["main"]
32+```
3334+## Engine
35+36+Next is the engine on which the workflow should run, defined using the **required** `engine` field. The currently supported engines are:
37+38+- `nixery`: This uses an instance of [Nixery](https://nixery.dev) to run steps, which allows you to add [dependencies](#dependencies) from [Nixpkgs](https://github.com/NixOS/nixpkgs). You can search for packages on https://search.nixos.org, and there's a pretty good chance the package(s) you're looking for will be there.
39+40+Example:
41+42+```yaml
43engine: "nixery"
44+```
45+46+## Clone options
4748+When a workflow starts, the first step is to clone the repository. You can customize this behavior using the **optional** `clone` field. It has the following fields:
49+50+- `skip`: Setting this to `true` will skip cloning the repository. This can be useful if your workflow is doing something that doesn't require anything from the repository itself. This is `false` by default.
51+- `depth`: This sets the number of commits, or the "clone depth", to fetch from the repository. For example, if you set this to 2, the last 2 commits will be fetched. By default, the depth is set to 1, meaning only the most recent commit will be fetched, which is the commit that triggered the workflow.
52+- `submodules`: If you use [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your repository, setting this field to `true` will recursively fetch all submodules. This is `false` by default.
53+54+The default settings are:
55+56+```yaml
57clone:
58 skip: false
59+ depth: 1
60+ submodules: false
61```
6263+## Dependencies
006465+Usually when you're running a workflow, you'll need additional dependencies. The `dependencies` field lets you define which dependencies to get, and from where. It's a key-value map, with the key being the registry to fetch dependencies from, and the value being the list of dependencies to fetch.
6667+Say you want to fetch Node.js and Go from `nixpkgs`, and a package called `my_pkg` you've made from your own registry at your repository at `https://tangled.sh/@example.com/my_pkg`. You can define those dependencies like so:
0006869```yaml
00000070dependencies:
71+ # nixpkgs
72 nixpkgs:
73 - nodejs
74+ - go
75+ # custom registry
76+ git+https://tangled.sh/@example.com/my_pkg:
77+ - my_pkg
78+```
7980+Now these dependencies are available to use in your workflow!
000008182+## Environment
83+84+The `environment` field allows you define environment variables that will be available throughout the entire workflow. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.**
85+86+Example:
87+88+```yaml
89+environment:
90+ GOOS: "linux"
91+ GOARCH: "arm64"
92+ NODE_ENV: "production"
93+ MY_ENV_VAR: "MY_ENV_VALUE"
94+```
9596+## Steps
00009798+The `steps` field allows you to define what steps should run in the workflow. It's a list of step objects, each with the following fields:
99+100+- `name`: This field allows you to give your step a name. This name is visible in your workflow runs, and is used to describe what the step is doing.
101+- `command`: This field allows you to define a command to run in that step. The step is run in a Bash shell, and the logs from the command will be visible in the pipelines page on the Tangled website. The [dependencies](#dependencies) you added will be available to use here.
102+- `environment`: Similar to the global [environment](#environment) config, this **optional** field is a key-value map that allows you to set environment variables for the step. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.**
103+104+Example:
105+106+```yaml
107+steps:
108+ - name: "Build backend"
109+ command: "go build"
110+ environment:
111+ GOOS: "darwin"
112+ GOARCH: "arm64"
113+ - name: "Build frontend"
114 command: "npm run build"
115 environment:
116 NODE_ENV: "production"
117+```
118119+## Complete workflow
00120121+```yaml
122+# .tangled/workflows/build.yml
123+124+when:
125+ - event: ["push", "manual"]
126+ branch: ["main", "develop"]
127+ - event: ["pull_request"]
128+ branch: ["main"]
129+130+engine: "nixery"
131+132+# using the default values
133clone:
134 skip: false
135+ depth: 1
136+ submodules: false
0137138+dependencies:
139+ # nixpkgs
140+ nixpkgs:
141+ - nodejs
142+ - go
143+ # custom registry
144+ git+https://tangled.sh/@example.com/my_pkg:
145+ - my_pkg
146147+environment:
148+ GOOS: "linux"
149+ GOARCH: "arm64"
150+ NODE_ENV: "production"
151+ MY_ENV_VAR: "MY_ENV_VALUE"
152+153+steps:
154+ - name: "Build backend"
155+ command: "go build"
156+ environment:
157+ GOOS: "darwin"
158+ GOARCH: "arm64"
159+ - name: "Build frontend"
160+ command: "npm run build"
161+ environment:
162+ NODE_ENV: "production"
163+```
164165+If you want another example of a workflow, you can look at the one [Tangled uses to build the project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
0
···1+package xrpc
2+3+import (
4+ "encoding/json"
5+ "fmt"
6+ "net/http"
7+ "runtime/debug"
8+9+ "tangled.sh/tangled.sh/core/api/tangled"
10+ xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
11+)
12+13+// version is set during build time.
14+var version string
15+16+func (x *Xrpc) Version(w http.ResponseWriter, r *http.Request) {
17+ if version == "" {
18+ info, ok := debug.ReadBuildInfo()
19+ if !ok {
20+ http.Error(w, "failed to read build info", http.StatusInternalServerError)
21+ return
22+ }
23+24+ var modVer string
25+ var sha string
26+ var modified bool
27+28+ for _, mod := range info.Deps {
29+ if mod.Path == "tangled.sh/tangled.sh/knotserver/xrpc" {
30+ modVer = mod.Version
31+ break
32+ }
33+ }
34+35+ for _, setting := range info.Settings {
36+ switch setting.Key {
37+ case "vcs.revision":
38+ sha = setting.Value
39+ case "vcs.modified":
40+ modified = setting.Value == "true"
41+ }
42+ }
43+44+ if modVer == "" {
45+ modVer = "unknown"
46+ }
47+48+ if sha == "" {
49+ version = modVer
50+ } else if modified {
51+ version = fmt.Sprintf("%s (%s with modifications)", modVer, sha)
52+ } else {
53+ version = fmt.Sprintf("%s (%s)", modVer, sha)
54+ }
55+ }
56+57+ response := tangled.KnotVersion_Output{
58+ Version: version,
59+ }
60+61+ w.Header().Set("Content-Type", "application/json")
62+ if err := json.NewEncoder(w).Encode(response); err != nil {
63+ x.Logger.Error("failed to encode response", "error", err)
64+ writeError(w, xrpcerr.NewXrpcError(
65+ xrpcerr.WithTag("InternalServerError"),
66+ xrpcerr.WithMessage("failed to encode response"),
67+ ), http.StatusInternalServerError)
68+ return
69+ }
70+}
+88
knotserver/xrpc/xrpc.go
···4 "encoding/json"
5 "log/slog"
6 "net/http"
00708 "tangled.sh/tangled.sh/core/api/tangled"
9 "tangled.sh/tangled.sh/core/idresolver"
10 "tangled.sh/tangled.sh/core/jetstream"
···50 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
51 // - use ETags on clients to keep requests to a minimum
52 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
00000000000000000000053 return r
000000000000000000000000000000000000000000000000000000000000000054}
5556func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
···4 "encoding/json"
5 "log/slog"
6 "net/http"
7+ "net/url"
8+ "strings"
910+ securejoin "github.com/cyphar/filepath-securejoin"
11 "tangled.sh/tangled.sh/core/api/tangled"
12 "tangled.sh/tangled.sh/core/idresolver"
13 "tangled.sh/tangled.sh/core/jetstream"
···53 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
54 // - use ETags on clients to keep requests to a minimum
55 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
56+57+ // repo query endpoints (no auth required)
58+ r.Get("/"+tangled.RepoTreeNSID, x.RepoTree)
59+ r.Get("/"+tangled.RepoLogNSID, x.RepoLog)
60+ r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches)
61+ r.Get("/"+tangled.RepoTagsNSID, x.RepoTags)
62+ r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob)
63+ r.Get("/"+tangled.RepoDiffNSID, x.RepoDiff)
64+ r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
65+ r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
66+ r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
67+ r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
68+ r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
69+70+ // knot query endpoints (no auth required)
71+ r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
72+ r.Get("/"+tangled.KnotVersionNSID, x.Version)
73+74+ // service query endpoints (no auth required)
75+ r.Get("/"+tangled.OwnerNSID, x.Owner)
76+77 return r
78+}
79+80+// parseRepoParam parses a repo parameter in 'did/repoName' format and returns
81+// the full repository path on disk
82+func (x *Xrpc) parseRepoParam(repo string) (string, error) {
83+ if repo == "" {
84+ return "", xrpcerr.NewXrpcError(
85+ xrpcerr.WithTag("InvalidRequest"),
86+ xrpcerr.WithMessage("missing repo parameter"),
87+ )
88+ }
89+90+ // Parse repo string (did/repoName format)
91+ parts := strings.Split(repo, "/")
92+ if len(parts) < 2 {
93+ return "", xrpcerr.NewXrpcError(
94+ xrpcerr.WithTag("InvalidRequest"),
95+ xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
96+ )
97+ }
98+99+ did := strings.Join(parts[:len(parts)-1], "/")
100+ repoName := parts[len(parts)-1]
101+102+ // Construct repository path using the same logic as didPath
103+ didRepoPath, err := securejoin.SecureJoin(did, repoName)
104+ if err != nil {
105+ return "", xrpcerr.NewXrpcError(
106+ xrpcerr.WithTag("RepoNotFound"),
107+ xrpcerr.WithMessage("failed to access repository"),
108+ )
109+ }
110+111+ repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
112+ if err != nil {
113+ return "", xrpcerr.NewXrpcError(
114+ xrpcerr.WithTag("RepoNotFound"),
115+ xrpcerr.WithMessage("failed to access repository"),
116+ )
117+ }
118+119+ return repoPath, nil
120+}
121+122+// parseStandardParams parses common query parameters used by most handlers
123+func (x *Xrpc) parseStandardParams(r *http.Request) (repo, repoPath, ref string, err error) {
124+ // Parse repo parameter
125+ repo = r.URL.Query().Get("repo")
126+ repoPath, err = x.parseRepoParam(repo)
127+ if err != nil {
128+ return "", "", "", err
129+ }
130+131+ // Parse and unescape ref parameter
132+ refParam := r.URL.Query().Get("ref")
133+ if refParam == "" {
134+ return "", "", "", xrpcerr.NewXrpcError(
135+ xrpcerr.WithTag("InvalidRequest"),
136+ xrpcerr.WithMessage("missing ref parameter"),
137+ )
138+ }
139+140+ ref, _ = url.QueryUnescape(refParam)
141+ return repo, repoPath, ref, nil
142}
143144func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
···1+# Privacy Policy
2+3+**Last updated:** January 15, 2025
4+5+This Privacy Policy describes how Tangled ("we," "us," or "our")
6+collects, uses, and shares your personal information when you use our
7+platform and services (the "Service").
8+9+## 1. Information We Collect
10+11+### Account Information
12+13+When you create an account, we collect:
14+15+- Your chosen username
16+- Email address
17+- Profile information you choose to provide
18+- Authentication data
19+20+### Content and Activity
21+22+We store:
23+24+- Code repositories and associated metadata
25+- Issues, pull requests, and comments
26+- Activity logs and usage patterns
27+- Public keys for authentication
28+29+## 2. Data Location and Hosting
30+31+### EU Data Hosting
32+33+**All Tangled service data is hosted within the European Union.**
34+Specifically:
35+36+- **Personal Data Servers (PDS):** Accounts hosted on Tangled PDS
37+ (*.tngl.sh) are located in Finland
38+- **Application Data:** All other service data is stored on EU-based
39+ servers
40+- **Data Processing:** All data processing occurs within EU
41+ jurisdiction
42+43+### External PDS Notice
44+45+**Important:** If your account is hosted on Bluesky's PDS or other
46+self-hosted Personal Data Servers (not *.tngl.sh), we do not control
47+that data. The data protection, storage location, and privacy
48+practices for such accounts are governed by the respective PDS
49+provider's policies, not this Privacy Policy. We only control data
50+processing within our own services and infrastructure.
51+52+## 3. Third-Party Data Processors
53+54+We only share your data with the following third-party processors:
55+56+### Resend (Email Services)
57+58+- **Purpose:** Sending transactional emails (account verification,
59+ notifications)
60+- **Data Shared:** Email address and necessary message content
61+62+### Cloudflare (Image Caching)
63+64+- **Purpose:** Caching and optimizing image delivery
65+- **Data Shared:** Public images and associated metadata for caching
66+ purposes
67+68+### Posthog (Usage Metrics Tracking)
69+70+- **Purpose:** Tracking usage and platform metrics
71+- **Data Shared:** Anonymous usage data, IP addresses, DIDs, and browser
72+ information
73+74+## 4. How We Use Your Information
75+76+We use your information to:
77+78+- Provide and maintain the Service
79+- Process your transactions and requests
80+- Send you technical notices and support messages
81+- Improve and develop new features
82+- Ensure security and prevent fraud
83+- Comply with legal obligations
84+85+## 5. Data Sharing and Disclosure
86+87+We do not sell, trade, or rent your personal information. We may share
88+your information only in the following circumstances:
89+90+- With the third-party processors listed above
91+- When required by law or legal process
92+- To protect our rights, property, or safety, or that of our users
93+- In connection with a merger, acquisition, or sale of assets (with
94+ appropriate protections)
95+96+## 6. Data Security
97+98+We implement appropriate technical and organizational measures to
99+protect your personal information against unauthorized access,
100+alteration, disclosure, or destruction. However, no method of
101+transmission over the Internet is 100% secure.
102+103+## 7. Data Retention
104+105+We retain your personal information for as long as necessary to provide
106+the Service and fulfill the purposes outlined in this Privacy Policy,
107+unless a longer retention period is required by law.
108+109+## 8. Your Rights
110+111+Under applicable data protection laws, you have the right to:
112+113+- Access your personal information
114+- Correct inaccurate information
115+- Request deletion of your information
116+- Object to processing of your information
117+- Data portability
118+- Withdraw consent (where applicable)
119+120+## 9. Cookies and Tracking
121+122+We use cookies and similar technologies to:
123+124+- Maintain your login session
125+- Remember your preferences
126+- Analyze usage patterns to improve the Service
127+128+You can control cookie settings through your browser preferences.
129+130+## 10. Children's Privacy
131+132+The Service is not intended for children under 16 years of age. We do
133+not knowingly collect personal information from children under 16. If
134+we become aware that we have collected such information, we will take
135+steps to delete it.
136+137+## 11. International Data Transfers
138+139+While all our primary data processing occurs within the EU, some of our
140+third-party processors may process data outside the EU. When this
141+occurs, we ensure appropriate safeguards are in place, such as Standard
142+Contractual Clauses or adequacy decisions.
143+144+## 12. Changes to This Privacy Policy
145+146+We may update this Privacy Policy from time to time. We will notify you
147+of any changes by posting the new Privacy Policy on this page and
148+updating the "Last updated" date.
149+150+## 13. Contact Information
151+152+If you have any questions about this Privacy Policy or wish to exercise
153+your rights, please contact us through our platform or via email.
154+155+---
156+157+This Privacy Policy complies with the EU General Data Protection
158+Regulation (GDPR) and other applicable data protection laws.
···1+# Terms of Service
2+3+**Last updated:** January 15, 2025
4+5+Welcome to Tangled. These Terms of Service ("Terms") govern your access
6+to and use of the Tangled platform and services (the "Service")
7+operated by us ("Tangled," "we," "us," or "our").
8+9+## 1. Acceptance of Terms
10+11+By accessing or using our Service, you agree to be bound by these Terms.
12+If you disagree with any part of these terms, then you may not access
13+the Service.
14+15+## 2. Account Registration
16+17+To use certain features of the Service, you must register for an
18+account. You agree to provide accurate, current, and complete
19+information during the registration process and to update such
20+information to keep it accurate, current, and complete.
21+22+## 3. Account Termination
23+24+> **Important Notice**
25+>
26+> **We reserve the right to terminate, suspend, or restrict access to
27+> your account at any time, for any reason, or for no reason at all, at
28+> our sole discretion.** This includes, but is not limited to,
29+> termination for violation of these Terms, inappropriate conduct, spam,
30+> abuse, or any other behavior we deem harmful to the Service or other
31+> users.
32+>
33+> Account termination may result in the loss of access to your
34+> repositories, data, and other content associated with your account. We
35+> are not obligated to provide advance notice of termination, though we
36+> may do so in our discretion.
37+38+## 4. Acceptable Use
39+40+You agree not to use the Service to:
41+42+- Violate any applicable laws or regulations
43+- Infringe upon the rights of others
44+- Upload, store, or share content that is illegal, harmful, threatening,
45+ abusive, harassing, defamatory, vulgar, obscene, or otherwise
46+ objectionable
47+- Engage in spam, phishing, or other deceptive practices
48+- Attempt to gain unauthorized access to the Service or other users'
49+ accounts
50+- Interfere with or disrupt the Service or servers connected to the
51+ Service
52+53+## 5. Content and Intellectual Property
54+55+You retain ownership of the content you upload to the Service. By
56+uploading content, you grant us a non-exclusive, worldwide, royalty-free
57+license to use, reproduce, modify, and distribute your content as
58+necessary to provide the Service.
59+60+## 6. Privacy
61+62+Your privacy is important to us. Please review our [Privacy
63+Policy](/privacy), which also governs your use of the Service.
64+65+## 7. Disclaimers
66+67+The Service is provided on an "AS IS" and "AS AVAILABLE" basis. We make
68+no warranties, expressed or implied, and hereby disclaim and negate all
69+other warranties including without limitation, implied warranties or
70+conditions of merchantability, fitness for a particular purpose, or
71+non-infringement of intellectual property or other violation of rights.
72+73+## 8. Limitation of Liability
74+75+In no event shall Tangled, nor its directors, employees, partners,
76+agents, suppliers, or affiliates, be liable for any indirect,
77+incidental, special, consequential, or punitive damages, including
78+without limitation, loss of profits, data, use, goodwill, or other
79+intangible losses, resulting from your use of the Service.
80+81+## 9. Indemnification
82+83+You agree to defend, indemnify, and hold harmless Tangled and its
84+affiliates, officers, directors, employees, and agents from and against
85+any and all claims, damages, obligations, losses, liabilities, costs,
86+or debt, and expenses (including attorney's fees).
87+88+## 10. Governing Law
89+90+These Terms shall be interpreted and governed by the laws of Finland,
91+without regard to its conflict of law provisions.
92+93+## 11. Changes to Terms
94+95+We reserve the right to modify or replace these Terms at any time. If a
96+revision is material, we will try to provide at least 30 days notice
97+prior to any new terms taking effect.
98+99+## 12. Contact Information
100+101+If you have any questions about these Terms of Service, please contact
102+us through our platform or via email.
103+104+---
105+106+These terms are effective as of the last updated date shown above and
107+will remain in effect except with respect to any changes in their
108+provisions in the future, which will be in effect immediately after
109+being posted on this page.
···119 // we have f1 and f2, combine them
120 combined, err := combineFiles(f1, f2)
121 if err != nil {
122- fmt.Println(err)
123 }
124125 // combined can be nil commit 2 reverted all changes from commit 1
···119 // we have f1 and f2, combine them
120 combined, err := combineFiles(f1, f2)
121 if err != nil {
122+ // fmt.Println(err)
123 }
124125 // combined can be nil commit 2 reverted all changes from commit 1
···17 Owner string `env:"OWNER, required"`
18 Secrets Secrets `env:",prefix=SECRETS_"`
19 LogDir string `env:"LOG_DIR, default=/var/log/spindle"`
20+ QueueSize int `env:"QUEUE_SIZE, default=100"`
21+ MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time
22}
2324func (s Server) Did() syntax.DID {