A container image tool that validates equilibrium between mutable tags and semantic version tags.
- The Problem
- How Equilibrium Solves It
- Tag Conversion Logic
- Installation
- Quick Start
- Output Formats & Schemas
- Constraints
- License
Container registries create a web of mutable tags that should point to specific semantic versions, but these relationships can break over time:
The Challenge:
- When you publish
myapp:1.2.3, registries should automatically createmyapp:latest,myapp:1,myapp:1.2 - But what happens when you later publish
myapp:1.2.4ormyapp:1.3.0? - Do all the mutable tags get updated correctly?
- How do you verify the entire tag ecosystem is in equilibrium?
Real-World Impact:
latestmight point to an outdated version- Major version tags like
1might miss recent patches - Minor version tags like
1.2might not reflect the latest patch - Users pulling
myapp:1expect the latest1.x.x, but get an old version
Equilibrium validates your container registry's tag ecosystem through a clear data flow:
flowchart LR
A[Registry Query] --> B[All Tags<br/>1.0.0<br/>1.1.0<br/>latest<br/>1<br/>1.1<br/>dev]
B --> C[Filter<br/>Semantic<br/>Versions]
B --> D[Filter<br/>Mutable<br/>Tags]
C --> E[Semantic Versions<br/>1.0.0<br/>1.1.0]
D --> F[Actual Mutable<br/>latestβ1.0<br/>1β1.0.0<br/>1.1β1.1.0]
E --> G[Compute<br/>Expected<br/>Mutable Tags]
G --> H[Expected Mutable<br/>latestβ1.1<br/>1β1.1.0<br/>1.1β1.1.0]
H --> I[Compare<br/>Expected vs<br/>Actual]
F --> I
I --> J[Analysis Report<br/>Missing: 1<br/>Mismatched: latest]
H --> K[catalog<br/>Command]
K --> L[Catalog Format<br/>External Integration]
L --> M[uncatalog<br/>Command]
M --> N[Back to Expected/<br/>Actual Format]
classDef source fill:#e1d5e7
classDef process fill:#fff2cc
classDef expected fill:#d5e8d4
classDef actual fill:#f8cecc
classDef result fill:#ffcccc
classDef catalog fill:#dae8fc
class A source
class C,D,G,I process
class E,H expected
class F actual
class J result
class K,M catalog
class L,N catalog
Process Steps:
- Fetch All Tags: Query registry for complete tag list
- Filter Semantic: Extract only valid semantic version tags (MAJOR.MINOR.PATCH)
- Compute Expected: Generate mutable tags based on semantic versions
- Fetch Actual: Query registry for current mutable tag state
- Compare & Analyze: Identify missing, unexpected, and mismatched tags with unified structure
- Format Conversion: Transform between expected/actual and catalog formats for external integration
gem install equilibriumFor containerized environments, use the Docker image:
# Pull the latest version
docker pull ghcr.io/tonycthsu/equilibrium:latestFor other installation methods, see equilibrium help
REPO="gcr.io/datadoghq/apm-inject"
# 1. Check what mutable tags should exist
equilibrium expected "$REPO"
# 2. Check what mutable tags actually exist
equilibrium actual "$REPO"
# 3. Compare and analyze differences
equilibrium expected "$REPO" --format json > expected.json
equilibrium actual "$REPO" --format json > actual.json
equilibrium analyze --expected expected.json --actual actual.json
# 4. Convert to catalog format and back (Round trip)
equilibrium catalog expected.json | tee catalog.json
equilibrium uncatalog catalog.json | tee uncatalog.jsonFor detailed command options, run equilibrium help [command]
Equilibrium transforms semantic version tags into their expected mutable tag ecosystem:
For any semantic version MAJOR.MINOR.PATCH, create mutable tags for each component level, pointing to the highest available version at that level.
Scenario 1: Single Version
Semantic versions: ["1.0.0"]
Expected mutable tags:
βββ latest β 1.0.0 (highest overall)
βββ 1 β 1.0.0 (highest major 1)
βββ 1.0 β 1.0.0 (highest minor 1.0)
Scenario 2: Multiple Patch Versions
Semantic versions: ["1.0.0", "1.0.1", "1.0.2"]
Expected mutable tags:
βββ latest β 1.0.2 (highest overall)
βββ 1 β 1.0.2 (highest major 1)
βββ 1.0 β 1.0.2 (highest minor 1.0)
Scenario 3: Multiple Minor Versions
Semantic versions: ["1.0.0", "1.0.1", "1.1.0", "1.1.3"]
Expected mutable tags:
βββ latest β 1.1.3 (highest overall)
βββ 1 β 1.1.3 (highest major 1)
βββ 1.0 β 1.0.1 (highest minor 1.0)
βββ 1.1 β 1.1.3 (highest minor 1.1)
Scenario 4: Multiple Major Versions
Semantic versions: ["0.9.0", "1.0.0", "1.2.3", "2.0.0", "2.1.0"]
Expected mutable tags:
βββ latest β 2.1.0 (highest overall)
βββ 0 β 0.9.0 (highest major 0)
βββ 0.9 β 0.9.0 (highest minor 0.9)
βββ 1 β 1.2.3 (highest major 1)
βββ 1.0 β 1.0.0 (highest minor 1.0)
βββ 1.2 β 1.2.3 (highest minor 1.2)
βββ 2 β 2.1.0 (highest major 2)
βββ 2.0 β 2.0.0 (highest minor 2.0)
βββ 2.1 β 2.1.0 (highest minor 2.1)
Scenario 5: Pre-release and Non-semantic Tags (Filtered Out)
All tags: ["1.0.0", "1.1.0-beta", "1.1.0-rc1", "1.1.0", "latest", "dev"]
Semantic versions: ["1.0.0", "1.1.0"] # Pre-releases and existing mutable tags filtered
Expected mutable tags:
βββ latest β 1.1.0 (highest overall)
βββ 1 β 1.1.0 (highest major 1)
βββ 1.0 β 1.0.0 (highest minor 1.0)
βββ 1.1 β 1.1.0 (highest minor 1.1)
All commands output structured JSON following validated schemas (see schemas):
Expected/Actual Commands (schema):
{
"repository_url": "gcr.io/project/image",
"repository_name": "image",
"digests": {
"latest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
"1": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
"1.2": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c"
},
"canonical_versions": {
"latest": "1.2.3",
"1": "1.2.3",
"1.2": "1.2.3"
}
}Analyze Command (schema):
{
"repository_url": "gcr.io/project/image",
"repository_name": "image",
"expected_count": 3,
"actual_count": 2,
"missing_tags": {
"1.2": {
"expected": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890",
"actual": ""
}
},
"unexpected_tags": {
"dev": {
"expected": "",
"actual": "sha256:xyz789ab123456789012345678901234567890123456789012345678901234"
}
},
"mismatched_tags": {
"latest": {
"expected": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
"actual": "sha256:old123ef456789012345678901234567890123456789012345678901234567890"
}
},
"status": "missing_tags"
}All tag analysis results (missing_tags, unexpected_tags, mismatched_tags) use a consistent structure:
-
missing_tags: Tags that should exist but don'texpected: The SHA256 digest the tag should point toactual: Empty string (tag doesn't exist)
-
unexpected_tags: Tags that exist but shouldn'texpected: Empty string (tag shouldn't exist)actual: The SHA256 digest the tag currently points to
-
mismatched_tags: Tags that exist but point to wrong digestexpected: The SHA256 digest the tag should point toactual: The SHA256 digest the tag currently points to
This unified structure makes programmatic processing easier and provides clear semantics about what was expected versus what was actually found.
Catalog Command (schema):
{
"repository_url": "gcr.io/project/image",
"repository_name": "image",
"images": [
{
"tag": "latest",
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
"canonical_version": "1.2.3"
},
{
"tag": "1",
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
"canonical_version": "1.2.3"
},
{
"tag": "1.2",
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
"canonical_version": "1.2.3"
}
]
}Uncatalog Command: Converts catalog format back to expected/actual format (same schema as expected/actual commands).
Human-readable table format for quick visual inspection.
- Registry Support: Public Google Container Registry (GCR) only
- Tag Format: Only processes semantic version tags (MAJOR.MINOR.PATCH)
- URL Format: Requires full repository URLs:
[REGISTRY_HOST]/[NAMESPACE]/[REPOSITORY]
To build the Docker image locally, you need the gem artifact in the pkg/ directory:
# Build the gem using the same method as CI
bundle exec rake clobber # Clean previous builds
bundle exec rake build # Rebuild gem
# Build image
podman build -f docker/Dockerfile -t equilibrium:local .
# Test the local build
podman run --rm localhost/equilibrium:local equilibrium versionNote: The Dockerfile expects the gem artifact in pkg/*.gem. Using bundle exec rake build ensures you build the gem exactly the same way as the CI pipeline.
- Ruby (>= 3.0.0) with Bundler
- No authentication required for public Google Container Registry (GCR) access
- Pure Ruby implementation - uses only Ruby standard library (Net::HTTP) for HTTP requests
# Ruby linting
bundle exec standardrb
bundle exec standardrb --fix
# Run tests
bundle exec rspecTo create a new release:
# Update version in lib/equilibrium/version.rb, then:
./bin/releaseThe script automatically:
- Reads the version from
equilibrium.gemspec - Creates a git tag with
vprefix (e.g.,v0.1.1) - Pushes the tag to trigger the automated release workflow
The GitHub Actions workflow validates that the tag version matches the gemspec version before publishing to RubyGems and GitHub Packages.
MIT License - see LICENSE file for details.