blob: 77a1b5f48751036fdfa3a7f961ad2bb3a77aa0cb [file] [log] [blame]
package image_repositories
import (
"context"
"fmt"
"net/http"
"os"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
git_http "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/google/go-github/v47/github"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
type ImageRepository struct {
Project string
githubClient *github.Client
githubProjectName string
gitAuth *git_http.BasicAuth
}
func NewImageRepository(project string) *ImageRepository {
githubToken := os.Getenv("GITHUB_TOKEN")
ctx := context.TODO()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
)
tc := oauth2.NewClient(ctx, ts)
return &ImageRepository{
Project: project,
githubClient: github.NewClient(tc),
githubProjectName: fmt.Sprintf("docker-openstack-%s", project),
gitAuth: &git_http.BasicAuth{
Username: "x-access-token",
Password: githubToken,
},
}
}
func (i *ImageRepository) WriteFiles(ctx context.Context, fs billy.Filesystem) error {
// .github/workflows/build.yml
build := NewBuildWorkflow(i.Project)
err := build.WriteFile(fs)
if err != nil {
return err
}
// .github/workflows/sync.yml
sync := NewSyncWorkflow(i.Project)
err = sync.WriteFile(fs)
if err != nil {
return err
}
// .dockerignore
di := NewDockerIgnore()
err = di.WriteFile(fs)
if err != nil {
return err
}
// .pre-commit-config.yaml
pcc := NewPreCommitConfig()
err = pcc.WriteFile(fs)
if err != nil {
return err
}
// Dockerfile
df, err := NewDockerfile(ctx, i)
if err != nil {
return err
}
err = df.WriteFile(fs)
if err != nil {
return err
}
// manifest.yml
mf, err := NewImageManifest(i.Project)
if err != nil {
return err
}
err = mf.WriteFile(fs)
if err != nil {
return err
}
// README.md
rm, err := NewReadme(i.Project)
if err != nil {
return err
}
err = rm.WriteFile(fs)
if err != nil {
return err
}
return nil
}
func (i *ImageRepository) CreateGithubRepository(ctx context.Context) error {
repo := &github.Repository{
Name: github.String(i.githubProjectName),
AutoInit: github.Bool(true),
}
_, _, err := i.githubClient.Repositories.Create(ctx, "vexxhost", repo)
if err != nil {
return err
}
return nil
}
func (i *ImageRepository) GetGitHubRepository(ctx context.Context, owner string) (*github.Repository, error) {
repo, _, err := i.githubClient.Repositories.Get(ctx, owner, i.githubProjectName)
if err != nil {
return nil, err
}
return repo, nil
}
func (i *ImageRepository) ForkGitHubRepository(ctx context.Context) (*github.Repository, error) {
repo, err := i.GetGitHubRepository(ctx, "vexxhost-bot")
if err != nil {
i.githubClient.Repositories.CreateFork(ctx, "vexxhost", i.githubProjectName, nil)
time.Sleep(20 * time.Second)
return i.GetGitHubRepository(ctx, "vexxhost-bot")
}
return repo, nil
}
func (i *ImageRepository) UpdateGithubConfiguration(ctx context.Context) error {
// Description
description := fmt.Sprintf("Docker image for OpenStack: %s", i.Project)
// Updated repository
repo := &github.Repository{
AllowMergeCommit: github.Bool(false),
AllowRebaseMerge: github.Bool(true),
AllowSquashMerge: github.Bool(false),
DeleteBranchOnMerge: github.Bool(true),
Description: github.String(description),
Visibility: github.String("public"),
HasWiki: github.Bool(false),
HasIssues: github.Bool(false),
HasProjects: github.Bool(false),
}
// Update the repository with the correct settings
repo, _, err := i.githubClient.Repositories.Edit(ctx, "vexxhost", i.githubProjectName, repo)
if err != nil {
return err
}
// Branch protection
protection := &github.ProtectionRequest{
RequiredPullRequestReviews: &github.PullRequestReviewsEnforcementRequest{
RequiredApprovingReviewCount: 1,
DismissStaleReviews: true,
BypassPullRequestAllowancesRequest: &github.BypassPullRequestAllowancesRequest{
Users: []string{"mnaser"},
Teams: []string{},
Apps: []string{},
},
},
RequiredStatusChecks: &github.RequiredStatusChecks{
Strict: true,
Contexts: nil,
Checks: []*github.RequiredStatusCheck{
{
Context: "image (wallaby)",
},
{
Context: "image (xena)",
},
{
Context: "image (yoga)",
},
},
},
RequiredConversationResolution: github.Bool(true),
RequireLinearHistory: github.Bool(true),
EnforceAdmins: false,
AllowForcePushes: github.Bool(false),
AllowDeletions: github.Bool(false),
}
_, _, err = i.githubClient.Repositories.UpdateBranchProtection(ctx, *repo.Owner.Login, *repo.Name, "main", protection)
if err != nil {
return err
}
return nil
}
func (i *ImageRepository) Synchronize(ctx context.Context, admin bool) error {
var githubRepo *github.Repository
var err error
if admin {
githubRepo, err = i.GetGitHubRepository(ctx, "vexxhost")
} else {
githubRepo, err = i.ForkGitHubRepository(ctx)
}
if err != nil {
return err
}
storer := memory.NewStorage()
fs := memfs.New()
upstreamUrl := fmt.Sprintf("https://github.com/vexxhost/%s.git", i.githubProjectName)
repo, err := git.Clone(storer, fs, &git.CloneOptions{
Auth: i.gitAuth,
URL: upstreamUrl,
RemoteName: "upstream",
})
if err != nil {
return err
}
_, err = repo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{*githubRepo.CloneURL},
})
if err != nil {
return err
}
headRef, err := repo.Head()
if err != nil {
return err
}
ref := plumbing.NewHashReference("refs/heads/sync/atmosphere-ci", headRef.Hash())
err = repo.Storer.SetReference(ref)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
err = worktree.Checkout(&git.CheckoutOptions{
Branch: ref.Name(),
})
if err != nil {
return err
}
err = i.WriteFiles(ctx, fs)
if err != nil {
return err
}
status, err := worktree.Status()
if err != nil {
return err
}
if status.IsClean() {
log.Info("No changes to commit")
return nil
}
_, err = worktree.Add(".")
if err != nil {
return err
}
commit, err := worktree.Commit("chore: sync using `atmosphere-ci`", &git.CommitOptions{
All: true,
Author: &object.Signature{
Name: "vexxhost-bot",
Email: "mnaser+bot@vexxhost.com",
When: time.Now(),
},
})
if err != nil {
return err
}
err = repo.Push(&git.PushOptions{
Auth: i.gitAuth,
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
RemoteName: "origin",
Force: true,
})
if err != nil {
return err
}
err = i.CreatePullRequest(ctx, githubRepo, commit)
if err != nil {
return err
}
return nil
}
func (i *ImageRepository) CreatePullRequest(ctx context.Context, repo *github.Repository, commit plumbing.Hash) error {
head := fmt.Sprintf("%s:%s", *repo.Owner.Login, "sync/atmosphere-ci")
newPR := &github.NewPullRequest{
Title: github.String("⚙️ Automatic sync from `atmosphere-ci`"),
Head: github.String(head),
Base: github.String("main"),
Body: github.String("This is an automatic pull request from `atmosphere-ci`"),
}
prs, _, err := i.githubClient.PullRequests.ListPullRequestsWithCommit(ctx, *repo.Owner.Login, *repo.Name, commit.String(), &github.PullRequestListOptions{})
if err != nil {
return err
}
if len(prs) > 0 {
log.Info("Pull request already exists: ", prs[0].GetHTMLURL())
return nil
}
pr, resp, err := i.githubClient.PullRequests.Create(ctx, "vexxhost", *repo.Name, newPR)
if err != nil && resp.StatusCode != http.StatusUnprocessableEntity {
return err
}
log.Info("PR created: ", pr.GetHTMLURL())
return nil
}