Skip to content

Commit 042fbb3

Browse files
authored
Merge pull request #477 from depot/dep-3975
feat(ci): send workspace_patch_cache_key when uploading a workspace patch
2 parents 7f31d71 + 170bca1 commit 042fbb3

4 files changed

Lines changed: 339 additions & 170 deletions

File tree

pkg/cmd/ci/run.go

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,25 @@ This command is in beta and subject to change.`,
162162
// Detect local changes as a patch
163163
patch := detectPatch(workflowDir)
164164

165+
var workspacePatchKey string
165166
if patch != nil {
167+
if err := validateWorkspacePatch(patch); err != nil {
168+
return err
169+
}
170+
workspacePatchKey = patchWorkspaceCacheKey(patch)
166171
fmt.Printf("Base: %s\n", patch.baseBranch)
167172
fmt.Printf("Merge base: %s\n", patch.mergeBase)
168173
fmt.Printf("Patch size: %d bytes\n", len(patch.content))
174+
fmt.Printf("Cache key: %s\n", workspacePatchKey)
169175

170-
hash := sha256.Sum256([]byte(patch.content))
171-
patchHash := fmt.Sprintf("%x", hash)[:16]
172-
cacheKey := fmt.Sprintf("patch/%s/%s", patch.mergeBase[:12], patchHash)
173-
fmt.Printf("Cache key: %s\n", cacheKey)
174-
175-
if err := api.UploadCacheEntry(ctx, tokenVal, orgID, cacheKey, []byte(patch.content)); err != nil {
176+
if err := api.UploadCacheEntry(ctx, tokenVal, orgID, workspacePatchKey, []byte(patch.content)); err != nil {
176177
return fmt.Errorf("failed to upload patch: %w", err)
177178
}
178179
fmt.Println("Patch uploaded to Depot Cache")
179180

180181
// Inject patch step into each selected job that has actions/checkout
181182
for _, jobName := range selectedJobs {
182-
injectPatchStep(jobs, jobName, patch.mergeBase, cacheKey)
183+
injectPatchStep(jobs, jobName, patch.mergeBase, workspacePatchKey)
183184
}
184185
}
185186

@@ -203,7 +204,9 @@ This command is in beta and subject to change.`,
203204
if sshAfterStep > 0 {
204205
fmt.Printf("Inserting tmate step after step %d\n", sshAfterStep)
205206
}
206-
if headSHA, err := resolveHEAD(workflowDir); err == nil {
207+
headSHA, headErr := resolveHEAD(workflowDir)
208+
headOK := headErr == nil
209+
if headOK {
207210
fmt.Printf("HEAD: %s\n", headSHA)
208211
}
209212
fmt.Println()
@@ -218,12 +221,7 @@ This command is in beta and subject to change.`,
218221
Repo: repo,
219222
WorkflowContent: []string{string(yamlBytes)},
220223
}
221-
222-
if patch != nil {
223-
req.Sha = &patch.mergeBase
224-
} else if headSHA, err := resolveHEAD(workflowDir); err == nil {
225-
req.Sha = &headSHA
226-
}
224+
setRunRequestGitContext(req, patch, headSHA, headOK, workspacePatchKey)
227225

228226
resp, err := api.CIRun(ctx, tokenVal, orgID, req)
229227
if err != nil {
@@ -300,6 +298,50 @@ type patchInfo struct {
300298
content string
301299
}
302300

301+
// validateWorkspacePatch returns an error if a detected patch cannot be uploaded safely.
302+
// patch must be non-nil (callers invoke this only when patch != nil).
303+
func validateWorkspacePatch(patch *patchInfo) error {
304+
if patch == nil {
305+
return fmt.Errorf("internal error: validateWorkspacePatch called with nil patch")
306+
}
307+
if patch.mergeBase == "" {
308+
return fmt.Errorf("cannot upload workspace patch: empty merge base")
309+
}
310+
return nil
311+
}
312+
313+
// patchWorkspaceCacheKey returns the Depot generic cache key for the workspace patch
314+
// (same value for upload, injectPatchStep, and RunRequest.workspace_patch_cache_key).
315+
func patchWorkspaceCacheKey(patch *patchInfo) string {
316+
if patch == nil {
317+
return ""
318+
}
319+
prefix := patch.mergeBase
320+
if len(prefix) > 12 {
321+
prefix = prefix[:12]
322+
}
323+
sum := sha256.Sum256([]byte(patch.content))
324+
patchHash := fmt.Sprintf("%x", sum)[:16]
325+
return fmt.Sprintf("patch/%s/%s", prefix, patchHash)
326+
}
327+
328+
// setRunRequestGitContext sets RunRequest Sha and, when a workspace patch was uploaded,
329+
// WorkspacePatchCacheKey. workspacePatchKey must be the exact string passed to UploadCacheEntry
330+
// and injectPatchStep (empty when patch == nil).
331+
func setRunRequestGitContext(req *civ1.RunRequest, patch *patchInfo, headSHA string, headOK bool, workspacePatchKey string) {
332+
if patch != nil {
333+
mb := patch.mergeBase
334+
req.Sha = &mb
335+
key := workspacePatchKey
336+
req.WorkspacePatchCacheKey = &key
337+
return
338+
}
339+
if headOK {
340+
sha := headSHA
341+
req.Sha = &sha
342+
}
343+
}
344+
303345
// findMergeBase picks the best base commit for patch generation.
304346
//
305347
// If the current branch has been pushed (origin/<branch> exists locally),
@@ -406,7 +448,7 @@ func resolveJobDeps(allJobs map[string]interface{}, requested []string) map[stri
406448
return needed
407449
}
408450

409-
func injectPatchStep(jobs map[string]interface{}, jobName, mergeBase, cacheKey string) {
451+
func injectPatchStep(jobs map[string]interface{}, jobName, mergeBase, workspacePatchKey string) {
410452
jobRaw, ok := jobs[jobName]
411453
if !ok {
412454
return
@@ -476,7 +518,7 @@ curl -fsSL "$PATCH_URL" -o /tmp/local.patch
476518
echo "Applying patch..."
477519
git apply --allow-empty /tmp/local.patch
478520
rm /tmp/local.patch
479-
echo "Patch applied successfully"`, cacheKey, cacheBaseURL),
521+
echo "Patch applied successfully"`, workspacePatchKey, cacheBaseURL),
480522
"env": map[string]interface{}{
481523
"DEPOT_TOKEN": "${{ secrets.DEPOT_TOKEN }}",
482524
},

pkg/cmd/ci/run_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package ci
22

33
import (
4+
"crypto/sha256"
5+
"fmt"
46
"os"
57
"os/exec"
68
"path/filepath"
79
"strings"
810
"testing"
11+
12+
civ1 "github.com/depot/cli/pkg/proto/depot/ci/v1"
913
)
1014

1115
// initBareRemote creates a bare git repo with one commit and returns its path.
@@ -169,3 +173,107 @@ func TestFindMergeBase_DetachedHEAD(t *testing.T) {
169173

170174
_ = baseBranch
171175
}
176+
177+
func TestValidateWorkspacePatch_emptyMergeBase(t *testing.T) {
178+
err := validateWorkspacePatch(&patchInfo{mergeBase: "", content: "x"})
179+
if err == nil {
180+
t.Fatal("expected error for empty merge base")
181+
}
182+
if !strings.Contains(err.Error(), "empty merge base") {
183+
t.Fatalf("unexpected error: %v", err)
184+
}
185+
}
186+
187+
func TestValidateWorkspacePatch_nil(t *testing.T) {
188+
err := validateWorkspacePatch(nil)
189+
if err == nil {
190+
t.Fatal("expected error for nil patch")
191+
}
192+
if !strings.Contains(err.Error(), "nil patch") {
193+
t.Fatalf("unexpected error: %v", err)
194+
}
195+
}
196+
197+
func TestValidateWorkspacePatch_ok(t *testing.T) {
198+
if err := validateWorkspacePatch(&patchInfo{mergeBase: "abc", content: "x"}); err != nil {
199+
t.Fatalf("unexpected error: %v", err)
200+
}
201+
}
202+
203+
func TestPatchWorkspaceCacheKey_nil(t *testing.T) {
204+
if got := patchWorkspaceCacheKey(nil); got != "" {
205+
t.Fatalf("expected empty key for nil patch, got %q", got)
206+
}
207+
}
208+
209+
func TestPatchWorkspaceCacheKey_format(t *testing.T) {
210+
mergeBase := "abcdef1234567890abcdef1234567890abcdef12"
211+
content := "diff --git a/x b/x\n"
212+
patch := &patchInfo{mergeBase: mergeBase, content: content}
213+
got := patchWorkspaceCacheKey(patch)
214+
215+
sum := sha256.Sum256([]byte(content))
216+
want := fmt.Sprintf("patch/%s/%s", mergeBase[:12], fmt.Sprintf("%x", sum)[:16])
217+
if got != want {
218+
t.Fatalf("patchWorkspaceCacheKey() = %q, want %q", got, want)
219+
}
220+
}
221+
222+
func TestPatchWorkspaceCacheKey_shortMergeBase(t *testing.T) {
223+
// Avoid panicking if merge base is unexpectedly short (e.g. corrupt git output).
224+
patch := &patchInfo{mergeBase: "abc", content: "x"}
225+
got := patchWorkspaceCacheKey(patch)
226+
sum := sha256.Sum256([]byte("x"))
227+
want := fmt.Sprintf("patch/%s/%s", "abc", fmt.Sprintf("%x", sum)[:16])
228+
if got != want {
229+
t.Fatalf("patchWorkspaceCacheKey() = %q, want %q", got, want)
230+
}
231+
}
232+
233+
func TestSetRunRequestGitContext_withPatch(t *testing.T) {
234+
mergeBase := "0123456789ab0123456789ab0123456789abcdef"
235+
patch := &patchInfo{mergeBase: mergeBase, content: "patch-bytes\n"}
236+
explicitKey := "patch/explicit/not-from-helper"
237+
req := &civ1.RunRequest{Repo: "o/r"}
238+
setRunRequestGitContext(req, patch, "headignored", true, explicitKey)
239+
240+
if req.GetSha() != mergeBase {
241+
t.Fatalf("Sha = %q, want merge base %q", req.GetSha(), mergeBase)
242+
}
243+
if req.GetWorkspacePatchCacheKey() != explicitKey {
244+
t.Fatalf("WorkspacePatchCacheKey = %q, want exact passed key %q", req.GetWorkspacePatchCacheKey(), explicitKey)
245+
}
246+
}
247+
248+
func TestSetRunRequestGitContext_noPatch_usesHead(t *testing.T) {
249+
req := &civ1.RunRequest{Repo: "o/r"}
250+
setRunRequestGitContext(req, nil, "deadbeefcafe", true, "")
251+
if req.GetSha() != "deadbeefcafe" {
252+
t.Fatalf("Sha = %q, want head SHA", req.GetSha())
253+
}
254+
if req.GetWorkspacePatchCacheKey() != "" {
255+
t.Fatalf("WorkspacePatchCacheKey should be unset, got %q", req.GetWorkspacePatchCacheKey())
256+
}
257+
}
258+
259+
func TestSetRunRequestGitContext_noPatch_headOK_emptySHA(t *testing.T) {
260+
req := &civ1.RunRequest{Repo: "o/r"}
261+
setRunRequestGitContext(req, nil, "", true, "")
262+
if req.GetSha() != "" {
263+
t.Fatalf("Sha = %q, want empty when HEAD resolved to empty string", req.GetSha())
264+
}
265+
if req.GetWorkspacePatchCacheKey() != "" {
266+
t.Fatalf("WorkspacePatchCacheKey should be unset, got %q", req.GetWorkspacePatchCacheKey())
267+
}
268+
}
269+
270+
func TestSetRunRequestGitContext_noPatch_headUnresolved(t *testing.T) {
271+
req := &civ1.RunRequest{Repo: "o/r"}
272+
setRunRequestGitContext(req, nil, "", false, "")
273+
if req.GetSha() != "" {
274+
t.Fatalf("Sha should be empty, got %q", req.GetSha())
275+
}
276+
if req.GetWorkspacePatchCacheKey() != "" {
277+
t.Fatalf("WorkspacePatchCacheKey should be unset, got %q", req.GetWorkspacePatchCacheKey())
278+
}
279+
}

0 commit comments

Comments
 (0)