diff --git a/go.mod b/go.mod index fb976cb..3daec7b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module git.geekeey.de/actions/artifacts +module code.geekeey.de/actions/artifacts go 1.23.2 + +require code.geekeey.de/actions/sdk v1.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6a21fc7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +code.geekeey.de/actions/sdk v1.2.0 h1:qQvOdTBtZVpR1xATNnApeiW78y34Jh83OjsQg1ajPjA= +code.geekeey.de/actions/sdk v1.2.0/go.mod h1:7iIOi3YUmdDe2BCz9s59wK1t0Ckec34e664q+TOreiE= diff --git a/save/github.go b/save/github.go new file mode 100644 index 0000000..81dc5fd --- /dev/null +++ b/save/github.go @@ -0,0 +1,144 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type ActionClient struct { + *http.Client + base string + token string +} + +func NewActionClient() *ActionClient { + c := &ActionClient{Client: &http.Client{}} + c.base = os.Getenv("ACTIONS_RESULTS_URL") + c.token = fmt.Sprintf("Bearer %s", os.Getenv("ACTIONS_RUNTIME_TOKEN")) + return c +} + +func (c *ActionClient) Do(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", c.token) + if !req.URL.IsAbs() { + u, err := url.Parse(fmt.Sprintf("%s%s", strings.TrimRight(c.base, "/"), req.URL)) + if err != nil { + return nil, err + } + req.URL = u + } + return c.Client.Do(req) +} + +func HasContentType(r *http.Response, mimetype string) bool { + contentType := r.Header.Get("Content-type") + if contentType == "" { + return mimetype == "application/octet-stream" + } + + for _, v := range strings.Split(contentType, ",") { + t, _, err := mime.ParseMediaType(v) + if err != nil { + break + } + if t == mimetype { + return true + } + } + return false +} + +type ApiClient interface { + Do(*http.Request) (*http.Response, error) +} + +type GitHub struct { + client ApiClient +} + +type CreateArtifactRequest struct { + RunID string `json:"workflowRunBackendId"` + JobRunID string `json:"workflowJobRunBackendId"` + Name string `json:"name"` + Version int32 `json:"version"` + + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +type CreateArtifactResponse struct { + Ok bool `json:"ok"` + SignedUploadUrl string `json:"signed_upload_url"` +} + +type FinalizeArtifactRequest struct { + RunID string `json:"workflowRunBackendId"` + JobRunID string `json:"workflowJobRunBackendId"` + Name string `json:"name"` + Size int `json:"size"` + Hash string `json:"hash,omitempty"` +} + +type FinalizeArtifactResponse struct { + Ok bool `json:"ok"` + ArtifactId string `json:"artifactId"` +} + +func (gh *GitHub) CreateArtifact(ctx context.Context, req CreateArtifactRequest) (*CreateArtifactResponse, error) { + endpoint := "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact" + var result CreateArtifactResponse + if err := gh.twirp(ctx, endpoint, req, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (gh *GitHub) FinalizeArtifact(ctx context.Context, req FinalizeArtifactRequest) (*FinalizeArtifactResponse, error) { + endpoint := "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact" + var result FinalizeArtifactResponse + if err := gh.twirp(ctx, endpoint, req, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (gh *GitHub) twirp(ctx context.Context, url string, in any, out any) error { + var b bytes.Buffer + + if err := json.NewEncoder(&b).Encode(in); err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + res, err := gh.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + if !HasContentType(res, "application/json") { + return fmt.Errorf("content-type: %q should be %q", + res.Header.Get("content-type"), "application/json") + } + dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024)) + + if res.StatusCode >= 200 && res.StatusCode < 300 { + return dec.Decode(out) + } else { + body, _ := io.ReadAll(res.Body) + return fmt.Errorf("twirp call %s failed: %d: %s", url, res.StatusCode, string(body)) + } +} diff --git a/save/main.go b/save/main.go index da29a2c..7a98ac2 100644 --- a/save/main.go +++ b/save/main.go @@ -1,4 +1,37 @@ package main +import ( + "context" + "os" + "os/signal" + + "code.geekeey.de/actions/sdk" +) + func main() { + action := &SaveArtifactAction{Action: sdk.New()} + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + if err := action.Run(ctx); err != nil { + action.Errorf("%s", err) + os.Exit(1) + } +} + +type SaveArtifactAction struct { + *sdk.Action + repository string + path string + reference string +} + +func (action *SaveArtifactAction) setup() error { + return nil +} + +func (action *SaveArtifactAction) Run(ctx context.Context) error { + if err := action.setup(); err != nil { + return err + } + return nil }