Skip to content

Commit 6d9cd4a

Browse files
committed
Add FlexInt64 type for JSON unmarshalling and enhance cache entry responses
1 parent d33e402 commit 6d9cd4a

4 files changed

Lines changed: 144 additions & 21 deletions

File tree

github/server/cache.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,32 @@ type CreateCacheEntryResponse struct {
6868
SignedUploadURL string `json:"signed_upload_url"`
6969
}
7070

71+
// FlexInt64 unmarshals from both JSON numbers and JSON strings.
72+
// Protobuf's canonical JSON encoding represents int64 as strings.
73+
type FlexInt64 int64
74+
75+
func (f *FlexInt64) UnmarshalJSON(data []byte) error {
76+
var n int64
77+
if err := json.Unmarshal(data, &n); err == nil {
78+
*f = FlexInt64(n)
79+
return nil
80+
}
81+
var s string
82+
if err := json.Unmarshal(data, &s); err != nil {
83+
return fmt.Errorf("FlexInt64: cannot unmarshal %s", string(data))
84+
}
85+
n, err := strconv.ParseInt(s, 10, 64)
86+
if err != nil {
87+
return fmt.Errorf("FlexInt64: invalid int64 string %q: %w", s, err)
88+
}
89+
*f = FlexInt64(n)
90+
return nil
91+
}
92+
7193
type FinalizeCacheEntryRequest struct {
72-
Key string `json:"key"`
73-
Version string `json:"version"`
74-
SizeBytes int64 `json:"size_bytes"`
94+
Key string `json:"key"`
95+
Version string `json:"version"`
96+
SizeBytes FlexInt64 `json:"size_bytes"`
7597
}
7698

7799
type FinalizeCacheEntryResponse struct {
@@ -89,6 +111,7 @@ type GetCacheEntryDownloadURLRequest struct {
89111
type GetCacheEntryDownloadURLResponse struct {
90112
Ok bool `json:"ok"`
91113
SignedDownloadURL string `json:"signed_download_url"`
114+
MatchedKey string `json:"matched_key"`
92115
}
93116

94117
type DeleteCacheEntryRequest struct {
@@ -166,7 +189,7 @@ func (s *Server) handleFinalizeCacheEntry(w http.ResponseWriter, r *http.Request
166189
writeTwirpError(w, http.StatusNotFound, "not_found", "cache entry not found")
167190
return
168191
}
169-
found.Size = req.SizeBytes
192+
found.Size = int64(req.SizeBytes)
170193
found.Finalized = true
171194
s.mu.Unlock()
172195

@@ -198,6 +221,7 @@ func (s *Server) handleGetCacheEntryDownloadURL(w http.ResponseWriter, r *http.R
198221
writeJSON(w, http.StatusOK, GetCacheEntryDownloadURLResponse{
199222
Ok: true,
200223
SignedDownloadURL: downloadURL,
224+
MatchedKey: entry.Key,
201225
})
202226
return
203227
}
@@ -224,6 +248,7 @@ func (s *Server) handleGetCacheEntryDownloadURL(w http.ResponseWriter, r *http.R
224248
writeJSON(w, http.StatusOK, GetCacheEntryDownloadURLResponse{
225249
Ok: true,
226250
SignedDownloadURL: downloadURL,
251+
MatchedKey: best.Key,
227252
})
228253
return
229254
}

github/server/cache_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,87 @@ func TestCacheInvalidAuth(t *testing.T) {
249249
}
250250
}
251251

252+
func TestCacheSizeBytesAsString(t *testing.T) {
253+
_, ts := setupTestServer(t)
254+
defer ts.Close()
255+
256+
token := makeTestJWT("Actions.Results:run1:job1")
257+
258+
// Create cache entry
259+
resp := cacheTwirpRequest(t, ts, "CreateCacheEntry", token, map[string]any{
260+
"key": "str-size-test",
261+
"version": "v1",
262+
})
263+
createResp := decodeResponse[CreateCacheEntryResponse](t, resp)
264+
req, _ := http.NewRequest("PUT", createResp.SignedUploadURL, bytes.NewReader([]byte("data")))
265+
r, _ := http.DefaultClient.Do(req)
266+
r.Body.Close()
267+
268+
// Finalize with size_bytes as a JSON string (protobuf int64 encoding)
269+
b, _ := json.Marshal(map[string]any{
270+
"key": "str-size-test",
271+
"version": "v1",
272+
"size_bytes": "4",
273+
})
274+
url := ts.URL + "/twirp/github.actions.results.api.v1.CacheService/FinalizeCacheEntryUpload"
275+
httpReq, _ := http.NewRequest("POST", url, bytes.NewReader(b))
276+
httpReq.Header.Set("Content-Type", "application/json")
277+
httpReq.Header.Set("Authorization", "Bearer "+token)
278+
resp, err := http.DefaultClient.Do(httpReq)
279+
if err != nil {
280+
t.Fatalf("request: %v", err)
281+
}
282+
if resp.StatusCode != http.StatusOK {
283+
body, _ := io.ReadAll(resp.Body)
284+
t.Fatalf("FinalizeCacheEntryUpload with string size_bytes: status=%d body=%s", resp.StatusCode, body)
285+
}
286+
finalResp := decodeResponse[FinalizeCacheEntryResponse](t, resp)
287+
if !finalResp.Ok {
288+
t.Fatal("finalize failed")
289+
}
290+
}
291+
292+
func TestCacheMatchedKey(t *testing.T) {
293+
_, ts := setupTestServer(t)
294+
defer ts.Close()
295+
296+
token := makeTestJWT("Actions.Results:run1:job1")
297+
298+
// Create, upload, finalize
299+
resp := cacheTwirpRequest(t, ts, "CreateCacheEntry", token, map[string]any{
300+
"key": "my-key-abc",
301+
"version": "v1",
302+
})
303+
createResp := decodeResponse[CreateCacheEntryResponse](t, resp)
304+
req, _ := http.NewRequest("PUT", createResp.SignedUploadURL, bytes.NewReader([]byte("data")))
305+
r, _ := http.DefaultClient.Do(req)
306+
r.Body.Close()
307+
cacheTwirpRequest(t, ts, "FinalizeCacheEntryUpload", token, map[string]any{
308+
"key": "my-key-abc", "version": "v1", "size_bytes": 4,
309+
})
310+
311+
// Exact match — matched_key should be the key
312+
resp = cacheTwirpRequest(t, ts, "GetCacheEntryDownloadURL", token, map[string]any{
313+
"key": "my-key-abc",
314+
"version": "v1",
315+
})
316+
dlResp := decodeResponse[GetCacheEntryDownloadURLResponse](t, resp)
317+
if dlResp.MatchedKey != "my-key-abc" {
318+
t.Fatalf("exact match: matched_key=%q, want %q", dlResp.MatchedKey, "my-key-abc")
319+
}
320+
321+
// Prefix match via restore_keys — matched_key should be the matched entry's key
322+
resp = cacheTwirpRequest(t, ts, "GetCacheEntryDownloadURL", token, map[string]any{
323+
"key": "my-key-xyz",
324+
"version": "v1",
325+
"restore_keys": []string{"my-key-"},
326+
})
327+
dlResp = decodeResponse[GetCacheEntryDownloadURLResponse](t, resp)
328+
if dlResp.MatchedKey != "my-key-abc" {
329+
t.Fatalf("prefix match: matched_key=%q, want %q", dlResp.MatchedKey, "my-key-abc")
330+
}
331+
}
332+
252333
func TestCacheInvalidContentType(t *testing.T) {
253334
_, ts := setupTestServer(t)
254335
defer ts.Close()

github/server/server.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,11 @@ func (s *Server) handleTwirp(w http.ResponseWriter, r *http.Request) {
155155
// --- Request/Response types ---
156156

157157
type CreateArtifactRequest struct {
158-
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
159-
WorkflowJobRunBackendID string `json:"workflow_job_run_backend_id"`
160-
Name string `json:"name"`
161-
Version int `json:"version"`
158+
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
159+
WorkflowJobRunBackendID string `json:"workflow_job_run_backend_id"`
160+
Name string `json:"name"`
161+
Version int `json:"version"`
162+
ExpiresAt *string `json:"expires_at,omitempty"`
162163
}
163164

164165
type CreateArtifactResponse struct {
@@ -180,9 +181,10 @@ type FinalizeArtifactResponse struct {
180181
}
181182

182183
type ListArtifactsRequest struct {
183-
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
184-
NameFilter *string `json:"name_filter,omitempty"`
185-
IDFilter *string `json:"id_filter,omitempty"`
184+
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
185+
WorkflowJobRunBackendID string `json:"workflow_job_run_backend_id"`
186+
NameFilter *string `json:"name_filter,omitempty"`
187+
IDFilter *string `json:"id_filter,omitempty"`
186188
}
187189

188190
type ListArtifactsResponse struct {
@@ -196,20 +198,23 @@ type ArtifactEntry struct {
196198
Name string `json:"name"`
197199
Size string `json:"size"`
198200
CreatedAt *string `json:"created_at,omitempty"`
201+
Digest *string `json:"digest,omitempty"`
199202
}
200203

201204
type GetSignedArtifactURLRequest struct {
202-
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
203-
Name string `json:"name"`
205+
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
206+
WorkflowJobRunBackendID string `json:"workflow_job_run_backend_id"`
207+
Name string `json:"name"`
204208
}
205209

206210
type GetSignedArtifactURLResponse struct {
207211
SignedURL string `json:"signed_url"`
208212
}
209213

210214
type DeleteArtifactRequest struct {
211-
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
212-
Name string `json:"name"`
215+
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
216+
WorkflowJobRunBackendID string `json:"workflow_job_run_backend_id"`
217+
Name string `json:"name"`
213218
}
214219

215220
type DeleteArtifactResponse struct {
@@ -218,9 +223,9 @@ type DeleteArtifactResponse struct {
218223
}
219224

220225
type MigrateArtifactRequest struct {
221-
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
222-
Name string `json:"name"`
223-
Version int `json:"version"`
226+
WorkflowRunBackendID string `json:"workflow_run_backend_id"`
227+
Name string `json:"name"`
228+
ExpiresAt *string `json:"expires_at,omitempty"`
224229
}
225230

226231
type MigrateArtifactResponse struct {
@@ -280,6 +285,11 @@ func (s *Server) handleCreateArtifact(w http.ResponseWriter, r *http.Request, ru
280285
BlobPath: blobPath,
281286
CreatedAt: time.Now(),
282287
}
288+
if req.ExpiresAt != nil {
289+
if t, err := time.Parse(time.RFC3339, *req.ExpiresAt); err == nil {
290+
art.ExpiresAt = t
291+
}
292+
}
283293
s.artifacts[key] = art
284294
s.artByID[id] = art
285295
s.uploadMu[id] = &sync.Mutex{}
@@ -299,7 +309,7 @@ func (s *Server) handleCreateArtifact(w http.ResponseWriter, r *http.Request, ru
299309
})
300310
}
301311

302-
func (s *Server) handleFinalizeArtifact(w http.ResponseWriter, r *http.Request, runID, jobID string) {
312+
func (s *Server) handleFinalizeArtifact(w http.ResponseWriter, r *http.Request, runID, _ string) {
303313
var req FinalizeArtifactRequest
304314
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
305315
writeTwirpError(w, http.StatusBadRequest, "invalid_argument", "invalid JSON")
@@ -362,14 +372,19 @@ func (s *Server) handleListArtifacts(w http.ResponseWriter, r *http.Request, run
362372
}
363373
}
364374
ts := art.CreatedAt.UTC().Format(time.RFC3339)
365-
entries = append(entries, ArtifactEntry{
375+
entry := ArtifactEntry{
366376
WorkflowRunBackendID: art.RunBackendID,
367377
WorkflowJobRunBackendID: art.JobBackendID,
368378
DatabaseID: strconv.FormatInt(art.ID, 10),
369379
Name: art.Name,
370380
Size: strconv.FormatInt(art.Size, 10),
371381
CreatedAt: &ts,
372-
})
382+
}
383+
if art.Hash != "" {
384+
h := art.Hash
385+
entry.Digest = &h
386+
}
387+
entries = append(entries, entry)
373388
}
374389
s.mu.RUnlock()
375390

github/server/start.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func (rs *RunningServer) InjectEnv(env map[string]string) {
3535
env["ACTIONS_RESULTS_URL"] = rs.URL + "/"
3636
env["ACTIONS_ID_TOKEN_REQUEST_URL"] = rs.URL + "/_services/token"
3737
env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = rs.RuntimeToken
38+
// Required for @actions/cache to use the twirp v2 api instead of the legacy REST API.
39+
env["ACTIONS_CACHE_SERVICE_V2"] = "true"
3840
}
3941

4042
// StartServer starts a local GitHub Actions mock server on a random port.

0 commit comments

Comments
 (0)