Universal GitHub Action to capture and display comprehensive metadata related to software builds across 15+ languages and build systems.
The build-metadata-action is a unified solution for extracting, processing, and
reporting build metadata for projects written in Python, Java,
JavaScript/TypeScript, Go, .NET, Rust, Ruby, and other languages. It consolidates
functionality from language-specific metadata actions while providing standardized
outputs and rich CI/CD integration.
- π Multi-Language Support: Python, Java (Maven/Gradle), Node.js, Go, .NET, Rust, Ruby, and more
- π Rich Reporting: Generates beautiful GitHub Step Summary outputs with project and build information
- π Version Detection: Integrates with
version-extract-actionfor comprehensive version extraction - π οΈ Environment Capture: Reports CI environment, tool versions, and runtime configuration
- π¦ Standardized Outputs: Consistent, namespaced outputs for downstream build actions
- π― Dynamic Versioning: Detects and handles dynamic versioning strategies
- π Monorepo Support: Handles multi-language and multi-project repositories
| Language | Build Systems | Version Files |
|---|---|---|
| Python | setuptools, poetry, flit, hatch | pyproject.toml, setup.py, setup.cfg |
| JavaScript/TypeScript | npm, yarn, pnpm | package.json, tsconfig.json |
| Java | Maven, Gradle (Groovy/Kotlin) | pom.xml, build.gradle, build.gradle.kts |
| .NET/C# | MSBuild, dotnet CLI | *.csproj, *.sln, *.props |
| Go | Go modules | go.mod |
| Rust | Cargo | Cargo.toml |
| Ruby | Bundler, RubyGems | *.gemspec, Gemfile |
| PHP | Composer | composer.json |
| Swift | Swift Package Manager | Package.swift |
| Dart/Flutter | pub | pubspec.yaml |
| Terraform/OpenTofu | Terraform, OpenTofu | *.tf, versions.tf |
| C/C++ | CMake, Autoconf, Meson | CMakeLists.txt, configure.ac |
| Scala | SBT | build.sbt |
| Elixir | Mix | mix.exs |
| Haskell | Cabal | *.cabal |
| Julia | Pkg | Project.toml |
- name: Extract Build Metadata
id: metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: .name: Build and Deploy
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Extract Build Metadata
id: metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: .
output_format: summary
include_environment: true
use_version_extract: true
verbose: false
artifact_upload: true
artifact_formats: json
- name: Use Metadata in Build
run: |
echo "Building ${{ steps.metadata.outputs.project_name }} \
v${{ steps.metadata.outputs.project_version }}"
echo "Project Type: ${{ steps.metadata.outputs.project_type }}"- name: Extract Python Metadata
id: python-metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: ./python-service
- name: Extract Node.js Metadata
id: node-metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: ./web-frontend
- name: Build Services
run: |
echo "Python: ${{ steps.python-metadata.outputs.project_version }}"
echo "Node.js: ${{ steps.node-metadata.outputs.project_version }}"Use export_env_vars: true to make metadata available as environment variables
in later steps:
- name: Extract Build Metadata
id: metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: .
export_env_vars: true
- name: Use Environment Variables
run: |
echo "Project: $PROJECT_NAME"
echo "Version: $PROJECT_VERSION"
echo "Type: $PROJECT_TYPE"
# All outputs become uppercase environment variables
# e.g., project_name -> PROJECT_NAME
# python_build_version -> PYTHON_BUILD_VERSIONGenerate output in one or more formats simultaneously (comma, space, or newline-separated):
- name: Extract Build Metadata
id: metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
path_prefix: .
# You can specify one or more formats
output_format: summary,json,markdown
# Or with spaces: "summary json markdown"
# Or with newlines:
# output_format: |
# summary
# json
# markdown
- name: Artifact Formats
uses: lfreleng-actions/build-metadata-action@v1
with:
artifact_upload: true
artifact_formats: json,yaml
# Uploads both JSON and YAML artifacts| Name | Required | Default | Description |
|---|---|---|---|
path_prefix |
No | . |
Path to the project root |
project_type |
No | "" |
Declare the project type instead of detecting it (e.g. java-maven, java-gradle). See Declaring the project type. |
output_format |
No | summary |
Output format(s): summary, json, markdown, yaml. Accepts comma-separated, space-separated, or newline-separated values. Set to empty string to disable output. |
include_environment |
No | true |
Include environment metadata |
use_version_extract |
No | true |
Use version-extract-action for version detection |
verbose |
No | false |
Enable verbose output |
artifact_upload |
No | true |
Upload gathered metadata as workflow artifacts |
artifact_name_prefix |
No | build-metadata |
Custom prefix for artifact names |
artifact_formats |
No | json |
Formats to upload as artifacts. Can be comma-separated, space-separated, or newline-separated (e.g., json, yaml, or json,yaml). |
validate_output |
No | true |
Check JSON/YAML output before uploading |
strict_validation |
No | true |
Use strict validation mode (round-trip testing) |
export_env_vars |
No | false |
Export all outputs as environment variables (uppercase with underscores) for use in later steps |
Detection resolves the first rule that matches in priority order, and those priorities are global rather than per language. A repository carrying more than one marker file resolves to whichever ranks highest, which is not always the thing the workflow builds.
The common case is a Maven project that also has a package.json, the
shape frontend-maven-plugin produces. javascript-npm outranks
java-maven, so:
| Output | Plain Maven | Maven plus package.json |
|---|---|---|
project_type |
java-maven |
javascript-npm |
build_tool |
maven |
npm |
java_version |
17 |
(empty) |
A consumer reading java_version gets nothing and falls back to its own
default, choosing a JDK the project never asked for.
Where the caller already knows, say so:
- uses: lfreleng-actions/build-metadata-action@<sha>
with:
project_type: java-mavenA reusable workflow dedicated to one build tool always has better information than a detector, because the caller chose that workflow.
The action reports an unrecognised value and discards it, running detection instead: detection still produces a real answer, whereas a type matching no extractor produces none at all.
All project types provide these standardized outputs:
| Output | Description | Example |
|---|---|---|
project_type |
Resolved project type, detected or supplied via the project_type input |
python-modern |
build_tool |
Build tool the project type implies; empty when not identified | maven |
project_name |
Project/package name | myproject |
project_version |
Current version | 1.2.3 |
project_path |
Absolute project path | /workspace/myproject |
version_source |
Source of version info | pyproject.toml |
versioning_type |
Versioning type: static or dynamic |
static |
version_properties_version |
Version from version.properties (LF/ONAP convention); empty when absent | 1.1.0 |
version_properties_match |
Whether version.properties matches project_version (empty when not comparable) |
true |
snapshot_version |
Synthesized interim/development version (X.Y.Z-SNAPSHOT convention) |
1.1.0-SNAPSHOT |
release_files |
Comma-separated release request files under releases/ (global-jjb/LF convention); empty when none |
releases/3.8.2.yaml |
release_file_count |
Number of release request files found under releases/ |
1 |
is_release_ready |
True when at least one release request file is present under releases/ |
true |
release_version |
Version parsed from a lone release file; empty when more than one exists | 3.8.2 |
release_ref |
Git ref parsed from a lone release file; empty when more than one exists | abc123... |
build_timestamp |
ISO 8601 build timestamp | 2025-11-03T12:00:00Z |
git_sha |
Current git commit SHA | abc123... |
git_branch |
Current git branch | main |
git_tag |
Current git tag | v1.2.3 |
ci_platform |
CI platform | github |
ci_run_id |
CI run identifier | 12345678 |
ci_run_url |
URL to CI run | https://github.com/... |
runner_os |
Runner OS | Linux |
runner_arch |
Runner architecture | X64 |
metadata_json |
Complete metadata as JSON | {...} |
success |
Extraction success indicator | true |
| Output | Description |
|---|---|
python_version |
Python interpreter version |
python_package_name |
Distribution package name |
python_requires_python |
Required Python version range |
python_build_backend |
Build backend (setuptools, poetry, etc.) |
python_metadata_source |
Source file (pyproject.toml, etc.) |
python_matrix_json |
CI matrix configuration as JSON |
python_dependencies |
Runtime dependencies |
| Output | Description |
|---|---|
java_version |
JDK version |
java_version_source |
JDK version source |
java_group_id |
Maven groupId |
java_artifact_id |
Maven artifactId |
java_packaging |
Packaging type (jar, war, etc.) |
java_has_parent |
Whether the POM declares a parent |
java_is_multi_module |
Multi-module (reactor) project |
java_module_count |
Number of reactor modules |
java_frameworks |
Detected frameworks |
java_source_dirs |
Main source directories |
java_test_source_dirs |
Test source directories |
java_coverage_tool |
Coverage tool, when configured |
java_coverage_report_paths |
Coverage report locations |
The layout outputs describe where a scanner should look. They are
module-relative, so a consumer applies them per module across a reactor,
and they fall back to Maven's conventions (src/main/java,
src/test/java) when <build> is silent, because that is what the build
itself uses.
java_coverage_tool and java_coverage_report_paths appear when the
build configures coverage, and are absent otherwise. Absence is a
deliberate answer: pointing a scanner at a report that is never produced
is worse than telling it there is none. The action finds JaCoCo in
<build><plugins>, in <build><pluginManagement>, and in declared
modules β a reactor root often configures nothing itself and delegates to
a parent module, as ONAP cps does with cps-parent/pom.xml.
The action resolves the Java level (java_version) in Maven's own
precedence: the POM's maven.compiler.release, then
maven.compiler.source/target, then java.version, then the
maven-compiler-plugin <configuration>. When the scanned POM declares
no level, the action inherits it from on-disk parent POMs (via
relativePath) and, for aggregator roots, from a reactor module β so an
ONAP-style root whose level lives in a shared *-parent module still
resolves. The java_version_source output reports where the value
came from (e.g. maven.compiler.release, maven-compiler-plugin/release,
module:cps-parent).
| Output | Description |
|---|---|
java_version |
JDK version |
java_version_source |
JDK version source |
java_group_id |
Project group |
java_artifact_id |
Project name |
java_build_dsl |
Build DSL (groovy or kotlin) |
java_is_multi_project |
Multi-project build |
java_frameworks |
Detected frameworks |
java_gradle_version |
Gradle version the wrapper declares |
java_gradle_version_source |
Source of that version |
For Gradle the action reads the level from the build file toolchain
(JavaLanguageVersion.of(N)), then source/targetCompatibility
(JavaVersion.VERSION_N or a bare/quoted literal), then
gradle.properties; java_version_source reports the form detected.
java_gradle_version comes from the wrapper's distributionUrl. This is
the version the project asks to build with, which is a different fact
from the version a CI step provisioned: gradle/actions/setup-gradle
reports what it set up itself, and sets up nothing when a build defers
to the wrapper, so that output is empty for wrapper-driven projects.
The output stays empty when the project has no wrapper, or when
distributionUrl names no recognisable version. A consumer comparing it
against a tool's floor needs to tell "too old" from "unknown", so the
action reports nothing rather than guessing.
| Output | Description |
|---|---|
node_version |
Node.js version |
npm_version |
npm version |
node_package_manager |
Detected package manager (npm, yarn, pnpm) |
node_engines |
Required node/npm versions |
node_workspaces |
Workspace packages (monorepo) |
| Output | Description |
|---|---|
dotnet_version |
.NET SDK version |
dotnet_framework |
Target framework(s) |
dotnet_assembly_name |
Assembly name |
dotnet_package_id |
NuGet package ID |
| Output | Description |
|---|---|
go_base_name |
Friendly name from the module path (/vN suffix stripped) |
go_module_path |
Go module path declared in go.mod |
go_go_version |
Go version from the go directive in go.mod |
go_metadata_source |
Source of Go metadata (go.mod) |
go_toolchain |
Toolchain directive from go.mod (when present) |
go_dependencies |
Direct dependencies as module@version |
go_indirect_dependencies |
Indirect dependencies as module@version |
go_dependency_count |
Number of direct dependencies |
go_total_dependency_count |
Total dependencies (direct plus indirect) |
go_dependency_map |
JSON object mapping modules to versions |
go_replace_directives |
Replace directives as JSON array of {old, new} |
go_replace_count |
Number of replace directives |
go_exclude_directives |
Exclude directives (comma-separated) |
go_exclude_count |
Number of exclude directives |
go_retract_directives |
Retract directives (comma-separated) |
go_retract_count |
Number of retract directives |
go_frameworks |
Detected Go frameworks/tools (comma-separated) |
go_go_version_matrix |
Supported (non-EOL) Go versions for testing |
go_matrix_json |
Go version test matrix as JSON |
The action derives the Go version matrix from live
endoflife.date data: it selects the
supported (non-EOL) Go releases at or above the version declared in
go.mod. When the API is unreachable, a static fallback list of the
supported releases applies instead.
| Output | Description |
|---|---|
rust_version |
Rust compiler version |
cargo_version |
Cargo version |
rust_edition |
Rust edition |
rust_workspace_members |
Workspace members |
When used in a GitHub Actions workflow, the action generates a rich step summary:
# π§ Build Metadata
## Project Information
| Key | Value |
| ------------------ | -------------------- |
| Project Type | Python (Modern) |
| Project Name | dependamerge |
| Project Version | 1.2.3 |
| Version Source | pyproject.toml |
| Dynamic Versioning | No |
| Build Timestamp | 2025-11-03T12:00:00Z |
| Git SHA | `abc1234` |
| Git Branch | `main` |
## CI Environment
| Component | Value |
| ----------- | -------------- |
| Platform | github |
| Runner OS | Linux |
| Runner Arch | X64 |
| Workflow | Build and Test |
| Run Number | 42 |
## Tool Versions
| Tool | Version |
| ---------- | ------- |
| python | 3.13.0 |
| pip | 24.0 |
| setuptools | 75.0.0 |
## Language-Specific Metadata
### Python Project Details
| Key | Value |
| --------------- | -------------- |
| Package Name | `dependamerge` |
| Requires Python | >=3.10 |
| Build Backend | setuptools |
| Metadata Source | pyproject.toml |
### Build Matrix
```json
{
"python-version": ["3.10", "3.11", "3.12", "3.13", "3.14"]
}
β Metadata extraction successful
- name: Extract Metadata
uses: lfreleng-actions/build-metadata-action@v1
with:
use_version_extract: true
env:
VERSION_EXTRACT_ACTION_PATH: /path/to/version-extract-action- name: Extract Metadata
id: metadata
uses: lfreleng-actions/build-metadata-action@v1
- name: Build Python Package
uses: lfreleng-actions/python-build-action@v1
with:
version: ${{ steps.metadata.outputs.project_version }}
python_version: ${{ steps.metadata.outputs.python_version }}The action detects and reports when projects use dynamic versioning:
- Python:
setuptools_scm,versioneer, PEP 621 dynamic versions - Node.js:
semantic-release, version0.0.0-development - Java: Maven properties, Gradle project version
- Rust:
0.0.0,0.1.0-devversions
Automatically detects and handles monorepo structures:
- Node.js workspaces
- Python multi-package projects
- Rust workspaces
- Maven multi-module projects
- Gradle multi-project builds
Built with Go using design patterns from version-extract-action:
- Strategy Pattern: Language-specific extractors
- Chain of Responsibility: Sequential project type detection
- Factory Pattern: Dynamic extractor selection
- Configuration-Driven: YAML-based pattern definitions
- Dynamic Version Fetching: Automatically updates version matrices from upstream sources with static fallbacks
To keep pace with fast-evolving language ecosystems, the action uses a dynamic + fallback strategy for version matrices:
- Primary: Fetches current stable version from
rust-lang.org- Generates ~6 recent versions (9 months of releases)
- Adapts to Rust's 6-week release cycle automatically
- 5-second timeout prevents workflow delays
- Fallback: Static version list (updated monthly)
- Ensures CI/CD reliability during network issues or API downtime
- Prevents build failures from temporary connectivity problems
- Provides reasonable version coverage even offline
Languages like Rust, Swift, and PHP release frequently (every 6-8 weeks). Static version lists become outdated within weeks, leading to:
- Missing security updates and new features in CI tests
- Manual maintenance burden to keep lists current
- Stale testing that doesn't catch real-world compatibility issues
Dynamic fetching solves this while the fallback ensures reliability.
See IMPLEMENTATION_PLAN.md for detailed architecture and design decisions.
- Go 1.24 or higher
- Git
- (Optional) Language toolchains for testing
make buildThe project includes a comprehensive test suite that validates metadata extraction across all supported languages and project types.
Quick Start:
make testComprehensive Testing:
The GitHub Actions workflow tests the action against:
- Real-world projects: 12+ actual open-source repositories
- Synthetic projects: 15+ minimal generated project structures
- All major languages: Python, JavaScript, Go, Rust, Java, PHP, Ruby, C#, Swift, Dart, Docker, Helm, Terraform, and more
Tests run in parallel using GitHub Actions matrix strategy for speed.
π See Testing Guide for detailed information about:
- Test architecture and strategy
- How to add new test cases
- Coverage across 50+ project types
- Performance and troubleshooting
./build-metadata --path /path/to/project --output-format summaryContributions are welcome! Please see our contributing guidelines and code of conduct.
Apache License 2.0 - see LICENSE for details.
- version-extract-action - Universal version extraction
- python-project-metadata-action - Python-specific metadata
- python-build-action - Python build automation
For questions, issues, or feature requests, please open an issue on GitHub.