open-code-review/examples
Lei Zhang 84be9d0413
feat(examples/gitlab): align GitLab CI review posting with the GitHub Action (#767)
* feat(gitlab-ci): align review posting with the GitHub Action behaviors

Port the production-hardening of scripts/github-actions/post-review-comments.js
into examples/gitlab_ci/post_review.py so the GitLab example matches the
GitHub Action's publication semantics.

Refactor the transport from a single post() callable into a Poster object
(GitLabPoster / DryRunPoster) exposing post_note / post_discussion / list_notes
/ update_note / list_discussions / get_mr_diffs, so reads (sticky upsert,
incremental history, idempotent reconciliation) and the 400-fallback diff
inventory can flow through it.

Pure-logic ports (Group A):
- category/severity badge [category · severity] on every inline discussion
  and fallback entry (byte-matching the CLI's buildBadge)
- publication policy routing findings to the summary by severity
  (OCR_ROUTE_SEVERITY_BELOW) and category (OCR_ROUTE_CATEGORIES), fail-open
  on unknown metadata; routed findings never enter the inline write path
- deterministic sort (path -> start_line -> end_line -> index) before posting
- detailed warning rendering (file (type): message bullets)
- backtick-safe fenced code blocks in the fallback Before/After; inline
  suggestion keeps the fixed suggestion:-0+0 fence so the Apply-suggestion
  button keeps working

Read-API features (Group B):
- sticky summary (default ON): the summary note is updated in place across
  runs via a <!-- ocr-summary --> marker (find-then-PUT), instead of
  accumulating one note per run; a pre-review anchor pins it above the
  discussions on the first run
- incremental mode (OCR_INCREMENTAL, default off): skip comments whose line
  range overlaps a prior bot discussion on the same path, using IoU above
  OCR_INCREMENTAL_OVERLAP_THRESHOLD (default 0.6); single-line vs multi-line
  never match; bot discussions detected by the <!-- ocr- marker
- idempotent retry: each inline discussion carries an invisible
  <!-- ocr-<pipeline>-<job>-<hex> --> id tag; on a 5xx/408/network failure
  (maybe-reached-server) the script GETs /discussions and skips the retry
  when the id is found (no duplicate); when the read API is unavailable it
  skips the retry and surfaces the reason rather than risk a duplicate

C group (minus batched createReview):
- 400 line-resolution fallback: when a discussion POST returns 400 with a
  position error, fetch GET /merge_requests/:iid/diffs, classify the comment
  (valid/invalid/unknown), and drop provably-out-of-diff findings to the
  summary instead of blindly retrying; unknown keeps the existing fallback
- wait-until-reset: when RateLimit-Remaining == 0, sleep until
  RateLimit-Reset (defensively handling both epoch-seconds and
  seconds-until-reset formats), ahead of Retry-After / backoff

D group (pipeline alignment):
- dotenv stats output (/tmp/ocr-stats.env) with OCR_COMMENTS_{TOTAL,INLINE,
  SUMMARY,ROUTED,SKIPPED,FAILED} and OCR_SUMMARY_URL, exposed via a
  reports: dotenv artifact
- artifacts (when: always, 1 week): /tmp/ocr-result.json and
  /tmp/ocr-stderr.log so a failed review is still inspectable
- severity fail gating (OCR_FAIL_ON_SEVERITY): non-zero exit when a comment
  severity is at/above the threshold; the summary note is still posted first

Tests: 57 -> 117 (stdlib unittest, no network, no wall-clock). New coverage
for badge/policy/sort/warnings/safe-fence, sticky upsert (cold start + reuse),
incremental IoU (single/multi/cross-type/read-failure), idempotent
reconciliation (posted/unavailable/not-posted/400-no-reconcile), 400
classification (valid/invalid/unknown/truncated patch), wait-until-reset
(seconds/epoch/non-zero-remaining), stats file, severity gating.

The only behavior change for existing users: the summary note now updates in
place by default (sticky_summary default true, matching the GitHub Action);
incremental/routing/gating default to off / no-op.

* fix(gitlab-ci): harden OCR_INCREMENTAL_OVERLAP_THRESHOLD parsing, drop dead regex

- build_config used a bare float() on OCR_INCREMENTAL_OVERLAP_THRESHOLD,
  which raised an unhandled ValueError and crashed the script when a user
  set a non-numeric value. Reuse resolve_threshold, which already handles
  non-numeric / out-of-range values and falls back to the default.
- Remove the unused _COMMENT_ID_RE module-level regex; the idempotency
  check in _is_comment_posted uses a plain substring match, so the regex
  was dead code.

Tests: +2 covering non-numeric and out-of-range env values (117 -> 119).

* fix(gitlab-ci): strip control chars in route_comment, drop dead badge init

- route_comment read category/severity via str(...).strip().lower() without
  stripping C0/C1 control characters, while build_badge uses sanitize_metadata
  for the same fields. If the LLM emits embedded control chars (e.g. "bug\r"),
  the badge rendered [bug · high] but routing classified it as "unknown" and
  skipped policy routing. Reuse sanitize_metadata to stay consistent.
- format_comment_fallback had a duplicate md/badge initialization block
  immediately overwritten by an identical block, a merge artifact. Removed
  the redundant first block.

* fix(gitlab-ci): classify line_code 400s, match MR version to OCR-reviewed SHAs

- LINE_RESOLUTION_PATTERNS missed the `line_code` substring, so GitLab's
  `:line_code=>["can't be blank", "must be a valid line code"]` 400 was never
  classified as a line-resolution failure. The per-comment diff-inventory
  fallback never fired, and the raw error JSON leaked into the summary note
  as `⚠️ Could not be posted inline: {"message":"400 Bad request - Note ..."}`
  instead of a friendly "out of diff" / "line resolution failure" reason. Add
  `line_code` to the pattern list.
- fetch_diff_refs took `versions[0]` blindly. After a force-push or follow-up
  commit, that version's head_commit_sha no longer matches the diff OCR
  actually reviewed, so the position's base_sha/head_sha describe the wrong
  diff and every inline position fails to resolve on GitLab's side — the root
  cause of the line_code 400 above. Now read resolved_head/resolved_base from
  the OCR result manifest and pick the matching MR version, falling back to
  newest-by-created_at with a warning when no version matches.
- Migrate artifact paths /tmp -> .ocr (GitLab Runner refuses to upload
  artifacts outside the build directory) and drop the now-redundant temporary
  sticky-summary diagnostic block.
- Add 7 tests: line_code pattern positive, version matching by head /
  head+base / no-match-fallback / no-expected-shas-sorts-newest, and
  _extract_expected_shas with/without manifest.

* feat(gitlab-ci): close GitHub Action parity gaps (config, flags, exit gate, pacing, per-run summary)

Brings the GitLab CI example to feature parity with the GitHub Action
(action.yml + scripts/github-actions/post-review-comments.js).

- .gitlab-ci.yml: expose action.yml inputs as CI/CD variables.
  - OCR_VERSION pins the npm spec (default latest) for reproducible reviews.
  - Optional LLM config written via `ocr config set` only when the CI var is
    non-empty (no clobbering): OCR_LANGUAGE, OCR_LLM_AUTH_HEADER,
    OCR_LLM_EXTRA_HEADERS. OCR_LLM_TIMEOUT is read natively by OCR from env.
  - Review flags threaded env→flag: OCR_REVIEW_CONCURRENCY, OCR_BACKGROUND,
    OCR_RULE.
  - OCR exit-code gate: drop `|| true`, capture OCR_EXIT_CODE, run
    post_review.py unconditionally (so stderr still posts to MR), then exit
    non-zero on OCR failure — mirrors the Action's "Fail job on OCR error".
  - Document `llm.use_anthropic` as a hardcoded (not CI-configurable) value
    under a separate "Not a CI/CD variable" header; the false listing as a
    required CI var was misleading.

- post_review.py: posting-logic parity.
  - Read-API pacing: `_read_with_pacing` wraps list_notes/list_discussions/
    get_mr_diffs with read_success_delay (0.5s default) and
    read_low_remaining_spacing (5s default) when RateLimit-Remaining is
    at/below rate_limit_threshold — mirrors readWithPacing so a large MR
    does not hammer the read API.
  - Non-sticky per-run summary matching: `summary_tag_for(run_tag)` /
    `wrap_summary_body` embed both SUMMARY_MARKER and a per-run
    `<!-- ocr-summary-run:TAG -->`; find_summary_note/upsert_summary/
    ensure_summary_anchor/finalize_summary take a `tag` param. The pre-review
    anchor now runs in non-sticky mode too (was sticky-only), so non-sticky
    shows the " Posting…" live state and finalize updates the same note
    instead of creating a duplicate.
  - build_config exposes read_success_delay / read_low_remaining_spacing.

- tests (+12, 142 total, 0.01s):
  - ReadPacingTest: success-delay pacing, low-quota long spacing, no pacing
    sleep on failed read.
  - NonStickySummaryTest: cold-start anchor→update same note; same-run reuse;
    different-run not reused.
  - SummaryTagPureTest: tag format, wrap embeds both markers, newest-first
    find by tag/marker, None on no match.
  - BuildConfigTest: read-pacing defaults & overrides.
  - Recorder.final_summary_body / summary_call_count helpers adapt existing
    assertions to the two-phase anchor→finalize flow.
  - MainAuthHeaderTest: mock urlopen with canned responses — was making real
    DNS lookups to gitlab.example, amplified 3x by the new always-on anchor
    (13.7s → 0.01s).

- README.md: document the 9 new CI vars in the variables table, add
  read-pacing vars to the retry/delay table, convert the version/rules/
  concurrency/background recipes from "edit the YAML" to "set this CI var".

* fix(gitlab-ci): if-guard optional vars, capture post_review exit code, split diff-refs reason

- Replace [ -n "$VAR" ] && cmd with if-guards so FF_ENABLE_BASH_EXIT_CODE_CHECK
  cannot abort on an empty optional variable (|| true would mask real failures)
- Capture post_review.py exit code (POST_EXIT_CODE) so the OCR_EXIT_CODE gate
  runs regardless of severity-gate or posting failures
- Split 'not path or not end_line or not diff_refs' into two checks so
  diff-refs-unavailable comments get an accurate reason in the summary

* fix(gitlab-ci): default OCR/POST exit codes to guard against unset gate vars

Without a default, an unset OCR_EXIT_CODE/POST_EXIT_CODE makes the gate
`[ "" != "0" ]` evaluate true and `exit ""` fail with
'numeric argument required'. Default both to 0 so the gate is always safe.
2026-08-07 15:58:03 +08:00
..
bitbucket_pipelines fix(examples): preserve Bitbucket OCR diagnostics (#486) 2026-07-24 21:03:52 +08:00
codeup_ci Add CodeUp CI integration (#570) 2026-07-29 22:37:29 +08:00
gerrit_ci feat(examples): add Gerrit CI integration example (Jenkins + Gerrit Trigger) (#401) 2026-07-21 13:25:30 +08:00
gitflic_ci docs(examples): OCR_LLM_MODEL is required, not optional (#431) 2026-07-22 09:14:39 +08:00
github_actions feat(action): extract reusable composite PR-review GitHub Action (#337) 2026-07-09 19:08:19 +08:00
gitlab_ci feat(examples/gitlab): align GitLab CI review posting with the GitHub Action (#767) 2026-08-07 15:58:03 +08:00
README.md docs(examples): add Bitbucket Pipelines CI integration example (#483) 2026-07-24 08:29:18 +08:00

CI/CD Integration Examples

This directory contains examples for integrating OpenCodeReview (OCR) into various CI/CD pipelines.

Contents

Each subdirectory contains its own README with detailed setup instructions.