From 91edb96847b3ec166c4273eb5e11d54f25e28fb0 Mon Sep 17 00:00:00 2001 From: Mathias Bogaert Date: Fri, 26 Sep 2025 09:52:44 +0100 Subject: [PATCH] fix(in): handle nested annotated tags when resolving commits ResolveTagToCommitSHA now correctly follows chains of annotated tags that point to other annotated tags before reaching a commit. Previously, the function would fail when encountering nested tags like those used in opencontainers/runc v1.0.0-rc90, returning an error that the tag didn't point to a commit. Fixes #99 Signed-off-by: Mathias Bogaert --- github.go | 36 ++++++++++++-------- github_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/github.go b/github.go index 54c87c9..854fb9b 100644 --- a/github.go +++ b/github.go @@ -317,32 +317,40 @@ func (g *GitHubClient) ResolveTagToCommitSHA(tagName string) (string, error) { if err != nil { return "", err } - res.Body.Close() - // Lightweight tag if *ref.Object.Type == "commit" { return *ref.Object.SHA, nil } - // Fail if we're not pointing to a annotated tag + // Fail if we're not pointing to a tag or commit if *ref.Object.Type != "tag" { - return "", fmt.Errorf("could not resolve tag %q to commit: returned type is not 'commit' or 'tag'", tagName) + return "", fmt.Errorf("could not resolve tag %q to commit: ref type is %q, expected 'commit' or 'tag'", tagName, *ref.Object.Type) } - // Resolve tag to commit sha - tag, res, err := g.client.Git.GetTag(context.TODO(), g.owner, g.repository, *ref.Object.SHA) - if err != nil { - return "", err - } + // Follow the chain of annotated tags until we reach a commit + currentSHA := *ref.Object.SHA + maxDepth := 10 - res.Body.Close() - - if *tag.Object.Type != "commit" { - return "", fmt.Errorf("could not resolve tag %q to commit: returned type is not 'commit'", tagName) + for i := 0; i < maxDepth; i++ { + tag, res, err := g.client.Git.GetTag(context.TODO(), g.owner, g.repository, currentSHA) + if err != nil { + return "", fmt.Errorf("could not get tag object %q: %w", currentSHA, err) + } + res.Body.Close() + + switch *tag.Object.Type { + case "commit": + return *tag.Object.SHA, nil + case "tag": + // Another annotated tag, continue following the chain + currentSHA = *tag.Object.SHA + default: + return "", fmt.Errorf("could not resolve tag %q to commit: tag object points to %q, expected 'commit' or 'tag'", tagName, *tag.Object.Type) + } } - return *tag.Object.SHA, nil + return "", fmt.Errorf("could not resolve tag %q to commit: exceeded maximum tag chain depth of %d", tagName, maxDepth) } func oauthClient(ctx context.Context, source Source) (*http.Client, error) { diff --git a/github_test.go b/github_test.go index 407dad6..42f5f13 100644 --- a/github_test.go +++ b/github_test.go @@ -597,6 +597,96 @@ var _ = Describe("GitHub Client", func() { }) }) + Context("When GitHub returns a chain of annotated tags", func() { + BeforeEach(func() { + // First call: get the ref which points to the first tag + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/v1.0.0-rc90"), + ghttp.RespondWith(200, `{ "ref": "refs/tags/v1.0.0-rc90", "object" : { "type": "tag", "sha": "first-tag-sha"} }`), + ), + ) + + // Second call: get the first tag object, which points to another tag + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/tags/first-tag-sha"), + ghttp.RespondWith(200, `{ "object" : { "type": "tag", "sha": "second-tag-sha"} }`), + ), + ) + + // Third call: get the second tag object, which finally points to a commit + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/tags/second-tag-sha"), + ghttp.RespondWith(200, `{ "object" : { "type": "commit", "sha": "final-commit-sha"} }`), + ), + ) + }) + + It("follows the chain and returns the final commit SHA", func() { + commitSHA, err := client.ResolveTagToCommitSHA("v1.0.0-rc90") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(commitSHA).To(Equal("final-commit-sha")) + }) + }) + + Context("When GitHub returns a very deep chain of annotated tags", func() { + BeforeEach(func() { + // First call: get the ref + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/deeply-nested"), + ghttp.RespondWith(200, `{ "ref": "refs/tags/deeply-nested", "object" : { "type": "tag", "sha": "tag-sha-0"} }`), + ), + ) + + // Add 11 tag-to-tag redirections (exceeding our max depth of 10) + for i := 0; i < 11; i++ { + currentSHA := fmt.Sprintf("tag-sha-%d", i) + nextSHA := fmt.Sprintf("tag-sha-%d", i+1) + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", fmt.Sprintf("/repos/concourse/concourse/git/tags/%s", currentSHA)), + ghttp.RespondWith(200, fmt.Sprintf(`{ "object" : { "type": "tag", "sha": "%s"} }`, nextSHA)), + ), + ) + } + }) + + It("returns an error when max depth is exceeded", func() { + _, err := client.ResolveTagToCommitSHA("deeply-nested") + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("exceeded maximum tag chain depth")) + }) + }) + + Context("When a tag in the chain points to an unexpected object type", func() { + BeforeEach(func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/bad-tag"), + ghttp.RespondWith(200, `{ "ref": "refs/tags/bad-tag", "object" : { "type": "tag", "sha": "tag-sha"} }`), + ), + ) + + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/tags/tag-sha"), + ghttp.RespondWith(200, `{ "object" : { "type": "tree", "sha": "tree-sha"} }`), + ), + ) + }) + + It("returns an error for unexpected object types", func() { + _, err := client.ResolveTagToCommitSHA("bad-tag") + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expected 'commit' or 'tag'")) + }) + }) }) Describe("DownloadReleaseAsset", func() {