diff --git a/README.md b/README.md index 848f70b..59b4e57 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,56 @@ Create/update GitHub PR comments through an executable (for e.g. in ci/cd script |`-owner=`|GitHub repository owner|true| |`-repo=`|GitHub repository name|true| |`-body=`|The contents of the comment|true| -|`-commentId=`|Update a certain comment by its ID|| -|`-bodyIncludes=`|Update a certain comment by its content|| +|`-comment-id=`|Update a certain comment by its ID|| +|`-body-includes=`|Update a certain comment by its content|| +|`-append`|Enable append mode for table-based updates|| +|`-row-id=`|Unique identifier for the row (used with `-append`)|| +|`-columns=`|Comma-separated column names (used with `-append`)|| +|`-values=`|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: Package Publishing Status +> Packages published from this PR" \ + -body-includes="" \ + -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: Package Publishing Status +> Packages published from this PR" \ + -body-includes="" \ + -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 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 | +``` diff --git a/main.go b/main.go index eaa1b06..118468a 100644 --- a/main.go +++ b/main.go @@ -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() @@ -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) + } } } @@ -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 +} +