package artifact import ( "context" "encoding/json" "fmt" "io" "net/http" "code.geekeey.de/actions/sdk" ) // RepositoryArtifact is an artifact as returned by the repository-scoped // actions API (compatible with the GitHub artifact REST schema). type RepositoryArtifact struct { Id int64 `json:"id"` Name string `json:"name"` SizeInBytes int64 `json:"size_in_bytes"` ArchiveDownloadURL string `json:"archive_download_url"` Expired bool `json:"expired"` } type ListRunArtifactsResponse struct { TotalCount int64 `json:"total_count"` Artifacts []RepositoryArtifact `json:"artifacts"` } // RepositoryClient talks to the repository-scoped actions artifacts API. Unlike // Client, which is bound to the current run by the runtime token, this client // can address any run in any repository the token has access to. type RepositoryClient struct { http *sdk.Client } // NewRepositoryClient returns a client for the actions API at base using the // given token. base is typically the value of GITHUB_API_URL. func NewRepositoryClient(base, token string) *RepositoryClient { return &RepositoryClient{http: sdk.NewClient(base, token)} } func (c *RepositoryClient) ListRunArtifacts(ctx context.Context, owner, repo string, runID int64) (*ListRunArtifactsResponse, error) { endpoint := fmt.Sprintf("/repos/%s/%s/actions/runs/%d/artifacts", owner, repo, runID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } req.Header.Set("accept", "application/json") res, err := c.http.Do(req) if err != nil { return nil, err } defer res.Body.Close() if !sdk.HasContentType(res, "application/json") { return nil, fmt.Errorf("content-type: %q should be %q", res.Header.Get("content-type"), "application/json") } if res.StatusCode < 200 || res.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(res.Body, 32*1024)) return nil, fmt.Errorf("list artifacts of %s/%s run %d failed: %s: %s", owner, repo, runID, res.Status, string(body)) } var out ListRunArtifactsResponse if err := json.NewDecoder(io.LimitReader(res.Body, 1024*1024)).Decode(&out); err != nil { return nil, err } return &out, nil } // DownloadArtifact downloads the zip archive of an artifact. The server // redirects to the signed blob URL, which the underlying client follows. func (c *RepositoryClient) DownloadArtifact(ctx context.Context, owner, repo string, artifactID int64) (io.ReadCloser, error) { endpoint := fmt.Sprintf("/repos/%s/%s/actions/artifacts/%d/zip", owner, repo, artifactID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } res, err := c.http.Do(req) if err != nil { return nil, err } if res.StatusCode < 200 || res.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(res.Body, 32*1024)) res.Body.Close() return nil, fmt.Errorf("download artifact %d failed: %s: %s", artifactID, res.Status, string(body)) } return res.Body, nil }