diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go
index 0335dbc8..36312457 100644
--- a/api/tangled/cbor_gen.go
+++ b/api/tangled/cbor_gen.go
@@ -669,7 +669,7 @@ func (t *CiPipeline) MarshalCBOR(w io.Writer) error {
}
cw := cbg.NewCborWriter(w)
- fieldCount := 6
+ fieldCount := 7
if t.CreatedAt == nil {
fieldCount--
@@ -679,6 +679,10 @@ func (t *CiPipeline) MarshalCBOR(w io.Writer) error {
fieldCount--
}
+ if t.SourceRepo == nil {
+ fieldCount--
+ }
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
}
@@ -834,6 +838,38 @@ func (t *CiPipeline) MarshalCBOR(w io.Writer) error {
}
}
+
+ // t.SourceRepo (string) (string)
+ if t.SourceRepo != nil {
+
+ if len("sourceRepo") > 1000000 {
+ return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sourceRepo")); err != nil {
+ return err
+ }
+
+ if t.SourceRepo == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.SourceRepo) > 1000000 {
+ return xerrors.Errorf("Value in field t.SourceRepo was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
+ return err
+ }
+ }
+ }
return nil
}
@@ -862,7 +898,7 @@ func (t *CiPipeline) UnmarshalCBOR(r io.Reader) (err error) {
n := extra
- nameBuf := make([]byte, 9)
+ nameBuf := make([]byte, 10)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
if err != nil {
@@ -1011,6 +1047,27 @@ func (t *CiPipeline) UnmarshalCBOR(r io.Reader) (err error) {
}
}
+ // t.SourceRepo (string) (string)
+ case "sourceRepo":
+
+ {
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.SourceRepo = (*string)(&sval)
+ }
+ }
default:
// Field doesn't exist on this type, so ignore it
@@ -2174,7 +2231,334 @@ func (t *CiSubscribePipelineLogs_Data) UnmarshalCBOR(r io.Reader) (err error) {
return nil
}
-func (t *CiTrigger_Manual) MarshalCBOR(w io.Writer) error {
+func (t *CiTrigger_Manual) MarshalCBOR(w io.Writer) error {
+ if t == nil {
+ _, err := w.Write(cbg.CborNull)
+ return err
+ }
+
+ cw := cbg.NewCborWriter(w)
+ fieldCount := 5
+
+ if t.Inputs == nil {
+ fieldCount--
+ }
+
+ if t.Ref == nil {
+ fieldCount--
+ }
+
+ if t.SourceRepo == nil {
+ fieldCount--
+ }
+
+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
+ return err
+ }
+
+ // t.Ref (string) (string)
+ if t.Ref != nil {
+
+ if len("ref") > 1000000 {
+ return xerrors.Errorf("Value in field \"ref\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("ref"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("ref")); err != nil {
+ return err
+ }
+
+ if t.Ref == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.Ref) > 1000000 {
+ return xerrors.Errorf("Value in field t.Ref was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Ref))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.Ref)); err != nil {
+ return err
+ }
+ }
+ }
+
+ // t.Sha (string) (string)
+ if len("sha") > 1000000 {
+ return xerrors.Errorf("Value in field \"sha\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sha"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sha")); err != nil {
+ return err
+ }
+
+ if len(t.Sha) > 1000000 {
+ return xerrors.Errorf("Value in field t.Sha was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Sha))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(t.Sha)); err != nil {
+ return err
+ }
+
+ // t.LexiconTypeID (string) (string)
+ if len("$type") > 1000000 {
+ return xerrors.Errorf("Value in field \"$type\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("$type")); err != nil {
+ return err
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.ci.trigger#manual"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sh.tangled.ci.trigger#manual")); err != nil {
+ return err
+ }
+
+ // t.Inputs ([]*tangled.CiTrigger_Pair) (slice)
+ if t.Inputs != nil {
+
+ if len("inputs") > 1000000 {
+ return xerrors.Errorf("Value in field \"inputs\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("inputs")); err != nil {
+ return err
+ }
+
+ if len(t.Inputs) > 8192 {
+ return xerrors.Errorf("Slice value in field t.Inputs was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil {
+ return err
+ }
+ for _, v := range t.Inputs {
+ if err := v.MarshalCBOR(cw); err != nil {
+ return err
+ }
+
+ }
+ }
+
+ // t.SourceRepo (string) (string)
+ if t.SourceRepo != nil {
+
+ if len("sourceRepo") > 1000000 {
+ return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sourceRepo")); err != nil {
+ return err
+ }
+
+ if t.SourceRepo == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.SourceRepo) > 1000000 {
+ return xerrors.Errorf("Value in field t.SourceRepo was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (t *CiTrigger_Manual) UnmarshalCBOR(r io.Reader) (err error) {
+ *t = CiTrigger_Manual{}
+
+ cr := cbg.NewCborReader(r)
+
+ maj, extra, err := cr.ReadHeader()
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ }()
+
+ if maj != cbg.MajMap {
+ return fmt.Errorf("cbor input should be of type map")
+ }
+
+ if extra > cbg.MaxLength {
+ return fmt.Errorf("CiTrigger_Manual: map struct too large (%d)", extra)
+ }
+
+ n := extra
+
+ nameBuf := make([]byte, 10)
+ for i := uint64(0); i < n; i++ {
+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+ if err != nil {
+ return err
+ }
+
+ if !ok {
+ // Field doesn't exist on this type, so ignore it
+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+ return err
+ }
+ continue
+ }
+
+ switch string(nameBuf[:nameLen]) {
+ // t.Ref (string) (string)
+ case "ref":
+
+ {
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.Ref = (*string)(&sval)
+ }
+ }
+ // t.Sha (string) (string)
+ case "sha":
+
+ {
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.Sha = string(sval)
+ }
+ // t.LexiconTypeID (string) (string)
+ case "$type":
+
+ {
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.LexiconTypeID = string(sval)
+ }
+ // t.Inputs ([]*tangled.CiTrigger_Pair) (slice)
+ case "inputs":
+
+ maj, extra, err = cr.ReadHeader()
+ if err != nil {
+ return err
+ }
+
+ if extra > 8192 {
+ return fmt.Errorf("t.Inputs: array too large (%d)", extra)
+ }
+
+ if maj != cbg.MajArray {
+ return fmt.Errorf("expected cbor array")
+ }
+
+ if extra > 0 {
+ t.Inputs = make([]*CiTrigger_Pair, extra)
+ }
+
+ for i := 0; i < int(extra); i++ {
+ {
+ var maj byte
+ var extra uint64
+ var err error
+ _ = maj
+ _ = extra
+ _ = err
+
+ {
+
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+ t.Inputs[i] = new(CiTrigger_Pair)
+ if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil {
+ return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err)
+ }
+ }
+
+ }
+
+ }
+ }
+ // t.SourceRepo (string) (string)
+ case "sourceRepo":
+
+ {
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.SourceRepo = (*string)(&sval)
+ }
+ }
+
+ default:
+ // Field doesn't exist on this type, so ignore it
+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+func (t *CiTrigger_Pair) MarshalCBOR(w io.Writer) error {
if t == nil {
_, err := w.Write(cbg.CborNull)
return err
@@ -2182,33 +2566,60 @@ func (t *CiTrigger_Manual) MarshalCBOR(w io.Writer) error {
cw := cbg.NewCborWriter(w)
- if _, err := cw.Write([]byte{161}); err != nil {
+ if _, err := cw.Write([]byte{162}); err != nil {
return err
}
- // t.LexiconTypeID (string) (string)
- if len("$type") > 1000000 {
- return xerrors.Errorf("Value in field \"$type\" was too long")
+ // t.Key (string) (string)
+ if len("key") > 1000000 {
+ return xerrors.Errorf("Value in field \"key\" was too long")
}
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil {
return err
}
- if _, err := cw.WriteString(string("$type")); err != nil {
+ if _, err := cw.WriteString(string("key")); err != nil {
return err
}
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.ci.trigger#manual"))); err != nil {
+ if len(t.Key) > 1000000 {
+ return xerrors.Errorf("Value in field t.Key was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil {
return err
}
- if _, err := cw.WriteString(string("sh.tangled.ci.trigger#manual")); err != nil {
+ if _, err := cw.WriteString(string(t.Key)); err != nil {
+ return err
+ }
+
+ // t.Value (string) (string)
+ if len("value") > 1000000 {
+ return xerrors.Errorf("Value in field \"value\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("value"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("value")); err != nil {
+ return err
+ }
+
+ if len(t.Value) > 1000000 {
+ return xerrors.Errorf("Value in field t.Value was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Value))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(t.Value)); err != nil {
return err
}
return nil
}
-func (t *CiTrigger_Manual) UnmarshalCBOR(r io.Reader) (err error) {
- *t = CiTrigger_Manual{}
+func (t *CiTrigger_Pair) UnmarshalCBOR(r io.Reader) (err error) {
+ *t = CiTrigger_Pair{}
cr := cbg.NewCborReader(r)
@@ -2227,7 +2638,7 @@ func (t *CiTrigger_Manual) UnmarshalCBOR(r io.Reader) (err error) {
}
if extra > cbg.MaxLength {
- return fmt.Errorf("CiTrigger_Manual: map struct too large (%d)", extra)
+ return fmt.Errorf("CiTrigger_Pair: map struct too large (%d)", extra)
}
n := extra
@@ -2248,8 +2659,8 @@ func (t *CiTrigger_Manual) UnmarshalCBOR(r io.Reader) (err error) {
}
switch string(nameBuf[:nameLen]) {
- // t.LexiconTypeID (string) (string)
- case "$type":
+ // t.Key (string) (string)
+ case "key":
{
sval, err := cbg.ReadStringWithMax(cr, 1000000)
@@ -2257,7 +2668,18 @@ func (t *CiTrigger_Manual) UnmarshalCBOR(r io.Reader) (err error) {
return err
}
- t.LexiconTypeID = string(sval)
+ t.Key = string(sval)
+ }
+ // t.Value (string) (string)
+ case "value":
+
+ {
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.Value = string(sval)
}
default:
@@ -2277,16 +2699,56 @@ func (t *CiTrigger_PullRequest) MarshalCBOR(w io.Writer) error {
}
cw := cbg.NewCborWriter(w)
- fieldCount := 5
+ fieldCount := 6
+
+ if t.Pull == nil {
+ fieldCount--
+ }
if t.SourceBranch == nil {
fieldCount--
}
+ if t.SourceRepo == nil {
+ fieldCount--
+ }
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
}
+ // t.Pull (string) (string)
+ if t.Pull != nil {
+
+ if len("pull") > 1000000 {
+ return xerrors.Errorf("Value in field \"pull\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("pull")); err != nil {
+ return err
+ }
+
+ if t.Pull == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.Pull) > 1000000 {
+ return xerrors.Errorf("Value in field t.Pull was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Pull))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.Pull)); err != nil {
+ return err
+ }
+ }
+ }
+
// t.LexiconTypeID (string) (string)
if len("$type") > 1000000 {
return xerrors.Errorf("Value in field \"$type\" was too long")
@@ -2306,29 +2768,6 @@ func (t *CiTrigger_PullRequest) MarshalCBOR(w io.Writer) error {
return err
}
- // t.Action (string) (string)
- if len("action") > 1000000 {
- return xerrors.Errorf("Value in field \"action\" was too long")
- }
-
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("action"))); err != nil {
- return err
- }
- if _, err := cw.WriteString(string("action")); err != nil {
- return err
- }
-
- if len(t.Action) > 1000000 {
- return xerrors.Errorf("Value in field t.Action was too long")
- }
-
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Action))); err != nil {
- return err
- }
- if _, err := cw.WriteString(string(t.Action)); err != nil {
- return err
- }
-
// t.SourceSha (string) (string)
if len("sourceSha") > 1000000 {
return xerrors.Errorf("Value in field \"sourceSha\" was too long")
@@ -2352,6 +2791,38 @@ func (t *CiTrigger_PullRequest) MarshalCBOR(w io.Writer) error {
return err
}
+ // t.SourceRepo (string) (string)
+ if t.SourceRepo != nil {
+
+ if len("sourceRepo") > 1000000 {
+ return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sourceRepo")); err != nil {
+ return err
+ }
+
+ if t.SourceRepo == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.SourceRepo) > 1000000 {
+ return xerrors.Errorf("Value in field t.SourceRepo was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
+ return err
+ }
+ }
+ }
+
// t.SourceBranch (string) (string)
if t.SourceBranch != nil {
@@ -2450,19 +2921,29 @@ func (t *CiTrigger_PullRequest) UnmarshalCBOR(r io.Reader) (err error) {
}
switch string(nameBuf[:nameLen]) {
- // t.LexiconTypeID (string) (string)
- case "$type":
+ // t.Pull (string) (string)
+ case "pull":
{
- sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ b, err := cr.ReadByte()
if err != nil {
return err
}
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
- t.LexiconTypeID = string(sval)
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.Pull = (*string)(&sval)
+ }
}
- // t.Action (string) (string)
- case "action":
+ // t.LexiconTypeID (string) (string)
+ case "$type":
{
sval, err := cbg.ReadStringWithMax(cr, 1000000)
@@ -2470,7 +2951,7 @@ func (t *CiTrigger_PullRequest) UnmarshalCBOR(r io.Reader) (err error) {
return err
}
- t.Action = string(sval)
+ t.LexiconTypeID = string(sval)
}
// t.SourceSha (string) (string)
case "sourceSha":
@@ -2483,6 +2964,27 @@ func (t *CiTrigger_PullRequest) UnmarshalCBOR(r io.Reader) (err error) {
t.SourceSha = string(sval)
}
+ // t.SourceRepo (string) (string)
+ case "sourceRepo":
+
+ {
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.SourceRepo = (*string)(&sval)
+ }
+ }
// t.SourceBranch (string) (string)
case "sourceBranch":
@@ -7885,32 +8387,46 @@ func (t *Pipeline_PullRequestTriggerData) MarshalCBOR(w io.Writer) error {
}
cw := cbg.NewCborWriter(w)
+ fieldCount := 4
- if _, err := cw.Write([]byte{164}); err != nil {
- return err
- }
-
- // t.Action (string) (string)
- if len("action") > 1000000 {
- return xerrors.Errorf("Value in field \"action\" was too long")
+ if t.Pull == nil {
+ fieldCount--
}
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("action"))); err != nil {
- return err
- }
- if _, err := cw.WriteString(string("action")); err != nil {
+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
}
- if len(t.Action) > 1000000 {
- return xerrors.Errorf("Value in field t.Action was too long")
- }
+ // t.Pull (string) (string)
+ if t.Pull != nil {
- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Action))); err != nil {
- return err
- }
- if _, err := cw.WriteString(string(t.Action)); err != nil {
- return err
+ if len("pull") > 1000000 {
+ return xerrors.Errorf("Value in field \"pull\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("pull")); err != nil {
+ return err
+ }
+
+ if t.Pull == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.Pull) > 1000000 {
+ return xerrors.Errorf("Value in field t.Pull was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Pull))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.Pull)); err != nil {
+ return err
+ }
+ }
}
// t.SourceSha (string) (string)
@@ -8025,16 +8541,26 @@ func (t *Pipeline_PullRequestTriggerData) UnmarshalCBOR(r io.Reader) (err error)
}
switch string(nameBuf[:nameLen]) {
- // t.Action (string) (string)
- case "action":
+ // t.Pull (string) (string)
+ case "pull":
{
- sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ b, err := cr.ReadByte()
if err != nil {
return err
}
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
- t.Action = string(sval)
+ t.Pull = (*string)(&sval)
+ }
}
// t.SourceSha (string) (string)
case "sourceSha":
@@ -8617,7 +9143,7 @@ func (t *Pipeline_TriggerMetadata) MarshalCBOR(w io.Writer) error {
}
cw := cbg.NewCborWriter(w)
- fieldCount := 5
+ fieldCount := 6
if t.Manual == nil {
fieldCount--
@@ -8631,6 +9157,10 @@ func (t *Pipeline_TriggerMetadata) MarshalCBOR(w io.Writer) error {
fieldCount--
}
+ if t.SourceRepo == nil {
+ fieldCount--
+ }
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
return err
}
@@ -8712,6 +9242,38 @@ func (t *Pipeline_TriggerMetadata) MarshalCBOR(w io.Writer) error {
}
}
+ // t.SourceRepo (string) (string)
+ if t.SourceRepo != nil {
+
+ if len("sourceRepo") > 1000000 {
+ return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string("sourceRepo")); err != nil {
+ return err
+ }
+
+ if t.SourceRepo == nil {
+ if _, err := cw.Write(cbg.CborNull); err != nil {
+ return err
+ }
+ } else {
+ if len(*t.SourceRepo) > 1000000 {
+ return xerrors.Errorf("Value in field t.SourceRepo was too long")
+ }
+
+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
+ return err
+ }
+ if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
+ return err
+ }
+ }
+ }
+
// t.PullRequest (tangled.Pipeline_PullRequestTriggerData) (struct)
if t.PullRequest != nil {
@@ -8845,6 +9407,27 @@ func (t *Pipeline_TriggerMetadata) UnmarshalCBOR(r io.Reader) (err error) {
}
}
+ // t.SourceRepo (string) (string)
+ case "sourceRepo":
+
+ {
+ b, err := cr.ReadByte()
+ if err != nil {
+ return err
+ }
+ if b != cbg.CborNull[0] {
+ if err := cr.UnreadByte(); err != nil {
+ return err
+ }
+
+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
+ if err != nil {
+ return err
+ }
+
+ t.SourceRepo = (*string)(&sval)
+ }
+ }
// t.PullRequest (tangled.Pipeline_PullRequestTriggerData) (struct)
case "pullRequest":
diff --git a/api/tangled/cipipeline.go b/api/tangled/cipipeline.go
index 4368e1bb..290917ad 100644
--- a/api/tangled/cipipeline.go
+++ b/api/tangled/cipipeline.go
@@ -26,6 +26,8 @@ type CiPipeline struct {
Id string `json:"id" cborgen:"id"`
// repo: Repository DID
Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"`
+ // sourceRepo: Repository DID that the commit was checked out from, if different from repo (e.g. a fork for a fork-based pull request)
+ SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
// trigger: Trigger event metadata
Trigger *CiPipeline_Trigger `json:"trigger" cborgen:"trigger"`
// workflows: Triggered workflows
diff --git a/api/tangled/citrigger.go b/api/tangled/citrigger.go
index f75c2723..3229d527 100644
--- a/api/tangled/citrigger.go
+++ b/api/tangled/citrigger.go
@@ -10,7 +10,20 @@ const ()
//
// RECORDTYPE: CiTrigger_Manual
type CiTrigger_Manual struct {
- LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#manual" cborgen:"$type,const=sh.tangled.ci.trigger#manual"`
+ LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#manual" cborgen:"$type,const=sh.tangled.ci.trigger#manual"`
+ Inputs []*CiTrigger_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"`
+ // ref: optional ref the SHA was resolved from, for display and TANGLED_REF
+ Ref *string `json:"ref,omitempty" cborgen:"ref,omitempty"`
+ // sha: commit SHA the manual run targets
+ Sha string `json:"sha" cborgen:"sha"`
+ // sourceRepo: Repository DID to check out code and workflow definitions from, if different from the target repo.
+ SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
+}
+
+// CiTrigger_Pair is a "pair" in the sh.tangled.ci.trigger schema.
+type CiTrigger_Pair struct {
+ Key string `json:"key" cborgen:"key"`
+ Value string `json:"value" cborgen:"value"`
}
// CiTrigger_PullRequest is a "pullRequest" in the sh.tangled.ci.trigger schema.
@@ -19,11 +32,14 @@ type CiTrigger_Manual struct {
//
// RECORDTYPE: CiTrigger_PullRequest
type CiTrigger_PullRequest struct {
- LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#pullRequest" cborgen:"$type,const=sh.tangled.ci.trigger#pullRequest"`
- Action string `json:"action" cborgen:"action"`
- SourceBranch *string `json:"sourceBranch,omitempty" cborgen:"sourceBranch,omitempty"`
- SourceSha string `json:"sourceSha" cborgen:"sourceSha"`
- TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
+ LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#pullRequest" cborgen:"$type,const=sh.tangled.ci.trigger#pullRequest"`
+ // pull: AT-URI of the sh.tangled.repo.pull record this run belongs to
+ Pull *string `json:"pull,omitempty" cborgen:"pull,omitempty"`
+ SourceBranch *string `json:"sourceBranch,omitempty" cborgen:"sourceBranch,omitempty"`
+ // sourceRepo: Repository DID to check out code and workflow definitions from, if different from the target repo.
+ SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
+ SourceSha string `json:"sourceSha" cborgen:"sourceSha"`
+ TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
}
// CiTrigger_Push is a "push" in the sh.tangled.ci.trigger schema.
diff --git a/api/tangled/citriggerPipeline.go b/api/tangled/citriggerPipeline.go
index d372886a..364672bf 100644
--- a/api/tangled/citriggerPipeline.go
+++ b/api/tangled/citriggerPipeline.go
@@ -6,6 +6,8 @@ package tangled
import (
"context"
+ "encoding/json"
+ "fmt"
"github.com/bluesky-social/indigo/lex/util"
)
@@ -16,16 +18,50 @@ const (
// CiTriggerPipeline_Input is the input argument to a sh.tangled.ci.triggerPipeline call.
type CiTriggerPipeline_Input struct {
- // ref: optional ref the SHA was resolved from, for display
- Ref *string `json:"ref,omitempty" cborgen:"ref,omitempty"`
- // repo: AT-URI of the sh.tangled.repo record
+ // repo: Target repository DID. Auth is checked against this repo.
Repo string `json:"repo" cborgen:"repo"`
- // sha: commit SHA to run the pipeline at
- Sha string `json:"sha" cborgen:"sha"`
+ // trigger: Trigger metadata for this dispatch.
+ Trigger *CiTriggerPipeline_Input_Trigger `json:"trigger" cborgen:"trigger"`
// workflows: Workflow names to run. When not provided, every dispatchable workflow is run.
Workflows []string `json:"workflows,omitempty" cborgen:"workflows,omitempty"`
}
+// Trigger metadata for this dispatch.
+type CiTriggerPipeline_Input_Trigger struct {
+ CiTrigger_Manual *CiTrigger_Manual
+ CiTrigger_PullRequest *CiTrigger_PullRequest
+}
+
+func (t *CiTriggerPipeline_Input_Trigger) MarshalJSON() ([]byte, error) {
+ if t.CiTrigger_Manual != nil {
+ t.CiTrigger_Manual.LexiconTypeID = "sh.tangled.ci.trigger#manual"
+ return json.Marshal(t.CiTrigger_Manual)
+ }
+ if t.CiTrigger_PullRequest != nil {
+ t.CiTrigger_PullRequest.LexiconTypeID = "sh.tangled.ci.trigger#pullRequest"
+ return json.Marshal(t.CiTrigger_PullRequest)
+ }
+ return nil, fmt.Errorf("cannot marshal empty enum")
+}
+func (t *CiTriggerPipeline_Input_Trigger) UnmarshalJSON(b []byte) error {
+ typ, err := util.TypeExtract(b)
+ if err != nil {
+ return err
+ }
+
+ switch typ {
+ case "sh.tangled.ci.trigger#manual":
+ t.CiTrigger_Manual = new(CiTrigger_Manual)
+ return json.Unmarshal(b, t.CiTrigger_Manual)
+ case "sh.tangled.ci.trigger#pullRequest":
+ t.CiTrigger_PullRequest = new(CiTrigger_PullRequest)
+ return json.Unmarshal(b, t.CiTrigger_PullRequest)
+
+ default:
+ return nil
+ }
+}
+
// CiTriggerPipeline_Output is the output of a sh.tangled.ci.triggerPipeline call.
type CiTriggerPipeline_Output struct {
// pipeline: AT-URI of the created pipeline
diff --git a/api/tangled/pipelinecancelPipeline.go b/api/tangled/pipelinecancelPipeline.go
index 584bf77d..975550d0 100644
--- a/api/tangled/pipelinecancelPipeline.go
+++ b/api/tangled/pipelinecancelPipeline.go
@@ -2,7 +2,7 @@
package tangled
-// schema: sh.tangled.ci.pipeline.cancelPipeline
+// schema: sh.tangled.pipeline.cancelPipeline
import (
"context"
@@ -11,22 +11,22 @@ import (
)
const (
- CiPipelineCancelPipelineNSID = "sh.tangled.ci.pipeline.cancelPipeline"
+ PipelineCancelPipelineNSID = "sh.tangled.pipeline.cancelPipeline"
)
-// CiPipelineCancelPipeline_Input is the input argument to a sh.tangled.ci.pipeline.cancelPipeline call.
-type CiPipelineCancelPipeline_Input struct {
- // pipeline: pipeline TID
+// PipelineCancelPipeline_Input is the input argument to a sh.tangled.pipeline.cancelPipeline call.
+type PipelineCancelPipeline_Input struct {
+ // pipeline: pipeline at-uri
Pipeline string `json:"pipeline" cborgen:"pipeline"`
- // repo: git repository DID
+ // repo: repo at-uri, spindle can't resolve repo from pipeline at-uri yet
Repo string `json:"repo" cborgen:"repo"`
- // workflows: Workflow names to filter. When not provided, entire pipeline will be canceled.
- Workflows []string `json:"workflows,omitempty" cborgen:"workflows,omitempty"`
+ // workflow: workflow name
+ Workflow string `json:"workflow" cborgen:"workflow"`
}
-// CiPipelineCancelPipeline calls the XRPC method "sh.tangled.ci.pipeline.cancelPipeline".
-func CiPipelineCancelPipeline(ctx context.Context, c util.LexClient, input *CiPipelineCancelPipeline_Input) error {
- if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.ci.pipeline.cancelPipeline", nil, input, nil); err != nil {
+// PipelineCancelPipeline calls the XRPC method "sh.tangled.pipeline.cancelPipeline".
+func PipelineCancelPipeline(ctx context.Context, c util.LexClient, input *PipelineCancelPipeline_Input) error {
+ if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.pipeline.cancelPipeline", nil, input, nil); err != nil {
return err
}
diff --git a/api/tangled/tangledpipeline.go b/api/tangled/tangledpipeline.go
index b51b1127..da00f406 100644
--- a/api/tangled/tangledpipeline.go
+++ b/api/tangled/tangledpipeline.go
@@ -49,10 +49,11 @@ type Pipeline_Pair struct {
// Pipeline_PullRequestTriggerData is a "pullRequestTriggerData" in the sh.tangled.pipeline schema.
type Pipeline_PullRequestTriggerData struct {
- Action string `json:"action" cborgen:"action"`
- SourceBranch string `json:"sourceBranch" cborgen:"sourceBranch"`
- SourceSha string `json:"sourceSha" cborgen:"sourceSha"`
- TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
+ // pull: AT-URI of the sh.tangled.repo.pull record this run belongs to
+ Pull *string `json:"pull,omitempty" cborgen:"pull,omitempty"`
+ SourceBranch string `json:"sourceBranch" cborgen:"sourceBranch"`
+ SourceSha string `json:"sourceSha" cborgen:"sourceSha"`
+ TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
}
// Pipeline_PushTriggerData is a "pushTriggerData" in the sh.tangled.pipeline schema.
@@ -69,6 +70,8 @@ type Pipeline_TriggerMetadata struct {
PullRequest *Pipeline_PullRequestTriggerData `json:"pullRequest,omitempty" cborgen:"pullRequest,omitempty"`
Push *Pipeline_PushTriggerData `json:"push,omitempty" cborgen:"push,omitempty"`
Repo *Pipeline_TriggerRepo `json:"repo" cborgen:"repo"`
+ // sourceRepo: Repository DID that code and workflow definitions are checked out from, when different from repo (e.g. a fork's commit for a fork-based manual trigger). If absent, source uses repo itself.
+ SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
}
// Pipeline_TriggerRepo is a "triggerRepo" in the sh.tangled.pipeline schema.
diff --git a/appview/ingester.go b/appview/ingester.go
index dc3c648d..65bbabd0 100644
--- a/appview/ingester.go
+++ b/appview/ingester.go
@@ -31,12 +31,12 @@ import (
"tangled.org/core/appview/mentions"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
- "tangled.org/core/appview/repoverify"
"tangled.org/core/appview/serververify"
"tangled.org/core/consts"
"tangled.org/core/idresolver"
"tangled.org/core/orm"
"tangled.org/core/rbac"
+ "tangled.org/core/repoverify"
)
type RepoPermissionChecker interface {
diff --git a/appview/ingester_repo.go b/appview/ingester_repo.go
index 95f7ed0a..953bf1f3 100644
--- a/appview/ingester_repo.go
+++ b/appview/ingester_repo.go
@@ -15,8 +15,8 @@ import (
"tangled.org/core/api/tangled"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
- "tangled.org/core/appview/repoverify"
"tangled.org/core/orm"
+ "tangled.org/core/repoverify"
)
func (i *Ingester) ingestRepo(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
diff --git a/appview/ingester_repo_test.go b/appview/ingester_repo_test.go
index a8eb452b..44302d81 100644
--- a/appview/ingester_repo_test.go
+++ b/appview/ingester_repo_test.go
@@ -16,9 +16,9 @@ import (
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
- "tangled.org/core/appview/repoverify"
"tangled.org/core/orm"
"tangled.org/core/rbac"
+ "tangled.org/core/repoverify"
)
func mustKnotURL(t *testing.T, raw string) *url.URL {
diff --git a/appview/knots/knots.go b/appview/knots/knots.go
index 7b920b93..8e574a06 100644
--- a/appview/knots/knots.go
+++ b/appview/knots/knots.go
@@ -20,13 +20,13 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/serververify"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/consts"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
+ "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/atclient"
diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go
index e9953cba..16eeb82d 100644
--- a/appview/oauth/oauth.go
+++ b/appview/oauth/oauth.go
@@ -22,6 +22,7 @@ import (
"golang.org/x/sync/singleflight"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
+ "tangled.org/core/hostutil"
"tangled.org/core/idresolver"
"tangled.org/core/rbac"
"tangled.org/core/xrpc/serviceauth"
@@ -445,6 +446,20 @@ func (o *OAuth) ServiceClient(r *http.Request, os ...ServiceClientOpt) (*xrpc.Cl
}, nil
}
+func (o *OAuth) SpindleServiceClient(r *http.Request, spindle, lxm string) (*xrpc.Client, error) {
+ hostname, noTLS, err := hostutil.ParseHostname(spindle)
+ if err != nil {
+ return nil, err
+ }
+ return o.ServiceClient(
+ r,
+ WithService(hostname),
+ WithLxm(lxm),
+ WithDev(noTLS),
+ WithTimeout(time.Second*30),
+ )
+}
+
func (o *OAuth) StartElevatedAuthFlow(ctx context.Context, w http.ResponseWriter, r *http.Request, did string, extraScopes []string, returnURL string) (string, error) {
parsedDid, err := syntax.ParseDID(did)
if err != nil {
diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
index f43e6e43..953d0409 100644
--- a/appview/pages/funcmap.go
+++ b/appview/pages/funcmap.go
@@ -35,12 +35,27 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages/markup"
"tangled.org/core/appview/pages/markup/sanitizer"
+ "tangled.org/core/appview/pages/repoinfo"
"tangled.org/core/crypto"
"tangled.org/core/idresolver"
+ "tangled.org/core/orm"
+ "tangled.org/core/types"
)
type tab map[string]string
+func (p *Pages) ownerSlashRepo(repo *models.Repo) string {
+ ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did)
+ if err != nil {
+ return repo.RepoIdentifier()
+ }
+ handle := ownerId.Handle
+ if handle != "" && !handle.IsInvalidHandle() {
+ return string(handle) + "/" + repo.Slug()
+ }
+ return repo.RepoIdentifier()
+}
+
func (p *Pages) funcMap() template.FuncMap {
return template.FuncMap{
"split": func(s string) []string {
@@ -91,16 +106,42 @@ func (p *Pages) funcMap() template.FuncMap {
}
return identity.PDSEndpoint()
},
- "ownerSlashRepo": func(repo *models.Repo) string {
- ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did)
- if err != nil {
- return repo.RepoIdentifier()
+ "ownerSlashRepo": p.ownerSlashRepo,
+ "pipelineCommitPath": func(repoInfo repoinfo.RepoInfo, pipeline types.Pipeline) string {
+ sha := pipeline.Sha()
+ if sourceRepo := pipeline.SourceRepo(); sourceRepo != nil {
+ if repo, err := db.GetRepoByDid(p.db, *sourceRepo); err == nil && repo != nil {
+ return "/" + p.ownerSlashRepo(repo) + "/commit/" + sha
+ }
}
- handle := ownerId.Handle
- if handle != "" && !handle.IsInvalidHandle() {
- return string(handle) + "/" + repo.Slug()
+ return "/" + repoInfo.FullName() + "/commit/" + sha
+ },
+ "pipelineSourceLabel": func(pipeline types.Pipeline) string {
+ branch := pipeline.Trigger().PRSourceBranch()
+ if branch == "" {
+ return branch
+ }
+ sourceRepo := pipeline.SourceRepo()
+ if sourceRepo == nil {
+ return branch
+ }
+ repo, err := db.GetRepoByDid(p.db, *sourceRepo)
+ if err != nil || repo == nil {
+ return branch
+ }
+ return p.ownerSlashRepo(repo) + "/" + branch
+ },
+ "pipelinePullPath": func(pipeline types.Pipeline) string {
+ pullAtStr := pipeline.Trigger().PRUri()
+ if pullAtStr == "" {
+ return ""
+ }
+ // GetPull's reverse-mapping already populates pull.Repo
+ pull, err := db.GetPull(p.db, orm.FilterEq("at_uri", pullAtStr))
+ if err != nil || pull == nil || pull.Repo == nil {
+ return ""
}
- return repo.RepoIdentifier()
+ return fmt.Sprintf("/%s/pulls/%d", p.ownerSlashRepo(pull.Repo), pull.PullId)
},
"truncateAt30": func(s string) string {
if len(s) <= 30 {
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index f99c12f4..5ec8513f 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -1442,6 +1442,12 @@ type RepoSinglePullParams struct {
ActiveRound int
IsInterdiff bool
+ // WorkflowsChanged and ChangedWorkflowFiles describe whether the latest
+ // round's patch touches .tangled/workflows/, for warning maintainers
+ // before they manually trigger CI on a fork-based pull request.
+ WorkflowsChanged bool
+ ChangedWorkflowFiles []string
+
Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
index e26438fd..04f86600 100644
--- a/appview/pages/templates/repo/pipelines/pipelines.html
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -97,6 +97,8 @@
{{ $target := .Trigger.TargetRef }}
{{ $workflows := .Workflows }}
+ {{ $pullPath := "" }}
+ {{ if .Trigger.IsPullRequest }}{{ $pullPath = pipelinePullPath . }}{{ end }}
@@ -115,7 +117,7 @@
Pull request
{{ $target }}
{{ i "arrow-left" "size-3 text-gray-500 dark:text-gray-400" }}
- {{ .Trigger.PRSourceBranch }}
+ {{ pipelineSourceLabel . }}
{{ else if .Trigger.IsManual }}
{{ i "circle-play" "size-4 text-gray-500 dark:text-gray-400 shrink-0" }}
Manual dispatch
@@ -123,6 +125,9 @@
{{ if .IsResponding }}
{{ end }}
+ {{ if $pullPath }}
+ (view PR)
+ {{ end }}
@@ -130,7 +135,7 @@
{{ if .Sha }}
-
+
{{ slice .Sha 0 8 }}
{{ end }}
diff --git a/appview/pages/templates/repo/pipelines/workflow.html b/appview/pages/templates/repo/pipelines/workflow.html
index c8e8a30b..3325b5ff 100644
--- a/appview/pages/templates/repo/pipelines/workflow.html
+++ b/appview/pages/templates/repo/pipelines/workflow.html
@@ -12,8 +12,7 @@
{{ block "sidebar" . }} {{ end }}
-
- {{ if $.RepoInfo.Roles.IsOwner }}
+ {{ if $.RepoInfo.Roles.IsPushAllowed }}
{{ $status := (index .Pipeline.Statuses .Workflow).Latest.Status }}
+{{ end }}
+
+{{ define "repo/pulls/fragments/triggerCi" }}
+{{ if .WorkflowsChanged }}
+
+
+
+ {{ i "triangle-alert" "w-4 h-4 flex-shrink-0" }}
+
Workflow files changed in this round
+
+ Expand
+ Collapse
+
+
+
+
+ {{ range .ChangedWorkflowFiles }}
+ -
+ {{ i "file-warning" "inline-flex w-4 h-4 mr-1.5 text-amber-600 dark:text-amber-500 flex-shrink-0" }}
+ {{ . }}
+
+ {{ end }}
+
+
+ {{ $confirm := printf "Workflow files changed in this round (%s). Review before running. Run anyway?" (join .ChangedWorkflowFiles ", ") }}
+ {{ template "repo/pulls/fragments/runCiButton" (dict "RepoInfo" .RepoInfo "Pull" .Pull "Confirm" $confirm) }}
+
+
+{{ else }}
+
+ {{ i "circle-play" "w-4 h-4 flex-shrink-0" }}
+ CI hasn't run on the latest commit
+ {{ template "repo/pulls/fragments/runCiButton" (dict "RepoInfo" .RepoInfo "Pull" .Pull "Confirm" "") }}
+
+{{ end }}
+
+{{ end }}
diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html
index 77b94bdd..ccbe7fac 100644
--- a/appview/pages/templates/repo/pulls/pull.html
+++ b/appview/pages/templates/repo/pulls/pull.html
@@ -514,9 +514,8 @@
{{ $item := index . 0 }}
{{ $root := index . 3 }}
{{ $pipeline := index $root.Pipelines $item.SourceRev }}
- {{ with $pipeline }}
- {{ $id := .Id }}
- {{ if .Statuses }}
+ {{ if and $pipeline $pipeline.Statuses }}
+ {{ $id := $pipeline.Id }}
{{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" false) }}
@@ -526,7 +525,7 @@
- {{ range $name, $all := .Statuses }}
+ {{ range $name, $all := $pipeline.Statuses }}
@@ -550,7 +549,8 @@
{{ end }}
- {{ end }}
+ {{ else if and $root.Pull.IsForkBased (eq $item.RoundNumber $root.Pull.LastRoundNumber) $root.RepoInfo.Roles.IsOwner }}
+ {{ template "repo/pulls/fragments/triggerCi" $root }}
{{ end }}
{{ end }}
diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go
index 48fa2619..7e69262d 100644
--- a/appview/pipelines/pipelines.go
+++ b/appview/pipelines/pipelines.go
@@ -46,7 +46,7 @@ func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler {
r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)
r.Group(func(r chi.Router) {
- r.Use(mw.RepoPermissionMiddleware("repo:owner"))
+ r.Use(mw.RepoPermissionMiddleware("repo:push"))
r.Post("/{pipeline}/workflow/{workflow}/cancel", p.CancelWorkflow)
r.Post("/{pipeline}/retry", p.RetryPipeline)
r.Post("/{pipeline}/workflow/{workflow}/retry", p.RetryWorkflow)
@@ -251,6 +251,50 @@ func (w *webLogScheduler) AddWork(ctx context.Context, _ string, val *tangled.Ci
// Shutdown implements [lexutil.Scheduler].
func (w *webLogScheduler) Shutdown() { close(w.ch) }
+func retryPipelineTrigger(orig *tangled.CiPipeline) *tangled.CiTriggerPipeline_Input_Trigger {
+ if orig.Trigger != nil && orig.Trigger.CiTrigger_PullRequest != nil {
+ pr := orig.Trigger.CiTrigger_PullRequest
+ sourceSha := pr.SourceSha
+ if sourceSha == "" {
+ sourceSha = orig.Commit
+ }
+ sourceRepo := pr.SourceRepo
+ if sourceRepo == nil {
+ sourceRepo = orig.SourceRepo
+ }
+
+ return &tangled.CiTriggerPipeline_Input_Trigger{
+ CiTrigger_PullRequest: &tangled.CiTrigger_PullRequest{
+ Pull: pr.Pull,
+ SourceBranch: pr.SourceBranch,
+ SourceRepo: sourceRepo,
+ SourceSha: sourceSha,
+ TargetBranch: pr.TargetBranch,
+ },
+ }
+ }
+
+ manual := &tangled.CiTrigger_Manual{
+ Sha: orig.Commit,
+ SourceRepo: orig.SourceRepo,
+ }
+ if orig.Trigger != nil && orig.Trigger.CiTrigger_Manual != nil {
+ origManual := orig.Trigger.CiTrigger_Manual
+ if origManual.Sha != "" {
+ manual.Sha = origManual.Sha
+ }
+ manual.Ref = origManual.Ref
+ manual.Inputs = origManual.Inputs
+ if origManual.SourceRepo != nil {
+ manual.SourceRepo = origManual.SourceRepo
+ }
+ }
+
+ return &tangled.CiTriggerPipeline_Input_Trigger{
+ CiTrigger_Manual: manual,
+ }
+}
+
func (p *Pipelines) Logs(w http.ResponseWriter, r *http.Request) {
l := p.logger.With("handler", "logs")
@@ -466,20 +510,19 @@ func (p *Pipelines) CancelWorkflow(w http.ResponseWriter, r *http.Request) {
l = l.With("pipeline", pipelineId, "workflow", workflowName)
- spindleClient, err := p.spindleServiceClient(r, f.Spindle, tangled.CiPipelineCancelPipelineNSID)
+ spindleClient, err := p.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiCancelPipelineNSID)
if err != nil {
l.Error("failed to prepare spindle client", "err", err)
p.pages.Notice(w, errorId, "Failed to cancel workflow")
return
}
- pipelineAtUri := fmt.Sprintf("at://did:web:%s/%s/%s", f.Knot, tangled.PipelineNSID, pipelineId.String())
- if err := tangled.CiPipelineCancelPipeline(
+ if err := tangled.CiCancelPipeline(
r.Context(),
spindleClient,
- &tangled.CiPipelineCancelPipeline_Input{
- Repo: string(f.RepoAt()),
- Pipeline: pipelineAtUri,
+ &tangled.CiCancelPipeline_Input{
+ Repo: f.RepoDid,
+ Pipeline: pipelineId.String(),
Workflows: []string{workflowName},
},
); err != nil {
@@ -570,7 +613,7 @@ func (p *Pipelines) retry(w http.ResponseWriter, r *http.Request, only string) {
}
redirectWf := workflows[0]
- spindleClient, err := p.spindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID)
+ spindleClient, err := p.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID)
if err != nil {
fail("failed to authorize with spindle", err)
return
@@ -580,8 +623,8 @@ func (p *Pipelines) retry(w http.ResponseWriter, r *http.Request, only string) {
r.Context(),
spindleClient,
&tangled.CiTriggerPipeline_Input{
- Repo: string(f.RepoAt()),
- Sha: orig.Commit,
+ Repo: f.RepoDid,
+ Trigger: retryPipelineTrigger(orig),
Workflows: workflows,
},
)
@@ -609,18 +652,3 @@ func (p *Pipelines) retry(w http.ResponseWriter, r *http.Request, only string) {
}
http.Redirect(w, r, dest, http.StatusSeeOther)
}
-
-// spindleServiceClient builds an authed spindle xrpc client
-func (p *Pipelines) spindleServiceClient(r *http.Request, spindle, lxm string) (*indigoxrpc.Client, error) {
- hostname, noTLS, err := hostutil.ParseHostname(spindle)
- if err != nil {
- return nil, err
- }
- return p.oauth.ServiceClient(
- r,
- oauth.WithService(hostname),
- oauth.WithLxm(lxm),
- oauth.WithDev(noTLS),
- oauth.WithTimeout(time.Second*30),
- )
-}
diff --git a/appview/pulls/compose.go b/appview/pulls/compose.go
index aa3d05e3..6b2936e4 100644
--- a/appview/pulls/compose.go
+++ b/appview/pulls/compose.go
@@ -18,9 +18,9 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pages/markup/sanitizer"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/patchutil"
"tangled.org/core/types"
+ "tangled.org/core/xrpc/xrpcclient"
"github.com/bluesky-social/indigo/atproto/syntax"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
diff --git a/appview/pulls/create.go b/appview/pulls/create.go
index 34f37a83..2767f663 100644
--- a/appview/pulls/create.go
+++ b/appview/pulls/create.go
@@ -16,11 +16,11 @@ import (
"tangled.org/core/appview/models"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/reporesolver"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/patchutil"
"tangled.org/core/tid"
"tangled.org/core/types"
"tangled.org/core/xrpc"
+ "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
diff --git a/appview/pulls/merge.go b/appview/pulls/merge.go
index de2b41fe..1b0bba83 100644
--- a/appview/pulls/merge.go
+++ b/appview/pulls/merge.go
@@ -10,8 +10,8 @@ import (
"tangled.org/core/appview/models"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/reporesolver"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
+ "tangled.org/core/xrpc/xrpcclient"
"github.com/bluesky-social/indigo/atproto/syntax"
)
diff --git a/appview/pulls/resubmit.go b/appview/pulls/resubmit.go
index 6309ea33..5fb6fbd7 100644
--- a/appview/pulls/resubmit.go
+++ b/appview/pulls/resubmit.go
@@ -13,11 +13,11 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/reporesolver"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
"tangled.org/core/patchutil"
"tangled.org/core/types"
"tangled.org/core/xrpc"
+ "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
diff --git a/appview/pulls/router.go b/appview/pulls/router.go
index 530d4941..03a43909 100644
--- a/appview/pulls/router.go
+++ b/appview/pulls/router.go
@@ -50,6 +50,11 @@ func (s *Pulls) Router(mw *middleware.Middleware) http.Handler {
r.Post("/merge", s.MergePull)
// maybe lock, etc.
})
+
+ r.Group(func(r chi.Router) {
+ r.Use(mw.RepoPermissionMiddleware("repo:push"))
+ r.Post("/trigger-ci", s.TriggerCi)
+ })
})
})
return r
diff --git a/appview/pulls/single.go b/appview/pulls/single.go
index 4c1427c8..40b5f451 100644
--- a/appview/pulls/single.go
+++ b/appview/pulls/single.go
@@ -10,10 +10,10 @@ import (
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
"tangled.org/core/patchutil"
"tangled.org/core/types"
+ "tangled.org/core/xrpc/xrpcclient"
"github.com/bluesky-social/indigo/atproto/syntax"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
@@ -187,6 +187,16 @@ func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff
return m
}(r.Context())
+ var workflowsChanged bool
+ var changedWorkflows []string
+ if _, hasPipeline := pipelines[pull.LatestSha()]; pull.IsForkBased() && !hasPipeline {
+ changedWorkflows, err = changedWorkflowFiles(pull.LatestSubmission().CombinedPatch())
+ if err != nil {
+ l.Error("failed to inspect latest round's patch for workflow changes", "err", err)
+ }
+ workflowsChanged = len(changedWorkflows) > 0
+ }
+
entities := []syntax.ATURI{pull.AtUri()}
for _, s := range pull.Submissions {
for _, c := range s.Comments {
@@ -274,6 +284,9 @@ func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff
ActiveRound: roundIdInt,
IsInterdiff: interdiff,
+ WorkflowsChanged: workflowsChanged,
+ ChangedWorkflowFiles: changedWorkflows,
+
Reactions: reactions,
UserReacted: userReactions,
diff --git a/appview/pulls/trigger_ci.go b/appview/pulls/trigger_ci.go
new file mode 100644
index 00000000..61dccf79
--- /dev/null
+++ b/appview/pulls/trigger_ci.go
@@ -0,0 +1,137 @@
+package pulls
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "tangled.org/core/api/tangled"
+ "tangled.org/core/appview/db"
+ "tangled.org/core/appview/models"
+ "tangled.org/core/patchutil"
+ "tangled.org/core/workflow"
+)
+
+func changedWorkflowFiles(patch string) ([]string, error) {
+ files, err := patchutil.AsDiff(patch)
+ if err != nil {
+ return nil, err
+ }
+
+ var changed []string
+ for _, f := range files {
+ if f == nil {
+ continue
+ }
+ for _, name := range []string{f.NewName, f.OldName} {
+ if name != "" && strings.HasPrefix(name, workflow.WorkflowDir+"/") {
+ changed = append(changed, name)
+ break
+ }
+ }
+ }
+ return changed, nil
+}
+
+// TriggerCi manually triggers a CI pipeline for a fork-based pull request.
+// authorized against and recorded under the target repo, but checked out
+// from the fork at the latest round's commit.
+func (s *Pulls) TriggerCi(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "TriggerCi")
+ errorId := "pull-error"
+
+ fail := func(msg string, err error) {
+ if err != nil {
+ l.Error(msg, "err", err)
+ } else {
+ l.Error(msg)
+ }
+ s.pages.Notice(w, errorId, msg)
+ }
+
+ f, err := s.repoResolver.Resolve(r)
+ if err != nil {
+ fail("failed to resolve repository", err)
+ return
+ }
+
+ pull, ok := r.Context().Value("pull").(*models.Pull)
+ if !ok {
+ fail("failed to get pull", nil)
+ return
+ }
+ l = l.With("pull_id", pull.PullId)
+
+ if !pull.IsForkBased() {
+ fail("this pull request is not fork-based", nil)
+ return
+ }
+
+ if f.Spindle == "" {
+ fail("this repository has no spindle configured", nil)
+ return
+ }
+
+ latest := pull.LatestSubmission()
+ if latest.SourceRev == "" {
+ fail("cannot trigger ci: this round has no commit to run", nil)
+ return
+ }
+
+ changedFiles, err := changedWorkflowFiles(latest.CombinedPatch())
+ if err != nil {
+ fail("failed to inspect the latest round's patch", err)
+ return
+ }
+ if len(changedFiles) > 0 && r.URL.Query().Get("confirm") != "1" {
+ fail(fmt.Sprintf("workflow files changed in this round (%s); review before running", strings.Join(changedFiles, ", ")), nil)
+ return
+ }
+
+ forkRepo, err := db.GetRepoByDid(s.db, pull.PullSource.RepoDid.String())
+ if err != nil {
+ fail("failed to resolve the fork this pull request comes from", err)
+ return
+ }
+
+ spindleClient, err := s.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID)
+ if err != nil {
+ fail("failed to authorize with spindle", err)
+ return
+ }
+
+ pullAt := pull.AtUri().String()
+ sourceBranch := pull.PullSource.Branch
+ targetBranch := pull.TargetBranch
+ out, err := tangled.CiTriggerPipeline(
+ r.Context(),
+ spindleClient,
+ &tangled.CiTriggerPipeline_Input{
+ Repo: f.RepoDid,
+ Trigger: &tangled.CiTriggerPipeline_Input_Trigger{
+ CiTrigger_PullRequest: &tangled.CiTrigger_PullRequest{
+ Pull: &pullAt,
+ SourceBranch: &sourceBranch,
+ SourceRepo: &forkRepo.RepoDid,
+ SourceSha: latest.SourceRev,
+ TargetBranch: targetBranch,
+ },
+ },
+ },
+ )
+ if err != nil {
+ fail("spindle rejected the trigger", err)
+ return
+ }
+ l.Info("triggered ci for fork-based pull", "pipeline", out.Pipeline)
+
+ user := s.oauth.GetMultiAccountUser(r)
+ repoInfo := s.repoResolver.GetRepoInfo(r, user)
+ dest := fmt.Sprintf("/%s/pulls/%d/round/%d", repoInfo.FullName(), pull.PullId, pull.LastRoundNumber())
+ if r.Header.Get("HX-Request") == "true" {
+ w.Header().Set("HX-Redirect", dest)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.Redirect(w, r, dest, http.StatusSeeOther)
+}
diff --git a/appview/repo/artifact.go b/appview/repo/artifact.go
index 2a8c6559..1dd39eb5 100644
--- a/appview/repo/artifact.go
+++ b/appview/repo/artifact.go
@@ -13,11 +13,11 @@ import (
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
"tangled.org/core/tid"
"tangled.org/core/types"
"tangled.org/core/xrpc"
+ "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
diff --git a/appview/repo/blob.go b/appview/repo/blob.go
index f9596802..d1bd951e 100644
--- a/appview/repo/blob.go
+++ b/appview/repo/blob.go
@@ -18,8 +18,8 @@ import (
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pages/markup"
"tangled.org/core/appview/reporesolver"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
"github.com/bluesky-social/indigo/util"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
diff --git a/appview/repo/branches.go b/appview/repo/branches.go
index 1a6ed938..f6705915 100644
--- a/appview/repo/branches.go
+++ b/appview/repo/branches.go
@@ -8,8 +8,8 @@ import (
"tangled.org/core/api/tangled"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
)
diff --git a/appview/repo/compare.go b/appview/repo/compare.go
index 5ba2af80..5b6442e0 100644
--- a/appview/repo/compare.go
+++ b/appview/repo/compare.go
@@ -10,9 +10,9 @@ import (
"tangled.org/core/api/tangled"
"tangled.org/core/appview/pages"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/patchutil"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"github.com/go-chi/chi/v5"
diff --git a/appview/repo/log.go b/appview/repo/log.go
index 9971b57f..22f4cec2 100644
--- a/appview/repo/log.go
+++ b/appview/repo/log.go
@@ -12,8 +12,8 @@ import (
"tangled.org/core/appview/commitverify"
"tangled.org/core/appview/db"
"tangled.org/core/appview/pages"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"github.com/go-chi/chi/v5"
diff --git a/appview/repo/repo.go b/appview/repo/repo.go
index 996010b9..7ecae68b 100644
--- a/appview/repo/repo.go
+++ b/appview/repo/repo.go
@@ -27,7 +27,6 @@ import (
"tangled.org/core/appview/pagination"
"tangled.org/core/appview/reporesolver"
"tangled.org/core/appview/sites"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/consts"
"tangled.org/core/idresolver"
"tangled.org/core/ogre"
@@ -35,6 +34,7 @@ import (
"tangled.org/core/rbac"
"tangled.org/core/tid"
"tangled.org/core/xrpc/serviceauth"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/atclient"
diff --git a/appview/repo/settings.go b/appview/repo/settings.go
index bc41fc4b..2bdae1c4 100644
--- a/appview/repo/settings.go
+++ b/appview/repo/settings.go
@@ -20,11 +20,11 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/sites"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/consts"
"tangled.org/core/orm"
"tangled.org/core/sets"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
diff --git a/appview/repo/tags.go b/appview/repo/tags.go
index 66d3752e..ae751ae9 100644
--- a/appview/repo/tags.go
+++ b/appview/repo/tags.go
@@ -11,9 +11,9 @@ import (
"tangled.org/core/appview/models"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/reporesolver"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"github.com/go-chi/chi/v5"
diff --git a/appview/repo/tree.go b/appview/repo/tree.go
index f72001d8..f54fbbda 100644
--- a/appview/repo/tree.go
+++ b/appview/repo/tree.go
@@ -13,8 +13,8 @@ import (
"tangled.org/core/appview/pages"
"tangled.org/core/appview/pages/markup"
"tangled.org/core/appview/reporesolver"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/types"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"github.com/go-chi/chi/v5"
diff --git a/appview/serververify/verify.go b/appview/serververify/verify.go
index 27b09e10..bdd90e8d 100644
--- a/appview/serververify/verify.go
+++ b/appview/serververify/verify.go
@@ -8,9 +8,9 @@ import (
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"tangled.org/core/api/tangled"
"tangled.org/core/appview/db"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/orm"
"tangled.org/core/rbac"
+ "tangled.org/core/xrpc/xrpcclient"
)
var (
diff --git a/appview/spindles/spindles.go b/appview/spindles/spindles.go
index f862aa51..6840fb39 100644
--- a/appview/spindles/spindles.go
+++ b/appview/spindles/spindles.go
@@ -18,11 +18,11 @@ import (
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
"tangled.org/core/appview/serververify"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/idresolver"
"tangled.org/core/orm"
"tangled.org/core/rbac"
"tangled.org/core/tid"
+ "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
diff --git a/appview/state/state.go b/appview/state/state.go
index e79412bd..b30fb464 100644
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -33,8 +33,6 @@ import (
"tangled.org/core/appview/pages"
pipelinessh "tangled.org/core/appview/pipelines/ssh"
"tangled.org/core/appview/reporesolver"
- "tangled.org/core/appview/repoverify"
- xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/consts"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
@@ -43,6 +41,8 @@ import (
tlog "tangled.org/core/log"
"tangled.org/core/orm"
"tangled.org/core/rbac"
+ "tangled.org/core/repoverify"
+ xrpcclient "tangled.org/core/xrpc/xrpcclient"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/atclient"
diff --git a/cmd/cborgen/cborgen.go b/cmd/cborgen/cborgen.go
index 50eceb01..36d45f61 100644
--- a/cmd/cborgen/cborgen.go
+++ b/cmd/cborgen/cborgen.go
@@ -21,6 +21,7 @@ func main() {
tangled.CiSubscribePipelineLogs_Control{},
tangled.CiSubscribePipelineLogs_Data{},
tangled.CiTrigger_Manual{},
+ tangled.CiTrigger_Pair{},
tangled.CiTrigger_PullRequest{},
tangled.CiTrigger_Push{},
tangled.FeedComment{},
diff --git a/flake.nix b/flake.nix
index 4cee0bb9..834e529a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -524,7 +524,7 @@
find api/tangled -maxdepth 1 -name '*.go' -not -name '*_ext.go' -exec \
sed -i.bak 's/\tutil/\/\/\tutil/' {} +
# lexgen generates incomplete Marshaler/Unmarshaler for union types
- find api/tangled/*.go -not -name "cbor_gen.go" -exec \
+ find api/tangled -maxdepth 1 -name '*.go' -not -name '*_ext.go' -not -name "cbor_gen.go" -exec \
sed -i '/^func.*\(MarshalCBOR\|UnmarshalCBOR\)/,/^}/ s/^/\/\/ /' {} +
for f in api/tangled/*_ext.go; do [ -e "''$f" ] && mv "''$f" "''$f.bak"; done
${pkgs.gotools}/bin/goimports -w api/tangled/*
diff --git a/lexicons/ci/pipeline.json b/lexicons/ci/pipeline.json
index ef89839f..33e81cc4 100644
--- a/lexicons/ci/pipeline.json
+++ b/lexicons/ci/pipeline.json
@@ -29,6 +29,11 @@
"type": "string",
"description": "Commit Id this pipeline is running on"
},
+ "sourceRepo": {
+ "type": "string",
+ "format": "did",
+ "description": "Repository DID that the commit was checked out from, if different from repo (e.g. a fork for a fork-based pull request)"
+ },
"createdAt": {
"type": "string",
"format": "datetime"
diff --git a/lexicons/ci/trigger.json b/lexicons/ci/trigger.json
index f621b65b..15c49e1d 100644
--- a/lexicons/ci/trigger.json
+++ b/lexicons/ci/trigger.json
@@ -23,9 +23,14 @@
},
"pullRequest": {
"type": "object",
- "required": ["targetBranch", "sourceSha", "action"],
+ "required": ["targetBranch", "sourceSha"],
"description": "TODO: reference PR record with strongRef instead of embedding raw values",
"properties": {
+ "sourceRepo": {
+ "type": "string",
+ "format": "did",
+ "description": "Repository DID to check out code and workflow definitions from, if different from the target repo."
+ },
"sourceBranch": {
"type": "string"
},
@@ -37,14 +42,52 @@
"minLength": 40,
"maxLength": 40
},
- "action": {
- "type": "string"
+ "pull": {
+ "type": "string",
+ "format": "at-uri",
+ "description": "AT-URI of the sh.tangled.repo.pull record this run belongs to"
}
}
},
"manual": {
"type": "object",
- "properties": {}
+ "required": ["sha"],
+ "properties": {
+ "sha": {
+ "type": "string",
+ "description": "commit SHA the manual run targets",
+ "minLength": 40,
+ "maxLength": 40
+ },
+ "ref": {
+ "type": "string",
+ "description": "optional ref the SHA was resolved from, for display and TANGLED_REF"
+ },
+ "sourceRepo": {
+ "type": "string",
+ "format": "did",
+ "description": "Repository DID to check out code and workflow definitions from, if different from the target repo."
+ },
+ "inputs": {
+ "type": "array",
+ "items": {
+ "type": "ref",
+ "ref": "#pair"
+ }
+ }
+ }
+ },
+ "pair": {
+ "type": "object",
+ "required": ["key", "value"],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
}
}
}
diff --git a/lexicons/ci/triggerPipeline.json b/lexicons/ci/triggerPipeline.json
index 2abe12b6..e45fdafa 100644
--- a/lexicons/ci/triggerPipeline.json
+++ b/lexicons/ci/triggerPipeline.json
@@ -4,27 +4,25 @@
"defs": {
"main": {
"type": "procedure",
- "description": "Manually trigger a pipeline at an explicit commit. Runs the named workflows, or every workflow defined in the repo when none are named.",
+ "description": "Trigger a pipeline at an explicit commit. Runs the named workflows, or every workflow defined in the repo when none are named.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
- "required": ["repo", "sha"],
+ "required": ["repo", "trigger"],
"properties": {
"repo": {
"type": "string",
- "format": "at-uri",
- "description": "AT-URI of the sh.tangled.repo record"
- },
- "sha": {
- "type": "string",
- "minLength": 40,
- "maxLength": 40,
- "description": "commit SHA to run the pipeline at"
+ "format": "did",
+ "description": "Target repository DID. Auth is checked against this repo."
},
- "ref": {
- "type": "string",
- "description": "optional ref the SHA was resolved from, for display"
+ "trigger": {
+ "type": "union",
+ "refs": [
+ "sh.tangled.ci.trigger#manual",
+ "sh.tangled.ci.trigger#pullRequest"
+ ],
+ "description": "Trigger metadata for this dispatch."
},
"workflows": {
"type": "array",
diff --git a/lexicons/pipeline/pipeline.json b/lexicons/pipeline/pipeline.json
index 5a6e4954..9deb76fd 100644
--- a/lexicons/pipeline/pipeline.json
+++ b/lexicons/pipeline/pipeline.json
@@ -59,6 +59,11 @@
"manual": {
"type": "ref",
"ref": "#manualTriggerData"
+ },
+ "sourceRepo": {
+ "type": "string",
+ "format": "did",
+ "description": "Repository DID that code and workflow definitions are checked out from, when different from repo (e.g. a fork's commit for a fork-based manual trigger). If absent, source uses repo itself."
}
}
},
@@ -118,8 +123,7 @@
"required": [
"sourceBranch",
"targetBranch",
- "sourceSha",
- "action"
+ "sourceSha"
],
"properties": {
"sourceBranch": {
@@ -133,8 +137,10 @@
"minLength": 40,
"maxLength": 40
},
- "action": {
- "type": "string"
+ "pull": {
+ "type": "string",
+ "format": "at-uri",
+ "description": "AT-URI of the sh.tangled.repo.pull record this run belongs to"
}
}
},
diff --git a/appview/repoverify/verify.go b/repoverify/verify.go
similarity index 95%
rename from appview/repoverify/verify.go
rename to repoverify/verify.go
index 8fe87a8d..77b2f034 100644
--- a/appview/repoverify/verify.go
+++ b/repoverify/verify.go
@@ -13,8 +13,8 @@ import (
"github.com/bluesky-social/indigo/atproto/syntax"
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"tangled.org/core/api/tangled"
- "tangled.org/core/appview/xrpcclient"
"tangled.org/core/idresolver"
+ "tangled.org/core/xrpc/xrpcclient"
)
type RepoDid syntax.DID
@@ -68,6 +68,9 @@ type Result struct {
RepoDid RepoDid
OwnerDid OwnerDid
KnotURL *url.URL
+ // Rkey of the sh.tangled.repo record tracked by the knot; empty when the
+ // knot does not support describeRepo.
+ Rkey string
}
type Verifier func(ctx context.Context, repoDid RepoDid) (Result, error)
@@ -129,6 +132,7 @@ func resolveAndDescribe(
RepoDid: repoDid,
OwnerDid: ownerDid,
KnotURL: knot,
+ Rkey: out.Rkey,
}, nil
}
diff --git a/appview/repoverify/verify_test.go b/repoverify/verify_test.go
similarity index 100%
rename from appview/repoverify/verify_test.go
rename to repoverify/verify_test.go
diff --git a/spindle/db/pipelines.go b/spindle/db/pipelines.go
index d2f4556b..cccaba9f 100644
--- a/spindle/db/pipelines.go
+++ b/spindle/db/pipelines.go
@@ -9,6 +9,7 @@ import (
"tangled.org/core/api/tangled"
"tangled.org/core/spindle/models"
+ "tangled.org/core/workflow"
)
func (d *DB) QueryPipelines(ctx context.Context, repoDid string, commits []string, cursor string, limit int) ([]*tangled.CiPipeline, string, int64, error) {
@@ -17,7 +18,7 @@ func (d *DB) QueryPipelines(ctx context.Context, repoDid string, commits []strin
}
var query string
- var args []interface{}
+ var args []any
query = `
select
rkey, event, created from events
@@ -33,7 +34,11 @@ func (d *DB) QueryPipelines(ctx context.Context, repoDid string, commits []strin
placeholders[i] = "?"
args = append(args, commits[i])
}
- query += " and json_extract(event, '$.triggerMetadata.push.newSha') in (" + strings.Join(placeholders, ",") + ")"
+ query += ` and coalesce(
+ json_extract(event, '$.triggerMetadata.push.newSha'),
+ json_extract(event, '$.triggerMetadata.pullRequest.sourceSha'),
+ json_extract(event, '$.triggerMetadata.manual.sha')
+ ) in (` + strings.Join(placeholders, ",") + ")"
}
if cursor != "" {
@@ -75,7 +80,7 @@ func (d *DB) QueryPipelines(ctx context.Context, repoDid string, commits []strin
continue
}
- p, err := d.mapToCiPipeline(ctx, rkey, created, rawPipeline)
+ p, err := d.mapToCiPipeline(rkey, created, rawPipeline)
if err != nil {
return nil, "", 0, err
}
@@ -113,10 +118,10 @@ func (d *DB) GetPipeline(ctx context.Context, rkey string) (*tangled.CiPipeline,
return nil, err
}
- return d.mapToCiPipeline(ctx, rkey, created, rawPipeline)
+ return d.mapToCiPipeline(rkey, created, rawPipeline)
}
-func (d *DB) mapToCiPipeline(ctx context.Context, rkey string, created int64, raw tangled.Pipeline) (*tangled.CiPipeline, error) {
+func (d *DB) mapToCiPipeline(rkey string, created int64, raw tangled.Pipeline) (*tangled.CiPipeline, error) {
createdAtStr := time.Unix(0, created).Format(time.RFC3339)
var repoDidStr string
@@ -132,8 +137,8 @@ func (d *DB) mapToCiPipeline(ctx context.Context, rkey string, created int64, ra
var trigger tangled.CiPipeline_Trigger
if raw.TriggerMetadata != nil {
- switch raw.TriggerMetadata.Kind {
- case "push":
+ switch workflow.TriggerKind(raw.TriggerMetadata.Kind) {
+ case workflow.TriggerKindPush:
if raw.TriggerMetadata.Push != nil {
commitSha = raw.TriggerMetadata.Push.NewSha
trigger.CiTrigger_Push = &tangled.CiTrigger_Push{
@@ -142,20 +147,26 @@ func (d *DB) mapToCiPipeline(ctx context.Context, rkey string, created int64, ra
Ref: raw.TriggerMetadata.Push.Ref,
}
}
- case "pullRequest":
+ case workflow.TriggerKindPullRequest:
if raw.TriggerMetadata.PullRequest != nil {
commitSha = raw.TriggerMetadata.PullRequest.SourceSha
trigger.CiTrigger_PullRequest = &tangled.CiTrigger_PullRequest{
- Action: raw.TriggerMetadata.PullRequest.Action,
SourceBranch: &raw.TriggerMetadata.PullRequest.SourceBranch,
+ SourceRepo: raw.TriggerMetadata.SourceRepo,
SourceSha: raw.TriggerMetadata.PullRequest.SourceSha,
TargetBranch: raw.TriggerMetadata.PullRequest.TargetBranch,
+ Pull: raw.TriggerMetadata.PullRequest.Pull,
}
}
- case "manual":
+ case workflow.TriggerKindManual:
if raw.TriggerMetadata.Manual != nil {
commitSha = raw.TriggerMetadata.Manual.Sha
- trigger.CiTrigger_Manual = &tangled.CiTrigger_Manual{}
+ trigger.CiTrigger_Manual = &tangled.CiTrigger_Manual{
+ Inputs: pipelinePairsToCiTriggerPairs(raw.TriggerMetadata.Manual.Inputs),
+ Ref: raw.TriggerMetadata.Manual.Ref,
+ Sha: raw.TriggerMetadata.Manual.Sha,
+ SourceRepo: raw.TriggerMetadata.SourceRepo,
+ }
}
}
}
@@ -192,16 +203,39 @@ func (d *DB) mapToCiPipeline(ctx context.Context, rkey string, created int64, ra
})
}
+ var sourceRepo *string
+ if raw.TriggerMetadata != nil {
+ sourceRepo = raw.TriggerMetadata.SourceRepo
+ }
+
return &tangled.CiPipeline{
- Id: rkey,
- Commit: commitSha,
- Repo: &repoDidStr,
- CreatedAt: &createdAtStr,
- Trigger: &trigger,
- Workflows: workflows,
+ Id: rkey,
+ Commit: commitSha,
+ Repo: &repoDidStr,
+ CreatedAt: &createdAtStr,
+ Trigger: &trigger,
+ Workflows: workflows,
+ SourceRepo: sourceRepo,
}, nil
}
+func pipelinePairsToCiTriggerPairs(inputs []*tangled.Pipeline_Pair) []*tangled.CiTrigger_Pair {
+ if len(inputs) == 0 {
+ return nil
+ }
+ pairs := make([]*tangled.CiTrigger_Pair, 0, len(inputs))
+ for _, input := range inputs {
+ if input == nil {
+ continue
+ }
+ pairs = append(pairs, &tangled.CiTrigger_Pair{
+ Key: input.Key,
+ Value: input.Value,
+ })
+ }
+ return pairs
+}
+
func (d *DB) GetWorkflowTimes(workflowId models.WorkflowId) (startedAt, finishedAt *string) {
pipelineAtUri := workflowId.PipelineId.AtUri()
diff --git a/spindle/engine/engine.go b/spindle/engine/engine.go
index b83fdae6..6589f9d1 100644
--- a/spindle/engine/engine.go
+++ b/spindle/engine/engine.go
@@ -27,12 +27,14 @@ type workflowFinalizer interface {
func StartWorkflows(l *slog.Logger, vault secrets.Manager, cfg *config.Config, db *db.DB, n *notifier.Notifier, ctx context.Context, pipeline *models.Pipeline, pipelineId models.PipelineId) {
l.Info("starting all workflows in parallel", "pipeline", pipelineId)
- // extract secrets
var allSecrets []secrets.UnlockedSecret
- if pipeline.RepoDid != "" {
+ // never pass secrets to pipelines that run untrusted (e.g. fork) code
+ if pipeline.TrustedSource && pipeline.RepoDid != "" {
if res, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(pipeline.RepoDid.String())); err == nil {
allSecrets = res
}
+ } else if !pipeline.TrustedSource {
+ l.Info("skipping secrets for untrusted pipeline source", "pipeline", pipelineId)
}
secretValues := make([]string, len(allSecrets))
@@ -51,10 +53,7 @@ func StartWorkflows(l *slog.Logger, vault secrets.Manager, cfg *config.Config, d
l.Info("using workflow timeout", "timeout", workflowTimeout)
for _, w := range wfs {
- wg.Add(1)
- go func() {
- defer wg.Done()
-
+ wg.Go(func() {
wid := models.WorkflowId{
PipelineId: pipelineId,
Name: w.Name,
@@ -169,7 +168,7 @@ func StartWorkflows(l *slog.Logger, vault secrets.Manager, cfg *config.Config, d
if err != nil {
l.Error("failed to set workflow status to success", "wid", wid, "err", err)
}
- }()
+ })
}
}
diff --git a/spindle/models/clone.go b/spindle/models/clone.go
index 9d1328f0..d9d54389 100644
--- a/spindle/models/clone.go
+++ b/spindle/models/clone.go
@@ -111,16 +111,14 @@ func extractCommitSHA(tr tangled.Pipeline_TriggerMetadata) (string, error) {
// BuildRepoURL constructs the repository URL from repo metadata.
func BuildRepoURL(repo *tangled.Pipeline_TriggerRepo) string {
- if repo == nil {
+ if repo == nil || repo.RepoDid == nil {
return ""
}
-
host, noSSL, _ := hostutil.ParseHostname(repo.Knot)
scheme := "https"
if noSSL {
scheme = "http"
}
-
return fmt.Sprintf("%s://%s/%s", scheme, host, *repo.RepoDid)
}
diff --git a/spindle/models/clone_test.go b/spindle/models/clone_test.go
index 82b676ec..a5feb8c6 100644
--- a/spindle/models/clone_test.go
+++ b/spindle/models/clone_test.go
@@ -83,7 +83,6 @@ func TestBuildCloneStep_PullRequestTrigger(t *testing.T) {
SourceSha: "pr-sha-789",
SourceBranch: "feature-branch",
TargetBranch: "main",
- Action: "opened",
},
Repo: &tangled.Pipeline_TriggerRepo{
Knot: "example.com",
@@ -101,6 +100,41 @@ func TestBuildCloneStep_PullRequestTrigger(t *testing.T) {
}
}
+func TestBuildCloneStep_SourceRepo(t *testing.T) {
+ twf := tangled.Pipeline_Workflow{
+ Clone: &tangled.Pipeline_CloneOpts{
+ Depth: 1,
+ Skip: false,
+ },
+ }
+ sourceRepoDid := "did:plc:fork"
+ tr := tangled.Pipeline_TriggerMetadata{
+ Kind: string(workflow.TriggerKindPullRequest),
+ PullRequest: &tangled.Pipeline_PullRequestTriggerData{
+ SourceSha: "pr-sha-789",
+ SourceBranch: "feature-branch",
+ TargetBranch: "main",
+ },
+ Repo: &tangled.Pipeline_TriggerRepo{
+ Knot: "fork.example.com",
+ Did: "did:plc:user456",
+ Repo: sp("fork-repo"),
+ RepoDid: &sourceRepoDid,
+ },
+ SourceRepo: &sourceRepoDid,
+ }
+
+ step := BuildCloneStep(twf, tr, false)
+
+ allCmds := strings.Join(step.Commands(), " ")
+ if !strings.Contains(allCmds, "https://fork.example.com/did:plc:fork") {
+ t.Error("Commands should clone from source repo URL")
+ }
+ if strings.Contains(allCmds, "https://target.example.com/did:plc:target") {
+ t.Error("Commands should not clone from target repo URL when sourceRepo is set")
+ }
+}
+
func TestBuildCloneStep_ManualTrigger(t *testing.T) {
twf := tangled.Pipeline_Workflow{
Clone: &tangled.Pipeline_CloneOpts{
diff --git a/spindle/models/pipeline.go b/spindle/models/pipeline.go
index fec501dc..a794644b 100644
--- a/spindle/models/pipeline.go
+++ b/spindle/models/pipeline.go
@@ -5,6 +5,8 @@ import "github.com/bluesky-social/indigo/atproto/syntax"
type Pipeline struct {
RepoDid syntax.DID
Workflows map[Engine][]Workflow
+ // whether the code being ran was checked out from RepoDid itself
+ TrustedSource bool
}
type Step interface {
diff --git a/spindle/models/pipeline_env.go b/spindle/models/pipeline_env.go
index e0226aa3..23534395 100644
--- a/spindle/models/pipeline_env.go
+++ b/spindle/models/pipeline_env.go
@@ -10,6 +10,10 @@ import (
// PipelineEnvVars builds the standard CI environment variables for a pipeline
func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId) map[string]string {
+ return PipelineEnvVarsForSource(tr, pipelineId, nil)
+}
+
+func PipelineEnvVarsForSource(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId, sourceRepo *tangled.Pipeline_TriggerRepo) map[string]string {
if tr == nil {
return nil
}
@@ -22,18 +26,28 @@ func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId
env["TANGLED_PIPELINE_ID"] = pipelineId.AtUri().String()
env["TANGLED_PIPELINE_KIND"] = tr.Kind
- // repo info
- if tr.Repo != nil {
- env["TANGLED_REPO_KNOT"] = tr.Repo.Knot
- env["TANGLED_REPO_DID"] = tr.Repo.Did
- if tr.Repo.Repo != nil {
- env["TANGLED_REPO_NAME"] = *tr.Repo.Repo
+ if tr.SourceRepo != nil && *tr.SourceRepo != "" {
+ env["TANGLED_PIPELINE_SOURCE"] = *tr.SourceRepo
+ } else if tr.Repo != nil && tr.Repo.RepoDid != nil {
+ env["TANGLED_PIPELINE_SOURCE"] = *tr.Repo.RepoDid
+ }
+
+ // prefer source repo, e.g. if this is a wf running on a fork
+ repoInfo := tr.Repo
+ if sourceRepo != nil {
+ repoInfo = sourceRepo
+ }
+ if repoInfo != nil {
+ env["TANGLED_REPO_KNOT"] = repoInfo.Knot
+ env["TANGLED_REPO_DID"] = repoInfo.Did
+ if repoInfo.Repo != nil {
+ env["TANGLED_REPO_NAME"] = *repoInfo.Repo
}
- if tr.Repo.RepoDid != nil {
- env["TANGLED_REPO_REPO_DID"] = *tr.Repo.RepoDid
+ if repoInfo.RepoDid != nil {
+ env["TANGLED_REPO_REPO_DID"] = *repoInfo.RepoDid
}
- env["TANGLED_REPO_DEFAULT_BRANCH"] = tr.Repo.DefaultBranch
- env["TANGLED_REPO_URL"] = BuildRepoURL(tr.Repo)
+ env["TANGLED_REPO_DEFAULT_BRANCH"] = repoInfo.DefaultBranch
+ env["TANGLED_REPO_URL"] = BuildRepoURL(repoInfo)
}
switch workflow.TriggerKind(tr.Kind) {
@@ -62,10 +76,11 @@ func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId
env["TANGLED_COMMIT_SHA"] = tr.PullRequest.SourceSha
// PR-specific env vars
+ env["TANGLED_PIPELINE_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch
+ env["TANGLED_PIPELINE_TARGET_BRANCH"] = tr.PullRequest.TargetBranch
env["TANGLED_PR_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch
env["TANGLED_PR_TARGET_BRANCH"] = tr.PullRequest.TargetBranch
env["TANGLED_PR_SOURCE_SHA"] = tr.PullRequest.SourceSha
- env["TANGLED_PR_ACTION"] = tr.PullRequest.Action
}
case workflow.TriggerKindManual:
diff --git a/spindle/models/pipeline_env_test.go b/spindle/models/pipeline_env_test.go
index cbedd769..54fc5f5e 100644
--- a/spindle/models/pipeline_env_test.go
+++ b/spindle/models/pipeline_env_test.go
@@ -110,7 +110,6 @@ func TestPipelineEnvVars_PullRequest(t *testing.T) {
SourceBranch: "feature-branch",
TargetBranch: "main",
SourceSha: "pr-sha-789",
- Action: "opened",
},
Repo: &tangled.Pipeline_TriggerRepo{
Knot: "example.com",
@@ -145,6 +144,12 @@ func TestPipelineEnvVars_PullRequest(t *testing.T) {
}
// Check PR-specific variables
+ if env["TANGLED_PIPELINE_SOURCE_BRANCH"] != "feature-branch" {
+ t.Errorf("Expected TANGLED_PIPELINE_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PIPELINE_SOURCE_BRANCH"])
+ }
+ if env["TANGLED_PIPELINE_TARGET_BRANCH"] != "main" {
+ t.Errorf("Expected TANGLED_PIPELINE_TARGET_BRANCH='main', got '%s'", env["TANGLED_PIPELINE_TARGET_BRANCH"])
+ }
if env["TANGLED_PR_SOURCE_BRANCH"] != "feature-branch" {
t.Errorf("Expected TANGLED_PR_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PR_SOURCE_BRANCH"])
}
@@ -154,8 +159,47 @@ func TestPipelineEnvVars_PullRequest(t *testing.T) {
if env["TANGLED_PR_SOURCE_SHA"] != "pr-sha-789" {
t.Errorf("Expected TANGLED_PR_SOURCE_SHA='pr-sha-789', got '%s'", env["TANGLED_PR_SOURCE_SHA"])
}
- if env["TANGLED_PR_ACTION"] != "opened" {
- t.Errorf("Expected TANGLED_PR_ACTION='opened', got '%s'", env["TANGLED_PR_ACTION"])
+}
+
+func TestPipelineEnvVars_SourceRepo(t *testing.T) {
+ sourceRepoDid := "did:plc:fork"
+ tr := &tangled.Pipeline_TriggerMetadata{
+ Kind: string(workflow.TriggerKindPullRequest),
+ PullRequest: &tangled.Pipeline_PullRequestTriggerData{
+ SourceBranch: "feature-branch",
+ TargetBranch: "main",
+ SourceSha: "pr-sha-789",
+ },
+ Repo: &tangled.Pipeline_TriggerRepo{
+ Knot: "target.example.com",
+ Did: "did:plc:user123",
+ Repo: sp("target-repo"),
+ RepoDid: sp("did:plc:target"),
+ },
+ SourceRepo: &sourceRepoDid,
+ }
+ sourceRepo := &tangled.Pipeline_TriggerRepo{
+ Knot: "fork.example.com",
+ Did: "did:plc:user456",
+ Repo: sp("fork-repo"),
+ RepoDid: &sourceRepoDid,
+ DefaultBranch: "feature-branch",
+ }
+ id := PipelineId{
+ Knot: "target.example.com",
+ Rkey: "123123",
+ }
+
+ env := PipelineEnvVarsForSource(tr, id, sourceRepo)
+
+ if env["TANGLED_PIPELINE_SOURCE"] != sourceRepoDid {
+ t.Errorf("Expected TANGLED_PIPELINE_SOURCE='%s', got '%s'", sourceRepoDid, env["TANGLED_PIPELINE_SOURCE"])
+ }
+ if env["TANGLED_REPO_URL"] != "https://fork.example.com/did:plc:fork" {
+ t.Errorf("Expected TANGLED_REPO_URL to point at source repo, got '%s'", env["TANGLED_REPO_URL"])
+ }
+ if env["TANGLED_REPO_REPO_DID"] != sourceRepoDid {
+ t.Errorf("Expected TANGLED_REPO_REPO_DID='%s', got '%s'", sourceRepoDid, env["TANGLED_REPO_REPO_DID"])
}
}
diff --git a/spindle/server.go b/spindle/server.go
index ad6eda41..e727ebf6 100644
--- a/spindle/server.go
+++ b/spindle/server.go
@@ -28,6 +28,7 @@ import (
"tangled.org/core/log"
"tangled.org/core/notifier"
"tangled.org/core/rbac"
+ "tangled.org/core/repoverify"
"tangled.org/core/spindle/config"
"tangled.org/core/spindle/db"
"tangled.org/core/spindle/engine"
@@ -64,6 +65,7 @@ type Spindle struct {
cfg *config.Config
ks *eventconsumer.Consumer
res *idresolver.Resolver
+ verify repoverify.Verifier
vault secrets.Manager
motd []byte
motdMu sync.RWMutex
@@ -156,6 +158,7 @@ func New(ctx context.Context, cfg *config.Config, d *db.DB, engines map[string]m
jq: jq,
cfg: cfg,
res: resolver,
+ verify: repoverify.New(resolver, cfg.Server.Dev),
vault: vault,
motd: defaultMotd,
rootCtx: ctx,
@@ -447,7 +450,7 @@ func (s *Spindle) processKnotStream(ctx context.Context, src eventconsumer.Sourc
Repo: triggerRepo,
}
- pipelineId, err := s.runPipeline(ctx, repoDid, trigger, event.ChangedFiles, repoCloneUri, repoPath, event.NewSha, nil)
+ pipelineId, err := s.runPipeline(ctx, repoDid, trigger, event.ChangedFiles, repoCloneUri, repoPath, event.NewSha, nil, triggerRepo)
if err != nil {
return err
}
@@ -463,31 +466,70 @@ func (s *Spindle) processKnotStream(ctx context.Context, src eventconsumer.Sourc
// buildTriggerRepo gathers trigger metadata, resolving default branch from the knot
func (s *Spindle) buildTriggerRepo(ctx context.Context, repo *db.Repo) (*tangled.Pipeline_TriggerRepo, error) {
+ rkey := string(repo.Rkey)
+ repoDid := repo.RepoDid.String()
+ return s.buildTriggerRepoFrom(ctx, repo.Knot, repo.Owner.String(), rkey, repoDid), nil
+}
+
+func (s *Spindle) buildTriggerRepoFrom(ctx context.Context, knot, did, rkey, repoDid string) *tangled.Pipeline_TriggerRepo {
scheme := "https"
if s.cfg.Server.Dev {
scheme = "http"
}
- client := &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, repo.Knot)}
+ client := &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, knot)}
- // todo(dawn): this should be in the refUpdate event itself to save a roundtrip
+ // this should maybe (?) be in the refUpdate event itself to save a roundtrip
defaultBranch := ""
- if out, err := tangled.RepoGetDefaultBranch(ctx, client, repo.RepoDid.String()); err == nil {
+ if out, err := tangled.RepoGetDefaultBranch(ctx, client, repoDid); err == nil {
defaultBranch = out.Name
}
- rkey := string(repo.Rkey)
- repoDid := repo.RepoDid.String()
+ var rkeyPtr *string
+ if rkey != "" {
+ rkeyPtr = &rkey
+ }
return &tangled.Pipeline_TriggerRepo{
- Did: repo.Owner.String(),
- Knot: repo.Knot,
- Repo: &rkey,
+ Did: did,
+ Knot: knot,
+ Repo: rkeyPtr,
RepoDid: &repoDid,
DefaultBranch: defaultBranch,
- }, nil
+ }
+}
+
+func (s *Spindle) resolvePipelineSourceRepo(ctx context.Context, trigger *tangled.Pipeline_TriggerMetadata) (*tangled.Pipeline_TriggerRepo, error) {
+ if trigger == nil {
+ return nil, nil
+ }
+ if trigger.SourceRepo == nil || *trigger.SourceRepo == "" {
+ return trigger.Repo, nil
+ }
+ repoDid, err := syntax.ParseDID(*trigger.SourceRepo)
+ if err != nil {
+ return nil, fmt.Errorf("parse sourceRepo %s: %w", *trigger.SourceRepo, err)
+ }
+ return s.resolveSourceRepoInfo(ctx, repoDid)
}
-// runPipeline compiles and enqueues the pipeline for the given revision
-func (s *Spindle) runPipeline(ctx context.Context, repoDid syntax.DID, trigger tangled.Pipeline_TriggerMetadata, changedFiles []string, repoCloneUri, repoPath, rev string, only []string) (models.PipelineId, error) {
+// resolveSourceRepoInfo resolves trigger-repo metadata for a source repo DID.
+func (s *Spindle) resolveSourceRepoInfo(ctx context.Context, repoDid syntax.DID) (*tangled.Pipeline_TriggerRepo, error) {
+ repo, err := s.db.GetRepoByDid(repoDid)
+ if err == nil {
+ return s.buildTriggerRepo(ctx, repo)
+ }
+
+ // verify repo, we don't want git sync to point to arbitrary endpoints
+ res, err := s.verify(ctx, repoverify.RepoDid(repoDid))
+ if err != nil {
+ return nil, fmt.Errorf("verify sourceRepo %s: %w", repoDid, err)
+ }
+ return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host, res.OwnerDid.String(), res.Rkey, repoDid.String()), nil
+}
+
+// runPipeline compiles and enqueues the pipeline for the given revision.
+// sourceRepo is the resolved repo the code was checked out from, forwarded to
+// processPipeline for env vars.
+func (s *Spindle) runPipeline(ctx context.Context, repoDid syntax.DID, trigger tangled.Pipeline_TriggerMetadata, changedFiles []string, repoCloneUri, repoPath, rev string, only []string, sourceRepo *tangled.Pipeline_TriggerRepo) (models.PipelineId, error) {
l := log.FromContext(ctx)
compiler := workflow.Compiler{
@@ -526,7 +568,7 @@ func (s *Spindle) runPipeline(ctx context.Context, repoDid syntax.DID, trigger t
if err := s.db.CreatePipelineEvent(pipelineId.Rkey, tpl, s.n); err != nil {
return models.PipelineId{}, fmt.Errorf("creating pipeline event: %w", err)
}
- err = s.processPipeline(repoDid, tpl, pipelineId)
+ err = s.processPipeline(repoDid, tpl, pipelineId, sourceRepo)
return pipelineId, err
}
@@ -548,8 +590,9 @@ func filterWorkflows(workflows []*tangled.Pipeline_Workflow, only []string) []*t
return filtered
}
-// TriggerManual dispatches a pipeline manually at sha
-func (s *Spindle) TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string) (syntax.ATURI, error) {
+// TriggerManual dispatches a pipeline at sha, authorized against and recorded
+// under repoDid. sourceRepo, pull, and inputs are optional trigger payload.
+func (s *Spindle) TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string, sourceRepo syntax.DID, pull xrpc.PullContext, inputs []*tangled.Pipeline_Pair) (syntax.ATURI, error) {
repo, err := s.db.GetRepoByDid(repoDid)
if err != nil {
return "", fmt.Errorf("unknown repoDid %s: %w", repoDid, err)
@@ -560,23 +603,48 @@ func (s *Spindle) TriggerManual(ctx context.Context, repoDid syntax.DID, sha, re
return "", fmt.Errorf("building trigger repo: %w", err)
}
- var refPtr *string
- if ref != "" {
- refPtr = &ref
- }
- trigger := tangled.Pipeline_TriggerMetadata{
- Kind: string(workflow.TriggerKindManual),
- Manual: &tangled.Pipeline_ManualTriggerData{
- Sha: sha,
- Ref: refPtr,
- },
- Repo: triggerRepo,
+ trigger := tangled.Pipeline_TriggerMetadata{Repo: triggerRepo}
+ if pull.IsPullRequest {
+ var pullAt *string
+ if pull.Pull != "" {
+ pullAtStr := pull.Pull.String()
+ pullAt = &pullAtStr
+ }
+ trigger.Kind = string(workflow.TriggerKindPullRequest)
+ trigger.PullRequest = &tangled.Pipeline_PullRequestTriggerData{
+ SourceBranch: pull.SourceBranch,
+ TargetBranch: pull.TargetBranch,
+ SourceSha: sha,
+ Pull: pullAt,
+ }
+ } else {
+ var refPtr *string
+ if ref != "" {
+ refPtr = &ref
+ }
+ trigger.Kind = string(workflow.TriggerKindManual)
+ trigger.Manual = &tangled.Pipeline_ManualTriggerData{
+ Sha: sha,
+ Ref: refPtr,
+ Inputs: inputs,
+ }
}
repoCloneUri := s.newRepoCloneUrl(repo.Knot, repoDid)
repoPath := s.newRepoPath(repoDid)
+ sourceInfo := triggerRepo // default: code comes from the repo itself
+ if sourceRepo != "" && sourceRepo != repoDid {
+ sourceInfo, err = s.resolveSourceRepoInfo(ctx, sourceRepo)
+ if err != nil {
+ return "", err
+ }
+ sourceRepoStr := sourceRepo.String()
+ trigger.SourceRepo = &sourceRepoStr
+ repoCloneUri = models.BuildRepoURL(sourceInfo)
+ repoPath = s.newRepoPath(sourceRepo)
+ }
- pipelineId, err := s.runPipeline(ctx, repoDid, trigger, nil, repoCloneUri, repoPath, sha, workflows)
+ pipelineId, err := s.runPipeline(ctx, repoDid, trigger, nil, repoCloneUri, repoPath, sha, workflows, sourceInfo)
if err != nil {
return "", err
}
@@ -624,9 +692,26 @@ func (s *Spindle) loadPipeline(ctx context.Context, repoUri, repoPath, rev strin
return rawPipeline, nil
}
-func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipelineId models.PipelineId) error {
- // Build pipeline environment variables once for all workflows
- pipelineEnv := models.PipelineEnvVars(tpl.TriggerMetadata, pipelineId)
+// processPipeline enqueues the workflows in tpl.
+func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipelineId models.PipelineId, sourceRepo *tangled.Pipeline_TriggerRepo) error {
+ // derive security-relevant things like whether this run is trusted and can be passed
+ // secrets to from the original metadata.
+ pipelineEnv := models.PipelineEnvVarsForSource(tpl.TriggerMetadata, pipelineId, sourceRepo)
+ trustedSource := true
+ if tm := tpl.TriggerMetadata; tm != nil && tm.SourceRepo != nil &&
+ *tm.SourceRepo != "" && *tm.SourceRepo != repoDid.String() {
+ trustedSource = false
+ }
+
+ // swap the repo with our sourceRepo if we are running a pipeline on a fork.
+ // the metadata stays the same. we check whether the repo is trusted above,
+ // so this only affects the clone URL.
+ initTpl := tpl
+ if sourceRepo != nil && tpl.TriggerMetadata != nil {
+ tm := *tpl.TriggerMetadata
+ tm.Repo = sourceRepo
+ initTpl.TriggerMetadata = &tm
+ }
// filter & init workflows
workflows := make(map[models.Engine][]models.Workflow)
@@ -647,7 +732,7 @@ func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipe
continue
}
- ewf, err := eng.InitWorkflow(*w, tpl)
+ ewf, err := eng.InitWorkflow(*w, initTpl)
if err != nil {
err = s.db.StatusFailed(models.WorkflowId{
PipelineId: pipelineId,
@@ -674,8 +759,9 @@ func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipe
ok := s.jq.Enqueue(repoDid, queue.Job{
Run: func() error {
engine.StartWorkflows(log.SubLogger(s.l, "engine"), s.vault, s.cfg, s.db, s.n, s.rootCtx, &models.Pipeline{
- RepoDid: repoDid,
- Workflows: workflows,
+ RepoDid: repoDid,
+ Workflows: workflows,
+ TrustedSource: trustedSource,
}, pipelineId)
return nil
},
diff --git a/spindle/tapclient.go b/spindle/tapclient.go
index 2a313895..6060d2ca 100644
--- a/spindle/tapclient.go
+++ b/spindle/tapclient.go
@@ -383,7 +383,6 @@ func (t *Tap) processPull(ctx context.Context, evt *tapc.RecordEventData) error
Trigger: tangled.Pipeline_TriggerMetadata{
Kind: string(workflow.TriggerKindPullRequest),
PullRequest: &tangled.Pipeline_PullRequestTriggerData{
- Action: "create",
SourceBranch: record.Source.Branch,
SourceSha: sourceSha,
TargetBranch: record.Target.Branch,
@@ -433,7 +432,12 @@ func (t *Tap) processPull(ctx context.Context, evt *tapc.RecordEventData) error
l.Error("failed to create pipeline event", "err", err)
return nil
}
- err = t.spindle.processPipeline(repo.RepoDid, tpl, pipelineId)
+ sourceRepo, err := t.spindle.resolvePipelineSourceRepo(ctx, tpl.TriggerMetadata)
+ if err != nil {
+ l.Error("failed resolving pipeline source repo", "err", err)
+ return nil
+ }
+ err = t.spindle.processPipeline(repo.RepoDid, tpl, pipelineId, sourceRepo)
if err != nil {
// don't retry
l.Error("failed processing pipeline", "err", err)
diff --git a/spindle/xrpc/ci_pipeline_trigger_pipeline.go b/spindle/xrpc/ci_pipeline_trigger_pipeline.go
index 669c3180..c31f4d59 100644
--- a/spindle/xrpc/ci_pipeline_trigger_pipeline.go
+++ b/spindle/xrpc/ci_pipeline_trigger_pipeline.go
@@ -7,9 +7,7 @@ import (
"fmt"
"net/http"
- "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"
- "github.com/bluesky-social/indigo/xrpc"
"tangled.org/core/api/tangled"
"tangled.org/core/rbac"
@@ -36,23 +34,83 @@ func (x *Xrpc) TriggerPipeline(w http.ResponseWriter, r *http.Request) {
return
}
- if len(input.Sha) != 40 {
- fail(xrpcerr.GenericError(fmt.Errorf("sha must be a 40-character commit hash")))
- return
- }
-
repoDid, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo)
if !ok {
fail(xerr)
return
}
+ var sha string
ref := ""
- if input.Ref != nil {
- ref = *input.Ref
+ var sourceRepo syntax.DID
+ var pull PullContext
+ var inputs []*tangled.Pipeline_Pair
+
+ switch {
+ case input.Trigger == nil:
+ fail(xrpcerr.GenericError(fmt.Errorf("trigger is required")))
+ return
+
+ case input.Trigger.CiTrigger_Manual != nil:
+ manual := input.Trigger.CiTrigger_Manual
+ sha = manual.Sha
+ if manual.Ref != nil {
+ ref = *manual.Ref
+ }
+ parsed, err := parseOptionalDID("sourceRepo", manual.SourceRepo)
+ if err != nil {
+ fail(xrpcerr.GenericError(err))
+ return
+ }
+ sourceRepo = parsed
+ inputs = ciTriggerPairsToPipelinePairs(manual.Inputs)
+
+ case input.Trigger.CiTrigger_PullRequest != nil:
+ pr := input.Trigger.CiTrigger_PullRequest
+ sha = pr.SourceSha
+ parsed, err := parseOptionalDID("sourceRepo", pr.SourceRepo)
+ if err != nil {
+ fail(xrpcerr.GenericError(err))
+ return
+ }
+ sourceRepo = parsed
+
+ if pr.TargetBranch == "" {
+ fail(xrpcerr.GenericError(fmt.Errorf("pull request trigger targetBranch is required")))
+ return
+ }
+
+ var pullAt syntax.ATURI
+ if pr.Pull != nil {
+ var err error
+ pullAt, err = syntax.ParseATURI(*pr.Pull)
+ if err != nil {
+ fail(xrpcerr.InvalidRepoError(*pr.Pull))
+ return
+ }
+ }
+ sourceBranch := ""
+ if pr.SourceBranch != nil {
+ sourceBranch = *pr.SourceBranch
+ }
+ pull = PullContext{
+ IsPullRequest: true,
+ Pull: pullAt,
+ SourceBranch: sourceBranch,
+ TargetBranch: pr.TargetBranch,
+ }
+
+ default:
+ fail(xrpcerr.GenericError(fmt.Errorf("unsupported trigger variant")))
+ return
}
- pipelineAt, err := x.Trigger.TriggerManual(r.Context(), repoDid, input.Sha, ref, input.Workflows)
+ if len(sha) != 40 {
+ fail(xrpcerr.GenericError(fmt.Errorf("sha must be a 40-character commit hash")))
+ return
+ }
+
+ pipelineAt, err := x.Trigger.TriggerManual(r.Context(), repoDid, sha, ref, input.Workflows, sourceRepo, pull, inputs)
if errors.Is(err, ErrNoMatchingWorkflows) {
fail(xrpcerr.GenericError(err))
return
@@ -69,37 +127,58 @@ func (x *Xrpc) TriggerPipeline(w http.ResponseWriter, r *http.Request) {
}
}
-// resolveOwnedRepo resolves a repo AT-URI to DID and checks owner auth
-func (x *Xrpc) resolveOwnedRepo(ctx context.Context, actorDid syntax.DID, repoAtUri string) (syntax.DID, xrpcerr.XrpcError, bool) {
- repoAt, err := syntax.ParseATURI(repoAtUri)
+func parseOptionalDID(field string, value *string) (syntax.DID, error) {
+ if value == nil || *value == "" {
+ return "", nil
+ }
+ did, err := syntax.ParseDID(*value)
if err != nil {
- return "", xrpcerr.InvalidRepoError(repoAtUri), false
+ return "", fmt.Errorf("invalid %s DID %q: %w", field, *value, err)
}
+ return did, nil
+}
- ident, err := x.Resolver.ResolveIdent(ctx, repoAt.Authority().String())
- if err != nil || ident.Handle.IsInvalidHandle() {
- return "", xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)), false
+func ciTriggerPairsToPipelinePairs(inputs []*tangled.CiTrigger_Pair) []*tangled.Pipeline_Pair {
+ if len(inputs) == 0 {
+ return nil
}
-
- xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
- resp, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
- if err != nil {
- return "", xrpcerr.GenericError(err), false
+ pairs := make([]*tangled.Pipeline_Pair, 0, len(inputs))
+ for _, input := range inputs {
+ if input == nil {
+ continue
+ }
+ pairs = append(pairs, &tangled.Pipeline_Pair{
+ Key: input.Key,
+ Value: input.Value,
+ })
}
+ return pairs
+}
- repoRec, ok := resp.Value.Val.(*tangled.Repo)
+// resolveOwnedRepo resolves a repository DID and checks push auth.
+func (x *Xrpc) resolveOwnedRepo(ctx context.Context, actorDid syntax.DID, repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) {
+ repoDid, xerr, ok := x.resolveKnownRepoDid(repoDidStr)
if !ok {
- return "", xrpcerr.RepoNotFoundError, false
+ return "", xerr, false
}
- if repoRec.RepoDid == nil || *repoRec.RepoDid == "" {
- return "", xrpcerr.GenericError(fmt.Errorf("repo record %s has no repoDid", repoAt)), false
- }
- repoDid := *repoRec.RepoDid
- isPushAllowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid)
+ isPushAllowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid.String())
if err != nil || !isPushAllowed {
return "", xrpcerr.AccessControlError(actorDid.String()), false
}
- return syntax.DID(repoDid), xrpcerr.XrpcError{}, true
+ return repoDid, xrpcerr.XrpcError{}, true
+}
+
+func (x *Xrpc) resolveKnownRepoDid(repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) {
+ repoDid, err := syntax.ParseDID(repoDidStr)
+ if err != nil {
+ return "", xrpcerr.GenericError(fmt.Errorf("invalid repo DID %q: %w", repoDidStr, err)), false
+ }
+
+ if _, err := x.Db.GetRepoByDid(repoDid); err != nil {
+ return "", xrpcerr.RepoNotFoundError, false
+ }
+
+ return repoDid, xrpcerr.XrpcError{}, true
}
diff --git a/spindle/xrpc/pipeline_cancel_pipeline.go b/spindle/xrpc/pipeline_cancel_pipeline.go
index f74ef35d..0238c946 100644
--- a/spindle/xrpc/pipeline_cancel_pipeline.go
+++ b/spindle/xrpc/pipeline_cancel_pipeline.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
- "strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/tangled"
@@ -26,38 +25,54 @@ func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) {
return
}
- var input tangled.CiPipelineCancelPipeline_Input
+ var input tangled.CiCancelPipeline_Input
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
fail(xrpcerr.GenericError(err))
return
}
- aturi := syntax.ATURI(input.Pipeline)
+ pipelineTid, err := syntax.ParseTID(input.Pipeline)
+ if err != nil {
+ fail(xrpcerr.GenericError(fmt.Errorf("invalid pipeline TID %q: %w", input.Pipeline, err)))
+ return
+ }
+
+ repoDid, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo)
+ if !ok {
+ fail(xerr)
+ return
+ }
+ repo, err := x.Db.GetRepoByDid(repoDid)
+ if err != nil {
+ fail(xrpcerr.GenericError(fmt.Errorf("failed to get repo: %w", err)))
+ return
+ }
+
+ // the actor is only authorized against input.Repo, so make sure the
+ // pipeline actually belongs to it before cancelling anything
+ p, err := x.Db.GetPipeline(r.Context(), pipelineTid.String())
+ if err != nil {
+ fail(xrpcerr.GenericError(fmt.Errorf("failed to get pipeline: %w", err)))
+ return
+ }
+ if p.Repo == nil || *p.Repo != repoDid.String() {
+ fail(xrpcerr.AccessControlError(actorDid.String()))
+ return
+ }
+
pipelineId := models.PipelineId{
- Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"),
- Rkey: aturi.RecordKey().String(),
+ Knot: repo.Knot,
+ Rkey: pipelineTid.String(),
}
- var workflows []string
- if len(input.Workflows) > 0 {
- workflows = input.Workflows
- } else {
- // fetch workflows from db if none are specified
- p, err := x.Db.GetPipeline(r.Context(), pipelineId.Rkey)
- if err != nil {
- fail(xrpcerr.GenericError(fmt.Errorf("failed to get pipeline: %w", err)))
- return
- }
+ workflows := input.Workflows
+ if len(workflows) == 0 {
+ // cancel every workflow when none are specified
for _, w := range p.Workflows {
workflows = append(workflows, w.Name)
}
}
- if _, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo); !ok {
- fail(xerr)
- return
- }
-
for _, wName := range workflows {
wid := models.WorkflowId{
PipelineId: pipelineId,
diff --git a/spindle/xrpc/xrpc.go b/spindle/xrpc/xrpc.go
index 11bcff93..d8232be4 100644
--- a/spindle/xrpc/xrpc.go
+++ b/spindle/xrpc/xrpc.go
@@ -25,16 +25,19 @@ import (
const ActorDid = serviceauth.ActorDid
-// ErrNoMatchingWorkflows is returned when a manual dispatch resolves to no
-// workflows to run: the repo defines none at the requested commit, or none of
-// the requested workflow names exist.
var ErrNoMatchingWorkflows = errors.New("no workflows to run")
-// PipelineTrigger builds and enqueues a manually-dispatched pipeline. It is
-// implemented by *spindle.Spindle, which owns the queue and engines; the xrpc
-// handler only does auth and input validation before delegating here.
+// this is to break an import cycle. spindle imports this package for Xrpc,
+// so this package can't import *spindle.Spindle back.
type PipelineTrigger interface {
- TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string) (syntax.ATURI, error)
+ TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string, sourceRepo syntax.DID, pull PullContext, inputs []*tangled.Pipeline_Pair) (syntax.ATURI, error)
+}
+
+type PullContext struct {
+ IsPullRequest bool
+ Pull syntax.ATURI
+ SourceBranch string
+ TargetBranch string
}
type Xrpc struct {
diff --git a/types/pipeline.go b/types/pipeline.go
index 68e0cfb3..3f9ecdf6 100644
--- a/types/pipeline.go
+++ b/types/pipeline.go
@@ -106,11 +106,11 @@ func (t Trigger) TargetRef() string {
}
if t.CiPipeline_Trigger.CiTrigger_Push != nil {
ref := t.CiPipeline_Trigger.CiTrigger_Push.Ref
- if strings.HasPrefix(ref, "refs/heads/") {
- return strings.TrimPrefix(ref, "refs/heads/")
+ if after, ok := strings.CutPrefix(ref, "refs/heads/"); ok {
+ return after
}
- if strings.HasPrefix(ref, "refs/tags/") {
- return strings.TrimPrefix(ref, "refs/tags/")
+ if after, ok := strings.CutPrefix(ref, "refs/tags/"); ok {
+ return after
}
return ref
}
@@ -131,6 +131,17 @@ func (t Trigger) PRSourceBranch() string {
return *sb
}
+func (t Trigger) PRUri() string {
+ if t.CiPipeline_Trigger == nil || t.CiPipeline_Trigger.CiTrigger_PullRequest == nil {
+ return ""
+ }
+ pull := t.CiPipeline_Trigger.CiTrigger_PullRequest.Pull
+ if pull == nil {
+ return ""
+ }
+ return *pull
+}
+
type Pipeline struct {
*tangled.CiPipeline
}
@@ -242,6 +253,14 @@ func (p Pipeline) Sha() string {
return p.CiPipeline.Commit
}
+// where the pipeline commit was checked out from, nil when checked out from repo itself
+func (p Pipeline) SourceRepo() *string {
+ if p.CiPipeline == nil {
+ return nil
+ }
+ return p.CiPipeline.SourceRepo
+}
+
func (p Pipeline) Workflows() []string {
var ws []string
if p.CiPipeline != nil {
diff --git a/appview/xrpcclient/xrpc.go b/xrpc/xrpcclient/xrpc.go
similarity index 100%
rename from appview/xrpcclient/xrpc.go
rename to xrpc/xrpcclient/xrpc.go