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.
This commit is contained in:
Jacob Bednarz 2023-05-01 23:39:31 +10:00 committed by GitHub
parent ac7bda2c71
commit 875010629a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 268 additions and 0 deletions

36
.github/workflows/issue-sync.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Sync issues internally
on:
issues:
types: [created]
workflow_dispatch:
inputs:
issue_number:
description: The issue to target
required: true
jobs:
internal-issue-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: 'tools/go.mod'
- run: make tools
- name: Set the issue number to sync
run: |
if [ -n "${{ github.event.inputs.issue_number }}" ]; then
echo "issue_number=${{ github.event.inputs.issue_number }}" >> $GITHUB_ENV
else
echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_ENV
fi
- run: go run cmd/sync-github-issue-to-jira/main.go ${{ env.issue_number }}
working-directory: ./tools
env:
GITHUB_OWNER: cloudflare
GITHUB_REPO: cloudflare-docs
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JIRA_HOSTNAME: ${{ secrets.JIRA_HOSTNAME }}
JIRA_AUTH_TOKEN: ${{ secrets.JIRA_AUTH_TOKEN }}
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}

View file

@ -9,3 +9,7 @@ hugo:
build: download hugo
npm run build:local
./minify -r public -o .
tools:
@echo "==> Installing development tooling..."
go generate -tags tools tools/tools.go

View file

@ -0,0 +1,172 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
)
type IssueName struct {
Name string `json:"name"`
}
type IssueValue struct {
Value string `json:"value"`
}
type IssueKey struct {
Key string `json:"key"`
}
type IssueFields struct {
Project IssueKey `json:"project"`
Summary string `json:"summary"`
Description string `json:"description"`
MyTeam IssueValue `json:"customfield_14803"`
IssueType IssueName `json:"issuetype"`
Components []Component `json:"components"`
}
type Component struct {
Name string `json:"name"`
}
type InternalIssue struct {
Fields IssueFields `json:"fields"`
}
type IssueCreationResponse struct {
ID string `json:"id"`
Key string `json:"key"`
Self string `json:"self"`
}
func main() {
ctx := context.Background()
if len(os.Args) < 2 {
log.Fatalf("Usage: sync-github-issue-to-jira <GitHub issue number>\n")
}
iss := os.Args[1]
issueNumber, err := strconv.Atoi(iss)
if err != nil {
log.Fatalf("error parsing issue %q as a number: %s", iss, err)
}
githubRepositoryOwner := os.Getenv("GITHUB_OWNER")
githubRepositoryName := os.Getenv("GITHUB_REPO")
githubAccessToken := os.Getenv("GITHUB_TOKEN")
jiraHostname := os.Getenv("JIRA_HOSTNAME")
jiraAuthToken := os.Getenv("JIRA_AUTH_TOKEN")
accessClientID := os.Getenv("CF_ACCESS_CLIENT_ID")
accessClientSecret := os.Getenv("CF_ACCESS_CLIENT_SECRET")
if githubRepositoryOwner == "" {
log.Fatal("GITHUB_OWNER not set")
}
if githubRepositoryName == "" {
log.Fatal("GITHUB_REPO not set")
}
if githubAccessToken == "" {
log.Fatal("GITHUB_TOKEN not set")
}
if jiraHostname == "" {
log.Fatal("JIRA_HOSTNAME not set")
}
if jiraAuthToken == "" {
log.Fatal("JIRA_AUTH_TOKEN not set")
}
if accessClientID == "" {
log.Fatal("CF_ACCESS_CLIENT_ID not set")
}
if accessClientSecret == "" {
log.Fatal("CF_ACCESS_CLIENT_SECRET not set")
}
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: githubAccessToken},
)
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
issue, _, err := client.Issues.Get(ctx, githubRepositoryOwner, githubRepositoryName, issueNumber)
if err != nil {
log.Fatalf("error retrieving issue %s/%s#%d: %s", githubRepositoryOwner, githubRepositoryName, issueNumber, err)
}
newIssue := InternalIssue{Fields: IssueFields{
Project: IssueKey{Key: "PCX"},
Summary: *issue.Title,
Description: jirafyBodyMarkdown(issue),
MyTeam: IssueValue{Value: "Product Management"},
IssueType: IssueName{Name: "Task"},
Components: []Component{{Name: "Other (Unknown)"}},
}}
res, err := json.Marshal(newIssue)
if err != nil {
fmt.Println(err)
}
url := fmt.Sprintf("https://%s/rest/api/latest/issue/", jiraHostname)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(res))
if err != nil {
log.Fatalf("failed to build HTTP request: %s", err)
}
req.Header.Set("authorization", "Basic "+jiraAuthToken)
req.Header.Set("cf-access-client-id", accessClientID)
req.Header.Set("cf-access-client-secret", accessClientSecret)
req.Header.Set("content-type", "application/json")
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("failed to read response body: %s", err)
}
var createdIssue IssueCreationResponse
json.Unmarshal([]byte(body), &createdIssue)
if resp.StatusCode != http.StatusCreated {
fmt.Println(fmt.Sprintf("failed to create new JIRA issue"))
os.Exit(1)
}
fmt.Println(fmt.Sprintf("successfully created internal JIRA issue: %s", createdIssue.Key))
os.Exit(0)
}
// jirafyBodyMarkdown takes GitHub markdown and makes it palatable for JIRA
// with reasonable formatting.
func jirafyBodyMarkdown(issue *github.Issue) string {
output := "GitHub issue: " + *issue.HTMLURL + "\n\n---\n\n"
output += *issue.Body
output = strings.ReplaceAll(output, "###", "h3.")
return output
}

16
tools/go.mod Normal file
View file

@ -0,0 +1,16 @@
module github.com/cloudflare/cloudflare-docs/tools
go 1.20
require (
github.com/google/go-github v17.0.0+incompatible
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
)
require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
golang.org/x/net v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)

28
tools/go.sum Normal file
View file

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

12
tools/tools.go Normal file
View file

@ -0,0 +1,12 @@
//go:build tools
// +build tools
package tools
//go:generate go install github.com/google/go-github/github
//go:generate go install golang.org/x/oauth2
import (
_ "github.com/google/go-github/github"
_ "golang.org/x/oauth2"
)