Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize some Tracker story edits back to the linked GitHub issue #1

Merged
merged 4 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Update GitHub issue assignees upon changes to the story's owners
- Make the mapping of Tracker user IDs to GitHub usernames configurable
  • Loading branch information
cfryanr committed Jan 29, 2021
commit 8a1d36ada4ad84a92a0bb23b758d78ff57e11991
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ The following changes to the user story will be reflected back to the correspond
| Estimated 5 points (only Fibonacci scale) | Labeled `estimate/XL` |
| Estimated 8 points (only Fibonacci and Powers of 2 scales) | Labeled `estimate/XXL` |
| Un-estimated | Updated the remove the above `estimate/*` labels |
| Assigned to an owner(s) | Updated to change the Assignees |
| Unassigned | Updated to clear the Assignees |

If the user story is deleted, and the integration panel is refreshed,
then the issue will reappear in the integration panel. The Tracker story changes which
Expand All @@ -69,6 +71,8 @@ At this time, the app has the following limitations, which might be addressed by
GitHub issues. Normally, even on a public repository, GitHub would prevent users who did not create the issue
or who do not have write access to the repository from making these edits. Think twice about using this app
if that is a concern for you.
- The app does not re-read configuration dynamically. When you change configuration you can
restart the application's pod(s) using `rollout restart deployment/issues2stories`.
- Each running instance of issues2stories can only be configured to link a
single GitHub repository to a single Tracker project. If you would like to
use issues2stories for multiple Tracker projects, you would currently
Expand All @@ -95,6 +99,47 @@ It is an HTTP server which listens on a single port to provide several REST-styl
It can be run on any platform which can run the container image and provide HTTPS
ingress with working TLS certificates that are trusted by Pivotal Tracker.

### Optional: Configuring GitHub Usernames for Tracker Project Members

If you would like the Assignees of a GitHub issue to be automatically updated when the owners of the linked
Tracker story are updated, then you'll need to provide a little extra configuration so issues2stories
knows how to map your team's Tracker users to your GitHub users.

It's hard to find Tracker user IDs in the Tracker UI, so we'll use the
[Tracker "GET members" API](https://www.pivotaltracker.com/help/api/rest/v5#projects_project_id_memberships_get)
to find the user IDs of your project members.

1. Find the ID of your Tracker project. This is shown in the URL bar of your browser
while you are viewing your Tracker project. e.g. `https://www.pivotaltracker.com/n/projects/2453999`
is the project with ID `2453999`.
1. Copy your Tracker API token from your [Tracker profile page](https://www.pivotaltracker.com/profile).
You may need to click the "Create New Token" button on that page if you have no token listed.
1. ```bash
export TRACKER_TOKEN='abc123' # replace this example value with your actual API token
export PROJECT_ID='2453999' # replace this number with your actual project ID
curl -s -H "X-TrackerToken: $TRACKER_TOKEN" "https://www.pivotaltracker.com/services/v5/projects/$PROJECT_ID/memberships" | jq -r '[.[] | .person]'
```
Note that if you have lots of members in your project, you may need to add the `limit` query
parameter to get more responses in the list.
See the [Tracker API pagination documentation](https://www.pivotaltracker.com/help/api#Paginating_List_Responses).
1. You'll get a list of values, where each value looks like this:
```json
{
"kind": "person",
"id": 3344177,
"name": "Ryan Richard",
"email": "[email protected]",
"initials": "RR",
"username": "rr"
}
```
1. Note the `id` value for each member. It is not necessary to provide configuration for every member. Members who
are not configured will not be set as assignees on GitHub issues when they become owners of Tracker stories.
1. Craft a YAML map of Tracker user IDs to GitHub usernames for the people on your team.
e.g. `{3344177: cfryanr, 1234567: some-other-github-username}`
1. Provide that map as the configuration value for ytt when deploying. See [deploy/values.yaml](deploy/values.yaml)
and also see deployment example below.

### Example: Installing on [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine)

The [deploy](deploy) directory contains [ytt](https://carvel.dev/ytt) templates
Expand Down
17 changes: 17 additions & 0 deletions deploy/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ stringData:
tracker: #@ data.values.tracker_token
github: #@ data.values.github_token
---
kind: ConfigMap
apiVersion: v1
metadata:
name: issues2stories-configmap
namespace: issues2stories
data:
#@yaml/text-templated-strings
config.yaml: |
tracker_id_to_github_username_mapping: (@= data.values.tracker_id_to_github_username_mapping or "null" @)
---
apiVersion: apps/v1
kind: Deployment
metadata:
Expand All @@ -40,6 +50,9 @@ spec:
- name: issues2stories
image: #@ data.values.container_image
imagePullPolicy: Always
volumeMounts:
- name: config-volume
mountPath: /etc/config
env:
- name: GITHUB_ORG
value: #@ data.values.github_org
Expand All @@ -55,6 +68,10 @@ spec:
secretKeyRef:
name: api-tokens
key: github
volumes:
- name: config-volume
configMap:
name: issues2stories-configmap
---
apiVersion: v1
kind: Service
Expand Down
12 changes: 12 additions & 0 deletions deploy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ tracker_token:
#! make API calls to read and edit GitHub issues in your GitHub project.
#! e.g. "1c11aef11aef1f11111111111111111111111111"
github_token:

#! See issues2stories project README for how to configure this.
#! The value should be formatted a string which can be evaluated as a YAML map.
#! Or the value can be omitted which will disable the feature which updates
#! the GitHub issue's assignees field when the Tracker story's owner(s) changes.
#! e.g. using a pipe to start a multiline string:
#! tracker_id_to_github_username_mapping: |
#! {
#! 3344177: cfryanr,
#! 1234567: some-other-github-username,
#! }
tracker_id_to_github_username_mapping:
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ require (
github.com/google/go-github/v33 v33.0.0
github.com/stretchr/testify v1.6.1
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)
6 changes: 6 additions & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package config

type Config struct {
// Note that UserIDMapping can be nil.
UserIDMapping map[int64]string `yaml:"tracker_id_to_github_username_mapping"`
}
20 changes: 20 additions & 0 deletions internal/trackeractivity/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,23 @@ func removeElements(removeFrom []string, removeThese []string) []string {
}
return result
}

func equalIgnoringOrder(a1, a2 []string) bool {
// Okay to have a slow implementation because our lists are always short.
if len(a1) != len(a2) {
return false
}
for _, valueFromA1 := range a1 {
foundInA2 := false
for _, valueFromA2 := range a2 {
if valueFromA1 == valueFromA2 {
foundInA2 = true
break
}
}
if !foundInA2 {
return false
}
}
return true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"kind": "story_update_activity",
"guid": "2453999_5795",
"project_version": 5795,
"message": "Ryan Richard edited this feature",
"highlight": "edited",
"changes": [
{
"kind": "story",
"change_type": "update",
"id": 176711643,
"original_values": {
"owner_ids": [
3344177
],
"updated_at": 1611938787000
},
"new_values": {
"owner_ids": [
3344177,
3344175
],
"updated_at": 1611938991000
},
"name": "Fake issue for testing, please ignore",
"story_type": "feature"
}
],
"primary_resources": [
{
"kind": "story",
"id": 176711643,
"name": "Fake issue for testing, please ignore",
"story_type": "feature",
"url": "https://www.pivotaltracker.com/story/show/176711643"
}
],
"secondary_resources": [
],
"project": {
"kind": "project",
"id": 2453999,
"name": "Example Project"
},
"performed_by": {
"kind": "person",
"id": 3344177,
"name": "Ryan Richard",
"initials": "RR"
},
"occurred_at": 1611938991000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"kind": "story_update_activity",
"guid": "2453999_5797",
"project_version": 5797,
"message": "Ryan Richard edited this feature",
"highlight": "edited",
"changes": [
{
"kind": "story",
"change_type": "update",
"id": 176711643,
"original_values": {
"owned_by_id": 3344177,
"owner_ids": [
3344177
],
"updated_at": 1611944684000
},
"new_values": {
"owned_by_id": null,
"owner_ids": [
],
"updated_at": 1611944704000
},
"name": "Fake issue for testing, please ignore",
"story_type": "feature"
}
],
"primary_resources": [
{
"kind": "story",
"id": 176711643,
"name": "Fake issue for testing, please ignore",
"story_type": "feature",
"url": "https://www.pivotaltracker.com/story/show/176711643"
}
],
"secondary_resources": [
],
"project": {
"kind": "project",
"id": 2453999,
"name": "Example Project"
},
"performed_by": {
"kind": "person",
"id": 3344177,
"name": "Ryan Richard",
"initials": "RR"
},
"occurred_at": 1611944704000
}
59 changes: 54 additions & 5 deletions internal/trackeractivity/tracker_activity_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"

"github.com/google/go-github/v33/github"
"issues2stories/internal/config"
"issues2stories/internal/githubapi"
"issues2stories/internal/trackerapi"
)
Expand All @@ -16,15 +17,18 @@ type handler struct {
trackerAPI trackerapi.TrackerAPI
gitHubClient githubapi.GitHubAPI

configuration *config.Config

labelsToRemoveOnStateChange []string
labelsToRemoveOnTypeChange []string
labelsToRemoveOnEstimateChange []string
}

func NewHandler(trackerAPI trackerapi.TrackerAPI, gitHubClient githubapi.GitHubAPI) http.Handler {
func NewHandler(trackerAPI trackerapi.TrackerAPI, gitHubClient githubapi.GitHubAPI, configuration *config.Config) http.Handler {
return &handler{
trackerAPI: trackerAPI,
gitHubClient: gitHubClient,
trackerAPI: trackerAPI,
gitHubClient: gitHubClient,
configuration: configuration,

labelsToRemoveOnStateChange: uniqueValuesFromMapOfSlices(issueLabelsToApplyPerStoryState),
labelsToRemoveOnTypeChange: uniqueValuesFromMapOfSlices(issueLabelsToApplyPerStoryType),
Expand Down Expand Up @@ -105,6 +109,7 @@ func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re
continue
}

// Get the GitHub issue's initial list of labels.
issueLabels := issueDetails.Labels
log.Printf("issue #%d had labels before update: %v", githubIssueID, issueLabels)

Expand Down Expand Up @@ -136,8 +141,52 @@ func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re
}
}

// Push the updates back to GitHub.
err = h.gitHubClient.UpdateIssue(request.Context(), githubIssueID, &github.IssueRequest{Labels: &issueLabels})
// All label processing is finished, so set the results on the request object.
issueRequest := github.IssueRequest{Labels: &issueLabels}
log.Printf("New labels for issue #%d: %v", githubIssueID, issueLabels)
if equalIgnoringOrder(issueDetails.Labels, issueLabels) {
log.Printf("No label updates needed for issue #%d", githubIssueID)
issueRequest.Labels = nil
}

// If the story's owners have changed, then consider overwriting the assignees of the linked issue.
// Skip this when a story is initially created, because it will always set the owners to empty list
// in the change object, so there's no point in overwriting the current issue assignees just because
// the issue was dragged and dropped into the backlog/icebox.
if change.NewValues.OwnerIDs.Present && h.configuration.UserIDMapping != nil && change.ChangeType != "create" {
newStoryOwners := *change.NewValues.OwnerIDs.Value
if len(newStoryOwners) == 0 {
// All of the previous owners were explicitly removed. Clear the issue assignees list on the issue.
log.Printf("Previous story owners we explicitly removed. Clearing all assignees on issue #%d", githubIssueID)
issueRequest.Assignees = &[]string{}
} else {
// There are new owners explicitly assigned. Try to find their GitHub usernames.
newIssueAssignees := []string{}
for _, ownerID := range newStoryOwners {
gitHubUsernameOfOwner := h.configuration.UserIDMapping[ownerID]
if gitHubUsernameOfOwner != "" {
newIssueAssignees = append(newIssueAssignees, gitHubUsernameOfOwner)
}
}
// If none of the new owners had GitHub usernames configured, then skip the update.
if len(newIssueAssignees) > 0 {
log.Printf("Updating issue assignees on issue #%d to: %v", githubIssueID, newIssueAssignees)
issueRequest.Assignees = &newIssueAssignees
} else {
log.Printf(
"Skipping updating issue #%d assignees: none of these new story owners had GitHub usernames configured: %v",
githubIssueID, newStoryOwners)
}
}
}

// Push the updates back to GitHub, if there are any changes to be made.
if issueRequest.Assignees == nil && issueRequest.Labels == nil {
log.Printf("No updates planned. Skipping GitHub API call for issue #%d", githubIssueID)
continue
}
log.Printf("Calling GitHub API to update issue #%d", githubIssueID)
err = h.gitHubClient.UpdateIssue(request.Context(), githubIssueID, &issueRequest)
if err != nil {
log.Printf("Error calling GitHub API: %v", err)
http.Error(responseWriter, "can't update GitHub issue via GitHub API", http.StatusBadGateway)
Expand Down
Loading