equilibrium 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22d45ae6c6368e01aa50e5c7abc828bee9a9f759beb57c2c80dc7f579e1b50da
4
- data.tar.gz: 445f71cbc4c40d5d60bea3a456197a9de6d817bc32cc4bf87aa678d9c5691231
3
+ metadata.gz: 495555ab3b3d0695f4e559e5372c5d8ab83ff131d11a9758dde1aa0df497472e
4
+ data.tar.gz: 4ce686d07757a3f5732cdc58a3e48c9084a1ddf4eef2130ba1cb5af5e5b10b95
5
5
  SHA512:
6
- metadata.gz: 0fbcb1559ecd07a6431b36342ca067f69cda326f547e32a70e256bfce35b762d76073b755a5365407b87ad6be9ff862eb5232a6866974a88d54ec1cebf1b304c
7
- data.tar.gz: 1f1b80fa1c4d4b08546e7cf77c5254de3ebca107c63f0d0c15d317628380d211f247ab0586248d4476d9bb5ed4e86302ad2abb5bd1777b34d0c5fa34054a0d42
6
+ metadata.gz: 8b69760396a2992caaafcaf2f219290a78555dbf2c474e52a38120d5b5cd81a549ba9e9c01afc7990239706d1a8be7d9db3ac2218152ba3bf3bf1597eff1b196
7
+ data.tar.gz: b33a4bc6bd29f7492850af6e310996c1c3967bb93f577b587086025307cce85b39bc67bb4f47b1cc169f894ac98a29c6f52cc827824b83604b2aec2b2b2989ac
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.8
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2025-08-26
11
+
12
+ ### Fixed
13
+ - Fix changelog format
14
+
15
+ ## [0.3.0] - 2025-08-26
16
+
17
+ ### Added
18
+ - Docker distribution support
19
+
20
+ ### Changed
21
+ - Use Ruby 3.4.5
22
+ - Unified tag structures across analyze command output
23
+ - Improved summary format for better readability and consistency
24
+ - Remove remediation_plan field from analyze command output
25
+
10
26
  ## [0.2.0] - 2025-08-15
11
27
 
12
28
  ### Added
@@ -54,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
70
  - Detailed architecture overview and data flow diagrams
55
71
  - Complete command reference and examples
56
72
 
73
+ [0.3.1]: https://github.com/TonyCTHsu/equilibrium/releases/tag/v0.3.1
74
+ [0.3.0]: https://github.com/TonyCTHsu/equilibrium/releases/tag/v0.3.0
57
75
  [0.2.0]: https://github.com/TonyCTHsu/equilibrium/releases/tag/v0.2.0
58
76
  [0.1.1]: https://github.com/TonyCTHsu/equilibrium/releases/tag/v0.1.1
59
77
  [0.1.0]: https://github.com/TonyCTHsu/equilibrium/releases/tag/v0.1.0
data/README.md CHANGED
@@ -53,7 +53,7 @@ flowchart LR
53
53
  H --> I[Compare<br/>Expected vs<br/>Actual]
54
54
  F --> I
55
55
 
56
- I --> J[Analysis &<br/>Remediation<br/>Update: latest]
56
+ I --> J[Analysis Report<br/>Missing: 1<br/>Mismatched: latest]
57
57
 
58
58
  H --> K[catalog<br/>Command]
59
59
  K --> L[Catalog Format<br/>External Integration]
@@ -81,7 +81,7 @@ flowchart LR
81
81
  2. **Filter Semantic**: Extract only valid semantic version tags (MAJOR.MINOR.PATCH)
82
82
  3. **Compute Expected**: Generate mutable tags based on semantic versions
83
83
  4. **Fetch Actual**: Query registry for current mutable tag state
84
- 5. **Compare & Analyze**: Identify mismatches and generate remediation plan
84
+ 5. **Compare & Analyze**: Identify missing, unexpected, and mismatched tags with unified structure
85
85
  6. **Format Conversion**: Transform between expected/actual and catalog formats for external integration
86
86
 
87
87
  ## Installation
@@ -90,6 +90,13 @@ flowchart LR
90
90
  gem install equilibrium
91
91
  ```
92
92
 
93
+ For containerized environments, use the Docker image:
94
+
95
+ ```bash
96
+ # Pull the latest version
97
+ docker pull ghcr.io/tonycthsu/equilibrium:latest
98
+ ```
99
+
93
100
  *For other installation methods, see `equilibrium help`*
94
101
 
95
102
  ## Quick Start
@@ -102,7 +109,7 @@ equilibrium expected "$REPO"
102
109
  # 2. Check what mutable tags actually exist
103
110
  equilibrium actual "$REPO"
104
111
 
105
- # 3. Compare and get remediation plan
112
+ # 3. Compare and analyze differences
106
113
  equilibrium expected "$REPO" --format json > expected.json
107
114
  equilibrium actual "$REPO" --format json > actual.json
108
115
  equilibrium analyze --expected expected.json --actual actual.json
@@ -209,37 +216,49 @@ All commands output structured JSON following validated schemas (see [schemas](l
209
216
  ```json
210
217
  {
211
218
  "repository_url": "gcr.io/project/image",
219
+ "repository_name": "image",
212
220
  "expected_count": 3,
213
221
  "actual_count": 2,
214
222
  "missing_tags": {
215
- "1.2": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890"
223
+ "1.2": {
224
+ "expected": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890",
225
+ "actual": ""
226
+ }
227
+ },
228
+ "unexpected_tags": {
229
+ "dev": {
230
+ "expected": "",
231
+ "actual": "sha256:xyz789ab123456789012345678901234567890123456789012345678901234"
232
+ }
216
233
  },
217
- "unexpected_tags": {},
218
234
  "mismatched_tags": {
219
235
  "latest": {
220
236
  "expected": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
221
237
  "actual": "sha256:old123ef456789012345678901234567890123456789012345678901234567890"
222
238
  }
223
239
  },
224
- "status": "missing_tags",
225
- "remediation_plan": [
226
- {
227
- "action": "create_tag",
228
- "tag": "1.2",
229
- "digest": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890",
230
- "command": "gcloud container images add-tag gcr.io/project/image@sha256:abc123... gcr.io/project/image:1.2"
231
- },
232
- {
233
- "action": "update_tag",
234
- "tag": "latest",
235
- "old_digest": "sha256:old123ef456789012345678901234567890123456789012345678901234567890",
236
- "new_digest": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
237
- "command": "gcloud container images add-tag gcr.io/project/image@sha256:def456... gcr.io/project/image:latest"
238
- }
239
- ]
240
+ "status": "missing_tags"
240
241
  }
241
242
  ```
242
243
 
244
+ #### Unified Tag Structure
245
+
246
+ All tag analysis results (`missing_tags`, `unexpected_tags`, `mismatched_tags`) use a consistent structure:
247
+
248
+ - **`missing_tags`**: Tags that should exist but don't
249
+ - `expected`: The SHA256 digest the tag should point to
250
+ - `actual`: Empty string (tag doesn't exist)
251
+
252
+ - **`unexpected_tags`**: Tags that exist but shouldn't
253
+ - `expected`: Empty string (tag shouldn't exist)
254
+ - `actual`: The SHA256 digest the tag currently points to
255
+
256
+ - **`mismatched_tags`**: Tags that exist but point to wrong digest
257
+ - `expected`: The SHA256 digest the tag should point to
258
+ - `actual`: The SHA256 digest the tag currently points to
259
+
260
+ This unified structure makes programmatic processing easier and provides clear semantics about what was expected versus what was actually found.
261
+
243
262
  **Catalog Command** ([schema](lib/equilibrium/schemas/catalog.rb)):
244
263
  ```json
245
264
  {
@@ -276,6 +295,24 @@ Human-readable table format for quick visual inspection.
276
295
  - **Tag Format**: Only processes semantic version tags (MAJOR.MINOR.PATCH)
277
296
  - **URL Format**: Requires full repository URLs: `[REGISTRY_HOST]/[NAMESPACE]/[REPOSITORY]`
278
297
 
298
+ ### Local Image Build
299
+
300
+ To build the Docker image locally, you need the gem artifact in the `pkg/` directory:
301
+
302
+ ```bash
303
+ # Build the gem using the same method as CI
304
+ bundle exec rake clobber # Clean previous builds
305
+ bundle exec rake build # Rebuild gem
306
+
307
+ # Build image
308
+ podman build -f docker/Dockerfile -t equilibrium:local .
309
+
310
+ # Test the local build
311
+ podman run --rm localhost/equilibrium:local equilibrium version
312
+ ```
313
+
314
+ **Note**: 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.
315
+
279
316
  ## Development
280
317
 
281
318
  ### Prerequisites
@@ -26,7 +26,7 @@ module Equilibrium
26
26
  final_repository_name = expected_name
27
27
  final_repository_url = expected_url || actual_url
28
28
 
29
- analysis = {
29
+ {
30
30
  repository_url: final_repository_url,
31
31
  repository_name: final_repository_name,
32
32
  expected_count: expected_tags.size,
@@ -36,54 +36,34 @@ module Equilibrium
36
36
  mismatched_tags: find_mismatched_tags(expected_tags, actual_tags),
37
37
  status: determine_status(expected_tags, actual_tags)
38
38
  }.compact
39
-
40
- # Add remediation plan for JSON format
41
- analysis[:remediation_plan] = generate_remediation_plan(analysis, final_repository_url, final_repository_name)
42
- analysis
43
39
  end
44
40
 
45
41
  private
46
42
 
47
- def generate_remediation_plan(analysis, repository_url, repository_name)
48
- plan = []
49
-
50
- analysis[:missing_tags].each do |tag, digest|
51
- plan << {
52
- action: "create_tag",
53
- tag: tag,
54
- digest: digest,
55
- command: "gcloud container images add-tag #{repository_url}@#{digest} #{repository_url}:#{tag}"
56
- }.compact
57
- end
58
-
59
- analysis[:mismatched_tags].each do |tag, data|
60
- plan << {
61
- action: "update_tag",
62
- tag: tag,
63
- old_digest: data[:actual],
64
- new_digest: data[:expected],
65
- command: "gcloud container images add-tag #{repository_url}@#{data[:expected]} #{repository_url}:#{tag}"
66
- }.compact
67
- end
68
-
69
- analysis[:unexpected_tags].each do |tag, digest|
70
- plan << {
71
- action: "remove_tag",
72
- tag: tag,
73
- digest: digest,
74
- command: "gcloud container images untag #{repository_url}:#{tag}"
75
- }.compact
76
- end
77
-
78
- plan
79
- end
80
-
81
43
  def find_missing_tags(expected, actual)
82
- expected.reject { |tag, digest| actual.key?(tag) }
44
+ missing = {}
45
+ expected.each do |tag, digest|
46
+ unless actual.key?(tag)
47
+ missing[tag] = {
48
+ expected: digest,
49
+ actual: ""
50
+ }
51
+ end
52
+ end
53
+ missing
83
54
  end
84
55
 
85
56
  def find_unexpected_tags(expected, actual)
86
- actual.reject { |tag, _| expected.key?(tag) }
57
+ unexpected = {}
58
+ actual.each do |tag, digest|
59
+ unless expected.key?(tag)
60
+ unexpected[tag] = {
61
+ expected: "",
62
+ actual: digest
63
+ }
64
+ end
65
+ end
66
+ unexpected
87
67
  end
88
68
 
89
69
  def find_mismatched_tags(expected, actual)
@@ -16,7 +16,7 @@ module Equilibrium
16
16
  true
17
17
  end
18
18
 
19
- desc "analyze", "Compare expected vs actual tags and generate remediation plan"
19
+ desc "analyze", "Compare expected vs actual tags and show analysis report"
20
20
  option :expected, type: :string, required: true, desc: "Expected tags JSON file"
21
21
  option :actual, type: :string, required: true, desc: "Actual tags JSON file"
22
22
  option :registry, type: :string, desc: "Repository URL for output"
@@ -11,7 +11,7 @@ require_relative "../analyzer"
11
11
 
12
12
  module Equilibrium
13
13
  module Commands
14
- # Command for analyzing expected vs actual tags and generating remediation plan
14
+ # Command for analyzing expected vs actual tags with unified structure
15
15
  class AnalyzeCommand
16
16
  include Mixins::ErrorHandling
17
17
  include Mixins::InputOutput
@@ -14,21 +14,26 @@ module Equilibrium
14
14
  # "missing_tags": {},
15
15
  # "unexpected_tags": {},
16
16
  # "mismatched_tags": {},
17
- # "status": "perfect",
18
- # "remediation_plan": []
17
+ # "status": "perfect"
19
18
  # }
20
19
  #
21
- # Example output (requires remediation):
20
+ # Example output (with discrepancies):
22
21
  # {
23
22
  # "repository_url": "gcr.io/datadoghq/example",
24
23
  # "repository_name": "example",
25
24
  # "expected_count": 3,
26
25
  # "actual_count": 2,
27
26
  # "missing_tags": {
28
- # "latest": "sha256:abc123ef456789..."
27
+ # "latest": {
28
+ # "expected": "sha256:abc123ef456789...",
29
+ # "actual": ""
30
+ # }
29
31
  # },
30
32
  # "unexpected_tags": {
31
- # "dev": "sha256:xyz789ab123456..."
33
+ # "dev": {
34
+ # "expected": "",
35
+ # "actual": "sha256:xyz789ab123456..."
36
+ # }
32
37
  # },
33
38
  # "mismatched_tags": {
34
39
  # "0.1": {
@@ -36,15 +41,7 @@ module Equilibrium
36
41
  # "actual": "sha256:old123ef456789..."
37
42
  # }
38
43
  # },
39
- # "status": "missing_tags",
40
- # "remediation_plan": [
41
- # {
42
- # "action": "create_tag",
43
- # "tag": "latest",
44
- # "digest": "sha256:abc123ef456789...",
45
- # "command": "gcloud container images add-tag gcr.io/datadoghq/example@sha256:abc123... gcr.io/datadoghq/example:latest"
46
- # }
47
- # ]
44
+ # "status": "missing_tags"
48
45
  # }
49
46
  ANALYZER_OUTPUT = {
50
47
  "$schema" => "https://json-schema.org/draft/2020-12/schema",
@@ -53,7 +50,7 @@ module Equilibrium
53
50
  "type" => "object",
54
51
  "required" => [
55
52
  "repository_url",
56
- "repository_name", "expected_count", "actual_count", "missing_tags", "unexpected_tags", "mismatched_tags", "status", "remediation_plan"
53
+ "repository_name", "expected_count", "actual_count", "missing_tags", "unexpected_tags", "mismatched_tags", "status"
57
54
  ],
58
55
  "properties" => {
59
56
  "repository_url" => {"type" => "string"},
@@ -64,13 +61,27 @@ module Equilibrium
64
61
  "missing_tags" => {
65
62
  "type" => "object",
66
63
  "patternProperties" => {
67
- ".*" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"}
64
+ ".*" => {
65
+ "type" => "object",
66
+ "required" => ["expected", "actual"],
67
+ "properties" => {
68
+ "expected" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
69
+ "actual" => {"type" => "string", "enum" => [""]}
70
+ }
71
+ }
68
72
  }
69
73
  },
70
74
  "unexpected_tags" => {
71
75
  "type" => "object",
72
76
  "patternProperties" => {
73
- ".*" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"}
77
+ ".*" => {
78
+ "type" => "object",
79
+ "required" => ["expected", "actual"],
80
+ "properties" => {
81
+ "expected" => {"type" => "string", "enum" => [""]},
82
+ "actual" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"}
83
+ }
84
+ }
74
85
  }
75
86
  },
76
87
  "mismatched_tags" => {
@@ -85,21 +96,6 @@ module Equilibrium
85
96
  }
86
97
  }
87
98
  }
88
- },
89
- "remediation_plan" => {
90
- "type" => "array",
91
- "items" => {
92
- "type" => "object",
93
- "required" => ["action", "tag"],
94
- "properties" => {
95
- "action" => {"enum" => ["create_tag", "update_tag", "remove_tag"]},
96
- "tag" => {"type" => "string"},
97
- "digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
98
- "old_digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
99
- "new_digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
100
- "command" => {"type" => "string"}
101
- }
102
- }
103
99
  }
104
100
  }
105
101
  }.freeze
@@ -43,9 +43,9 @@ module Equilibrium
43
43
  has_issues = true
44
44
  say "Missing Tags (should be created):"
45
45
  missing_table = [["Tag", "Should Point To"]]
46
- analysis[:missing_tags].each do |tag, digest|
47
- short_digest = digest ? digest.split(":").last[0..11] : "unknown"
48
- missing_table << [tag, short_digest]
46
+ analysis[:missing_tags].each do |tag, details|
47
+ full_digest = details[:expected] || "unknown"
48
+ missing_table << [tag, full_digest]
49
49
  end
50
50
  print_table(missing_table, borders: true)
51
51
  say ""
@@ -58,10 +58,10 @@ module Equilibrium
58
58
  mismatched_table = [["Tag", "Expected", "Actual"]]
59
59
  analysis[:mismatched_tags].each do |tag, details|
60
60
  if details.is_a?(Hash)
61
- expected = details[:expected] ? details[:expected].split(":").last[0..11] : "unknown"
62
- actual = details[:actual] ? details[:actual].split(":").last[0..11] : "unknown"
61
+ expected = details[:expected] || "unknown"
62
+ actual = details[:actual] || "unknown"
63
63
  else
64
- expected = details ? details.split(":").last[0..11] : "unknown"
64
+ expected = details || "unknown"
65
65
  actual = "unknown"
66
66
  end
67
67
  mismatched_table << [tag, expected, actual]
@@ -75,17 +75,17 @@ module Equilibrium
75
75
  has_issues = true
76
76
  say "Unexpected Tags (should be removed):"
77
77
  unexpected_table = [["Tag", "Currently Points To"]]
78
- analysis[:unexpected_tags].each do |tag, digest|
79
- short_digest = digest ? digest.split(":").last[0..11] : "unknown"
80
- unexpected_table << [tag, short_digest]
78
+ analysis[:unexpected_tags].each do |tag, details|
79
+ full_digest = details[:actual] || "unknown"
80
+ unexpected_table << [tag, full_digest]
81
81
  end
82
82
  print_table(unexpected_table, borders: true)
83
83
  say ""
84
84
  end
85
85
 
86
86
  if has_issues
87
- say "To see detailed remediation commands, use:"
88
- say " equilibrium analyze --expected expected.json --actual actual.json --format=json | jq '.remediation_plan'"
87
+ say "To see detailed analysis data, use:"
88
+ say " equilibrium analyze --expected expected.json --actual actual.json --format=json"
89
89
  else
90
90
  say "✓ Registry is in perfect equilibrium!", :green
91
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Equilibrium
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: equilibrium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Hsu
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-08-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: thor
@@ -96,16 +95,14 @@ files:
96
95
  - lib/equilibrium/tag_sorter.rb
97
96
  - lib/equilibrium/tags_operation_service.rb
98
97
  - lib/equilibrium/version.rb
99
- - tmp/.gitkeep
100
98
  homepage: https://github.com/TonyCTHsu/equilibrium
101
99
  licenses:
102
100
  - MIT
103
101
  metadata:
104
102
  homepage_uri: https://github.com/TonyCTHsu/equilibrium
105
103
  source_code_uri: https://github.com/TonyCTHsu/equilibrium
106
- changelog_uri: https://github.com/TonyCTHsu/equilibrium/blob/v0.2.0/CHANGELOG.md
104
+ changelog_uri: https://github.com/TonyCTHsu/equilibrium/blob/v0.3.1/CHANGELOG.md
107
105
  github_repo: ssh://github.com/TonyCTHsu/equilibrium
108
- post_install_message:
109
106
  rdoc_options: []
110
107
  require_paths:
111
108
  - lib
@@ -120,8 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
117
  - !ruby/object:Gem::Version
121
118
  version: '0'
122
119
  requirements: []
123
- rubygems_version: 3.4.19
124
- signing_key:
120
+ rubygems_version: 3.6.9
125
121
  specification_version: 4
126
122
  summary: Container image tag validation tool
127
123
  test_files: []
data/tmp/.gitkeep DELETED
@@ -1,2 +0,0 @@
1
- # This file ensures the tmp/ directory is tracked by git
2
- # while keeping all other contents ignored via .gitignore