From 641885a2d7648671f4ad3006036a6cd9d4b51d62e6e25edecb5267def8bd4a8f Mon Sep 17 00:00:00 2001 From: Adam Majer Date: Thu, 1 Aug 2024 18:10:45 +0200 Subject: [PATCH] . --- bots-common/obs_utils.go | 203 +++++++++++++++++++++++++++++++++++++-- obs-staging-bot/main.go | 102 ++++++++++++-------- 2 files changed, 256 insertions(+), 49 deletions(-) diff --git a/bots-common/obs_utils.go b/bots-common/obs_utils.go index 64b8857..f280cec 100644 --- a/bots-common/obs_utils.go +++ b/bots-common/obs_utils.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "net/url" + "slices" ) type ObsClient struct { @@ -179,19 +180,20 @@ func (c *ObsClient) DeleteProject(project string) error { } -type BuildStatus struct { +type PackageBuildStatus struct { Package string `xml:"package,attr"` Code string `xml:"code,attr"` Details string `xml:"details"` } type BuildResult struct { - Project string `xml:"project,attr"` - Repository string `xml:"repository,attr"` - Arch string `xml:"arch,attr"` - Code string `xml:"code,attr"` - Status []BuildStatus `xml:"status"` - Binaries []BinaryList `xml:"binarylist"` + Project string `xml:"project,attr"` + Repository string `xml:"repository,attr"` + Arch string `xml:"arch,attr"` + Code string `xml:"code,attr"` + Dirty bool `xml:"dirty,attr"` + Status []PackageBuildStatus `xml:"status"` + Binaries []BinaryList `xml:"binarylist"` } type Binary struct { @@ -210,6 +212,193 @@ type BuildResultList struct { Result []BuildResult `xml:"result"` } +func (r *BuildResultList) GetPackageList() []string { + pkgList := make([]string, 0, 16) + + for _, res := range r.Result { + // TODO: enough to iterate over one result set? + + for _, status := range res.Status { + if !slices.Contains(pkgList, status.Package) { + pkgList = append(pkgList, status.Package) + } + } + } + + return pkgList +} + +func (r *BuildResultList) BuildResultSummary() (success, finished bool) { + finished = len(r.Result) > 0 && len(r.Result[0].Status) > 0 + success = finished + + for _, resultSet := range r.Result { + repoDetail, ok := ObsRepoStatusDetails[resultSet.Code] + + if !ok { + panic("Unknown repo result code: " + resultSet.Code) + } + + finished = repoDetail.Finished + if !finished || resultSet.Dirty { + return + } + + for _, result := range resultSet.Status { + detail, ok := ObsBuildStatusDetails[result.Code] + + if !ok { + panic("Unknown result code: " + result.Code) + } + + finished = finished && detail.Finished + success = success && detail.Success + + if !finished { + return + } + } + } + + return +} + +var ObsBuildStatusDetails map[string]ObsBuildStatusDetail +var ObsRepoStatusDetails map[string]ObsBuildStatusDetail + +type ObsBuildStatusDetail struct { + Code string + Description string + Finished bool + Success bool +} + +func init() { + ObsBuildStatusDetails = make(map[string]ObsBuildStatusDetail) + ObsRepoStatusDetails = make(map[string]ObsBuildStatusDetail) + + // package status + ObsBuildStatusDetails["succeeded"] = ObsBuildStatusDetail{ + Code: "succeeded", + Description: "Package has built successfully and can be used to build further packages.", + Finished: true, + Success: true, + } + ObsBuildStatusDetails["failed"] = ObsBuildStatusDetail{ + Code: "failed", + Description: "The package does not build successfully. No packages have been created. Packages that depend on this package will be built using any previously created packages, if they exist.", + Finished: true, + Success: false, + } + ObsBuildStatusDetails["unresolvable"] = ObsBuildStatusDetail{ + Code: "unresolvable", + Description: "The build can not begin, because required packages are either missing or not explicitly defined.", + Finished: true, + Success: false, + } + ObsBuildStatusDetails["broken"] = ObsBuildStatusDetail{ + Code: "broken", + Description: "The sources either contain no build description (e.g. specfile), automatic source processing failed or a merge conflict does exist.", + Finished: true, + Success: false, + } + ObsBuildStatusDetails["blocked"] = ObsBuildStatusDetail{ + Code: "blocked", + Description: "This package waits for other packages to be built. These can be in the same or other projects.", + Finished: false, + } + ObsBuildStatusDetails["scheduled"] = ObsBuildStatusDetail{ + Code: "scheduled", + Description: "A package has been marked for building, but the build has not started yet.", + Finished: false, + } + ObsBuildStatusDetails["dispatching"] = ObsBuildStatusDetail{ + Code: "dispatching", + Description: "A package is being copied to a build host. This is an intermediate state before building.", + Finished: false, + } + ObsBuildStatusDetails["building"] = ObsBuildStatusDetail{ + Code: "building", + Description: "The package is currently being built.", + Finished: false, + } + ObsBuildStatusDetails["signing"] = ObsBuildStatusDetail{ + Code: "signing", + Description: "The package has been built successfully and is assigned to get signed.", + Finished: false, + } + ObsBuildStatusDetails["finished"] = ObsBuildStatusDetail{ + Code: "finished", + Description: "The package has been built and signed, but has not yet been picked up by the scheduler. This is an intermediate state prior to 'succeeded' or 'failed'.", + Finished: false, + } + ObsBuildStatusDetails["disabled"] = ObsBuildStatusDetail{ + Code: "disabled", + Description: "The package has been disabled from building in project or package metadata. Packages that depend on this package will be built using any previously created packages, if they still exist.", + Finished: true, + Success: true, + } + ObsBuildStatusDetails["excluded"] = ObsBuildStatusDetail{ + Code: "excluded", + Description: "The package build has been disabled in package build description (for example in the .spec file) or does not provide a matching build description for the target.", + Finished: true, + Success: true, + } + ObsBuildStatusDetails["locked"] = ObsBuildStatusDetail{ + Code: "locked", + Description: "The package is frozen", + Finished: true, + Success: true, + } + ObsBuildStatusDetails["unknown"] = ObsBuildStatusDetail{ + Code: "unknown", + Description: "The scheduler has not yet evaluated this package. Should be a short intermediate state for new packages.", + Finished: false, + } + + // repo status + ObsRepoStatusDetails["published"] = ObsBuildStatusDetail{ + Code: "published", + Description: "Repository has been published", + Finished: true, + } + ObsRepoStatusDetails["publishing"] = ObsBuildStatusDetail{ + Code: "publishing", + Description: "Repository is being created right now", + Finished: true, + } + ObsRepoStatusDetails["unpublished"] = ObsBuildStatusDetail{ + Code: "unpublished", + Description: "Build finished, but repository publishing is disabled", + Finished: true, + } + ObsRepoStatusDetails["building"] = ObsBuildStatusDetail{ + Code: "building", + Description: "Build jobs exist for the repository", + Finished: false, + } + ObsRepoStatusDetails["finished"] = ObsBuildStatusDetail{ + Code: "finished", + Description: "Build jobs have been processed, new repository is not yet created", + Finished: true, + } + ObsRepoStatusDetails["blocked"] = ObsBuildStatusDetail{ + Code: "blocked", + Description: "No build possible at the moment, waiting for jobs in other repositories", + Finished: false, + } + ObsRepoStatusDetails["broken"] = ObsBuildStatusDetail{ + Code: "broken", + Description: "The repository setup is broken, build or publish not possible", + Finished: true, + } + ObsRepoStatusDetails["scheduling"] = ObsBuildStatusDetail{ + Code: "scheduling", + Description: "The repository state is being calculated right now", + Finished: false, + } +} + func parseBuildResults(data []byte) (*BuildResultList, error) { result := BuildResultList{} err := xml.Unmarshal(data, &result) diff --git a/obs-staging-bot/main.go b/obs-staging-bot/main.go index 4fc7e32..eec8f16 100644 --- a/obs-staging-bot/main.go +++ b/obs-staging-bot/main.go @@ -10,6 +10,7 @@ import ( "path" "regexp" "slices" + "sort" "strconv" "strings" "time" @@ -60,35 +61,50 @@ func getObsProjectAssociatedWithPr(baseProject string, pr *models.PullRequest) s ) } -type BuildStatusSummary string - -const BuildUnresolveable = BuildStatusSummary("unresolveable") -const BuildBuilding = BuildStatusSummary("building") -const BuildFailed = BuildStatusSummary("failed") -const BuildSuccess = BuildStatusSummary("success") - -/* -'published' => 'Repository has been published', -'publishing' => 'Repository is being created right now', -'unpublished' => 'Build finished, but repository publishing is disabled', -'building' => 'Build jobs exist for the repository', -'finished' => 'Build jobs have been processed, new repository is not yet created', -'blocked' => 'No build possible at the moment, waiting for jobs in other repositories', -'broken' => 'The repository setup is broken, build or publish not possible', -'scheduling' => 'The repository state is being calculated right now' -*/ -type BuildStatus struct { - Status BuildStatusSummary - Detail string -} - -var buildStatus map[string]BuildStatus - func processBuildStatusUpdate() { } -func processBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary { - return BuildBuilding +type BuildStatusSummary int + +const ( + BuildStatusSummarySuccess = 1 + BuildStatusSummaryFailed = 2 + BuildStatusSummaryBuilding = 3 + BuildStatusSummaryUnknown = 4 +) + +func processBuildStatus(h *common.RequestHandler, project, refProject *common.BuildResultList) BuildStatusSummary { + targetResults := project.Result + + if _, finished := project.BuildResultSummary(); !finished { + return BuildStatusSummaryBuilding + } + + if _, finished := refProject.BuildResultSummary(); !finished { + h.LogError("refProject not finished building??") + return BuildStatusSummaryUnknown + } + + // the repositories should be setup equally between the projects. We + // need to verify that packages that are building in `refProject` are not + // failing in the `project` + BuildResultSorter := func(a, b common.BuildResult) int { + if c := strings.Compare(a.Project, b.Project); c != 0 { + return c + } + if c := strings.Compare(a.Repository, b.Repository); c != 0 { + return c + } + if c := strings.Compare(a.Arch, b.Arch); c != 0 { + return c + } + + panic("Should not happen -- BuiltResultSorter equal repos?") + } + slices.SortFunc(project.Result, BuildResultSorter) + slices.SortFunc(refProject.Result, BuildResultSorter) + + return BuildStatusSummaryUnknown } func startBuild(h *common.RequestHandler, pr *models.PullRequest, obsClient *common.ObsClient) error { @@ -267,16 +283,19 @@ func processPullNotification(h *common.RequestHandler, thread *models.Notificati } h.Log("repo content fetching ...") - refProject := string(h.GitCatFile(dir, pr.Head.Sha, "project.build")) - // buildPrjBytes, err := h.GetPullRequestFileContent(pr, "project.build") + refPrj := string(bytes.TrimSpace(h.GitCatFile(dir, pr.Head.Sha, "project.build"))) + + if len(refPrj) < 1 { + _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project") + if err != nil { + h.LogPlainError(err) + return + } + h.LogError("Cannot find reference project for %s PR#%d", pr.Base.Name, pr.Index) + return + } if h.HasError() { h.LogPlainError(h.Error) - /* - _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot find reference project") - if err != nil { - h.LogPlainError(err) - } - */ return } @@ -287,33 +306,32 @@ func processPullNotification(h *common.RequestHandler, thread *models.Notificati // recreate missing project h.LogError("missing OBS project ... recreating '%s': %v", obsProject, err) startBuild(h, pr, obsClient) + return } h.LogError("failed fetching build status for '%s': %v", obsProject, err) return } - refProjectResult, err := obsClient.BuildStatus(refProject) + refProjectResult, err := obsClient.BuildStatus(refPrj, prjResult.GetPackageList()...) if err != nil { - h.LogError("failed fetching ref project status for '%s': %v", refProject, err) + h.LogError("failed fetching ref project status for '%s': %v", refPrj, err) } - buildStatus := processBuildStatus(prjResult, refProjectResult) + buildStatus := processBuildStatus(h, prjResult, refProjectResult) switch buildStatus { - case BuildSuccess: + case BuildStatusSummarySuccess: _, err := h.AddReviewComment(pr, common.ReviewStateApproved, "Build successful") if err != nil { h.LogPlainError(err) } - case BuildFailed: + case BuildStatusSummaryFailed: _, err := h.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed") if err != nil { h.LogPlainError(err) } - case BuildBuilding: } + // waiting for build results -- nothing to do - // waiting for build results - // project := getObsProjectAssociatedWithPr(obsClient.HomeProject, pr) case common.ReviewStateApproved: // done, mark notification as read h.Log("processing request for success build ...")