Skip to content

Commit 8750106

Browse files
authored
feat(tools): support syncing issues internally (#8621)
Introduces some automation to take the GitHub issue and sync it internally allowing us to map internal/external and track in both systems.
1 parent ac7bda2 commit 8750106

6 files changed

Lines changed: 268 additions & 0 deletions

File tree

‎.github/workflows/issue-sync.yml‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Sync issues internally
2+
on:
3+
issues:
4+
types: [created]
5+
workflow_dispatch:
6+
inputs:
7+
issue_number:
8+
description: The issue to target
9+
required: true
10+
jobs:
11+
internal-issue-sync:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- name: Set up Go
16+
uses: actions/setup-go@v4
17+
with:
18+
go-version-file: 'tools/go.mod'
19+
- run: make tools
20+
- name: Set the issue number to sync
21+
run: |
22+
if [ -n "${{ github.event.inputs.issue_number }}" ]; then
23+
echo "issue_number=${{ github.event.inputs.issue_number }}" >> $GITHUB_ENV
24+
else
25+
echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_ENV
26+
fi
27+
- run: go run cmd/sync-github-issue-to-jira/main.go ${{ env.issue_number }}
28+
working-directory: ./tools
29+
env:
30+
GITHUB_OWNER: cloudflare
31+
GITHUB_REPO: cloudflare-docs
32+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
JIRA_HOSTNAME: ${{ secrets.JIRA_HOSTNAME }}
34+
JIRA_AUTH_TOKEN: ${{ secrets.JIRA_AUTH_TOKEN }}
35+
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
36+
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}

‎Makefile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ hugo:
99
build: download hugo
1010
npm run build:local
1111
./minify -r public -o .
12+
13+
tools:
14+
@echo "==> Installing development tooling..."
15+
go generate -tags tools tools/tools.go
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io/ioutil"
9+
"log"
10+
"net/http"
11+
"os"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/google/go-github/github"
16+
"golang.org/x/oauth2"
17+
)
18+
19+
type IssueName struct {
20+
Name string `json:"name"`
21+
}
22+
23+
type IssueValue struct {
24+
Value string `json:"value"`
25+
}
26+
27+
type IssueKey struct {
28+
Key string `json:"key"`
29+
}
30+
31+
type IssueFields struct {
32+
Project IssueKey `json:"project"`
33+
Summary string `json:"summary"`
34+
Description string `json:"description"`
35+
MyTeam IssueValue `json:"customfield_14803"`
36+
IssueType IssueName `json:"issuetype"`
37+
Components []Component `json:"components"`
38+
}
39+
40+
type Component struct {
41+
Name string `json:"name"`
42+
}
43+
44+
type InternalIssue struct {
45+
Fields IssueFields `json:"fields"`
46+
}
47+
48+
type IssueCreationResponse struct {
49+
ID string `json:"id"`
50+
Key string `json:"key"`
51+
Self string `json:"self"`
52+
}
53+
54+
func main() {
55+
ctx := context.Background()
56+
if len(os.Args) < 2 {
57+
log.Fatalf("Usage: sync-github-issue-to-jira <GitHub issue number>\n")
58+
}
59+
iss := os.Args[1]
60+
issueNumber, err := strconv.Atoi(iss)
61+
if err != nil {
62+
log.Fatalf("error parsing issue %q as a number: %s", iss, err)
63+
}
64+
65+
githubRepositoryOwner := os.Getenv("GITHUB_OWNER")
66+
githubRepositoryName := os.Getenv("GITHUB_REPO")
67+
githubAccessToken := os.Getenv("GITHUB_TOKEN")
68+
jiraHostname := os.Getenv("JIRA_HOSTNAME")
69+
jiraAuthToken := os.Getenv("JIRA_AUTH_TOKEN")
70+
accessClientID := os.Getenv("CF_ACCESS_CLIENT_ID")
71+
accessClientSecret := os.Getenv("CF_ACCESS_CLIENT_SECRET")
72+
73+
if githubRepositoryOwner == "" {
74+
log.Fatal("GITHUB_OWNER not set")
75+
}
76+
77+
if githubRepositoryName == "" {
78+
log.Fatal("GITHUB_REPO not set")
79+
}
80+
81+
if githubAccessToken == "" {
82+
log.Fatal("GITHUB_TOKEN not set")
83+
}
84+
85+
if jiraHostname == "" {
86+
log.Fatal("JIRA_HOSTNAME not set")
87+
}
88+
89+
if jiraAuthToken == "" {
90+
log.Fatal("JIRA_AUTH_TOKEN not set")
91+
}
92+
93+
if accessClientID == "" {
94+
log.Fatal("CF_ACCESS_CLIENT_ID not set")
95+
}
96+
97+
if accessClientSecret == "" {
98+
log.Fatal("CF_ACCESS_CLIENT_SECRET not set")
99+
}
100+
101+
ts := oauth2.StaticTokenSource(
102+
&oauth2.Token{AccessToken: githubAccessToken},
103+
)
104+
tc := oauth2.NewClient(ctx, ts)
105+
106+
client := github.NewClient(tc)
107+
108+
issue, _, err := client.Issues.Get(ctx, githubRepositoryOwner, githubRepositoryName, issueNumber)
109+
if err != nil {
110+
log.Fatalf("error retrieving issue %s/%s#%d: %s", githubRepositoryOwner, githubRepositoryName, issueNumber, err)
111+
}
112+
113+
newIssue := InternalIssue{Fields: IssueFields{
114+
Project: IssueKey{Key: "PCX"},
115+
Summary: *issue.Title,
116+
Description: jirafyBodyMarkdown(issue),
117+
MyTeam: IssueValue{Value: "Product Management"},
118+
IssueType: IssueName{Name: "Task"},
119+
Components: []Component{{Name: "Other (Unknown)"}},
120+
}}
121+
122+
res, err := json.Marshal(newIssue)
123+
if err != nil {
124+
fmt.Println(err)
125+
}
126+
127+
url := fmt.Sprintf("https://%s/rest/api/latest/issue/", jiraHostname)
128+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(res))
129+
if err != nil {
130+
log.Fatalf("failed to build HTTP request: %s", err)
131+
}
132+
133+
req.Header.Set("authorization", "Basic "+jiraAuthToken)
134+
req.Header.Set("cf-access-client-id", accessClientID)
135+
req.Header.Set("cf-access-client-secret", accessClientSecret)
136+
req.Header.Set("content-type", "application/json")
137+
138+
httpClient := &http.Client{}
139+
resp, err := httpClient.Do(req)
140+
if err != nil {
141+
panic(err)
142+
}
143+
defer resp.Body.Close()
144+
145+
body, err := ioutil.ReadAll(resp.Body)
146+
if err != nil {
147+
log.Fatalf("failed to read response body: %s", err)
148+
}
149+
150+
var createdIssue IssueCreationResponse
151+
json.Unmarshal([]byte(body), &createdIssue)
152+
153+
if resp.StatusCode != http.StatusCreated {
154+
fmt.Println(fmt.Sprintf("failed to create new JIRA issue"))
155+
os.Exit(1)
156+
}
157+
158+
fmt.Println(fmt.Sprintf("successfully created internal JIRA issue: %s", createdIssue.Key))
159+
160+
os.Exit(0)
161+
}
162+
163+
// jirafyBodyMarkdown takes GitHub markdown and makes it palatable for JIRA
164+
// with reasonable formatting.
165+
func jirafyBodyMarkdown(issue *github.Issue) string {
166+
output := "GitHub issue: " + *issue.HTMLURL + "\n\n---\n\n"
167+
168+
output += *issue.Body
169+
output = strings.ReplaceAll(output, "###", "h3.")
170+
171+
return output
172+
}

‎tools/go.mod‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/cloudflare/cloudflare-docs/tools
2+
3+
go 1.20
4+
5+
require (
6+
github.com/google/go-github v17.0.0+incompatible
7+
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
8+
)
9+
10+
require (
11+
github.com/golang/protobuf v1.5.2 // indirect
12+
github.com/google/go-querystring v1.1.0 // indirect
13+
golang.org/x/net v0.7.0 // indirect
14+
google.golang.org/appengine v1.6.7 // indirect
15+
google.golang.org/protobuf v1.28.0 // indirect
16+
)

‎tools/go.sum‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
2+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
3+
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
4+
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
5+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
6+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7+
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
8+
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
9+
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
10+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
11+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
12+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
13+
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
14+
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
15+
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
16+
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
17+
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
18+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
19+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
20+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
21+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
22+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
23+
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
24+
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
25+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
26+
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
27+
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
28+
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

‎tools/tools.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build tools
2+
// +build tools
3+
4+
package tools
5+
6+
//go:generate go install github.com/google/go-github/github
7+
//go:generate go install golang.org/x/oauth2
8+
9+
import (
10+
_ "github.com/google/go-github/github"
11+
_ "golang.org/x/oauth2"
12+
)

0 commit comments

Comments
 (0)