Skip to content

feat(publish): add ocr publish gitflic to post reviews to GitFlic MRs#138

Open
revenant20 wants to merge 3 commits into
alibaba:mainfrom
revenant20:feat/publish-gitflic
Open

feat(publish): add ocr publish gitflic to post reviews to GitFlic MRs#138
revenant20 wants to merge 3 commits into
alibaba:mainfrom
revenant20:feat/publish-gitflic

Conversation

@revenant20

Copy link
Copy Markdown
Contributor

Implements #102.

What

First-class support for publishing OCR review results to GitFlic merge
requests, mirroring the existing GitHub Actions / GitLab CI examples (#14):

  • ocr publish gitflic — reads ocr review --format json output and
    posts it onto an MR as inline discussions, with a fallback note for
    comments that can't be placed inline and a summary note. Auto-detects the
    MR context from GitFlic's predefined CI variables (CI_PROJECT_NAMESPACE,
    CI_PROJECT_NAME, CI_MERGE_REQUEST_LOCAL_ID, CI_COMMIT_SHA); token via
    GITFLIC_TOKEN; --dry-run for offline debugging.
  • examples/gitflic_ci/ — a merge-request pipeline demo, same shape as
    examples/gitlab_ci/.

Why a native command (rather than CI glue)

For GitHub/GitLab the posting logic lives in the example YAML, which works
well. GitFlic's Discussions API differs in one important way: an inline code
comment requires all four of newLine, oldLine, newPath, oldPath
if any is missing it silently falls back to a general (non-inline) comment.
ocr review reports positions on the new-file side only, so the old-side
line has to be computed from the diff hunks.

A CI script would have to reimplement unified-diff parsing to do that. OCR
already has this machinery (internal/diff.ParseHunks + the merge-base diff
provider used by the review itself), so doing the mapping inside OCR keeps the
line numbers guaranteed-consistent with the review that produced them — in the
spirit of the project's deterministic-engineering design.

Implementation

  • internal/publish/gitflic/ — stdlib-only REST client (client.go),
    new→old line mapping on top of internal/diff (linemap.go), and the
    publisher with inline/fallback/summary logic (publisher.go).
  • cmd/opencodereview/publish_cmd.go + registration in main.go.

Testing

  • Unit tests for the line mapping and the full posting flow (httptest).
  • Opt-in e2e (make test-e2e-gitflic, build tag gitflic_e2e, skipped
    without GITFLIC_TOKEN) against a live GitFlic instance: provisions a
    throwaway project, seeds a modified line / inserted line / new file, opens
    an MR, publishes a synthetic review and asserts the discussions GitFlic
    actually stored — inline positions (incl. old-line anchors), the fallback
    note and the summary note. Regular make test is unaffected.

Note

This puts review-posting in the core CLI, whereas for GitHub/GitLab it has
lived in the example scripts. I went this route for the diff-parsing reason
above, but I'm happy to adapt if you'd prefer to keep posting out of the core.
Discussion in #102.

Add native publishing of review results to GitFlic merge requests:

- internal/publish/gitflic: minimal REST client (discussions/create),
  new-to-old line mapping built on diff.ParseHunks (GitFlic requires all
  four of newLine/oldLine/newPath/oldPath for an inline code comment),
  and a publisher that posts inline discussions, a fallback note for
  comments that cannot be placed inline, and a summary note.
- cmd/opencodereview: 'ocr publish gitflic' subcommand; flags default to
  the predefined GitFlic CI variables (CI_PROJECT_NAMESPACE,
  CI_PROJECT_NAME, CI_MERGE_REQUEST_LOCAL_ID, CI_COMMIT_SHA), token via
  GITFLIC_TOKEN, --dry-run for offline debugging.
- examples/gitflic_ci: merge request pipeline demo using the new command.
Gated behind the gitflic_e2e build tag (regular 'make test' is unaffected)
and skipped when GITFLIC_TOKEN is unset. The test provisions a throwaway
project over REST, seeds master/feature branches with a modified line, an
inserted line and a new file, opens a merge request, publishes a synthetic
review result, and asserts the discussions GitFlic actually stored:
inline positions (including the old-line anchors), the fallback note and
the summary note. The project is deleted on cleanup.

Defaults target a local GitFlic CE (localhost:8080); the instance, git
credentials and API URL are configurable via GITFLIC_E2E_* variables.
Run with 'make test-e2e-gitflic'.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 OpenCodeReview found 5 issue(s) in this PR.

  • ✅ 5 posted as inline comment(s)
  • 📝 0 posted as summary

- |
ocr config set llm.url $OCR_LLM_URL
ocr config set llm.auth_token $OCR_LLM_AUTH_TOKEN
ocr config set llm.model $OCR_LLM_MODEL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: OCR_LLM_MODEL is documented as an optional CI/CD variable (line 17), but this command runs unconditionally. When the variable is not set, the shell expands $OCR_LLM_MODEL to an empty string, causing ocr config set llm.model "" to write an empty model into the config. This will then fail endpoint resolution because the resolver requires a non-empty model value (ep.Model != "" in resolver.go).

Consider guarding this command so it only runs when the variable is set, for example:

    - |
      if [ -n "$OCR_LLM_MODEL" ]; then
        ocr config set llm.model $OCR_LLM_MODEL
      fi

The same concern applies to llm.extra_body below if it were ever made optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Kept this unconditional to match the gitlab/github examples (they set llm.model the same way). If OCR_LLM_MODEL is unset the unquoted var just expands to nothing, so config set errors out instead of writing an empty model — and a model is required everywhere anyway, so a guard would only move the failure to ocr review.

Comment thread internal/publish/gitflic/client.go Outdated
Comment on lines +46 to +47
NewLine int `json:"newLine,omitempty"`
OldLine int `json:"oldLine,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The omitempty tag on int fields means that when NewLine or OldLine is 0, the field will be completely omitted from the JSON payload. Per the struct's own documentation, if any of these four fields is missing, GitFlic silently creates a general comment instead of an inline one.

While the current internal usage in publisher.go guards against this (filtering out EndLine <= 0 and using clampLine to ensure oldLine >= 1), the Discussion struct is exported as part of the public API. Any external consumer who sets NewLine: 0 or OldLine: 0 would get silent, incorrect behavior with no error.

Consider using pointer types (*int) with omitempty so that an explicit zero value is serialized, while nil still omits the field. Alternatively, remove omitempty from the int fields since they are always expected to be present for inline comments.

Suggestion:

Suggested change
NewLine int `json:"newLine,omitempty"`
OldLine int `json:"oldLine,omitempty"`
NewLine *int `json:"newLine,omitempty"`
OldLine *int `json:"oldLine,omitempty"`

Comment thread internal/publish/gitflic/publisher.go Outdated
var failed []model.LlmComment
for _, c := range result.Comments {
d, ok := byPath[c.Path]
if !ok || d.IsBinary || c.EndLine <= 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Deleted files are not excluded from inline comment posting. When d.IsDeleted is true, the file no longer exists in the new version, so NewPath may be /dev/null or empty and there are no new-side lines to anchor to. This will likely cause the GitFlic API to reject the discussion or create a malformed one. Add d.IsDeleted to the guard condition alongside d.IsBinary.

Suggestion:

Suggested change
if !ok || d.IsBinary || c.EndLine <= 0 {
if !ok || d.IsBinary || d.IsDeleted || c.EndLine <= 0 {

Comment thread internal/publish/gitflic/publisher.go Outdated
// GitFlic has no old side for a new file; anchor to the new path.
oldPath = d.NewPath
} else {
oldLine = oldLineFor(diff.ParseHunks(d.Diff), c.EndLine)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: diff.ParseHunks(d.Diff) is called for every comment inside the loop. When multiple comments reference the same file, the same diff text is re-parsed each time. Consider pre-computing a map of parsed hunks keyed by file path before the loop to avoid redundant parsing.

Comment on lines +103 to +107
d, ok := byPath[c.Path]
if !ok || d.IsBinary || c.EndLine <= 0 {
failed = append(failed, c)
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observability: When a comment's c.Path does not match any entry in the diffs map (!ok), the comment is silently added to the failed list without any log message explaining why. This makes debugging path mismatches difficult. Consider adding a diagnostic log (similar to the one at line 126) when the path lookup fails.

Suggestion:

Suggested change
d, ok := byPath[c.Path]
if !ok || d.IsBinary || c.EndLine <= 0 {
failed = append(failed, c)
continue
}
d, ok := byPath[c.Path]
if !ok {
p.logf("no diff found for path %s, falling back to summary", c.Path)
failed = append(failed, c)
continue
}
if d.IsBinary || d.IsDeleted || c.EndLine <= 0 {
failed = append(failed, c)
continue
}

The PR's own OpenCodeReview run flagged a few items on the gitflic
publisher; apply the ones that improve clarity/robustness without
changing behavior for real inputs:

- client.go: make Discussion.NewLine/OldLine *int so the nullable wire
  fields are explicit (absent vs zero) rather than relying on omitempty
  over a plain int
- publisher.go: skip deleted files in the inline guard (also covered by
  the no-diff path, but makes the intent explicit)
- publisher.go: parse each file's diff hunks once instead of per comment
- publisher.go: log when a comment's path has no diff and is folded into
  the summary note
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant