From 42f17ba82ec18aed1105519d80989c3d6c263b35 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 13:32:56 -0600 Subject: [PATCH 01/31] [Actions] Create Enrich Pull Request Action WIP --- actions/github/enrichPullRequest/Dockerfile | 41 +++++ actions/github/enrichPullRequest/action.yml | 125 +++++++++++++++ .../drivers/branch_name/branch_name.go | 64 ++++++++ .../enrichPullRequest/drivers/drivers.go | 4 + .../enrichPullRequest/drivers/jira/jira.go | 148 ++++++++++++++++++ actions/github/enrichPullRequest/go.mod | 35 +++++ actions/github/enrichPullRequest/go.sum | 45 ++++++ actions/github/enrichPullRequest/main.go | 92 +++++++++++ .../support/github/github.go | 148 ++++++++++++++++++ 9 files changed, 702 insertions(+) create mode 100644 actions/github/enrichPullRequest/Dockerfile create mode 100644 actions/github/enrichPullRequest/action.yml create mode 100644 actions/github/enrichPullRequest/drivers/branch_name/branch_name.go create mode 100644 actions/github/enrichPullRequest/drivers/drivers.go create mode 100644 actions/github/enrichPullRequest/drivers/jira/jira.go create mode 100644 actions/github/enrichPullRequest/go.mod create mode 100644 actions/github/enrichPullRequest/go.sum create mode 100644 actions/github/enrichPullRequest/main.go create mode 100644 actions/github/enrichPullRequest/support/github/github.go diff --git a/actions/github/enrichPullRequest/Dockerfile b/actions/github/enrichPullRequest/Dockerfile new file mode 100644 index 0000000..197806c --- /dev/null +++ b/actions/github/enrichPullRequest/Dockerfile @@ -0,0 +1,41 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY main.go ./ + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# Final stage - minimal image +FROM alpine:latest + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates + +# Create a non-root user +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Set working directory +WORKDIR /app + +# Copy the binary from builder stage +COPY --from=builder /app/main . + +# Set ownership and permissions +RUN chown appuser:appgroup /app/main && \ + chmod +x /app/main + +# Switch to non-root user +USER appuser + +# Run the binary +ENTRYPOINT ["/app/main"] \ No newline at end of file diff --git a/actions/github/enrichPullRequest/action.yml b/actions/github/enrichPullRequest/action.yml new file mode 100644 index 0000000..fffbd28 --- /dev/null +++ b/actions/github/enrichPullRequest/action.yml @@ -0,0 +1,125 @@ +name: 'Format Pull Request Title' +description: 'Formats the pull request title based on the branch name' +inputs: + repository: + description: 'The repository name' + required: true + type: string + pullRequestNumber: + description: 'The pull request number' + required: true + type: string + branch: + description: 'The branch name' + required: true + type: string + token: + description: 'GitHub token' + required: true + customFormatting: + description: "User defined custom formatting rules for specific words." + required: false + default: "" + useGo: + description: 'Use the Go version of the action. Default is false [NOTICE: This option will be removed in the next major version.]' + required: false + default: false + type: boolean + enableExperiments: + description: 'Enabled experimental features' + required: false + default: false + type: boolean + strategy: + description: 'Which formatting strategy should be used' + required: false + default: 'branch-name' + type: string +runs: + using: 'composite' + steps: + - uses: actions/checkout@v4 + with: + repository: ${{github.action_repository}} + + - name: Setup Go + if: ${{ inputs.useGo }} + uses: actions/setup-go@v5 + with: + go-version: '>=1.24.1' + go-version-file: '${{github.action_path}}/go.mod' + cache: true + cache-dependency-path: '${{github.action_path}}/go.sum' + + - name: Generate Main Hash + id: mainHash + shell: bash + run: echo "hash=$(sha256sum ${{github.action_path}}/main.go | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT + + - name: Generate Module Hash + id: moduleHash + shell: bash + run: echo "hash=$(sha256sum ${{github.action_path}}/go.sum | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT + + - name: Restore Go Modules Cache + if: ${{ inputs.useGo }} + uses: actions/cache@v4 + id: goModuleCache + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-modules-${{ steps.moduleHash.outputs.hash }} + + - name: Restore Binary Cache + if: ${{ inputs.useGo }} + uses: actions/cache@v4 + id: binary-cache + with: + path: ${{github.action_path}}/action-binary + key: ${{ runner.os }}-go-binary-${{ steps.mainHash.outputs.hash }} + + - name: Tidy Go Modules + if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit != 'true' }} + shell: bash + working-directory: ${{github.action_path}} + run: go mod tidy + + - name: Build Binary + if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit != 'true' }} + shell: bash + working-directory: ${{github.action_path}} + run: go build -o ${{github.action_path}}/action-binary main.go + + - name: Set Execute Permissions on Binary + if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit == 'true' }} + shell: bash + working-directory: ${{github.action_path}} + run: chmod +x ${{github.action_path}}/action-binary + + - name: "Format Pull Request Title" + if: ${{ inputs.useGo }} + shell: bash + working-directory: ${{github.action_path}} + run: ${{github.action_path}}/action-binary + env: + GH_TOKEN: ${{ inputs.token }} + GH_REPOSITORY: ${{ inputs.repository }} + PR_NUMBER: ${{ inputs.pullRequestNumber }} + BRANCH_NAME: ${{ inputs.branch }} + ENABLE_EXPERIMENTS: ${{ inputs.enableExperiments }} + CI_FMT_WORDS: ${{ inputs.customFormatting }} + CI_FMT_STRATEGY: $${{ inputs.strategy }} + + - name: "Set execute permissions [NOTICE: This will be removed in the next major version]" + if: ${{ !inputs.useGo }} + shell: bash + run: chmod +x ${{github.action_path}}/entrypoint.sh + + - name: "Format Pull Request Title (Legacy) [NOTICE: This will be removed in the next major version]" + if: ${{ !inputs.useGo }} + shell: bash + run: ${{github.action_path}}/entrypoint.sh + env: + GH_TOKEN: ${{ inputs.token }} + GH_REPOSITORY: ${{ inputs.repository }} + PR_NUMBER: ${{ inputs.pullRequestNumber }} + BRANCH_NAME: ${{ inputs.branch }} diff --git a/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go b/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go new file mode 100644 index 0000000..e8fcd06 --- /dev/null +++ b/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go @@ -0,0 +1,64 @@ +package branchname + +import ( + "fmt" + "os" + "regexp" + + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" + "github.com/EncoreDigitalGroup/golib/logger" +) + +var regexWithIssueType = regexp.MustCompile(`^(epic|feature|bugfix|hotfix)/([A-Z]+-[0-9]+)-(.+)$`) +var regexWithoutIssueType = regexp.MustCompile(`^([A-Z]+-[0-9]+)-(.+)$`) +var pullRequestTitle string + +func Format(gh github.GitHub) { + branchName, err := gh.GetBranchName() + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + if !gh.BranchNameMatchesPRTitle(branchName) { + formattedTitle := formatTitle(gh, branchName) + gh.UpdatePRTitle(formattedTitle) + } +} + +func GetIssueKeyFromBranchName(branchName string) (string, error) { + if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[2], nil + } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[1], nil + } else { + fmt.Println("Title does not match expected format") + logger.Info(pullRequestTitle) + return "", nil + } +} + +func GetIssueNameFromBranchName(branchName string) (string, error) { + if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[3], nil + } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[2], nil + } else { + fmt.Println("Title does not match expected format") + logger.Info(pullRequestTitle) + return "", nil + } +} + +func formatTitle(gh github.GitHub, branchName string) string { + issueKey, err := GetIssueKeyFromBranchName(branchName) + issueName, err := GetIssueNameFromBranchName(branchName) + + if err != nil { + fmt.Println("Title does not match expected format") + logger.Error(err.Error()) + return pullRequestTitle + } + + return gh.ApplyFormatting(issueKey, issueName) +} diff --git a/actions/github/enrichPullRequest/drivers/drivers.go b/actions/github/enrichPullRequest/drivers/drivers.go new file mode 100644 index 0000000..736d384 --- /dev/null +++ b/actions/github/enrichPullRequest/drivers/drivers.go @@ -0,0 +1,4 @@ +package drivers + +const BranchName = "branch-name" +const Jira = "jira" diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go new file mode 100644 index 0000000..0d429ab --- /dev/null +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -0,0 +1,148 @@ +package jira + +import ( + "context" + "fmt" + "os" + "strings" + + branchname "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/branch_name" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" + "github.com/EncoreDigitalGroup/golib/logger" + "github.com/ctreminiom/go-atlassian/jira/v3" +) + +type Configuration struct { + Enable bool + URL string + Token string + IssueKey string +} + +type Information struct { + ParentPrefix string + Title string + HasJiraInfo bool +} + +func Format(gh github.GitHub) { + branchName, err := gh.GetBranchName() + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + envUrl := os.Getenv("JIRA_URL") + if envUrl == "" { + logger.Error("JIRA_URL is not set") + os.Exit(1) + } + + envToken := os.Getenv("JIRA_TOKEN") + if envToken == "" { + logger.Error("JIRA_TOKEN is not set") + os.Exit(1) + } + + issueKey, err := branchname.GetIssueKeyFromBranchName(branchName) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + config := Configuration{ + Enable: true, + URL: envUrl, + Token: envToken, + IssueKey: issueKey, + } + + jira := getJiraInfo(config) + + newPRTitle := gh.ApplyFormatting(issueKey, jira.Title) + + if jira.ParentPrefix != "" { + newPRTitle = fmt.Sprintf("[%s]%s", jira.ParentPrefix, newPRTitle) + } + + gh.UpdatePRTitle(newPRTitle) +} + +func createJiraClient(jiraURL, jiraToken string) (*v3.Client, error) { + client, err := v3.New(nil, jiraURL) + if err != nil { + return nil, err + } + + client.Auth.SetBearerToken(jiraToken) + return client, nil +} + +func getCurrentIssueInfo(client *v3.Client, issueKey string) (string, error) { + issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) + } + + return issue.Fields.Summary, nil +} + +func getParentIssuePrefix(client *v3.Client, issueKey string) (string, error) { + issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) + } + + if issue.Fields.Parent == nil { + return "", nil + } + + parentIssue, _, err := client.Issue.Get(context.Background(), issue.Fields.Parent.Key, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to fetch parent Jira issue %s: %v", issue.Fields.Parent.Key, err) + } + + if strings.ToLower(parentIssue.Fields.IssueType.Name) == "epic" { + return "", nil + } + + return fmt.Sprintf("[%s]", issue.Fields.Parent.Key), nil +} + +func getJiraInfo(config Configuration) Information { + if config.Enable { + return Information{HasJiraInfo: false} + } + + if config.URL == "" || config.Token == "" { + logger.Error("JIRA_URL and JIRA_TOKEN must be set when ENABLE_JIRA is true") + return Information{HasJiraInfo: false} + } + + client, err := createJiraClient(config.URL, config.Token) + if err != nil { + logger.Errorf("Failed to create Jira client: %v", err) + return Information{HasJiraInfo: false} + } + + result := Information{HasJiraInfo: true} + + // Get current issue title + title, err := getCurrentIssueInfo(client, config.IssueKey) + if err != nil { + logger.Errorf("Failed to get current issue info: %v", err) + return Information{HasJiraInfo: false} + } + result.Title = title + + // Get parent issue prefix if applicable + parentPrefix, err := getParentIssuePrefix(client, config.IssueKey) + if err != nil { + logger.Errorf("Failed to get parent issue info: %v", err) + // Don't fail completely, just continue without parent prefix + } else { + result.ParentPrefix = parentPrefix + } + + return result +} diff --git a/actions/github/enrichPullRequest/go.mod b/actions/github/enrichPullRequest/go.mod new file mode 100644 index 0000000..3060fb5 --- /dev/null +++ b/actions/github/enrichPullRequest/go.mod @@ -0,0 +1,35 @@ +module github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle + +go 1.24.1 + +require ( + github.com/EncoreDigitalGroup/golib v0.1.1 + github.com/ctreminiom/go-atlassian v1.6.1 + github.com/google/go-github/v70 v70.0.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/text v0.23.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/log v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.30.0 // indirect +) diff --git a/actions/github/enrichPullRequest/go.sum b/actions/github/enrichPullRequest/go.sum new file mode 100644 index 0000000..d8c2941 --- /dev/null +++ b/actions/github/enrichPullRequest/go.sum @@ -0,0 +1,45 @@ +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/EncoreDigitalGroup/golib v0.1.1/go.mod h1:CxaCQZp09pWRXqI89reWl/e2qqJbPNteS5NvNDI/3m0= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/ctreminiom/go-atlassian v1.6.1/go.mod h1:dd5M0O8Co3bALyLQqWxPXoBfQNr6FFlpzUrA19IpLEo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go new file mode 100644 index 0000000..f67f2e9 --- /dev/null +++ b/actions/github/enrichPullRequest/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "os" + "strconv" + "strings" + + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/branch_name" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/jira" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" + "github.com/EncoreDigitalGroup/golib/logger" +) + +var gh github.GitHub + +const envGHRepository = "GH_REPOSITORY" +const envPRNumber = "PR_NUMBER" +const envBranchName = "BRANCH_NAME" +const envEnableExperiments = "ENABLE_EXPERIMENTS" +const envStrategy = "CI_FMT_STRATEGY" + +// Retrieve environment variables +var strategy = os.Getenv(envStrategy) +var repo = os.Getenv(envGHRepository) +var prNumberStr = os.Getenv(envPRNumber) +var branchName = os.Getenv(envBranchName) +var enableExperiments = getEnableExperiments() +var parts = strings.Split(repo, "/") + +var pullRequestTitle = "" + +// getEnableExperiments retrieves the ENABLE_EXPERIMENTS environment variable as a boolean +func getEnableExperiments() bool { + envValue := os.Getenv(envEnableExperiments) + return strings.ToLower(envValue) == "true" +} + +// Main function to execute the program +func main() { + checkEnvVars() + repoOwner := parts[0] + repoName := parts[1] + + // Convert PR_NUMBER to integer + prNumber, err := strconv.Atoi(prNumberStr) + if err != nil { + logger.Errorf(envPRNumber+" is not a valid integer: %v", err) + } + + gh = github.New(repoOwner, repoName, prNumber) + + if strategy == drivers.BranchName { + branchname.Format(gh) + } + + if strategy == drivers.Jira { + jira.Format(gh) + } +} + +func checkEnvVars() { + isMissingVar := false + if strategy == "" { + logger.Error(envStrategy + " environment variable is not set") + isMissingVar = true + } + + if branchName == "" { + logger.Error(envBranchName + " environment variable is not set") + isMissingVar = true + } + + if repo == "" { + logger.Error(envGHRepository + " environment variable is not set") + isMissingVar = true + } + + if prNumberStr == "" { + logger.Error(envPRNumber + " environment variable is not set") + isMissingVar = true + } + + if len(parts) != 2 { + logger.Error(envGHRepository + " must be in the format owner/repo") + isMissingVar = true + } + + if isMissingVar { + os.Exit(1) + } +} diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go new file mode 100644 index 0000000..143b69a --- /dev/null +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -0,0 +1,148 @@ +package github + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "github.com/EncoreDigitalGroup/golib/logger" + "github.com/google/go-github/v70/github" + "golang.org/x/oauth2" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +const envGHToken = "GH_TOKEN" +const envBranchName = "BRANCH_NAME" + +// GitHub interface defines the contract for GitHub operations +type GitHub interface { + GetBranchName() (string, error) + BranchNameMatchesPRTitle(currentPRTitle string) bool + UpdatePRTitle(newPRTitle string) + ApplyFormatting(issueKey string, issueName string) string +} + +// GitHubClient implements the GitHub interface +type GitHubClient struct { + client *github.Client + repositoryOwner string + repositoryName string + pullRequestNumber int +} + +var ( + client *github.Client + once sync.Once +) + +func New(repoOwner string, repoName string, prNumber int) GitHub { + once.Do(func() { + githubToken := os.Getenv(envGHToken) + + if githubToken == "" { + logger.Error(envGHToken + " is not set") + os.Exit(1) + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken}) + tc := oauth2.NewClient(ctx, ts) + client = github.NewClient(tc) + }) + + return &GitHubClient{ + client: client, + repositoryOwner: repoOwner, + repositoryName: repoName, + pullRequestNumber: prNumber, + } +} + +func (gh *GitHubClient) GetBranchName() (string, error) { + branchName := os.Getenv(envBranchName) + + if branchName == "" { + logger.Error(envBranchName + " is not set") + return "", fmt.Errorf("%s is not set", envBranchName) + } + + return branchName, nil +} + +func (gh *GitHubClient) BranchNameMatchesPRTitle(currentPRTitle string) bool { + pullRequest, _, err := gh.client.PullRequests.Get(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber) + if err != nil { + logger.Errorf("Failed to get pullRequest request: %v", err) + } + + if currentPRTitle == *pullRequest.Title { + logger.Info("Pull Request Titles Match; No Need to Update.") + return true + } + + logger.Info("Pull Request Titles Do Not Match; Update Needed.") + return false +} + +func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { + fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + + _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ + Title: &newPRTitle, + }) + + if err != nil { + logger.Errorf("Failed to update pull request prTitle: %v", err) + } + + logger.Infof("Updated Pull Request Title to: %s", newPRTitle) +} + +func (gh *GitHubClient) ApplyFormatting(issueKey string, issueName string) string { + // Replace hyphens with spaces and capitalize each word + formattedIssueName := strings.ReplaceAll(issueName, "-", " ") + titleCaser := cases.Title(language.English) + formattedIssueName = titleCaser.String(formattedIssueName) + + defaultExceptions := map[string]string{ + "Api": "API", + "Css": "CSS", + "Db": "DB", + "Html": "HTML", + "Rest": "REST", + "Rockrms": "RockRMS", + "Mpc": "MPC", + "Myportal": "MyPortal", + "Pco": "PCO", + "Php": "PHP", + "Phpstan": "PHPStan", + "Servicepoint": "ServicePoint", + "Themekit": "ThemeKit", + "Uri": "URI", + "Webcms": "WebCMS", + "Webui": "WebUI", + } + + if userDefinedExceptions := os.Getenv("CI_FMT_WORDS"); userDefinedExceptions != "" { + pairs := strings.Split(userDefinedExceptions, ",") + for _, pair := range pairs { + kv := strings.SplitN(pair, ":", 2) + if len(kv) == 2 { + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + defaultExceptions[key] = value + } + } + } + words := strings.Fields(issueName) + for i, word := range words { + if val, ok := defaultExceptions[word]; ok { + words[i] = val + } + } + + return fmt.Sprintf("[%s] %s", issueKey, formattedIssueName) +} From 868676ec9a4a639637fff70ee8346f46387e0ec2 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 13:57:22 -0600 Subject: [PATCH 02/31] [Actions] Refactor Enrich Pull Request --- actions/github/enrichPullRequest/action.yml | 111 +------- .../drivers/branch_name/branch_name.go | 81 +++--- .../enrichPullRequest/drivers/jira/jira.go | 238 +++++++++-------- actions/github/enrichPullRequest/go.mod | 2 +- actions/github/enrichPullRequest/main.go | 114 ++++---- .../support/github/github.go | 252 ++++++++++-------- 6 files changed, 370 insertions(+), 428 deletions(-) diff --git a/actions/github/enrichPullRequest/action.yml b/actions/github/enrichPullRequest/action.yml index fffbd28..3f492df 100644 --- a/actions/github/enrichPullRequest/action.yml +++ b/actions/github/enrichPullRequest/action.yml @@ -1,5 +1,5 @@ -name: 'Format Pull Request Title' -description: 'Formats the pull request title based on the branch name' +name: 'Enrich Pull Request' +description: "Enrich's the pull request with information from your project management system." inputs: repository: description: 'The repository name' @@ -20,106 +20,19 @@ inputs: description: "User defined custom formatting rules for specific words." required: false default: "" - useGo: - description: 'Use the Go version of the action. Default is false [NOTICE: This option will be removed in the next major version.]' - required: false - default: false - type: boolean - enableExperiments: - description: 'Enabled experimental features' - required: false - default: false - type: boolean strategy: description: 'Which formatting strategy should be used' required: false default: 'branch-name' type: string runs: - using: 'composite' - steps: - - uses: actions/checkout@v4 - with: - repository: ${{github.action_repository}} - - - name: Setup Go - if: ${{ inputs.useGo }} - uses: actions/setup-go@v5 - with: - go-version: '>=1.24.1' - go-version-file: '${{github.action_path}}/go.mod' - cache: true - cache-dependency-path: '${{github.action_path}}/go.sum' - - - name: Generate Main Hash - id: mainHash - shell: bash - run: echo "hash=$(sha256sum ${{github.action_path}}/main.go | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT - - - name: Generate Module Hash - id: moduleHash - shell: bash - run: echo "hash=$(sha256sum ${{github.action_path}}/go.sum | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT - - - name: Restore Go Modules Cache - if: ${{ inputs.useGo }} - uses: actions/cache@v4 - id: goModuleCache - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-modules-${{ steps.moduleHash.outputs.hash }} - - - name: Restore Binary Cache - if: ${{ inputs.useGo }} - uses: actions/cache@v4 - id: binary-cache - with: - path: ${{github.action_path}}/action-binary - key: ${{ runner.os }}-go-binary-${{ steps.mainHash.outputs.hash }} - - - name: Tidy Go Modules - if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit != 'true' }} - shell: bash - working-directory: ${{github.action_path}} - run: go mod tidy - - - name: Build Binary - if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit != 'true' }} - shell: bash - working-directory: ${{github.action_path}} - run: go build -o ${{github.action_path}}/action-binary main.go - - - name: Set Execute Permissions on Binary - if: ${{ inputs.useGo && steps.binary-cache.outputs.cache-hit == 'true' }} - shell: bash - working-directory: ${{github.action_path}} - run: chmod +x ${{github.action_path}}/action-binary - - - name: "Format Pull Request Title" - if: ${{ inputs.useGo }} - shell: bash - working-directory: ${{github.action_path}} - run: ${{github.action_path}}/action-binary - env: - GH_TOKEN: ${{ inputs.token }} - GH_REPOSITORY: ${{ inputs.repository }} - PR_NUMBER: ${{ inputs.pullRequestNumber }} - BRANCH_NAME: ${{ inputs.branch }} - ENABLE_EXPERIMENTS: ${{ inputs.enableExperiments }} - CI_FMT_WORDS: ${{ inputs.customFormatting }} - CI_FMT_STRATEGY: $${{ inputs.strategy }} - - - name: "Set execute permissions [NOTICE: This will be removed in the next major version]" - if: ${{ !inputs.useGo }} - shell: bash - run: chmod +x ${{github.action_path}}/entrypoint.sh - - - name: "Format Pull Request Title (Legacy) [NOTICE: This will be removed in the next major version]" - if: ${{ !inputs.useGo }} - shell: bash - run: ${{github.action_path}}/entrypoint.sh - env: - GH_TOKEN: ${{ inputs.token }} - GH_REPOSITORY: ${{ inputs.repository }} - PR_NUMBER: ${{ inputs.pullRequestNumber }} - BRANCH_NAME: ${{ inputs.branch }} + using: 'docker' + image: 'docker://ghcr.io/encoredigitalgroup/gh-action-create-github-release:latest' + env: + GH_TOKEN: ${{ inputs.token }} + GH_REPOSITORY: ${{ inputs.repository }} + PR_NUMBER: ${{ inputs.pullRequestNumber }} + BRANCH_NAME: ${{ inputs.branch }} + ENABLE_EXPERIMENTS: ${{ inputs.enableExperiments }} + CI_FMT_WORDS: ${{ inputs.customFormatting }} + CI_FMT_STRATEGY: ${{ inputs.strategy }} \ No newline at end of file diff --git a/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go b/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go index e8fcd06..042209b 100644 --- a/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go +++ b/actions/github/enrichPullRequest/drivers/branch_name/branch_name.go @@ -1,12 +1,13 @@ package branchname import ( - "fmt" - "os" - "regexp" + "fmt" + "os" + "regexp" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" - "github.com/EncoreDigitalGroup/golib/logger" + "github.com/EncoreDigitalGroup/golib/logger" + + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/support/github" ) var regexWithIssueType = regexp.MustCompile(`^(epic|feature|bugfix|hotfix)/([A-Z]+-[0-9]+)-(.+)$`) @@ -14,51 +15,51 @@ var regexWithoutIssueType = regexp.MustCompile(`^([A-Z]+-[0-9]+)-(.+)$`) var pullRequestTitle string func Format(gh github.GitHub) { - branchName, err := gh.GetBranchName() - if err != nil { - logger.Error(err.Error()) - os.Exit(1) - } + branchName, err := gh.GetBranchName() + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } - if !gh.BranchNameMatchesPRTitle(branchName) { - formattedTitle := formatTitle(gh, branchName) - gh.UpdatePRTitle(formattedTitle) - } + if !gh.BranchNameMatchesPRTitle(branchName) { + formattedTitle := formatTitle(gh, branchName) + gh.UpdatePRTitle(formattedTitle) + } } func GetIssueKeyFromBranchName(branchName string) (string, error) { - if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { - return matches[2], nil - } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { - return matches[1], nil - } else { - fmt.Println("Title does not match expected format") - logger.Info(pullRequestTitle) - return "", nil - } + if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[2], nil + } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[1], nil + } else { + fmt.Println("Title does not match expected format") + logger.Info(pullRequestTitle) + return "", nil + } } func GetIssueNameFromBranchName(branchName string) (string, error) { - if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { - return matches[3], nil - } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { - return matches[2], nil - } else { - fmt.Println("Title does not match expected format") - logger.Info(pullRequestTitle) - return "", nil - } + if matches := regexWithIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[3], nil + } else if matches := regexWithoutIssueType.FindStringSubmatch(branchName); matches != nil { + return matches[2], nil + } else { + fmt.Println("Title does not match expected format") + logger.Info(pullRequestTitle) + return "", nil + } } func formatTitle(gh github.GitHub, branchName string) string { - issueKey, err := GetIssueKeyFromBranchName(branchName) - issueName, err := GetIssueNameFromBranchName(branchName) + issueKey, err := GetIssueKeyFromBranchName(branchName) + issueName, err := GetIssueNameFromBranchName(branchName) - if err != nil { - fmt.Println("Title does not match expected format") - logger.Error(err.Error()) - return pullRequestTitle - } + if err != nil { + fmt.Println("Title does not match expected format") + logger.Error(err.Error()) + return pullRequestTitle + } - return gh.ApplyFormatting(issueKey, issueName) + return gh.ApplyFormatting(issueKey, issueName) } diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 0d429ab..667693f 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -1,148 +1,150 @@ package jira import ( - "context" - "fmt" - "os" - "strings" - - branchname "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/branch_name" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" - "github.com/EncoreDigitalGroup/golib/logger" - "github.com/ctreminiom/go-atlassian/jira/v3" + "context" + "fmt" + "os" + "strings" + + "github.com/EncoreDigitalGroup/golib/logger" + "github.com/ctreminiom/go-atlassian/jira/v3" + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + + branchname "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/branch_name" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/support/github" ) type Configuration struct { - Enable bool - URL string - Token string - IssueKey string + Enable bool + URL string + Token string + IssueKey string } type Information struct { - ParentPrefix string - Title string - HasJiraInfo bool + ParentPrefix string + Title string + HasJiraInfo bool } func Format(gh github.GitHub) { - branchName, err := gh.GetBranchName() - if err != nil { - logger.Error(err.Error()) - os.Exit(1) - } - - envUrl := os.Getenv("JIRA_URL") - if envUrl == "" { - logger.Error("JIRA_URL is not set") - os.Exit(1) - } - - envToken := os.Getenv("JIRA_TOKEN") - if envToken == "" { - logger.Error("JIRA_TOKEN is not set") - os.Exit(1) - } - - issueKey, err := branchname.GetIssueKeyFromBranchName(branchName) - if err != nil { - logger.Error(err.Error()) - os.Exit(1) - } - - config := Configuration{ - Enable: true, - URL: envUrl, - Token: envToken, - IssueKey: issueKey, - } - - jira := getJiraInfo(config) - - newPRTitle := gh.ApplyFormatting(issueKey, jira.Title) - - if jira.ParentPrefix != "" { - newPRTitle = fmt.Sprintf("[%s]%s", jira.ParentPrefix, newPRTitle) - } - - gh.UpdatePRTitle(newPRTitle) + branchName, err := gh.GetBranchName() + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + envUrl := os.Getenv("JIRA_URL") + if envUrl == "" { + logger.Error("JIRA_URL is not set") + os.Exit(1) + } + + envToken := os.Getenv("JIRA_TOKEN") + if envToken == "" { + logger.Error("JIRA_TOKEN is not set") + os.Exit(1) + } + + issueKey, err := branchname.GetIssueKeyFromBranchName(branchName) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + config := Configuration{ + Enable: true, + URL: envUrl, + Token: envToken, + IssueKey: issueKey, + } + + jira := getJiraInfo(config) + + newPRTitle := gh.ApplyFormatting(issueKey, jira.Title) + + if jira.ParentPrefix != "" { + newPRTitle = fmt.Sprintf("[%s]%s", jira.ParentPrefix, newPRTitle) + } + + gh.UpdatePRTitle(newPRTitle) } func createJiraClient(jiraURL, jiraToken string) (*v3.Client, error) { - client, err := v3.New(nil, jiraURL) - if err != nil { - return nil, err - } + client, err := v3.New(nil, jiraURL) + if err != nil { + return nil, err + } - client.Auth.SetBearerToken(jiraToken) - return client, nil + client.Auth.SetBearerToken(jiraToken) + return client, nil } -func getCurrentIssueInfo(client *v3.Client, issueKey string) (string, error) { - issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) - if err != nil { - return "", fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) - } +func getCurrentIssueInfo(client *v3.Client, issueKey string) (*models.IssueScheme, error) { + issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) + } - return issue.Fields.Summary, nil + return issue, nil } func getParentIssuePrefix(client *v3.Client, issueKey string) (string, error) { - issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) - if err != nil { - return "", fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) - } + issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) + } - if issue.Fields.Parent == nil { - return "", nil - } + if issue.Fields.Parent == nil { + return "", nil + } - parentIssue, _, err := client.Issue.Get(context.Background(), issue.Fields.Parent.Key, nil, nil) - if err != nil { - return "", fmt.Errorf("failed to fetch parent Jira issue %s: %v", issue.Fields.Parent.Key, err) - } + parentIssue, _, err := client.Issue.Get(context.Background(), issue.Fields.Parent.Key, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to fetch parent Jira issue %s: %v", issue.Fields.Parent.Key, err) + } - if strings.ToLower(parentIssue.Fields.IssueType.Name) == "epic" { - return "", nil - } + if strings.ToLower(parentIssue.Fields.IssueType.Name) == "epic" { + return "", nil + } - return fmt.Sprintf("[%s]", issue.Fields.Parent.Key), nil + return fmt.Sprintf("[%s]", issue.Fields.Parent.Key), nil } func getJiraInfo(config Configuration) Information { - if config.Enable { - return Information{HasJiraInfo: false} - } - - if config.URL == "" || config.Token == "" { - logger.Error("JIRA_URL and JIRA_TOKEN must be set when ENABLE_JIRA is true") - return Information{HasJiraInfo: false} - } - - client, err := createJiraClient(config.URL, config.Token) - if err != nil { - logger.Errorf("Failed to create Jira client: %v", err) - return Information{HasJiraInfo: false} - } - - result := Information{HasJiraInfo: true} - - // Get current issue title - title, err := getCurrentIssueInfo(client, config.IssueKey) - if err != nil { - logger.Errorf("Failed to get current issue info: %v", err) - return Information{HasJiraInfo: false} - } - result.Title = title - - // Get parent issue prefix if applicable - parentPrefix, err := getParentIssuePrefix(client, config.IssueKey) - if err != nil { - logger.Errorf("Failed to get parent issue info: %v", err) - // Don't fail completely, just continue without parent prefix - } else { - result.ParentPrefix = parentPrefix - } - - return result + if config.Enable { + return Information{HasJiraInfo: false} + } + + if config.URL == "" || config.Token == "" { + logger.Error("JIRA_URL and JIRA_TOKEN must be set when ENABLE_JIRA is true") + return Information{HasJiraInfo: false} + } + + client, err := createJiraClient(config.URL, config.Token) + if err != nil { + logger.Errorf("Failed to create Jira client: %v", err) + return Information{HasJiraInfo: false} + } + + result := Information{HasJiraInfo: true} + + // Get current issue jiraIssue + jiraIssue, err := getCurrentIssueInfo(client, config.IssueKey) + if err != nil { + logger.Errorf("Failed to get current issue info: %v", err) + return Information{HasJiraInfo: false} + } + result.Title = jiraIssue.Fields.Summary + + // Get parent issue prefix if applicable + parentPrefix, err := getParentIssuePrefix(client, config.IssueKey) + if err != nil { + logger.Errorf("Failed to get parent issue info: %v", err) + // Don't fail completely, just continue without parent prefix + } else { + result.ParentPrefix = parentPrefix + } + + return result } diff --git a/actions/github/enrichPullRequest/go.mod b/actions/github/enrichPullRequest/go.mod index 3060fb5..afa7bd2 100644 --- a/actions/github/enrichPullRequest/go.mod +++ b/actions/github/enrichPullRequest/go.mod @@ -1,4 +1,4 @@ -module github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle +module github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest go 1.24.1 diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index f67f2e9..631c36b 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -1,15 +1,16 @@ package main import ( - "os" - "strconv" - "strings" - - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/branch_name" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/drivers/jira" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/formatPullRequestTitle/support/github" - "github.com/EncoreDigitalGroup/golib/logger" + "os" + "strconv" + "strings" + + "github.com/EncoreDigitalGroup/golib/logger" + + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/branch_name" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/jira" + "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/support/github" ) var gh github.GitHub @@ -25,68 +26,61 @@ var strategy = os.Getenv(envStrategy) var repo = os.Getenv(envGHRepository) var prNumberStr = os.Getenv(envPRNumber) var branchName = os.Getenv(envBranchName) -var enableExperiments = getEnableExperiments() var parts = strings.Split(repo, "/") var pullRequestTitle = "" -// getEnableExperiments retrieves the ENABLE_EXPERIMENTS environment variable as a boolean -func getEnableExperiments() bool { - envValue := os.Getenv(envEnableExperiments) - return strings.ToLower(envValue) == "true" -} - // Main function to execute the program func main() { - checkEnvVars() - repoOwner := parts[0] - repoName := parts[1] + checkEnvVars() + repoOwner := parts[0] + repoName := parts[1] - // Convert PR_NUMBER to integer - prNumber, err := strconv.Atoi(prNumberStr) - if err != nil { - logger.Errorf(envPRNumber+" is not a valid integer: %v", err) - } + // Convert PR_NUMBER to integer + prNumber, err := strconv.Atoi(prNumberStr) + if err != nil { + logger.Errorf(envPRNumber+" is not a valid integer: %v", err) + } - gh = github.New(repoOwner, repoName, prNumber) + gh = github.New(repoOwner, repoName, prNumber) - if strategy == drivers.BranchName { - branchname.Format(gh) - } + if strategy == drivers.BranchName { + branchname.Format(gh) + } - if strategy == drivers.Jira { - jira.Format(gh) - } + if strategy == drivers.Jira { + jira.Format(gh) + } } func checkEnvVars() { - isMissingVar := false - if strategy == "" { - logger.Error(envStrategy + " environment variable is not set") - isMissingVar = true - } - - if branchName == "" { - logger.Error(envBranchName + " environment variable is not set") - isMissingVar = true - } - - if repo == "" { - logger.Error(envGHRepository + " environment variable is not set") - isMissingVar = true - } - - if prNumberStr == "" { - logger.Error(envPRNumber + " environment variable is not set") - isMissingVar = true - } - - if len(parts) != 2 { - logger.Error(envGHRepository + " must be in the format owner/repo") - isMissingVar = true - } - - if isMissingVar { - os.Exit(1) - } + isMissingVar := false + if strategy == "" { + logger.Error(envStrategy + " environment variable is not set") + isMissingVar = true + } + + if branchName == "" { + logger.Error(envBranchName + " environment variable is not set") + isMissingVar = true + } + + if repo == "" { + logger.Error(envGHRepository + " environment variable is not set") + isMissingVar = true + } + + if prNumberStr == "" { + logger.Error(envPRNumber + " environment variable is not set") + isMissingVar = true + } + + if len(parts) != 2 { + logger.Error(envGHRepository + " must be in the format owner/repo") + isMissingVar = true + } + + if isMissingVar { + os.Exit(1) + } } diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 143b69a..f323c98 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -1,17 +1,17 @@ package github import ( - "context" - "fmt" - "os" - "strings" - "sync" - - "github.com/EncoreDigitalGroup/golib/logger" - "github.com/google/go-github/v70/github" - "golang.org/x/oauth2" - "golang.org/x/text/cases" - "golang.org/x/text/language" + "context" + "fmt" + "os" + "strings" + "sync" + + "github.com/EncoreDigitalGroup/golib/logger" + "github.com/google/go-github/v70/github" + "golang.org/x/oauth2" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) const envGHToken = "GH_TOKEN" @@ -19,130 +19,162 @@ const envBranchName = "BRANCH_NAME" // GitHub interface defines the contract for GitHub operations type GitHub interface { - GetBranchName() (string, error) - BranchNameMatchesPRTitle(currentPRTitle string) bool - UpdatePRTitle(newPRTitle string) - ApplyFormatting(issueKey string, issueName string) string + GetBranchName() (string, error) + BranchNameMatchesPRTitle(currentPRTitle string) bool + GetPRInformation() *github.PullRequest + UpdatePRTitle(newPRTitle string) + UpdatePRDescription(newPRDescription string) + ApplyFormatting(issueKey string, issueName string) string } // GitHubClient implements the GitHub interface type GitHubClient struct { - client *github.Client - repositoryOwner string - repositoryName string - pullRequestNumber int + client *github.Client + repositoryOwner string + repositoryName string + pullRequestNumber int + pullRequestInfo *github.PullRequest } var ( - client *github.Client - once sync.Once + client *github.Client + once sync.Once ) func New(repoOwner string, repoName string, prNumber int) GitHub { - once.Do(func() { - githubToken := os.Getenv(envGHToken) - - if githubToken == "" { - logger.Error(envGHToken + " is not set") - os.Exit(1) - } - - ctx := context.Background() - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken}) - tc := oauth2.NewClient(ctx, ts) - client = github.NewClient(tc) - }) - - return &GitHubClient{ - client: client, - repositoryOwner: repoOwner, - repositoryName: repoName, - pullRequestNumber: prNumber, - } + once.Do(func() { + githubToken := os.Getenv(envGHToken) + + if githubToken == "" { + logger.Error(envGHToken + " is not set") + os.Exit(1) + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken}) + tc := oauth2.NewClient(ctx, ts) + client = github.NewClient(tc) + }) + + return &GitHubClient{ + client: client, + repositoryOwner: repoOwner, + repositoryName: repoName, + pullRequestNumber: prNumber, + pullRequestInfo: nil, + } } func (gh *GitHubClient) GetBranchName() (string, error) { - branchName := os.Getenv(envBranchName) + branchName := os.Getenv(envBranchName) - if branchName == "" { - logger.Error(envBranchName + " is not set") - return "", fmt.Errorf("%s is not set", envBranchName) - } + if branchName == "" { + logger.Error(envBranchName + " is not set") + return "", fmt.Errorf("%s is not set", envBranchName) + } - return branchName, nil + return branchName, nil } func (gh *GitHubClient) BranchNameMatchesPRTitle(currentPRTitle string) bool { - pullRequest, _, err := gh.client.PullRequests.Get(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber) - if err != nil { - logger.Errorf("Failed to get pullRequest request: %v", err) - } - - if currentPRTitle == *pullRequest.Title { - logger.Info("Pull Request Titles Match; No Need to Update.") - return true - } - - logger.Info("Pull Request Titles Do Not Match; Update Needed.") - return false + pullRequest, _, err := gh.client.PullRequests.Get(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber) + if err != nil { + logger.Errorf("Failed to get pullRequest request: %v", err) + } + + if currentPRTitle == *pullRequest.Title { + logger.Info("Pull Request Titles Match; No Need to Update.") + return true + } + + logger.Info("Pull Request Titles Do Not Match; Update Needed.") + return false +} + +func (gh *GitHubClient) GetPRInformation() *github.PullRequest { + if gh.pullRequestInfo != nil { + return gh.pullRequestInfo + } + + pullRequestInformation, _, err := gh.client.PullRequests.Get(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber) + if err != nil { + logger.Errorf("Failed to get pullRequest information: %v", err) + os.Exit(1) + } + + gh.pullRequestInfo = pullRequestInformation + + return gh.pullRequestInfo } func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { - fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + + _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ + Title: &newPRTitle, + }) - _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ - Title: &newPRTitle, - }) + if err != nil { + logger.Errorf("Failed to update pull request prTitle: %v", err) + } + + logger.Infof("Updated Pull Request Title to: %s", newPRTitle) +} - if err != nil { - logger.Errorf("Failed to update pull request prTitle: %v", err) - } +func (gh *GitHubClient) UpdatePRDescription(newPRDescription string) { + pullRequestInformation := gh.GetPRInformation() - logger.Infof("Updated Pull Request Title to: %s", newPRTitle) + pullRequestInformation.Body = &newPRDescription + _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ + Body: &newPRDescription, + }) + if err != nil { + logger.Errorf("Failed to update pull request prDescription: %v", err) + } } func (gh *GitHubClient) ApplyFormatting(issueKey string, issueName string) string { - // Replace hyphens with spaces and capitalize each word - formattedIssueName := strings.ReplaceAll(issueName, "-", " ") - titleCaser := cases.Title(language.English) - formattedIssueName = titleCaser.String(formattedIssueName) - - defaultExceptions := map[string]string{ - "Api": "API", - "Css": "CSS", - "Db": "DB", - "Html": "HTML", - "Rest": "REST", - "Rockrms": "RockRMS", - "Mpc": "MPC", - "Myportal": "MyPortal", - "Pco": "PCO", - "Php": "PHP", - "Phpstan": "PHPStan", - "Servicepoint": "ServicePoint", - "Themekit": "ThemeKit", - "Uri": "URI", - "Webcms": "WebCMS", - "Webui": "WebUI", - } - - if userDefinedExceptions := os.Getenv("CI_FMT_WORDS"); userDefinedExceptions != "" { - pairs := strings.Split(userDefinedExceptions, ",") - for _, pair := range pairs { - kv := strings.SplitN(pair, ":", 2) - if len(kv) == 2 { - key := strings.TrimSpace(kv[0]) - value := strings.TrimSpace(kv[1]) - defaultExceptions[key] = value - } - } - } - words := strings.Fields(issueName) - for i, word := range words { - if val, ok := defaultExceptions[word]; ok { - words[i] = val - } - } - - return fmt.Sprintf("[%s] %s", issueKey, formattedIssueName) + // Replace hyphens with spaces and capitalize each word + formattedIssueName := strings.ReplaceAll(issueName, "-", " ") + titleCaser := cases.Title(language.English) + formattedIssueName = titleCaser.String(formattedIssueName) + + defaultExceptions := map[string]string{ + "Api": "API", + "Css": "CSS", + "Db": "DB", + "Html": "HTML", + "Rest": "REST", + "Rockrms": "RockRMS", + "Mpc": "MPC", + "Myportal": "MyPortal", + "Pco": "PCO", + "Php": "PHP", + "Phpstan": "PHPStan", + "Servicepoint": "ServicePoint", + "Themekit": "ThemeKit", + "Uri": "URI", + "Webcms": "WebCMS", + "Webui": "WebUI", + } + + if userDefinedExceptions := os.Getenv("CI_FMT_WORDS"); userDefinedExceptions != "" { + pairs := strings.Split(userDefinedExceptions, ",") + for _, pair := range pairs { + kv := strings.SplitN(pair, ":", 2) + if len(kv) == 2 { + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + defaultExceptions[key] = value + } + } + } + words := strings.Fields(issueName) + for i, word := range words { + if val, ok := defaultExceptions[word]; ok { + words[i] = val + } + } + + return fmt.Sprintf("[%s] %s", issueKey, formattedIssueName) } From 13154b32c49cbed7119e1260cfcc26e9cb455ba6 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 14:02:04 -0600 Subject: [PATCH 03/31] [Actions] Suppress multiple runs if label is present --- .../github/enrichPullRequest/drivers/jira/jira.go | 5 +++++ .../enrichPullRequest/support/github/github.go | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 667693f..59d1f39 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -28,6 +28,11 @@ type Information struct { } func Format(gh github.GitHub) { + if gh.HasLabel("jira-sync-complete") { + logger.Info("PR already has 'jira-sync-complete' label, skipping Jira sync") + return + } + branchName, err := gh.GetBranchName() if err != nil { logger.Error(err.Error()) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index f323c98..6846a12 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -25,6 +25,7 @@ type GitHub interface { UpdatePRTitle(newPRTitle string) UpdatePRDescription(newPRDescription string) ApplyFormatting(issueKey string, issueName string) string + HasLabel(labelName string) bool } // GitHubClient implements the GitHub interface @@ -178,3 +179,15 @@ func (gh *GitHubClient) ApplyFormatting(issueKey string, issueName string) strin return fmt.Sprintf("[%s] %s", issueKey, formattedIssueName) } + +func (gh *GitHubClient) HasLabel(labelName string) bool { + pullRequestInformation := gh.GetPRInformation() + + for _, label := range pullRequestInformation.Labels { + if label.Name != nil && *label.Name == labelName { + return true + } + } + + return false +} From 9e4f8e58fa1d225ab6339fbe41ed107999ba6d20 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 14:07:21 -0600 Subject: [PATCH 04/31] [Actions] label circuit breaker wip --- .../enrichPullRequest/drivers/jira/jira.go | 4 +++ .../support/github/github.go | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 59d1f39..629c1ab 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -73,6 +73,10 @@ func Format(gh github.GitHub) { } gh.UpdatePRTitle(newPRTitle) + + // Ensure the jira-sync-complete label exists and add it to the PR + gh.EnsureLabelExists("jira-sync-complete", "Indicates that Jira synchronization has been completed for this PR", "0e8a16") + gh.AddLabelToPR("jira-sync-complete") } func createJiraClient(jiraURL, jiraToken string) (*v3.Client, error) { diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 6846a12..fa4db77 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -26,6 +26,8 @@ type GitHub interface { UpdatePRDescription(newPRDescription string) ApplyFormatting(issueKey string, issueName string) string HasLabel(labelName string) bool + AddLabelToPR(labelName string) + EnsureLabelExists(labelName string, description string, color string) } // GitHubClient implements the GitHub interface @@ -191,3 +193,37 @@ func (gh *GitHubClient) HasLabel(labelName string) bool { return false } + +func (gh *GitHubClient) EnsureLabelExists(labelName string, description string, color string) { + // Check if label already exists in the repository + _, _, err := gh.client.Issues.GetLabel(context.Background(), gh.repositoryOwner, gh.repositoryName, labelName) + if err == nil { + // Label already exists + return + } + + // Create the label if it doesn't exist + label := &github.Label{ + Name: &labelName, + Description: &description, + Color: &color, + } + + _, _, err = gh.client.Issues.CreateLabel(context.Background(), gh.repositoryOwner, gh.repositoryName, label) + if err != nil { + logger.Errorf("Failed to create label '%s': %v", labelName, err) + } else { + logger.Infof("Created label '%s' in repository", labelName) + } +} + +func (gh *GitHubClient) AddLabelToPR(labelName string) { + labels := []string{labelName} + + _, _, err := gh.client.Issues.AddLabelsToIssue(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, labels) + if err != nil { + logger.Errorf("Failed to add label '%s' to PR: %v", labelName, err) + } else { + logger.Infof("Added label '%s' to PR #%d", labelName, gh.pullRequestNumber) + } +} From 9ab4c4f31e85c19f34b9285df2cdaf8b5f8e88ec Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 14:29:39 -0600 Subject: [PATCH 05/31] [Actions] set pr title and description based on jira info --- .../enrichPullRequest/drivers/jira/jira.go | 49 +++++++++++-------- actions/github/enrichPullRequest/main.go | 3 +- .../support/github/github.go | 4 +- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 629c1ab..428fba9 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -24,12 +24,13 @@ type Configuration struct { type Information struct { ParentPrefix string Title string + Description string HasJiraInfo bool } func Format(gh github.GitHub) { - if gh.HasLabel("jira-sync-complete") { - logger.Info("PR already has 'jira-sync-complete' label, skipping Jira sync") + if jiraLabelSyncEnabled() && gh.HasLabel(jiraLabelSyncName()) { + logger.Info("PR already has '" + jiraLabelSyncName() + "' label, skipping Jira sync") return } @@ -39,15 +40,15 @@ func Format(gh github.GitHub) { os.Exit(1) } - envUrl := os.Getenv("JIRA_URL") + envUrl := os.Getenv("OPT_JIRA_URL") if envUrl == "" { - logger.Error("JIRA_URL is not set") + logger.Error("OPT_JIRA_URL is not set") os.Exit(1) } - envToken := os.Getenv("JIRA_TOKEN") + envToken := os.Getenv("OPT_JIRA_TOKEN") if envToken == "" { - logger.Error("JIRA_TOKEN is not set") + logger.Error("OPT_JIRA_TOKEN is not set") os.Exit(1) } @@ -73,10 +74,12 @@ func Format(gh github.GitHub) { } gh.UpdatePRTitle(newPRTitle) + gh.UpdatePRDescription(jira.Description) - // Ensure the jira-sync-complete label exists and add it to the PR - gh.EnsureLabelExists("jira-sync-complete", "Indicates that Jira synchronization has been completed for this PR", "0e8a16") - gh.AddLabelToPR("jira-sync-complete") + if jiraLabelSyncEnabled() { + gh.EnsureLabelExists(jiraLabelSyncName(), "Indicates that Jira synchronization has been completed for this PR", "0e8a16") + gh.AddLabelToPR(jiraLabelSyncName()) + } } func createJiraClient(jiraURL, jiraToken string) (*v3.Client, error) { @@ -121,12 +124,12 @@ func getParentIssuePrefix(client *v3.Client, issueKey string) (string, error) { } func getJiraInfo(config Configuration) Information { - if config.Enable { + if !config.Enable { return Information{HasJiraInfo: false} } if config.URL == "" || config.Token == "" { - logger.Error("JIRA_URL and JIRA_TOKEN must be set when ENABLE_JIRA is true") + logger.Error("OPT_JIRA_URL and OPT_JIRA_TOKEN must be set when OPT_ENABLE_JIRA is true") return Information{HasJiraInfo: false} } @@ -138,22 +141,28 @@ func getJiraInfo(config Configuration) Information { result := Information{HasJiraInfo: true} - // Get current issue jiraIssue jiraIssue, err := getCurrentIssueInfo(client, config.IssueKey) if err != nil { logger.Errorf("Failed to get current issue info: %v", err) return Information{HasJiraInfo: false} } result.Title = jiraIssue.Fields.Summary + result.ParentPrefix = jiraIssue.Fields.Parent.Key + result.Description = jiraIssue.Fields.Description.Text - // Get parent issue prefix if applicable - parentPrefix, err := getParentIssuePrefix(client, config.IssueKey) - if err != nil { - logger.Errorf("Failed to get parent issue info: %v", err) - // Don't fail completely, just continue without parent prefix - } else { - result.ParentPrefix = parentPrefix + return result +} + +func jiraLabelSyncEnabled() bool { + return strings.ToLower(os.Getenv("OPT_ENABLE_JIRA_SYNC_LABEL")) == "true" +} + +func jiraLabelSyncName() string { + label := os.Getenv("OPT_JIRA_SYNC_LABEL") + + if label == "" { + return "jira-sync-complete" } - return result + return label } diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index 631c36b..3b65e25 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -18,8 +18,7 @@ var gh github.GitHub const envGHRepository = "GH_REPOSITORY" const envPRNumber = "PR_NUMBER" const envBranchName = "BRANCH_NAME" -const envEnableExperiments = "ENABLE_EXPERIMENTS" -const envStrategy = "CI_FMT_STRATEGY" +const envStrategy = "OPT_FMT_STRATEGY" // Retrieve environment variables var strategy = os.Getenv(envStrategy) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index fa4db77..030e46a 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -161,7 +161,7 @@ func (gh *GitHubClient) ApplyFormatting(issueKey string, issueName string) strin "Webui": "WebUI", } - if userDefinedExceptions := os.Getenv("CI_FMT_WORDS"); userDefinedExceptions != "" { + if userDefinedExceptions := os.Getenv("OPT_FMT_WORDS"); userDefinedExceptions != "" { pairs := strings.Split(userDefinedExceptions, ",") for _, pair := range pairs { kv := strings.SplitN(pair, ":", 2) @@ -195,14 +195,12 @@ func (gh *GitHubClient) HasLabel(labelName string) bool { } func (gh *GitHubClient) EnsureLabelExists(labelName string, description string, color string) { - // Check if label already exists in the repository _, _, err := gh.client.Issues.GetLabel(context.Background(), gh.repositoryOwner, gh.repositoryName, labelName) if err == nil { // Label already exists return } - // Create the label if it doesn't exist label := &github.Label{ Name: &labelName, Description: &description, From 0666db51062821c16db4c124d473709df23a99ef Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 14:37:29 -0600 Subject: [PATCH 06/31] [Actions] add jira description sync support --- .../support/github/github.go | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 030e46a..989d366 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -127,9 +127,33 @@ func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { func (gh *GitHubClient) UpdatePRDescription(newPRDescription string) { pullRequestInformation := gh.GetPRInformation() - pullRequestInformation.Body = &newPRDescription + const jiraStartMarker = "" + const jiraEndMarker = "" + var finalDescription string + + if pullRequestInformation.Body != nil && *pullRequestInformation.Body != "" { + existingBody := *pullRequestInformation.Body + + // Check if Jira markers already exist + startIndex := strings.Index(existingBody, jiraStartMarker) + endIndex := strings.Index(existingBody, jiraEndMarker) + + if startIndex != -1 && endIndex != -1 && endIndex > startIndex { + // Both markers exist - replace content between them + beforeJira := existingBody[:startIndex] + afterJira := existingBody[endIndex+len(jiraEndMarker):] + finalDescription = strings.TrimSpace(beforeJira) + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + strings.TrimSpace(afterJira) + } else { + // Markers don't exist or are malformed - append with markers + finalDescription = existingBody + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + } + } else { + // No existing description - add markers and Jira description + finalDescription = jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + } + _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ - Body: &newPRDescription, + Body: &finalDescription, }) if err != nil { logger.Errorf("Failed to update pull request prDescription: %v", err) From 1946e3c9b8c765f6bf7a606d2ad237c464e84490 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 15:02:21 -0600 Subject: [PATCH 07/31] [Actions] parent prefix --- .../enrichPullRequest/drivers/jira/jira.go | 22 +++++++++-- .../support/github/github.go | 39 ++++++++++++------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 428fba9..626a036 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -73,8 +73,11 @@ func Format(gh github.GitHub) { newPRTitle = fmt.Sprintf("[%s]%s", jira.ParentPrefix, newPRTitle) } - gh.UpdatePRTitle(newPRTitle) - gh.UpdatePRDescription(jira.Description) + if jiraDescriptionSyncEnabled() { + gh.UpdatePR(newPRTitle, jira.Description) + } else { + gh.UpdatePRTitle(newPRTitle) + } if jiraLabelSyncEnabled() { gh.EnsureLabelExists(jiraLabelSyncName(), "Indicates that Jira synchronization has been completed for this PR", "0e8a16") @@ -147,7 +150,16 @@ func getJiraInfo(config Configuration) Information { return Information{HasJiraInfo: false} } result.Title = jiraIssue.Fields.Summary - result.ParentPrefix = jiraIssue.Fields.Parent.Key + + // Get parent issue prefix if applicable + parentPrefix, err := getParentIssuePrefix(client, config.IssueKey) + if err != nil { + logger.Errorf("Failed to get parent issue info: %v", err) + // Don't fail completely, just continue without parent prefix + } else { + result.ParentPrefix = parentPrefix + } + result.Description = jiraIssue.Fields.Description.Text return result @@ -157,6 +169,10 @@ func jiraLabelSyncEnabled() bool { return strings.ToLower(os.Getenv("OPT_ENABLE_JIRA_SYNC_LABEL")) == "true" } +func jiraDescriptionSyncEnabled() bool { + return strings.ToLower(os.Getenv("OPT_ENABLE_JIRA_SYNC_DESCRIPTION")) == "true" +} + func jiraLabelSyncName() string { label := os.Getenv("OPT_JIRA_SYNC_LABEL") diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 989d366..b88fc15 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -22,8 +22,8 @@ type GitHub interface { GetBranchName() (string, error) BranchNameMatchesPRTitle(currentPRTitle string) bool GetPRInformation() *github.PullRequest + UpdatePR(newPRTitle string, newPRDescription string) UpdatePRTitle(newPRTitle string) - UpdatePRDescription(newPRDescription string) ApplyFormatting(issueKey string, issueName string) string HasLabel(labelName string) bool AddLabelToPR(labelName string) @@ -124,16 +124,11 @@ func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { logger.Infof("Updated Pull Request Title to: %s", newPRTitle) } -func (gh *GitHubClient) UpdatePRDescription(newPRDescription string) { - pullRequestInformation := gh.GetPRInformation() - +func (gh *GitHubClient) processDescriptionWithMarkers(existingBody string, newPRDescription string) string { const jiraStartMarker = "" const jiraEndMarker = "" - var finalDescription string - - if pullRequestInformation.Body != nil && *pullRequestInformation.Body != "" { - existingBody := *pullRequestInformation.Body + if existingBody != "" { // Check if Jira markers already exist startIndex := strings.Index(existingBody, jiraStartMarker) endIndex := strings.Index(existingBody, jiraEndMarker) @@ -142,21 +137,39 @@ func (gh *GitHubClient) UpdatePRDescription(newPRDescription string) { // Both markers exist - replace content between them beforeJira := existingBody[:startIndex] afterJira := existingBody[endIndex+len(jiraEndMarker):] - finalDescription = strings.TrimSpace(beforeJira) + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + strings.TrimSpace(afterJira) + return strings.TrimSpace(beforeJira) + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + strings.TrimSpace(afterJira) } else { // Markers don't exist or are malformed - append with markers - finalDescription = existingBody + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + return existingBody + "\n\n" + jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker } } else { // No existing description - add markers and Jira description - finalDescription = jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + return jiraStartMarker + "\n" + newPRDescription + "\n" + jiraEndMarker + } +} + +func (gh *GitHubClient) UpdatePR(newPRTitle string, newPRDescription string) { + pullRequestInformation := gh.GetPRInformation() + + var existingBody string + if pullRequestInformation.Body != nil { + existingBody = *pullRequestInformation.Body } + finalDescription := gh.processDescriptionWithMarkers(existingBody, newPRDescription) + + fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ - Body: &finalDescription, + Title: &newPRTitle, + Body: &finalDescription, }) + if err != nil { - logger.Errorf("Failed to update pull request prDescription: %v", err) + logger.Errorf("Failed to update pull request: %v", err) + } else { + logger.Infof("Updated Pull Request Title to: %s", newPRTitle) + logger.Info("Updated Pull Request Description") } } From 101a7bd54463dfc088839e7712985669afaea007 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 6 Nov 2025 15:04:05 -0600 Subject: [PATCH 08/31] [Actions] jira-sync-complete label defaults to atlassian blue --- actions/github/enrichPullRequest/drivers/jira/jira.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 626a036..df3f27d 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -80,7 +80,7 @@ func Format(gh github.GitHub) { } if jiraLabelSyncEnabled() { - gh.EnsureLabelExists(jiraLabelSyncName(), "Indicates that Jira synchronization has been completed for this PR", "0e8a16") + gh.EnsureLabelExists(jiraLabelSyncName(), "Indicates that Jira synchronization has been completed for this PR", "0052cc") gh.AddLabelToPR(jiraLabelSyncName()) } } From 9511dfe675f1e2cf33c2e1b2ca4d00c6213e9576 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 08:56:09 -0600 Subject: [PATCH 09/31] [Actions] add jira configuration options to action inputs --- .github/workflows/_release.yml | 2 ++ actions/github/enrichPullRequest/action.yml | 36 +++++++++++++++++-- .../enrichPullRequest/drivers/jira/jira.go | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index bdc919f..af0b503 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -23,6 +23,8 @@ jobs: imageName: 'gh-action-create-github-release' - actionPath: 'actions/github/jsonDiffAlert' imageName: 'gh-action-json-diff-alert' + - actionPath: 'actions/github/enrichPullRequest' + imageName: 'gh-action-enrich-pull-request' uses: ./.github/workflows/_github_createAndReleaseActionDockerImage.yml with: actionPath: ${{ matrix.actionPath }} diff --git a/actions/github/enrichPullRequest/action.yml b/actions/github/enrichPullRequest/action.yml index 3f492df..4d04e3d 100644 --- a/actions/github/enrichPullRequest/action.yml +++ b/actions/github/enrichPullRequest/action.yml @@ -21,18 +21,48 @@ inputs: required: false default: "" strategy: + type: string description: 'Which formatting strategy should be used' required: false default: 'branch-name' + jiraURL: + type: string + description: 'URL to your jira instance' + required: false + default: '' + jiraToken: + type: string + description: 'Jira auth token' + required: false + default: '' + jiraEnableSyncLabel: + type: boolean + description: 'Use a sync label' + required: false + default: true + jiraEnableSyncDescription: + type: boolean + description: 'Sync Jira description to PR description' + required: false + default: true + jiraSyncLabelName: type: string + description: 'Name of the sync label' + required: false + default: 'jira-sync-complete' runs: using: 'docker' - image: 'docker://ghcr.io/encoredigitalgroup/gh-action-create-github-release:latest' + image: 'docker://ghcr.io/encoredigitalgroup/gh-action-enrich-pull-request:latest' env: GH_TOKEN: ${{ inputs.token }} GH_REPOSITORY: ${{ inputs.repository }} PR_NUMBER: ${{ inputs.pullRequestNumber }} BRANCH_NAME: ${{ inputs.branch }} ENABLE_EXPERIMENTS: ${{ inputs.enableExperiments }} - CI_FMT_WORDS: ${{ inputs.customFormatting }} - CI_FMT_STRATEGY: ${{ inputs.strategy }} \ No newline at end of file + OPT_FMT_WORDS: ${{ inputs.customFormatting }} + OPT_FMT_STRATEGY: ${{ inputs.strategy }} + OPT_JIRA_URL: ${{ inputs.jiraURL }} + OPT_JIRA_TOKEN: ${{ inputs.jiraToken }} + OPT_ENABLE_JIRA_SYNC_LABEL: ${{ inputs.jiraEnableSyncLabel }} + OPT_JIRA_SYNC_LABEL_NAME: ${{ inputs.jiraSyncLabelName }} + OPT_ENABLE_JIRA_SYNC_DESCRIPTION: ${{ inputs.jiraEnableSyncDescription }} \ No newline at end of file diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index df3f27d..43d5ca3 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -132,7 +132,7 @@ func getJiraInfo(config Configuration) Information { } if config.URL == "" || config.Token == "" { - logger.Error("OPT_JIRA_URL and OPT_JIRA_TOKEN must be set when OPT_ENABLE_JIRA is true") + logger.Error("OPT_JIRA_URL and OPT_JIRA_TOKEN must be set when configured strategy is 'jira'.") return Information{HasJiraInfo: false} } From 0d1b144aeeb4ed1e73e24b62ebdb4ba868940374 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 08:57:17 -0600 Subject: [PATCH 10/31] [Actions] fix copy command in dockerfile --- actions/github/enrichPullRequest/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/Dockerfile b/actions/github/enrichPullRequest/Dockerfile index 197806c..4b61edb 100644 --- a/actions/github/enrichPullRequest/Dockerfile +++ b/actions/github/enrichPullRequest/Dockerfile @@ -9,7 +9,7 @@ COPY go.mod go.sum ./ RUN go mod download # Copy source code -COPY main.go ./ +COPY . . # Build the binary RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . From a2ce2552980b057f810ffad4eec67c27164f5f6c Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:04:24 -0600 Subject: [PATCH 11/31] [Actions] go mod tidy --- actions/github/enrichPullRequest/go.sum | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/actions/github/enrichPullRequest/go.sum b/actions/github/enrichPullRequest/go.sum index d8c2941..f3a4fb3 100644 --- a/actions/github/enrichPullRequest/go.sum +++ b/actions/github/enrichPullRequest/go.sum @@ -1,45 +1,79 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/EncoreDigitalGroup/golib v0.1.1 h1:vYcn6phvp1IQnbsczX7SeHLRuvoc0q1A+v6tagRY8KY= github.com/EncoreDigitalGroup/golib v0.1.1/go.mod h1:CxaCQZp09pWRXqI89reWl/e2qqJbPNteS5NvNDI/3m0= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/ctreminiom/go-atlassian v1.6.1 h1:thH/oaWlvWLN5a4AcgQ30yPmnn0mQaTiqsq1M6bA9BY= github.com/ctreminiom/go-atlassian v1.6.1/go.mod h1:dd5M0O8Co3bALyLQqWxPXoBfQNr6FFlpzUrA19IpLEo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= +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/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From b975c361489504fac3229bc68deb193239f88b14 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:04:55 -0600 Subject: [PATCH 12/31] [Actions] remove unused var --- actions/github/enrichPullRequest/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index 3b65e25..6b8c277 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -27,8 +27,6 @@ var prNumberStr = os.Getenv(envPRNumber) var branchName = os.Getenv(envBranchName) var parts = strings.Split(repo, "/") -var pullRequestTitle = "" - // Main function to execute the program func main() { checkEnvVars() From 5bb13884052082aa9541a93a67b35993e8897bca Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:21:18 -0600 Subject: [PATCH 13/31] [Actions] fix unused user defined formatting --- actions/github/enrichPullRequest/support/github/github.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index b88fc15..189d8a0 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -209,12 +209,13 @@ func (gh *GitHubClient) ApplyFormatting(issueKey string, issueName string) strin } } } - words := strings.Fields(issueName) + words := strings.Fields(formattedIssueName) for i, word := range words { if val, ok := defaultExceptions[word]; ok { words[i] = val } } + formattedIssueName = strings.Join(words, " ") return fmt.Sprintf("[%s] %s", issueKey, formattedIssueName) } From b95fc06ef3fb6a1a87684411107ff97ace4af066 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:22:25 -0600 Subject: [PATCH 14/31] [Actions] add nil check to jira issue description --- actions/github/enrichPullRequest/drivers/jira/jira.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 43d5ca3..2491cd6 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -160,7 +160,9 @@ func getJiraInfo(config Configuration) Information { result.ParentPrefix = parentPrefix } - result.Description = jiraIssue.Fields.Description.Text + if jiraIssue.Fields.Description != nil { + result.Description = jiraIssue.Fields.Description.Text + } return result } From ec9bf51aef41bcecd6aebde47e0ac0b6ef5223cb Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:24:11 -0600 Subject: [PATCH 15/31] [Actions] remove branch name env from main in favor of driver implementations --- actions/github/enrichPullRequest/main.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index 6b8c277..8aed472 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -17,14 +17,12 @@ var gh github.GitHub const envGHRepository = "GH_REPOSITORY" const envPRNumber = "PR_NUMBER" -const envBranchName = "BRANCH_NAME" const envStrategy = "OPT_FMT_STRATEGY" // Retrieve environment variables var strategy = os.Getenv(envStrategy) var repo = os.Getenv(envGHRepository) var prNumberStr = os.Getenv(envPRNumber) -var branchName = os.Getenv(envBranchName) var parts = strings.Split(repo, "/") // Main function to execute the program @@ -57,11 +55,6 @@ func checkEnvVars() { isMissingVar = true } - if branchName == "" { - logger.Error(envBranchName + " environment variable is not set") - isMissingVar = true - } - if repo == "" { logger.Error(envGHRepository + " environment variable is not set") isMissingVar = true From 36f1993da880a7ba0c553b9b47a1cd50b469b444 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:25:19 -0600 Subject: [PATCH 16/31] [Actions] exit if pr is not a valid int --- actions/github/enrichPullRequest/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index 8aed472..eae37fd 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -35,6 +35,7 @@ func main() { prNumber, err := strconv.Atoi(prNumberStr) if err != nil { logger.Errorf(envPRNumber+" is not a valid integer: %v", err) + os.Exit(1) } gh = github.New(repoOwner, repoName, prNumber) From 750f49d941502bc666c9e3230d3732d4d971a682 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:26:10 -0600 Subject: [PATCH 17/31] [Actions] add import alias --- actions/github/enrichPullRequest/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index eae37fd..882de72 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -8,7 +8,7 @@ import ( "github.com/EncoreDigitalGroup/golib/logger" "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers" - "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/branch_name" + branchname "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/branch_name" "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/drivers/jira" "github.com/EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest/support/github" ) From c79a527bf9aad5c5f4262d96f75b01738714b9f2 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 7 Nov 2025 09:45:09 -0600 Subject: [PATCH 18/31] [Docs] Add enrichPullRequest.md --- docs/Actions/GitHub/enrichPullRequest.md | 362 +++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 docs/Actions/GitHub/enrichPullRequest.md diff --git a/docs/Actions/GitHub/enrichPullRequest.md b/docs/Actions/GitHub/enrichPullRequest.md new file mode 100644 index 0000000..051fda9 --- /dev/null +++ b/docs/Actions/GitHub/enrichPullRequest.md @@ -0,0 +1,362 @@ +# GitHub Enrich Pull Request Action + +## Overview + +The `actions/github/enrichPullRequest` action automatically enriches pull request titles and descriptions with information from project management systems. It supports +multiple strategies including branch name parsing and Jira integration, providing enhanced PR context and consistency across development workflows. + +## Language/Tool Support + +- **GitHub**: Pull request management and API integration +- **Jira**: Issue tracking and project management integration +- **Branch Naming**: Automatic parsing of branch name conventions + +## Features + +- **Multiple Enrichment Strategies**: Support for branch-name and Jira strategies +- **Automatic PR Title Formatting**: Converts branch names and issue keys to readable titles +- **Jira Integration**: Syncs Jira issue titles and descriptions to pull requests +- **Custom Formatting Rules**: User-defined formatting preferences +- **Label Management**: Automatic label creation and assignment for Jira sync tracking +- **Parent Issue Support**: Includes parent issue prefixes for hierarchical issues + +## Usage + +```yaml +- name: Enrich Pull Request + uses: EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest@v3 + with: + repository: ${{ github.repository }} + pullRequestNumber: ${{ github.event.number }} + branch: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + strategy: "branch-name" + customFormatting: "api:API,ui:User Interface" +``` + +## Inputs + +| Input | Type | Required | Default | Description | +|-----------------------------|---------|----------|------------------------|--------------------------------------------------------| +| `repository` | string | ✅ | - | GitHub repository in format "owner/repo" | +| `pullRequestNumber` | string | ✅ | - | Pull request number to update | +| `branch` | string | ✅ | - | Branch name to parse for enrichment | +| `token` | string | ✅ | - | GitHub token with pull request write permissions | +| `strategy` | string | ❌ | `"branch-name"` | Enrichment strategy: "branch-name" or "jira" | +| `customFormatting` | string | ❌ | `""` | Custom word formatting rules (comma-separated pairs) | +| `jiraURL` | string | ❌ | `""` | URL to your Jira instance (required for jira strategy) | +| `jiraToken` | string | ❌ | `""` | Jira authentication token (required for jira strategy) | +| `jiraEnableSyncLabel` | boolean | ❌ | `true` | Create and assign sync completion label | +| `jiraEnableSyncDescription` | boolean | ❌ | `true` | Sync Jira description to PR description | +| `jiraSyncLabelName` | string | ❌ | `"jira-sync-complete"` | Name of the sync completion label | + +## Action Implementation + +This action runs in a Docker container: + +- **Image**: `ghcr.io/encoredigitalgroup/gh-action-enrich-pull-request:latest` +- **Environment**: All inputs are passed as environment variables +- **Execution**: Containerized Go application with multiple strategy drivers + +## Enrichment Strategies + +### Branch Name Strategy + +Extracts issue keys and descriptions from branch names using regex patterns: + +**Supported Branch Patterns:** + +- `(epic|feature|bugfix|hotfix)/[A-Z]+-[0-9]+-summary` +- `[A-Z]+-[0-9]+-summary` + +**Examples:** + +- `feature/PROJ-123-user-authentication` → "[PROJ-123] User Authentication" +- `bugfix/ISSUE-456-login-fix` → "[ISSUE-456] Login Fix" +- `TASK-789-api-improvements` → "[TASK-789] API Improvements" + +### Jira Strategy + +Integrates with Jira to fetch issue information and enrich PRs: + +**Features:** + +- Fetches issue title from Jira +- Optionally syncs issue description to PR (enabled by default) +- Adds parent issue prefixes for subtasks +- Creates sync completion labels (enabled by default) +- Prevents duplicate syncing + +## Usage Examples + +### Basic Branch Name Enrichment + +```yaml +name: Enrich PR with Branch Info +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + enrich-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Enrich Pull Request + uses: EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest@v3 + with: + repository: ${{ github.repository }} + pullRequestNumber: ${{ github.event.number }} + branch: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + strategy: "branch-name" +``` + +### Jira Integration + +```yaml +name: Enrich PR with Jira +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + enrich-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Enrich with Jira Info + uses: EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest@v3 + with: + repository: ${{ github.repository }} + pullRequestNumber: ${{ github.event.number }} + branch: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + strategy: "jira" + jiraURL: ${{ vars.JIRA_URL }} + jiraToken: ${{ secrets.JIRA_TOKEN }} + jiraEnableSyncLabel: true + jiraEnableSyncDescription: true +``` + +### Custom Formatting Rules + +```yaml +name: Enrich with Custom Formatting +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + enrich-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Enrich with Custom Rules + uses: EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest@v3 + with: + repository: ${{ github.repository }} + pullRequestNumber: ${{ github.event.number }} + branch: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + strategy: "branch-name" + customFormatting: "api:API Integration,ui:User Interface,db:Database Operations,auth:Authentication" +``` + +## Integration Patterns + +### Complete PR Workflow + +```yaml +name: PR Quality Gate +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + enrich: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Enrich PR Information + uses: EncoreDigitalGroup/ci-workflows/actions/github/enrichPullRequest@v3 + with: + repository: ${{ github.repository }} + pullRequestNumber: ${{ github.event.number }} + branch: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + strategy: "jira" + jiraURL: ${{ vars.JIRA_URL }} + jiraToken: ${{ secrets.JIRA_TOKEN }} + + validate: + needs: enrich + runs-on: ubuntu-latest + steps: + - name: Validate PR + run: echo "Running validation..." + + test: + needs: enrich + runs-on: ubuntu-latest + steps: + - name: Run Tests + run: echo "Running tests..." +``` + +## Branch Name Patterns + +### Supported Formats + +The branch-name strategy recognizes these patterns: + +**With Issue Type Prefix:** + +``` +epic/PROJ-123-epic-title +feature/PROJ-456-new-feature +bugfix/PROJ-789-bug-description +hotfix/PROJ-101-critical-fix +``` + +**Without Issue Type Prefix:** + +``` +PROJ-123-task-description +ISSUE-456-improvement-title +TICKET-789-maintenance-task +``` + +### Pattern Matching + +- **Issue Key**: `[A-Z]+-[0-9]+` (e.g., PROJ-123, ISSUE-456) +- **Description**: Hyphen-separated words after issue key +- **Type Prefixes**: epic, feature, bugfix, hotfix (optional) + +## Jira Integration Details + +### Authentication + +```yaml +# Using organization variables and secrets +jiraURL: ${{ vars.JIRA_URL }} # https://yourorg.atlassian.net +jiraToken: ${{ secrets.JIRA_TOKEN }} # Jira API token or PAT +``` + +### Issue Information + +The Jira strategy fetches and uses: + +- **Issue Summary**: Used as PR title +- **Issue Description**: Optionally synced to PR description +- **Parent Issue**: Added as prefix for subtasks (excluding epics) +- **Issue Status**: Used for validation + +### Label Management + +When `jiraEnableSyncLabel: true`: + +- Creates label if it doesn't exist +- Label description: "Indicates that Jira synchronization has been completed for this PR" +- Prevents duplicate syncing on subsequent runs + +## Required Permissions + +The GitHub token must have the following permissions: + +```yaml +permissions: + pull-requests: write # Required for updating PR title/description and labels + contents: read # Required for accessing repository and branch information +``` + +## Custom Formatting Rules + +Define custom word formatting with key-value pairs: + +### Format + +``` +"key1:value1,key2:value2,key3:value3" +``` + +### Examples + +```yaml +# Technical abbreviations +customFormatting: "api:API,ui:UI,db:Database,auth:Authentication,ci:Continuous Integration" + +# Business domains +customFormatting: "crm:Customer Relations,hr:Human Resources,fin:Finance,ops:Operations" + +# Component names +customFormatting: "comp:Component,svc:Service,lib:Library,util:Utility,cfg:Configuration" +``` + +## Troubleshooting + +### Common Issues + +**PR Not Updated** + +- Verify token has `pull-requests: write` permission +- Check pull request number is correct +- Ensure branch name matches expected patterns + +**Jira Connection Issues** + +- Verify Jira URL format: `https://yourorg.atlassian.net` +- Check Jira token permissions +- Ensure issue key exists in Jira + +**Branch Pattern Mismatch** + +- Verify branch name follows supported patterns +- Check regex matching in action logs +- Test with simpler branch names + +### Error Resolution + +**Authentication Failed** + +```yaml +# Ensure proper Jira authentication +jiraToken: ${{ secrets.JIRA_API_TOKEN }} # Use API token, not password +``` + +**Issue Not Found** + +```bash +# Verify issue exists and is accessible +curl -H "Authorization: Bearer $JIRA_TOKEN" \ + "https://yourorg.atlassian.net/rest/api/3/issue/PROJ-123" +``` + +**Permission Denied** + +```yaml +# Ensure proper GitHub permissions +permissions: + pull-requests: write + contents: read +``` + +## Best Practices + +### Branch Naming Conventions + +1. **Consistent Prefixes**: Use standard issue type prefixes +2. **Issue Keys**: Always include project issue keys +3. **Descriptive Names**: Use clear, hyphen-separated descriptions +4. **Lowercase**: Use lowercase for consistency + +### Jira Integration + +1. **Service Account**: Create a service account in your Jira system dedicated to integrations. +2. **API Tokens**: Use Jira API tokens instead of passwords +3. **Issue Templates**: Maintain consistent issue title formats +4. **Parent Relationships**: Properly structure issue hierarchies \ No newline at end of file From 90e8c9c755bea2465291e8495e7569cc6deeca2a Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:15:20 -0600 Subject: [PATCH 19/31] [Actions] Add Comment to PR when Label Actions Fail --- .../support/github/github.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 189d8a0..99c60af 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -28,6 +28,7 @@ type GitHub interface { HasLabel(labelName string) bool AddLabelToPR(labelName string) EnsureLabelExists(labelName string, description string, color string) + AddPRComment(comment string) } // GitHubClient implements the GitHub interface @@ -248,6 +249,11 @@ func (gh *GitHubClient) EnsureLabelExists(labelName string, description string, _, _, err = gh.client.Issues.CreateLabel(context.Background(), gh.repositoryOwner, gh.repositoryName, label) if err != nil { logger.Errorf("Failed to create label '%s': %v", labelName, err) + prComment := "The label `" + labelName + "` does not exist in this repository and we encountered an " + prComment = prComment + "error when attempting to create it.\n\n" + prComment = prComment + "Please ensure the access token provided has permission to manage labels." + gh.AddPRComment(prComment) + } else { logger.Infof("Created label '%s' in repository", labelName) } @@ -259,7 +265,23 @@ func (gh *GitHubClient) AddLabelToPR(labelName string) { _, _, err := gh.client.Issues.AddLabelsToIssue(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, labels) if err != nil { logger.Errorf("Failed to add label '%s' to PR: %v", labelName, err) + prComment := "We failed to add the `" + labelName + "` label to this PR.\n\n" + prComment = prComment + "Please ensure the access token provided has permission to manage labels." + gh.AddPRComment(prComment) } else { logger.Infof("Added label '%s' to PR #%d", labelName, gh.pullRequestNumber) } } + +func (gh *GitHubClient) AddPRComment(comment string) { + issueComment := &github.IssueComment{ + Body: &comment, + } + + _, _, err := gh.client.Issues.CreateComment(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, issueComment) + if err != nil { + logger.Errorf("Failed to add comment to PR: %v", err) + } else { + logger.Infof("Added comment to PR #%d", gh.pullRequestNumber) + } +} From 9ef65374203b5936c8c7e5c4d967dc516a69d889 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:27:10 -0600 Subject: [PATCH 20/31] [Actions] Update Jira Driver for EnrichPullRequest --- .../enrichPullRequest/drivers/jira/jira.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 2491cd6..9c79139 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -17,6 +17,7 @@ import ( type Configuration struct { Enable bool URL string + Email string Token string IssueKey string } @@ -46,6 +47,12 @@ func Format(gh github.GitHub) { os.Exit(1) } + envEmail := os.Getenv("OPT_JIRA_EMAIL") + if envEmail == "" { + logger.Error("OPT_JIRA_EMAIL is not set") + os.Exit(1) + } + envToken := os.Getenv("OPT_JIRA_TOKEN") if envToken == "" { logger.Error("OPT_JIRA_TOKEN is not set") @@ -61,6 +68,7 @@ func Format(gh github.GitHub) { config := Configuration{ Enable: true, URL: envUrl, + Email: envEmail, Token: envToken, IssueKey: issueKey, } @@ -85,13 +93,13 @@ func Format(gh github.GitHub) { } } -func createJiraClient(jiraURL, jiraToken string) (*v3.Client, error) { +func createJiraClient(jiraURL, jiraEmail, jiraToken string) (*v3.Client, error) { client, err := v3.New(nil, jiraURL) if err != nil { return nil, err } - client.Auth.SetBearerToken(jiraToken) + client.Auth.SetBasicAuth(jiraEmail, jiraToken) return client, nil } @@ -131,12 +139,12 @@ func getJiraInfo(config Configuration) Information { return Information{HasJiraInfo: false} } - if config.URL == "" || config.Token == "" { - logger.Error("OPT_JIRA_URL and OPT_JIRA_TOKEN must be set when configured strategy is 'jira'.") + if config.URL == "" || config.Email == "" || config.Token == "" { + logger.Error("OPT_JIRA_URL OPT_JIRA_EMAIL, and OPT_JIRA_TOKEN must be set when configured strategy is 'jira'.") return Information{HasJiraInfo: false} } - client, err := createJiraClient(config.URL, config.Token) + client, err := createJiraClient(config.URL, config.Email, config.Token) if err != nil { logger.Errorf("Failed to create Jira client: %v", err) return Information{HasJiraInfo: false} From a76c0b2e4027478d441d4474ee2772dfd5e66f9b Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:31:38 -0600 Subject: [PATCH 21/31] [Actions] Don't Place Brackets Around Parent Prefix on Retrieval --- actions/github/enrichPullRequest/drivers/jira/jira.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 9c79139..e57fcd0 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -131,7 +131,7 @@ func getParentIssuePrefix(client *v3.Client, issueKey string) (string, error) { return "", nil } - return fmt.Sprintf("[%s]", issue.Fields.Parent.Key), nil + return fmt.Sprintf("%s", issue.Fields.Parent.Key), nil } func getJiraInfo(config Configuration) Information { From 5263a2687e771bf7030527b255900ff11827d4b3 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:35:25 -0600 Subject: [PATCH 22/31] [Actions] Early Return if Issue Key is Empty --- actions/github/enrichPullRequest/drivers/jira/jira.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index e57fcd0..a1865fc 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -65,6 +65,11 @@ func Format(gh github.GitHub) { os.Exit(1) } + if issueKey == "" { + logger.Error("Issue key is empty") + return + } + config := Configuration{ Enable: true, URL: envUrl, From 255beb95998a2845564ed04fe3e772ee052f0d10 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:37:55 -0600 Subject: [PATCH 23/31] [Docs] Update enrichPullRequest.md --- docs/Actions/GitHub/enrichPullRequest.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/Actions/GitHub/enrichPullRequest.md b/docs/Actions/GitHub/enrichPullRequest.md index 051fda9..f4c0ab3 100644 --- a/docs/Actions/GitHub/enrichPullRequest.md +++ b/docs/Actions/GitHub/enrichPullRequest.md @@ -45,6 +45,7 @@ multiple strategies including branch name parsing and Jira integration, providin | `strategy` | string | ❌ | `"branch-name"` | Enrichment strategy: "branch-name" or "jira" | | `customFormatting` | string | ❌ | `""` | Custom word formatting rules (comma-separated pairs) | | `jiraURL` | string | ❌ | `""` | URL to your Jira instance (required for jira strategy) | +| `jiraEmail` | string | ❌ | `""` | Jira authentication email (required for jira strategy) | | `jiraToken` | string | ❌ | `""` | Jira authentication token (required for jira strategy) | | `jiraEnableSyncLabel` | boolean | ❌ | `true` | Create and assign sync completion label | | `jiraEnableSyncDescription` | boolean | ❌ | `true` | Sync Jira description to PR description | @@ -136,6 +137,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} strategy: "jira" jiraURL: ${{ vars.JIRA_URL }} + jiraEmail: ${{ vars.JIRA_EMAIL }} jiraToken: ${{ secrets.JIRA_TOKEN }} jiraEnableSyncLabel: true jiraEnableSyncDescription: true @@ -191,6 +193,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} strategy: "jira" jiraURL: ${{ vars.JIRA_URL }} + jiraEmail: ${{ vars.JIRA_EMAIL }} jiraToken: ${{ secrets.JIRA_TOKEN }} validate: @@ -243,7 +246,8 @@ TICKET-789-maintenance-task ```yaml # Using organization variables and secrets -jiraURL: ${{ vars.JIRA_URL }} # https://yourorg.atlassian.net +jiraURL: ${{ vars.JIRA_URL }} # https://yourorg.atlassian.net +jiraEmail: ${{ vars.JIRA_EMAIL }} # someone@yourcompany.com jiraToken: ${{ secrets.JIRA_TOKEN }} # Jira API token or PAT ``` @@ -271,6 +275,7 @@ The GitHub token must have the following permissions: ```yaml permissions: pull-requests: write # Required for updating PR title/description and labels + issues: write # Required for creating the sync label if it doesn't exist contents: read # Required for accessing repository and branch information ``` @@ -342,6 +347,7 @@ curl -H "Authorization: Bearer $JIRA_TOKEN" \ # Ensure proper GitHub permissions permissions: pull-requests: write + issues: write contents: read ``` From c7f053171b66bb4eaf14d242d82ee15bd29efbad Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:42:54 -0600 Subject: [PATCH 24/31] [Actions] early return --- actions/github/enrichPullRequest/support/github/github.go | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 99c60af..f1ead63 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -120,6 +120,7 @@ func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { if err != nil { logger.Errorf("Failed to update pull request prTitle: %v", err) + return } logger.Infof("Updated Pull Request Title to: %s", newPRTitle) From aafacb474e18fa5cd55dec4bcf37ac664782d3e9 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 11:50:20 -0600 Subject: [PATCH 25/31] [Actions] Add Driver/Strategy Validation --- .../github/enrichPullRequest/drivers/drivers.go | 15 +++++++++++++++ actions/github/enrichPullRequest/main.go | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/actions/github/enrichPullRequest/drivers/drivers.go b/actions/github/enrichPullRequest/drivers/drivers.go index 736d384..84236bc 100644 --- a/actions/github/enrichPullRequest/drivers/drivers.go +++ b/actions/github/enrichPullRequest/drivers/drivers.go @@ -2,3 +2,18 @@ package drivers const BranchName = "branch-name" const Jira = "jira" + +func Validate(driver string) bool { + validDrivers := []string{ + BranchName, + Jira, + } + + for _, validDriver := range validDrivers { + if driver == validDriver { + return true + } + } + + return false +} diff --git a/actions/github/enrichPullRequest/main.go b/actions/github/enrichPullRequest/main.go index 882de72..01e9aa7 100644 --- a/actions/github/enrichPullRequest/main.go +++ b/actions/github/enrichPullRequest/main.go @@ -27,6 +27,13 @@ var parts = strings.Split(repo, "/") // Main function to execute the program func main() { + validStrategy := drivers.Validate(strategy) + + if !validStrategy { + logger.Errorf("Invalid strategy: %s", strategy) + os.Exit(1) + } + checkEnvVars() repoOwner := parts[0] repoName := parts[1] From fd094becba5a3f9d9b728369bae76b70bd3bc660 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:08:39 -0600 Subject: [PATCH 26/31] [Actions] Logger --- actions/github/enrichPullRequest/support/github/github.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index f1ead63..1354e4b 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -112,7 +112,7 @@ func (gh *GitHubClient) GetPRInformation() *github.PullRequest { } func (gh *GitHubClient) UpdatePRTitle(newPRTitle string) { - fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + logger.Infof("Attempting to Update Pull Request Title to: %s", newPRTitle) _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ Title: &newPRTitle, @@ -160,7 +160,7 @@ func (gh *GitHubClient) UpdatePR(newPRTitle string, newPRDescription string) { finalDescription := gh.processDescriptionWithMarkers(existingBody, newPRDescription) - fmt.Println("Attempting to Update Pull Request Title to:", newPRTitle) + logger.Infof("Attempting to Update Pull Request Title to: %s", newPRTitle) _, _, err := gh.client.PullRequests.Edit(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, &github.PullRequest{ Title: &newPRTitle, From 26c534bbd65570d190947e478ef95bf19f82b03d Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:11:04 -0600 Subject: [PATCH 27/31] [Actions] Add Jira Email Input --- actions/github/enrichPullRequest/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/actions/github/enrichPullRequest/action.yml b/actions/github/enrichPullRequest/action.yml index 4d04e3d..d1cd070 100644 --- a/actions/github/enrichPullRequest/action.yml +++ b/actions/github/enrichPullRequest/action.yml @@ -30,6 +30,11 @@ inputs: description: 'URL to your jira instance' required: false default: '' + jiraEmail: + type: string + description: 'Jira auth email' + required: false + default: '' jiraToken: type: string description: 'Jira auth token' @@ -62,6 +67,7 @@ runs: OPT_FMT_WORDS: ${{ inputs.customFormatting }} OPT_FMT_STRATEGY: ${{ inputs.strategy }} OPT_JIRA_URL: ${{ inputs.jiraURL }} + OPT_JIRA_EMAIL: ${{ inputs.jiraEmail }} OPT_JIRA_TOKEN: ${{ inputs.jiraToken }} OPT_ENABLE_JIRA_SYNC_LABEL: ${{ inputs.jiraEnableSyncLabel }} OPT_JIRA_SYNC_LABEL_NAME: ${{ inputs.jiraSyncLabelName }} From 5f24a30722cd93c5a21dec9f2a125c0d846945b5 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:11:44 -0600 Subject: [PATCH 28/31] [Actions] Add Missing Comma in log statement --- actions/github/enrichPullRequest/drivers/jira/jira.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index a1865fc..283912d 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -145,7 +145,7 @@ func getJiraInfo(config Configuration) Information { } if config.URL == "" || config.Email == "" || config.Token == "" { - logger.Error("OPT_JIRA_URL OPT_JIRA_EMAIL, and OPT_JIRA_TOKEN must be set when configured strategy is 'jira'.") + logger.Error("OPT_JIRA_URL, OPT_JIRA_EMAIL, and OPT_JIRA_TOKEN must be set when configured strategy is 'jira'.") return Information{HasJiraInfo: false} } From 4f87db65e4cca2e8905abec3b82b85dbfb0cd0cb Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:24:22 -0600 Subject: [PATCH 29/31] [Actions] Add Jira Auth Issue Handling --- .../enrichPullRequest/drivers/jira/jira.go | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/actions/github/enrichPullRequest/drivers/jira/jira.go b/actions/github/enrichPullRequest/drivers/jira/jira.go index 283912d..fbffd05 100644 --- a/actions/github/enrichPullRequest/drivers/jira/jira.go +++ b/actions/github/enrichPullRequest/drivers/jira/jira.go @@ -2,7 +2,9 @@ package jira import ( "context" + "errors" "fmt" + "net/http" "os" "strings" @@ -27,6 +29,16 @@ type Information struct { Title string Description string HasJiraInfo bool + AuthFailure bool +} + +type JiraError struct { + IsAuthFailure bool + OriginalError error +} + +func (e *JiraError) Error() string { + return e.OriginalError.Error() } func Format(gh github.GitHub) { @@ -80,6 +92,21 @@ func Format(gh github.GitHub) { jira := getJiraInfo(config) + if jira.AuthFailure { + logger.Errorf("Jira authentication failed") + + comment := "**Jira Authentication Failed**\n\n" + + "Unable to authenticate with Jira to fetch issue information. " + + "Please verify that the Jira credentials (URL, email, and token) are correctly configured and that the token has not expired.\n\n" + + "**Possible solutions:**\n" + + "- Check that `OPT_JIRA_URL`, `OPT_JIRA_EMAIL`, and `OPT_JIRA_TOKEN` environment variables are set correctly\n" + + "- Verify that the Jira API token is still valid\n" + + "- Ensure the Jira user has permission to access the issue: `" + issueKey + "`" + + gh.AddPRComment(comment) + return + } + newPRTitle := gh.ApplyFormatting(issueKey, jira.Title) if jira.ParentPrefix != "" { @@ -109,9 +136,13 @@ func createJiraClient(jiraURL, jiraEmail, jiraToken string) (*v3.Client, error) } func getCurrentIssueInfo(client *v3.Client, issueKey string) (*models.IssueScheme, error) { - issue, _, err := client.Issue.Get(context.Background(), issueKey, nil, nil) + issue, response, err := client.Issue.Get(context.Background(), issueKey, nil, nil) if err != nil { - return nil, fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err) + isAuthFailure := response != nil && response.StatusCode == http.StatusUnauthorized + return nil, &JiraError{ + IsAuthFailure: isAuthFailure, + OriginalError: fmt.Errorf("failed to fetch Jira issue %s: %v", issueKey, err), + } } return issue, nil @@ -160,6 +191,11 @@ func getJiraInfo(config Configuration) Information { jiraIssue, err := getCurrentIssueInfo(client, config.IssueKey) if err != nil { logger.Errorf("Failed to get current issue info: %v", err) + var jiraErr *JiraError + if errors.As(err, &jiraErr) && jiraErr.IsAuthFailure { + return Information{HasJiraInfo: false, AuthFailure: true} + } + return Information{HasJiraInfo: false} } result.Title = jiraIssue.Fields.Summary From 18955a8c102b6322300b339d822673a6b5b26fb7 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:25:35 -0600 Subject: [PATCH 30/31] [Actions] Update String Concat Formatting --- .../github/enrichPullRequest/support/github/github.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 1354e4b..93c20b1 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -250,9 +250,10 @@ func (gh *GitHubClient) EnsureLabelExists(labelName string, description string, _, _, err = gh.client.Issues.CreateLabel(context.Background(), gh.repositoryOwner, gh.repositoryName, label) if err != nil { logger.Errorf("Failed to create label '%s': %v", labelName, err) - prComment := "The label `" + labelName + "` does not exist in this repository and we encountered an " - prComment = prComment + "error when attempting to create it.\n\n" - prComment = prComment + "Please ensure the access token provided has permission to manage labels." + prComment := "The label `" + labelName + "` does not exist in this repository and we encountered an " + + "error when attempting to create it.\n\n" + + "Please ensure the access token provided has permission to manage labels." + gh.AddPRComment(prComment) } else { @@ -266,8 +267,8 @@ func (gh *GitHubClient) AddLabelToPR(labelName string) { _, _, err := gh.client.Issues.AddLabelsToIssue(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber, labels) if err != nil { logger.Errorf("Failed to add label '%s' to PR: %v", labelName, err) - prComment := "We failed to add the `" + labelName + "` label to this PR.\n\n" - prComment = prComment + "Please ensure the access token provided has permission to manage labels." + prComment := "We failed to add the `" + labelName + "` label to this PR.\n\n" + + "Please ensure the access token provided has permission to manage labels." gh.AddPRComment(prComment) } else { logger.Infof("Added label '%s' to PR #%d", labelName, gh.pullRequestNumber) From f02af8cb008e4e7635216e69503fab2dec7f5200 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Mon, 10 Nov 2025 13:29:48 -0600 Subject: [PATCH 31/31] [Actions] Early Return --- actions/github/enrichPullRequest/support/github/github.go | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/github/enrichPullRequest/support/github/github.go b/actions/github/enrichPullRequest/support/github/github.go index 93c20b1..16ccb6e 100644 --- a/actions/github/enrichPullRequest/support/github/github.go +++ b/actions/github/enrichPullRequest/support/github/github.go @@ -84,6 +84,7 @@ func (gh *GitHubClient) BranchNameMatchesPRTitle(currentPRTitle string) bool { pullRequest, _, err := gh.client.PullRequests.Get(context.Background(), gh.repositoryOwner, gh.repositoryName, gh.pullRequestNumber) if err != nil { logger.Errorf("Failed to get pullRequest request: %v", err) + return false } if currentPRTitle == *pullRequest.Title {