A GitHub Action that collects workflow step metrics and traces, and exports them to Google Cloud Monitoring and Cloud Trace.
See working example workflow: use-action.yaml
- 📊 Metrics: Duration histograms and success/failure counters for jobs and steps
- 🔍 Traces: Distributed traces showing job and step execution timeline with parent-child relationships
- 🏷️ Rich attributes and labels (workflow, job, repository, run info, step attribution)
- 🔒 Minimal permissions (only metric writer and trace agent)
- 🔄 Always runs (even when steps fail)
Create a service account with minimal permissions to write metrics and traces:
# Set your GCP project ID
PROJECT_ID="your-project-id"
# Create the service account
gcloud iam service-accounts create gcp-metrics-action \
--display-name="GCP Metrics Exporter for GitHub Actions" \
--description="Service account for GitHub Actions to export metrics and traces" \
--project="${PROJECT_ID}"
# Grant required roles
SA_EMAIL="gcp-metrics-action@${PROJECT_ID}.iam.gserviceaccount.com"
# For metrics
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/monitoring.metricWriter" \
--condition=None
# For traces
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/cloudtrace.agent" \
--condition=None
# Create and download a JSON key
gcloud iam service-accounts keys create github-actions-metrics-key.json \
--iam-account="${SA_EMAIL}"Roles explained:
roles/monitoring.metricWriter- Write custom metrics to Cloud Monitoringroles/cloudtrace.agent- Write traces to Cloud Trace
You have two options:
Option A: Use GitHub Secrets (Recommended for production)
-
Copy the contents of the JSON key file:
cat github-actions-metrics-key.json
-
In your GitHub repository, go to Settings → Secrets and variables → Actions
-
Click New repository secret
-
Name:
SERVICE_ACCOUNT_KEY(or whatever you want) -
Value: Paste the entire JSON content
-
Click Add secret
-
Securely delete the key file:
rm github-actions-metrics-key.json
Option B: Commit the key file (For private repos only)
You can commit the key file directly:
git add github-actions-metrics-key.json
git commit -m "Add service account key for metrics"
git push- MUST be a private repository (action will refuse to run this way in public repos)
- Warning: For production security or public repos, use GitHub Secrets or Workload Identity Federation instead
The action includes built-in security checks:
- ✓ Refuses to run if repository is public (checked via GitHub API)
- ✓ Best-effort: Verifies service account has minimal roles
- ✓ Errors if excessive permissions are detected
This is mainly intended to be used in pull_request workflows where secrets and workload identity federation are not available.
Service Account Permission Validation:
The action attempts to call the GCP IAM API to verify that the service account has only minimal required permissions (roles/monitoring.metricWriter).
Important: This check requires the service account to have resourcemanager.projects.getIamPolicy permission, which is not included in roles/monitoring.metricWriter. Therefore:
- ✅ If successful: Will error and refuse to run if excessive permissions are detected
⚠️ If unsuccessful: Will log an info message and proceed (this is expected with minimal permissions)
To enable the permission check (optional, adds minimal permissions):
# Create a custom role with only the getIamPolicy permission
gcloud iam roles create githubActionsMetricsChecker \
--project=jason-chainguard \
--title="GitHub Actions Metrics Permission Checker" \
--permissions=resourcemanager.projects.getIamPolicy
# Grant it to the service account
gcloud projects add-iam-policy-binding jason-chainguard \
--member="serviceAccount:github-actions-metrics@jason-chainguard.iam.gserviceaccount.com" \
--role="projects/jason-chainguard/roles/githubActionsMetricsChecker" \
--condition=NoneWith this optional permission, the action can verify it has no excessive roles and provide specific remediation commands if issues are found.
This action supports two authentication methods:
| Method | Best For | Works With | Setup Complexity |
|---|---|---|---|
| Service Account Key File | Private repos, weaker security | pull_request only |
Simplest - only supported for private repos |
| Service Account Key File (Secret) | Production, good security | push, pull_request_target only |
Simple - just create and store a key |
| Workload Identity Federation | Production, best security | push, pull_request_target only |
Moderate - requires WIF setup |
Quick Decision:
- Using
pull_requestfrom private forks? → You can use Service Account Key File - Production workflows on
push? → Use GitHub Secret or Workload Identity Federation
For private repositories, you can commit the key file:
steps:
- uses: actions/checkout@v4
- uses: imjasonh/gcp-metrics-action@...
with:
github-token: ${{ github.token }}
gcp-service-account-key-file: github-actions-metrics-key.json
# Your workflow steps...The action automatically detects the GCP project ID in this order:
- Explicit input:
gcp-project-idparameter - Service account key file: Extracted from
project_idfield in the JSON key - Environment variable:
GOOGLE_CLOUD_PROJECT,GCLOUD_PROJECT, orGCP_PROJECT - Application Default Credentials: Detected from ADC configuration
For most use cases, you don't need to specify gcp-project-id explicitly.
- uses: imjasonh/gcp-metrics-action@...
with:
github-token: ${{ github.token }}
gcp-service-account-key-file: github-actions-metrics-key.json
# Optional: Override project ID (auto-detected in most cases)
# gcp-project-id: 'my-project-id'
# Optional: Customize service name for resource attributes
service-name: 'my-app-ci'
# Optional: Customize service namespace
service-namespace: 'production'
# Optional: Customize metric name prefix
metric-prefix: 'ci.metrics'
# Optional: Fail workflow if metrics/traces export fails (default: false)
# Useful for production to ensure observability data is always captured
fail-on-error: true- Metric:
github.actions.job.duration - Type: Histogram
- Unit: milliseconds
- Labels:
workflow.name- Name of the workflowjob.name- Name of the jobjob.status- Status (completed, in_progress, etc.)job.conclusion- Conclusion (success, failure, cancelled, etc.)repository.owner- Repository ownerrepository.name- Repository namerepository.full_name- Full repository name (owner/repo)run.id- Workflow run IDrun.number- Workflow run numberrun.attempt- Run attempt numbergit.sha- Commit SHAgit.ref- Full ref (e.g., refs/heads/main, refs/pull/123/merge)git.ref_name- Short ref name (e.g., main, feature-branch) *git.base_ref- Base branch for PRs (e.g., main) *git.head_ref- Head branch for PRs (e.g., feature-branch) *event.name- Event that triggered workflow (push, pull_request, etc.)event.actor- User who triggered the workflowpull_request.number- PR number (if applicable) *runner.os- Runner operating system (Linux, Windows, macOS)runner.arch- Runner architecture (X64, ARM64, etc.)runner.name- Runner name *runner.label- Primary runner label (e.g., ubuntu-latest, ubuntu-4-cores) *
* = Optional attributes, only present when applicable
- Metric:
github.actions.job.estimated_cost - Type: Histogram
- Unit: USD
- Labels: Same as job duration
- Note: Only recorded when runner OS is known. Automatically calculates cost based on:
- Standard runners: Linux: $0.008/min, Windows: $0.016/min, macOS: $0.08/min
- Larger runners: Detected from runner label (e.g., ubuntu-4-cores, ubuntu-8-cores)
- Pricing scales from $0.016/min (2-cores) to $1.024/min (64-cores Windows)
- Self-hosted runners: Not recorded (cost = $0)
- Ref: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions
Benefits:
- Track cost trends for specific workflows over time
- Alert on cost increase
- Metric:
github.actions.repo.size - Type: Gauge
- Unit: KB (kilobytes)
- Labels: Same as job duration
- Note: Records the current repository size at workflow run time
Benefits:
- Track repository growth over time
- Correlate build performance with repository size
- Alert when repository grows too large
- Identify when to implement size optimizations
- Metric:
github.actions.step.duration - Type: Histogram
- Unit: milliseconds
- Labels: All job labels plus:
step.name- Name of the stepstep.number- Step numberstep.status- Status (completed, in_progress, etc.)step.conclusion- Conclusion (success, failure, skipped, etc.)
- Metric:
github.actions.artifact.size - Type: Histogram
- Unit: bytes
- Labels: All job labels plus:
artifact.name- Name of the artifact
- Note: Recorded once per artifact. Only available when artifacts are found (typically not until after workflow completes)
Benefits:
- Track size trends for specific artifacts over time
- Count unique artifacts by counting time series
- Monitor total storage usage across all artifacts
- Alert on artifact size growth
The action creates distributed traces showing the execution timeline of your workflow:
-
Job Span: Root span covering the entire job execution
- Span name:
Job: {job-name} - Includes all job attributes (workflow, repository, run info, job status/conclusion)
- Marked as error if job fails
- Span name:
-
Step Spans: Child spans for each workflow step
- Span name:
Step: {step-name} - Parent: Job span (creates hierarchical trace)
- Includes all step attributes (name, number, status, conclusion)
- Marked as error if step fails
- Accurate start/end times from GitHub API
- Span name:
Benefits:
- Visualize workflow execution in Cloud Trace timeline view
- Identify slow steps at a glance
- See step dependencies and parallelization
- Correlate failures across steps
- Track execution patterns over time
Metrics will appear in Google Cloud Monitoring under custom metrics:
- Go to Cloud Console → Monitoring → Metrics Explorer
- Search for:
custom.googleapis.com/github.actions - Available metrics:
custom.googleapis.com/github.actions/job.durationcustom.googleapis.com/github.actions/job.estimated_costcustom.googleapis.com/github.actions/repo.sizecustom.googleapis.com/github.actions/step.durationcustom.googleapis.com/github.actions/artifact.size(when artifacts available)
Average step duration by step name:
custom.googleapis.com/github.actions/step.duration
| filter resource.project_id = "your-project-id"
| group_by [metric.step.name]
| mean
Job failure rate over time:
custom.googleapis.com/github.actions/job.duration
| filter metric.job.conclusion = "failure"
| group_by [], .rate(1h)
Metrics for a specific PR:
custom.googleapis.com/github.actions/job.duration
| filter metric.pull_request.number = "123"
Build duration by branch:
custom.googleapis.com/github.actions/job.duration
| filter metric.git.ref_name != ""
| group_by [metric.git.ref_name]
| mean
Metrics triggered by specific user:
custom.googleapis.com/github.actions/step.duration
| filter metric.event.actor = "username"
Compare push vs pull_request performance:
custom.googleapis.com/github.actions/job.duration
| group_by [metric.event.name]
| mean
Artifact size over time by artifact name:
custom.googleapis.com/github.actions/artifact.size
| group_by [metric.artifact.name]
| mean
Count of unique artifacts being uploaded:
custom.googleapis.com/github.actions/artifact.size
| group_by [metric.artifact.name]
| count
Total CI costs:
custom.googleapis.com/github.actions/job.estimated_cost
| sum
Monthly CI costs by runner type:
custom.googleapis.com/github.actions/job.estimated_cost
| group_by [metric.runner.os, metric.runner.label]
| sum
Most expensive workflows:
custom.googleapis.com/github.actions/job.estimated_cost
| group_by [metric.workflow.name]
| sum
| top 10
Most expensive jobs:
custom.googleapis.com/github.actions/job.estimated_cost
| group_by [metric.job.name]
| sum
| top 10
Traces will appear in Google Cloud Trace:
- Go to Cloud Console → Trace → Trace Explorer
- You'll see traces for each workflow job execution
- Click on a trace to see the timeline:
- Job span showing total execution time
- Step spans showing individual step durations
- Failed steps highlighted in red
- Hover over spans to see attributes (workflow name, repository, etc.)
Trace URL format:
https://console.cloud.google.com/traces/list?project=your-project-id
Benefits of the trace view:
- See all steps in a single timeline
- Identify bottlenecks visually
- Understand step execution order
- Correlate metrics with traces for deeper insights
The action exports the root trace context so your workflow steps can create child spans:
The action sets the TRACEPARENT environment variable (W3C Trace Context format) that subsequent steps can use:
steps:
- uses: imjasonh/gcp-metrics-action@...
id: gcp-metrics
with:
github-token: ${{ github.token }}
# TRACEPARENT is now available in environment
# Your application can use it to create child spans
- name: Your instrumented step
run: |
# The TRACEPARENT env var is automatically set
echo "Trace context: $TRACEPARENT"
echo "Span ID: ${{ steps.gcp-metrics.outputs.span-id }}"
# Your app can use this to create child spans under the job spantraceparent- W3C Trace Context header value (use this for most instrumentation)trace-id- OpenTelemetry Trace ID (32-character hex string)span-id- Root span ID for this job (16-character hex string)
- uses: ./
with:
github-token: ${{ github.token }}
gcp-service-account-key-file: key.json
- name: Run instrumented app
env:
# TRACEPARENT already set automatically
OTEL_EXPORTER_OTLP_ENDPOINT: https://your-collector:4318
run: |
# Your app reads TRACEPARENT and creates child spans
node my-app.jsYour application can use standard OpenTelemetry libraries to read TRACEPARENT and create child spans that will appear under the job span in Cloud Trace.
Instead of service account keys, you can use Workload Identity Federation (WIF). This is the recommended approach for production as it doesn't require managing service account key files.
id-token: write permission, which is only available for:
pusheventspull_request_targetevents (runs in the context of the target repo)
❌ WIF will NOT work with:
pull_requestevents (runs in the context of the fork, cannot get OIDC tokens)
For pull requests from forks, you must use service account keys instead.
name: CI with WIF
on:
push:
branches: [ main ]
pull_request_target: # Use pull_request_target, not pull_request
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for WIF
actions: read # Required to read workflow/job info
steps:
- uses: actions/checkout@v4
# Authenticate with GCP first
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
# Setup metrics collection
- uses: imjasonh/gcp-metrics-action@...
with:
github-token: ${{ github.token }}
# gcp-project-id is auto-detected from ADC
# gcp-service-account-key-file is omitted - uses ADC from WIF
# Your workflow steps...
- run: npm install
- run: npm test- Create a Workload Identity Pool and Provider in GCP
- Grant the service account permissions to the pool
- Add secrets to GitHub:
WIF_PROVIDER: Full resource name of the workload identity providerWIF_SERVICE_ACCOUNT: Email of the service accountGCP_PROJECT_ID: Your GCP project ID
See Google's documentation for detailed setup instructions.
npm installnpm testThe action uses @vercel/ncc to compile the JavaScript and dependencies into a single file for distribution.
npm run buildThis creates:
dist/index.js- Main entry pointdist/post/index.js- Post-action with all dependencies
Important: The dist/ directory must be committed for the action to work in GitHub Actions.
This project uses pre-commit to automatically run tests and build before each commit.
Setup:
# Install pre-commit (if not already installed)
brew install pre-commit # macOS
# or: pip install pre-commit # other platforms
# Install the git hook scripts
pre-commit installNow npm test and npm run build will run automatically before each commit. If either fails, the commit will be aborted.
Manual execution:
# Run hooks on all files
pre-commit run --all-files/
├── action.yml # Action definition
├── index.js # Main entry point (source)
├── post.js # Post-action (source)
├── dist/ # Compiled action (committed)
│ ├── index.js # Built main entry
│ └── post/
│ └── index.js # Built post-action
├── lib/
│ ├── config.js # Configuration parsing
│ ├── collector.js # GitHub API metrics collection
│ └── exporter.js # OpenTelemetry export
└── test/
├── collector.test.js
└── exporter.test.js
Apache-2.0
Issues and pull requests welcome!

