Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions pkg/cmd/ci/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,7 @@ func injectPatchStep(jobs map[string]interface{}, jobName, mergeBase, workspaceP
}
stepsRaw, ok := job["steps"]
if !ok {
if _, ok := job["uses"].(string); ok {
fmt.Fprintf(os.Stderr, "Warning: job %q calls a reusable workflow, so your local changes were not applied. This job will run the code at %s.\n", jobName, mergeBase)
} else {
if _, ok := job["uses"].(string); !ok {
fmt.Fprintf(os.Stderr, "Warning: job %q has no steps, skipping patch injection.\n", jobName)
}
return
Expand Down
133 changes: 133 additions & 0 deletions pkg/cmd/ci/run_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package ci

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"connectrpc.com/connect"
cachev1 "github.com/depot/cli/pkg/proto/depot/cache/v1"
"github.com/depot/cli/pkg/proto/depot/cache/v1/cachev1connect"
civ1 "github.com/depot/cli/pkg/proto/depot/ci/v1"
"github.com/depot/cli/pkg/proto/depot/ci/v1/civ1connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"gopkg.in/yaml.v3"
)

// initBareRemote creates a bare git repo with one commit and returns its path.
Expand Down Expand Up @@ -504,6 +515,128 @@ func TestInjectPatchStep_pinsCheckoutToMergeBase(t *testing.T) {
}
}

type reusablePatchRunService struct {
cachev1connect.UnimplementedCacheServiceHandler
civ1connect.UnimplementedCIServiceHandler
cacheKey string
cacheContent []byte
runRequest *civ1.RunRequest
presignedURL string
}

func (s *reusablePatchRunService) CreateEntry(_ context.Context, req *connect.Request[cachev1.CreateEntryRequest]) (*connect.Response[cachev1.CreateEntryResponse], error) {
s.cacheKey = req.Msg.GetKey()
return connect.NewResponse(&cachev1.CreateEntryResponse{
EntryId: "entry",
UploadPartUrls: []string{s.presignedURL},
}), nil
}

func (s *reusablePatchRunService) FinalizeEntry(context.Context, *connect.Request[cachev1.FinalizeEntryRequest]) (*connect.Response[cachev1.FinalizeEntryResponse], error) {
return connect.NewResponse(&cachev1.FinalizeEntryResponse{}), nil
}

func (s *reusablePatchRunService) Run(_ context.Context, req *connect.Request[civ1.RunRequest]) (*connect.Response[civ1.RunResponse], error) {
s.runRequest = req.Msg
return connect.NewResponse(&civ1.RunResponse{OrgId: "org", RunId: "run"}), nil
}

func TestRun_reusableJobKeepsPatchKeyWithoutWarning(t *testing.T) {
service := &reusablePatchRunService{}
mux := http.NewServeMux()
cachePath, cacheHandler := cachev1connect.NewCacheServiceHandler(service)
mux.Handle(cachePath, cacheHandler)
ciPath, ciHandler := civ1connect.NewCIServiceHandler(service)
mux.Handle(ciPath, ciHandler)
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
service.cacheContent, _ = io.ReadAll(r.Body)
w.Header().Set("ETag", `"test-etag"`)
})
server := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{}))
t.Cleanup(server.Close)
service.presignedURL = server.URL + "/upload"
t.Setenv("DEPOT_API_URL", server.URL)
t.Setenv("DEPOT_CACHE_HOST", server.URL)

bare := initBareRemote(t)
repo := cloneRepo(t, bare)
workflowPath := filepath.Join(repo, ".depot", "workflows", "ci.yml")
if err := os.MkdirAll(filepath.Dir(workflowPath), 0755); err != nil {
t.Fatal(err)
}
writeFile(t, workflowPath, `name: ci
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo build
reusable:
uses: ./.depot/workflows/reusable.yml
`)
run(t, repo, "git", "add", ".depot/workflows/ci.yml")
run(t, repo, "git", "commit", "-m", "add workflow")
run(t, repo, "git", "push", "origin", "HEAD")

writeFile(t, filepath.Join(repo, "README.md"), "local changes")
stderrFile, err := os.CreateTemp(t.TempDir(), "stderr-")
if err != nil {
t.Fatal(err)
}
originalStderr := os.Stderr
os.Stderr = stderrFile
t.Cleanup(func() {
os.Stderr = originalStderr
})

cmd := NewCmdRun()
cmd.SetArgs([]string{
"--workflow", workflowPath,
"--job", "build",
"--job", "reusable",
"--repo", "owner/repo",
"--forge", "github",
"--token", "test-token",
"--org", "org",
})
if err := cmd.ExecuteContext(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := stderrFile.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
stderr, err := io.ReadAll(stderrFile)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(stderr), "calls a reusable workflow") {
t.Fatalf("reusable workflow warning = %q, want no warning", stderr)
}
if service.runRequest == nil {
t.Fatal("CI run request was not sent")
}
if service.runRequest.GetWorkspacePatchCacheKey() == "" || service.runRequest.GetWorkspacePatchCacheKey() != service.cacheKey {
t.Fatalf("run patch key = %q, uploaded cache key = %q", service.runRequest.GetWorkspacePatchCacheKey(), service.cacheKey)
}
if len(service.cacheContent) == 0 {
t.Fatal("workspace patch was not uploaded")
}

var workflow map[string]interface{}
if err := yaml.Unmarshal([]byte(service.runRequest.GetWorkflowContent()[0]), &workflow); err != nil {
t.Fatal(err)
}
jobs := workflow["jobs"].(map[string]interface{})
if got := jobs["reusable"].(map[string]interface{}); len(got) != 1 || got["uses"] != "./.depot/workflows/reusable.yml" {
t.Fatalf("reusable call changed: %#v", got)
}
steps := jobs["build"].(map[string]interface{})["steps"].([]interface{})
if len(steps) != 3 || steps[1].(map[string]interface{})["name"] != "Apply local patch from Depot Cache" {
t.Fatalf("ordinary job patch steps = %#v", steps)
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

// Depot CI checks out from Depot Code only when the checkout step has no inputs,
// so a Depot Code run must not add one.
func TestInjectPatchStep_leavesDepotCodeCheckoutUntouched(t *testing.T) {
Expand Down
Loading