chrono_forge 0.10.0 → 0.11.0
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
- data/CHANGELOG.md +34 -1
- data/README.md +188 -105
- data/Rakefile +4 -0
- data/cliff.toml +62 -0
- data/docs/design/per-child-commit-overhead.md +213 -0
- data/docs/fanout-scale-test.md +247 -0
- data/docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md +205 -0
- data/docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md.tasks.json +33 -0
- data/docs/superpowers/plans/2026-07-01-workflow-definition-dag.md +1373 -0
- data/docs/superpowers/plans/2026-07-01-workflow-definition-dag.md.tasks.json +68 -0
- data/docs/superpowers/specs/2026-07-01-workflow-definition-dag-design.md +203 -0
- data/lib/chrono_forge/branch_merge_job.rb +158 -21
- data/lib/chrono_forge/branch_probe.rb +44 -0
- data/lib/chrono_forge/configuration.rb +25 -0
- data/lib/chrono_forge/definition.rb +37 -0
- data/lib/chrono_forge/definition_analyzer.rb +501 -0
- data/lib/chrono_forge/executor/context.rb +23 -0
- data/lib/chrono_forge/executor/lock_strategy.rb +10 -3
- data/lib/chrono_forge/executor/methods/continue_if.rb +15 -6
- data/lib/chrono_forge/executor/methods/durably_execute.rb +15 -7
- data/lib/chrono_forge/executor/methods/durably_repeat.rb +30 -14
- data/lib/chrono_forge/executor/methods/merge_branches.rb +5 -4
- data/lib/chrono_forge/executor/methods/workflow_states.rb +35 -47
- data/lib/chrono_forge/executor.rb +34 -9
- data/lib/chrono_forge/version.rb +1 -1
- data/lib/chrono_forge.rb +8 -0
- data/lib/tasks/release.rake +212 -0
- metadata +28 -2
|
@@ -107,20 +107,28 @@ module ChronoForge
|
|
|
107
107
|
validate_step_name_segment!(name || method)
|
|
108
108
|
step_name = "durably_repeat$#{name || method}"
|
|
109
109
|
|
|
110
|
-
# Get or create the main coordination log for this periodic task
|
|
110
|
+
# Get or create the main coordination log for this periodic task. A fresh
|
|
111
|
+
# coordinator records its first pass in the INSERT (attempts: 1,
|
|
112
|
+
# last_executed_at); only later passes bump via UPDATE.
|
|
111
113
|
coordination_log = find_or_create_execution_log!(step_name) do |log|
|
|
112
|
-
|
|
114
|
+
now = Time.current
|
|
115
|
+
log.started_at = now
|
|
116
|
+
log.last_executed_at = now
|
|
117
|
+
log.attempts = 1
|
|
113
118
|
log.metadata = {last_execution_at: nil}
|
|
114
119
|
end
|
|
115
120
|
|
|
116
121
|
# Return if already completed
|
|
117
122
|
return if coordination_log.completed?
|
|
118
123
|
|
|
119
|
-
# Update coordination log attempt tracking
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
# Update coordination log attempt tracking (first pass already recorded
|
|
125
|
+
# above on create).
|
|
126
|
+
unless coordination_log.previously_new_record?
|
|
127
|
+
coordination_log.update!(
|
|
128
|
+
attempts: coordination_log.attempts + 1,
|
|
129
|
+
last_executed_at: Time.current
|
|
130
|
+
)
|
|
131
|
+
end
|
|
124
132
|
|
|
125
133
|
# Check if we should stop repeating
|
|
126
134
|
condition_met = if till.is_a?(Symbol)
|
|
@@ -252,9 +260,14 @@ module ChronoForge
|
|
|
252
260
|
def execute_or_schedule_repetition(method, coordination_log, next_execution_at, every, policy, timeout, on_error)
|
|
253
261
|
step_name = "#{coordination_log.step_name}$#{next_execution_at.to_i}"
|
|
254
262
|
|
|
255
|
-
# Create execution log for this specific repetition
|
|
263
|
+
# Create execution log for this specific repetition. A fresh repetition
|
|
264
|
+
# records its first attempt in the INSERT itself (attempts: 1,
|
|
265
|
+
# last_executed_at), so there is no separate pre-execution UPDATE.
|
|
256
266
|
repetition_log = find_or_create_execution_log!(step_name) do |log|
|
|
257
|
-
|
|
267
|
+
now = Time.current
|
|
268
|
+
log.started_at = now
|
|
269
|
+
log.last_executed_at = now
|
|
270
|
+
log.attempts = 1
|
|
258
271
|
log.metadata = {
|
|
259
272
|
scheduled_for: next_execution_at,
|
|
260
273
|
timeout_at: next_execution_at + timeout,
|
|
@@ -265,11 +278,14 @@ module ChronoForge
|
|
|
265
278
|
# Return if this repetition is already completed
|
|
266
279
|
return if repetition_log.completed?
|
|
267
280
|
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
# Existing logs (a resume of a scheduled-for-later repetition) still need
|
|
282
|
+
# the attempt bump; a freshly-created one recorded its first above.
|
|
283
|
+
unless repetition_log.previously_new_record?
|
|
284
|
+
repetition_log.update!(
|
|
285
|
+
attempts: repetition_log.attempts + 1,
|
|
286
|
+
last_executed_at: Time.current
|
|
287
|
+
)
|
|
288
|
+
end
|
|
273
289
|
|
|
274
290
|
# Check if it's time to execute this repetition
|
|
275
291
|
if next_execution_at <= Time.current
|
|
@@ -5,7 +5,8 @@ module ChronoForge
|
|
|
5
5
|
# Join one or more named branches. Separate from dispatch so branches run
|
|
6
6
|
# concurrently. Does one immediate check; if not done, hands off to the
|
|
7
7
|
# lightweight BranchMergeJob and halts (the heavy parent is not replayed
|
|
8
|
-
# per poll).
|
|
8
|
+
# per poll). Poll cadence is driven by estimated time-to-drain, clamped
|
|
9
|
+
# between min/max (see BranchMergeJob#reschedule_delay).
|
|
9
10
|
def merge_branches(*names, min_interval: 5.seconds, max_interval: 5.minutes)
|
|
10
11
|
names.each do |nm|
|
|
11
12
|
validate_step_name_segment!(nm) # rejects "$"
|
|
@@ -16,9 +17,9 @@ module ChronoForge
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Validate cadence here, in the parent, so a misconfiguration fails at the
|
|
19
|
-
# call site instead of deep inside the poller — where
|
|
20
|
-
#
|
|
21
|
-
# dead-letters BranchMergeJob and orphans the parent.
|
|
20
|
+
# call site instead of deep inside the poller — where the clamp to
|
|
21
|
+
# [min_interval, max_interval] would raise ArgumentError, a non-transient
|
|
22
|
+
# error that dead-letters BranchMergeJob and orphans the parent.
|
|
22
23
|
if min_interval > max_interval
|
|
23
24
|
raise ArgumentError,
|
|
24
25
|
"min_interval (#{min_interval}) must be <= max_interval (#{max_interval})"
|
|
@@ -50,34 +50,28 @@ module ChronoForge
|
|
|
50
50
|
def complete_workflow!
|
|
51
51
|
enforce_branch_joins!
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
# Completion is two writes with no external side effect between them: the
|
|
54
|
+
# workflow → :completed transition and the (born-completed) marker. Batch
|
|
55
|
+
# them in one transaction so a trivial child pays a single commit here,
|
|
56
|
+
# and write the marker in its terminal state in a single INSERT.
|
|
57
|
+
execution_log = nil
|
|
58
58
|
begin
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# attempt-count basis, so there is no need for a separate pre-write.
|
|
65
|
-
execution_log.update!(
|
|
66
|
-
attempts: execution_log.attempts + 1,
|
|
67
|
-
last_executed_at: Time.current,
|
|
68
|
-
state: :completed,
|
|
69
|
-
completed_at: Time.current
|
|
70
|
-
)
|
|
59
|
+
ActiveRecord::Base.transaction do
|
|
60
|
+
workflow.completed_at = Time.current
|
|
61
|
+
workflow.completed!
|
|
62
|
+
execution_log = create_completed_execution_log!("$workflow_completion$")
|
|
63
|
+
end
|
|
71
64
|
|
|
72
65
|
# Return the execution log for tracking
|
|
73
66
|
execution_log
|
|
74
67
|
rescue => e
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
# The transaction rolled back (so the marker may be gone too). Re-find
|
|
69
|
+
# or recreate it and record the failure for observability, then re-raise.
|
|
70
|
+
# The workflow stays not-completed, so a resume retries completion.
|
|
71
|
+
log = find_or_create_execution_log!("$workflow_completion$") do |l|
|
|
72
|
+
l.started_at = Time.current
|
|
73
|
+
end
|
|
74
|
+
log.update!(state: :failed, error_message: e.message, error_class: e.class.name)
|
|
81
75
|
raise
|
|
82
76
|
end
|
|
83
77
|
end
|
|
@@ -153,36 +147,30 @@ module ChronoForge
|
|
|
153
147
|
# - Safe to call multiple times with same error_log
|
|
154
148
|
#
|
|
155
149
|
def fail_workflow!(error_log)
|
|
156
|
-
|
|
157
|
-
execution_log = find_or_create_execution_log!("$workflow_failure$#{error_log.id}") do |log|
|
|
158
|
-
log.started_at = Time.current
|
|
159
|
-
log.metadata = {
|
|
160
|
-
error_log_id: error_log.id
|
|
161
|
-
}
|
|
162
|
-
end
|
|
150
|
+
step_name = "$workflow_failure$#{error_log.id}"
|
|
163
151
|
|
|
152
|
+
# Mirror complete_workflow!: the workflow → :failed transition and the
|
|
153
|
+
# (born-completed) failure marker are batched in one transaction, and the
|
|
154
|
+
# marker is written in its terminal state in a single INSERT.
|
|
155
|
+
execution_log = nil
|
|
164
156
|
begin
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
attempts: execution_log.attempts + 1,
|
|
172
|
-
last_executed_at: Time.current,
|
|
173
|
-
state: :completed,
|
|
174
|
-
completed_at: Time.current
|
|
175
|
-
)
|
|
157
|
+
ActiveRecord::Base.transaction do
|
|
158
|
+
workflow.failed!
|
|
159
|
+
execution_log = create_completed_execution_log!(step_name) do |log|
|
|
160
|
+
log.metadata = {error_log_id: error_log.id}
|
|
161
|
+
end
|
|
162
|
+
end
|
|
176
163
|
|
|
177
164
|
# Return the execution log for tracking
|
|
178
165
|
execution_log
|
|
179
166
|
rescue => e
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
# The transaction rolled back; re-find/recreate the marker and record
|
|
168
|
+
# the failure for observability, then re-raise.
|
|
169
|
+
log = find_or_create_execution_log!(step_name) do |l|
|
|
170
|
+
l.started_at = Time.current
|
|
171
|
+
l.metadata = {error_log_id: error_log.id}
|
|
172
|
+
end
|
|
173
|
+
log.update!(state: :failed, error_message: e.message, error_class: e.class.name)
|
|
186
174
|
raise
|
|
187
175
|
end
|
|
188
176
|
end
|
|
@@ -210,15 +210,6 @@ module ChronoForge
|
|
|
210
210
|
workflow.kwargs = kwargs
|
|
211
211
|
workflow.started_at = Time.current
|
|
212
212
|
end
|
|
213
|
-
|
|
214
|
-
# Branch children are pre-inserted by their parent (dispatch_children's
|
|
215
|
-
# insert_all), so the creation block above never runs for them and their
|
|
216
|
-
# started_at stays nil. Stamp it the first time the child actually executes
|
|
217
|
-
# so started_at reliably means "has been picked up and run" — the
|
|
218
|
-
# BranchMergeJob rekick poller treats a nil started_at as a never-executed
|
|
219
|
-
# (dropped) child, and must not mistake a child that ran and is now parked
|
|
220
|
-
# on a wait (also :idle) for one that was never picked up.
|
|
221
|
-
@workflow.update_column(:started_at, Time.current) if @workflow.started_at.nil?
|
|
222
213
|
end
|
|
223
214
|
|
|
224
215
|
def setup_context!
|
|
@@ -259,6 +250,40 @@ module ChronoForge
|
|
|
259
250
|
ExecutionLog.create_or_find_by!(workflow: @workflow, step_name: step_name, &)
|
|
260
251
|
end
|
|
261
252
|
|
|
253
|
+
# Record a step that completes synchronously within this pass as a single
|
|
254
|
+
# INSERT already in its terminal :completed state — there is no
|
|
255
|
+
# started→completed UPDATE chasing the INSERT (one statement, not two). Use
|
|
256
|
+
# this for steps with no deferral point between create and completion
|
|
257
|
+
# (workflow completion/failure markers and similar); deferring steps (waits,
|
|
258
|
+
# branch coordination) must stay :started across resumes and use
|
|
259
|
+
# find_or_create_execution_log! instead.
|
|
260
|
+
#
|
|
261
|
+
# Idempotent: an already-existing row (a resume after a partial pass, or a
|
|
262
|
+
# create race) is flipped to :completed instead of duplicated. The block runs
|
|
263
|
+
# only on create, letting callers attach metadata to the new row.
|
|
264
|
+
def create_completed_execution_log!(step_name)
|
|
265
|
+
now = Time.current
|
|
266
|
+
log = find_or_create_execution_log!(step_name) do |l|
|
|
267
|
+
l.attempts = 1
|
|
268
|
+
l.started_at = now
|
|
269
|
+
l.last_executed_at = now
|
|
270
|
+
l.completed_at = now
|
|
271
|
+
l.state = :completed
|
|
272
|
+
yield l if block_given?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
unless log.completed?
|
|
276
|
+
log.update!(
|
|
277
|
+
attempts: log.attempts + 1,
|
|
278
|
+
last_executed_at: now,
|
|
279
|
+
completed_at: now,
|
|
280
|
+
state: :completed
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
log
|
|
285
|
+
end
|
|
286
|
+
|
|
262
287
|
# One bulk read of this workflow's completed steps, mapping step_name to its
|
|
263
288
|
# metadata, memoized for the duration of a single replay pass.
|
|
264
289
|
#
|
data/lib/chrono_forge/version.rb
CHANGED
data/lib/chrono_forge.rb
CHANGED
|
@@ -13,4 +13,12 @@ module ChronoForge
|
|
|
13
13
|
class Error < StandardError; end
|
|
14
14
|
|
|
15
15
|
def self.ApplicationRecord = defined?(::ApplicationRecord) ? ::ApplicationRecord : ActiveRecord::Base
|
|
16
|
+
|
|
17
|
+
# Engine configuration (see ChronoForge::Configuration).
|
|
18
|
+
# ChronoForge.configure { |c| c.branch_merge_queue = :chrono_forge_pollers }
|
|
19
|
+
def self.config = @config ||= Configuration.new
|
|
20
|
+
|
|
21
|
+
def self.configure = yield(config)
|
|
22
|
+
|
|
23
|
+
def self.reset_configuration! = @config = Configuration.new
|
|
16
24
|
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Release flow (chrono_forge monorepo: core + dashboard)
|
|
4
|
+
# ------------------------------------------------------
|
|
5
|
+
# Publishing happens from a laptop. CI does NOT push to any registry — it only
|
|
6
|
+
# cuts the GitHub Release (notes + the built gem) when the tag lands.
|
|
7
|
+
#
|
|
8
|
+
# 1. rake release:core:prepare # auto-computes next version (git-cliff)
|
|
9
|
+
# rake release:core:prepare[0.11.0] # ...or pass one explicitly
|
|
10
|
+
# 2. git diff # review the bump + changelog (nothing committed yet)
|
|
11
|
+
# 3. rake release:core:publish # commit, build + push gem, then tag + push → CI cuts the Release
|
|
12
|
+
#
|
|
13
|
+
# Same tasks under release:dashboard:*. Release `core` BEFORE `dashboard` —
|
|
14
|
+
# the dashboard depends on core, so bump its `chrono_forge` floor first if needed.
|
|
15
|
+
#
|
|
16
|
+
# prepare leaves the bump + changelog UNCOMMITTED so you can review the diff
|
|
17
|
+
# first; publish commits them, then publishes. publish is idempotent + resumable:
|
|
18
|
+
# it skips a gem already live and only tags if the tag is missing, so a partial
|
|
19
|
+
# failure can just be re-run.
|
|
20
|
+
|
|
21
|
+
RELEASE_CLIFF_CONFIG = "cliff.toml"
|
|
22
|
+
|
|
23
|
+
# Per-gem config. tag_pattern + scope mirror what each CHANGELOG should reflect:
|
|
24
|
+
# core = everything except the dashboard subtree, dashboard = only that subtree.
|
|
25
|
+
RELEASE_GEMS = {
|
|
26
|
+
"core" => {
|
|
27
|
+
name: "chrono_forge",
|
|
28
|
+
version_file: "lib/chrono_forge/version.rb",
|
|
29
|
+
changelog: "CHANGELOG.md",
|
|
30
|
+
gemspec: "chrono_forge.gemspec",
|
|
31
|
+
build_dir: ".",
|
|
32
|
+
tag_prefix: "v",
|
|
33
|
+
tag_pattern: "^v[0-9]",
|
|
34
|
+
scope: ["--exclude-path", "chrono_forge-dashboard/**"],
|
|
35
|
+
extra_files: []
|
|
36
|
+
},
|
|
37
|
+
"dashboard" => {
|
|
38
|
+
name: "chrono_forge-dashboard",
|
|
39
|
+
version_file: "chrono_forge-dashboard/lib/chrono_forge/dashboard/version.rb",
|
|
40
|
+
changelog: "chrono_forge-dashboard/CHANGELOG.md",
|
|
41
|
+
gemspec: "chrono_forge-dashboard.gemspec",
|
|
42
|
+
build_dir: "chrono_forge-dashboard",
|
|
43
|
+
tag_prefix: "chrono_forge-dashboard-v",
|
|
44
|
+
tag_pattern: "^chrono_forge-dashboard-v[0-9]",
|
|
45
|
+
scope: ["--include-path", "chrono_forge-dashboard/**"],
|
|
46
|
+
# The dashboard ships a compiled stylesheet — recompile it so the tagged
|
|
47
|
+
# tree (and the gem) never ship CSS that lags the source/views.
|
|
48
|
+
assets: -> {
|
|
49
|
+
Dir.chdir("chrono_forge-dashboard") do
|
|
50
|
+
system("bundle", "exec", "rake", "tailwind:build") || abort("tailwind:build failed")
|
|
51
|
+
end
|
|
52
|
+
},
|
|
53
|
+
extra_files: ["chrono_forge-dashboard/app/assets/chrono_forge/dashboard/dashboard.css"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
namespace :release do
|
|
58
|
+
# --- helpers --------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def git_cliff?
|
|
61
|
+
system("which git-cliff > /dev/null 2>&1")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def release_current_version(cfg)
|
|
65
|
+
File.read(cfg[:version_file])[/VERSION = "([\d.]+)"/, 1] ||
|
|
66
|
+
abort("Could not read VERSION from #{cfg[:version_file]}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def release_cliff_cmd(cfg, *extra)
|
|
70
|
+
["git-cliff", "--config", RELEASE_CLIFF_CONFIG, "--tag-pattern", cfg[:tag_pattern], *cfg[:scope], *extra]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Capture git-cliff stdout, discarding the stderr update-check chatter.
|
|
74
|
+
def release_capture(cmd)
|
|
75
|
+
IO.popen(cmd, err: File::NULL, &:read)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Next version per conventional commits. git-cliff owns the semver math
|
|
79
|
+
# (including the pre-1.0 rules under [bump] in cliff.toml). Returns it
|
|
80
|
+
# without the gem's tag prefix.
|
|
81
|
+
def release_next_version(cfg)
|
|
82
|
+
abort "git-cliff not found. Install with: brew install git-cliff" unless git_cliff?
|
|
83
|
+
bumped = release_capture(release_cliff_cmd(cfg, "--bumped-version")).strip
|
|
84
|
+
abort "git-cliff could not compute a version (no conventional commits since the last #{cfg[:name]} tag?)" if bumped.empty?
|
|
85
|
+
bumped.delete_prefix(cfg[:tag_prefix])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def release_gem_published?(cfg, version)
|
|
89
|
+
out = `gem list --remote --exact --all #{cfg[:name]} 2>/dev/null`
|
|
90
|
+
out.include?("#{version},") || out.include?("#{version})") || out.include?(" #{version} ")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Inject ONLY this version's section above the latest entry. A full -o regen
|
|
94
|
+
# would misattribute past releases because path-filtering confuses cliff's
|
|
95
|
+
# historical tag boundaries; existing entries are preserved verbatim.
|
|
96
|
+
def release_prepend_changelog(path, section)
|
|
97
|
+
body = File.read(path)
|
|
98
|
+
block = "#{section.strip}\n\n"
|
|
99
|
+
updated = (body =~ /^## \[/) ? body.sub(/^## \[/, "#{block}## [") : "#{body.rstrip}\n\n#{block}"
|
|
100
|
+
File.write(path, updated)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# --- per-gem tasks --------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
RELEASE_GEMS.each do |key, cfg|
|
|
106
|
+
namespace key do
|
|
107
|
+
desc "Show #{cfg[:name]}'s next version computed from conventional commits"
|
|
108
|
+
task :version do
|
|
109
|
+
puts "#{cfg[:name]} current: #{release_current_version(cfg)}"
|
|
110
|
+
puts "#{cfg[:name]} next: #{release_next_version(cfg)}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
desc "Prepare a #{cfg[:name]} release commit (bump + changelog + assets). Version optional; git-cliff computes it."
|
|
114
|
+
task :prepare, [:version] do |_t, args|
|
|
115
|
+
version = args[:version] || release_next_version(cfg)
|
|
116
|
+
abort "Error: version must be in format X.Y.Z (got #{version.inspect})" unless version.match?(/^\d+\.\d+\.\d+$/)
|
|
117
|
+
abort "Error: working tree is dirty. Commit or stash first." unless `git status --porcelain`.strip.empty?
|
|
118
|
+
abort "Error: not on main." unless `git rev-parse --abbrev-ref HEAD`.strip == "main"
|
|
119
|
+
|
|
120
|
+
system("git fetch -q origin")
|
|
121
|
+
abort "Error: main is not in sync with origin/main." unless `git rev-parse HEAD`.strip == `git rev-parse origin/main`.strip
|
|
122
|
+
|
|
123
|
+
tag = "#{cfg[:tag_prefix]}#{version}"
|
|
124
|
+
abort "Error: tag #{tag} already exists." if system("git rev-parse #{tag} >/dev/null 2>&1")
|
|
125
|
+
|
|
126
|
+
puts "Preparing #{cfg[:name]} #{version} (tag #{tag})..."
|
|
127
|
+
|
|
128
|
+
# Bump version.
|
|
129
|
+
content = File.read(cfg[:version_file])
|
|
130
|
+
File.write(cfg[:version_file], content.gsub(/VERSION = "[\d.]+"/, %(VERSION = "#{version}")))
|
|
131
|
+
puts "✓ #{cfg[:version_file]}"
|
|
132
|
+
|
|
133
|
+
# Compile assets (dashboard CSS), if any.
|
|
134
|
+
cfg[:assets]&.call
|
|
135
|
+
|
|
136
|
+
# Changelog — same config CI uses for the notes, so they agree.
|
|
137
|
+
section = release_capture(release_cliff_cmd(cfg, "--tag", tag, "--unreleased", "--strip", "all"))
|
|
138
|
+
abort "git-cliff found no entries since the last #{cfg[:name]} tag — nothing to release." if section.strip.empty?
|
|
139
|
+
release_prepend_changelog(cfg[:changelog], section)
|
|
140
|
+
puts "✓ #{cfg[:changelog]}"
|
|
141
|
+
|
|
142
|
+
# Leave everything uncommitted so the bump + changelog can be reviewed
|
|
143
|
+
# before anything is committed or published. publish makes the commit.
|
|
144
|
+
files = [cfg[:version_file], cfg[:changelog], *cfg[:extra_files]]
|
|
145
|
+
|
|
146
|
+
puts "\n✓ Prepared #{cfg[:name]} #{version} — nothing committed yet."
|
|
147
|
+
puts "Next:"
|
|
148
|
+
puts " git diff -- #{files.join(" ")}"
|
|
149
|
+
puts " rake release:#{key}:publish # commit, build + push gem, then tag + push"
|
|
150
|
+
puts " (abort with: git checkout -- #{files.join(" ")})"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
desc "Publish #{cfg[:name]} (build + push gem, then tag + push). Idempotent + resumable."
|
|
154
|
+
task :publish do
|
|
155
|
+
version = release_current_version(cfg)
|
|
156
|
+
tag = "#{cfg[:tag_prefix]}#{version}"
|
|
157
|
+
files = [cfg[:version_file], cfg[:changelog], *cfg[:extra_files]]
|
|
158
|
+
|
|
159
|
+
# Commit the prepared changes (you review the diff between prepare and
|
|
160
|
+
# here). Resumable: if they're already committed — e.g. a re-run after a
|
|
161
|
+
# partial failure — skip straight to publishing.
|
|
162
|
+
if `git status --porcelain -- #{files.join(" ")}`.strip.empty?
|
|
163
|
+
unless `git log -1 --format=%s`.strip == "chore(release): #{cfg[:name]} #{version}"
|
|
164
|
+
abort "Nothing prepared — run rake release:#{key}:prepare first."
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
system("git", "add", *files) || abort("git add failed")
|
|
168
|
+
system("git", "commit", "-m", "chore(release): #{cfg[:name]} #{version}") || abort("git commit failed")
|
|
169
|
+
puts "✓ Committed #{cfg[:name]} #{version}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if release_gem_published?(cfg, version)
|
|
173
|
+
puts "• #{cfg[:name]} #{version} already on RubyGems — skipping"
|
|
174
|
+
else
|
|
175
|
+
puts "Building + pushing gem..."
|
|
176
|
+
Dir.chdir(cfg[:build_dir]) do
|
|
177
|
+
system("gem build #{cfg[:gemspec]}") || abort("Gem build failed")
|
|
178
|
+
gem_file = "#{cfg[:name]}-#{version}.gem"
|
|
179
|
+
system("gem push #{gem_file}") || abort("Gem push failed")
|
|
180
|
+
File.delete(gem_file) if File.exist?(gem_file)
|
|
181
|
+
end
|
|
182
|
+
puts "✓ Published #{cfg[:name]} #{version} to RubyGems"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Tag + push last, so CI cuts the Release only once the gem is live.
|
|
186
|
+
branch = `git branch --show-current`.strip
|
|
187
|
+
if system("git rev-parse #{tag} >/dev/null 2>&1")
|
|
188
|
+
puts "• tag #{tag} already exists — skipping tag"
|
|
189
|
+
else
|
|
190
|
+
system("git", "tag", tag) || abort("git tag failed")
|
|
191
|
+
end
|
|
192
|
+
system("git", "push", "origin", branch) || abort("git push branch failed")
|
|
193
|
+
system("git", "push", "origin", tag) || abort("git push tag failed")
|
|
194
|
+
|
|
195
|
+
puts "\n✓ Released #{tag}. GitHub Actions will cut the Release from the tag."
|
|
196
|
+
if key == "core"
|
|
197
|
+
puts " Next: bump the dashboard's chrono_forge floor if it should require #{version}, then release:dashboard:*"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Neutralize the dangerous bare `rake release` that bundler/gem_tasks defines
|
|
205
|
+
# (it would tag + gem push directly). Point people at the real flow instead.
|
|
206
|
+
if Rake::Task.task_defined?("release")
|
|
207
|
+
Rake::Task["release"].clear
|
|
208
|
+
desc "Disabled — use release:core:* or release:dashboard:* (see lib/tasks/release.rake)"
|
|
209
|
+
task :release do
|
|
210
|
+
warn "Use `rake release:core:prepare` then `rake release:core:publish` (or :dashboard). See lib/tasks/release.rake."
|
|
211
|
+
end
|
|
212
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chrono_forge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Froelich
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -52,6 +52,20 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: prism
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: rake
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -194,7 +208,10 @@ files:
|
|
|
194
208
|
- LICENSE.txt
|
|
195
209
|
- README.md
|
|
196
210
|
- Rakefile
|
|
211
|
+
- cliff.toml
|
|
197
212
|
- config.ru
|
|
213
|
+
- docs/design/per-child-commit-overhead.md
|
|
214
|
+
- docs/fanout-scale-test.md
|
|
198
215
|
- docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md
|
|
199
216
|
- docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md.tasks.json
|
|
200
217
|
- docs/superpowers/plans/2026-06-25-composite-retry-policies.md
|
|
@@ -205,6 +222,10 @@ files:
|
|
|
205
222
|
- docs/superpowers/plans/2026-06-26-branches-spawn-merge.md.tasks.json
|
|
206
223
|
- docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md
|
|
207
224
|
- docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md.tasks.json
|
|
225
|
+
- docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md
|
|
226
|
+
- docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md.tasks.json
|
|
227
|
+
- docs/superpowers/plans/2026-07-01-workflow-definition-dag.md
|
|
228
|
+
- docs/superpowers/plans/2026-07-01-workflow-definition-dag.md.tasks.json
|
|
208
229
|
- docs/superpowers/specs/2026-06-03-unified-retry-policy-design.md
|
|
209
230
|
- docs/superpowers/specs/2026-06-25-chrono_forge-dashboard-design.md
|
|
210
231
|
- docs/superpowers/specs/2026-06-25-composite-retry-policies-design.md
|
|
@@ -212,6 +233,7 @@ files:
|
|
|
212
233
|
- docs/superpowers/specs/2026-06-25-spawn-merge-branches-design.md
|
|
213
234
|
- docs/superpowers/specs/2026-06-26-dashboard-branch-view-design.md
|
|
214
235
|
- docs/superpowers/specs/2026-06-26-deferral-continuation-race-and-catchup-design.md
|
|
236
|
+
- docs/superpowers/specs/2026-07-01-workflow-definition-dag-design.md
|
|
215
237
|
- examples/continue_if_webhook_example.rb
|
|
216
238
|
- gemfiles/rails_7.1.gemfile
|
|
217
239
|
- gemfiles/rails_7.1.gemfile.lock
|
|
@@ -220,6 +242,9 @@ files:
|
|
|
220
242
|
- lib/chrono_forge/branch_probe.rb
|
|
221
243
|
- lib/chrono_forge/cleanup.rb
|
|
222
244
|
- lib/chrono_forge/cleanup_job.rb
|
|
245
|
+
- lib/chrono_forge/configuration.rb
|
|
246
|
+
- lib/chrono_forge/definition.rb
|
|
247
|
+
- lib/chrono_forge/definition_analyzer.rb
|
|
223
248
|
- lib/chrono_forge/error_log.rb
|
|
224
249
|
- lib/chrono_forge/execution_log.rb
|
|
225
250
|
- lib/chrono_forge/executor.rb
|
|
@@ -248,6 +273,7 @@ files:
|
|
|
248
273
|
- lib/generators/chrono_forge/templates/install_chrono_forge.rb
|
|
249
274
|
- lib/generators/chrono_forge/upgrade/USAGE
|
|
250
275
|
- lib/generators/chrono_forge/upgrade/upgrade_generator.rb
|
|
276
|
+
- lib/tasks/release.rake
|
|
251
277
|
- sig/chrono_forge.rbs
|
|
252
278
|
homepage: https://github.com/radioactive-labs/chrono_forge
|
|
253
279
|
licenses:
|