From 17c48d1b148d723ae9eb03c4c488a2e67ceaa620 Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Tue, 22 Sep 2026 14:47:29 +0000 Subject: [PATCH] README,cmd/issubot,go.{sum,mod}: update issuebot to support Jira keys - Add support for known jira keys for issue link detection - Update dependencies - Use `go fix` to modernize --- README.md | 12 +++ cmd/issuebot/autoissue.go | 8 +- cmd/issuebot/issuebot.go | 94 +++++++++++++++++++++-- cmd/issuebot/issuebot_test.go | 138 +++++++++++++++++++++++++++++++++- go.mod | 19 +++-- go.sum | 117 ++++++++++++++-------------- 6 files changed, 312 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index b44b1cb..3500ed7 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ If any commit contains "skip-issuebot" (and no issue is mentioned from other commits), a stub issue will be created for the PR that you can fill out later. This also makes the CI check pass, like with "#cleanup". +## Jira issue references + +By default only GitHub issue references satisfy the requirement. Set +`--jira-project-keys` to a comma-separated list of Jira project keys to also +accept Jira-style references for those projects: + +```shell +issuebot --jira-project-keys=EDGE,CORP +``` + +With that set, `Fixes EDGE-9` and `Fixes [EDGE-12]` count as links. + ## Installation ```go diff --git a/cmd/issuebot/autoissue.go b/cmd/issuebot/autoissue.go index 5b01044..837ca4e 100644 --- a/cmd/issuebot/autoissue.go +++ b/cmd/issuebot/autoissue.go @@ -87,9 +87,9 @@ func (p pullRequest) createStubIssue(ctx context.Context, cli *github.Client) (i prAuthor := p.pr.GetUser().GetLogin() labels := []string{issuebotStubLabel} issue, _, err := cli.Issues.Create(ctx, owner, repoName, &github.IssueRequest{ - Title: github.Ptr(fmt.Sprintf(issueTitleTemplate, prNumber)), - Assignee: github.Ptr(prAuthor), - Body: github.Ptr(fmt.Sprintf("TODO(@%s): Add details about PR #%d", + Title: new(fmt.Sprintf(issueTitleTemplate, prNumber)), + Assignee: new(prAuthor), + Body: new(fmt.Sprintf("TODO(@%s): Add details about PR #%d", prAuthor, prNumber)), Labels: &labels, }) @@ -100,7 +100,7 @@ func (p pullRequest) createStubIssue(ctx context.Context, cli *github.Client) (i // Add a comment to the PR thread indicating what we did. if _, _, err := cli.Issues.CreateComment(ctx, owner, repoName, prNumber, &github.IssueComment{ - Body: github.Ptr(fmt.Sprintf(issueCommentTemplate, issueNumber)), + Body: new(fmt.Sprintf(issueCommentTemplate, issueNumber)), }); err != nil { p.logf("error adding comment (continuing): %v", err) } diff --git a/cmd/issuebot/issuebot.go b/cmd/issuebot/issuebot.go index b5109a8..39379df 100644 --- a/cmd/issuebot/issuebot.go +++ b/cmd/issuebot/issuebot.go @@ -24,6 +24,10 @@ // If any commit contains "skip-issuebot" (and no issue is mentioned from other // commits), a stub issue will be created for the PR that you can fill out // later. This also makes the CI check pass, like with "#cleanup". +// +// By default only GitHub issue references satisfy the requirement. If +// --jira-project-keys is set, Jira-style references for those projects are +// also accepted, so that "Fixes EDGE-9" counts alongside "Fixes #9". package main import ( @@ -57,6 +61,8 @@ var ( "If set, fetch secrets from this service (https://hostname)") botAuthorEmail = flag.String("bot-author-regexp", "", "If set, a regexp matching author e-mails to be treated as automation bots (RE2)") + jiraProjectKeys = flag.String("jira-project-keys", "", + `If set, a comma-separated list of Jira project keys (e.g. "EDGE,CORP") whose issue references satisfy the link requirement`) // Access tokens // @@ -69,16 +75,44 @@ var ( client *github.Client botAuthorRE *regexp.Regexp + + // jiraKeyRE matches a Jira issue reference for one of the project keys + // named by --jira-project-keys. It is nil if the flag is unset, in which + // case only GitHub issue references are accepted. + jiraKeyRE *regexp.Regexp + + // missingCommitDescription is the commit status description posted when + // no commit in a PR links to an issue. It is replaced at startup if Jira + // matching is enabled. + missingCommitDescription = missingCommitExplanation ) const ( appPrivateKeyName = "prod/issuebot/app-private-key" githubWebhookSecretName = "prod/issuebot/github-webhook-secret" - // The description is limited to 140 characters, so be brief. + // GitHub limits a commit status description to this many characters. + maxStatusDescriptionLen = 140 + + // Both descriptions must fit in maxStatusDescriptionLen, so be brief. missingCommitExplanation = `Any non-trivial git commit must link to a GitHub issue tracking the work. Edit each commit with a tag like "Updates #nn", and update the PR.` + + // As above, but for when Jira references are also accepted. The "%s" is + // filled in with a configured project key. + missingCommitExplanationJira = `Any non-trivial commit must link to an issue. Edit each commit with a tag like "Updates #nn" or "Fixes %s-nn", then update the PR.` ) +// jiraMissingCommitDescription returns the commit status description to post +// when no commit in a PR links to an issue and Jira matching is enabled for +// key. If key is too long for the description to fit the GitHub limit, a +// generic placeholder is used instead. +func jiraMissingCommitDescription(key string) string { + if s := fmt.Sprintf(missingCommitExplanationJira, key); len(s) <= maxStatusDescriptionLen { + return s + } + return fmt.Sprintf(missingCommitExplanationJira, "KEY") +} + // Return an HTTP client suitable to use with the GitHub API, initialized with // our API keys and certificate. // @@ -130,6 +164,15 @@ func (p pullRequest) checkCommitMessage(message string) pullRequestStatus { p.logf("accept: %q", line) return prLinked } + // A Jira reference like "Fixes EDGE-9" or "Fixes [EDGE-12]" counts + // too, but only for the project keys we were configured with. We + // match the unfolded line because Jira keys are conventionally + // upper-case, and folding invites false positives on things like + // source file names ("edge-12.go"). + if jiraKeyRE != nil && jiraKeyRE.MatchString(line) { + p.logf("accept: %q", line) + return prLinked + } } } @@ -144,6 +187,38 @@ func (p pullRequest) checkCommitMessage(message string) pullRequestStatus { return prFailed } +// parseJiraProjectKeys parses the comma-separated Jira project keys in spec, +// as given to --jira-project-keys. It reports an error if any key is not a +// plausible Jira project key (letters and digits, starting with a letter). +// Requiring an explicit allowlist keeps a bare "PROJ-123" shape from matching +// incidental text like "Fixes UTF-8 handling" or "Fixes RFC-2119". +func parseJiraProjectKeys(spec string) ([]string, error) { + var keys []string + for key := range strings.SplitSeq(spec, ",") { + key = strings.TrimSpace(key) + if key == "" { + continue + } + if !jiraProjectKeyRE.MatchString(key) { + return nil, fmt.Errorf("invalid Jira project key %q", key) + } + keys = append(keys, key) + } + return keys, nil +} + +// jiraProjectKeyRE matches a well-formed Jira project key. +var jiraProjectKeyRE = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*$`) + +// compileJiraKeyRE builds a regexp matching a Jira issue reference for any of +// the given project keys as a standalone token, so "EDGE,CORP" yields a regexp +// matching "EDGE-9" and "CORP-123". Surrounding brackets need no special +// handling: "[EDGE-12]" matches on the word boundary. The keys must already +// have been validated by parseJiraProjectKeys. +func compileJiraKeyRE(keys []string) *regexp.Regexp { + return regexp.MustCompile(`\b(?:` + strings.Join(keys, "|") + `)-\d+\b`) +} + func (p pullRequest) checkCommitMetadata(repoCommit *github.RepositoryCommit) pullRequestStatus { // Requiring bots to link to a bug means they'd link all of their commits to // the same bug, which wouldn't be useful. @@ -201,14 +276,14 @@ func isAutomationBotAuthor(u *github.CommitAuthor) bool { func (p pullRequest) annotateCommitStatus(headSHA string, failed bool) { now := time.Now() status := &github.RepoStatus{ - Context: github.Ptr("issuebot"), + Context: new("issuebot"), UpdatedAt: &github.Timestamp{Time: now}, } if failed { - status.State = github.Ptr("failure") - status.Description = github.Ptr(missingCommitExplanation) + status.State = new("failure") + status.Description = new(missingCommitDescription) } else { - status.State = github.Ptr("success") + status.State = new("success") } ctx := context.Background() @@ -375,6 +450,15 @@ func main() { botAuthorRE = regexp.MustCompile(*botAuthorEmail) log.Printf("Enabled bot regexp matching: %q", botAuthorRE) } + jiraKeys, err := parseJiraProjectKeys(*jiraProjectKeys) + if err != nil { + log.Fatalf("Invalid --jira-project-keys: %v", err) + } + if len(jiraKeys) > 0 { + jiraKeyRE = compileJiraKeyRE(jiraKeys) + missingCommitDescription = jiraMissingCommitDescription(jiraKeys[0]) + log.Printf("Enabled Jira issue matching: %q", jiraKeyRE) + } // Fetch secrets from the secrets service, if configured. if *useSecretsService != "" { diff --git a/cmd/issuebot/issuebot_test.go b/cmd/issuebot/issuebot_test.go index 6f4b5b9..12a5326 100644 --- a/cmd/issuebot/issuebot_test.go +++ b/cmd/issuebot/issuebot_test.go @@ -6,6 +6,8 @@ package main import ( "regexp" + "slices" + "strings" "testing" "github.com/google/go-github/v72/github" @@ -72,7 +74,10 @@ func TestCheckCommitMessage(t *testing.T) { {"prLinked GitHub resolved number\nResolved #1", prLinked}, {"prLinked GitHub for number\nFor #1", prLinked}, - {"prLinked Linear number\nUpdates XXX-123", prFailed}, // https://github.com/tailscale/corp/issues/21347 + // With no --jira-project-keys configured, a bare project-style + // reference does not count. https://github.com/tailscale/corp/issues/21347 + {"prFailed Linear number\nUpdates XXX-123", prFailed}, + {"prFailed Jira number\nFixes EDGE-9", prFailed}, {"Revert 0123456789abcdef", prRevert}, {"prCleanup\nJust a #cleanup", prCleanup}, @@ -88,3 +93,134 @@ func TestCheckCommitMessage(t *testing.T) { } } } + +// enableJiraKeys enables Jira issue matching for the comma-separated project +// keys in spec for the duration of the test. +func enableJiraKeys(t *testing.T, spec string) { + keys, err := parseJiraProjectKeys(spec) + if err != nil { + t.Fatalf("parseJiraProjectKeys(%q): unexpected error: %v", spec, err) + } + jiraKeyRE = compileJiraKeyRE(keys) + t.Cleanup(func() { jiraKeyRE = nil }) +} + +func TestCheckCommitMessageJira(t *testing.T) { + enableJiraKeys(t, "EDGE, CORP") + + tests := []struct { + commit string + result pullRequestStatus + }{ + {"prLinked Jira fixes\nFixes EDGE-9", prLinked}, + {"prLinked Jira bracketed\nFixes [EDGE-12]", prLinked}, + {"prLinked Jira updates\nUpdates EDGE-12", prLinked}, + {"prLinked Jira second key\nResolves CORP-4321", prLinked}, + + // GitHub references keep working. + {"prLinked GitHub still works\nFixes #1", prLinked}, + + // Unconfigured project keys are still rejected. + {"prFailed unknown key\nFixes XXX-123", prFailed}, + + // Keys are matched against the unfolded line, so source file names + // and other lower-case incidentals do not count. + {"prFailed lower-case\nFixes edge-12.go formatting", prFailed}, + + // No verb, no link, even with a well-formed key. + {"prFailed no verb\nSomething about EDGE-9", prFailed}, + } + for _, tc := range tests { + p := pullRequest{} + got := p.checkCommitMessage(tc.commit) + if got != tc.result { + t.Errorf("checkCommitMessage(%q): got %v, want %v", tc.commit, got, tc.result) + } + } +} + +func TestParseJiraProjectKeys(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + for _, spec := range []string{"", " ", ",", " , ,, "} { + keys, err := parseJiraProjectKeys(spec) + if err != nil { + t.Errorf("parseJiraProjectKeys(%q): unexpected error: %v", spec, err) + } else if len(keys) != 0 { + t.Errorf("parseJiraProjectKeys(%q): got %q, want none", spec, keys) + } + } + }) + + t.Run("Invalid", func(t *testing.T) { + // Keys that are not plausible Jira project keys are rejected rather + // than silently compiled into a regexp that matches the wrong things. + for _, spec := range []string{"EDGE-1", "1EDGE", "ED GE", "ED.GE", "EDGE|CORP", ".*", "EDGE,(", "EDGE,1"} { + if keys, err := parseJiraProjectKeys(spec); err == nil { + t.Errorf("parseJiraProjectKeys(%q): got %q, want error", spec, keys) + } + } + }) + + t.Run("Valid", func(t *testing.T) { + keys, err := parseJiraProjectKeys(" EDGE, CORP ,") + if err != nil { + t.Fatalf("parseJiraProjectKeys: unexpected error: %v", err) + } + if want := []string{"EDGE", "CORP"}; !slices.Equal(keys, want) { + t.Errorf("parseJiraProjectKeys: got %q, want %q", keys, want) + } + }) +} + +func TestCompileJiraKeyRE(t *testing.T) { + re := compileJiraKeyRE([]string{"EDGE"}) + tests := []struct { + line string + match bool + }{ + {"Fixes EDGE-9", true}, + {"Fixes [EDGE-12]", true}, + {"Fixes (EDGE-12)", true}, + {"Updates EDGE-1, EDGE-2", true}, + {"Fixes EDGE-9.", true}, + + {"Fixes EDGE", false}, + {"Fixes EDGE-", false}, + {"Fixes edge-9", false}, + {"Fixes EDGETEAM-1", false}, + {"Fixes NOTEDGE-1", false}, + {"Fixes CORP-1", false}, + } + for _, tc := range tests { + if got := re.MatchString(tc.line); got != tc.match { + t.Errorf("MatchString(%q): got %v, want %v", tc.line, got, tc.match) + } + } +} + +func TestMissingCommitDescription(t *testing.T) { + if n := len(missingCommitExplanation); n > maxStatusDescriptionLen { + t.Errorf("missingCommitExplanation: length %d exceeds %d", n, maxStatusDescriptionLen) + } + + tests := []struct { + key string + want string // substring the description must mention + }{ + {"EDGE", "EDGE-nn"}, + {"INFRASTRUCT", "INFRASTRUCT-nn"}, // Jira's default maximum key length is 10; this is 11 + + // A key too long to fit falls back to a generic placeholder rather + // than dropping the Jira wording entirely. + {strings.Repeat("X", 40), "KEY-nn"}, + } + for _, tc := range tests { + got := jiraMissingCommitDescription(tc.key) + if n := len(got); n > maxStatusDescriptionLen { + t.Errorf("jiraMissingCommitDescription(%q): length %d exceeds %d", tc.key, n, maxStatusDescriptionLen) + } + if !strings.Contains(got, tc.want) { + t.Errorf("jiraMissingCommitDescription(%q): got %q, want it to mention %q", tc.key, got, tc.want) + } + } +} diff --git a/go.mod b/go.mod index 13ce89e..7ece964 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,22 @@ module github.com/tailscale/issuebot -go 1.24.2 +go 1.26.6 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 github.com/google/go-github/v72 v72.0.0 - github.com/tailscale/setec v0.0.0-20250611230422-f66888ab66d4 - tailscale.com v1.84.3 + github.com/tailscale/setec v0.0.0-20251203133219-2ab774e4129a + tailscale.com v1.102.4 ) require ( - github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d // indirect + github.com/creachadair/msync v0.8.1 // indirect + github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect + golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 8b33bf0..3295d68 100644 --- a/go.sum +++ b/go.sum @@ -1,84 +1,89 @@ -github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= -github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= -github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= -github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= -github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo= github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 h1:B91r9bHtXp/+XRgS5aZm6ZzTdz3ahgJYmkt4xZkgDz8= github.com/bradleyfalzon/ghinstallation/v2 v2.16.0/go.mod h1:OeVe5ggFzoBnmgitZe/A+BqGOnv1DvU/0uiLQi1wutM= -github.com/creachadair/mds v0.24.1 h1:bzL4ItCtAUxxO9KkotP0PVzlw4tnJicAcjPu82v2mGs= -github.com/creachadair/mds v0.24.1/go.mod h1:ArfS0vPHoLV/SzuIzoqTEZfoYmac7n9Cj8XPANHocvw= +github.com/creachadair/mds v0.25.13 h1:PsSUHV6zsfPd29k4kvm1rMoee1YFia7JyNGeMPmDcPM= +github.com/creachadair/mds v0.25.13/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs= +github.com/creachadair/msync v0.8.1 h1:QRd8si3qZ2Q4TaDL7tS/MG/lFE3YND7U7J9fy42eAFM= +github.com/creachadair/msync v0.8.1/go.mod h1:dt0bscS09J8Ie3AdccK9JpCb7LfStaDGlAmDLukOlY4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d h1:+d6m5Bjvv0/RJct1VcOw2P5bvBOGjENmxORJYnSYDow= -github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= +github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/tailscale/setec v0.0.0-20250611230422-f66888ab66d4 h1:JEZbNTVg8RTW7rpd0UAT9Vyum5MwMDUOLpCEYPbKfrs= -github.com/tailscale/setec v0.0.0-20250611230422-f66888ab66d4/go.mod h1:9Aqotyti+m+Z9dlnHOq7Mz+Ap+8SJ2LGGo4kAVyAOco= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/tailscale/setec v0.0.0-20251203133219-2ab774e4129a h1:TApskGPim53XY5WRt5hX4DnO8V6CmVoimSklryIoGMM= +github.com/tailscale/setec v0.0.0-20251203133219-2ab774e4129a/go.mod h1:+6WyG6kub5/5uPsMdYQuSti8i6F5WuKpFWLQnZt/Mms= github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok= github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -tailscale.com v1.84.3 h1:Ur9LMedSgicwbqpy5xn7t49G8490/s6rqAJOk5Q5AYE= -tailscale.com v1.84.3/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +tailscale.com v1.102.4 h1:FcAkb7MgfFFUIUASg18Mv5cAiraxb+eMgOQaOKqKyPo= +tailscale.com v1.102.4/go.mod h1:47bv91Xbg4K1p5wti7F1dmKvUVWV5BXF78d9EWJ+d6c=