feat(cmd): release v1.10.0 — shallow clone guard and --check preflight
- refuse to release when the clone is shallow and no previous release tag is found: tags beyond the fetch depth would silently restart versioning at X.Y.0; --allow-shallow bypasses the guard for a true first release - new --check preflight validates the release environment without releasing: branch resolution + pattern, working tree, tag discovery, shallow clone, remote origin URL parse (same parse the push performs), push auth method, version files, release target — all problems reported at once, non-zero exit on failure - .releaser.gitlab-ci.yml gains an optional .releaser:check MR-pipeline job; CI examples now set GIT_DEPTH: 0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,18 @@ func CurrentBranch(repo *gogit.Repository) (string, error) {
|
||||
return "", fmt.Errorf("HEAD is detached and no CI branch variable is set (%s) — use --branch to specify the release branch", strings.Join(ciBranchEnvVars, ", "))
|
||||
}
|
||||
|
||||
// IsShallow reports whether the repository is a shallow clone. In shallow CI
|
||||
// checkouts (GitLab CI defaults to GIT_DEPTH: 20) release tags beyond the
|
||||
// fetch depth are invisible to LatestTag, which would silently restart
|
||||
// versioning at X.Y.0.
|
||||
func IsShallow(repo *gogit.Repository) (bool, error) {
|
||||
roots, err := repo.Storer.Shallow()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(roots) > 0, nil
|
||||
}
|
||||
|
||||
type tagCandidate struct {
|
||||
name string
|
||||
patch int
|
||||
|
||||
@@ -248,6 +248,48 @@ func TestCurrentBranchDetachedCIFallback(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// ── IsShallow ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestIsShallow(t *testing.T) {
|
||||
repo, dir := newTestRepo(t)
|
||||
hash := addCommit(t, repo, dir, "chore: init", "initial")
|
||||
|
||||
shallow, err := IsShallow(repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if shallow {
|
||||
t.Error("full clone reported as shallow")
|
||||
}
|
||||
|
||||
// A shallow clone is marked by .git/shallow listing the boundary commits.
|
||||
shallowPath := filepath.Join(dir, ".git", "shallow")
|
||||
if err := os.WriteFile(shallowPath, []byte(hash.String()+"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shallow, err = IsShallow(repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !shallow {
|
||||
t.Error("clone with .git/shallow not reported as shallow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsShallowReadError(t *testing.T) {
|
||||
repo, dir := newTestRepo(t)
|
||||
addCommit(t, repo, dir, "chore: init", "initial")
|
||||
|
||||
// A directory where the shallow file is expected: Open succeeds but
|
||||
// reading fails, exercising the Storer.Shallow error path.
|
||||
if err := os.Mkdir(filepath.Join(dir, ".git", "shallow"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := IsShallow(repo); err == nil {
|
||||
t.Error("expected error when shallow file is unreadable")
|
||||
}
|
||||
}
|
||||
|
||||
// ── LatestTag ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestLatestTagNoTags(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user