Measurement

This repository is a CI lab. It is not a library people depend on. There is no published tag and no install coordinate. The libs ship group publishes models and lib to this repo's GitHub Packages. That coordinate is those two modules.

Report.of measures one completed GitHub Actions run. It asks gh for the run, its jobs, those jobs' logs, and the repo's Actions cache, then returns one RunReport. The program is lab/Measure.scala, a scala-cli script. It is not an sbt project.

Running it

scala-cli run lab/Measure.scala -- <run-id>
scala-cli run lab/Measure.scala -- <run-id> --repo owner/name

The default repo is early-effect/zipx-ci-lab. gh must already be authenticated. Stdout is the report as pretty JSON. A MeasureError goes to stderr and the process exits non-zero.

MeasureError is bad arguments, gh could not be started, gh exited non-zero, JSON the decoder does not know, a run whose status is not completed, or a jobs page larger than the tool reads. It does not retry, and it does not follow a second page.

Report.of

This is the measurement. The footer link is that definition at this commit.

lab.measure.Report.of
def of(run: RunId): ZIO[Gh, MeasureError, RunReport] =
    ZIO.serviceWithZIO[Gh] { gh =>
      for
        apiRun <- gh.json[ApiRun](s"actions/runs/${run.value}")
        _      <- ZIO.fail(MeasureError.NotCompleted(apiRun.id, apiRun.status)).when(apiRun.status != "completed")
        page   <- gh.json[ApiJobs](s"actions/runs/${run.value}/jobs?per_page=100&filter=latest")
        _      <- ZIO.fail(MeasureError.TooManyJobs(page.totalCount)).when(page.totalCount > page.jobs.size)
        ran = page.jobs.filter(Jobs.ran)
        facts <- ZIO
          .foreachPar(ran)(job => gh.jobLog(job.id).map(log => job -> LogFacts.from(log)))
          .withParallelism(8)
        jobs = facts.map((job, fact) => Jobs.report(job, fact))
        caches <- gh.json[ApiCaches]("actions/caches?per_page=100")
      yield RunReport(
        runId = apiRun.id,
        event = apiRun.event,
        branch = apiRun.headBranch,
        sha = apiRun.headSha,
        conclusion = apiRun.conclusion,
        // A run held by its concurrency group has `run_started_at` set already; its jobs appear when it is released.
        pendingSeconds = page.jobs
          .map(_.createdAt)
          .minOption
          .fold(0L)(firstJob => JDuration.between(apiRun.createdAt, firstJob).toSeconds),
        affected = facts.collectFirst { case (job, fact) if job.name == "affected" => fact.printedModules }.flatten,
        jobsRan = jobs.map(_.name),
        jobsWorked = jobs.filter(_.didWork).map(_.name),
        cacheSaves = jobs.map(_.savedKeys.size).sum,
        jobs = jobs,
        cache = CacheReport(
          entries = caches.totalCount,
          totalBytes = caches.actionsCaches.map(_.sizeInBytes).sum,
          mainPresent = caches.actionsCaches.exists(c => c.ref == "refs/heads/main" && LogFacts.isBuildCache(c.key)),
          build = caches.actionsCaches
            .filter(c => LogFacts.isBuildCache(c.key))
            .map(c => CacheEntry(c.ref, c.key, c.sizeInBytes / (1024 * 1024))),
        ),
      )
    }

View source

Which jobs count

Jobs come from actions/runs/<id>/jobs?per_page=100&filter=latest. If total_count is greater than the number of jobs in that page, the tool stops with TooManyJobs. One hundred jobs is the whole page it reads.

A job ran when its conclusion is not skipped and it has at least one step.

A job did work when one of its own steps concluded success or failure. These steps do not count: Set up job, Complete job, zipx sbt setup, Log in to GHCR, any name that starts with Post , and any name that starts with Run actions/. A missing job conclusion is stored as none.

What a log contributes

Each job that ran has its log fetched, eight at a time. The request passes --allow-escape-sequences because the log contains ANSI colour. Before matching, ANSI CSI sequences and a leading timestamp ending in Z are stripped.

A build-cache key contains -sbt- and does not contain -sbt-runner-. The setup-sbt launcher cache is not the build.

  • restoredKey is the first Cache restored from key: whose key is a build cache.

  • cacheMiss is true when some Cache not found for input keys: names a build cache.

  • savedKeys is every Cache saved with key: that is a build cache.

  • compiled comes from [info] [module] compiling N ... to DIR. A directory ending in test-classes is recorded as module/test. Any other directory is module. Names ending in -build are left out of this list. If any such name was compiled, metaBuildCompiled is true. The list is distinct and sorted.

  • suites is unindented ZIO Test lines + label, trimmed, distinct, and sorted. Indented lines are tests under a suite, not suites.

  • The first line that is [] or starts with [" and parses as a JSON array of strings is the module list that job printed. RunReport.affected is that list from the job named affected. It is absent when that job printed none.

Pending seconds

pendingSeconds is the whole seconds between the run's created_at and the earliest created_at among the jobs on the page, skipped jobs included. It is 0 when the page has no jobs. A run held by its concurrency group already has created_at set. Its jobs appear when the group releases it, so the gap is how long it sat before the first job existed.

The repo cache

The cache listing is actions/caches?per_page=100 for the repository, not for the run.

  • entries is the API's total_count. That can be larger than the page returned.

  • totalBytes is the sum of size_in_bytes on the returned page only.

  • mainPresent is true when a returned entry has ref refs/heads/main and a build-cache key.

  • build lists those build-cache entries. megabytes is size_in_bytes / (1024 * 1024), integer division, so a remainder under 1 MiB is dropped.

The report

FieldWhat it is
runId, event, branch, sha, conclusionThe run. branch and conclusion are absent when the API omits them.
pendingSecondsSeconds from run creation to the first job on the page, or 0.
affectedThe JSON array printed by the job named affected.
jobsRanNames of jobs that ran, in API order.
jobsWorkedNames of those that did work, in API order.
cacheSavesHow many build-cache keys the ran jobs saved.
jobsOne object per ran job: conclusion, whether it did work, restore, miss, saves, compiled modules, meta-build compile, suites.
cacheThe repo cache fields above.