kettle-family 1.2.97 → 1.2.99
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +31 -1
- data/README.md +16 -1
- data/lib/kettle/family/execution_profile.rb +12 -0
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +75 -11
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3fff3ab628dc6f5338ddf0a820485b986ce5f40aec27a905f1ee15e18450ab9
|
|
4
|
+
data.tar.gz: fa34533d50ee506f05e2f1cd9f865c011d12e91996b1c38cbabfe76eb0f9c676
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81f62b1263cd2afbe483f7af09d0a4c1b3e4efac0328938592c84e381d5d731188031e1e9dad97ce343573b025a50d9de62424ce92d12be7245beced5de9996d
|
|
7
|
+
data.tar.gz: 95713fbc85dd8e285b7b94364bfe799735df2d2b9f19b1205d2ad0819d793f66c94e39aa3b3816826b2987dacabc14519e24f01a0447f29af1edb55567d49f95
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,32 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.2.99] - 2026-09-09
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.2.99][1.2.99t]
|
|
36
|
+
- COVERAGE: 93.52% -- 6261/6695 lines in 35 files
|
|
37
|
+
- BRANCH COVERAGE: 78.19% -- 2542/3251 branches in 35 files
|
|
38
|
+
- 32.75% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Keep resumed family releases on the prepared CI commit by skipping dependency-floor reconciliation and disabling sibling paths for child release validation.
|
|
43
|
+
|
|
44
|
+
- Scope branch-stack release resumes to the prepared branch so later targets run their complete release lifecycle before CI monitoring.
|
|
45
|
+
|
|
46
|
+
## [1.2.98] - 2026-09-09
|
|
47
|
+
|
|
48
|
+
- TAG: [v1.2.98][1.2.98t]
|
|
49
|
+
- COVERAGE: 93.50% -- 6245/6679 lines in 35 files
|
|
50
|
+
- BRANCH COVERAGE: 78.08% -- 2529/3239 branches in 35 files
|
|
51
|
+
- 32.75% documented
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Commit the final normalized Gemfile.lock after sibling and branch-worktree template runs.
|
|
56
|
+
|
|
57
|
+
- Bootstrap member releases from the family tool bundle before registry lockfile normalization.
|
|
58
|
+
|
|
33
59
|
## [1.2.97] - 2026-09-08
|
|
34
60
|
|
|
35
61
|
- TAG: [v1.2.97][1.2.97t]
|
|
@@ -2679,7 +2705,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2679
2705
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2680
2706
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2681
2707
|
|
|
2682
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.
|
|
2708
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.99...HEAD
|
|
2709
|
+
[1.2.99]: https://github.com/kettle-dev/kettle-family/compare/v1.2.98...v1.2.99
|
|
2710
|
+
[1.2.99t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.99
|
|
2711
|
+
[1.2.98]: https://github.com/kettle-dev/kettle-family/compare/v1.2.97...v1.2.98
|
|
2712
|
+
[1.2.98t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.98
|
|
2683
2713
|
[1.2.97]: https://github.com/kettle-dev/kettle-family/compare/v1.2.96...v1.2.97
|
|
2684
2714
|
[1.2.97t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.97
|
|
2685
2715
|
[1.2.96]: https://github.com/kettle-dev/kettle-family/compare/v1.2.95...v1.2.96
|
data/README.md
CHANGED
|
@@ -435,6 +435,12 @@ metadata fix requires a new full release run from step 0:
|
|
|
435
435
|
kettle-family release --publish --start-step 10 --local-ci
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
+
For `release.target_branches`, a resume position applies only to the first
|
|
439
|
+
selected branch, which is the one whose already-pushed release commit is being
|
|
440
|
+
resumed. Each later branch starts from step 0 so it creates, commits, and pushes
|
|
441
|
+
its own release candidate before `kettle-release` begins remote CI monitoring.
|
|
442
|
+
Use `--start-at member@branch` when the resumed branch is not the first target.
|
|
443
|
+
|
|
438
444
|
`--skip-changelog` skips only `kettle-changelog`; `--skip-appraisals` skips
|
|
439
445
|
only the Appraisal generation portion of release step 5 and still runs
|
|
440
446
|
`bin/rake yard`; and `--skip-ci` skips only remote CI monitoring. The generic
|
|
@@ -662,6 +668,15 @@ and leaves RubyGems MFA prompts interactive by default:
|
|
|
662
668
|
kettle-family release --publish --execute
|
|
663
669
|
```
|
|
664
670
|
|
|
671
|
+
Release uses two explicitly separate execution contexts. `release_bootstrap`
|
|
672
|
+
starts `kettle-release` from the family-root tool bundle, so a family can retain
|
|
673
|
+
its configured local sibling graph while it prepares a release wave. It never
|
|
674
|
+
uses a member's development Gemfile to boot the release tool. `release_registry`
|
|
675
|
+
then resets each tracked release lockfile against registry dependencies and is
|
|
676
|
+
also used by every project release child command. Local sibling paths therefore
|
|
677
|
+
remain available only before that transition; they cannot enter the canonical
|
|
678
|
+
lockfile, the release-preparation commit, or CI-facing project commands.
|
|
679
|
+
|
|
665
680
|
Use named recovery modes when a publish wave has already prepared a member:
|
|
666
681
|
|
|
667
682
|
```console
|
|
@@ -1171,7 +1186,7 @@ Thanks for RTFM. ☺️
|
|
|
1171
1186
|
[📌gitmoji]: https://gitmoji.dev
|
|
1172
1187
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1173
1188
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1174
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-6.
|
|
1189
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-6.695-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1175
1190
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
1176
1191
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1177
1192
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -40,6 +40,18 @@ module Kettle
|
|
|
40
40
|
host_platform_policy: :active_platform_must_resolve,
|
|
41
41
|
allowed_mutations: %i[template development_lockfiles generated]
|
|
42
42
|
),
|
|
43
|
+
# This profile only starts the release implementation from the family
|
|
44
|
+
# tool bundle. It is intentionally distinct from release_registry:
|
|
45
|
+
# canonical lockfile reset and all project release children use that
|
|
46
|
+
# registry-only profile after the tool has booted.
|
|
47
|
+
release_bootstrap: Definition.new(
|
|
48
|
+
name: :release_bootstrap,
|
|
49
|
+
path_gem_policy: :configured_siblings,
|
|
50
|
+
lockfile_role: :tool,
|
|
51
|
+
install_location: :family,
|
|
52
|
+
host_platform_policy: :active_platform_must_resolve,
|
|
53
|
+
allowed_mutations: %i[release_tool_bootstrap]
|
|
54
|
+
),
|
|
43
55
|
release_registry: Definition.new(
|
|
44
56
|
name: :release_registry,
|
|
45
57
|
path_gem_policy: :registry_only,
|
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "1.2.
|
|
8
|
+
VERSION = "1.2.99"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -1451,6 +1451,7 @@ module Kettle
|
|
|
1451
1451
|
normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "normalize_lockfiles", commit_changes: commit_changes)
|
|
1452
1452
|
emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label)
|
|
1453
1453
|
commit_template_changes(member: member, runner: runner, memo: memo) if commit_changes
|
|
1454
|
+
commit_template_normalized_gemfile_lock(member: member, runner: runner, memo: memo, commit_changes: commit_changes)
|
|
1454
1455
|
emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label) if memo.last&.phase == "commit_template"
|
|
1455
1456
|
ensure
|
|
1456
1457
|
template_result = memo.find { |result| result.phase == "template" } || memo.last
|
|
@@ -1491,7 +1492,7 @@ module Kettle
|
|
|
1491
1492
|
memo.concat(template_branch_sync_results(branch_members, runner: runner))
|
|
1492
1493
|
break memo unless memo.last&.ok?
|
|
1493
1494
|
end
|
|
1494
|
-
branch_results = current_branch_results(branch_members)
|
|
1495
|
+
branch_results = with_release_branch_resume_scope { current_branch_results(branch_members) }
|
|
1495
1496
|
tag_branch_results(branch_results, branch)
|
|
1496
1497
|
memo.concat(branch_results)
|
|
1497
1498
|
|
|
@@ -2603,6 +2604,10 @@ module Kettle
|
|
|
2603
2604
|
end
|
|
2604
2605
|
|
|
2605
2606
|
def release_dependency_floor_reconciliation_results(release_members)
|
|
2607
|
+
# A step resume must continue the already-prepared release commit.
|
|
2608
|
+
# Reconciliation can rewrite lockfiles and create a new commit, which
|
|
2609
|
+
# invalidates the CI SHA selected by the caller's --start-step.
|
|
2610
|
+
return [] if release_step_resume?
|
|
2606
2611
|
return [] unless execute && auto_dependency_floors
|
|
2607
2612
|
# Monorepo members intentionally keep their family dependencies as
|
|
2608
2613
|
# paths in the canonical development lockfile. Registry reconciliation
|
|
@@ -2670,6 +2675,7 @@ module Kettle
|
|
|
2670
2675
|
end
|
|
2671
2676
|
|
|
2672
2677
|
def append_dependency_floor_results(released_members:, dependent_members:, runner:, memo:)
|
|
2678
|
+
return if release_step_resume?
|
|
2673
2679
|
return unless auto_dependency_floors
|
|
2674
2680
|
return if dependent_members.empty?
|
|
2675
2681
|
|
|
@@ -3485,11 +3491,33 @@ module Kettle
|
|
|
3485
3491
|
end
|
|
3486
3492
|
|
|
3487
3493
|
def release_start_step_for(member)
|
|
3494
|
+
return nil if release_branch_resume_scoped? && !@release_branch_resume_active
|
|
3488
3495
|
return start_step unless fast_recovery_for?(member)
|
|
3489
3496
|
|
|
3490
3497
|
(fast_recovery == "retry-ci") ? 10 : 11
|
|
3491
3498
|
end
|
|
3492
3499
|
|
|
3500
|
+
# A release resume identifies one already-prepared branch commit. Later
|
|
3501
|
+
# branches in the same stack still need their complete lifecycle: build,
|
|
3502
|
+
# commit, push, and only then CI monitoring. Do not carry start_step (or
|
|
3503
|
+
# named CI recovery) beyond that first branch.
|
|
3504
|
+
def with_release_branch_resume_scope
|
|
3505
|
+
return yield unless release_branch_resume_scoped?
|
|
3506
|
+
|
|
3507
|
+
previous = @release_branch_resume_active
|
|
3508
|
+
@release_branch_resume_active = !@release_branch_resume_consumed
|
|
3509
|
+
yield
|
|
3510
|
+
ensure
|
|
3511
|
+
@release_branch_resume_consumed = true if @release_branch_resume_active
|
|
3512
|
+
@release_branch_resume_active = previous
|
|
3513
|
+
end
|
|
3514
|
+
|
|
3515
|
+
def release_branch_resume_scoped?
|
|
3516
|
+
command == "release" &&
|
|
3517
|
+
!config.release_target_branches.empty? &&
|
|
3518
|
+
(!start_step.nil? || !fast_recovery.nil?)
|
|
3519
|
+
end
|
|
3520
|
+
|
|
3493
3521
|
def release_skip_steps_for(member)
|
|
3494
3522
|
steps = Array(skip_steps).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
|
|
3495
3523
|
steps << "10" if skip_ci
|
|
@@ -3637,9 +3665,16 @@ module Kettle
|
|
|
3637
3665
|
# changelog may live at the family root. Pass the shared paths to the
|
|
3638
3666
|
# member release phase as well as to the separate family phase.
|
|
3639
3667
|
def release_env_for_member(member, family_root: config.root, wave_jobs: 1)
|
|
3640
|
-
env = release_env
|
|
3668
|
+
env = execution_profile(release_bootstrap_execution_profile, release_env)
|
|
3641
3669
|
env.merge!(release_wave_local_path_env_for(member, family_root: family_root))
|
|
3642
3670
|
env.merge!(release_local_path_policy_env(family_root: family_root))
|
|
3671
|
+
# A resumed child kettle-release starts in the member's mise
|
|
3672
|
+
# environment after the release candidate was already prepared.
|
|
3673
|
+
# Disable sibling paths so mise cannot reactivate them before the
|
|
3674
|
+
# resumed release validates that immutable lockfile. A new release
|
|
3675
|
+
# retains the staged local-graph policy until kettle-release reaches
|
|
3676
|
+
# its own release-lockfile boundary.
|
|
3677
|
+
env.merge!(release_lockfile_local_path_env_overrides(member)) if release_step_resume?
|
|
3643
3678
|
# Family release performs one live pin review before member releases.
|
|
3644
3679
|
# Child kettle-release must consume that reviewed cache instead of
|
|
3645
3680
|
# repeating the GitHub API lookup for every member.
|
|
@@ -3703,15 +3738,15 @@ module Kettle
|
|
|
3703
3738
|
candidates.find { |path| File.file?(path) }
|
|
3704
3739
|
end
|
|
3705
3740
|
|
|
3706
|
-
# A member's development
|
|
3707
|
-
#
|
|
3708
|
-
#
|
|
3709
|
-
#
|
|
3710
|
-
#
|
|
3741
|
+
# A member's development Gemfile can contain a deliberately local family
|
|
3742
|
+
# graph that is not independently bootable after an earlier release wave
|
|
3743
|
+
# changes a sibling's dependency floor. Launch kettle-release from the
|
|
3744
|
+
# family tool bundle instead. The release tool still runs project commands
|
|
3745
|
+
# from the member checkout, where it first normalizes canonical lockfiles
|
|
3746
|
+
# with local paths disabled.
|
|
3711
3747
|
def release_tool_gemfile_path(local_kettle_dev)
|
|
3712
3748
|
local_kettle_dev_gemfile = local_kettle_dev_gemfile_path(local_kettle_dev)
|
|
3713
3749
|
return local_kettle_dev_gemfile if local_kettle_dev_gemfile
|
|
3714
|
-
return unless config.configured_monorepo_release?
|
|
3715
3750
|
|
|
3716
3751
|
family_gemfile = File.join(config.root, "Gemfile")
|
|
3717
3752
|
family_gemfile if File.file?(family_gemfile)
|
|
@@ -5319,6 +5354,23 @@ module Kettle
|
|
|
5319
5354
|
end
|
|
5320
5355
|
end
|
|
5321
5356
|
|
|
5357
|
+
# Kettle Jem commits its own changes for sibling repositories. The final
|
|
5358
|
+
# lockfile normalization therefore happens after that commit and needs a
|
|
5359
|
+
# separate, deliberately narrow cleanup commit. Monorepos defer the
|
|
5360
|
+
# template commit, which already includes the normalized lockfile.
|
|
5361
|
+
def commit_template_normalized_gemfile_lock(member:, runner:, memo:, commit_changes:)
|
|
5362
|
+
return unless execute && commit_changes && memo.all?
|
|
5363
|
+
return if deferred_monorepo_template_commit?(member)
|
|
5364
|
+
|
|
5365
|
+
commit_normalized_lockfiles(
|
|
5366
|
+
branch_members: [member],
|
|
5367
|
+
runner: runner,
|
|
5368
|
+
memo: memo,
|
|
5369
|
+
reason: "template",
|
|
5370
|
+
paths: ["Gemfile.lock"]
|
|
5371
|
+
)
|
|
5372
|
+
end
|
|
5373
|
+
|
|
5322
5374
|
def template_commit_message(member)
|
|
5323
5375
|
"🎨 Template #{member.name} by kettle-family"
|
|
5324
5376
|
end
|
|
@@ -5357,6 +5409,10 @@ module Kettle
|
|
|
5357
5409
|
ExecutionProfile.fetch(:template_local)
|
|
5358
5410
|
end
|
|
5359
5411
|
|
|
5412
|
+
def release_bootstrap_execution_profile
|
|
5413
|
+
ExecutionProfile.fetch(:release_bootstrap)
|
|
5414
|
+
end
|
|
5415
|
+
|
|
5360
5416
|
def release_lockfile_execution_profile
|
|
5361
5417
|
return ExecutionProfile.fetch(:release_recovery) if release_recovery?
|
|
5362
5418
|
return ExecutionProfile.fetch(:release_monorepo) if preserve_monorepo_template_context?
|
|
@@ -5368,6 +5424,10 @@ module Kettle
|
|
|
5368
5424
|
!start_step.nil? || !skip_steps.to_s.empty? || fast_recovery || skip_ci
|
|
5369
5425
|
end
|
|
5370
5426
|
|
|
5427
|
+
def release_step_resume?
|
|
5428
|
+
!start_step.nil? || !skip_steps.to_s.empty?
|
|
5429
|
+
end
|
|
5430
|
+
|
|
5371
5431
|
def execution_profile(profile, env)
|
|
5372
5432
|
profile = ExecutionProfile.fetch(profile) unless profile.is_a?(ExecutionProfile::Definition)
|
|
5373
5433
|
env = env.dup
|
|
@@ -5375,7 +5435,7 @@ module Kettle
|
|
|
5375
5435
|
when :template_local
|
|
5376
5436
|
env["K_JEM_TEMPLATING"] = "true"
|
|
5377
5437
|
env["BUNDLE_DISABLE_CHECKSUM_VALIDATION"] = "true"
|
|
5378
|
-
when :release_registry, :release_monorepo, :release_recovery
|
|
5438
|
+
when :release_bootstrap, :release_registry, :release_monorepo, :release_recovery
|
|
5379
5439
|
# Release profiles deliberately leave Bundler mutation decisions to
|
|
5380
5440
|
# kettle-dev. The profile selection controls which local-path
|
|
5381
5441
|
# overrides this workflow contributes below.
|
|
@@ -5588,10 +5648,14 @@ module Kettle
|
|
|
5588
5648
|
text.include?(File::SEPARATOR)
|
|
5589
5649
|
end
|
|
5590
5650
|
|
|
5591
|
-
def commit_normalized_lockfiles(branch_members:, runner:, memo:, reason: command, force: false)
|
|
5651
|
+
def commit_normalized_lockfiles(branch_members:, runner:, memo:, reason: command, force: false, paths: nil)
|
|
5592
5652
|
return unless commit
|
|
5593
5653
|
return unless force || commit_normalized_lockfiles?(reason)
|
|
5594
5654
|
|
|
5655
|
+
pathspecs = Array(paths).map { |path| Shellwords.escape(path) }
|
|
5656
|
+
pathspecs = ["Gemfile.lock", "'*.lock'", "'**/*.lock'"] if pathspecs.empty?
|
|
5657
|
+
pathspec = pathspecs.join(" ")
|
|
5658
|
+
|
|
5595
5659
|
branch_members.each do |member|
|
|
5596
5660
|
result = runner.call(
|
|
5597
5661
|
member: member,
|
|
@@ -5599,7 +5663,7 @@ module Kettle
|
|
|
5599
5663
|
command: [
|
|
5600
5664
|
"sh",
|
|
5601
5665
|
"-lc",
|
|
5602
|
-
"files=$(git ls-files --modified --others --exclude-standard --
|
|
5666
|
+
"files=$(git ls-files --modified --others --exclude-standard -- #{pathspec}); " \
|
|
5603
5667
|
"if [ -n \"$files\" ]; then printf '%s\\n' \"$files\" | xargs git add -- && git commit -m " \
|
|
5604
5668
|
"#{Shellwords.escape(normalized_lockfiles_commit_message(reason))}; fi"
|
|
5605
5669
|
]
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-family
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.99
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -443,10 +443,10 @@ licenses:
|
|
|
443
443
|
- AGPL-3.0-only
|
|
444
444
|
metadata:
|
|
445
445
|
homepage_uri: https://kettle-family.galtzo.com
|
|
446
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.
|
|
447
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.
|
|
446
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.99
|
|
447
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.99/CHANGELOG.md
|
|
448
448
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
449
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.
|
|
449
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.99
|
|
450
450
|
funding_uri: https://github.com/sponsors/pboling
|
|
451
451
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
452
452
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|