diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /services/webhook/sourcehut/builds.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | services/webhook/sourcehut/builds.go | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/services/webhook/sourcehut/builds.go b/services/webhook/sourcehut/builds.go new file mode 100644 index 0000000..7b7ace1 --- /dev/null +++ b/services/webhook/sourcehut/builds.go @@ -0,0 +1,301 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sourcehut + +import ( + "cmp" + "context" + "fmt" + "html/template" + "io/fs" + "net/http" + "strings" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" + gitea_context "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/webhook/shared" + + "gitea.com/go-chi/binding" + "gopkg.in/yaml.v3" +) + +type BuildsHandler struct{} + +func (BuildsHandler) Type() webhook_module.HookType { return webhook_module.SOURCEHUT_BUILDS } +func (BuildsHandler) Metadata(w *webhook_model.Webhook) any { + s := &BuildsMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("sourcehut.BuildsHandler.Metadata(%d): %v", w.ID, err) + } + return s +} + +func (BuildsHandler) Icon(size int) template.HTML { + return shared.ImgIcon("sourcehut.svg", size) +} + +type buildsForm struct { + forms.WebhookCoreForm + PayloadURL string `binding:"Required;ValidUrl"` + ManifestPath string `binding:"Required"` + Visibility string `binding:"Required;In(PUBLIC,UNLISTED,PRIVATE)"` + Secrets bool + AccessToken string `binding:"Required"` +} + +var _ binding.Validator = &buildsForm{} + +// Validate implements binding.Validator. +func (f *buildsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := gitea_context.GetWebContext(req) + if !fs.ValidPath(f.ManifestPath) { + errs = append(errs, binding.Error{ + FieldNames: []string{"ManifestPath"}, + Classification: "", + Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_path"), + }) + } + f.AuthorizationHeader = "Bearer " + strings.TrimSpace(f.AccessToken) + return errs +} + +func (BuildsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { + var form buildsForm + bind(&form) + + return forms.WebhookForm{ + WebhookCoreForm: form.WebhookCoreForm, + URL: form.PayloadURL, + ContentType: webhook_model.ContentTypeJSON, + Secret: "", + HTTPMethod: http.MethodPost, + Metadata: &BuildsMeta{ + ManifestPath: form.ManifestPath, + Visibility: form.Visibility, + Secrets: form.Secrets, + }, + } +} + +type ( + graphqlPayload[V any] struct { + Query string `json:"query,omitempty"` + Error string `json:"error,omitempty"` + Variables V `json:"variables,omitempty"` + } + // buildsVariables according to https://man.sr.ht/builds.sr.ht/graphql.md + buildsVariables struct { + Manifest string `json:"manifest"` + Tags []string `json:"tags"` + Note string `json:"note"` + Secrets bool `json:"secrets"` + Execute bool `json:"execute"` + Visibility string `json:"visibility"` + } + + // BuildsMeta contains the metadata for the webhook + BuildsMeta struct { + ManifestPath string `json:"manifest_path"` + Visibility string `json:"visibility"` + Secrets bool `json:"secrets"` + } +) + +type sourcehutConvertor struct { + ctx context.Context + meta BuildsMeta +} + +var _ shared.PayloadConvertor[graphqlPayload[buildsVariables]] = sourcehutConvertor{} + +func (BuildsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := BuildsMeta{} + if err := json.Unmarshal([]byte(w.Meta), &meta); err != nil { + return nil, nil, fmt.Errorf("newSourcehutRequest meta json: %w", err) + } + pc := sourcehutConvertor{ + ctx: ctx, + meta: meta, + } + return shared.NewJSONRequest(pc, w, t, false) +} + +// Create implements PayloadConvertor Create method +func (pc sourcehutConvertor) Create(p *api.CreatePayload) (graphqlPayload[buildsVariables], error) { + return pc.newPayload(p.Repo, p.Sha, p.Ref, p.RefType+" "+git.RefName(p.Ref).ShortName()+" created", true) +} + +// Delete implements PayloadConvertor Delete method +func (pc sourcehutConvertor) Delete(_ *api.DeletePayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Fork implements PayloadConvertor Fork method +func (pc sourcehutConvertor) Fork(_ *api.ForkPayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Push implements PayloadConvertor Push method +func (pc sourcehutConvertor) Push(p *api.PushPayload) (graphqlPayload[buildsVariables], error) { + return pc.newPayload(p.Repo, p.HeadCommit.ID, p.Ref, p.HeadCommit.Message, true) +} + +// Issue implements PayloadConvertor Issue method +func (pc sourcehutConvertor) Issue(_ *api.IssuePayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// IssueComment implements PayloadConvertor IssueComment method +func (pc sourcehutConvertor) IssueComment(_ *api.IssueCommentPayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// PullRequest implements PayloadConvertor PullRequest method +func (pc sourcehutConvertor) PullRequest(_ *api.PullRequestPayload) (graphqlPayload[buildsVariables], error) { + // TODO + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Review implements PayloadConvertor Review method +func (pc sourcehutConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Repository implements PayloadConvertor Repository method +func (pc sourcehutConvertor) Repository(_ *api.RepositoryPayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Wiki implements PayloadConvertor Wiki method +func (pc sourcehutConvertor) Wiki(_ *api.WikiPayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// Release implements PayloadConvertor Release method +func (pc sourcehutConvertor) Release(_ *api.ReleasePayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + +// mustBuildManifest adjusts the manifest to submit to the builds service +// +// in case of an error the Error field will be set, to be visible by the end-user under recent deliveries +func (pc sourcehutConvertor) newPayload(repo *api.Repository, commitID, ref, note string, trusted bool) (graphqlPayload[buildsVariables], error) { + manifest, err := pc.buildManifest(repo, commitID, ref) + if err != nil { + if len(manifest) == 0 { + return graphqlPayload[buildsVariables]{}, err + } + // the manifest contains an error for the user: log the actual error and construct the payload + // the error will be visible under the "recent deliveries" of the webhook settings. + log.Warn("sourcehut.builds: could not construct manifest for %s: %v", repo.FullName, err) + msg := fmt.Sprintf("%s:%s %s", repo.FullName, ref, manifest) + return graphqlPayload[buildsVariables]{ + Error: msg, + }, nil + } + + gitRef := git.RefName(ref) + return graphqlPayload[buildsVariables]{ + Query: `mutation ( + $manifest: String! + $tags: [String!] + $note: String! + $secrets: Boolean! + $execute: Boolean! + $visibility: Visibility! +) { + submit( + manifest: $manifest + tags: $tags + note: $note + secrets: $secrets + execute: $execute + visibility: $visibility + ) { + id + } +}`, Variables: buildsVariables{ + Manifest: string(manifest), + Tags: []string{repo.FullName, gitRef.RefType() + "/" + gitRef.ShortName(), pc.meta.ManifestPath}, + Note: note, + Secrets: pc.meta.Secrets && trusted, + Execute: trusted, + Visibility: cmp.Or(pc.meta.Visibility, "PRIVATE"), + }, + }, nil +} + +// buildManifest adjusts the manifest to submit to the builds service +// in case of an error the []byte might contain an error that can be displayed to the user +func (pc sourcehutConvertor) buildManifest(repo *api.Repository, commitID, gitRef string) ([]byte, error) { + gitRepo, err := gitrepo.OpenRepository(pc.ctx, repo) + if err != nil { + msg := "could not open repository" + return []byte(msg), fmt.Errorf(msg+": %w", err) + } + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit(commitID) + if err != nil { + msg := fmt.Sprintf("could not get commit %q", commitID) + return []byte(msg), fmt.Errorf(msg+": %w", err) + } + entry, err := commit.GetTreeEntryByPath(pc.meta.ManifestPath) + if err != nil { + msg := fmt.Sprintf("could not open manifest %q", pc.meta.ManifestPath) + return []byte(msg), fmt.Errorf(msg+": %w", err) + } + r, err := entry.Blob().DataAsync() + if err != nil { + msg := fmt.Sprintf("could not read manifest %q", pc.meta.ManifestPath) + return []byte(msg), fmt.Errorf(msg+": %w", err) + } + defer r.Close() + + // reference: https://man.sr.ht/builds.sr.ht/manifest.md + var manifest struct { + Sources []string `yaml:"sources"` + Environment map[string]string `yaml:"environment"` + + Rest map[string]yaml.Node `yaml:",inline"` + } + if err := yaml.NewDecoder(r).Decode(&manifest); err != nil { + msg := fmt.Sprintf("could not decode manifest %q", pc.meta.ManifestPath) + return []byte(msg), fmt.Errorf(msg+": %w", err) + } + + if manifest.Environment == nil { + manifest.Environment = make(map[string]string) + } + manifest.Environment["BUILD_SUBMITTER"] = "forgejo" + manifest.Environment["BUILD_SUBMITTER_URL"] = setting.AppURL + manifest.Environment["GIT_REF"] = gitRef + + source := repo.CloneURL + "#" + commitID + found := false + for i, s := range manifest.Sources { + if s == repo.CloneURL { + manifest.Sources[i] = source + found = true + break + } + } + if !found { + manifest.Sources = append(manifest.Sources, source) + } + + return yaml.Marshal(manifest) +} |