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 . */ import ( "encoding/json" "errors" "fmt" "io" "log" "os" "strings" "github.com/tailscale/hujson" ) //go:generate mockgen -source=config.go -destination=mock/config.go -typed const ( ProjectConfigFile = "workflow.config" StagingConfigFile = "staging.config" ) 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 } 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 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 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 } if c.Organization == org && c.Branch == branch { return c } } return nil } 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 := "master" 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]) } } return org, repo, branch } func (config *AutogitConfig) GetRemoteBranch() string { return "origin_" + config.Branch } 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 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 }