README
¶
ghaperf
ghaperf analyzes the performance of GitHub Actions workflows using GitHub API and raw job logs. Unlike other tools, it can detect bottlenecks inside composite actions by parsing job logs and extracting step-level timing data.
Why ghaperf?
- Existing tools can't detect bottlenecks inside steps. ghaperf solves this by analyzing actions logs.
- Many existing tools need metrics storage, so the setup is bothersome and it costs 💸. ghaperf doesn't need any storage, so it's easy to use and free
Key Features
- Deep visibility into composite actions - Detect bottlenecks inside composite actions that other tools miss
- No infrastructure needed - Just a CLI tool, no backend or metrics storage required
- Multiple analysis modes - Analyze workflows, workflow runs, or individual jobs
- Markdown reports - Generate shareable performance reports
- Intelligent caching - Cache GitHub API responses for completed runs to speed up analysis
- Flexible filtering - Filter and normalize job names using configuration files
Quick Start
export GITHUB_TOKEN=xxx
# Analyze a workflow across multiple runs
ghaperf --repo aquaproj/aqua-registry --workflow test.yaml --count 10 --threshold 2s
Example Report
Example Report Output
This report was generated by ghaperf.
| ghaperf version | v0.1.1 |
| Created At | 2025-11-02T21:03:34+09:00 |
| Threshold | 2s |
| Repository | aquaproj/aqua-registry |
| The Number of Workflow Runs | 10 |
| Workflow Name | test.yaml |
Job: test / test / test (macos-13)
| Average Job Duration | 1m15s (11m11s/9) |
| Slowest Jobs | 8m25s, 24s, 23s |
Slow steps
- 1m4s (8m31s/8): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 1m36s (7m58s/5): Run aqua i --test
- 16s (16s/1): Run rustup update
- 6s (48s/8): Set up job
- 6s (49s/8): GITHUB_TOKEN Permissions
- 4s (30s/8): Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- 4s (29s/8): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
- 3s (21s/8): Run if [ "${SKIP_INSTALL_AQUA:-}" = true ] && command -v aqua >/dev/null; then
Job: test / test / test (windows-latest, arm64)
| Average Job Duration | 1m6s (10m59s/10) |
| Slowest Jobs | 6m19s, 41s, 39s |
Slow steps
- 36s (5m57s/10): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 5m4s (5m4s/1): Run aqua i --test
- 18s (18s/1): Run rustup update
- 2s (17s/8): Run aqua exec -- ci-info run | sed -E "s/^export //" >> "$GITHUB_ENV"
- 13s (2m10s/10): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
- 11s (1m25s/8): Run $(if($env:AQUA_ROOT_DIR) {echo $env:AQUA_ROOT_DIR} else {echo "$HOME/AppData/Local/aquaproj-aqua/bin"}) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- 4s (36s/8): Run if [ "${SKIP_INSTALL_AQUA:-}" = true ] && command -v aqua >/dev/null; then
- 8s (1m23s/10): Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- 2s (23s/10): Setting up auth
- 4s (38s/10): Set up job
- 4s (37s/10): GITHUB_TOKEN Permissions
- 2s (23s/10): Post Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- 3s (18s/7): Run aqua i --test
- 6s (6s/1): Run $(if($env:AQUA_ROOT_DIR) {echo $env:AQUA_ROOT_DIR} else {echo "$HOME/AppData/Local/aquaproj-aqua/bin"}) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- 5s (5s/1): Checking out the ref
Job: test / test / test (windows-latest)
| Average Job Duration | 1m4s (10m36s/10) |
| Slowest Jobs | 6m0s, 37s, 36s |
Slow steps
- 34s (5m43s/10): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 4m44s (4m44s/1): Run aqua i --test
- 20s (20s/1): Run rustup update
- 2s (17s/8): Run aqua exec -- ci-info run | sed -E "s/^export //" >> "$GITHUB_ENV"
- 2s (13s/6): Run if grep '- import:' aqua/test.yaml | sed 's| - import: ../(.*)/pkg.yaml|\1/registry.yaml|' | xargs cat | grep "type: cargo" > /dev/null 2>&1; then
- 13s (2m9s/10): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
- 10s (1m22s/8): Run $(if($env:AQUA_ROOT_DIR) {echo $env:AQUA_ROOT_DIR} else {echo "$HOME/AppData/Local/aquaproj-aqua/bin"}) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- 5s (40s/8): Run if [ "${SKIP_INSTALL_AQUA:-}" = true ] && command -v aqua >/dev/null; then
- 7s (1m12s/10): Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- 2s (22s/10): Setting up auth
- 4s (39s/10): Set up job
- 4s (39s/10): GITHUB_TOKEN Permissions
- 2s (23s/10): Post Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- 3s (18s/7): Run aqua i --test
- 5s (5s/1): Run $(if($env:AQUA_ROOT_DIR) {echo $env:AQUA_ROOT_DIR} else {echo "$HOME/AppData/Local/aquaproj-aqua/bin"}) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- 5s (5s/1): Checking out the ref
Job: test / test / test (macos-14)
| Average Job Duration | 34s (5m40s/10) |
| Slowest Jobs | 2m52s, 26s, 21s |
Slow steps
- 19s (3m7s/10): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 39s (2m37s/4): Run aqua i --test
- 10s (10s/1): Run rustup update
- 6s (55s/10): Set up job
- 5s (55s/10): GITHUB_TOKEN Permissions
- 3s (30s/10): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
- 2s (24s/10): Run if [ "${SKIP_INSTALL_AQUA:-}" = true ] && command -v aqua >/dev/null; then
- 2s (21s/10): Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
Job: test / test / test (ubuntu-24.04)
| Average Job Duration | 28s (4m42s/10) |
| Slowest Jobs | 2m46s, 18s, 15s |
Slow steps
- 19s (3m5s/10): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 32s (2m40s/5): Run aqua i --test
- 9s (9s/1): Run rustup update
- 3s (31s/10): Set up job
- 3s (32s/10): GITHUB_TOKEN Permissions
Job: test / test / test (ubuntu-24.04-arm)
| Average Job Duration | 27s (4m32s/10) |
| Slowest Jobs | 2m30s, 16s, 16s |
Slow steps
- 16s (2m43s/10): Run aquaproj/registry-action/test@68f10339de561d67f9acea40b91dc36aa5011ea8
- 22s (2m13s/6): Run aqua i --test
- 7s (7s/1): Run rustup update
- 3s (31s/10): Set up job
- 3s (31s/10): GITHUB_TOKEN Permissions
- 2s (24s/10): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
Job: test / lintnet / lintnet
| Average Job Duration | 15s (2m34s/10) |
| Slowest Jobs | 19s, 18s, 18s |
Slow steps
- 6s (59s/10): Run lintnet lint
- 8s (1m5s/8): Run lintnet lint
- 3s (30s/10): Run aquaproj/aqua-installer@ea518c135a02fc11ff8024364510c181a5c6b342
- 2s (22s/10): Post Run actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
Job: test / ci-info / ci-info
| Average Job Duration | 9s (1m33s/10) |
| Slowest Jobs | 12s, 12s, 10s |
Slow steps
- 2s (22s/10): Run suzuki-shunsuke/ci-info-action/store@ceeb10dd50cd632db31e7eccf92cbbb6856f3191
Job: test / check-files / check-files
| Average Job Duration | 5s (54s/10) |
| Slowest Jobs | 7s, 6s, 6s |
The job has no slow steps
Job: test / path-filter
| Average Job Duration | 4s (40s/10) |
| Slowest Jobs | 6s, 5s, 5s |
The job has no slow steps
Job: status-check
| Average Job Duration | 4s (7s/2) |
| Slowest Jobs | 3s, 4s |
The job has no slow steps
Installation
See INSTALL.md for detailed installation instructions.
Usage
For complete usage documentation, see USAGE.md.
Prerequisites
A GitHub access token is required to download action logs. See GitHub Access Token for details.
export GITHUB_TOKEN=xxx
Analysis Modes
- Analyze multiple workflow runs (recommended for performance insights)
ghaperf \
--repo suzuki-shunsuke/ghaperf \
--workflow test.yaml \
--count 10 \
--threshold 2s
[!NOTE] Higher
--countvalues provide better insights but take longer to process.
- Analyze a single workflow run
ghaperf \
--repo "suzuki-shunsuke/tfaction" \
--run-id "<workflow run id>"
- Analyze a specific job
ghaperf \
--repo suzuki-shunsuke/tfaction \
--job-id "<workflow job id>"
- Analyze a local log file
ghaperf --log-file path/to/job.log
See the example log file for format reference.
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
GHAPERF_LOG_LEVEL |
Log level: debug, info, warn, error |
info |
GHAPERF_GITHUB_TOKEN |
GitHub access token | - |
GITHUB_TOKEN |
GitHub access token (alternative) | - |
GHAPERF_GHTKN |
Enable ghtkn integration | false |
GHAPERF_THRESHOLD |
Default threshold for slow steps/log groups | 30s |
GitHub Access Token
A GitHub access token is required to fetch workflow runs, jobs, and logs via the GitHub API.
[!WARNING] Even if repositories are public, an access token is required. This is a known issue of GitHub. https://github.com/orgs/community/discussions/24742
{ "message": "Must have admin rights to Repository.", "documentation_url": "https://docs.github.com/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run", "status": "403" }The message says
Must have admin rights to Repository., but actually we confirmed the message is wrong because non admin user can download logs.
Required Permissions:
- Public repositories
- Permissions to access action logs is required, but they are ambiguous due to the bug of GitHub. And there are sevel types of GitHub Access tokens, so it's a bit difficult to describe accurately
- Private repositories:
Actions: Readpermission
Setup:
# Option 1: Use GITHUB_TOKEN
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxx
# Option 2: Use GHAPERF_GITHUB_TOKEN
export GHAPERF_GITHUB_TOKEN=ghp_xxxxxxxxxxxxx
# Option 3: Use ghtkn integration
export GHAPERF_GHTKN=true
Threshold
ghaperf reports steps and log groups that exceed the specified threshold:
Threshold Configuration:
- Default:
30s - Set via
--thresholdflag orGHAPERF_THRESHOLDenvironment variable - Format: Go duration (e.g.,
1s,2m30s)
Configuration File
Use configuration files to filter jobs by job name and normalize job names. All settings are optional.
Scaffold the configuration file:
ghaperf --init # Generate ghaperf.yaml
Pass the configuration file:
ghperf --config <configuration file path> ...
e.g.
# Only analyze jobs matching these regular expressions
job_names:
- "test / test / test .*"
# Exclude jobs matching these regular expressions
# excluded_job_names:
# - "test / test / test .*"
# Normalize matrix job names for aggregation
job_name_mappings:
"test / test / test .*": "test / test / test"
Available Fields:
job_names: List of regular expressions - only matching jobs are analyzedexcluded_job_names: List of regular expressions - matching jobs are excludedjob_name_mappings: Map of regular expressions to normalized names for matrix jobs and old job names
JSON Schema and Validation:
The configuration schema is available at json-schema/ghaperf.json.
Validate your configuration with ajv-cli:
ajv --spec=draft2020 -s json-schema/ghaperf.json -d ghaperf.yaml
IDE Support:
Enable auto-completion in your editor by adding this to your config file:
Latest version:
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghaperf/main/json-schema/ghaperf.json
Or pin to a specific version:
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghaperf/v0.0.3/json-schema/ghaperf.json
Advanced Topics
Caching
ghaperf automatically caches API responses for completed workflow runs and jobs to improve performance on subsequent analyses.
Cache location: ${XDG_CACHE_HOME:-${HOME}/.cache}/ghaperf/
This speeds up repeated analyses and reduces API calls.
Group log lines
ref. Group log lines
By grouping log lines properly, you can analyze the performance using ghaperf more deeply. Especially, if a specific step (run step, JavaScript Action, or Docker Action) is doing various things and slow, you can analyze the bottlenecks inside the step by grouping log lines properly.
Important Notes
-
Log availability timing: Job logs must be fully processed by GitHub. If a job just completed, the API may not have logs ready yet. Wait a few moments and retry.
-
Log format changes: GitHub's log format is not officially documented. ghaperf parses logs based on observed patterns, which may break if GitHub changes the format unexpectedly.
-
Log retention: GitHub retains workflow logs for 90 days by default. Analysis of older runs may fail if logs have been deleted.
Related Projects
While these tools are excellent for analyzing GitHub Actions performance, they don't provide step-level visibility into composite actions like ghaperf does. We don't aim to replace these tools with ghaperf. For this reason, it is not our intention to re-implement features already present in other tools, such as CIAnalyzer, in ghaperf. Rather, ghaperf preserves features that other tools are missing. Tools like CIAnalyzer are more suitable if you want to know how performance changes over the medium to long term. On the other hand, ghaperf is more suitable if you want to investigate current performance bottlenecks in more detail.
- GitHub Actions Performance Metrics - Official GitHub metrics
- actions-timeline - GitHub Actions to visualize timeline of a workflow job in a job summary
- CIAnalyzer - Collect metrics to BigQuery
- Collect metrics to OpenTelemetry
- github_actions_otel_trace - Export traces to OpenTelemetry
- otel-export-trace-action - OpenTelemetry trace export
- workflow-telemetry-action
- github-actions-opentelemetry