metanorma-release 0.2.21 → 0.2.23

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: 870b32feb3cf51fae8f217ca433e725d783904cd986947264f886e16bc30e03b
4
- data.tar.gz: f37c695d3ed8bd4becde735a1eb7f0d598ac2e1c946a047c3574d910eff08596
3
+ metadata.gz: fcbd94afb53b355f5637604bd713ec156adaf4e061c49dda44c1cf6cf777938a
4
+ data.tar.gz: 6dfa4f1d17e6aa4bac1bccc991510d9ac3856012d2205c9ce46644aa045d1ede
5
5
  SHA512:
6
- metadata.gz: 8d3292688c6e9043a2c05c75704fc3362e07344285c2d928e2aa85b3e48592aed79ed573424c36ca583e977e745640d44609887849511bb5853d093c38ae40e3
7
- data.tar.gz: 20c61ff249f15acc7022c24acf13206dfc4d08193168a9597d96a4c32df98166e54c1ab7f1a339e894f1bd43fc42d9e2fca43cbb11544551c3629002267d1010
6
+ metadata.gz: 5acb0eb4eaaa919c5a5f5f5a868aea80cc0d1de643fc9377d4497e0148f6b84171dee1208832f037b3dcab9ab285e8e7fdcba600cb0db99a2169ed4da484f50d
7
+ data.tar.gz: 9e55e666c20c4e5c93a2545ff5d8927328721878f83ae8f4affdbbe40bc31d94f021f796ce437d3389297eeecca802875dc2f3174545db67f2d586218f10156c
@@ -0,0 +1,28 @@
1
+ # 01 — Extract routing domain model (OCP / model-driven)
2
+
3
+ ## Problem
4
+ `Config` mixes data access with routing behavior. Matching logic for pattern,
5
+ source, stage, and doctype is embedded as private methods using raw hashes.
6
+ This violates OCP — adding a new routing criterion requires modifying Config.
7
+
8
+ ## Solution
9
+ Extract value objects that encapsulate matching logic:
10
+
11
+ - `DocumentEntry` — matches by `pattern` (slug glob) or `source` (file path)
12
+ - `RoutingRule` — matches by `stage` and/or `doctype`
13
+ - `ChannelResolver` — composes strategies, resolves channels for a Publication
14
+
15
+ Config becomes a pure data reader. ChannelResolver owns the behavior.
16
+ New routing criteria = new value object + register in resolver, no existing code changes.
17
+
18
+ ## Files
19
+ - Create `lib/metanorma/release/channel_resolver.rb`
20
+ - Update `lib/metanorma/release/config.rb` — remove private routing methods
21
+ - Update `lib/metanorma/release/release_pipeline.rb` — use ChannelResolver
22
+ - Update `lib/metanorma/release.rb` — add autoload
23
+ - Update `spec/domain/config_spec.rb` — move routing specs
24
+ - Create `spec/domain/channel_resolver_spec.rb`
25
+
26
+ ## Status
27
+ - [x] Implement — DocumentEntry merged with RoutingRule, single `documents` list
28
+ - [x] Specs pass — 234 examples, 0 failures
@@ -0,0 +1,31 @@
1
+ # 02 — Update metanorma.org documentation
2
+
3
+ ## Problem
4
+ Three documentation files still reference the old three-file architecture
5
+ (org config repo, per-repo channels.yml, org: key).
6
+
7
+ ## Files to update
8
+
9
+ ### `_pages/install/publication-setup.adoc`
10
+ - Remove org config repo section and channels.yml schema
11
+ - Remove `org:` key from config examples
12
+ - Remove `.metanorma/channels.yml` from file checklist
13
+ - Move `display_categories` into aggregator config schema
14
+ - Document two-file architecture: release manifest + aggregate config
15
+
16
+ ### `_posts/2026-05-13-channel-based-publication.adoc`
17
+ - Remove `.metanorma/channels.yml` sections (lines 64-77, 124-130, 238-242)
18
+ - Remove CalConnect/.metanorma references (line 504)
19
+ - Update `display_category` reference (line 449) — now in aggregator config
20
+ - Reflect two-file architecture throughout
21
+
22
+ ### `_pages/install/cicd.adoc`
23
+ - Remove "Channel discovery manifest" subsection (lines 476-483)
24
+ - Remove `channels.yml` from file tree (line 224)
25
+ - Remove `.metanorma/channels.yml` section (line 243)
26
+ - Remove org config references (lines 408-432)
27
+
28
+ ## Status
29
+ - [x] publication-setup.adoc
30
+ - [x] blog post
31
+ - [x] cicd.adoc
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+ # Delete .metanorma/channels.yml (and .metanorma/ dir) from CalConnect repos.
3
+ # Usage: bash TODO.cleanups/03-cleanup-repos.sh [--dry-run]
4
+ #
5
+ # This removes dead config files — the gem never reads per-repo channels.yml.
6
+
7
+ set -uo pipefail
8
+
9
+ DRY_RUN=false
10
+ [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
11
+
12
+ BRANCH="chore/remove-dead-channels-yml"
13
+ COMMIT_MSG="chore: remove dead .metanorma/channels.yml
14
+
15
+ The metanorma-release gem never reads per-repo .metanorma/channels.yml.
16
+ Channel routing is configured in metanorma.release.yml via documents[]
17
+ entries. This file was dead code that created confusion about the
18
+ architecture."
19
+
20
+ count=0
21
+ skipped=0
22
+
23
+ for channels_file in $(find /Users/mulgogi/src/calconnect -maxdepth 3 -name "channels.yml" -path "*/.metanorma/*" 2>/dev/null | sort); do
24
+ metanorma_dir=$(dirname "$channels_file")
25
+ repo_dir=$(dirname "$metanorma_dir")
26
+ repo_name=$(basename "$repo_dir")
27
+
28
+ # Skip the org config repo
29
+ if [[ "$repo_name" == "dot-metanorma" ]]; then
30
+ echo "SKIP: $repo_name (org config repo — handle separately)"
31
+ skipped=$((skipped + 1))
32
+ continue
33
+ fi
34
+
35
+ count=$((count + 1))
36
+
37
+ if $DRY_RUN; then
38
+ echo "DRY: $repo_name — would remove $channels_file"
39
+ continue
40
+ fi
41
+
42
+ cd "$repo_dir"
43
+
44
+ # Check for uncommitted changes
45
+ if ! git diff --quiet HEAD 2>/dev/null; then
46
+ echo "SKIP: $repo_name — uncommitted changes"
47
+ continue
48
+ fi
49
+
50
+ # Create branch from main
51
+ git checkout main -q 2>/dev/null
52
+ git pull -q 2>/dev/null || true
53
+ git checkout -b "$BRANCH" -q 2>/dev/null || git checkout "$BRANCH" -q
54
+
55
+ # Remove the file and directory
56
+ rm -rf "$metanorma_dir"
57
+
58
+ # Commit and push
59
+ git add -A
60
+ if git diff --cached --quiet; then
61
+ echo "SKIP: $repo_name — no changes (already removed?)"
62
+ git checkout main -q 2>/dev/null
63
+ continue
64
+ fi
65
+
66
+ git commit -m "$COMMIT_MSG" -q
67
+ git push -u origin "$BRANCH" -q 2>/dev/null
68
+
69
+ # Create PR
70
+ gh pr create \
71
+ --title "chore: remove dead .metanorma/channels.yml" \
72
+ --body "$COMMIT_MSG" \
73
+ --base main \
74
+ --head "$BRANCH" \
75
+ 2>/dev/null || echo "WARN: $repo_name — PR creation failed (may already exist)"
76
+
77
+ git checkout main -q 2>/dev/null
78
+
79
+ echo "DONE: $repo_name"
80
+ done
81
+
82
+ echo ""
83
+ echo "Processed: $count repos, skipped: $skipped"
84
+ if $DRY_RUN; then
85
+ echo "(dry run — no changes made)"
86
+ fi
@@ -0,0 +1,20 @@
1
+ # 03 — Delete .metanorma/channels.yml from 55 CalConnect repos
2
+
3
+ ## Problem
4
+ 55 CalConnect repos have `.metanorma/channels.yml` that is dead code.
5
+ The gem never reads these files. They create confusion about the architecture.
6
+
7
+ ## Approach
8
+ Script-based cleanup via GitHub API:
9
+ 1. Find all repos with `.metanorma/channels.yml`
10
+ 2. Create branch `chore/remove-dead-channels-yml`
11
+ 3. Delete `.metanorma/channels.yml` (and `.metanorma/` dir if empty)
12
+ 4. Commit, push, create PR
13
+
14
+ ## Repo list (55 repos)
15
+ Found via: `find /Users/mulgogi/src/calconnect -maxdepth 3 -name "channels.yml"`
16
+
17
+ ## Status
18
+ - [x] Script written — `03-cleanup-repos.sh`
19
+ - [x] Dry run verified — 54 repos found
20
+ - [x] Run on all repos — 53 branches pushed, 53 PRs created, all 53 rebase-merged
@@ -0,0 +1,13 @@
1
+ # 04 — Version bump
2
+
3
+ ## Problem
4
+ Breaking changes require a version bump. Removed OrgConfig class, stages filter,
5
+ and org: key support. Added pattern-based routing, display_categories in
6
+ aggregator config.
7
+
8
+ ## Changes
9
+ - Bump version in `lib/metanorma/release/version.rb`
10
+ - Update CHANGELOG if present
11
+
12
+ ## Status
13
+ - [x] Bumped to 0.2.21
@@ -84,7 +84,7 @@ module Metanorma
84
84
 
85
85
  private
86
86
 
87
- def process_repo(repo, _output_dir, config)
87
+ def process_repo(repo, output_dir, config)
88
88
  repo_key = repo.to_s
89
89
 
90
90
  manifest_channels = @deps.manifest_reader.read(repo)
@@ -120,9 +120,11 @@ module Metanorma
120
120
 
121
121
  if @deps.delta_state.processed?(repo_key, tag, content_hash)
122
122
  files = @deps.delta_state.release_files(repo_key, tag)
123
- publications << build_publication(metadata, files, content_hash,
124
- release, repo)
125
- next
123
+ if files.all? { |f| File.exist?(File.join(output_dir, f)) }
124
+ publications << build_publication(metadata, files, content_hash,
125
+ release, repo)
126
+ next
127
+ end
126
128
  end
127
129
 
128
130
  zip_asset = find_zip_asset(release)
@@ -3,15 +3,13 @@
3
3
  module Metanorma
4
4
  module Release
5
5
  class MetadataFilter
6
- def initialize(channels: [], stages: [])
6
+ def initialize(channels: [])
7
7
  @channels = channels.map { |c| Channel.new(c) }
8
- @stages = Set.new(stages.map(&:downcase))
9
8
  @all_channels = @channels.empty?
10
- @all_stages = @stages.empty?
11
9
  end
12
10
 
13
11
  def matches?(release_metadata)
14
- channel_match?(release_metadata) && stage_match?(release_metadata)
12
+ channel_match?(release_metadata)
15
13
  end
16
14
 
17
15
  def overlaps?(manifest_channels)
@@ -31,12 +29,6 @@ module Metanorma
31
29
  end
32
30
  release_channels.any? { |rc| rc.matches?(@channels) }
33
31
  end
34
-
35
- def stage_match?(release_metadata)
36
- return true if @all_stages
37
-
38
- @stages.include?(release_metadata["stage"].to_s.downcase)
39
- end
40
32
  end
41
33
  end
42
34
  end
@@ -75,8 +75,6 @@ module Metanorma
75
75
  option :repos, type: :array, desc: "Explicit repo list"
76
76
  option :channels, type: :array, default: [],
77
77
  desc: "Filter channels"
78
- option :stages, type: :array, default: [],
79
- desc: "Filter stages"
80
78
  option :output_dir, type: :string, default: "_site/cc",
81
79
  desc: "Output directory"
82
80
  option :file_routing, type: :string,
@@ -98,7 +96,6 @@ module Metanorma
98
96
  topic: options[:topic],
99
97
  repos: options[:repos],
100
98
  channels: options[:channels],
101
- stages: options[:stages],
102
99
  output_dir: options[:output_dir],
103
100
  file_routing: options[:file_routing],
104
101
  cache_dir: options[:cache_dir],
@@ -8,9 +8,9 @@ module Metanorma
8
8
  class AggregateCommand
9
9
  Config = Struct.new(
10
10
  :source, :organizations, :topic, :repos, :repo_pattern, :local_path,
11
- :channels, :stages, :output_dir, :file_routing, :cache_dir,
11
+ :channels, :output_dir, :file_routing, :cache_dir,
12
12
  :data_dir, :include_drafts, :concurrency, :min_documents, :token,
13
- :create_zip, :org,
13
+ :create_zip, :display_categories,
14
14
  keyword_init: true
15
15
  )
16
16
 
@@ -19,17 +19,16 @@ module Metanorma
19
19
 
20
20
  def initialize(config)
21
21
  @config = config
22
- @org_config = nil
23
22
  end
24
23
 
25
24
  def call
26
- load_org_config
27
25
  result = run_aggregation
28
26
  return result unless result.publications.any?
29
27
 
30
28
  index = build_index(result)
31
29
  site = Site.new(index: index, output_dir: @config.output_dir,
32
- data_dir: @config.data_dir, org_config: @org_config)
30
+ data_dir: @config.data_dir,
31
+ display_categories: @config.display_categories)
33
32
  site.write!
34
33
  site.enrich!
35
34
  site.package! if @config.create_zip
@@ -47,7 +46,6 @@ module Metanorma
47
46
  topic: merged[:topic],
48
47
  repos: merged[:repos],
49
48
  channels: merged[:channels],
50
- stages: merged[:stages],
51
49
  output_dir: merged[:output_dir],
52
50
  file_routing: merged[:file_routing],
53
51
  cache_dir: merged[:cache_dir] || DEFAULT_CACHE_DIR,
@@ -57,7 +55,7 @@ module Metanorma
57
55
  min_documents: merged[:min_documents],
58
56
  token: merged[:token],
59
57
  create_zip: merged[:create_zip],
60
- org: merged[:org],
58
+ display_categories: merged[:display_categories],
61
59
  )
62
60
  end
63
61
 
@@ -76,7 +74,6 @@ module Metanorma
76
74
  topic: cli_options[:topic] || gh["topic"],
77
75
  repos: cli_options[:repos] || file_data["repos"],
78
76
  channels: cli_options[:channels].any? ? cli_options[:channels] : Array(file_data["channels"]),
79
- stages: cli_options[:stages].any? ? cli_options[:stages] : Array(file_data["stages"]),
80
77
  output_dir: cli_options[:output_dir] || file_data["output_dir"],
81
78
  file_routing: cli_options[:file_routing] || file_data["file_routing"] || "by-document",
82
79
  cache_dir: cli_options[:cache_dir] || file_data["cache_dir"],
@@ -86,7 +83,7 @@ module Metanorma
86
83
  min_documents: cli_options[:min_documents] || file_data["min_documents"],
87
84
  token: cli_options[:token],
88
85
  create_zip: cli_options[:create_zip],
89
- org: file_data["org"],
86
+ display_categories: file_data["display_categories"] || [],
90
87
  }
91
88
  end
92
89
 
@@ -104,7 +101,6 @@ module Metanorma
104
101
 
105
102
  metadata_filter = MetadataFilter.new(
106
103
  channels: Channel.parse_list(@config.channels),
107
- stages: @config.stages || [],
108
104
  )
109
105
  routing = FileRoutingFactory.from_name(@config.file_routing)
110
106
  asset_processor = AssetProcessor.new(
@@ -169,27 +165,6 @@ module Metanorma
169
165
  rescue LoadError
170
166
  warn " (relaton gem not available — bibliography skipped)"
171
167
  end
172
-
173
- def load_org_config
174
- return unless @config.org
175
-
176
- ref = OrgConfig.parse_ref(@config.org)
177
- local_path = OrgConfig.remote_path(ref)
178
- @org_config = File.exist?(local_path) ? OrgConfig.from_file(local_path) : fetch_org_config_from_github(ref)
179
- end
180
-
181
- def fetch_org_config_from_github(ref)
182
- require "octokit"
183
- token = @config.token || ENV.fetch("GITHUB_TOKEN", nil)
184
- client = token ? Octokit::Client.new(access_token: token) : Octokit::Client.new
185
- remote = OrgConfig.remote_path(ref)
186
- contents = client.contents("#{ref.owner}/#{ref.repo}", path: remote)
187
- decoded = Base64.decode64(contents[:content])
188
- OrgConfig.from_yaml(decoded)
189
- rescue StandardError => e
190
- warn " (org config not loaded from #{ref.owner}/#{ref.repo}: #{e.message})"
191
- OrgConfig.defaults
192
- end
193
168
  end
194
169
  end
195
170
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
3
  require "yaml"
5
4
 
6
5
  module Metanorma
@@ -17,8 +16,7 @@ module Metanorma
17
16
  end
18
17
 
19
18
  def call
20
- org_config = load_org_config
21
- config = load_config(org_config: org_config)
19
+ config = load_config
22
20
  options = { token: @config.token }
23
21
  publisher = PlatformFactory.build_publisher(@config.platform, options)
24
22
  channel_override = Channel.parse_list(@config.channels) if @config.channels
@@ -49,59 +47,15 @@ module Metanorma
49
47
 
50
48
  private
51
49
 
52
- def load_config(org_config: nil)
50
+ def load_config
53
51
  if @config.config_source && File.exist?(@config.config_source)
54
- Metanorma::Release::Config.from_file(@config.config_source, org_config: org_config)
52
+ Metanorma::Release::Config.from_file(@config.config_source)
55
53
  elsif @config.manifest && File.exist?(@config.manifest)
56
- Metanorma::Release::Config.from_file(@config.manifest, org_config: org_config)
54
+ Metanorma::Release::Config.from_file(@config.manifest)
57
55
  else
58
- Metanorma::Release::Config.defaults(org_config: org_config)
56
+ Metanorma::Release::Config.defaults
59
57
  end
60
58
  end
61
-
62
- def load_org_config
63
- path = find_config_path
64
- return nil unless path
65
-
66
- org_ref = extract_org_ref(path)
67
- return nil unless org_ref
68
-
69
- resolve_org_config(org_ref)
70
- end
71
-
72
- def find_config_path
73
- if @config.config_source && File.exist?(@config.config_source)
74
- @config.config_source
75
- elsif @config.manifest && File.exist?(@config.manifest)
76
- @config.manifest
77
- end
78
- end
79
-
80
- def extract_org_ref(path)
81
- raw = YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
82
- raw["org"]
83
- end
84
-
85
- def resolve_org_config(org_ref)
86
- ref = OrgConfig.parse_ref(org_ref)
87
- local_path = OrgConfig.remote_path(ref)
88
- return OrgConfig.from_file(local_path) if File.exist?(local_path)
89
-
90
- fetch_org_config_from_github(ref)
91
- end
92
-
93
- def fetch_org_config_from_github(ref)
94
- require "octokit"
95
- token = @config.token || ENV.fetch("GITHUB_TOKEN", nil)
96
- client = token ? Octokit::Client.new(access_token: token) : Octokit::Client.new
97
- remote = OrgConfig.remote_path(ref)
98
- contents = client.contents("#{ref.owner}/#{ref.repo}", path: remote)
99
- decoded = Base64.decode64(contents[:content])
100
- OrgConfig.from_yaml(decoded)
101
- rescue StandardError => e
102
- warn " (org config not loaded from #{ref.owner}/#{ref.repo}: #{e.message})"
103
- OrgConfig.defaults
104
- end
105
59
  end
106
60
  end
107
61
  end
@@ -5,27 +5,26 @@ require "yaml"
5
5
  module Metanorma
6
6
  module Release
7
7
  class Config
8
- def self.from_yaml(yaml_string, org_config: nil)
8
+ def self.from_yaml(yaml_string)
9
9
  data = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
10
- new(data || {}, org_config: org_config)
10
+ new(data || {})
11
11
  end
12
12
 
13
- def self.from_file(path, org_config: nil)
13
+ def self.from_file(path)
14
14
  unless File.exist?(path)
15
15
  raise ArgumentError,
16
16
  "Config file not found: #{path}"
17
17
  end
18
18
 
19
- from_yaml(File.read(path), org_config: org_config)
19
+ from_yaml(File.read(path))
20
20
  end
21
21
 
22
- def self.defaults(org_config: nil)
23
- new({}, org_config: org_config)
22
+ def self.defaults
23
+ new({})
24
24
  end
25
25
 
26
- def initialize(data, org_config: nil)
26
+ def initialize(data)
27
27
  @data = data
28
- @org_config = org_config
29
28
  end
30
29
 
31
30
  def org
@@ -36,18 +35,6 @@ module Metanorma
36
35
  @data.fetch("channels", [])
37
36
  end
38
37
 
39
- def routing
40
- @data.fetch("routing", {})
41
- end
42
-
43
- def routing_default
44
- routing.fetch("default", ["public"])
45
- end
46
-
47
- def routing_rules
48
- routing.fetch("rules", [])
49
- end
50
-
51
38
  def slug_config
52
39
  @data.fetch("slug", {})
53
40
  end
@@ -64,66 +51,60 @@ module Metanorma
64
51
  @data.fetch("documents", [])
65
52
  end
66
53
 
67
- def defaults
68
- @data.fetch("defaults", {})
69
- end
70
-
71
- def default_channels
72
- list = defaults.fetch("channels", nil)
73
- return ["public"] unless list
74
-
75
- list
54
+ def document_entries
55
+ @document_entries ||= documents.map { |d| DocumentEntry.new(d) }
76
56
  end
77
57
 
78
58
  def resolve_channels(publication)
79
- manifest_channels = resolve_manifest_channels(publication)
80
- return manifest_channels if manifest_channels
81
-
82
- rule_channels = resolve_routing_rules(publication)
83
- return rule_channels if rule_channels
84
-
85
- org_rule_channels = resolve_org_routing_rules(publication)
86
- return org_rule_channels if org_rule_channels
59
+ ChannelResolver.resolve(publication, self)
60
+ end
61
+ end
87
62
 
88
- local_default = routing_default
89
- return local_default unless local_default == ["public"] && @org_config
63
+ # Single routing entry — matches by any combination of pattern, source,
64
+ # stage, and doctype. An entry with no criteria matches everything (catch-all).
65
+ DocumentEntry = Struct.new(:pattern, :source, :stages, :doctypes, :channels, keyword_init: true) do
66
+ def initialize(data)
67
+ super(
68
+ pattern: data["pattern"],
69
+ source: data["source"],
70
+ stages: Array(data["stage"]).map(&:to_s),
71
+ doctypes: Array(data["doctype"]).map(&:to_s),
72
+ channels: Array(data["channels"]).map(&:to_s),
73
+ )
74
+ end
90
75
 
91
- org_default = @org_config&.routing_default
92
- return org_default unless org_default.nil? || org_default.empty?
76
+ def matches?(publication)
77
+ return false if channels.empty?
93
78
 
94
- default_channels
95
- end
79
+ if pattern && !File.fnmatch?(pattern, publication.slug)
80
+ return false
81
+ end
96
82
 
97
- private
83
+ if source && !(publication.source_path&.end_with?(source) || false)
84
+ return false
85
+ end
98
86
 
99
- def resolve_manifest_channels(publication)
100
- documents.each do |entry|
101
- next unless entry["source"] && publication.source_path&.end_with?(entry["source"])
102
- return entry["channels"] if entry["channels"]
87
+ if !stages.empty? && !stages.include?(publication.stage.to_s)
88
+ return false
103
89
  end
104
- nil
105
- end
106
90
 
107
- def resolve_routing_rules(publication)
108
- routing_rules.each do |rule|
109
- match = true
110
- match &&= Array(rule["stage"]).map(&:to_s).include?(publication.stage.to_s) if rule["stage"]
111
- match &&= Array(rule["doctype"]).map(&:to_s).include?(publication.doctype.to_s) if rule["doctype"]
112
- return rule["channels"] if match && rule["channels"]
91
+ if !doctypes.empty? && !doctypes.include?(publication.doctype.to_s)
92
+ return false
113
93
  end
114
- nil
94
+
95
+ true
115
96
  end
97
+ end
116
98
 
117
- def resolve_org_routing_rules(publication)
118
- return nil unless @org_config
99
+ # Iterates document entries: first match wins. Falls back to ["public"].
100
+ class ChannelResolver
101
+ FALLBACK = ["public"].freeze
119
102
 
120
- @org_config.routing_rules.each do |rule|
121
- match = true
122
- match &&= Array(rule["stage"]).map(&:to_s).include?(publication.stage.to_s) if rule["stage"]
123
- match &&= Array(rule["doctype"]).map(&:to_s).include?(publication.doctype.to_s) if rule["doctype"]
124
- return rule["channels"] if match && rule["channels"]
103
+ def self.resolve(publication, config)
104
+ config.document_entries.each do |entry|
105
+ return entry.channels if entry.matches?(publication)
125
106
  end
126
- nil
107
+ FALLBACK
127
108
  end
128
109
  end
129
110
  end
@@ -21,7 +21,9 @@ module Metanorma
21
21
  parsed = YAML.safe_load(yaml, permitted_classes: [Symbol])
22
22
  return nil unless parsed.is_a?(Hash)
23
23
 
24
- (parsed["channels"] || []).map(&:to_s)
24
+ channels = Array(parsed["channels"])
25
+ Array(parsed["documents"]).each { |doc| channels.concat(Array(doc["channels"])) }
26
+ channels.map(&:to_s).uniq
25
27
  rescue StandardError
26
28
  nil
27
29
  end
@@ -9,11 +9,11 @@ module Metanorma
9
9
  class Site
10
10
  attr_reader :index, :output_dir
11
11
 
12
- def initialize(index:, output_dir:, data_dir: nil, org_config: nil)
12
+ def initialize(index:, output_dir:, data_dir: nil, display_categories: [])
13
13
  @index = index
14
14
  @output_dir = output_dir
15
15
  @data_dir = data_dir
16
- @org_config = org_config
16
+ @display_categories = display_categories || []
17
17
  end
18
18
 
19
19
  def write!
@@ -168,9 +168,13 @@ module Metanorma
168
168
  end
169
169
 
170
170
  def resolve_display_category(doctype)
171
- return nil unless @org_config
171
+ return nil if doctype.nil? || doctype.empty?
172
172
 
173
- @org_config.display_category_for(doctype)
173
+ @display_categories.each do |cat|
174
+ doctypes = cat["doctypes"] || []
175
+ return { "name" => cat["name"], "slug" => cat["slug"] } if doctypes.include?(doctype)
176
+ end
177
+ nil
174
178
  end
175
179
 
176
180
  def add_contributors(hash, bib)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- VERSION = "0.2.21"
5
+ VERSION = "0.2.23"
6
6
  end
7
7
  end
@@ -11,8 +11,9 @@ module Metanorma
11
11
  autoload :Index, "metanorma/release/index"
12
12
  autoload :Site, "metanorma/release/site"
13
13
  autoload :Channel, "metanorma/release/channel"
14
- autoload :OrgConfig, "metanorma/release/org_config"
15
14
  autoload :Config, "metanorma/release/config"
15
+ autoload :DocumentEntry, "metanorma/release/config"
16
+ autoload :ChannelResolver, "metanorma/release/config"
16
17
  autoload :ContentHash, "metanorma/release/content_hash"
17
18
 
18
19
  # Strategies
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.21
4
+ version: 0.2.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-20 00:00:00.000000000 Z
11
+ date: 2026-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: relaton-bib
@@ -84,6 +84,11 @@ files:
84
84
  - PROMPT.md
85
85
  - README.adoc
86
86
  - Rakefile
87
+ - TODO.cleanups/01-channel-resolver-ocp.md
88
+ - TODO.cleanups/02-documentation.md
89
+ - TODO.cleanups/03-cleanup-repos.sh
90
+ - TODO.cleanups/03-repo-channels-yml-cleanup.md
91
+ - TODO.cleanups/04-version-bump.md
87
92
  - exe/metanorma-release
88
93
  - lib/metanorma/release.rb
89
94
  - lib/metanorma/release/aggregation_pipeline.rb
@@ -102,7 +107,6 @@ files:
102
107
  - lib/metanorma/release/file_routing.rb
103
108
  - lib/metanorma/release/index.rb
104
109
  - lib/metanorma/release/interfaces.rb
105
- - lib/metanorma/release/org_config.rb
106
110
  - lib/metanorma/release/platform.rb
107
111
  - lib/metanorma/release/platform/github.rb
108
112
  - lib/metanorma/release/platform/github/manifest_reader.rb
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module Metanorma
6
- module Release
7
- class OrgConfig
8
- Ref = Struct.new(:owner, :repo, :name, keyword_init: true)
9
-
10
- def self.parse_ref(org_string)
11
- parts = org_string.to_s.split("#", 2)
12
- slug = parts[0].to_s.strip
13
- segments = slug.split("/", 2)
14
- raise ArgumentError, "Invalid org reference: #{org_string}" unless segments.length == 2
15
-
16
- Ref.new(owner: segments[0], repo: segments[1], name: parts[1]&.strip)
17
- end
18
-
19
- def self.default_config_name
20
- "channels"
21
- end
22
-
23
- def self.remote_path(ref)
24
- name = ref.name || default_config_name
25
- ".metanorma/#{name}.yml"
26
- end
27
-
28
- def self.from_yaml(yaml_string)
29
- data = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
30
- new(data || {})
31
- end
32
-
33
- def self.from_file(path)
34
- raise ArgumentError, "Org config file not found: #{path}" unless File.exist?(path)
35
-
36
- from_yaml(File.read(path))
37
- end
38
-
39
- def self.defaults
40
- new({})
41
- end
42
-
43
- def initialize(data)
44
- @data = data
45
- end
46
-
47
- def channels
48
- @data.fetch("channels", [])
49
- end
50
-
51
- def routing_default
52
- dig_defaults_routing("default") || []
53
- end
54
-
55
- def routing_rules
56
- dig_defaults_routing("rules") || []
57
- end
58
-
59
- def valid_channel?(name)
60
- return true if channels.empty?
61
-
62
- ch = Channel.new(name)
63
- channels.any? do |valid|
64
- valid_ch = Channel.new(valid)
65
- ch.eql?(valid_ch) || ch.name.start_with?("#{valid_ch.name}/") || valid_ch.name.start_with?("#{ch.name}/")
66
- end
67
- end
68
-
69
- def display_categories
70
- @data.fetch("display_categories", [])
71
- end
72
-
73
- def display_category_for(doctype)
74
- return nil if doctype.nil? || doctype.empty?
75
-
76
- display_categories.each do |cat|
77
- doctypes = cat["doctypes"] || []
78
- return { "name" => cat["name"], "slug" => cat["slug"] } if doctypes.include?(doctype)
79
- end
80
- nil
81
- end
82
-
83
- private
84
-
85
- def dig_defaults_routing(key)
86
- @data.dig("defaults", "routing", key)
87
- end
88
- end
89
- end
90
- end