Using the BuildDisableRepos configuration, it is now possible to
define which repositories to build-disable in the QA project meta.
This is for example useful for the SLES development workflow, where
the product repository should only be enabled after the packagelist
definitions have been built - so it is not desirable to have them
built as soon as the QA project is created.
Example:
{
"ObsProject": "SUSE:SLFO:Main",
"StagingProject": "SUSE:SLFO:Main:PullRequest",
"QA": [
{
"Name": "SLES",
"Origin": "SUSE:SLFO:Products:SLES:16.1",
"BuildDisableRepos": ["product"]
}
]
}
Signed-off-by: Eugenio Paolantonio <eugenio.paolantonio@suse.com>
329 lines
8.6 KiB
Go
329 lines
8.6 KiB
Go
package common
|
|
|
|
/*
|
|
* This file is part of Autogits.
|
|
*
|
|
* Copyright © 2024 SUSE LLC
|
|
*
|
|
* Autogits is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License as published by the Free Software
|
|
* Foundation, either version 2 of the License, or (at your option) any later
|
|
* version.
|
|
*
|
|
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/tailscale/hujson"
|
|
)
|
|
|
|
//go:generate mockgen -source=config.go -destination=mock/config.go -typed
|
|
|
|
const (
|
|
ProjectConfigFile = "workflow.config"
|
|
StagingConfigFile = "staging.config"
|
|
|
|
Permission_ForceMerge = "force-merge"
|
|
Permission_Group = "release-engineering"
|
|
)
|
|
|
|
type ConfigFile struct {
|
|
GitProjectNames []string
|
|
}
|
|
|
|
type ReviewGroup struct {
|
|
Name string
|
|
Silent bool // will not request reviews from group members
|
|
Reviewers []string
|
|
}
|
|
|
|
type QAConfig struct {
|
|
Name string
|
|
Origin string
|
|
BuildDisableRepos []string // which repos to build disable in the new project
|
|
}
|
|
|
|
type Permissions struct {
|
|
Permission string
|
|
Members []string
|
|
}
|
|
|
|
const (
|
|
Label_StagingAuto = "staging/Auto"
|
|
Label_ReviewPending = "review/Pending"
|
|
Label_ReviewDone = "review/Done"
|
|
)
|
|
|
|
func LabelKey(tag_value string) string {
|
|
// capitalize first letter and remove /
|
|
if len(tag_value) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(tag_value[0:1]) + strings.ReplaceAll(tag_value[1:], "/", "")
|
|
}
|
|
|
|
type AutogitConfig struct {
|
|
Workflows []string // [pr, direct, test]
|
|
Organization string
|
|
GitProjectName string // Organization/GitProjectName.git is PrjGit
|
|
Branch string // branch name of PkgGit that aligns with PrjGit submodules
|
|
Reviewers []string // only used by `pr` workflow
|
|
Permissions []*Permissions // only used by `pr` workflow
|
|
ReviewGroups []*ReviewGroup
|
|
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
|
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
|
|
|
|
Labels map[string]string // list of tags, if not default, to apply
|
|
|
|
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
|
|
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
|
|
ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
|
|
}
|
|
|
|
type AutogitConfigs []*AutogitConfig
|
|
|
|
func ReadConfig(reader io.Reader) (*ConfigFile, error) {
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading config data: %w", err)
|
|
}
|
|
|
|
config := ConfigFile{}
|
|
data, err = hujson.Standardize(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse json: %w", err)
|
|
}
|
|
if err := json.Unmarshal(data, &config.GitProjectNames); err != nil {
|
|
return nil, fmt.Errorf("Error parsing Git Project paths: %w", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
func ReadConfigFile(filename string) (*ConfigFile, error) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot open config file for reading. err: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
return ReadConfig(file)
|
|
}
|
|
|
|
type GiteaFileContentAndRepoFetcher interface {
|
|
GiteaFileContentReader
|
|
GiteaRepoFetcher
|
|
}
|
|
|
|
func UnmarshalWorkflowConfig(data []byte) (*AutogitConfig, error) {
|
|
var config AutogitConfig
|
|
data, err := hujson.Standardize(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse json: %w", err)
|
|
}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("Error parsing workflow config file: %s: %w", string(data), err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string) (*AutogitConfig, error) {
|
|
hash := strings.Split(git_project, "#")
|
|
branch := ""
|
|
if len(hash) > 1 {
|
|
branch = hash[1]
|
|
}
|
|
|
|
a := strings.Split(hash[0], "/")
|
|
prjGitRepo := DefaultGitPrj
|
|
switch len(a) {
|
|
case 1:
|
|
case 2:
|
|
prjGitRepo = a[1]
|
|
default:
|
|
return nil, fmt.Errorf("Missing org/repo in projectgit: %s", git_project)
|
|
}
|
|
|
|
data, _, err := gitea.GetRepositoryFileContent(a[0], prjGitRepo, branch, ProjectConfigFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error fetching 'workflow.config' for %s/%s#%s: %w", a[0], prjGitRepo, branch, err)
|
|
}
|
|
|
|
config, err := UnmarshalWorkflowConfig(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(config.Organization) < 1 {
|
|
config.Organization = a[0]
|
|
}
|
|
config.GitProjectName = a[0] + "/" + prjGitRepo
|
|
if len(branch) == 0 {
|
|
if r, err := gitea.GetRepository(a[0], prjGitRepo); err == nil {
|
|
branch = r.DefaultBranch
|
|
} else {
|
|
return nil, fmt.Errorf("Failed to read workflow config in %s: %w", git_project, err)
|
|
}
|
|
}
|
|
config.GitProjectName = config.GitProjectName + "#" + branch
|
|
return config, nil
|
|
}
|
|
|
|
func ResolveWorkflowConfigs(gitea GiteaFileContentAndRepoFetcher, config *ConfigFile) (AutogitConfigs, error) {
|
|
configs := make([]*AutogitConfig, 0, len(config.GitProjectNames))
|
|
for _, git_project := range config.GitProjectNames {
|
|
c, err := ReadWorkflowConfig(gitea, git_project)
|
|
if err != nil {
|
|
// can't sync, so ignore for now
|
|
log.Println(err)
|
|
} else {
|
|
configs = append(configs, c)
|
|
}
|
|
}
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *AutogitConfig {
|
|
prjgit := org + "/" + repo + "#" + branch
|
|
for _, c := range configs {
|
|
if c.GitProjectName == prjgit {
|
|
return c
|
|
}
|
|
}
|
|
for _, c := range configs {
|
|
if c.Organization == org && c.Branch == branch {
|
|
return c
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (config *AutogitConfig) HasPermission(user, permission string) bool {
|
|
if config == nil {
|
|
return false
|
|
}
|
|
|
|
for _, p := range config.Permissions {
|
|
if p.Permission == permission {
|
|
if slices.Contains(p.Members, user) {
|
|
return true
|
|
}
|
|
|
|
for _, m := range p.Members {
|
|
if members, err := config.GetReviewGroupMembers(m); err == nil && slices.Contains(members, user) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (config *AutogitConfig) GetReviewGroupMembers(reviewer string) ([]string, error) {
|
|
for _, g := range config.ReviewGroups {
|
|
if g.Name == reviewer {
|
|
return g.Reviewers, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
|
}
|
|
|
|
func (config *AutogitConfig) GetReviewGroup(reviewer string) (*ReviewGroup, error) {
|
|
for _, g := range config.ReviewGroups {
|
|
if g.Name == reviewer {
|
|
return g, nil
|
|
}
|
|
}
|
|
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
|
}
|
|
|
|
func (config *AutogitConfig) GetPrjGit() (string, string, string) {
|
|
org := config.Organization
|
|
repo := DefaultGitPrj
|
|
branch := ""
|
|
|
|
a := strings.Split(config.GitProjectName, "/")
|
|
if len(a[0]) > 0 {
|
|
repo = strings.TrimSpace(a[0])
|
|
}
|
|
if len(a) == 2 {
|
|
if a[0] = strings.TrimSpace(a[0]); len(a[0]) > 0 {
|
|
org = a[0]
|
|
}
|
|
repo = strings.TrimSpace(a[1])
|
|
}
|
|
b := strings.Split(repo, "#")
|
|
if len(b) == 2 {
|
|
if b[0] = strings.TrimSpace(b[0]); len(b[0]) > 0 {
|
|
repo = b[0]
|
|
} else {
|
|
repo = DefaultGitPrj
|
|
}
|
|
if b[1] = strings.TrimSpace(b[1]); len(b[1]) > 0 {
|
|
branch = strings.TrimSpace(b[1])
|
|
}
|
|
}
|
|
|
|
if len(branch) == 0 {
|
|
panic("branch for project is undefined. Should not happend." + org + "/" + repo)
|
|
}
|
|
return org, repo, branch
|
|
}
|
|
|
|
func (config *AutogitConfig) GetRemoteBranch() string {
|
|
return "origin_" + config.Branch
|
|
}
|
|
|
|
func (config *AutogitConfig) Label(label string) string {
|
|
if t, found := config.Labels[LabelKey(label)]; found {
|
|
return t
|
|
}
|
|
|
|
return label
|
|
}
|
|
|
|
type StagingConfig struct {
|
|
ObsProject string
|
|
RebuildAll bool
|
|
CleanupDelay int // cleanup delay, in hours, for unmerged closed PRs (def: 48)
|
|
|
|
// if set, then only use pull request numbers as unique identifiers
|
|
StagingProject string
|
|
QA []QAConfig
|
|
}
|
|
|
|
func ParseStagingConfig(data []byte) (*StagingConfig, error) {
|
|
var staging StagingConfig
|
|
if len(data) == 0 {
|
|
return nil, errors.New("non-existent config file.")
|
|
}
|
|
data, err := hujson.Standardize(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
staging.CleanupDelay = 48
|
|
if err := json.Unmarshal(data, &staging); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &staging, nil
|
|
}
|