Skip to content

Conversation

@Amulyam24
Copy link

This PR adds a new plugin issue-management which has commands for linking and unlinking issues to a PR.
Ref - https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue

  • The commands can be used to link an issue to a PR in the current repository or in a different repository as well as handle multiple issues
  • This is done by adding the supported keyword Fixes to the body of the PR if it doesn't already exist or by appending the issue to the existing Fixes line
  • Supported formats are issue-number and org/repo-name#issue-number

A new plugin has been added to accommodate any existing issue commands or provide flexibility to support more issue commands in the future.

Fixes #359

@netlify
Copy link

netlify bot commented Nov 25, 2025

Deploy Preview for k8s-prow ready!

Name Link
🔨 Latest commit c65a580
🔍 Latest deploy log https://app.netlify.com/projects/k8s-prow/deploys/692d60b9defd3a000813d3a3
😎 Deploy Preview https://deploy-preview-556--k8s-prow.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Amulyam24
Once this PR has been reviewed and has the lgtm label, please assign cjwagner for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. area/hook Issues or PRs related to prow's hook component labels Nov 25, 2025
@k8s-ci-robot k8s-ci-robot added area/plugins Issues or PRs related to prow's plugins for the hook component size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Nov 25, 2025
@@ -0,0 +1,95 @@
/*
Copyright 2025 The Kubernetes Authors.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, current guidance is to not include the year for these copyright headers. I don't think it matters, just pointing it out to help spread awareness.

unlinkIssueRegex = regexp.MustCompile(`(?mi)^/unlink-issue((?: +(?:\d+|[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#\d+))+)\b`)
)

type githubClient interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there is a reason for defining this interface unless you are going to mock it for unit testing. Is that planned as a follow up? Might be good to have that included in this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was planning that as a follow up. Sure, I'll add it to this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @stmcginnis, I explored a couple of ways on adding the UT.

At the end I feel having this interface and using the existing fake client is convenient than mocking the githubClient or using the plugins.PluginGitHubClient directly instead of githubClient as that would need mocking as well.

PTAL and let me know your thoughts!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a small local interface is somewhat established pattern in GH-facing plugins (retitle, blunderbuss), the GH client interface surface is massive and it is often useful to have an idea of what GH operations the plugin is limited to (besides the mentioned test mock use case). I'd prefer to keep it.

@k8s-ci-robot
Copy link
Contributor

@Amulyam24: The label(s) `/label do-not-merge/work-in-progress

cannot be applied. These labels are supported:api-review, tide/merge-method-merge, tide/merge-method-rebase, tide/merge-method-squash, team/katacoda, refactor, ci-short, ci-extended, ci-full. Is this label configured under labels -> additional_labelsorlabels -> restricted_labelsinplugin.yaml`?

In response to this:

/label do-not-merge/work-in-progress

Adding UT

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@Amulyam24
Copy link
Author

/hold

WIP: Adding UT

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Nov 27, 2025
@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Nov 28, 2025
@Amulyam24 Amulyam24 force-pushed the issues-plugin branch 3 times, most recently from 2419527 to afde1e0 Compare December 1, 2025 06:35
This PR adds a new plugin issue-management which has commands for linking and unlinking issues to a PR.

- The commands can be used to link an issue to a PR in the current repository or in a different repository as well as handle multiple issues
- This is done by adding the supported keyword Fixes to the body of the PR if it doesn't already exist or by appending the issue to the existing Fixes line
- Supported formats are issue-number and org/repo-name#issue-number

Signed-off-by: Amulyam24 <amulmek1@in.ibm.com>
@Amulyam24
Copy link
Author

/unhold

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Dec 1, 2025
unlinkIssueRegex = regexp.MustCompile(`(?mi)^/unlink-issue((?: +(?:\d+|[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+#\d+))+)\b`)
)

type githubClient interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a small local interface is somewhat established pattern in GH-facing plugins (retitle, blunderbuss), the GH client interface surface is massive and it is often useful to have an idea of what GH operations the plugin is limited to (besides the mentioned test mock use case). I'd prefer to keep it.

Comment on lines +77 to +81
log.WithFields(logrus.Fields{
"org": e.Repo.Owner.Login,
"repo": e.Repo.Name,
"number": e.Number,
"user": e.User.Login,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this logrus.Fieds instance in handleGenericComment and pass it to here with pc.Logger.WithFields, then this method already gets a populated logger and can just log.Info the necessary message

Comment on lines +63 to +77
regex := linkIssueRegex
if !linkIssue {
regex = unlinkIssueRegex
}

matches := regex.FindStringSubmatch(e.Body)
if len(matches) == 0 {
return nil
}

issues := strings.Fields(matches[1])
if len(issues) == 0 {
log.Info("No issue references provided in the comment.")
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we first do a MatchString at the callsite (in handleIssues), set a boolean based on that, then we use that boolean to select the regex again and match it again with FindStringSubmatch

feels like we can do all this at the callsite (in handleIssues) and just pass issues to this method (in addition to the link/unlink boolean)

}

// Handling issue references in format org/repo#issue-number
if strings.Contains(issue, "/") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would slightly prefer making this a standard guard clause

Suggested change
if strings.Contains(issue, "/") {
if !strings.Contains(issue, "/") {
return nil, fmt.Errorf("unrecognized issue reference: %s", issue)
}
parts := strings.Split(issue, "#")
...
return &IssueRef{Org: orgRepo[0], Repo: orgRepo[1], Num: num}, nil


newBody := updateFixesLine(pr.Body, issueRefs, linkIssue)
if newBody == pr.Body {
log.Info("PR body is already up-to-date. No changes needed.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls downgrade to debug

Comment on lines +84 to +91
case unlinkIssueRegex.MatchString(e.Body):
log.WithFields(logrus.Fields{
"org": e.Repo.Owner.Login,
"repo": e.Repo.Name,
"number": e.Number,
"user": e.User.Login,
}).Info("Handling unlink issue command")
return handleLinkIssue(gc, log, e, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm this means the user can either say /link or say /unlink but not both; if they do both, then only the /link will count. But these operations are sometimes symmetrical, let's say you make a typo and link something wrong, then you'd probably want to fix it like this:

/unlink wrong
/link right

So I think we should have something like a method that is given a comment body and it produces issues to add links to, and issues to remove links to, and then we'd pass both to handleLinkIssue instead of the add boolean`:

toLink, to unlink := parseCommentForLinkCommands(...)
if len(toLink) == 0 && len(toUnlink) == 0 {
  return nothing to do
}
return handleLinkIssue(..., toLink, toUnlink)

// If repo in issue reference is different from the PR, check if it exists
if repo != issueRef.Repo {
if _, err := gc.GetRepo(issueRef.Org, issueRef.Repo); err != nil {
return fmt.Errorf("failed to get repo: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect errors and respond with summary comment

// Verify if the issue exists
fetchedIssue, err := gc.GetIssue(issueRef.Org, issueRef.Repo, issueRef.Num)
if err != nil {
return fmt.Errorf("failed to get issue: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect errors and respond with summary comment

}
if fetchedIssue.IsPullRequest() {
response := fmt.Sprintf("Skipping #%d of repo **%s** and org **%s** as it is a *pull request*.", fetchedIssue.Number, issueRef.Repo, issueRef.Org)
if err := gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, response)); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect errors and respond with summary comment

return fmt.Sprintf("%s/%s#%d", ref.Org, ref.Repo, ref.Num)
}

func updateFixesLine(body string, issueRefs []string, add bool) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now if this is changed to the toLink / toUnlink concept I propsed above, then I think we can use sets to compute the resulting issues that the pr should have links to: existing.Difference(toUnlink).Union(toLink)

}

// If repo in issue reference is different from the PR, check if it exists
if repo != issueRef.Repo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if repo != issueRef.Repo {
if org != issueRef.Org || repo != issueRef.Repo {

I think we need to do the same if the repo name is the same but org differs (like in a fork)

@petr-muller
Copy link
Contributor

I think this is pretty close, I appreciate the test coverage. Left some comment about the code structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/hook Issues or PRs related to prow's hook component area/plugins Issues or PRs related to prow's plugins for the hook component cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Command for linking a PR to an issue

4 participants