···165165 </div>
166166 <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
167167 A knot hosts repository data and handles Git operations.
168168- You can also <a href="/knots" class="underline">register your own knot</a>.
168168+ You can also <a href="/settings/knots" class="underline">register your own knot</a>.
169169 </p>
170170 </div>
171171{{ end }}
···6677 "tangled.org/core/appview/db"
88 "tangled.org/core/appview/models"
99+ "tangled.org/core/orm"
910)
10111112func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
1213 // if comments have parents, only ingest ones that are 1 level deep
1314 if comment.ReplyTo != nil {
1414- parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
1515+ parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
1516 if err != nil {
1617 return fmt.Errorf("failed to fetch parent comment: %w", err)
1718 }
+1-34
crypto/verify.go
···55 "crypto/sha256"
66 "encoding/base64"
77 "fmt"
88- "strings"
98109 "github.com/hiddeco/sshsig"
1110 "golang.org/x/crypto/ssh"
1212- "tangled.org/core/types"
1311)
14121513func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···2826 // multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
2927 // to sha-512 for all key types anyway.
3028 err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
3131- return err, err == nil
3232-}
33293434-// VerifyCommitSignature reconstructs the payload used to sign a commit. This is
3535-// essentially the git cat-file output but without the gpgsig header.
3636-//
3737-// Caveats: signature verification will fail on commits with more than one parent,
3838-// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field
3939-// and we are unable to reconstruct the payload correctly.
4040-//
4141-// Ideally this should directly operate on an *object.Commit.
4242-func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) {
4343- signature := commit.Commit.PGPSignature
4444-4545- author := bytes.NewBuffer([]byte{})
4646- committer := bytes.NewBuffer([]byte{})
4747- commit.Commit.Author.Encode(author)
4848- commit.Commit.Committer.Encode(committer)
4949-5050- payload := strings.Builder{}
5151-5252- fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree)
5353- if commit.Commit.Parent != "" {
5454- fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent)
5555- }
5656- fmt.Fprintf(&payload, "author %s\n", author.String())
5757- fmt.Fprintf(&payload, "committer %s\n", committer.String())
5858- if commit.Commit.ChangedId != "" {
5959- fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId)
6060- }
6161- fmt.Fprintf(&payload, "\n%s", commit.Commit.Message)
6262-6363- return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String()))
3030+ return err, err == nil
6431}
65326633// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+3-3
docs/hacking.md
···117117# type `poweroff` at the shell to exit the VM
118118```
119119120120-This starts a knot on port 6000, a spindle on port 6555
120120+This starts a knot on port 6444, a spindle on port 6555
121121with `ssh` exposed on port 2222.
122122123123Once the services are running, head to
124124-http://localhost:3000/knots and hit verify. It should
124124+http://localhost:3000/settings/knots and hit verify. It should
125125verify the ownership of the services instantly if everything
126126went smoothly.
127127···146146### running a spindle
147147148148The above VM should already be running a spindle on
149149-`localhost:6555`. Head to http://localhost:3000/spindles and
149149+`localhost:6555`. Head to http://localhost:3000/settings/spindles and
150150hit verify. You can then configure each repository to use
151151this spindle and run CI jobs.
152152
+1-1
docs/knot-hosting.md
···131131132132You should now have a running knot server! You can finalize
133133your registration by hitting the `verify` button on the
134134-[/knots](https://tangled.org/knots) page. This simply creates
134134+[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
135135a record on your PDS to announce the existence of the knot.
136136137137### custom paths
+3-3
docs/migrations.md
···1414For knots:
15151616- Upgrade to latest tag (v1.9.0 or above)
1717-- Head to the [knot dashboard](https://tangled.org/knots) and
1717+- Head to the [knot dashboard](https://tangled.org/settings/knots) and
1818 hit the "retry" button to verify your knot
19192020For spindles:
21212222- Upgrade to latest tag (v1.9.0 or above)
2323- Head to the [spindle
2424- dashboard](https://tangled.org/spindles) and hit the
2424+ dashboard](https://tangled.org/settings/spindles) and hit the
2525 "retry" button to verify your spindle
26262727## Upgrading from v1.7.x
···4141 [settings](https://tangled.org/settings) page.
4242- Restart your knot once you have replaced the environment
4343 variable
4444-- Head to the [knot dashboard](https://tangled.org/knots) and
4444+- Head to the [knot dashboard](https://tangled.org/settings/knots) and
4545 hit the "retry" button to verify your knot. This simply
4646 writes a `sh.tangled.knot` record to your PDS.
4747
···7272 // existing instances of the closure when j.WantedDids is mutated
7373 return func(ctx context.Context, evt *models.Event) error {
74747575+ j.mu.RLock()
7576 // empty filter => all dids allowed
7676- if len(j.wantedDids) == 0 {
7777- return processFunc(ctx, evt)
7777+ matches := len(j.wantedDids) == 0
7878+ if !matches {
7979+ if _, ok := j.wantedDids[evt.Did]; ok {
8080+ matches = true
8181+ }
7882 }
8383+ j.mu.RUnlock()
79848080- if _, ok := j.wantedDids[evt.Did]; ok {
8585+ if matches {
8186 return processFunc(ctx, evt)
8287 } else {
8388 return nil
···122127123128 go func() {
124129 if j.waitForDid {
125125- for len(j.wantedDids) == 0 {
130130+ for {
131131+ j.mu.RLock()
132132+ hasDid := len(j.wantedDids) != 0
133133+ j.mu.RUnlock()
134134+ if hasDid {
135135+ break
136136+ }
126137 time.Sleep(time.Second)
127138 }
128139 }
+81
knotserver/db/db.go
···11+package db
22+33+import (
44+ "context"
55+ "database/sql"
66+ "log/slog"
77+ "strings"
88+99+ _ "github.com/mattn/go-sqlite3"
1010+ "tangled.org/core/log"
1111+)
1212+1313+type DB struct {
1414+ db *sql.DB
1515+ logger *slog.Logger
1616+}
1717+1818+func Setup(ctx context.Context, dbPath string) (*DB, error) {
1919+ // https://github.com/mattn/go-sqlite3#connection-string
2020+ opts := []string{
2121+ "_foreign_keys=1",
2222+ "_journal_mode=WAL",
2323+ "_synchronous=NORMAL",
2424+ "_auto_vacuum=incremental",
2525+ }
2626+2727+ logger := log.FromContext(ctx)
2828+ logger = log.SubLogger(logger, "db")
2929+3030+ db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
3131+ if err != nil {
3232+ return nil, err
3333+ }
3434+3535+ conn, err := db.Conn(ctx)
3636+ if err != nil {
3737+ return nil, err
3838+ }
3939+ defer conn.Close()
4040+4141+ _, err = conn.ExecContext(ctx, `
4242+ create table if not exists known_dids (
4343+ did text primary key
4444+ );
4545+4646+ create table if not exists public_keys (
4747+ id integer primary key autoincrement,
4848+ did text not null,
4949+ key text not null,
5050+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
5151+ unique(did, key),
5252+ foreign key (did) references known_dids(did) on delete cascade
5353+ );
5454+5555+ create table if not exists _jetstream (
5656+ id integer primary key autoincrement,
5757+ last_time_us integer not null
5858+ );
5959+6060+ create table if not exists events (
6161+ rkey text not null,
6262+ nsid text not null,
6363+ event text not null, -- json
6464+ created integer not null default (strftime('%s', 'now')),
6565+ primary key (rkey, nsid)
6666+ );
6767+6868+ create table if not exists migrations (
6969+ id integer primary key autoincrement,
7070+ name text unique
7171+ );
7272+ `)
7373+ if err != nil {
7474+ return nil, err
7575+ }
7676+7777+ return &DB{
7878+ db: db,
7979+ logger: logger,
8080+ }, nil
8181+}
-64
knotserver/db/init.go
···11-package db
22-33-import (
44- "database/sql"
55- "strings"
66-77- _ "github.com/mattn/go-sqlite3"
88-)
99-1010-type DB struct {
1111- db *sql.DB
1212-}
1313-1414-func Setup(dbPath string) (*DB, error) {
1515- // https://github.com/mattn/go-sqlite3#connection-string
1616- opts := []string{
1717- "_foreign_keys=1",
1818- "_journal_mode=WAL",
1919- "_synchronous=NORMAL",
2020- "_auto_vacuum=incremental",
2121- }
2222-2323- db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
2424- if err != nil {
2525- return nil, err
2626- }
2727-2828- // NOTE: If any other migration is added here, you MUST
2929- // copy the pattern in appview: use a single sql.Conn
3030- // for every migration.
3131-3232- _, err = db.Exec(`
3333- create table if not exists known_dids (
3434- did text primary key
3535- );
3636-3737- create table if not exists public_keys (
3838- id integer primary key autoincrement,
3939- did text not null,
4040- key text not null,
4141- created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
4242- unique(did, key),
4343- foreign key (did) references known_dids(did) on delete cascade
4444- );
4545-4646- create table if not exists _jetstream (
4747- id integer primary key autoincrement,
4848- last_time_us integer not null
4949- );
5050-5151- create table if not exists events (
5252- rkey text not null,
5353- nsid text not null,
5454- event text not null, -- json
5555- created integer not null default (strftime('%s', 'now')),
5656- primary key (rkey, nsid)
5757- );
5858- `)
5959- if err != nil {
6060- return nil, err
6161- }
6262-6363- return &DB{db: db}, nil
6464-}
···2222)
23232424type Workflow struct {
2525- Steps []Step
2626- Name string
2727- Data any
2525+ Steps []Step
2626+ Name string
2727+ Data any
2828+ Environment map[string]string
2829}
+77
spindle/models/pipeline_env.go
···11+package models
22+33+import (
44+ "strings"
55+66+ "github.com/go-git/go-git/v5/plumbing"
77+ "tangled.org/core/api/tangled"
88+ "tangled.org/core/workflow"
99+)
1010+1111+// PipelineEnvVars extracts environment variables from pipeline trigger metadata.
1212+// These are framework-provided variables that are injected into workflow steps.
1313+func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId, devMode bool) map[string]string {
1414+ if tr == nil {
1515+ return nil
1616+ }
1717+1818+ env := make(map[string]string)
1919+2020+ // Standard CI environment variable
2121+ env["CI"] = "true"
2222+2323+ env["TANGLED_PIPELINE_ID"] = pipelineId.AtUri().String()
2424+2525+ // Repo info
2626+ if tr.Repo != nil {
2727+ env["TANGLED_REPO_KNOT"] = tr.Repo.Knot
2828+ env["TANGLED_REPO_DID"] = tr.Repo.Did
2929+ env["TANGLED_REPO_NAME"] = tr.Repo.Repo
3030+ env["TANGLED_REPO_DEFAULT_BRANCH"] = tr.Repo.DefaultBranch
3131+ env["TANGLED_REPO_URL"] = BuildRepoURL(tr.Repo, devMode)
3232+ }
3333+3434+ switch workflow.TriggerKind(tr.Kind) {
3535+ case workflow.TriggerKindPush:
3636+ if tr.Push != nil {
3737+ refName := plumbing.ReferenceName(tr.Push.Ref)
3838+ refType := "branch"
3939+ if refName.IsTag() {
4040+ refType = "tag"
4141+ }
4242+4343+ env["TANGLED_REF"] = tr.Push.Ref
4444+ env["TANGLED_REF_NAME"] = refName.Short()
4545+ env["TANGLED_REF_TYPE"] = refType
4646+ env["TANGLED_SHA"] = tr.Push.NewSha
4747+ env["TANGLED_COMMIT_SHA"] = tr.Push.NewSha
4848+ }
4949+5050+ case workflow.TriggerKindPullRequest:
5151+ if tr.PullRequest != nil {
5252+ // For PRs, the "ref" is the source branch
5353+ env["TANGLED_REF"] = "refs/heads/" + tr.PullRequest.SourceBranch
5454+ env["TANGLED_REF_NAME"] = tr.PullRequest.SourceBranch
5555+ env["TANGLED_REF_TYPE"] = "branch"
5656+ env["TANGLED_SHA"] = tr.PullRequest.SourceSha
5757+ env["TANGLED_COMMIT_SHA"] = tr.PullRequest.SourceSha
5858+5959+ // PR-specific variables
6060+ env["TANGLED_PR_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch
6161+ env["TANGLED_PR_TARGET_BRANCH"] = tr.PullRequest.TargetBranch
6262+ env["TANGLED_PR_SOURCE_SHA"] = tr.PullRequest.SourceSha
6363+ env["TANGLED_PR_ACTION"] = tr.PullRequest.Action
6464+ }
6565+6666+ case workflow.TriggerKindManual:
6767+ // Manual triggers may not have ref/sha info
6868+ // Include any manual inputs if present
6969+ if tr.Manual != nil {
7070+ for _, pair := range tr.Manual.Inputs {
7171+ env["TANGLED_INPUT_"+strings.ToUpper(pair.Key)] = pair.Value
7272+ }
7373+ }
7474+ }
7575+7676+ return env
7777+}