fix: support fresh delivery promotion
This commit is contained in:
parent
d3f204c873
commit
7038197757
|
|
@ -28,6 +28,12 @@ spec:
|
|||
default: ""
|
||||
- name: app-revision
|
||||
default: ""
|
||||
- name: forgejo-base-url
|
||||
default: ""
|
||||
- name: forgejo-owner
|
||||
default: ""
|
||||
- name: manifests-repo
|
||||
default: ""
|
||||
steps:
|
||||
- name: update
|
||||
image: alpine/git:2.47.2
|
||||
|
|
@ -52,6 +58,12 @@ spec:
|
|||
value: $(params.app-url)
|
||||
- name: APP_REVISION
|
||||
value: $(params.app-revision)
|
||||
- name: FORGEJO_BASE_URL
|
||||
value: $(params.forgejo-base-url)
|
||||
- name: FORGEJO_OWNER
|
||||
value: $(params.forgejo-owner)
|
||||
- name: MANIFESTS_REPO
|
||||
value: $(params.manifests-repo)
|
||||
script: |
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
|
@ -61,13 +73,29 @@ spec:
|
|||
valid_commit() { [ "${#1}" -eq 40 ] || fail; case "$1" in *[!0-9a-fA-F]*) fail ;; esac; }
|
||||
valid_url() { case "$1" in https://*/*.git) ;; *) fail ;; esac; case "$1" in *[@?#]*) fail ;; esac; }
|
||||
valid_repository() { case "$1" in */*) ;; *) fail ;; esac; case "$1" in *..*|*//*|/*|*/) fail ;; esac; }
|
||||
valid_repository_part() { case "$1" in ''|*[!A-Za-z0-9._-]*|.*|*.) fail ;; esac; }
|
||||
valid_forgejo_base_url() { case "$1" in https://*) ;; *) fail ;; esac; host=${1#https://}; case "$host" in *:*) name=${host%:*}; port=${host##*:}; case "$port" in ''|*[!0-9]*) fail ;; esac ;; *) name=$host ;; esac; case "$name" in ''|*[!A-Za-z0-9.-]*|.*|*.) fail ;; esac; }
|
||||
valid_pr_number() { case "$1" in [1-9]*) ;; *) fail ;; esac; case "$1" in *[!0-9]*) fail ;; esac; [ $((${#APP_NAME} + ${#1} + 4)) -le 63 ] || fail; }
|
||||
valid_name "$APP_NAME"
|
||||
valid_url "$MANIFESTS_URL"
|
||||
valid_revision "$MANIFESTS_BRANCH"
|
||||
valid_repository "$APP_REPOSITORY"
|
||||
valid_commit "$TAG"
|
||||
if [ "$ENVIRONMENT" = production ]; then
|
||||
valid_forgejo_base_url "$FORGEJO_BASE_URL"
|
||||
valid_repository_part "$FORGEJO_OWNER"
|
||||
valid_repository_part "$MANIFESTS_REPO"
|
||||
[ "$MANIFESTS_URL" = "$FORGEJO_BASE_URL/$FORGEJO_OWNER/$MANIFESTS_REPO.git" ] || fail
|
||||
PROMOTION_BRANCH="maidn/promotion-$APP_NAME-$TAG"
|
||||
if git ls-remote --exit-code "$MANIFESTS_URL" "refs/heads/$PROMOTION_BRANCH" >/dev/null 2>&1; then
|
||||
git clone --branch "$PROMOTION_BRANCH" "$MANIFESTS_URL" /tmp/manifests
|
||||
else
|
||||
git clone --branch "$MANIFESTS_BRANCH" "$MANIFESTS_URL" /tmp/manifests
|
||||
git -C /tmp/manifests checkout -b "$PROMOTION_BRANCH"
|
||||
fi
|
||||
else
|
||||
git clone --branch "$MANIFESTS_BRANCH" "$MANIFESTS_URL" /tmp/manifests
|
||||
fi
|
||||
cd /tmp/manifests
|
||||
if [ "$ENVIRONMENT" = preview ]; then
|
||||
valid_pr_number "$PR_NUMBER"
|
||||
|
|
@ -159,10 +187,96 @@ spec:
|
|||
root=apps/previews/kustomization.yaml
|
||||
grep -q '^resources:' "$root" || fail
|
||||
grep -qxF " - $APP_NAME-pr-$PR_NUMBER" "$root" 2>/dev/null || printf ' - %s-pr-%s\n' "$APP_NAME" "$PR_NUMBER" >> "$root"
|
||||
elif [ "$ENVIRONMENT" = staging ] || [ "$ENVIRONMENT" = production ]; then
|
||||
app_dir="apps/$ENVIRONMENT/$APP_NAME"
|
||||
[ -f "$app_dir/release.yaml" ] || fail
|
||||
elif [ "$ENVIRONMENT" = staging ]; then
|
||||
app_dir="apps/staging/$APP_NAME"
|
||||
root=apps/staging/kustomization.yaml
|
||||
if [ -e "$app_dir/release.yaml" ]; then
|
||||
[ -d "$app_dir" ] && [ ! -L "$app_dir" ] && [ -f "$app_dir/release.yaml" ] && [ ! -L "$app_dir/release.yaml" ] || fail
|
||||
sed -i -E "s|^([[:space:]]*tag:).*|\1 $TAG|" "$app_dir/release.yaml"
|
||||
else
|
||||
[ ! -e "$app_dir" ] || fail
|
||||
mkdir -p "$app_dir"
|
||||
cat > "$app_dir/release.yaml" <<EOF
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: $APP_NAME
|
||||
spec:
|
||||
interval: 5m
|
||||
chart:
|
||||
spec:
|
||||
chart: ./charts/$APP_NAME
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: $APP_NAME
|
||||
namespace: flux-system
|
||||
values:
|
||||
image:
|
||||
repository: $IMAGE
|
||||
tag: $TAG
|
||||
EOF
|
||||
cat > "$app_dir/kustomization.yaml" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- release.yaml
|
||||
EOF
|
||||
if [ ! -e "$root" ]; then
|
||||
mkdir -p "$(dirname "$root")"
|
||||
cat > "$root" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
EOF
|
||||
fi
|
||||
[ -f "$root" ] && [ ! -L "$root" ] && grep -qx 'resources:' "$root" || fail
|
||||
grep -qxF " - $APP_NAME" "$root" 2>/dev/null || printf ' - %s\n' "$APP_NAME" >> "$root"
|
||||
fi
|
||||
elif [ "$ENVIRONMENT" = production ]; then
|
||||
app_dir="apps/production/$APP_NAME"
|
||||
root=apps/production/kustomization.yaml
|
||||
if [ -e "$app_dir" ]; then
|
||||
[ -d "$app_dir" ] && [ ! -L "$app_dir" ] || fail
|
||||
fi
|
||||
mkdir -p "$app_dir"
|
||||
for file in "$app_dir/release.yaml" "$app_dir/kustomization.yaml"; do
|
||||
[ ! -e "$file" ] || { [ -f "$file" ] && [ ! -L "$file" ]; } || fail
|
||||
done
|
||||
cat > "$app_dir/release.yaml" <<EOF
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: $APP_NAME
|
||||
spec:
|
||||
interval: 5m
|
||||
chart:
|
||||
spec:
|
||||
chart: ./charts/$APP_NAME
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: $APP_NAME
|
||||
namespace: flux-system
|
||||
values:
|
||||
image:
|
||||
repository: $IMAGE
|
||||
tag: $TAG
|
||||
EOF
|
||||
cat > "$app_dir/kustomization.yaml" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- release.yaml
|
||||
EOF
|
||||
if [ ! -e "$root" ]; then
|
||||
mkdir -p "$(dirname "$root")"
|
||||
cat > "$root" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
EOF
|
||||
fi
|
||||
[ -f "$root" ] && [ ! -L "$root" ] && grep -qx 'resources:' "$root" || fail
|
||||
grep -qxF " - $APP_NAME" "$root" 2>/dev/null || printf ' - %s\n' "$APP_NAME" >> "$root"
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
|
@ -170,7 +284,39 @@ spec:
|
|||
git config user.email maidn@free-maidn.com
|
||||
git add apps
|
||||
git diff --cached --quiet || git commit -m "chore: deploy $APP_NAME $TAG"
|
||||
if [ "$ENVIRONMENT" = production ]; then
|
||||
git push origin "HEAD:$PROMOTION_BRANCH"
|
||||
credentials=$(printf 'url=%s\n\n' "$MANIFESTS_URL" | GIT_TERMINAL_PROMPT=0 git credential fill)
|
||||
forgejo_user=$(printf '%s\n' "$credentials" | sed -n 's/^username=//p')
|
||||
forgejo_password=$(printf '%s\n' "$credentials" | sed -n 's/^password=//p')
|
||||
unset credentials
|
||||
[ -n "$forgejo_user" ] && [ -n "$forgejo_password" ] || fail
|
||||
forgejo_auth=$(printf '%s:%s' "$forgejo_user" "$forgejo_password" | base64 | tr -d '\n')
|
||||
unset forgejo_user forgejo_password
|
||||
pr_endpoint="$FORGEJO_BASE_URL/api/v1/repos/$FORGEJO_OWNER/$MANIFESTS_REPO/pulls"
|
||||
pr_query="$pr_endpoint?state=open&head=$FORGEJO_OWNER%3A$PROMOTION_BRANCH"
|
||||
pr_response=$(mktemp)
|
||||
pr_body=$(mktemp)
|
||||
trap 'rm -f "$pr_response" "$pr_body"' EXIT
|
||||
open_pr_count() {
|
||||
grep -q '^[[:space:]]*\[' "$pr_response" || fail
|
||||
grep -o '"number"[[:space:]]*:[[:space:]]*[0-9][0-9]*' "$pr_response" | wc -l | tr -d ' '
|
||||
}
|
||||
wget -q -O "$pr_response" --header="Authorization: Basic $forgejo_auth" "$pr_query" || fail
|
||||
case "$(open_pr_count)" in
|
||||
0)
|
||||
printf '{"title":"chore: promote %s %s","head":"%s","base":"%s"}' "$APP_NAME" "$TAG" "$PROMOTION_BRANCH" "$MANIFESTS_BRANCH" > "$pr_body"
|
||||
wget -q -O "$pr_response" --header="Authorization: Basic $forgejo_auth" --header="Content-Type: application/json" --post-file "$pr_body" "$pr_endpoint" || true
|
||||
wget -q -O "$pr_response" --header="Authorization: Basic $forgejo_auth" "$pr_query" || fail
|
||||
[ "$(open_pr_count)" = 1 ] || fail
|
||||
;;
|
||||
1) ;;
|
||||
*) fail ;;
|
||||
esac
|
||||
unset forgejo_auth
|
||||
else
|
||||
git push origin "$MANIFESTS_BRANCH"
|
||||
fi
|
||||
---
|
||||
apiVersion: tekton.dev/v1
|
||||
kind: Pipeline
|
||||
|
|
@ -197,6 +343,12 @@ spec:
|
|||
default: {{ quote .ManifestsURL }}
|
||||
- name: manifests-branch
|
||||
default: {{ quote .ManifestsBranch }}
|
||||
- name: forgejo-base-url
|
||||
default: {{ quote .ForgejoBaseURL }}
|
||||
- name: forgejo-owner
|
||||
default: {{ quote .ForgejoOwner }}
|
||||
- name: manifests-repo
|
||||
default: {{ quote .ManifestsRepo }}
|
||||
workspaces:
|
||||
- name: source
|
||||
taskRunTemplate:
|
||||
|
|
@ -307,6 +459,12 @@ spec:
|
|||
value: $(params.git-revision)
|
||||
- name: environment
|
||||
value: production
|
||||
- name: forgejo-base-url
|
||||
value: $(params.forgejo-base-url)
|
||||
- name: forgejo-owner
|
||||
value: $(params.forgejo-owner)
|
||||
- name: manifests-repo
|
||||
value: $(params.manifests-repo)
|
||||
- name: cleanup-preview
|
||||
when:
|
||||
- input: $(params.event-type)
|
||||
|
|
|
|||
|
|
@ -543,7 +543,9 @@ type appDeliveryTemplateConfig struct {
|
|||
BuildOutputDirectory string
|
||||
BuildConfiguration string
|
||||
ForgejoBaseURL string
|
||||
ForgejoOwner string
|
||||
ManifestsURL string
|
||||
ManifestsRepo string
|
||||
ManifestsBranch string
|
||||
}
|
||||
|
||||
|
|
@ -619,9 +621,9 @@ func renderAppDelivery(cfg config.Config) ([]byte, error) {
|
|||
AppName: cfg.Delivery.AppName, AppRepository: appRepository, AppRepoURL: cfg.Delivery.AppRepoURL,
|
||||
AppRepoRef: cfg.Delivery.AppRepoRef, ProductionBranch: cfg.Delivery.ProductionBranch, ImageRepository: cfg.Delivery.ImageRepository,
|
||||
BuildOutputDirectory: cfg.Delivery.BuildOutputDirectory, BuildConfiguration: cfg.Delivery.BuildConfiguration,
|
||||
ForgejoBaseURL: origin, ManifestsURL: forgejo.CloneURL(origin, cfg.Git.Owner, cfg.Flux.ManifestsRepo), ManifestsBranch: cfg.Flux.Branch,
|
||||
ForgejoBaseURL: origin, ForgejoOwner: cfg.Git.Owner, ManifestsURL: forgejo.CloneURL(origin, cfg.Git.Owner, cfg.Flux.ManifestsRepo), ManifestsRepo: cfg.Flux.ManifestsRepo, ManifestsBranch: cfg.Flux.Branch,
|
||||
}
|
||||
for name, value := range map[string]string{"appRepository": values.AppRepository, "appRepoUrl": values.AppRepoURL, "appRepoRef": values.AppRepoRef, "productionBranch": values.ProductionBranch, "imageRepository": values.ImageRepository, "buildOutputDirectory": values.BuildOutputDirectory, "buildConfiguration": values.BuildConfiguration, "forgejoBaseUrl": values.ForgejoBaseURL, "manifestsUrl": values.ManifestsURL, "manifestsBranch": values.ManifestsBranch} {
|
||||
for name, value := range map[string]string{"appRepository": values.AppRepository, "appRepoUrl": values.AppRepoURL, "appRepoRef": values.AppRepoRef, "productionBranch": values.ProductionBranch, "imageRepository": values.ImageRepository, "buildOutputDirectory": values.BuildOutputDirectory, "buildConfiguration": values.BuildConfiguration, "forgejoBaseUrl": values.ForgejoBaseURL, "forgejoOwner": values.ForgejoOwner, "manifestsUrl": values.ManifestsURL, "manifestsRepo": values.ManifestsRepo, "manifestsBranch": values.ManifestsBranch} {
|
||||
if value == "" || strings.ContainsAny(value, "\r\n") || config.RedactURL(value) != value {
|
||||
return nil, fmt.Errorf("delivery %s cannot be empty or contain credentials", name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,33 @@ func TestGeneratedDeliveryIsGenericAndUsesSafePreviewCleanupContract(t *testing.
|
|||
}
|
||||
}
|
||||
|
||||
func TestGeneratedDeliveryInitializesStagingAndPromotesByPullRequest(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
||||
Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"},
|
||||
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production"},
|
||||
}
|
||||
content, err := renderAppDelivery(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rendered := string(content)
|
||||
for _, expected := range []string{
|
||||
`app_dir="apps/staging/$APP_NAME"`, `root=apps/staging/kustomization.yaml`, `name: $APP_NAME`, `namespace: flux-system`,
|
||||
`PROMOTION_BRANCH="maidn/promotion-$APP_NAME-$TAG"`, `git push origin "HEAD:$PROMOTION_BRANCH"`, `--post-file "$pr_body"`,
|
||||
`head=$FORGEJO_OWNER%3A$PROMOTION_BRANCH`, `value: $(params.forgejo-base-url)`,
|
||||
} {
|
||||
if !strings.Contains(rendered, expected) {
|
||||
t.Fatalf("generated delivery does not contain %q", expected)
|
||||
}
|
||||
}
|
||||
production := rendered[strings.Index(rendered, `if [ "$ENVIRONMENT" = production ]; then`):]
|
||||
production = production[:strings.Index(production, "\n else\n")]
|
||||
if strings.Contains(production, `git push origin "$MANIFESTS_BRANCH"`) {
|
||||
t.Fatal("production path pushes directly to manifests main")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAppDeliveryRequiresCompleteConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := GenerateAppDelivery(dir, config.Config{Git: config.GitConfig{BaseURL: "https://git.example.test"}, Delivery: config.DeliveryConfig{AppName: "legacy-app"}})
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
|||
if err := forgejo.EnsureCleanCheckout(sourceDir); err != nil {
|
||||
return err
|
||||
}
|
||||
branch, err := forgejo.CurrentBranch(sourceDir)
|
||||
sourceBranch, err := forgejo.CurrentBranch(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if branch != resolved.Delivery.AppRepoRef {
|
||||
if sourceBranch != resolved.Delivery.AppRepoRef {
|
||||
return errors.New("--from branch must match delivery appRepoRef")
|
||||
}
|
||||
owner, repository, err := forgejo.RepositoryFromURL(resolved.Delivery.AppRepoURL)
|
||||
|
|
@ -45,13 +45,13 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
|||
if _, err := manager.EnsureRepository(repository, "Application source for Maidn CI/CD delivery"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := publishInitialAppBranches(manager, sourceDir, resolved.Delivery.AppRepoURL, sourceBranch, resolved.Delivery.AppRepoRef, resolved.Delivery.ProductionBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manager.EnsureProtectedBranch(repository, resolved.Delivery.ProductionBranch); err != nil {
|
||||
return fmt.Errorf("protect Forgejo production branch: %w", err)
|
||||
}
|
||||
if err := manager.PushRef(sourceDir, resolved.Delivery.AppRepoURL, branch, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
changed, err := manager.PublishDeliveryBranch(sourceDir, branch, resolved.Delivery.AppRepoURL, deliveryBranch, func(dir string) error {
|
||||
changed, err := manager.PublishDeliveryBranch(sourceDir, sourceBranch, resolved.Delivery.AppRepoURL, deliveryBranch, func(dir string) error {
|
||||
return GenerateAppDelivery(dir, resolved)
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -98,6 +98,45 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// publishInitialAppBranches establishes the immutable source baseline before delivery setup.
|
||||
func publishInitialAppBranches(manager *forgejo.RepoManager, sourceDir, targetURL, sourceBranch, targetBranch, productionBranch string) error {
|
||||
sourceRevision, err := forgejo.BranchRevision(sourceDir, sourceBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mainRevision, err := manager.RemoteBranchRevision(targetURL, targetBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read target base branch: %w", err)
|
||||
}
|
||||
if mainRevision == "" {
|
||||
if err := manager.PushRef(sourceDir, targetURL, sourceRevision, targetBranch); err != nil {
|
||||
return fmt.Errorf("publish source base branch: %w", err)
|
||||
}
|
||||
} else if mainRevision != sourceRevision {
|
||||
return errors.New("target base branch differs from the validated source ref; refusing to overwrite it")
|
||||
}
|
||||
if productionRevision, err := manager.RemoteBranchRevision(targetURL, productionBranch); err != nil {
|
||||
return fmt.Errorf("read target production branch: %w", err)
|
||||
} else if productionRevision == "" {
|
||||
if err := manager.PushRef(sourceDir, targetURL, sourceRevision, productionBranch); err != nil {
|
||||
return fmt.Errorf("create production from source base branch: %w", err)
|
||||
}
|
||||
}
|
||||
mainRevision, err = manager.RemoteBranchRevision(targetURL, targetBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify target base branch: %w", err)
|
||||
}
|
||||
if mainRevision != sourceRevision {
|
||||
return errors.New("target base branch does not match the validated source ref")
|
||||
}
|
||||
if productionRevision, err := manager.RemoteBranchRevision(targetURL, productionBranch); err != nil {
|
||||
return fmt.Errorf("verify target production branch: %w", err)
|
||||
} else if productionRevision == "" {
|
||||
return errors.New("target production branch was not created")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAppInCluster writes only the managed Flux registration for one app.
|
||||
func RegisterAppInCluster(dir string, cfg config.Config) error {
|
||||
content, err := renderAppRegistration(cfg)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,26 @@ package bootstrap
|
|||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
|
||||
)
|
||||
|
||||
func onboardingGit(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
command := exec.Command("git", args...)
|
||||
command.Dir = dir
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output)
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
func onboardingConfig() config.Config {
|
||||
return config.Config{
|
||||
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "test-org-2"},
|
||||
|
|
@ -76,3 +89,56 @@ func TestRegisterAppInClusterRendersManagedFluxSource(t *testing.T) {
|
|||
t.Fatalf("unmanaged app registration was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
||||
source := filepath.Join(t.TempDir(), "source")
|
||||
target := filepath.Join(t.TempDir(), "target.git")
|
||||
if err := os.Mkdir(source, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onboardingGit(t, source, "init", "-b", "source")
|
||||
onboardingGit(t, source, "config", "user.name", "Test")
|
||||
onboardingGit(t, source, "config", "user.email", "test@example.test")
|
||||
if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("source\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onboardingGit(t, source, "add", "README.md")
|
||||
onboardingGit(t, source, "commit", "-m", "source")
|
||||
sourceRevision := onboardingGit(t, source, "rev-parse", "source")
|
||||
onboardingGit(t, "", "init", "--bare", target)
|
||||
|
||||
manager := forgejo.NewRepoManager("https://git.example.test", "", "owner", "", "", "", "main", "")
|
||||
if err := publishInitialAppBranches(manager, source, target, "source", "main", "production"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, branch := range []string{"main", "production"} {
|
||||
if got := onboardingGit(t, "", "--git-dir", target, "rev-parse", "refs/heads/"+branch); got != sourceRevision {
|
||||
t.Fatalf("%s = %s, want source %s", branch, got, sourceRevision)
|
||||
}
|
||||
}
|
||||
|
||||
preservedTarget := filepath.Join(t.TempDir(), "preserved-target.git")
|
||||
onboardingGit(t, "", "init", "--bare", preservedTarget)
|
||||
onboardingGit(t, source, "push", preservedTarget, "source:main")
|
||||
production := filepath.Join(t.TempDir(), "production")
|
||||
if err := os.Mkdir(production, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onboardingGit(t, production, "init", "-b", "production")
|
||||
onboardingGit(t, production, "config", "user.name", "Test")
|
||||
onboardingGit(t, production, "config", "user.email", "test@example.test")
|
||||
if err := os.WriteFile(filepath.Join(production, "README.md"), []byte("existing production\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onboardingGit(t, production, "add", "README.md")
|
||||
onboardingGit(t, production, "commit", "-m", "existing production")
|
||||
existingProduction := onboardingGit(t, production, "rev-parse", "production")
|
||||
onboardingGit(t, production, "push", preservedTarget, "production:production")
|
||||
|
||||
if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/production"); got != existingProduction {
|
||||
t.Fatalf("production = %s, want existing %s", got, existingProduction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -640,6 +640,43 @@ func (rm *RepoManager) HasRemoteBranch(repoURL, branch string) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// BranchRevision resolves branch to the checked-out commit that may be published.
|
||||
func BranchRevision(dir, branch string) (string, error) {
|
||||
command := exec.Command("git", "rev-parse", "--verify", branch+"^{commit}")
|
||||
command.Dir = dir
|
||||
revision, err := command.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve source branch %q: %w", branch, err)
|
||||
}
|
||||
if revision = bytes.TrimSpace(revision); len(revision) == 0 {
|
||||
return "", fmt.Errorf("source branch %q has no commit", branch)
|
||||
}
|
||||
return string(revision), nil
|
||||
}
|
||||
|
||||
// RemoteBranchRevision returns the remote branch commit, or an empty string when absent.
|
||||
func (rm *RepoManager) RemoteBranchRevision(repoURL, branch string) (string, error) {
|
||||
cleanupAskPass, environment, err := rm.gitEnvironment()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cleanupAskPass()
|
||||
command := exec.Command("git", "ls-remote", "--refs", repoURL, "refs/heads/"+branch)
|
||||
command.Env = environment
|
||||
output, err := command.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fields := strings.Fields(string(output))
|
||||
if len(fields) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(fields) != 2 || fields[1] != "refs/heads/"+branch {
|
||||
return "", fmt.Errorf("unexpected remote ref response for branch %q", branch)
|
||||
}
|
||||
return fields[0], nil
|
||||
}
|
||||
|
||||
func CurrentBranch(dir string) (string, error) {
|
||||
command := exec.Command("git", "branch", "--show-current")
|
||||
command.Dir = dir
|
||||
|
|
|
|||
Loading…
Reference in a new issue