Derive module release identity and accept full Git tags

This commit is contained in:
2026-10-10 02:10:18 +00:00
parent dbfae9ea81
commit f99fb59cd4
9 changed files with 475 additions and 79 deletions

View File

@@ -8,23 +8,26 @@ import (
// PrepareTaggedCheckout fetches one explicit tag ref, checks its peeled commit
// against expectedCommit, and checks out that commit detached.
func PrepareTaggedCheckout(ctx context.Context, repositoryDir, version, remote, expectedCommit string) error {
if !validVersionRef(version) || !validObjectID(expectedCommit) {
return fmt.Errorf("invalid tagged checkout identity %q at %q", expectedCommit, version)
func PrepareTaggedCheckout(ctx context.Context, repositoryDir, tag, remote, expectedCommit string) error {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return err
}
ref := "refs/tags/" + version
if !validObjectID(expectedCommit) {
return fmt.Errorf("invalid tagged checkout commit %q for tag %q", expectedCommit, tag)
}
ref := "refs/tags/" + tag
if _, err := run(ctx, repositoryDir, nil, "fetch", "--no-tags", remote, ref+":"+ref); err != nil {
return fmt.Errorf("fetch tag %s from remote %s: %w", version, remote, err)
return fmt.Errorf("fetch tag %s from remote %s: %w", tag, remote, err)
}
source, err := InspectTaggedSource(ctx, repositoryDir, version)
source, err := InspectTaggedSource(ctx, repositoryDir, tag)
if err != nil {
return err
}
if !strings.EqualFold(source.TagCommit, expectedCommit) {
return fmt.Errorf("tag %s resolves to commit %s, expected %s", version, source.TagCommit, expectedCommit)
return fmt.Errorf("tag %s resolves to commit %s, expected %s", tag, source.TagCommit, expectedCommit)
}
if _, err := run(ctx, repositoryDir, nil, "checkout", "--detach", source.TagCommit); err != nil {
return fmt.Errorf("check out tag %s at %s: %w", version, source.TagCommit, err)
return fmt.Errorf("check out tag %s at %s: %w", tag, source.TagCommit, err)
}
head, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", "HEAD^{commit}")
if err != nil {

View File

@@ -18,7 +18,7 @@ var (
ErrEndpointChanged = errors.New("Git publication endpoint changed")
ErrRemoteMainMissing = errors.New("authoritative remote has no main branch")
ErrRemoteMainConflict = errors.New("authoritative main does not identify the candidate commit")
ErrRemoteTagConflict = errors.New("version tag already exists on the authoritative remote")
ErrRemoteTagConflict = errors.New("tag already exists on the authoritative remote")
ErrRemoteStateUnreadable = errors.New("authoritative remote state is unreadable")
)
@@ -50,7 +50,7 @@ type RemoteRef struct {
Peeled string
}
// Snapshot is the result of querying refs/heads/main and one exact version tag.
// Snapshot is the result of querying refs/heads/main and one exact tag.
type Snapshot struct {
Main RemoteRef
Tag RemoteRef
@@ -97,14 +97,14 @@ func (endpoint Endpoint) Recheck(ctx context.Context, repositoryDir string) erro
return endpoint.verifyScopedURLs(ctx, repositoryDir)
}
// Query reads exact main and version-tag refs. A command failure is unreadable,
// Query reads exact main and tag refs. A command failure is unreadable,
// never evidence that a ref is absent.
func (endpoint Endpoint) Query(ctx context.Context, repositoryDir, version string) (Snapshot, error) {
if !validVersionRef(version) {
return Snapshot{}, fmt.Errorf("invalid version tag name %q", version)
func (endpoint Endpoint) Query(ctx context.Context, repositoryDir, tag string) (Snapshot, error) {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return Snapshot{}, err
}
mainRef := "refs/heads/main"
tagRef := "refs/tags/" + version
tagRef := "refs/tags/" + tag
peeledRef := tagRef + "^{}"
output, err := run(ctx, repositoryDir, endpoint.scopedConfig(), "ls-remote", endpoint.ScopedName, mainRef, tagRef, peeledRef)
if err != nil {
@@ -112,7 +112,7 @@ func (endpoint Endpoint) Query(ctx context.Context, repositoryDir, version strin
return snapshot, fmt.Errorf("%w: query authoritative refs at %s: %w", ErrRemoteStateUnreadable, endpoint.PushURL, err)
}
main := RemoteRef{State: RefAbsent}
tag := RemoteRef{State: RefAbsent}
remoteTag := RemoteRef{State: RefAbsent}
for _, line := range strings.Split(strings.TrimSuffix(output, "\n"), "\n") {
if line == "" {
continue
@@ -125,19 +125,19 @@ func (endpoint Endpoint) Query(ctx context.Context, repositoryDir, version strin
case mainRef:
main = mergeRef(main, object, false)
case tagRef:
tag = mergeRef(tag, object, false)
remoteTag = mergeRef(remoteTag, object, false)
case peeledRef:
tag = mergeRef(tag, object, true)
remoteTag = mergeRef(remoteTag, object, true)
}
}
return Snapshot{Main: main, Tag: tag}, nil
return Snapshot{Main: main, Tag: remoteTag}, nil
}
// RequireCandidate requires exact main equality and an absent remote version
// RequireCandidate requires exact main equality and an absent remote
// tag. It deliberately does not accept a main branch that merely contains the
// candidate as an ancestor.
func (endpoint Endpoint) RequireCandidate(ctx context.Context, repositoryDir, version, commit string) (Snapshot, error) {
snapshot, err := endpoint.Query(ctx, repositoryDir, version)
func (endpoint Endpoint) RequireCandidate(ctx context.Context, repositoryDir, tag, commit string) (Snapshot, error) {
snapshot, err := endpoint.Query(ctx, repositoryDir, tag)
if err != nil {
return snapshot, err
}
@@ -294,25 +294,14 @@ func mergeRef(ref RemoteRef, object string, peeled bool) RemoteRef {
return ref
}
func validVersionRef(version string) bool {
if len(version) < 6 || !strings.HasPrefix(version, "v") {
return false
func checkTagRef(ctx context.Context, repositoryDir, tag string) error {
if tag == "" {
return fmt.Errorf("invalid Git tag name %q", tag)
}
parts := strings.Split(version[1:], ".")
if len(parts) != 3 {
return false
if _, err := run(ctx, repositoryDir, nil, "check-ref-format", "refs/tags/"+tag); err != nil {
return fmt.Errorf("invalid Git tag name %q: %w", tag, err)
}
for _, part := range parts {
if part == "" || (len(part) > 1 && part[0] == '0') {
return false
}
for _, char := range part {
if char < '0' || char > '9' {
return false
}
}
}
return true
return nil
}
func validObjectID(object string) bool {

View File

@@ -42,6 +42,32 @@ func TestResolveUsesPushURLAndQueriesWithoutMutation(t *testing.T) {
}
}
func TestRemoteTagOperationsAcceptSlashContainingRefs(t *testing.T) {
fixture := newRemoteFixture(t)
endpoint, err := Resolve(context.Background(), fixture.work, "origin")
if err != nil {
t.Fatal(err)
}
tag := "integrations/check/v1.2.3"
if _, err := endpoint.RequireCandidate(context.Background(), fixture.work, tag, fixture.commit); err != nil {
t.Fatalf("RequireCandidate(%q): %v", tag, err)
}
object, err := CreateAnnotatedTag(context.Background(), fixture.work, tag, fixture.commit, []byte("release note\n"))
if err != nil {
t.Fatal(err)
}
if err := endpoint.PushTag(context.Background(), fixture.work, tag, object); err != nil {
t.Fatal(err)
}
remote, err := endpoint.QueryTag(context.Background(), fixture.work, tag)
if err != nil {
t.Fatal(err)
}
if remote.State != RefPresent || remote.Object != object || remote.Peeled != fixture.commit {
t.Fatalf("remote tag = %#v", remote)
}
}
func TestResolveAppliesGitURLRewrites(t *testing.T) {
for _, kind := range []string{"insteadOf", "pushInsteadOf"} {
t.Run(kind, func(t *testing.T) {

View File

@@ -32,9 +32,9 @@ type TaggedSource struct {
// InspectTaggedSource reads the local HEAD, worktree state, and selected tag.
// Both lightweight historical tags and annotated tags are supported.
func InspectTaggedSource(ctx context.Context, repositoryDir, version string) (TaggedSource, error) {
if !validVersionRef(version) {
return TaggedSource{}, fmt.Errorf("invalid tag version %q", version)
func InspectTaggedSource(ctx context.Context, repositoryDir, tag string) (TaggedSource, error) {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return TaggedSource{}, err
}
var source TaggedSource
head, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", "HEAD^{commit}")
@@ -50,10 +50,10 @@ func InspectTaggedSource(ctx context.Context, repositoryDir, version string) (Ta
return TaggedSource{}, fmt.Errorf("inspect tagged checkout state: %w", err)
}
source.Status = strings.TrimSpace(status)
ref := "refs/tags/" + version
ref := "refs/tags/" + tag
object, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", ref)
if err != nil {
return TaggedSource{}, fmt.Errorf("read local tag %s: %w", version, err)
return TaggedSource{}, fmt.Errorf("read local tag %s: %w", tag, err)
}
source.TagObject = strings.TrimSpace(object)
if !validObjectID(source.TagObject) {
@@ -61,7 +61,7 @@ func InspectTaggedSource(ctx context.Context, repositoryDir, version string) (Ta
}
peeled, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", ref+"^{commit}")
if err != nil {
return TaggedSource{}, fmt.Errorf("resolve local tag %s to a commit: %w", version, err)
return TaggedSource{}, fmt.Errorf("resolve local tag %s to a commit: %w", tag, err)
}
source.TagCommit = strings.TrimSpace(peeled)
if !validObjectID(source.TagCommit) {
@@ -69,11 +69,11 @@ func InspectTaggedSource(ctx context.Context, repositoryDir, version string) (Ta
}
tagType, err := run(ctx, repositoryDir, nil, "cat-file", "-t", ref)
if err != nil {
return TaggedSource{}, fmt.Errorf("inspect local tag %s: %w", version, err)
return TaggedSource{}, fmt.Errorf("inspect local tag %s: %w", tag, err)
}
source.TagObjectType = strings.TrimSpace(tagType)
if source.TagObjectType != "tag" && source.TagObjectType != "commit" {
return TaggedSource{}, fmt.Errorf("local tag %s has unsupported object type %q", version, source.TagObjectType)
return TaggedSource{}, fmt.Errorf("local tag %s has unsupported object type %q", tag, source.TagObjectType)
}
return source, nil
}

View File

@@ -32,6 +32,37 @@ func TestInspectTaggedSourceSupportsLightweightAndAnnotatedTags(t *testing.T) {
}
}
func TestTagOperationsAcceptFullSlashContainingRefs(t *testing.T) {
fixture := newRemoteFixture(t)
tag := "integrations/check/v2.3.4"
if err := checkTagRef(context.Background(), fixture.work, tag); err != nil {
t.Fatalf("checkTagRef(%q): %v", tag, err)
}
object, err := CreateAnnotatedTag(context.Background(), fixture.work, tag, fixture.commit, []byte("release note\n"))
if err != nil {
t.Fatal(err)
}
if err := InspectLocalTag(context.Background(), fixture.work, tag, object); err != nil {
t.Fatal(err)
}
source, err := InspectTaggedSource(context.Background(), fixture.work, tag)
if err != nil {
t.Fatal(err)
}
if source.TagCommit != fixture.commit || source.TagObject != object || source.TagObjectType != "tag" {
t.Fatalf("source for %q = %#v", tag, source)
}
}
func TestTagReferenceValidationUsesGitRules(t *testing.T) {
fixture := newRemoteFixture(t)
for _, tag := range []string{"", "v1..2.3", "v1.2.3.lock", "v1.2.3/", "v1.2.3\\bad"} {
if err := checkTagRef(context.Background(), fixture.work, tag); err == nil {
t.Errorf("checkTagRef(%q) succeeded", tag)
}
}
}
func TestCandidateObjectReadsStayLocalInPartialClones(t *testing.T) {
fixture := newRemoteFixture(t)
gitRun(t, fixture.work, "config", "uploadpack.allowFilter", "true")

View File

@@ -10,9 +10,9 @@ import (
// CreateAnnotatedTag records the captured note as an unsigned annotated tag
// on the captured commit and returns the tag object's ID.
func CreateAnnotatedTag(ctx context.Context, repositoryDir, version, commit string, note []byte) (string, error) {
if !validVersionRef(version) {
return "", fmt.Errorf("invalid version tag name %q", version)
func CreateAnnotatedTag(ctx context.Context, repositoryDir, tag, commit string, note []byte) (string, error) {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return "", err
}
if !validObjectID(commit) {
return "", fmt.Errorf("invalid candidate commit object ID %q", commit)
@@ -30,64 +30,67 @@ func CreateAnnotatedTag(ctx context.Context, repositoryDir, version, commit stri
if err := message.Close(); err != nil {
return "", fmt.Errorf("close temporary release note: %w", err)
}
if _, err := run(ctx, repositoryDir, []string{"tag.gpgSign=false"}, "tag", "--annotate", "--cleanup=verbatim", "--file", messagePath, version, commit); err != nil {
return "", fmt.Errorf("create annotated tag %s: %w", version, err)
if _, err := run(ctx, repositoryDir, []string{"tag.gpgSign=false"}, "tag", "--annotate", "--cleanup=verbatim", "--file", messagePath, tag, commit); err != nil {
return "", fmt.Errorf("create annotated tag %s: %w", tag, err)
}
ref := "refs/tags/" + version
ref := "refs/tags/" + tag
tagType, err := run(ctx, repositoryDir, nil, "cat-file", "-t", ref)
if err != nil {
return "", fmt.Errorf("inspect created tag %s: %w", version, err)
return "", fmt.Errorf("inspect created tag %s: %w", tag, err)
}
if strings.TrimSpace(tagType) != "tag" {
return "", fmt.Errorf("created tag %s has object type %q, want tag", version, strings.TrimSpace(tagType))
return "", fmt.Errorf("created tag %s has object type %q, want tag", tag, strings.TrimSpace(tagType))
}
tagObject, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", ref)
if err != nil {
return "", fmt.Errorf("read created tag object %s: %w", version, err)
return "", fmt.Errorf("read created tag object %s: %w", tag, err)
}
tagObject = strings.TrimSpace(tagObject)
if !validObjectID(tagObject) {
return "", fmt.Errorf("created tag %s has invalid object ID %q", version, tagObject)
return "", fmt.Errorf("created tag %s has invalid object ID %q", tag, tagObject)
}
peeled, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", ref+"^{}")
if err != nil {
return "", fmt.Errorf("read peeled commit for tag %s: %w", version, err)
return "", fmt.Errorf("read peeled commit for tag %s: %w", tag, err)
}
if strings.TrimSpace(peeled) != commit {
return "", fmt.Errorf("created tag %s peels to %s, want candidate %s", version, strings.TrimSpace(peeled), commit)
return "", fmt.Errorf("created tag %s peels to %s, want candidate %s", tag, strings.TrimSpace(peeled), commit)
}
tagContents, err := run(ctx, repositoryDir, nil, "cat-file", "tag", ref)
if err != nil {
return "", fmt.Errorf("read annotated tag %s: %w", version, err)
return "", fmt.Errorf("read annotated tag %s: %w", tag, err)
}
separator := bytes.Index([]byte(tagContents), []byte("\n\n"))
if separator < 0 || !bytes.Equal([]byte(tagContents[separator+2:]), note) {
return "", fmt.Errorf("annotated tag %s message does not match the captured release note", version)
return "", fmt.Errorf("annotated tag %s message does not match the captured release note", tag)
}
return tagObject, nil
}
// InspectLocalTag confirms the selected tag still points to the exact
// annotated tag object captured before publication.
func InspectLocalTag(ctx context.Context, repositoryDir, version, expectedObject string) error {
if !validVersionRef(version) || !validObjectID(expectedObject) {
return fmt.Errorf("invalid expected tag identity %q at %q", expectedObject, version)
func InspectLocalTag(ctx context.Context, repositoryDir, tag, expectedObject string) error {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return err
}
ref := "refs/tags/" + version
if !validObjectID(expectedObject) {
return fmt.Errorf("invalid expected tag object identity %q at %q", expectedObject, tag)
}
ref := "refs/tags/" + tag
tagType, err := run(ctx, repositoryDir, nil, "cat-file", "-t", ref)
if err != nil {
return fmt.Errorf("inspect local tag %s: %w", version, err)
return fmt.Errorf("inspect local tag %s: %w", tag, err)
}
if strings.TrimSpace(tagType) != "tag" {
return fmt.Errorf("local tag %s has type %q, want annotated tag", version, strings.TrimSpace(tagType))
return fmt.Errorf("local tag %s has type %q, want annotated tag", tag, strings.TrimSpace(tagType))
}
object, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", ref)
if err != nil {
return fmt.Errorf("read local tag %s: %w", version, err)
return fmt.Errorf("read local tag %s: %w", tag, err)
}
if strings.TrimSpace(object) != expectedObject {
return fmt.Errorf("local tag %s identifies %s, want %s", version, strings.TrimSpace(object), expectedObject)
return fmt.Errorf("local tag %s identifies %s, want %s", tag, strings.TrimSpace(object), expectedObject)
}
return nil
}
@@ -95,18 +98,18 @@ func InspectLocalTag(ctx context.Context, repositoryDir, version, expectedObject
// QueryTag verifies the captured endpoint still resolves to itself, then reads
// one exact authoritative version tag and its peeled commit. Failed
// verification or reads are unreadable and never interpreted as absence.
func (endpoint Endpoint) QueryTag(ctx context.Context, repositoryDir, version string) (RemoteRef, error) {
if !validVersionRef(version) {
return RemoteRef{}, fmt.Errorf("invalid version tag name %q", version)
func (endpoint Endpoint) QueryTag(ctx context.Context, repositoryDir, tag string) (RemoteRef, error) {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return RemoteRef{}, err
}
if err := endpoint.verifyScopedURLs(ctx, repositoryDir); err != nil {
return RemoteRef{State: RefUnreadable}, fmt.Errorf("%w: verify authoritative endpoint before tag query: %w", ErrRemoteStateUnreadable, err)
}
ref := "refs/tags/" + version
ref := "refs/tags/" + tag
peeledRef := ref + "^{}"
output, err := run(ctx, repositoryDir, endpoint.scopedConfig(), "ls-remote", endpoint.ScopedName, ref, peeledRef)
if err != nil {
return RemoteRef{State: RefUnreadable}, fmt.Errorf("%w: query authoritative tag %s at %s: %w", ErrRemoteStateUnreadable, version, endpoint.PushURL, err)
return RemoteRef{State: RefUnreadable}, fmt.Errorf("%w: query authoritative tag %s at %s: %w", ErrRemoteStateUnreadable, tag, endpoint.PushURL, err)
}
remoteTag := RemoteRef{State: RefAbsent}
for _, line := range outputLines(output) {
@@ -125,13 +128,16 @@ func (endpoint Endpoint) QueryTag(ctx context.Context, repositoryDir, version st
}
// PushTag publishes only the captured tag object to its exact tag ref.
func (endpoint Endpoint) PushTag(ctx context.Context, repositoryDir, version, tagObject string) error {
if !validVersionRef(version) || !validObjectID(tagObject) {
return fmt.Errorf("invalid tag publication identity %q at %q", tagObject, version)
func (endpoint Endpoint) PushTag(ctx context.Context, repositoryDir, tag, tagObject string) error {
if err := checkTagRef(ctx, repositoryDir, tag); err != nil {
return err
}
refspec := tagObject + ":refs/tags/" + version
if !validObjectID(tagObject) {
return fmt.Errorf("invalid tag publication object %q at %q", tagObject, tag)
}
refspec := tagObject + ":refs/tags/" + tag
if _, err := run(ctx, repositoryDir, endpoint.scopedConfig(), "push", "--no-follow-tags", "--porcelain", endpoint.ScopedName, refspec); err != nil {
return fmt.Errorf("push annotated tag %s to %s: %w", version, endpoint.PushURL, err)
return fmt.Errorf("push annotated tag %s to %s: %w", tag, endpoint.PushURL, err)
}
return nil
}