Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview,spindle,lexicons: support triggering ci for fork-based pull requests

Signed-off-by: dawn <dawn@tangled.org>

authored by

dawn and committed by
Tangled
(Jul 4, 2026, 8:12 AM +0300) b4261269 dd7a27b1

+1635 -308
+640 -57
api/tangled/cbor_gen.go
··· 669 669 } 670 670 671 671 cw := cbg.NewCborWriter(w) 672 - fieldCount := 6 672 + fieldCount := 7 673 673 674 674 if t.CreatedAt == nil { 675 675 fieldCount-- 676 676 } 677 677 678 678 if t.Repo == nil { 679 + fieldCount-- 680 + } 681 + 682 + if t.SourceRepo == nil { 679 683 fieldCount-- 680 684 } 681 685 ··· 834 838 } 835 839 836 840 } 841 + 842 + // t.SourceRepo (string) (string) 843 + if t.SourceRepo != nil { 844 + 845 + if len("sourceRepo") > 1000000 { 846 + return xerrors.Errorf("Value in field \"sourceRepo\" was too long") 847 + } 848 + 849 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil { 850 + return err 851 + } 852 + if _, err := cw.WriteString(string("sourceRepo")); err != nil { 853 + return err 854 + } 855 + 856 + if t.SourceRepo == nil { 857 + if _, err := cw.Write(cbg.CborNull); err != nil { 858 + return err 859 + } 860 + } else { 861 + if len(*t.SourceRepo) > 1000000 { 862 + return xerrors.Errorf("Value in field t.SourceRepo was too long") 863 + } 864 + 865 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil { 866 + return err 867 + } 868 + if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil { 869 + return err 870 + } 871 + } 872 + } 837 873 return nil 838 874 } 839 875 ··· 862 898 863 899 n := extra 864 900 865 - nameBuf := make([]byte, 9) 901 + nameBuf := make([]byte, 10) 866 902 for i := uint64(0); i < n; i++ { 867 903 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 868 904 if err != nil { ··· 1009 1045 1010 1046 } 1011 1047 1048 + } 1049 + } 1050 + // t.SourceRepo (string) (string) 1051 + case "sourceRepo": 1052 + 1053 + { 1054 + b, err := cr.ReadByte() 1055 + if err != nil { 1056 + return err 1057 + } 1058 + if b != cbg.CborNull[0] { 1059 + if err := cr.UnreadByte(); err != nil { 1060 + return err 1061 + } 1062 + 1063 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1064 + if err != nil { 1065 + return err 1066 + } 1067 + 1068 + t.SourceRepo = (*string)(&sval) 1012 1069 } 1013 1070 } 1014 1071 ··· 2181 2238 } 2182 2239 2183 2240 cw := cbg.NewCborWriter(w) 2241 + fieldCount := 5 2184 2242 2185 - if _, err := cw.Write([]byte{161}); err != nil { 2243 + if t.Inputs == nil { 2244 + fieldCount-- 2245 + } 2246 + 2247 + if t.Ref == nil { 2248 + fieldCount-- 2249 + } 2250 + 2251 + if t.SourceRepo == nil { 2252 + fieldCount-- 2253 + } 2254 + 2255 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2256 + return err 2257 + } 2258 + 2259 + // t.Ref (string) (string) 2260 + if t.Ref != nil { 2261 + 2262 + if len("ref") > 1000000 { 2263 + return xerrors.Errorf("Value in field \"ref\" was too long") 2264 + } 2265 + 2266 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("ref"))); err != nil { 2267 + return err 2268 + } 2269 + if _, err := cw.WriteString(string("ref")); err != nil { 2270 + return err 2271 + } 2272 + 2273 + if t.Ref == nil { 2274 + if _, err := cw.Write(cbg.CborNull); err != nil { 2275 + return err 2276 + } 2277 + } else { 2278 + if len(*t.Ref) > 1000000 { 2279 + return xerrors.Errorf("Value in field t.Ref was too long") 2280 + } 2281 + 2282 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Ref))); err != nil { 2283 + return err 2284 + } 2285 + if _, err := cw.WriteString(string(*t.Ref)); err != nil { 2286 + return err 2287 + } 2288 + } 2289 + } 2290 + 2291 + // t.Sha (string) (string) 2292 + if len("sha") > 1000000 { 2293 + return xerrors.Errorf("Value in field \"sha\" was too long") 2294 + } 2295 + 2296 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sha"))); err != nil { 2297 + return err 2298 + } 2299 + if _, err := cw.WriteString(string("sha")); err != nil { 2300 + return err 2301 + } 2302 + 2303 + if len(t.Sha) > 1000000 { 2304 + return xerrors.Errorf("Value in field t.Sha was too long") 2305 + } 2306 + 2307 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Sha))); err != nil { 2308 + return err 2309 + } 2310 + if _, err := cw.WriteString(string(t.Sha)); err != nil { 2186 2311 return err 2187 2312 } 2188 2313 ··· 2204 2329 if _, err := cw.WriteString(string("sh.tangled.ci.trigger#manual")); err != nil { 2205 2330 return err 2206 2331 } 2332 + 2333 + // t.Inputs ([]*tangled.CiTrigger_Pair) (slice) 2334 + if t.Inputs != nil { 2335 + 2336 + if len("inputs") > 1000000 { 2337 + return xerrors.Errorf("Value in field \"inputs\" was too long") 2338 + } 2339 + 2340 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inputs"))); err != nil { 2341 + return err 2342 + } 2343 + if _, err := cw.WriteString(string("inputs")); err != nil { 2344 + return err 2345 + } 2346 + 2347 + if len(t.Inputs) > 8192 { 2348 + return xerrors.Errorf("Slice value in field t.Inputs was too long") 2349 + } 2350 + 2351 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Inputs))); err != nil { 2352 + return err 2353 + } 2354 + for _, v := range t.Inputs { 2355 + if err := v.MarshalCBOR(cw); err != nil { 2356 + return err 2357 + } 2358 + 2359 + } 2360 + } 2361 + 2362 + // t.SourceRepo (string) (string) 2363 + if t.SourceRepo != nil { 2364 + 2365 + if len("sourceRepo") > 1000000 { 2366 + return xerrors.Errorf("Value in field \"sourceRepo\" was too long") 2367 + } 2368 + 2369 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil { 2370 + return err 2371 + } 2372 + if _, err := cw.WriteString(string("sourceRepo")); err != nil { 2373 + return err 2374 + } 2375 + 2376 + if t.SourceRepo == nil { 2377 + if _, err := cw.Write(cbg.CborNull); err != nil { 2378 + return err 2379 + } 2380 + } else { 2381 + if len(*t.SourceRepo) > 1000000 { 2382 + return xerrors.Errorf("Value in field t.SourceRepo was too long") 2383 + } 2384 + 2385 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil { 2386 + return err 2387 + } 2388 + if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil { 2389 + return err 2390 + } 2391 + } 2392 + } 2207 2393 return nil 2208 2394 } 2209 2395 ··· 2232 2418 2233 2419 n := extra 2234 2420 2235 - nameBuf := make([]byte, 5) 2421 + nameBuf := make([]byte, 10) 2236 2422 for i := uint64(0); i < n; i++ { 2237 2423 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2238 2424 if err != nil { ··· 2248 2434 } 2249 2435 2250 2436 switch string(nameBuf[:nameLen]) { 2251 - // t.LexiconTypeID (string) (string) 2437 + // t.Ref (string) (string) 2438 + case "ref": 2439 + 2440 + { 2441 + b, err := cr.ReadByte() 2442 + if err != nil { 2443 + return err 2444 + } 2445 + if b != cbg.CborNull[0] { 2446 + if err := cr.UnreadByte(); err != nil { 2447 + return err 2448 + } 2449 + 2450 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2451 + if err != nil { 2452 + return err 2453 + } 2454 + 2455 + t.Ref = (*string)(&sval) 2456 + } 2457 + } 2458 + // t.Sha (string) (string) 2459 + case "sha": 2460 + 2461 + { 2462 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2463 + if err != nil { 2464 + return err 2465 + } 2466 + 2467 + t.Sha = string(sval) 2468 + } 2469 + // t.LexiconTypeID (string) (string) 2252 2470 case "$type": 2253 2471 2254 2472 { ··· 2259 2477 2260 2478 t.LexiconTypeID = string(sval) 2261 2479 } 2480 + // t.Inputs ([]*tangled.CiTrigger_Pair) (slice) 2481 + case "inputs": 2482 + 2483 + maj, extra, err = cr.ReadHeader() 2484 + if err != nil { 2485 + return err 2486 + } 2487 + 2488 + if extra > 8192 { 2489 + return fmt.Errorf("t.Inputs: array too large (%d)", extra) 2490 + } 2491 + 2492 + if maj != cbg.MajArray { 2493 + return fmt.Errorf("expected cbor array") 2494 + } 2495 + 2496 + if extra > 0 { 2497 + t.Inputs = make([]*CiTrigger_Pair, extra) 2498 + } 2499 + 2500 + for i := 0; i < int(extra); i++ { 2501 + { 2502 + var maj byte 2503 + var extra uint64 2504 + var err error 2505 + _ = maj 2506 + _ = extra 2507 + _ = err 2508 + 2509 + { 2510 + 2511 + b, err := cr.ReadByte() 2512 + if err != nil { 2513 + return err 2514 + } 2515 + if b != cbg.CborNull[0] { 2516 + if err := cr.UnreadByte(); err != nil { 2517 + return err 2518 + } 2519 + t.Inputs[i] = new(CiTrigger_Pair) 2520 + if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil { 2521 + return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err) 2522 + } 2523 + } 2524 + 2525 + } 2526 + 2527 + } 2528 + } 2529 + // t.SourceRepo (string) (string) 2530 + case "sourceRepo": 2531 + 2532 + { 2533 + b, err := cr.ReadByte() 2534 + if err != nil { 2535 + return err 2536 + } 2537 + if b != cbg.CborNull[0] { 2538 + if err := cr.UnreadByte(); err != nil { 2539 + return err 2540 + } 2541 + 2542 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2543 + if err != nil { 2544 + return err 2545 + } 2546 + 2547 + t.SourceRepo = (*string)(&sval) 2548 + } 2549 + } 2262 2550 2263 2551 default: 2264 2552 // Field doesn't exist on this type, so ignore it ··· 2270 2558 2271 2559 return nil 2272 2560 } 2273 - func (t *CiTrigger_PullRequest) MarshalCBOR(w io.Writer) error { 2561 + func (t *CiTrigger_Pair) MarshalCBOR(w io.Writer) error { 2274 2562 if t == nil { 2275 2563 _, err := w.Write(cbg.CborNull) 2276 2564 return err 2277 2565 } 2278 2566 2279 2567 cw := cbg.NewCborWriter(w) 2280 - fieldCount := 5 2568 + 2569 + if _, err := cw.Write([]byte{162}); err != nil { 2570 + return err 2571 + } 2572 + 2573 + // t.Key (string) (string) 2574 + if len("key") > 1000000 { 2575 + return xerrors.Errorf("Value in field \"key\" was too long") 2576 + } 2577 + 2578 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil { 2579 + return err 2580 + } 2581 + if _, err := cw.WriteString(string("key")); err != nil { 2582 + return err 2583 + } 2281 2584 2282 - if t.SourceBranch == nil { 2283 - fieldCount-- 2585 + if len(t.Key) > 1000000 { 2586 + return xerrors.Errorf("Value in field t.Key was too long") 2284 2587 } 2285 2588 2286 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2589 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil { 2590 + return err 2591 + } 2592 + if _, err := cw.WriteString(string(t.Key)); err != nil { 2287 2593 return err 2288 2594 } 2289 2595 2290 - // t.LexiconTypeID (string) (string) 2291 - if len("$type") > 1000000 { 2292 - return xerrors.Errorf("Value in field \"$type\" was too long") 2596 + // t.Value (string) (string) 2597 + if len("value") > 1000000 { 2598 + return xerrors.Errorf("Value in field \"value\" was too long") 2293 2599 } 2294 2600 2295 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 2601 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("value"))); err != nil { 2296 2602 return err 2297 2603 } 2298 - if _, err := cw.WriteString(string("$type")); err != nil { 2604 + if _, err := cw.WriteString(string("value")); err != nil { 2299 2605 return err 2300 2606 } 2301 2607 2302 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.ci.trigger#pullRequest"))); err != nil { 2608 + if len(t.Value) > 1000000 { 2609 + return xerrors.Errorf("Value in field t.Value was too long") 2610 + } 2611 + 2612 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Value))); err != nil { 2303 2613 return err 2304 2614 } 2305 - if _, err := cw.WriteString(string("sh.tangled.ci.trigger#pullRequest")); err != nil { 2615 + if _, err := cw.WriteString(string(t.Value)); err != nil { 2306 2616 return err 2307 2617 } 2618 + return nil 2619 + } 2308 2620 2309 - // t.Action (string) (string) 2310 - if len("action") > 1000000 { 2311 - return xerrors.Errorf("Value in field \"action\" was too long") 2621 + func (t *CiTrigger_Pair) UnmarshalCBOR(r io.Reader) (err error) { 2622 + *t = CiTrigger_Pair{} 2623 + 2624 + cr := cbg.NewCborReader(r) 2625 + 2626 + maj, extra, err := cr.ReadHeader() 2627 + if err != nil { 2628 + return err 2629 + } 2630 + defer func() { 2631 + if err == io.EOF { 2632 + err = io.ErrUnexpectedEOF 2633 + } 2634 + }() 2635 + 2636 + if maj != cbg.MajMap { 2637 + return fmt.Errorf("cbor input should be of type map") 2638 + } 2639 + 2640 + if extra > cbg.MaxLength { 2641 + return fmt.Errorf("CiTrigger_Pair: map struct too large (%d)", extra) 2642 + } 2643 + 2644 + n := extra 2645 + 2646 + nameBuf := make([]byte, 5) 2647 + for i := uint64(0); i < n; i++ { 2648 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2649 + if err != nil { 2650 + return err 2651 + } 2652 + 2653 + if !ok { 2654 + // Field doesn't exist on this type, so ignore it 2655 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 2656 + return err 2657 + } 2658 + continue 2659 + } 2660 + 2661 + switch string(nameBuf[:nameLen]) { 2662 + // t.Key (string) (string) 2663 + case "key": 2664 + 2665 + { 2666 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2667 + if err != nil { 2668 + return err 2669 + } 2670 + 2671 + t.Key = string(sval) 2672 + } 2673 + // t.Value (string) (string) 2674 + case "value": 2675 + 2676 + { 2677 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2678 + if err != nil { 2679 + return err 2680 + } 2681 + 2682 + t.Value = string(sval) 2683 + } 2684 + 2685 + default: 2686 + // Field doesn't exist on this type, so ignore it 2687 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 2688 + return err 2689 + } 2690 + } 2312 2691 } 2313 2692 2314 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("action"))); err != nil { 2693 + return nil 2694 + } 2695 + func (t *CiTrigger_PullRequest) MarshalCBOR(w io.Writer) error { 2696 + if t == nil { 2697 + _, err := w.Write(cbg.CborNull) 2315 2698 return err 2316 2699 } 2317 - if _, err := cw.WriteString(string("action")); err != nil { 2700 + 2701 + cw := cbg.NewCborWriter(w) 2702 + fieldCount := 6 2703 + 2704 + if t.Pull == nil { 2705 + fieldCount-- 2706 + } 2707 + 2708 + if t.SourceBranch == nil { 2709 + fieldCount-- 2710 + } 2711 + 2712 + if t.SourceRepo == nil { 2713 + fieldCount-- 2714 + } 2715 + 2716 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2318 2717 return err 2319 2718 } 2320 2719 2321 - if len(t.Action) > 1000000 { 2322 - return xerrors.Errorf("Value in field t.Action was too long") 2720 + // t.Pull (string) (string) 2721 + if t.Pull != nil { 2722 + 2723 + if len("pull") > 1000000 { 2724 + return xerrors.Errorf("Value in field \"pull\" was too long") 2725 + } 2726 + 2727 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { 2728 + return err 2729 + } 2730 + if _, err := cw.WriteString(string("pull")); err != nil { 2731 + return err 2732 + } 2733 + 2734 + if t.Pull == nil { 2735 + if _, err := cw.Write(cbg.CborNull); err != nil { 2736 + return err 2737 + } 2738 + } else { 2739 + if len(*t.Pull) > 1000000 { 2740 + return xerrors.Errorf("Value in field t.Pull was too long") 2741 + } 2742 + 2743 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Pull))); err != nil { 2744 + return err 2745 + } 2746 + if _, err := cw.WriteString(string(*t.Pull)); err != nil { 2747 + return err 2748 + } 2749 + } 2750 + } 2751 + 2752 + // t.LexiconTypeID (string) (string) 2753 + if len("$type") > 1000000 { 2754 + return xerrors.Errorf("Value in field \"$type\" was too long") 2323 2755 } 2324 2756 2325 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Action))); err != nil { 2757 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 2326 2758 return err 2327 2759 } 2328 - if _, err := cw.WriteString(string(t.Action)); err != nil { 2760 + if _, err := cw.WriteString(string("$type")); err != nil { 2761 + return err 2762 + } 2763 + 2764 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.ci.trigger#pullRequest"))); err != nil { 2765 + return err 2766 + } 2767 + if _, err := cw.WriteString(string("sh.tangled.ci.trigger#pullRequest")); err != nil { 2329 2768 return err 2330 2769 } 2331 2770 ··· 2352 2791 return err 2353 2792 } 2354 2793 2794 + // t.SourceRepo (string) (string) 2795 + if t.SourceRepo != nil { 2796 + 2797 + if len("sourceRepo") > 1000000 { 2798 + return xerrors.Errorf("Value in field \"sourceRepo\" was too long") 2799 + } 2800 + 2801 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil { 2802 + return err 2803 + } 2804 + if _, err := cw.WriteString(string("sourceRepo")); err != nil { 2805 + return err 2806 + } 2807 + 2808 + if t.SourceRepo == nil { 2809 + if _, err := cw.Write(cbg.CborNull); err != nil { 2810 + return err 2811 + } 2812 + } else { 2813 + if len(*t.SourceRepo) > 1000000 { 2814 + return xerrors.Errorf("Value in field t.SourceRepo was too long") 2815 + } 2816 + 2817 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil { 2818 + return err 2819 + } 2820 + if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil { 2821 + return err 2822 + } 2823 + } 2824 + } 2825 + 2355 2826 // t.SourceBranch (string) (string) 2356 2827 if t.SourceBranch != nil { 2357 2828 ··· 2450 2921 } 2451 2922 2452 2923 switch string(nameBuf[:nameLen]) { 2453 - // t.LexiconTypeID (string) (string) 2454 - case "$type": 2924 + // t.Pull (string) (string) 2925 + case "pull": 2455 2926 2456 2927 { 2457 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2928 + b, err := cr.ReadByte() 2458 2929 if err != nil { 2459 2930 return err 2460 2931 } 2932 + if b != cbg.CborNull[0] { 2933 + if err := cr.UnreadByte(); err != nil { 2934 + return err 2935 + } 2461 2936 2462 - t.LexiconTypeID = string(sval) 2937 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2938 + if err != nil { 2939 + return err 2940 + } 2941 + 2942 + t.Pull = (*string)(&sval) 2943 + } 2463 2944 } 2464 - // t.Action (string) (string) 2465 - case "action": 2945 + // t.LexiconTypeID (string) (string) 2946 + case "$type": 2466 2947 2467 2948 { 2468 2949 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 2470 2951 return err 2471 2952 } 2472 2953 2473 - t.Action = string(sval) 2954 + t.LexiconTypeID = string(sval) 2474 2955 } 2475 2956 // t.SourceSha (string) (string) 2476 2957 case "sourceSha": ··· 2482 2963 } 2483 2964 2484 2965 t.SourceSha = string(sval) 2966 + } 2967 + // t.SourceRepo (string) (string) 2968 + case "sourceRepo": 2969 + 2970 + { 2971 + b, err := cr.ReadByte() 2972 + if err != nil { 2973 + return err 2974 + } 2975 + if b != cbg.CborNull[0] { 2976 + if err := cr.UnreadByte(); err != nil { 2977 + return err 2978 + } 2979 + 2980 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2981 + if err != nil { 2982 + return err 2983 + } 2984 + 2985 + t.SourceRepo = (*string)(&sval) 2986 + } 2485 2987 } 2486 2988 // t.SourceBranch (string) (string) 2487 2989 case "sourceBranch": ··· 7885 8387 } 7886 8388 7887 8389 cw := cbg.NewCborWriter(w) 8390 + fieldCount := 4 7888 8391 7889 - if _, err := cw.Write([]byte{164}); err != nil { 7890 - return err 8392 + if t.Pull == nil { 8393 + fieldCount-- 7891 8394 } 7892 8395 7893 - // t.Action (string) (string) 7894 - if len("action") > 1000000 { 7895 - return xerrors.Errorf("Value in field \"action\" was too long") 7896 - } 7897 - 7898 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("action"))); err != nil { 7899 - return err 7900 - } 7901 - if _, err := cw.WriteString(string("action")); err != nil { 8396 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 7902 8397 return err 7903 8398 } 7904 8399 7905 - if len(t.Action) > 1000000 { 7906 - return xerrors.Errorf("Value in field t.Action was too long") 7907 - } 8400 + // t.Pull (string) (string) 8401 + if t.Pull != nil { 7908 8402 7909 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Action))); err != nil { 7910 - return err 7911 - } 7912 - if _, err := cw.WriteString(string(t.Action)); err != nil { 7913 - return err 8403 + if len("pull") > 1000000 { 8404 + return xerrors.Errorf("Value in field \"pull\" was too long") 8405 + } 8406 + 8407 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { 8408 + return err 8409 + } 8410 + if _, err := cw.WriteString(string("pull")); err != nil { 8411 + return err 8412 + } 8413 + 8414 + if t.Pull == nil { 8415 + if _, err := cw.Write(cbg.CborNull); err != nil { 8416 + return err 8417 + } 8418 + } else { 8419 + if len(*t.Pull) > 1000000 { 8420 + return xerrors.Errorf("Value in field t.Pull was too long") 8421 + } 8422 + 8423 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Pull))); err != nil { 8424 + return err 8425 + } 8426 + if _, err := cw.WriteString(string(*t.Pull)); err != nil { 8427 + return err 8428 + } 8429 + } 7914 8430 } 7915 8431 7916 8432 // t.SourceSha (string) (string) ··· 8025 8541 } 8026 8542 8027 8543 switch string(nameBuf[:nameLen]) { 8028 - // t.Action (string) (string) 8029 - case "action": 8544 + // t.Pull (string) (string) 8545 + case "pull": 8030 8546 8031 8547 { 8032 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8548 + b, err := cr.ReadByte() 8033 8549 if err != nil { 8034 8550 return err 8035 8551 } 8552 + if b != cbg.CborNull[0] { 8553 + if err := cr.UnreadByte(); err != nil { 8554 + return err 8555 + } 8036 8556 8037 - t.Action = string(sval) 8557 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 8558 + if err != nil { 8559 + return err 8560 + } 8561 + 8562 + t.Pull = (*string)(&sval) 8563 + } 8038 8564 } 8039 8565 // t.SourceSha (string) (string) 8040 8566 case "sourceSha": ··· 8617 9143 } 8618 9144 8619 9145 cw := cbg.NewCborWriter(w) 8620 - fieldCount := 5 9146 + fieldCount := 6 8621 9147 8622 9148 if t.Manual == nil { 8623 9149 fieldCount-- ··· 8628 9154 } 8629 9155 8630 9156 if t.Push == nil { 9157 + fieldCount-- 9158 + } 9159 + 9160 + if t.SourceRepo == nil { 8631 9161 fieldCount-- 8632 9162 } 8633 9163 ··· 8712 9242 } 8713 9243 } 8714 9244 9245 + // t.SourceRepo (string) (string) 9246 + if t.SourceRepo != nil { 9247 + 9248 + if len("sourceRepo") > 1000000 { 9249 + return xerrors.Errorf("Value in field \"sourceRepo\" was too long") 9250 + } 9251 + 9252 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil { 9253 + return err 9254 + } 9255 + if _, err := cw.WriteString(string("sourceRepo")); err != nil { 9256 + return err 9257 + } 9258 + 9259 + if t.SourceRepo == nil { 9260 + if _, err := cw.Write(cbg.CborNull); err != nil { 9261 + return err 9262 + } 9263 + } else { 9264 + if len(*t.SourceRepo) > 1000000 { 9265 + return xerrors.Errorf("Value in field t.SourceRepo was too long") 9266 + } 9267 + 9268 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil { 9269 + return err 9270 + } 9271 + if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil { 9272 + return err 9273 + } 9274 + } 9275 + } 9276 + 8715 9277 // t.PullRequest (tangled.Pipeline_PullRequestTriggerData) (struct) 8716 9278 if t.PullRequest != nil { 8717 9279 ··· 8844 9406 } 8845 9407 } 8846 9408 9409 + } 9410 + // t.SourceRepo (string) (string) 9411 + case "sourceRepo": 9412 + 9413 + { 9414 + b, err := cr.ReadByte() 9415 + if err != nil { 9416 + return err 9417 + } 9418 + if b != cbg.CborNull[0] { 9419 + if err := cr.UnreadByte(); err != nil { 9420 + return err 9421 + } 9422 + 9423 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 9424 + if err != nil { 9425 + return err 9426 + } 9427 + 9428 + t.SourceRepo = (*string)(&sval) 9429 + } 8847 9430 } 8848 9431 // t.PullRequest (tangled.Pipeline_PullRequestTriggerData) (struct) 8849 9432 case "pullRequest":
+2
api/tangled/cipipeline.go
··· 26 26 Id string `json:"id" cborgen:"id"` 27 27 // repo: Repository DID 28 28 Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 29 + // sourceRepo: Repository DID that the commit was checked out from, if different from repo (e.g. a fork for a fork-based pull request) 30 + SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"` 29 31 // trigger: Trigger event metadata 30 32 Trigger *CiPipeline_Trigger `json:"trigger" cborgen:"trigger"` 31 33 // workflows: Triggered workflows
+22 -6
api/tangled/citrigger.go
··· 10 10 // 11 11 // RECORDTYPE: CiTrigger_Manual 12 12 type CiTrigger_Manual struct { 13 - LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#manual" cborgen:"$type,const=sh.tangled.ci.trigger#manual"` 13 + LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#manual" cborgen:"$type,const=sh.tangled.ci.trigger#manual"` 14 + Inputs []*CiTrigger_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 15 + // ref: optional ref the SHA was resolved from, for display and TANGLED_REF 16 + Ref *string `json:"ref,omitempty" cborgen:"ref,omitempty"` 17 + // sha: commit SHA the manual run targets 18 + Sha string `json:"sha" cborgen:"sha"` 19 + // sourceRepo: Repository DID to check out code and workflow definitions from, if different from the target repo. 20 + SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"` 21 + } 22 + 23 + // CiTrigger_Pair is a "pair" in the sh.tangled.ci.trigger schema. 24 + type CiTrigger_Pair struct { 25 + Key string `json:"key" cborgen:"key"` 26 + Value string `json:"value" cborgen:"value"` 14 27 } 15 28 16 29 // CiTrigger_PullRequest is a "pullRequest" in the sh.tangled.ci.trigger schema. ··· 19 32 // 20 33 // RECORDTYPE: CiTrigger_PullRequest 21 34 type CiTrigger_PullRequest struct { 22 - LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#pullRequest" cborgen:"$type,const=sh.tangled.ci.trigger#pullRequest"` 23 - Action string `json:"action" cborgen:"action"` 24 - SourceBranch *string `json:"sourceBranch,omitempty" cborgen:"sourceBranch,omitempty"` 25 - SourceSha string `json:"sourceSha" cborgen:"sourceSha"` 26 - TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 35 + LexiconTypeID string `json:"$type,const=sh.tangled.ci.trigger#pullRequest" cborgen:"$type,const=sh.tangled.ci.trigger#pullRequest"` 36 + // pull: AT-URI of the sh.tangled.repo.pull record this run belongs to 37 + Pull *string `json:"pull,omitempty" cborgen:"pull,omitempty"` 38 + SourceBranch *string `json:"sourceBranch,omitempty" cborgen:"sourceBranch,omitempty"` 39 + // sourceRepo: Repository DID to check out code and workflow definitions from, if different from the target repo. 40 + SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"` 41 + SourceSha string `json:"sourceSha" cborgen:"sourceSha"` 42 + TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 27 43 } 28 44 29 45 // CiTrigger_Push is a "push" in the sh.tangled.ci.trigger schema.
+41 -5
api/tangled/citriggerPipeline.go
··· 6 6 7 7 import ( 8 8 "context" 9 + "encoding/json" 10 + "fmt" 9 11 10 12 "github.com/bluesky-social/indigo/lex/util" 11 13 ) ··· 16 18 17 19 // CiTriggerPipeline_Input is the input argument to a sh.tangled.ci.triggerPipeline call. 18 20 type CiTriggerPipeline_Input struct { 19 - // ref: optional ref the SHA was resolved from, for display 20 - Ref *string `json:"ref,omitempty" cborgen:"ref,omitempty"` 21 - // repo: AT-URI of the sh.tangled.repo record 21 + // repo: Target repository DID. Auth is checked against this repo. 22 22 Repo string `json:"repo" cborgen:"repo"` 23 - // sha: commit SHA to run the pipeline at 24 - Sha string `json:"sha" cborgen:"sha"` 23 + // trigger: Trigger metadata for this dispatch. 24 + Trigger *CiTriggerPipeline_Input_Trigger `json:"trigger" cborgen:"trigger"` 25 25 // workflows: Workflow names to run. When not provided, every dispatchable workflow is run. 26 26 Workflows []string `json:"workflows,omitempty" cborgen:"workflows,omitempty"` 27 + } 28 + 29 + // Trigger metadata for this dispatch. 30 + type CiTriggerPipeline_Input_Trigger struct { 31 + CiTrigger_Manual *CiTrigger_Manual 32 + CiTrigger_PullRequest *CiTrigger_PullRequest 33 + } 34 + 35 + func (t *CiTriggerPipeline_Input_Trigger) MarshalJSON() ([]byte, error) { 36 + if t.CiTrigger_Manual != nil { 37 + t.CiTrigger_Manual.LexiconTypeID = "sh.tangled.ci.trigger#manual" 38 + return json.Marshal(t.CiTrigger_Manual) 39 + } 40 + if t.CiTrigger_PullRequest != nil { 41 + t.CiTrigger_PullRequest.LexiconTypeID = "sh.tangled.ci.trigger#pullRequest" 42 + return json.Marshal(t.CiTrigger_PullRequest) 43 + } 44 + return nil, fmt.Errorf("cannot marshal empty enum") 45 + } 46 + func (t *CiTriggerPipeline_Input_Trigger) UnmarshalJSON(b []byte) error { 47 + typ, err := util.TypeExtract(b) 48 + if err != nil { 49 + return err 50 + } 51 + 52 + switch typ { 53 + case "sh.tangled.ci.trigger#manual": 54 + t.CiTrigger_Manual = new(CiTrigger_Manual) 55 + return json.Unmarshal(b, t.CiTrigger_Manual) 56 + case "sh.tangled.ci.trigger#pullRequest": 57 + t.CiTrigger_PullRequest = new(CiTrigger_PullRequest) 58 + return json.Unmarshal(b, t.CiTrigger_PullRequest) 59 + 60 + default: 61 + return nil 62 + } 27 63 } 28 64 29 65 // CiTriggerPipeline_Output is the output of a sh.tangled.ci.triggerPipeline call.
+11 -11
api/tangled/pipelinecancelPipeline.go
··· 2 2 3 3 package tangled 4 4 5 - // schema: sh.tangled.ci.pipeline.cancelPipeline 5 + // schema: sh.tangled.pipeline.cancelPipeline 6 6 7 7 import ( 8 8 "context" ··· 11 11 ) 12 12 13 13 const ( 14 - CiPipelineCancelPipelineNSID = "sh.tangled.ci.pipeline.cancelPipeline" 14 + PipelineCancelPipelineNSID = "sh.tangled.pipeline.cancelPipeline" 15 15 ) 16 16 17 - // CiPipelineCancelPipeline_Input is the input argument to a sh.tangled.ci.pipeline.cancelPipeline call. 18 - type CiPipelineCancelPipeline_Input struct { 19 - // pipeline: pipeline TID 17 + // PipelineCancelPipeline_Input is the input argument to a sh.tangled.pipeline.cancelPipeline call. 18 + type PipelineCancelPipeline_Input struct { 19 + // pipeline: pipeline at-uri 20 20 Pipeline string `json:"pipeline" cborgen:"pipeline"` 21 - // repo: git repository DID 21 + // repo: repo at-uri, spindle can't resolve repo from pipeline at-uri yet 22 22 Repo string `json:"repo" cborgen:"repo"` 23 - // workflows: Workflow names to filter. When not provided, entire pipeline will be canceled. 24 - Workflows []string `json:"workflows,omitempty" cborgen:"workflows,omitempty"` 23 + // workflow: workflow name 24 + Workflow string `json:"workflow" cborgen:"workflow"` 25 25 } 26 26 27 - // CiPipelineCancelPipeline calls the XRPC method "sh.tangled.ci.pipeline.cancelPipeline". 28 - func CiPipelineCancelPipeline(ctx context.Context, c util.LexClient, input *CiPipelineCancelPipeline_Input) error { 29 - if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.ci.pipeline.cancelPipeline", nil, input, nil); err != nil { 27 + // PipelineCancelPipeline calls the XRPC method "sh.tangled.pipeline.cancelPipeline". 28 + func PipelineCancelPipeline(ctx context.Context, c util.LexClient, input *PipelineCancelPipeline_Input) error { 29 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.pipeline.cancelPipeline", nil, input, nil); err != nil { 30 30 return err 31 31 } 32 32
+7 -4
api/tangled/tangledpipeline.go
··· 49 49 50 50 // Pipeline_PullRequestTriggerData is a "pullRequestTriggerData" in the sh.tangled.pipeline schema. 51 51 type Pipeline_PullRequestTriggerData struct { 52 - Action string `json:"action" cborgen:"action"` 53 - SourceBranch string `json:"sourceBranch" cborgen:"sourceBranch"` 54 - SourceSha string `json:"sourceSha" cborgen:"sourceSha"` 55 - TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 52 + // pull: AT-URI of the sh.tangled.repo.pull record this run belongs to 53 + Pull *string `json:"pull,omitempty" cborgen:"pull,omitempty"` 54 + SourceBranch string `json:"sourceBranch" cborgen:"sourceBranch"` 55 + SourceSha string `json:"sourceSha" cborgen:"sourceSha"` 56 + TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 56 57 } 57 58 58 59 // Pipeline_PushTriggerData is a "pushTriggerData" in the sh.tangled.pipeline schema. ··· 69 70 PullRequest *Pipeline_PullRequestTriggerData `json:"pullRequest,omitempty" cborgen:"pullRequest,omitempty"` 70 71 Push *Pipeline_PushTriggerData `json:"push,omitempty" cborgen:"push,omitempty"` 71 72 Repo *Pipeline_TriggerRepo `json:"repo" cborgen:"repo"` 73 + // 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. 74 + SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"` 72 75 } 73 76 74 77 // Pipeline_TriggerRepo is a "triggerRepo" in the sh.tangled.pipeline schema.
+1 -1
appview/ingester.go
··· 31 31 "tangled.org/core/appview/mentions" 32 32 "tangled.org/core/appview/models" 33 33 "tangled.org/core/appview/notify" 34 - "tangled.org/core/appview/repoverify" 35 34 "tangled.org/core/appview/serververify" 36 35 "tangled.org/core/consts" 37 36 "tangled.org/core/idresolver" 38 37 "tangled.org/core/orm" 39 38 "tangled.org/core/rbac" 39 + "tangled.org/core/repoverify" 40 40 ) 41 41 42 42 type RepoPermissionChecker interface {
+1 -1
appview/ingester_repo.go
··· 15 15 "tangled.org/core/api/tangled" 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/models" 18 - "tangled.org/core/appview/repoverify" 19 18 "tangled.org/core/orm" 19 + "tangled.org/core/repoverify" 20 20 ) 21 21 22 22 func (i *Ingester) ingestRepo(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
+1 -1
appview/ingester_repo_test.go
··· 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/models" 18 18 "tangled.org/core/appview/notify" 19 - "tangled.org/core/appview/repoverify" 20 19 "tangled.org/core/orm" 21 20 "tangled.org/core/rbac" 21 + "tangled.org/core/repoverify" 22 22 ) 23 23 24 24 func mustKnotURL(t *testing.T, raw string) *url.URL {
+1 -1
appview/knots/knots.go
··· 20 20 "tangled.org/core/appview/oauth" 21 21 "tangled.org/core/appview/pages" 22 22 "tangled.org/core/appview/serververify" 23 - "tangled.org/core/appview/xrpcclient" 24 23 "tangled.org/core/consts" 25 24 "tangled.org/core/eventconsumer" 26 25 "tangled.org/core/idresolver" 27 26 "tangled.org/core/orm" 28 27 "tangled.org/core/rbac" 29 28 "tangled.org/core/tid" 29 + "tangled.org/core/xrpc/xrpcclient" 30 30 31 31 comatproto "github.com/bluesky-social/indigo/api/atproto" 32 32 "github.com/bluesky-social/indigo/atproto/atclient"
+15
appview/oauth/oauth.go
··· 22 22 "golang.org/x/sync/singleflight" 23 23 "tangled.org/core/appview/config" 24 24 "tangled.org/core/appview/db" 25 + "tangled.org/core/hostutil" 25 26 "tangled.org/core/idresolver" 26 27 "tangled.org/core/rbac" 27 28 "tangled.org/core/xrpc/serviceauth" ··· 443 444 Timeout: opts.timeout, 444 445 }, 445 446 }, nil 447 + } 448 + 449 + func (o *OAuth) SpindleServiceClient(r *http.Request, spindle, lxm string) (*xrpc.Client, error) { 450 + hostname, noTLS, err := hostutil.ParseHostname(spindle) 451 + if err != nil { 452 + return nil, err 453 + } 454 + return o.ServiceClient( 455 + r, 456 + WithService(hostname), 457 + WithLxm(lxm), 458 + WithDev(noTLS), 459 + WithTimeout(time.Second*30), 460 + ) 446 461 } 447 462 448 463 func (o *OAuth) StartElevatedAuthFlow(ctx context.Context, w http.ResponseWriter, r *http.Request, did string, extraScopes []string, returnURL string) (string, error) {
+49 -8
appview/pages/funcmap.go
··· 35 35 "tangled.org/core/appview/oauth" 36 36 "tangled.org/core/appview/pages/markup" 37 37 "tangled.org/core/appview/pages/markup/sanitizer" 38 + "tangled.org/core/appview/pages/repoinfo" 38 39 "tangled.org/core/crypto" 39 40 "tangled.org/core/idresolver" 41 + "tangled.org/core/orm" 42 + "tangled.org/core/types" 40 43 ) 41 44 42 45 type tab map[string]string 46 + 47 + func (p *Pages) ownerSlashRepo(repo *models.Repo) string { 48 + ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 49 + if err != nil { 50 + return repo.RepoIdentifier() 51 + } 52 + handle := ownerId.Handle 53 + if handle != "" && !handle.IsInvalidHandle() { 54 + return string(handle) + "/" + repo.Slug() 55 + } 56 + return repo.RepoIdentifier() 57 + } 43 58 44 59 func (p *Pages) funcMap() template.FuncMap { 45 60 return template.FuncMap{ ··· 91 106 } 92 107 return identity.PDSEndpoint() 93 108 }, 94 - "ownerSlashRepo": func(repo *models.Repo) string { 95 - ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 96 - if err != nil { 97 - return repo.RepoIdentifier() 109 + "ownerSlashRepo": p.ownerSlashRepo, 110 + "pipelineCommitPath": func(repoInfo repoinfo.RepoInfo, pipeline types.Pipeline) string { 111 + sha := pipeline.Sha() 112 + if sourceRepo := pipeline.SourceRepo(); sourceRepo != nil { 113 + if repo, err := db.GetRepoByDid(p.db, *sourceRepo); err == nil && repo != nil { 114 + return "/" + p.ownerSlashRepo(repo) + "/commit/" + sha 115 + } 116 + } 117 + return "/" + repoInfo.FullName() + "/commit/" + sha 118 + }, 119 + "pipelineSourceLabel": func(pipeline types.Pipeline) string { 120 + branch := pipeline.Trigger().PRSourceBranch() 121 + if branch == "" { 122 + return branch 98 123 } 99 - handle := ownerId.Handle 100 - if handle != "" && !handle.IsInvalidHandle() { 101 - return string(handle) + "/" + repo.Slug() 124 + sourceRepo := pipeline.SourceRepo() 125 + if sourceRepo == nil { 126 + return branch 102 127 } 103 - return repo.RepoIdentifier() 128 + repo, err := db.GetRepoByDid(p.db, *sourceRepo) 129 + if err != nil || repo == nil { 130 + return branch 131 + } 132 + return p.ownerSlashRepo(repo) + "/" + branch 133 + }, 134 + "pipelinePullPath": func(pipeline types.Pipeline) string { 135 + pullAtStr := pipeline.Trigger().PRUri() 136 + if pullAtStr == "" { 137 + return "" 138 + } 139 + // GetPull's reverse-mapping already populates pull.Repo 140 + pull, err := db.GetPull(p.db, orm.FilterEq("at_uri", pullAtStr)) 141 + if err != nil || pull == nil || pull.Repo == nil { 142 + return "" 143 + } 144 + return fmt.Sprintf("/%s/pulls/%d", p.ownerSlashRepo(pull.Repo), pull.PullId) 104 145 }, 105 146 "truncateAt30": func(s string) string { 106 147 if len(s) <= 30 {
+6
appview/pages/pages.go
··· 1442 1442 ActiveRound int 1443 1443 IsInterdiff bool 1444 1444 1445 + // WorkflowsChanged and ChangedWorkflowFiles describe whether the latest 1446 + // round's patch touches .tangled/workflows/, for warning maintainers 1447 + // before they manually trigger CI on a fork-based pull request. 1448 + WorkflowsChanged bool 1449 + ChangedWorkflowFiles []string 1450 + 1445 1451 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1446 1452 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1447 1453
+7 -2
appview/pages/templates/repo/pipelines/pipelines.html
··· 97 97 <div class="flex items-center gap-2 flex-wrap"> 98 98 {{ $target := .Trigger.TargetRef }} 99 99 {{ $workflows := .Workflows }} 100 + {{ $pullPath := "" }} 101 + {{ if .Trigger.IsPullRequest }}{{ $pullPath = pipelinePullPath . }}{{ end }} 100 102 101 103 <!-- Status icon with tooltip --> 102 104 <span class="relative z-50 mr-4"> ··· 115 117 <span class="text-sm text-gray-600 dark:text-gray-400">Pull request</span> 116 118 <span class="font-semibold dark:text-white">{{ $target }}</span> 117 119 {{ i "arrow-left" "size-3 text-gray-500 dark:text-gray-400" }} 118 - <span class="font-semibold dark:text-white">{{ .Trigger.PRSourceBranch }}</span> 120 + <span class="font-semibold dark:text-white">{{ pipelineSourceLabel . }}</span> 119 121 {{ else if .Trigger.IsManual }} 120 122 {{ i "circle-play" "size-4 text-gray-500 dark:text-gray-400 shrink-0" }} 121 123 <span class="text-sm text-gray-600 dark:text-gray-400">Manual dispatch</span> ··· 123 125 {{ if .IsResponding }} 124 126 </a> 125 127 {{ end }} 128 + {{ if $pullPath }} 129 + <a href="{{ $pullPath }}" class="text-sm text-gray-500 dark:text-gray-400 no-underline hover:underline">(view PR)</a> 130 + {{ end }} 126 131 </div> 127 132 128 133 <!-- Metadata stacked on mobile, separate columns on desktop --> ··· 130 135 <!-- Commit SHA --> 131 136 <div class="font-mono text-xs text-gray-500 dark:text-gray-400"> 132 137 {{ if .Sha }} 133 - <a href="/{{ $root.RepoInfo.FullName }}/commit/{{ .Sha }}" class="text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900 no-underline hover:underline px-2 py-1 rounded"> 138 + <a href="{{ pipelineCommitPath $root.RepoInfo . }}" class="text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900 no-underline hover:underline px-2 py-1 rounded"> 134 139 {{ slice .Sha 0 8 }} 135 140 </a> 136 141 {{ end }}
+1 -2
appview/pages/templates/repo/pipelines/workflow.html
··· 12 12 {{ block "sidebar" . }} {{ end }} 13 13 </div> 14 14 <div class="col-span-1 md:col-span-3"> 15 - <!-- TODO(boltless): explicitly check for pipeline cancel permission --> 16 - {{ if $.RepoInfo.Roles.IsOwner }} 15 + {{ if $.RepoInfo.Roles.IsPushAllowed }} 17 16 {{ $status := (index .Pipeline.Statuses .Workflow).Latest.Status }} 18 17 <div class="flex justify-between items-center mb-2"> 19 18 <button
+49
appview/pages/templates/repo/pulls/fragments/triggerCi.html
··· 1 + {{ define "repo/pulls/fragments/runCiButton" }} 2 + <button 3 + class="btn-flat p-2 flex items-center gap-2 flex-shrink-0 group" 4 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/trigger-ci{{ if .Confirm }}?confirm=1{{ end }}" 5 + hx-swap="none" 6 + hx-disabled-elt="this" 7 + {{ if .Confirm }}hx-confirm="{{ .Confirm }}"{{ end }} 8 + > 9 + {{ i "circle-play" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 10 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 11 + Run CI 12 + </button> 13 + {{ end }} 14 + 15 + {{ define "repo/pulls/fragments/triggerCi" }} 16 + {{ if .WorkflowsChanged }} 17 + <details class="group/ci"> 18 + <summary class="flex items-center justify-between gap-2 cursor-pointer list-none"> 19 + <div class="flex items-center gap-2 min-w-0 text-amber-600 dark:text-amber-500"> 20 + {{ i "triangle-alert" "w-4 h-4 flex-shrink-0" }} 21 + <span class="font-medium truncate">Workflow files changed in this round</span> 22 + <div class="text-sm text-gray-500 dark:text-gray-400"> 23 + <span class="group-open/ci:hidden inline">Expand</span> 24 + <span class="hidden group-open/ci:inline">Collapse</span> 25 + </div> 26 + </div> 27 + </summary> 28 + <ul class="space-y-1 mt-2 overflow-x-auto"> 29 + {{ range .ChangedWorkflowFiles }} 30 + <li class="flex items-center whitespace-nowrap"> 31 + {{ i "file-warning" "inline-flex w-4 h-4 mr-1.5 text-amber-600 dark:text-amber-500 flex-shrink-0" }} 32 + <span class="font-mono">{{ . }}</span> 33 + </li> 34 + {{ end }} 35 + </ul> 36 + <div class="mt-2"> 37 + {{ $confirm := printf "Workflow files changed in this round (%s). Review before running. Run anyway?" (join .ChangedWorkflowFiles ", ") }} 38 + {{ template "repo/pulls/fragments/runCiButton" (dict "RepoInfo" .RepoInfo "Pull" .Pull "Confirm" $confirm) }} 39 + </div> 40 + </details> 41 + {{ else }} 42 + <div class="flex items-center gap-2 text-gray-600 dark:text-gray-400"> 43 + {{ i "circle-play" "w-4 h-4 flex-shrink-0" }} 44 + <span class="font-medium">CI hasn't run on the latest commit</span> 45 + {{ template "repo/pulls/fragments/runCiButton" (dict "RepoInfo" .RepoInfo "Pull" .Pull "Confirm" "") }} 46 + </div> 47 + {{ end }} 48 + <div id="pull-error" class="text-red-500 dark:text-red-400"></div> 49 + {{ end }}
+5 -5
appview/pages/templates/repo/pulls/pull.html
··· 514 514 {{ $item := index . 0 }} 515 515 {{ $root := index . 3 }} 516 516 {{ $pipeline := index $root.Pipelines $item.SourceRev }} 517 - {{ with $pipeline }} 518 - {{ $id := .Id }} 519 - {{ if .Statuses }} 517 + {{ if and $pipeline $pipeline.Statuses }} 518 + {{ $id := $pipeline.Id }} 520 519 <details class="group/pipeline"> 521 520 <summary class="cursor-pointer list-none flex items-center gap-2"> 522 521 {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" false) }} ··· 526 525 </div> 527 526 </summary> 528 527 <div class="my-2 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"> 529 - {{ range $name, $all := .Statuses }} 528 + {{ range $name, $all := $pipeline.Statuses }} 530 529 <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 531 530 <div 532 531 class="flex gap-2 items-center justify-between p-2"> ··· 550 549 {{ end }} 551 550 </div> 552 551 </details> 553 - {{ end }} 552 + {{ else if and $root.Pull.IsForkBased (eq $item.RoundNumber $root.Pull.LastRoundNumber) $root.RepoInfo.Roles.IsOwner }} 553 + {{ template "repo/pulls/fragments/triggerCi" $root }} 554 554 {{ end }} 555 555 {{ end }} 556 556
+53 -25
appview/pipelines/pipelines.go
··· 46 46 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 47 47 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 48 48 r.Group(func(r chi.Router) { 49 - r.Use(mw.RepoPermissionMiddleware("repo:owner")) 49 + r.Use(mw.RepoPermissionMiddleware("repo:push")) 50 50 r.Post("/{pipeline}/workflow/{workflow}/cancel", p.CancelWorkflow) 51 51 r.Post("/{pipeline}/retry", p.RetryPipeline) 52 52 r.Post("/{pipeline}/workflow/{workflow}/retry", p.RetryWorkflow) ··· 251 251 // Shutdown implements [lexutil.Scheduler]. 252 252 func (w *webLogScheduler) Shutdown() { close(w.ch) } 253 253 254 + func retryPipelineTrigger(orig *tangled.CiPipeline) *tangled.CiTriggerPipeline_Input_Trigger { 255 + if orig.Trigger != nil && orig.Trigger.CiTrigger_PullRequest != nil { 256 + pr := orig.Trigger.CiTrigger_PullRequest 257 + sourceSha := pr.SourceSha 258 + if sourceSha == "" { 259 + sourceSha = orig.Commit 260 + } 261 + sourceRepo := pr.SourceRepo 262 + if sourceRepo == nil { 263 + sourceRepo = orig.SourceRepo 264 + } 265 + 266 + return &tangled.CiTriggerPipeline_Input_Trigger{ 267 + CiTrigger_PullRequest: &tangled.CiTrigger_PullRequest{ 268 + Pull: pr.Pull, 269 + SourceBranch: pr.SourceBranch, 270 + SourceRepo: sourceRepo, 271 + SourceSha: sourceSha, 272 + TargetBranch: pr.TargetBranch, 273 + }, 274 + } 275 + } 276 + 277 + manual := &tangled.CiTrigger_Manual{ 278 + Sha: orig.Commit, 279 + SourceRepo: orig.SourceRepo, 280 + } 281 + if orig.Trigger != nil && orig.Trigger.CiTrigger_Manual != nil { 282 + origManual := orig.Trigger.CiTrigger_Manual 283 + if origManual.Sha != "" { 284 + manual.Sha = origManual.Sha 285 + } 286 + manual.Ref = origManual.Ref 287 + manual.Inputs = origManual.Inputs 288 + if origManual.SourceRepo != nil { 289 + manual.SourceRepo = origManual.SourceRepo 290 + } 291 + } 292 + 293 + return &tangled.CiTriggerPipeline_Input_Trigger{ 294 + CiTrigger_Manual: manual, 295 + } 296 + } 297 + 254 298 func (p *Pipelines) Logs(w http.ResponseWriter, r *http.Request) { 255 299 l := p.logger.With("handler", "logs") 256 300 ··· 466 510 467 511 l = l.With("pipeline", pipelineId, "workflow", workflowName) 468 512 469 - spindleClient, err := p.spindleServiceClient(r, f.Spindle, tangled.CiPipelineCancelPipelineNSID) 513 + spindleClient, err := p.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiCancelPipelineNSID) 470 514 if err != nil { 471 515 l.Error("failed to prepare spindle client", "err", err) 472 516 p.pages.Notice(w, errorId, "Failed to cancel workflow") 473 517 return 474 518 } 475 519 476 - pipelineAtUri := fmt.Sprintf("at://did:web:%s/%s/%s", f.Knot, tangled.PipelineNSID, pipelineId.String()) 477 - if err := tangled.CiPipelineCancelPipeline( 520 + if err := tangled.CiCancelPipeline( 478 521 r.Context(), 479 522 spindleClient, 480 - &tangled.CiPipelineCancelPipeline_Input{ 481 - Repo: string(f.RepoAt()), 482 - Pipeline: pipelineAtUri, 523 + &tangled.CiCancelPipeline_Input{ 524 + Repo: f.RepoDid, 525 + Pipeline: pipelineId.String(), 483 526 Workflows: []string{workflowName}, 484 527 }, 485 528 ); err != nil { ··· 570 613 } 571 614 redirectWf := workflows[0] 572 615 573 - spindleClient, err := p.spindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID) 616 + spindleClient, err := p.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID) 574 617 if err != nil { 575 618 fail("failed to authorize with spindle", err) 576 619 return ··· 580 623 r.Context(), 581 624 spindleClient, 582 625 &tangled.CiTriggerPipeline_Input{ 583 - Repo: string(f.RepoAt()), 584 - Sha: orig.Commit, 626 + Repo: f.RepoDid, 627 + Trigger: retryPipelineTrigger(orig), 585 628 Workflows: workflows, 586 629 }, 587 630 ) ··· 609 652 } 610 653 http.Redirect(w, r, dest, http.StatusSeeOther) 611 654 } 612 - 613 - // spindleServiceClient builds an authed spindle xrpc client 614 - func (p *Pipelines) spindleServiceClient(r *http.Request, spindle, lxm string) (*indigoxrpc.Client, error) { 615 - hostname, noTLS, err := hostutil.ParseHostname(spindle) 616 - if err != nil { 617 - return nil, err 618 - } 619 - return p.oauth.ServiceClient( 620 - r, 621 - oauth.WithService(hostname), 622 - oauth.WithLxm(lxm), 623 - oauth.WithDev(noTLS), 624 - oauth.WithTimeout(time.Second*30), 625 - ) 626 - }
+1 -1
appview/pulls/compose.go
··· 18 18 "tangled.org/core/appview/oauth" 19 19 "tangled.org/core/appview/pages" 20 20 "tangled.org/core/appview/pages/markup/sanitizer" 21 - "tangled.org/core/appview/xrpcclient" 22 21 "tangled.org/core/patchutil" 23 22 "tangled.org/core/types" 23 + "tangled.org/core/xrpc/xrpcclient" 24 24 25 25 "github.com/bluesky-social/indigo/atproto/syntax" 26 26 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+1 -1
appview/pulls/create.go
··· 16 16 "tangled.org/core/appview/models" 17 17 "tangled.org/core/appview/oauth" 18 18 "tangled.org/core/appview/reporesolver" 19 - "tangled.org/core/appview/xrpcclient" 20 19 "tangled.org/core/patchutil" 21 20 "tangled.org/core/tid" 22 21 "tangled.org/core/types" 23 22 "tangled.org/core/xrpc" 23 + "tangled.org/core/xrpc/xrpcclient" 24 24 25 25 comatproto "github.com/bluesky-social/indigo/api/atproto" 26 26 "github.com/bluesky-social/indigo/atproto/syntax"
+1 -1
appview/pulls/merge.go
··· 10 10 "tangled.org/core/appview/models" 11 11 "tangled.org/core/appview/oauth" 12 12 "tangled.org/core/appview/reporesolver" 13 - "tangled.org/core/appview/xrpcclient" 14 13 "tangled.org/core/orm" 14 + "tangled.org/core/xrpc/xrpcclient" 15 15 16 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 17 )
+1 -1
appview/pulls/resubmit.go
··· 13 13 "tangled.org/core/appview/oauth" 14 14 "tangled.org/core/appview/pages" 15 15 "tangled.org/core/appview/reporesolver" 16 - "tangled.org/core/appview/xrpcclient" 17 16 "tangled.org/core/orm" 18 17 "tangled.org/core/patchutil" 19 18 "tangled.org/core/types" 20 19 "tangled.org/core/xrpc" 20 + "tangled.org/core/xrpc/xrpcclient" 21 21 22 22 comatproto "github.com/bluesky-social/indigo/api/atproto" 23 23 "github.com/bluesky-social/indigo/atproto/syntax"
+5
appview/pulls/router.go
··· 50 50 r.Post("/merge", s.MergePull) 51 51 // maybe lock, etc. 52 52 }) 53 + 54 + r.Group(func(r chi.Router) { 55 + r.Use(mw.RepoPermissionMiddleware("repo:push")) 56 + r.Post("/trigger-ci", s.TriggerCi) 57 + }) 53 58 }) 54 59 }) 55 60 return r
+14 -1
appview/pulls/single.go
··· 10 10 "tangled.org/core/appview/db" 11 11 "tangled.org/core/appview/models" 12 12 "tangled.org/core/appview/pages" 13 - "tangled.org/core/appview/xrpcclient" 14 13 "tangled.org/core/orm" 15 14 "tangled.org/core/patchutil" 16 15 "tangled.org/core/types" 16 + "tangled.org/core/xrpc/xrpcclient" 17 17 18 18 "github.com/bluesky-social/indigo/atproto/syntax" 19 19 indigoxrpc "github.com/bluesky-social/indigo/xrpc" ··· 187 187 return m 188 188 }(r.Context()) 189 189 190 + var workflowsChanged bool 191 + var changedWorkflows []string 192 + if _, hasPipeline := pipelines[pull.LatestSha()]; pull.IsForkBased() && !hasPipeline { 193 + changedWorkflows, err = changedWorkflowFiles(pull.LatestSubmission().CombinedPatch()) 194 + if err != nil { 195 + l.Error("failed to inspect latest round's patch for workflow changes", "err", err) 196 + } 197 + workflowsChanged = len(changedWorkflows) > 0 198 + } 199 + 190 200 entities := []syntax.ATURI{pull.AtUri()} 191 201 for _, s := range pull.Submissions { 192 202 for _, c := range s.Comments { ··· 273 283 DiffOpts: diffOpts, 274 284 ActiveRound: roundIdInt, 275 285 IsInterdiff: interdiff, 286 + 287 + WorkflowsChanged: workflowsChanged, 288 + ChangedWorkflowFiles: changedWorkflows, 276 289 277 290 Reactions: reactions, 278 291 UserReacted: userReactions,
+137
appview/pulls/trigger_ci.go
··· 1 + package pulls 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strings" 7 + 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/patchutil" 12 + "tangled.org/core/workflow" 13 + ) 14 + 15 + func changedWorkflowFiles(patch string) ([]string, error) { 16 + files, err := patchutil.AsDiff(patch) 17 + if err != nil { 18 + return nil, err 19 + } 20 + 21 + var changed []string 22 + for _, f := range files { 23 + if f == nil { 24 + continue 25 + } 26 + for _, name := range []string{f.NewName, f.OldName} { 27 + if name != "" && strings.HasPrefix(name, workflow.WorkflowDir+"/") { 28 + changed = append(changed, name) 29 + break 30 + } 31 + } 32 + } 33 + return changed, nil 34 + } 35 + 36 + // TriggerCi manually triggers a CI pipeline for a fork-based pull request. 37 + // authorized against and recorded under the target repo, but checked out 38 + // from the fork at the latest round's commit. 39 + func (s *Pulls) TriggerCi(w http.ResponseWriter, r *http.Request) { 40 + l := s.logger.With("handler", "TriggerCi") 41 + errorId := "pull-error" 42 + 43 + fail := func(msg string, err error) { 44 + if err != nil { 45 + l.Error(msg, "err", err) 46 + } else { 47 + l.Error(msg) 48 + } 49 + s.pages.Notice(w, errorId, msg) 50 + } 51 + 52 + f, err := s.repoResolver.Resolve(r) 53 + if err != nil { 54 + fail("failed to resolve repository", err) 55 + return 56 + } 57 + 58 + pull, ok := r.Context().Value("pull").(*models.Pull) 59 + if !ok { 60 + fail("failed to get pull", nil) 61 + return 62 + } 63 + l = l.With("pull_id", pull.PullId) 64 + 65 + if !pull.IsForkBased() { 66 + fail("this pull request is not fork-based", nil) 67 + return 68 + } 69 + 70 + if f.Spindle == "" { 71 + fail("this repository has no spindle configured", nil) 72 + return 73 + } 74 + 75 + latest := pull.LatestSubmission() 76 + if latest.SourceRev == "" { 77 + fail("cannot trigger ci: this round has no commit to run", nil) 78 + return 79 + } 80 + 81 + changedFiles, err := changedWorkflowFiles(latest.CombinedPatch()) 82 + if err != nil { 83 + fail("failed to inspect the latest round's patch", err) 84 + return 85 + } 86 + if len(changedFiles) > 0 && r.URL.Query().Get("confirm") != "1" { 87 + fail(fmt.Sprintf("workflow files changed in this round (%s); review before running", strings.Join(changedFiles, ", ")), nil) 88 + return 89 + } 90 + 91 + forkRepo, err := db.GetRepoByDid(s.db, pull.PullSource.RepoDid.String()) 92 + if err != nil { 93 + fail("failed to resolve the fork this pull request comes from", err) 94 + return 95 + } 96 + 97 + spindleClient, err := s.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID) 98 + if err != nil { 99 + fail("failed to authorize with spindle", err) 100 + return 101 + } 102 + 103 + pullAt := pull.AtUri().String() 104 + sourceBranch := pull.PullSource.Branch 105 + targetBranch := pull.TargetBranch 106 + out, err := tangled.CiTriggerPipeline( 107 + r.Context(), 108 + spindleClient, 109 + &tangled.CiTriggerPipeline_Input{ 110 + Repo: f.RepoDid, 111 + Trigger: &tangled.CiTriggerPipeline_Input_Trigger{ 112 + CiTrigger_PullRequest: &tangled.CiTrigger_PullRequest{ 113 + Pull: &pullAt, 114 + SourceBranch: &sourceBranch, 115 + SourceRepo: &forkRepo.RepoDid, 116 + SourceSha: latest.SourceRev, 117 + TargetBranch: targetBranch, 118 + }, 119 + }, 120 + }, 121 + ) 122 + if err != nil { 123 + fail("spindle rejected the trigger", err) 124 + return 125 + } 126 + l.Info("triggered ci for fork-based pull", "pipeline", out.Pipeline) 127 + 128 + user := s.oauth.GetMultiAccountUser(r) 129 + repoInfo := s.repoResolver.GetRepoInfo(r, user) 130 + dest := fmt.Sprintf("/%s/pulls/%d/round/%d", repoInfo.FullName(), pull.PullId, pull.LastRoundNumber()) 131 + if r.Header.Get("HX-Request") == "true" { 132 + w.Header().Set("HX-Redirect", dest) 133 + w.WriteHeader(http.StatusOK) 134 + return 135 + } 136 + http.Redirect(w, r, dest, http.StatusSeeOther) 137 + }
+1 -1
appview/repo/artifact.go
··· 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/models" 15 15 "tangled.org/core/appview/pages" 16 - "tangled.org/core/appview/xrpcclient" 17 16 "tangled.org/core/orm" 18 17 "tangled.org/core/tid" 19 18 "tangled.org/core/types" 20 19 "tangled.org/core/xrpc" 20 + "tangled.org/core/xrpc/xrpcclient" 21 21 22 22 comatproto "github.com/bluesky-social/indigo/api/atproto" 23 23 "github.com/bluesky-social/indigo/atproto/syntax"
+1 -1
appview/repo/blob.go
··· 18 18 "tangled.org/core/appview/pages" 19 19 "tangled.org/core/appview/pages/markup" 20 20 "tangled.org/core/appview/reporesolver" 21 - xrpcclient "tangled.org/core/appview/xrpcclient" 22 21 "tangled.org/core/types" 22 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 23 23 24 24 "github.com/bluesky-social/indigo/util" 25 25 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+1 -1
appview/repo/branches.go
··· 8 8 "tangled.org/core/api/tangled" 9 9 "tangled.org/core/appview/oauth" 10 10 "tangled.org/core/appview/pages" 11 - xrpcclient "tangled.org/core/appview/xrpcclient" 12 11 "tangled.org/core/types" 12 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 13 13 14 14 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 15 )
+1 -1
appview/repo/compare.go
··· 10 10 11 11 "tangled.org/core/api/tangled" 12 12 "tangled.org/core/appview/pages" 13 - xrpcclient "tangled.org/core/appview/xrpcclient" 14 13 "tangled.org/core/patchutil" 15 14 "tangled.org/core/types" 15 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 16 16 17 17 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 18 "github.com/go-chi/chi/v5"
+1 -1
appview/repo/log.go
··· 12 12 "tangled.org/core/appview/commitverify" 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/pages" 15 - xrpcclient "tangled.org/core/appview/xrpcclient" 16 15 "tangled.org/core/types" 16 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 17 17 18 18 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 19 19 "github.com/go-chi/chi/v5"
+1 -1
appview/repo/repo.go
··· 27 27 "tangled.org/core/appview/pagination" 28 28 "tangled.org/core/appview/reporesolver" 29 29 "tangled.org/core/appview/sites" 30 - xrpcclient "tangled.org/core/appview/xrpcclient" 31 30 "tangled.org/core/consts" 32 31 "tangled.org/core/idresolver" 33 32 "tangled.org/core/ogre" ··· 35 34 "tangled.org/core/rbac" 36 35 "tangled.org/core/tid" 37 36 "tangled.org/core/xrpc/serviceauth" 37 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 38 38 39 39 comatproto "github.com/bluesky-social/indigo/api/atproto" 40 40 "github.com/bluesky-social/indigo/atproto/atclient"
+1 -1
appview/repo/settings.go
··· 20 20 "tangled.org/core/appview/oauth" 21 21 "tangled.org/core/appview/pages" 22 22 "tangled.org/core/appview/sites" 23 - xrpcclient "tangled.org/core/appview/xrpcclient" 24 23 "tangled.org/core/consts" 25 24 "tangled.org/core/orm" 26 25 "tangled.org/core/sets" 27 26 "tangled.org/core/types" 27 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 28 28 29 29 comatproto "github.com/bluesky-social/indigo/api/atproto" 30 30 "github.com/bluesky-social/indigo/atproto/syntax"
+1 -1
appview/repo/tags.go
··· 11 11 "tangled.org/core/appview/models" 12 12 "tangled.org/core/appview/pages" 13 13 "tangled.org/core/appview/reporesolver" 14 - xrpcclient "tangled.org/core/appview/xrpcclient" 15 14 "tangled.org/core/orm" 16 15 "tangled.org/core/types" 16 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 17 17 18 18 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 19 19 "github.com/go-chi/chi/v5"
+1 -1
appview/repo/tree.go
··· 13 13 "tangled.org/core/appview/pages" 14 14 "tangled.org/core/appview/pages/markup" 15 15 "tangled.org/core/appview/reporesolver" 16 - xrpcclient "tangled.org/core/appview/xrpcclient" 17 16 "tangled.org/core/types" 17 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 18 18 19 19 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 20 20 "github.com/go-chi/chi/v5"
+5 -1
appview/repoverify/verify.go repoverify/verify.go
··· 13 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 14 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 15 "tangled.org/core/api/tangled" 16 - "tangled.org/core/appview/xrpcclient" 17 16 "tangled.org/core/idresolver" 17 + "tangled.org/core/xrpc/xrpcclient" 18 18 ) 19 19 20 20 type RepoDid syntax.DID ··· 68 68 RepoDid RepoDid 69 69 OwnerDid OwnerDid 70 70 KnotURL *url.URL 71 + // Rkey of the sh.tangled.repo record tracked by the knot; empty when the 72 + // knot does not support describeRepo. 73 + Rkey string 71 74 } 72 75 73 76 type Verifier func(ctx context.Context, repoDid RepoDid) (Result, error) ··· 129 132 RepoDid: repoDid, 130 133 OwnerDid: ownerDid, 131 134 KnotURL: knot, 135 + Rkey: out.Rkey, 132 136 }, nil 133 137 } 134 138
appview/repoverify/verify_test.go repoverify/verify_test.go
+1 -1
appview/serververify/verify.go
··· 8 8 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 9 9 "tangled.org/core/api/tangled" 10 10 "tangled.org/core/appview/db" 11 - "tangled.org/core/appview/xrpcclient" 12 11 "tangled.org/core/orm" 13 12 "tangled.org/core/rbac" 13 + "tangled.org/core/xrpc/xrpcclient" 14 14 ) 15 15 16 16 var (
+1 -1
appview/spindles/spindles.go
··· 18 18 "tangled.org/core/appview/oauth" 19 19 "tangled.org/core/appview/pages" 20 20 "tangled.org/core/appview/serververify" 21 - "tangled.org/core/appview/xrpcclient" 22 21 "tangled.org/core/idresolver" 23 22 "tangled.org/core/orm" 24 23 "tangled.org/core/rbac" 25 24 "tangled.org/core/tid" 25 + "tangled.org/core/xrpc/xrpcclient" 26 26 27 27 comatproto "github.com/bluesky-social/indigo/api/atproto" 28 28 "github.com/bluesky-social/indigo/atproto/syntax"
+2 -2
appview/state/state.go
··· 33 33 "tangled.org/core/appview/pages" 34 34 pipelinessh "tangled.org/core/appview/pipelines/ssh" 35 35 "tangled.org/core/appview/reporesolver" 36 - "tangled.org/core/appview/repoverify" 37 - xrpcclient "tangled.org/core/appview/xrpcclient" 38 36 "tangled.org/core/consts" 39 37 "tangled.org/core/eventconsumer" 40 38 "tangled.org/core/idresolver" ··· 43 41 tlog "tangled.org/core/log" 44 42 "tangled.org/core/orm" 45 43 "tangled.org/core/rbac" 44 + "tangled.org/core/repoverify" 45 + xrpcclient "tangled.org/core/xrpc/xrpcclient" 46 46 47 47 comatproto "github.com/bluesky-social/indigo/api/atproto" 48 48 "github.com/bluesky-social/indigo/atproto/atclient"
appview/xrpcclient/xrpc.go xrpc/xrpcclient/xrpc.go
+1
cmd/cborgen/cborgen.go
··· 21 21 tangled.CiSubscribePipelineLogs_Control{}, 22 22 tangled.CiSubscribePipelineLogs_Data{}, 23 23 tangled.CiTrigger_Manual{}, 24 + tangled.CiTrigger_Pair{}, 24 25 tangled.CiTrigger_PullRequest{}, 25 26 tangled.CiTrigger_Push{}, 26 27 tangled.FeedComment{},
+1 -1
flake.nix
··· 524 524 find api/tangled -maxdepth 1 -name '*.go' -not -name '*_ext.go' -exec \ 525 525 sed -i.bak 's/\tutil/\/\/\tutil/' {} + 526 526 # lexgen generates incomplete Marshaler/Unmarshaler for union types 527 - find api/tangled/*.go -not -name "cbor_gen.go" -exec \ 527 + find api/tangled -maxdepth 1 -name '*.go' -not -name '*_ext.go' -not -name "cbor_gen.go" -exec \ 528 528 sed -i '/^func.*\(MarshalCBOR\|UnmarshalCBOR\)/,/^}/ s/^/\/\/ /' {} + 529 529 for f in api/tangled/*_ext.go; do [ -e "''$f" ] && mv "''$f" "''$f.bak"; done 530 530 ${pkgs.gotools}/bin/goimports -w api/tangled/*
+5
lexicons/ci/pipeline.json
··· 29 29 "type": "string", 30 30 "description": "Commit Id this pipeline is running on" 31 31 }, 32 + "sourceRepo": { 33 + "type": "string", 34 + "format": "did", 35 + "description": "Repository DID that the commit was checked out from, if different from repo (e.g. a fork for a fork-based pull request)" 36 + }, 32 37 "createdAt": { 33 38 "type": "string", 34 39 "format": "datetime"
+47 -4
lexicons/ci/trigger.json
··· 23 23 }, 24 24 "pullRequest": { 25 25 "type": "object", 26 - "required": ["targetBranch", "sourceSha", "action"], 26 + "required": ["targetBranch", "sourceSha"], 27 27 "description": "TODO: reference PR record with strongRef instead of embedding raw values", 28 28 "properties": { 29 + "sourceRepo": { 30 + "type": "string", 31 + "format": "did", 32 + "description": "Repository DID to check out code and workflow definitions from, if different from the target repo." 33 + }, 29 34 "sourceBranch": { 30 35 "type": "string" 31 36 }, ··· 37 42 "minLength": 40, 38 43 "maxLength": 40 39 44 }, 40 - "action": { 41 - "type": "string" 45 + "pull": { 46 + "type": "string", 47 + "format": "at-uri", 48 + "description": "AT-URI of the sh.tangled.repo.pull record this run belongs to" 42 49 } 43 50 } 44 51 }, 45 52 "manual": { 46 53 "type": "object", 47 - "properties": {} 54 + "required": ["sha"], 55 + "properties": { 56 + "sha": { 57 + "type": "string", 58 + "description": "commit SHA the manual run targets", 59 + "minLength": 40, 60 + "maxLength": 40 61 + }, 62 + "ref": { 63 + "type": "string", 64 + "description": "optional ref the SHA was resolved from, for display and TANGLED_REF" 65 + }, 66 + "sourceRepo": { 67 + "type": "string", 68 + "format": "did", 69 + "description": "Repository DID to check out code and workflow definitions from, if different from the target repo." 70 + }, 71 + "inputs": { 72 + "type": "array", 73 + "items": { 74 + "type": "ref", 75 + "ref": "#pair" 76 + } 77 + } 78 + } 79 + }, 80 + "pair": { 81 + "type": "object", 82 + "required": ["key", "value"], 83 + "properties": { 84 + "key": { 85 + "type": "string" 86 + }, 87 + "value": { 88 + "type": "string" 89 + } 90 + } 48 91 } 49 92 } 50 93 }
+11 -13
lexicons/ci/triggerPipeline.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", 7 - "description": "Manually trigger a pipeline at an explicit commit. Runs the named workflows, or every workflow defined in the repo when none are named.", 7 + "description": "Trigger a pipeline at an explicit commit. Runs the named workflows, or every workflow defined in the repo when none are named.", 8 8 "input": { 9 9 "encoding": "application/json", 10 10 "schema": { 11 11 "type": "object", 12 - "required": ["repo", "sha"], 12 + "required": ["repo", "trigger"], 13 13 "properties": { 14 14 "repo": { 15 15 "type": "string", 16 - "format": "at-uri", 17 - "description": "AT-URI of the sh.tangled.repo record" 18 - }, 19 - "sha": { 20 - "type": "string", 21 - "minLength": 40, 22 - "maxLength": 40, 23 - "description": "commit SHA to run the pipeline at" 16 + "format": "did", 17 + "description": "Target repository DID. Auth is checked against this repo." 24 18 }, 25 - "ref": { 26 - "type": "string", 27 - "description": "optional ref the SHA was resolved from, for display" 19 + "trigger": { 20 + "type": "union", 21 + "refs": [ 22 + "sh.tangled.ci.trigger#manual", 23 + "sh.tangled.ci.trigger#pullRequest" 24 + ], 25 + "description": "Trigger metadata for this dispatch." 28 26 }, 29 27 "workflows": { 30 28 "type": "array",
+10 -4
lexicons/pipeline/pipeline.json
··· 59 59 "manual": { 60 60 "type": "ref", 61 61 "ref": "#manualTriggerData" 62 + }, 63 + "sourceRepo": { 64 + "type": "string", 65 + "format": "did", 66 + "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." 62 67 } 63 68 } 64 69 }, ··· 118 123 "required": [ 119 124 "sourceBranch", 120 125 "targetBranch", 121 - "sourceSha", 122 - "action" 126 + "sourceSha" 123 127 ], 124 128 "properties": { 125 129 "sourceBranch": { ··· 133 137 "minLength": 40, 134 138 "maxLength": 40 135 139 }, 136 - "action": { 137 - "type": "string" 140 + "pull": { 141 + "type": "string", 142 + "format": "at-uri", 143 + "description": "AT-URI of the sh.tangled.repo.pull record this run belongs to" 138 144 } 139 145 } 140 146 },
+51 -17
spindle/db/pipelines.go
··· 9 9 10 10 "tangled.org/core/api/tangled" 11 11 "tangled.org/core/spindle/models" 12 + "tangled.org/core/workflow" 12 13 ) 13 14 14 15 func (d *DB) QueryPipelines(ctx context.Context, repoDid string, commits []string, cursor string, limit int) ([]*tangled.CiPipeline, string, int64, error) { ··· 17 18 } 18 19 19 20 var query string 20 - var args []interface{} 21 + var args []any 21 22 query = ` 22 23 select 23 24 rkey, event, created from events ··· 33 34 placeholders[i] = "?" 34 35 args = append(args, commits[i]) 35 36 } 36 - query += " and json_extract(event, '$.triggerMetadata.push.newSha') in (" + strings.Join(placeholders, ",") + ")" 37 + query += ` and coalesce( 38 + json_extract(event, '$.triggerMetadata.push.newSha'), 39 + json_extract(event, '$.triggerMetadata.pullRequest.sourceSha'), 40 + json_extract(event, '$.triggerMetadata.manual.sha') 41 + ) in (` + strings.Join(placeholders, ",") + ")" 37 42 } 38 43 39 44 if cursor != "" { ··· 75 80 continue 76 81 } 77 82 78 - p, err := d.mapToCiPipeline(ctx, rkey, created, rawPipeline) 83 + p, err := d.mapToCiPipeline(rkey, created, rawPipeline) 79 84 if err != nil { 80 85 return nil, "", 0, err 81 86 } ··· 113 118 return nil, err 114 119 } 115 120 116 - return d.mapToCiPipeline(ctx, rkey, created, rawPipeline) 121 + return d.mapToCiPipeline(rkey, created, rawPipeline) 117 122 } 118 123 119 - func (d *DB) mapToCiPipeline(ctx context.Context, rkey string, created int64, raw tangled.Pipeline) (*tangled.CiPipeline, error) { 124 + func (d *DB) mapToCiPipeline(rkey string, created int64, raw tangled.Pipeline) (*tangled.CiPipeline, error) { 120 125 createdAtStr := time.Unix(0, created).Format(time.RFC3339) 121 126 122 127 var repoDidStr string ··· 132 137 var trigger tangled.CiPipeline_Trigger 133 138 134 139 if raw.TriggerMetadata != nil { 135 - switch raw.TriggerMetadata.Kind { 136 - case "push": 140 + switch workflow.TriggerKind(raw.TriggerMetadata.Kind) { 141 + case workflow.TriggerKindPush: 137 142 if raw.TriggerMetadata.Push != nil { 138 143 commitSha = raw.TriggerMetadata.Push.NewSha 139 144 trigger.CiTrigger_Push = &tangled.CiTrigger_Push{ ··· 142 147 Ref: raw.TriggerMetadata.Push.Ref, 143 148 } 144 149 } 145 - case "pullRequest": 150 + case workflow.TriggerKindPullRequest: 146 151 if raw.TriggerMetadata.PullRequest != nil { 147 152 commitSha = raw.TriggerMetadata.PullRequest.SourceSha 148 153 trigger.CiTrigger_PullRequest = &tangled.CiTrigger_PullRequest{ 149 - Action: raw.TriggerMetadata.PullRequest.Action, 150 154 SourceBranch: &raw.TriggerMetadata.PullRequest.SourceBranch, 155 + SourceRepo: raw.TriggerMetadata.SourceRepo, 151 156 SourceSha: raw.TriggerMetadata.PullRequest.SourceSha, 152 157 TargetBranch: raw.TriggerMetadata.PullRequest.TargetBranch, 158 + Pull: raw.TriggerMetadata.PullRequest.Pull, 153 159 } 154 160 } 155 - case "manual": 161 + case workflow.TriggerKindManual: 156 162 if raw.TriggerMetadata.Manual != nil { 157 163 commitSha = raw.TriggerMetadata.Manual.Sha 158 - trigger.CiTrigger_Manual = &tangled.CiTrigger_Manual{} 164 + trigger.CiTrigger_Manual = &tangled.CiTrigger_Manual{ 165 + Inputs: pipelinePairsToCiTriggerPairs(raw.TriggerMetadata.Manual.Inputs), 166 + Ref: raw.TriggerMetadata.Manual.Ref, 167 + Sha: raw.TriggerMetadata.Manual.Sha, 168 + SourceRepo: raw.TriggerMetadata.SourceRepo, 169 + } 159 170 } 160 171 } 161 172 } ··· 192 203 }) 193 204 } 194 205 206 + var sourceRepo *string 207 + if raw.TriggerMetadata != nil { 208 + sourceRepo = raw.TriggerMetadata.SourceRepo 209 + } 210 + 195 211 return &tangled.CiPipeline{ 196 - Id: rkey, 197 - Commit: commitSha, 198 - Repo: &repoDidStr, 199 - CreatedAt: &createdAtStr, 200 - Trigger: &trigger, 201 - Workflows: workflows, 212 + Id: rkey, 213 + Commit: commitSha, 214 + Repo: &repoDidStr, 215 + CreatedAt: &createdAtStr, 216 + Trigger: &trigger, 217 + Workflows: workflows, 218 + SourceRepo: sourceRepo, 202 219 }, nil 220 + } 221 + 222 + func pipelinePairsToCiTriggerPairs(inputs []*tangled.Pipeline_Pair) []*tangled.CiTrigger_Pair { 223 + if len(inputs) == 0 { 224 + return nil 225 + } 226 + pairs := make([]*tangled.CiTrigger_Pair, 0, len(inputs)) 227 + for _, input := range inputs { 228 + if input == nil { 229 + continue 230 + } 231 + pairs = append(pairs, &tangled.CiTrigger_Pair{ 232 + Key: input.Key, 233 + Value: input.Value, 234 + }) 235 + } 236 + return pairs 203 237 } 204 238 205 239 func (d *DB) GetWorkflowTimes(workflowId models.WorkflowId) (startedAt, finishedAt *string) {
+6 -7
spindle/engine/engine.go
··· 27 27 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) { 28 28 l.Info("starting all workflows in parallel", "pipeline", pipelineId) 29 29 30 - // extract secrets 31 30 var allSecrets []secrets.UnlockedSecret 32 - if pipeline.RepoDid != "" { 31 + // never pass secrets to pipelines that run untrusted (e.g. fork) code 32 + if pipeline.TrustedSource && pipeline.RepoDid != "" { 33 33 if res, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(pipeline.RepoDid.String())); err == nil { 34 34 allSecrets = res 35 35 } 36 + } else if !pipeline.TrustedSource { 37 + l.Info("skipping secrets for untrusted pipeline source", "pipeline", pipelineId) 36 38 } 37 39 38 40 secretValues := make([]string, len(allSecrets)) ··· 51 53 l.Info("using workflow timeout", "timeout", workflowTimeout) 52 54 53 55 for _, w := range wfs { 54 - wg.Add(1) 55 - go func() { 56 - defer wg.Done() 57 - 56 + wg.Go(func() { 58 57 wid := models.WorkflowId{ 59 58 PipelineId: pipelineId, 60 59 Name: w.Name, ··· 169 168 if err != nil { 170 169 l.Error("failed to set workflow status to success", "wid", wid, "err", err) 171 170 } 172 - }() 171 + }) 173 172 } 174 173 } 175 174
+1 -3
spindle/models/clone.go
··· 111 111 112 112 // BuildRepoURL constructs the repository URL from repo metadata. 113 113 func BuildRepoURL(repo *tangled.Pipeline_TriggerRepo) string { 114 - if repo == nil { 114 + if repo == nil || repo.RepoDid == nil { 115 115 return "" 116 116 } 117 - 118 117 host, noSSL, _ := hostutil.ParseHostname(repo.Knot) 119 118 scheme := "https" 120 119 if noSSL { 121 120 scheme = "http" 122 121 } 123 - 124 122 return fmt.Sprintf("%s://%s/%s", scheme, host, *repo.RepoDid) 125 123 } 126 124
+35 -1
spindle/models/clone_test.go
··· 83 83 SourceSha: "pr-sha-789", 84 84 SourceBranch: "feature-branch", 85 85 TargetBranch: "main", 86 - Action: "opened", 87 86 }, 88 87 Repo: &tangled.Pipeline_TriggerRepo{ 89 88 Knot: "example.com", ··· 98 97 allCmds := strings.Join(step.Commands(), " ") 99 98 if !strings.Contains(allCmds, "pr-sha-789") { 100 99 t.Error("Commands should contain PR commit SHA") 100 + } 101 + } 102 + 103 + func TestBuildCloneStep_SourceRepo(t *testing.T) { 104 + twf := tangled.Pipeline_Workflow{ 105 + Clone: &tangled.Pipeline_CloneOpts{ 106 + Depth: 1, 107 + Skip: false, 108 + }, 109 + } 110 + sourceRepoDid := "did:plc:fork" 111 + tr := tangled.Pipeline_TriggerMetadata{ 112 + Kind: string(workflow.TriggerKindPullRequest), 113 + PullRequest: &tangled.Pipeline_PullRequestTriggerData{ 114 + SourceSha: "pr-sha-789", 115 + SourceBranch: "feature-branch", 116 + TargetBranch: "main", 117 + }, 118 + Repo: &tangled.Pipeline_TriggerRepo{ 119 + Knot: "fork.example.com", 120 + Did: "did:plc:user456", 121 + Repo: sp("fork-repo"), 122 + RepoDid: &sourceRepoDid, 123 + }, 124 + SourceRepo: &sourceRepoDid, 125 + } 126 + 127 + step := BuildCloneStep(twf, tr, false) 128 + 129 + allCmds := strings.Join(step.Commands(), " ") 130 + if !strings.Contains(allCmds, "https://fork.example.com/did:plc:fork") { 131 + t.Error("Commands should clone from source repo URL") 132 + } 133 + if strings.Contains(allCmds, "https://target.example.com/did:plc:target") { 134 + t.Error("Commands should not clone from target repo URL when sourceRepo is set") 101 135 } 102 136 } 103 137
+2
spindle/models/pipeline.go
··· 5 5 type Pipeline struct { 6 6 RepoDid syntax.DID 7 7 Workflows map[Engine][]Workflow 8 + // whether the code being ran was checked out from RepoDid itself 9 + TrustedSource bool 8 10 } 9 11 10 12 type Step interface {
+26 -11
spindle/models/pipeline_env.go
··· 10 10 11 11 // PipelineEnvVars builds the standard CI environment variables for a pipeline 12 12 func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId) map[string]string { 13 + return PipelineEnvVarsForSource(tr, pipelineId, nil) 14 + } 15 + 16 + func PipelineEnvVarsForSource(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId, sourceRepo *tangled.Pipeline_TriggerRepo) map[string]string { 13 17 if tr == nil { 14 18 return nil 15 19 } ··· 22 26 env["TANGLED_PIPELINE_ID"] = pipelineId.AtUri().String() 23 27 env["TANGLED_PIPELINE_KIND"] = tr.Kind 24 28 25 - // repo info 26 - if tr.Repo != nil { 27 - env["TANGLED_REPO_KNOT"] = tr.Repo.Knot 28 - env["TANGLED_REPO_DID"] = tr.Repo.Did 29 - if tr.Repo.Repo != nil { 30 - env["TANGLED_REPO_NAME"] = *tr.Repo.Repo 29 + if tr.SourceRepo != nil && *tr.SourceRepo != "" { 30 + env["TANGLED_PIPELINE_SOURCE"] = *tr.SourceRepo 31 + } else if tr.Repo != nil && tr.Repo.RepoDid != nil { 32 + env["TANGLED_PIPELINE_SOURCE"] = *tr.Repo.RepoDid 33 + } 34 + 35 + // prefer source repo, e.g. if this is a wf running on a fork 36 + repoInfo := tr.Repo 37 + if sourceRepo != nil { 38 + repoInfo = sourceRepo 39 + } 40 + if repoInfo != nil { 41 + env["TANGLED_REPO_KNOT"] = repoInfo.Knot 42 + env["TANGLED_REPO_DID"] = repoInfo.Did 43 + if repoInfo.Repo != nil { 44 + env["TANGLED_REPO_NAME"] = *repoInfo.Repo 31 45 } 32 - if tr.Repo.RepoDid != nil { 33 - env["TANGLED_REPO_REPO_DID"] = *tr.Repo.RepoDid 46 + if repoInfo.RepoDid != nil { 47 + env["TANGLED_REPO_REPO_DID"] = *repoInfo.RepoDid 34 48 } 35 - env["TANGLED_REPO_DEFAULT_BRANCH"] = tr.Repo.DefaultBranch 36 - env["TANGLED_REPO_URL"] = BuildRepoURL(tr.Repo) 49 + env["TANGLED_REPO_DEFAULT_BRANCH"] = repoInfo.DefaultBranch 50 + env["TANGLED_REPO_URL"] = BuildRepoURL(repoInfo) 37 51 } 38 52 39 53 switch workflow.TriggerKind(tr.Kind) { ··· 62 76 env["TANGLED_COMMIT_SHA"] = tr.PullRequest.SourceSha 63 77 64 78 // PR-specific env vars 79 + env["TANGLED_PIPELINE_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch 80 + env["TANGLED_PIPELINE_TARGET_BRANCH"] = tr.PullRequest.TargetBranch 65 81 env["TANGLED_PR_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch 66 82 env["TANGLED_PR_TARGET_BRANCH"] = tr.PullRequest.TargetBranch 67 83 env["TANGLED_PR_SOURCE_SHA"] = tr.PullRequest.SourceSha 68 - env["TANGLED_PR_ACTION"] = tr.PullRequest.Action 69 84 } 70 85 71 86 case workflow.TriggerKindManual:
+47 -3
spindle/models/pipeline_env_test.go
··· 110 110 SourceBranch: "feature-branch", 111 111 TargetBranch: "main", 112 112 SourceSha: "pr-sha-789", 113 - Action: "opened", 114 113 }, 115 114 Repo: &tangled.Pipeline_TriggerRepo{ 116 115 Knot: "example.com", ··· 145 144 } 146 145 147 146 // Check PR-specific variables 147 + if env["TANGLED_PIPELINE_SOURCE_BRANCH"] != "feature-branch" { 148 + t.Errorf("Expected TANGLED_PIPELINE_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PIPELINE_SOURCE_BRANCH"]) 149 + } 150 + if env["TANGLED_PIPELINE_TARGET_BRANCH"] != "main" { 151 + t.Errorf("Expected TANGLED_PIPELINE_TARGET_BRANCH='main', got '%s'", env["TANGLED_PIPELINE_TARGET_BRANCH"]) 152 + } 148 153 if env["TANGLED_PR_SOURCE_BRANCH"] != "feature-branch" { 149 154 t.Errorf("Expected TANGLED_PR_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PR_SOURCE_BRANCH"]) 150 155 } ··· 154 159 if env["TANGLED_PR_SOURCE_SHA"] != "pr-sha-789" { 155 160 t.Errorf("Expected TANGLED_PR_SOURCE_SHA='pr-sha-789', got '%s'", env["TANGLED_PR_SOURCE_SHA"]) 156 161 } 157 - if env["TANGLED_PR_ACTION"] != "opened" { 158 - t.Errorf("Expected TANGLED_PR_ACTION='opened', got '%s'", env["TANGLED_PR_ACTION"]) 162 + } 163 + 164 + func TestPipelineEnvVars_SourceRepo(t *testing.T) { 165 + sourceRepoDid := "did:plc:fork" 166 + tr := &tangled.Pipeline_TriggerMetadata{ 167 + Kind: string(workflow.TriggerKindPullRequest), 168 + PullRequest: &tangled.Pipeline_PullRequestTriggerData{ 169 + SourceBranch: "feature-branch", 170 + TargetBranch: "main", 171 + SourceSha: "pr-sha-789", 172 + }, 173 + Repo: &tangled.Pipeline_TriggerRepo{ 174 + Knot: "target.example.com", 175 + Did: "did:plc:user123", 176 + Repo: sp("target-repo"), 177 + RepoDid: sp("did:plc:target"), 178 + }, 179 + SourceRepo: &sourceRepoDid, 180 + } 181 + sourceRepo := &tangled.Pipeline_TriggerRepo{ 182 + Knot: "fork.example.com", 183 + Did: "did:plc:user456", 184 + Repo: sp("fork-repo"), 185 + RepoDid: &sourceRepoDid, 186 + DefaultBranch: "feature-branch", 187 + } 188 + id := PipelineId{ 189 + Knot: "target.example.com", 190 + Rkey: "123123", 191 + } 192 + 193 + env := PipelineEnvVarsForSource(tr, id, sourceRepo) 194 + 195 + if env["TANGLED_PIPELINE_SOURCE"] != sourceRepoDid { 196 + t.Errorf("Expected TANGLED_PIPELINE_SOURCE='%s', got '%s'", sourceRepoDid, env["TANGLED_PIPELINE_SOURCE"]) 197 + } 198 + if env["TANGLED_REPO_URL"] != "https://fork.example.com/did:plc:fork" { 199 + t.Errorf("Expected TANGLED_REPO_URL to point at source repo, got '%s'", env["TANGLED_REPO_URL"]) 200 + } 201 + if env["TANGLED_REPO_REPO_DID"] != sourceRepoDid { 202 + t.Errorf("Expected TANGLED_REPO_REPO_DID='%s', got '%s'", sourceRepoDid, env["TANGLED_REPO_REPO_DID"]) 159 203 } 160 204 } 161 205
+119 -33
spindle/server.go
··· 28 28 "tangled.org/core/log" 29 29 "tangled.org/core/notifier" 30 30 "tangled.org/core/rbac" 31 + "tangled.org/core/repoverify" 31 32 "tangled.org/core/spindle/config" 32 33 "tangled.org/core/spindle/db" 33 34 "tangled.org/core/spindle/engine" ··· 64 65 cfg *config.Config 65 66 ks *eventconsumer.Consumer 66 67 res *idresolver.Resolver 68 + verify repoverify.Verifier 67 69 vault secrets.Manager 68 70 motd []byte 69 71 motdMu sync.RWMutex ··· 156 158 jq: jq, 157 159 cfg: cfg, 158 160 res: resolver, 161 + verify: repoverify.New(resolver, cfg.Server.Dev), 159 162 vault: vault, 160 163 motd: defaultMotd, 161 164 rootCtx: ctx, ··· 447 450 Repo: triggerRepo, 448 451 } 449 452 450 - pipelineId, err := s.runPipeline(ctx, repoDid, trigger, event.ChangedFiles, repoCloneUri, repoPath, event.NewSha, nil) 453 + pipelineId, err := s.runPipeline(ctx, repoDid, trigger, event.ChangedFiles, repoCloneUri, repoPath, event.NewSha, nil, triggerRepo) 451 454 if err != nil { 452 455 return err 453 456 } ··· 463 466 464 467 // buildTriggerRepo gathers trigger metadata, resolving default branch from the knot 465 468 func (s *Spindle) buildTriggerRepo(ctx context.Context, repo *db.Repo) (*tangled.Pipeline_TriggerRepo, error) { 469 + rkey := string(repo.Rkey) 470 + repoDid := repo.RepoDid.String() 471 + return s.buildTriggerRepoFrom(ctx, repo.Knot, repo.Owner.String(), rkey, repoDid), nil 472 + } 473 + 474 + func (s *Spindle) buildTriggerRepoFrom(ctx context.Context, knot, did, rkey, repoDid string) *tangled.Pipeline_TriggerRepo { 466 475 scheme := "https" 467 476 if s.cfg.Server.Dev { 468 477 scheme = "http" 469 478 } 470 - client := &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, repo.Knot)} 479 + client := &indigoxrpc.Client{Host: fmt.Sprintf("%s://%s", scheme, knot)} 471 480 472 - // todo(dawn): this should be in the refUpdate event itself to save a roundtrip 481 + // this should maybe (?) be in the refUpdate event itself to save a roundtrip 473 482 defaultBranch := "" 474 - if out, err := tangled.RepoGetDefaultBranch(ctx, client, repo.RepoDid.String()); err == nil { 483 + if out, err := tangled.RepoGetDefaultBranch(ctx, client, repoDid); err == nil { 475 484 defaultBranch = out.Name 476 485 } 477 486 478 - rkey := string(repo.Rkey) 479 - repoDid := repo.RepoDid.String() 487 + var rkeyPtr *string 488 + if rkey != "" { 489 + rkeyPtr = &rkey 490 + } 480 491 return &tangled.Pipeline_TriggerRepo{ 481 - Did: repo.Owner.String(), 482 - Knot: repo.Knot, 483 - Repo: &rkey, 492 + Did: did, 493 + Knot: knot, 494 + Repo: rkeyPtr, 484 495 RepoDid: &repoDid, 485 496 DefaultBranch: defaultBranch, 486 - }, nil 497 + } 498 + } 499 + 500 + func (s *Spindle) resolvePipelineSourceRepo(ctx context.Context, trigger *tangled.Pipeline_TriggerMetadata) (*tangled.Pipeline_TriggerRepo, error) { 501 + if trigger == nil { 502 + return nil, nil 503 + } 504 + if trigger.SourceRepo == nil || *trigger.SourceRepo == "" { 505 + return trigger.Repo, nil 506 + } 507 + repoDid, err := syntax.ParseDID(*trigger.SourceRepo) 508 + if err != nil { 509 + return nil, fmt.Errorf("parse sourceRepo %s: %w", *trigger.SourceRepo, err) 510 + } 511 + return s.resolveSourceRepoInfo(ctx, repoDid) 487 512 } 488 513 489 - // runPipeline compiles and enqueues the pipeline for the given revision 490 - 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) { 514 + // resolveSourceRepoInfo resolves trigger-repo metadata for a source repo DID. 515 + func (s *Spindle) resolveSourceRepoInfo(ctx context.Context, repoDid syntax.DID) (*tangled.Pipeline_TriggerRepo, error) { 516 + repo, err := s.db.GetRepoByDid(repoDid) 517 + if err == nil { 518 + return s.buildTriggerRepo(ctx, repo) 519 + } 520 + 521 + // verify repo, we don't want git sync to point to arbitrary endpoints 522 + res, err := s.verify(ctx, repoverify.RepoDid(repoDid)) 523 + if err != nil { 524 + return nil, fmt.Errorf("verify sourceRepo %s: %w", repoDid, err) 525 + } 526 + return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host, res.OwnerDid.String(), res.Rkey, repoDid.String()), nil 527 + } 528 + 529 + // runPipeline compiles and enqueues the pipeline for the given revision. 530 + // sourceRepo is the resolved repo the code was checked out from, forwarded to 531 + // processPipeline for env vars. 532 + 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) { 491 533 l := log.FromContext(ctx) 492 534 493 535 compiler := workflow.Compiler{ ··· 526 568 if err := s.db.CreatePipelineEvent(pipelineId.Rkey, tpl, s.n); err != nil { 527 569 return models.PipelineId{}, fmt.Errorf("creating pipeline event: %w", err) 528 570 } 529 - err = s.processPipeline(repoDid, tpl, pipelineId) 571 + err = s.processPipeline(repoDid, tpl, pipelineId, sourceRepo) 530 572 return pipelineId, err 531 573 } 532 574 ··· 548 590 return filtered 549 591 } 550 592 551 - // TriggerManual dispatches a pipeline manually at sha 552 - func (s *Spindle) TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string) (syntax.ATURI, error) { 593 + // TriggerManual dispatches a pipeline at sha, authorized against and recorded 594 + // under repoDid. sourceRepo, pull, and inputs are optional trigger payload. 595 + 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) { 553 596 repo, err := s.db.GetRepoByDid(repoDid) 554 597 if err != nil { 555 598 return "", fmt.Errorf("unknown repoDid %s: %w", repoDid, err) ··· 560 603 return "", fmt.Errorf("building trigger repo: %w", err) 561 604 } 562 605 563 - var refPtr *string 564 - if ref != "" { 565 - refPtr = &ref 566 - } 567 - trigger := tangled.Pipeline_TriggerMetadata{ 568 - Kind: string(workflow.TriggerKindManual), 569 - Manual: &tangled.Pipeline_ManualTriggerData{ 570 - Sha: sha, 571 - Ref: refPtr, 572 - }, 573 - Repo: triggerRepo, 606 + trigger := tangled.Pipeline_TriggerMetadata{Repo: triggerRepo} 607 + if pull.IsPullRequest { 608 + var pullAt *string 609 + if pull.Pull != "" { 610 + pullAtStr := pull.Pull.String() 611 + pullAt = &pullAtStr 612 + } 613 + trigger.Kind = string(workflow.TriggerKindPullRequest) 614 + trigger.PullRequest = &tangled.Pipeline_PullRequestTriggerData{ 615 + SourceBranch: pull.SourceBranch, 616 + TargetBranch: pull.TargetBranch, 617 + SourceSha: sha, 618 + Pull: pullAt, 619 + } 620 + } else { 621 + var refPtr *string 622 + if ref != "" { 623 + refPtr = &ref 624 + } 625 + trigger.Kind = string(workflow.TriggerKindManual) 626 + trigger.Manual = &tangled.Pipeline_ManualTriggerData{ 627 + Sha: sha, 628 + Ref: refPtr, 629 + Inputs: inputs, 630 + } 574 631 } 575 632 576 633 repoCloneUri := s.newRepoCloneUrl(repo.Knot, repoDid) 577 634 repoPath := s.newRepoPath(repoDid) 635 + sourceInfo := triggerRepo // default: code comes from the repo itself 636 + if sourceRepo != "" && sourceRepo != repoDid { 637 + sourceInfo, err = s.resolveSourceRepoInfo(ctx, sourceRepo) 638 + if err != nil { 639 + return "", err 640 + } 641 + sourceRepoStr := sourceRepo.String() 642 + trigger.SourceRepo = &sourceRepoStr 643 + repoCloneUri = models.BuildRepoURL(sourceInfo) 644 + repoPath = s.newRepoPath(sourceRepo) 645 + } 578 646 579 - pipelineId, err := s.runPipeline(ctx, repoDid, trigger, nil, repoCloneUri, repoPath, sha, workflows) 647 + pipelineId, err := s.runPipeline(ctx, repoDid, trigger, nil, repoCloneUri, repoPath, sha, workflows, sourceInfo) 580 648 if err != nil { 581 649 return "", err 582 650 } ··· 624 692 return rawPipeline, nil 625 693 } 626 694 627 - func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipelineId models.PipelineId) error { 628 - // Build pipeline environment variables once for all workflows 629 - pipelineEnv := models.PipelineEnvVars(tpl.TriggerMetadata, pipelineId) 695 + // processPipeline enqueues the workflows in tpl. 696 + func (s *Spindle) processPipeline(repoDid syntax.DID, tpl tangled.Pipeline, pipelineId models.PipelineId, sourceRepo *tangled.Pipeline_TriggerRepo) error { 697 + // derive security-relevant things like whether this run is trusted and can be passed 698 + // secrets to from the original metadata. 699 + pipelineEnv := models.PipelineEnvVarsForSource(tpl.TriggerMetadata, pipelineId, sourceRepo) 700 + trustedSource := true 701 + if tm := tpl.TriggerMetadata; tm != nil && tm.SourceRepo != nil && 702 + *tm.SourceRepo != "" && *tm.SourceRepo != repoDid.String() { 703 + trustedSource = false 704 + } 705 + 706 + // swap the repo with our sourceRepo if we are running a pipeline on a fork. 707 + // the metadata stays the same. we check whether the repo is trusted above, 708 + // so this only affects the clone URL. 709 + initTpl := tpl 710 + if sourceRepo != nil && tpl.TriggerMetadata != nil { 711 + tm := *tpl.TriggerMetadata 712 + tm.Repo = sourceRepo 713 + initTpl.TriggerMetadata = &tm 714 + } 630 715 631 716 // filter & init workflows 632 717 workflows := make(map[models.Engine][]models.Workflow) ··· 647 732 continue 648 733 } 649 734 650 - ewf, err := eng.InitWorkflow(*w, tpl) 735 + ewf, err := eng.InitWorkflow(*w, initTpl) 651 736 if err != nil { 652 737 err = s.db.StatusFailed(models.WorkflowId{ 653 738 PipelineId: pipelineId, ··· 674 759 ok := s.jq.Enqueue(repoDid, queue.Job{ 675 760 Run: func() error { 676 761 engine.StartWorkflows(log.SubLogger(s.l, "engine"), s.vault, s.cfg, s.db, s.n, s.rootCtx, &models.Pipeline{ 677 - RepoDid: repoDid, 678 - Workflows: workflows, 762 + RepoDid: repoDid, 763 + Workflows: workflows, 764 + TrustedSource: trustedSource, 679 765 }, pipelineId) 680 766 return nil 681 767 },
+6 -2
spindle/tapclient.go
··· 383 383 Trigger: tangled.Pipeline_TriggerMetadata{ 384 384 Kind: string(workflow.TriggerKindPullRequest), 385 385 PullRequest: &tangled.Pipeline_PullRequestTriggerData{ 386 - Action: "create", 387 386 SourceBranch: record.Source.Branch, 388 387 SourceSha: sourceSha, 389 388 TargetBranch: record.Target.Branch, ··· 433 432 l.Error("failed to create pipeline event", "err", err) 434 433 return nil 435 434 } 436 - err = t.spindle.processPipeline(repo.RepoDid, tpl, pipelineId) 435 + sourceRepo, err := t.spindle.resolvePipelineSourceRepo(ctx, tpl.TriggerMetadata) 436 + if err != nil { 437 + l.Error("failed resolving pipeline source repo", "err", err) 438 + return nil 439 + } 440 + err = t.spindle.processPipeline(repo.RepoDid, tpl, pipelineId, sourceRepo) 437 441 if err != nil { 438 442 // don't retry 439 443 l.Error("failed processing pipeline", "err", err)
+109 -30
spindle/xrpc/ci_pipeline_trigger_pipeline.go
··· 7 7 "fmt" 8 8 "net/http" 9 9 10 - "github.com/bluesky-social/indigo/api/atproto" 11 10 "github.com/bluesky-social/indigo/atproto/syntax" 12 - "github.com/bluesky-social/indigo/xrpc" 13 11 14 12 "tangled.org/core/api/tangled" 15 13 "tangled.org/core/rbac" ··· 36 34 return 37 35 } 38 36 39 - if len(input.Sha) != 40 { 40 - fail(xrpcerr.GenericError(fmt.Errorf("sha must be a 40-character commit hash"))) 41 - return 42 - } 43 - 44 37 repoDid, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo) 45 38 if !ok { 46 39 fail(xerr) 47 40 return 48 41 } 49 42 43 + var sha string 50 44 ref := "" 51 - if input.Ref != nil { 52 - ref = *input.Ref 45 + var sourceRepo syntax.DID 46 + var pull PullContext 47 + var inputs []*tangled.Pipeline_Pair 48 + 49 + switch { 50 + case input.Trigger == nil: 51 + fail(xrpcerr.GenericError(fmt.Errorf("trigger is required"))) 52 + return 53 + 54 + case input.Trigger.CiTrigger_Manual != nil: 55 + manual := input.Trigger.CiTrigger_Manual 56 + sha = manual.Sha 57 + if manual.Ref != nil { 58 + ref = *manual.Ref 59 + } 60 + parsed, err := parseOptionalDID("sourceRepo", manual.SourceRepo) 61 + if err != nil { 62 + fail(xrpcerr.GenericError(err)) 63 + return 64 + } 65 + sourceRepo = parsed 66 + inputs = ciTriggerPairsToPipelinePairs(manual.Inputs) 67 + 68 + case input.Trigger.CiTrigger_PullRequest != nil: 69 + pr := input.Trigger.CiTrigger_PullRequest 70 + sha = pr.SourceSha 71 + parsed, err := parseOptionalDID("sourceRepo", pr.SourceRepo) 72 + if err != nil { 73 + fail(xrpcerr.GenericError(err)) 74 + return 75 + } 76 + sourceRepo = parsed 77 + 78 + if pr.TargetBranch == "" { 79 + fail(xrpcerr.GenericError(fmt.Errorf("pull request trigger targetBranch is required"))) 80 + return 81 + } 82 + 83 + var pullAt syntax.ATURI 84 + if pr.Pull != nil { 85 + var err error 86 + pullAt, err = syntax.ParseATURI(*pr.Pull) 87 + if err != nil { 88 + fail(xrpcerr.InvalidRepoError(*pr.Pull)) 89 + return 90 + } 91 + } 92 + sourceBranch := "" 93 + if pr.SourceBranch != nil { 94 + sourceBranch = *pr.SourceBranch 95 + } 96 + pull = PullContext{ 97 + IsPullRequest: true, 98 + Pull: pullAt, 99 + SourceBranch: sourceBranch, 100 + TargetBranch: pr.TargetBranch, 101 + } 102 + 103 + default: 104 + fail(xrpcerr.GenericError(fmt.Errorf("unsupported trigger variant"))) 105 + return 106 + } 107 + 108 + if len(sha) != 40 { 109 + fail(xrpcerr.GenericError(fmt.Errorf("sha must be a 40-character commit hash"))) 110 + return 53 111 } 54 112 55 - pipelineAt, err := x.Trigger.TriggerManual(r.Context(), repoDid, input.Sha, ref, input.Workflows) 113 + pipelineAt, err := x.Trigger.TriggerManual(r.Context(), repoDid, sha, ref, input.Workflows, sourceRepo, pull, inputs) 56 114 if errors.Is(err, ErrNoMatchingWorkflows) { 57 115 fail(xrpcerr.GenericError(err)) 58 116 return ··· 69 127 } 70 128 } 71 129 72 - // resolveOwnedRepo resolves a repo AT-URI to DID and checks owner auth 73 - func (x *Xrpc) resolveOwnedRepo(ctx context.Context, actorDid syntax.DID, repoAtUri string) (syntax.DID, xrpcerr.XrpcError, bool) { 74 - repoAt, err := syntax.ParseATURI(repoAtUri) 130 + func parseOptionalDID(field string, value *string) (syntax.DID, error) { 131 + if value == nil || *value == "" { 132 + return "", nil 133 + } 134 + did, err := syntax.ParseDID(*value) 75 135 if err != nil { 76 - return "", xrpcerr.InvalidRepoError(repoAtUri), false 136 + return "", fmt.Errorf("invalid %s DID %q: %w", field, *value, err) 77 137 } 138 + return did, nil 139 + } 78 140 79 - ident, err := x.Resolver.ResolveIdent(ctx, repoAt.Authority().String()) 80 - if err != nil || ident.Handle.IsInvalidHandle() { 81 - return "", xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)), false 141 + func ciTriggerPairsToPipelinePairs(inputs []*tangled.CiTrigger_Pair) []*tangled.Pipeline_Pair { 142 + if len(inputs) == 0 { 143 + return nil 82 144 } 83 - 84 - xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 85 - resp, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 86 - if err != nil { 87 - return "", xrpcerr.GenericError(err), false 145 + pairs := make([]*tangled.Pipeline_Pair, 0, len(inputs)) 146 + for _, input := range inputs { 147 + if input == nil { 148 + continue 149 + } 150 + pairs = append(pairs, &tangled.Pipeline_Pair{ 151 + Key: input.Key, 152 + Value: input.Value, 153 + }) 88 154 } 155 + return pairs 156 + } 89 157 90 - repoRec, ok := resp.Value.Val.(*tangled.Repo) 158 + // resolveOwnedRepo resolves a repository DID and checks push auth. 159 + func (x *Xrpc) resolveOwnedRepo(ctx context.Context, actorDid syntax.DID, repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) { 160 + repoDid, xerr, ok := x.resolveKnownRepoDid(repoDidStr) 91 161 if !ok { 92 - return "", xrpcerr.RepoNotFoundError, false 93 - } 94 - if repoRec.RepoDid == nil || *repoRec.RepoDid == "" { 95 - return "", xrpcerr.GenericError(fmt.Errorf("repo record %s has no repoDid", repoAt)), false 162 + return "", xerr, false 96 163 } 97 - repoDid := *repoRec.RepoDid 98 164 99 - isPushAllowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid) 165 + isPushAllowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid.String()) 100 166 if err != nil || !isPushAllowed { 101 167 return "", xrpcerr.AccessControlError(actorDid.String()), false 102 168 } 103 169 104 - return syntax.DID(repoDid), xrpcerr.XrpcError{}, true 170 + return repoDid, xrpcerr.XrpcError{}, true 171 + } 172 + 173 + func (x *Xrpc) resolveKnownRepoDid(repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) { 174 + repoDid, err := syntax.ParseDID(repoDidStr) 175 + if err != nil { 176 + return "", xrpcerr.GenericError(fmt.Errorf("invalid repo DID %q: %w", repoDidStr, err)), false 177 + } 178 + 179 + if _, err := x.Db.GetRepoByDid(repoDid); err != nil { 180 + return "", xrpcerr.RepoNotFoundError, false 181 + } 182 + 183 + return repoDid, xrpcerr.XrpcError{}, true 105 184 }
+35 -20
spindle/xrpc/pipeline_cancel_pipeline.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "net/http" 7 - "strings" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" 10 9 "tangled.org/core/api/tangled" ··· 26 25 return 27 26 } 28 27 29 - var input tangled.CiPipelineCancelPipeline_Input 28 + var input tangled.CiCancelPipeline_Input 30 29 if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 31 30 fail(xrpcerr.GenericError(err)) 32 31 return 33 32 } 34 33 35 - aturi := syntax.ATURI(input.Pipeline) 34 + pipelineTid, err := syntax.ParseTID(input.Pipeline) 35 + if err != nil { 36 + fail(xrpcerr.GenericError(fmt.Errorf("invalid pipeline TID %q: %w", input.Pipeline, err))) 37 + return 38 + } 39 + 40 + repoDid, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo) 41 + if !ok { 42 + fail(xerr) 43 + return 44 + } 45 + repo, err := x.Db.GetRepoByDid(repoDid) 46 + if err != nil { 47 + fail(xrpcerr.GenericError(fmt.Errorf("failed to get repo: %w", err))) 48 + return 49 + } 50 + 51 + // the actor is only authorized against input.Repo, so make sure the 52 + // pipeline actually belongs to it before cancelling anything 53 + p, err := x.Db.GetPipeline(r.Context(), pipelineTid.String()) 54 + if err != nil { 55 + fail(xrpcerr.GenericError(fmt.Errorf("failed to get pipeline: %w", err))) 56 + return 57 + } 58 + if p.Repo == nil || *p.Repo != repoDid.String() { 59 + fail(xrpcerr.AccessControlError(actorDid.String())) 60 + return 61 + } 62 + 36 63 pipelineId := models.PipelineId{ 37 - Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"), 38 - Rkey: aturi.RecordKey().String(), 64 + Knot: repo.Knot, 65 + Rkey: pipelineTid.String(), 39 66 } 40 67 41 - var workflows []string 42 - if len(input.Workflows) > 0 { 43 - workflows = input.Workflows 44 - } else { 45 - // fetch workflows from db if none are specified 46 - p, err := x.Db.GetPipeline(r.Context(), pipelineId.Rkey) 47 - if err != nil { 48 - fail(xrpcerr.GenericError(fmt.Errorf("failed to get pipeline: %w", err))) 49 - return 50 - } 68 + workflows := input.Workflows 69 + if len(workflows) == 0 { 70 + // cancel every workflow when none are specified 51 71 for _, w := range p.Workflows { 52 72 workflows = append(workflows, w.Name) 53 73 } 54 - } 55 - 56 - if _, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo); !ok { 57 - fail(xerr) 58 - return 59 74 } 60 75 61 76 for _, wName := range workflows {
+10 -7
spindle/xrpc/xrpc.go
··· 25 25 26 26 const ActorDid = serviceauth.ActorDid 27 27 28 - // ErrNoMatchingWorkflows is returned when a manual dispatch resolves to no 29 - // workflows to run: the repo defines none at the requested commit, or none of 30 - // the requested workflow names exist. 31 28 var ErrNoMatchingWorkflows = errors.New("no workflows to run") 32 29 33 - // PipelineTrigger builds and enqueues a manually-dispatched pipeline. It is 34 - // implemented by *spindle.Spindle, which owns the queue and engines; the xrpc 35 - // handler only does auth and input validation before delegating here. 30 + // this is to break an import cycle. spindle imports this package for Xrpc, 31 + // so this package can't import *spindle.Spindle back. 36 32 type PipelineTrigger interface { 37 - TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string) (syntax.ATURI, error) 33 + TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string, sourceRepo syntax.DID, pull PullContext, inputs []*tangled.Pipeline_Pair) (syntax.ATURI, error) 34 + } 35 + 36 + type PullContext struct { 37 + IsPullRequest bool 38 + Pull syntax.ATURI 39 + SourceBranch string 40 + TargetBranch string 38 41 } 39 42 40 43 type Xrpc struct {
+23 -4
types/pipeline.go
··· 106 106 } 107 107 if t.CiPipeline_Trigger.CiTrigger_Push != nil { 108 108 ref := t.CiPipeline_Trigger.CiTrigger_Push.Ref 109 - if strings.HasPrefix(ref, "refs/heads/") { 110 - return strings.TrimPrefix(ref, "refs/heads/") 109 + if after, ok := strings.CutPrefix(ref, "refs/heads/"); ok { 110 + return after 111 111 } 112 - if strings.HasPrefix(ref, "refs/tags/") { 113 - return strings.TrimPrefix(ref, "refs/tags/") 112 + if after, ok := strings.CutPrefix(ref, "refs/tags/"); ok { 113 + return after 114 114 } 115 115 return ref 116 116 } ··· 129 129 return "" 130 130 } 131 131 return *sb 132 + } 133 + 134 + func (t Trigger) PRUri() string { 135 + if t.CiPipeline_Trigger == nil || t.CiPipeline_Trigger.CiTrigger_PullRequest == nil { 136 + return "" 137 + } 138 + pull := t.CiPipeline_Trigger.CiTrigger_PullRequest.Pull 139 + if pull == nil { 140 + return "" 141 + } 142 + return *pull 132 143 } 133 144 134 145 type Pipeline struct { ··· 240 251 return "" 241 252 } 242 253 return p.CiPipeline.Commit 254 + } 255 + 256 + // where the pipeline commit was checked out from, nil when checked out from repo itself 257 + func (p Pipeline) SourceRepo() *string { 258 + if p.CiPipeline == nil { 259 + return nil 260 + } 261 + return p.CiPipeline.SourceRepo 243 262 } 244 263 245 264 func (p Pipeline) Workflows() []string {