feat: publish app delivery branches

This commit is contained in:
eding 2026-07-26 20:43:44 +02:00
parent 72fbbca0ac
commit a45618d704
3 changed files with 72 additions and 1 deletions

View file

@ -5,6 +5,7 @@ import (
"github.com/Pingu-Studio/MaidnCLI/internal/bootstrap"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
"github.com/Pingu-Studio/MaidnCLI/internal/openbao"
"github.com/Pingu-Studio/MaidnCLI/internal/ui"
"github.com/spf13/cobra"
@ -17,6 +18,7 @@ var bootstrapYes bool
var bootstrapPromptDemocraticCSI bool
var bootstrapPromptOperationalSecrets bool
var bootstrapInitializeOpenBaoRecovery bool
var bootstrapPublishAppFrom string
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
@ -33,11 +35,34 @@ func init() {
bootstrapCmd.Flags().BoolVar(&bootstrapPromptDemocraticCSI, "prompt-democratic-csi", false, "Prompt for and save Democratic CSI settings in --config")
bootstrapCmd.Flags().BoolVar(&bootstrapPromptOperationalSecrets, "prompt-operational-secrets", false, "Prompt for and encrypt operational secrets for --config")
bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBaoRecovery, "initialize-openbao-recovery", false, "Create and save a separate OpenBao recovery age identity for --config")
bootstrapCmd.Flags().StringVar(&bootstrapPublishAppFrom, "publish-app-from", "", "Push this app checkout's current branch and create a Forgejo delivery PR")
}
func runBootstrap(cmd *cobra.Command, args []string) error {
var cfg config.Config
var err error
if bootstrapPublishAppFrom != "" {
if bootstrapConfigPath == "" {
return fmt.Errorf("--publish-app-from requires --config")
}
cfg, err = config.Load(bootstrapConfigPath)
if err != nil {
return err
}
branch, err := forgejo.CurrentBranch(bootstrapPublishAppFrom)
if err != nil {
return err
}
owner, repo, err := forgejo.RepositoryFromURL(cfg.Delivery.AppRepoURL)
if err != nil {
return err
}
manager := forgejo.NewRepoManager(cfg.Git.BaseURL, cfg.Git.Token, owner, cfg.Git.Username, "", "", cfg.Delivery.AppRepoRef, "")
if err := manager.PushBranch(bootstrapPublishAppFrom, cfg.Delivery.AppRepoURL, branch); err != nil {
return err
}
return manager.CreatePullRequest(repo, "feat: migrate delivery to Tekton", branch, cfg.Delivery.AppRepoRef)
}
if bootstrapConfigPath != "" {
if bootstrapPromptDemocraticCSI || bootstrapPromptOperationalSecrets || bootstrapInitializeOpenBaoRecovery {

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -163,7 +164,11 @@ func (rm *RepoManager) setupRepository(repoURL, repoName string, existing bool,
}
func (rm *RepoManager) createMigrationPullRequest(repo, branch string) error {
body, err := json.Marshal(pullRequestRequest{Title: "feat: bootstrap Maidn CI/CD structure", Head: branch, Base: rm.Branch})
return rm.CreatePullRequest(repo, "feat: bootstrap Maidn CI/CD structure", branch, rm.Branch)
}
func (rm *RepoManager) CreatePullRequest(repo, title, head, base string) error {
body, err := json.Marshal(pullRequestRequest{Title: title, Head: head, Base: base})
if err != nil {
return err
}
@ -177,6 +182,40 @@ func (rm *RepoManager) createMigrationPullRequest(repo, branch string) error {
return nil
}
func (rm *RepoManager) PushBranch(dir, repoURL, branch string) error {
cleanupAskPass, environment, err := rm.gitEnvironment(dir)
if err != nil {
return err
}
defer cleanupAskPass()
return runGit(dir, environment, "push", repoURL, "HEAD:refs/heads/"+branch)
}
func CurrentBranch(dir string) (string, error) {
command := exec.Command("git", "branch", "--show-current")
command.Dir = dir
branch, err := command.Output()
if err != nil {
return "", err
}
if value := strings.TrimSpace(string(branch)); value != "" {
return value, nil
}
return "", fmt.Errorf("source repository is in detached HEAD state")
}
func RepositoryFromURL(repoURL string) (string, string, error) {
parsed, err := url.Parse(repoURL)
if err != nil {
return "", "", err
}
parts := strings.Split(strings.Trim(strings.TrimSuffix(parsed.Path, ".git"), "/"), "/")
if len(parts) < 2 || parts[len(parts)-2] == "" || parts[len(parts)-1] == "" {
return "", "", fmt.Errorf("invalid Forgejo repository URL")
}
return parts[len(parts)-2], parts[len(parts)-1], nil
}
func (rm *RepoManager) EnsureWebhook(repo, webhookURL, authorization string) error {
if webhookURL == "" || authorization == "" {
return fmt.Errorf("Forgejo webhook URL and authorization are required")

View file

@ -82,6 +82,13 @@ func TestEnsureWebhookCreatesMissingWebhook(t *testing.T) {
}
}
func TestRepositoryFromURL(t *testing.T) {
owner, repo, err := RepositoryFromURL("https://git.example.test/team/app.git")
if err != nil || owner != "team" || repo != "app" {
t.Fatalf("RepositoryFromURL() = (%q, %q, %v)", owner, repo, err)
}
}
func TestRepoExistsReturnsFalseOnNotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNotFound)