Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,56 @@ Create/update GitHub PR comments through an executable (for e.g. in ci/cd script
|`-owner=<repo_owner>`|GitHub repository owner|true|
|`-repo=<repo_name>`|GitHub repository name|true|
|`-body=<text>`|The contents of the comment|true|
|`-commentId=<comment_id>`|Update a certain comment by its ID||
|`-bodyIncludes=<text>`|Update a certain comment by its content||
|`-comment-id=<comment_id>`|Update a certain comment by its ID||
|`-body-includes=<text>`|Update a certain comment by its content||
|`-append`|Enable append mode for table-based updates||
|`-row-id=<identifier>`|Unique identifier for the row (used with `-append`)||
|`-columns=<csv>`|Comma-separated column names (used with `-append`)||
|`-values=<csv>`|Comma-separated column values (used with `-append`)||

> [!NOTE]
> If no `commentId` or `bodyIncludes` is provided, it will always create a new comment, else it only creates a new comment when not found.
> If no `comment-id` or `body-includes` is provided, it will always create a new comment, else it only creates a new comment when not found.

## Append Mode (Table-based updates)

When using `-append` mode, the tool will maintain a single comment with a table of custom columns. This is useful for CI/CD pipelines that need to consolidate multiple results in one comment.

**Example:**
```bash
# First package
./pr-comments -token="$GITHUB_TOKEN" -pr="$PR_NUMBER" \
-owner="owner" -repo="repo" \
-body="<!-- package-publish-status -->
## :package: Package Publishing Status
> Packages published from this PR" \
-body-includes="<!-- package-publish-status -->" \
-append \
-row-id="@myorg/package-a" \
-columns="Package,Status,Version,Time" \
-values="@myorg/package-a,:white_check_mark: success,\`@myorg/package-a@1.0.0-alpha.1\`,12:34:56"

# Second package
./pr-comments -token="$GITHUB_TOKEN" -pr="$PR_NUMBER" \
-owner="owner" -repo="repo" \
-body="<!-- package-publish-status -->
## :package: Package Publishing Status
> Packages published from this PR" \
-body-includes="<!-- package-publish-status -->" \
-append \
-row-id="@myorg/package-b" \
-columns="Package,Status,Version,Time" \
-values="@myorg/package-b,:x: failure,-,12:35:10"
```

This will create or update a comment that looks like:

```markdown
<!-- package-publish-status -->
## 📦 Package Publishing Status
> Packages published from this PR

| Package | Status | Version | Time |
|---------|--------|---------|------|
| @myorg/package-a | ✅ success | `@myorg/package-a@1.0.0-alpha.1` | 12:34:56 |
| @myorg/package-b | ❌ failure | - | 12:35:10 |
```
185 changes: 182 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func main() {
body := flag.String("body", "", "Body to send")
commentId := flag.Int64("comment-id", 0, "Comment ID")
bodyIncludes := flag.String("body-includes", "", "Comment ID")
appendMode := flag.Bool("append", false, "Append to existing comment instead of replacing")
rowId := flag.String("row-id", "", "Unique identifier for the row (e.g., package name)")
columns := flag.String("columns", "", "Comma-separated column names (e.g., 'Package,Status,Version')")
values := flag.String("values", "", "Comma-separated column values (e.g., '@org/pkg,success,1.0.0')")

flag.Parse()

Expand Down Expand Up @@ -53,10 +57,15 @@ func main() {
*commentId = findComment(client, *owner, *repo, *prNumber, *bodyIncludes)
}

if *commentId == 0 {
createComment(client, *owner, *repo, *prNumber, *body)
if *appendMode {
// Append mode: update or create a comment with table format
appendToComment(client, *owner, *repo, *prNumber, *commentId, *bodyIncludes, *rowId, *columns, *values, *body)
} else {
updateComment(client, *owner, *repo, *commentId, *body)
if *commentId == 0 {
createComment(client, *owner, *repo, *prNumber, *body)
} else {
updateComment(client, *owner, *repo, *commentId, *body)
}
}
}

Expand Down Expand Up @@ -112,3 +121,173 @@ func findComment(client *github.Client, owner string, repo string, prNumber int,
return 0
}
}

func appendToComment(client *github.Client, owner string, repo string, prNumber int, commentId int64, bodyIncludes string, rowId string, columns string, values string, customBody string) {
// Parse columns and values
columnList := parseCSV(columns)
valueList := parseCSV(values)

if len(columnList) == 0 {
fmt.Println("Error: columns are required in append mode")
os.Exit(1)
}

if len(valueList) == 0 {
fmt.Println("Error: values are required in append mode")
os.Exit(1)
}

if len(columnList) != len(valueList) {
fmt.Println("Error: number of columns must match number of values")
os.Exit(1)
}

var newBody string
if commentId == 0 {
// Create a new comment with table
newBody = createTableComment(bodyIncludes, rowId, columnList, valueList, customBody)
createComment(client, owner, repo, prNumber, newBody)
} else {
// Update existing comment by adding/updating row
existingComment := getComment(client, owner, repo, commentId)
newBody = updateTableComment(existingComment, rowId, columnList, valueList)
updateComment(client, owner, repo, commentId, newBody)
}
}

func parseCSV(input string) []string {
if len(strings.TrimSpace(input)) == 0 {
return []string{}
}
parts := strings.Split(input, ",")
result := make([]string, len(parts))
for i, part := range parts {
result[i] = strings.TrimSpace(part)
}
return result
}

func getComment(client *github.Client, owner string, repo string, commentId int64) string {
comment, _, err := client.Issues.GetComment(context.Background(), owner, repo, commentId)

if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}

return comment.GetBody()
}

func createTableComment(identifier string, rowId string, columns []string, values []string, customBody string) string {
var body strings.Builder

// Create header row
body.WriteString("|")
for _, col := range columns {
body.WriteString(" ")
body.WriteString(col)
body.WriteString(" |")
}
body.WriteString("\n")

// Create separator row
body.WriteString("|")
for range columns {
body.WriteString("---------|")
}
body.WriteString("\n")

// Create data row
body.WriteString(formatTableRow(columns, values))

return body.String()
}

func updateTableComment(existingBody string, rowId string, columns []string, values []string) string {
lines := strings.Split(existingBody, "\n")

tableStartIndex := -1
headerSeparatorIndex := -1
rowIndex := -1
firstColumnIndex := -1

for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "|") && strings.Count(trimmedLine, "|") >= 2 {
if tableStartIndex == -1 {
tableStartIndex = i
firstColumnIndex = 0
} else if headerSeparatorIndex == -1 && strings.Contains(trimmedLine, "---") {
headerSeparatorIndex = i
} else if headerSeparatorIndex != -1 && len(rowId) > 0 {
cells := parseTableRow(trimmedLine)
if len(cells) > firstColumnIndex && strings.Contains(cells[firstColumnIndex], rowId) {
rowIndex = i
break
}
}
}
}

newRow := formatTableRow(columns, values)

if rowIndex != -1 {
// Update existing row
lines[rowIndex] = newRow
} else if headerSeparatorIndex != -1 {
// Check if column counts match
existingHeaderLine := lines[tableStartIndex]
existingColumnCount := strings.Count(existingHeaderLine, "|") - 1
newColumnCount := len(columns)

if existingColumnCount != newColumnCount {
// Column mismatch - recreate table
return createTableComment("", rowId, columns, values, existingBody)
}

// Add new row after the header separator
insertIndex := headerSeparatorIndex + 1
lines = append(lines[:insertIndex], append([]string{newRow}, lines[insertIndex:]...)...)
} else {
// Table doesn't exist, create it
return createTableComment("", rowId, columns, values, existingBody)
}

return strings.Join(lines, "\n")
}

func parseTableRow(line string) []string {
// Remove leading and trailing pipes and split
trimmed := strings.Trim(strings.TrimSpace(line), "|")
parts := strings.Split(trimmed, "|")
result := make([]string, len(parts))
for i, part := range parts {
result[i] = strings.TrimSpace(part)
}
return result
}

func formatTableRow(columns []string, values []string) string {
var row strings.Builder
row.WriteString("|")

for i, value := range values {
row.WriteString(" ")
// Apply formatting based on column name or value patterns
formatted := formatCellValue(value)
row.WriteString(formatted)
row.WriteString(" |")
}

return row.String()
}

func formatCellValue(value string) string {
// Empty values become dashes
if len(strings.TrimSpace(value)) == 0 {
return "-"
}

return value
}