Skip to content

Summarize each tests run in one report and keep only release artifacts - #263

Merged
avsej merged 15 commits into
mainfrom
ci-reporting
Oct 9, 2026
Merged

avsej merged 15 commits into
mainfrom
ci-reporting

Conversation

@avsej

@avsej avsej commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Problem

  • Each test job publishes its own JUnit summary. The results of one run are spread over one job page per test configuration, and build failures appear only in the job list.
  • Nothing says whether a failed job hit a test failure, a broken build, a timeout or an infrastructure problem. A cancelled run gets no summary.
  • Every run keeps scripts-* and tests-* artifacts that only its own jobs read.
  • Every run keeps each per-Ruby binary gem, although its fat gem holds the same binary.
  • Failure artifacts list test/**/*.{log,xml}. upload-artifact does not expand braces, so the JUnit reports and test logs are never in them.
  • A hung test runs until the job timeout. The job is then cancelled, and no logs are uploaded.
  • The test log prints a test only when it ends, so a hung test never appears in it.
  • The server and CNG jobs run the tests with the checkout's lib/ on the load path. They test the Ruby sources, not the packaged gem.
  • Run scripts paste job outputs with ${{ }}. The gem versions come from lib/couchbase/version.rb, which a pull request controls.
  • Checkouts keep the job token in .git/config for the rest of the job.
  • Actions and the Alpine image are referenced by tags, which their owners can move. cbdinocluster is downloaded without a checksum.
  • Nothing audits the workflows for unsafe patterns, reports a pinned action that falls behind its latest release, or reports a build image that falls out of security support.
  • The musl gem is built on Alpine 3.18, which reached end of life on 2025-05-09.
  • RuboCop offenses appear only in the job log.
  • No CI job publishes test coverage.

Change

  • Jobs that need scripts or tests check out the commit. Test jobs remove lib/ so every suite loads the installed gem.

  • Test jobs upload their JUnit reports as junit-<job name> artifacts.

  • The test log prints each test's name and UTC start time when it starts, and its result when it ends. A hung test is the last, unfinished line.

  • Each Test step has its own timeout: 8 minutes in mock jobs, 20 minutes in cluster jobs. A hung test fails its step, so the logs are still uploaded.

  • The test jobs and the summary job have a job timeout above the sum of their step timeouts, so a step times out before its job does.

  • A summary job runs after all others, including on a cancelled run. bin/ci-summary.rb writes one step summary for the run:

    • each failed job with its cause, the step it stopped at and the error annotation;
    • a table of every test configuration, including jobs that did not run, left no report or a stale one, or reported no tests;
    • each failed test with the configurations it failed in and its output;
    • the slowest tests;
    • every job's result, with links;
    • the artifacts the run keeps.
  • The summary annotates the first ten failed tests at the line of the test file where they failed. These annotations replace the mikepenz/action-junit-report steps.

  • A tests-cleanup workflow, triggered by workflow_run, runs bin/ci-cleanup.rb. It deletes each per-Ruby gem once its fat gem exists, and the JUnit reports when the run succeeded. Reports stay after a failure, so "Re-run failed jobs" can still summarize the jobs it skips.

    • For workflow_run, GitHub takes the workflow and its checkout from the default branch. Pull request code therefore never runs with its actions: write token. It downloads no artifact from the run it cleans.
    • The script uses only the Ruby standard library and the runner's gh, so no gem is installed next to the token.
    • It refuses a run of another workflow or a run that has not completed. The repository and every id are validated before they reach an API path.
  • The summary job, which runs pull request code, has read permissions only.

  • Job outputs reach run scripts through environment variables rather than ${{ }}, and no checkout keeps the token in .git/config.

  • Causes, from the first failed or cancelled step and the job's annotations:

    Cause Decided by
    Test failures Test failed and the report has failed tests
    Test run aborted Test failed with no failed tests, or with no current report
    Gem install Install failed
    Build Precompile, Build …, Repackage or documentation failed
    Infrastructure any other step, or a job that never started
    Cancelled cancelled by a person, superseded by a newer run, or cancelled with no stated reason
    Timed out the job or a step ran out of time
    Runner lost the runner stopped responding

    A failed step keeps its cause when a later step times out or the run is cancelled.

  • The report survives a failed checkout, artifact download, Ruby setup or API outage. Unavailable sections are replaced by a note, and without a report the job results from needs are written. The summary job fails whenever its report is incomplete.

  • RuboCop also writes its offenses with the github formatter, so they appear as annotations.

  • Every action and reusable workflow from another repository is pinned to a commit, commented with the most specific tag or the branch it was resolved from. The Alpine image is pinned to its digest. The nightly FIT runs are the exception. fit-tests.yml follows fit-cli's ci release tag, its latest stable release. It is also the channel the nightly installs. bin/check-action-pins.rb runs in linters:

    • It fails on a uses: not pinned that way, or on a container image without a digest.
    • It fails on a tag comment that does not match its commit, or on a pin it could not check.
    • It warns about a newer release tag or a moved branch, once per action and version.
  • Pinned actions are updated to their latest releases, including major bumps of actions/setup-go (v7, in start-cng) and actions/checkout (v7, in validate-fit-performer).

  • create-cluster verifies the checksum of the cbdinocluster binary it downloads, installs it into /usr/local/bin, and writes neither GITHUB_PATH nor GITHUB_ENV.

  • The linters workflow runs zizmor and fails on findings of medium severity or above. The Bundler cache of the source job is an accepted finding: GitHub scopes caches by branch, so a pull request cannot write a cache that main or release* reads.

  • The musl gem is built on the oldest Alpine release with security support, now 3.21. An older musl than the build one may lack symbols the extension uses.

  • A coverage job on ubuntu-26.04 builds the extension from the checkout and runs the tests with lib/ on the load path, against the mock and against a cluster. SimpleCov merges both runs into one Cobertura report of lib/ and couchbase-opentelemetry/lib/, uploaded as the coverage artifact. The C++ extension is not measured.

  • bin/check-alpine-image.rb runs in linters. It warns when the pinned release is past end of life or is not the oldest supported one. It also warns when its tag points at another image.

Known gaps

  • Pinning covers this repository's uses: only. Actions that pinned actions fetch at run time are not covered: sdk-docker-build-action uses actions/checkout@v6.
  • The nightly FIT runs follow fit-cli's ci release, in a job that can assume a cloud role.
  • fit-cli fetches its installer from its own main branch.
  • tests-cleanup runs only from the default branch, so this pull request cannot exercise it. After merge it can be run by hand on a run id through workflow_dispatch.
  • The Alpine musl gem is built but not tested.

Motivation
----------
The source job uploads the scripts and the tests as two artifacts so
that later jobs can download them. They show up in every run beside
the gems and docs, although nothing needs them once the run ends.

The failure logs list test/**/*.{log,xml}. upload-artifact globs do
not expand braces, so the JUnit reports and test logs are missing
from every failure artifact.

The server and CNG test jobs check out the repository and then run
the tests with lib/ on the load path. Those jobs test the Ruby
sources against the installed gem's native extension, not the gem
as it is packaged.

Modifications
-------------
- Every job that needs scripts or tests checks out the commit. The
  repackage jobs use a sparse checkout of bin/jenkins.
- The Alpine job checks out before restoring ccache, because checkout
  empties the workspace that holds the cache.
- Test jobs remove lib/ before running, so every suite loads the
  installed gem.
- The Alpine job applies the build number that the source job
  computed, so build-gem.sh finds the source gem by version.
- mock_windows disables core.autocrlf, so test data keeps LF line
  endings as it had in the artifact.
- Failure logs list test/**/*.log and test/reports/.

Results
-------
A run no longer produces the scripts and tests artifacts. Failure
artifacts carry the JUnit reports. The server and CNG suites now
test the packaged gem.
Comment thread bin/ci-summary.rb Fixed

This comment was marked as resolved.

This comment was marked as resolved.

@avsej
avsej requested a balanced review from Copilot October 8, 2026 23:24

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI 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.

🔵 Needs a closer look

It changes privileged cleanup, artifact lifecycle, test execution, and supply-chain controls across the full CI matrix.

0 open findings

🧠 Review effort: Balanced

avsej added 2 commits October 9, 2026 11:56
Motivation
----------
No CI job publishes test coverage. The test jobs load the installed
gem, so SimpleCov, which tracks files under the project root, records
no coverage of lib/ in them.

Modifications
-------------
- A coverage job on ubuntu-26.04 builds the extension from the
  checkout with rake compile and runs the tests twice with lib/ on the
  load path: against the mock, then against a cbdinocluster cluster.
  The cluster run goes ahead after failed mock tests.
- SimpleCov names each run by COVERAGE_NAME and merges runs up to
  three hours old into one report. It counts only lib/ and
  couchbase-opentelemetry/lib/. The Gemfile requires simplecov 1.x,
  which has the skip filter this uses.
- The job fails unless coverage.xml was written and the result set
  holds both runs. It uploads coverage.xml (Cobertura) and the result
  set as the "coverage" artifact, and both JUnit reports as
  junit-coverage. The summary job waits for it.
- .github/actionlint.yaml lists ubuntu-26.04, which actionlint does
  not know yet.
- The summary treats a step named "Test <something>" as a test step:
  a failure there is a test failure, and a job with one must leave a
  report. A configuration whose job did not succeed is marked even
  when its report holds no failure. The coverage job counts as a test
  job, so it gets a row when skipped.
- The summary finds reports in subdirectories of a lone report
  artifact, and an uploaded report that was not downloaded is a
  report problem.
- A test that fails in both coverage runs lists the coverage
  configuration once.

Results
-------
Every run of the tests workflow keeps one coverage report of the Ruby
code of both gems, across the mock and a cluster. The C++ extension is
not measured.
Motivation
----------
The spec reporter prints a test's line when the test ends. When a run
hangs, the log shows the tests that finished, but not the one that was
running or since when.

Modifications
-------------
The test reporter prints each test's name and UTC start time when the
test starts, and its status and duration on the same line when it
ends. The failure summary at the end of the run is unchanged.

A CI log keeps a line only once it ends, so a watchdog thread ends the
line of a test still running after 60 seconds with "still running
after 60s".

Results
-------
A hung test shows in the log with its start time. A hung run without
that line stopped while holding the GVL, where no Ruby thread runs.
@avsej
avsej marked this pull request as ready for review October 9, 2026 19:08
@avsej
avsej merged commit 8af24da into main Oct 9, 2026
61 of 65 checks passed
@avsej
avsej deleted the ci-reporting branch October 9, 2026 20:24
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.

3 participants