Skip to content

Releases: dangra/durable

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 18 Sep 03:29
b403860

Modules

  • github.com/dangra/durable@v0.10.0
  • github.com/dangra/durable/contrib/durableotel@v0.10.0

Migration from v0.9.0

durable owes no compatibility to earlier releases; this release breaks the generated API, the handler contract, the proto options, and the cancel model.

  • Regenerate every pipeline proto (#99). The durable.v1.step and durable.v1.pipeline options are now extensions 1374 and 1375, durable's registered numbers; a descriptor compiled against 51841/51842 is invisible to the generator and the runtime.
  • Steps nest in their pipeline message (#95). A step is a message declared directly inside the pipeline message, in topology order; the steps list option is gone. Generated step types are Pipeline_Step.
  • One handler interface per pipeline (#93, #96). NewXxx(h XxxHandlers) takes one implementation with a method per step, Unwind<Step> per unwinding step, and ReduceOutput / ReduceFailure when the pipeline declares outputs. Per-step handler types and XxxFunc adapters are gone; handler methods receive XxxInvocation, a durable.TypedInvocation[*XxxInput].
  • Per-pipeline middleware (#97). NewXxx(h, xxxpb.WithMiddleware(mw...)) installs middleware on one pipeline, composed inside the engine chain once at Bind; hand-rolled definitions set pipelinedef.Config.Middleware.
  • Cancellation (#92, #98). A dormant or parked operation resolves as canceled at once with no handler run; a running attempt's context is canceled with a *durable.PreemptedError cause and anything but success resolves it as canceled; reduction is uncancellable; a park cascades to its targets only with durable.WithCancelCascade(). CancelRequested, FailFastOnCancel, and Yield are removed.
  • Shutdown interrupts, it does not fail (#89). An attempt cut by Stop that returns an ordinary error records no last error and no retry backoff; observers see AttemptInterrupted.
  • Run classes and unbounded workers (#87, #88). run_class on a pipeline plus engine.WithRunClass bound started nonterminal Runs with an eligibility-ordered line; workers are unbounded unless WithConcurrency is set.

What's Changed

  • Run classes: a run-scoped counting bound with an eligibility-ordered line by @dangra in #87
  • Unbounded workers by default by @dangra in #88
  • Run-class test: wait for every queued Run before reading Stats by @dangra in #91
  • Shutdown interrupts an attempt; it does not fail it by @dangra in #89
  • Cancellation: resolve the pending operation, cut the running one, cascade to children by @dangra in #92
  • Generated pipelines: one handler interface, one typed invocation by @dangra in #93
  • Steps nest in their pipeline message; the steps list is gone by @dangra in #95
  • Generated handlers: Reduce is ReduceOutput by @dangra in #96
  • A cancel request racing an attempt's registration still cuts it by @dangra in #98
  • Per-pipeline middleware, composed once at Bind by @dangra in #97
  • Extension numbers 1374 and 1375, durable's registered allocation by @dangra in #99
  • Release v0.10.0 by @dangra in #100

Full Changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 22:54
996b2e8

v0.9.0: the terminal stage, blobs in their place, and reads shaped to what they answer

Twelve changes since v0.8.0, on one theme: what a run costs the store and the engine to keep, read, and finish. Four reshape contracts and set this release's migration section.

  • The terminal stage (#74, #76). A run's terminality commit replaces its whole nonterminal stage — input, operation history, cursor, failure, cancel — with one terminal record, in the same atomic step. What a finished run keeps is its identity, outcome, output, commit time, failure, cancel request, and failed unwinds; everything else is released at terminality, not at retention. bbolt's layout is rebuilt around it: one append-only active bucket keyed by run and tag with operations in resolution order, the cursor on its own, a terminal row per finished run, an expiry index so reap walks its victims and decodes nothing, and a staged bucket that frees released stages in batches. ListRuns leaves the store contract, InputBytes on a terminal run returns ErrRunTerminal, and every reserved proto field is deleted outright.

  • Blobs in their place, and cached (#78, #81). An input, state, or output over half a page lives in a nested bucket beside its row, so writing or deleting it touches its own pages; smaller ones stay inside their records. Fat-state disk bytes drop about 65%. The store keeps in-memory copies of the blobs of runs in flight and of the large outputs of terminal runs, two bounded LRUs (64 MiB and 16 MiB by default) set through bbolt.Open options or the URI query. Input, State, and Output slices are declared immutable and shareable: stores retain and return shared memory, and bytes per op fall 65–75% in fat-input scenarios.

  • Reads shaped to what they answer (#83, #85). GetRunHead returns a run without its blobs or history, and it is what Status, Wait, the lookups, await bookkeeping, and recovery read — polling many idle runs no longer churns the blob cache, and cold-run polling is about six times faster. The reconcile loop reads the full record once per dispatch and carries it; a cancel goes through the engine, which marks the run dirty so the loop re-reads. A straight run costs three store reads instead of eleven, and allocations per op drop 25–39% across the population scenarios. Wait registers its watcher before its single read.

  • Cancel tightened (#85). A cancel request after the first is a true no-op. It used to re-preempt whatever attempt was running, including the retry the contract says continues cooperatively.

  • The perf suite as an instrument (#75, #77, #79, #80, #82). Recovery and SupersedeCycle sized past a single leaf per bucket, a shape matrix of none, slim, and fat for input, state, and output, output scenarios that read their output, a cold working-set scenario, and reported polls/sec and reads/run.

Breaking changes and migration

  • bbolt on-disk layout changed with no migration: a database written by v0.8.0 or earlier is not readable by this driver. Discard it.
  • Terminal runs release their input: Run.InputBytes and the generated typed Input() return durable.ErrRunTerminal once the run is terminal. Read the input before Wait returns, or read the output, which the reducer folded it into.
  • Immutable blobs: the slices InputBytes, OutputBytes, and a store's RunRecord return are shared with the store and must not be modified. The generated typed accessors still return caller-owned messages.
  • Store SPI (store/driver): ListRuns is gone; GetRunHead is new and required; ListNonterminal returns heads; RunRecord.CompactTerminal is the shared terminal-stage rule; Clone shares blob slices. store/mem keeps Runs() as a test enumeration.
  • bbolt driver: Open(path, opts ...Option) with WithBlobCache and WithOutputCache; URI query keys blob_cache and output_cache with K/M/G/KiB/MiB/GiB units, 0 disabling; the database is opened with NoFreelistSync.
  • Storage proto: RunMeta drops its input, Terminal is new, Cursor.awaiting_run_id and every reserved field are removed; buf's RESERVED_MESSAGE_NO_DELETE and FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED are excepted for the internal schema.

contrib/durableotel v0.9.0 is released in lockstep.

Modules

  • github.com/dangra/durable@v0.9.0
  • github.com/dangra/durable/contrib/durableotel@v0.9.0

What's Changed

  • perf: size Recovery and SupersedeCycle past a single leaf per bucket by @dangra in #75
  • Terminal stage: release input and step states when a run ends by @dangra in #74
  • proto: drop every reserved and deprecated field by @dangra in #76
  • perf: fat state and fat output scenarios by @dangra in #77
  • perf: the shape matrix by @dangra in #79
  • perf: drop the FatState and FatOutput composites by @dangra in #80
  • bbolt: large inputs, states, and outputs in buckets of their own by @dangra in #78
  • perf: the output shapes read their output after Wait by @dangra in #82
  • bbolt: cache the blobs of the runs in flight; Input, State, and Output are immutable and shareable by @dangra in #81
  • store: GetRunHead, the read for observation by @dangra in #83
  • Drop the perf.test binary committed by mistake by @dangra in #84
  • engine: the reconcile loop reads the full record once per dispatch by @dangra in #85
  • Release v0.9.0 by @dangra in #86

Full Changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 02:59
b7303f6

v0.8.0: mutexes, operation records, one Failure, and the failure reducer

Eight changes since v0.7.0. Four reshape contracts and set this release's migration section; four are additive.

  • Mutexes replace exclusion groups (#67). A pipeline names any number of mutexes; a run holds each on its ResourceID from admission to terminality, and two pipelines exclude each other exactly when they share a name. Exclusion is pairwise, so a snapshot pipeline can exclude both a deploy and a backup while deploys and backups run together. Still an admission rule resolved at Start, never persisted, so changes converge on their own.

  • Operation records (#70). A step's forward execution and its unwind are two rows, each written once when it resolves, carrying status, attempts, the committed state (forward only), the permanent failure that resolved it, and its resolution order. An unwind never rewrites the forward row's state, and nothing in the bbolt driver is read back to be rewritten. Result.UnwindFailures is gone; the per-step facts live in the store.

  • One Failure type (#71). FailureRecord, RootFailure, and UnwindFailure collapse into Failure; the role is where the value sits. A run's failure is Result.Failure, Status.Failure (set from the moment the run starts unwinding), and Invocation.Failure(). The aggregate handler-side struct and the unwind-failure list are gone from the handler contract.

  • Failure reducer (#72). A pipeline may declare failure_output; a FailureReducer folds the input, the committed states, the run's Failure, and the per-step UnwindFailure(step) into it when the unwind completes, and the typed Result exposes it as FailureOutput(). The snapshots example reports the object a jammed storage backend left behind.

  • Additive: generated NewXxxInvocation(core) and XxxReducer.Reduce(view) so durabletest.NewInvocation drives generated handlers and reducers directly (#64); durableotel unwind spans carry durable.run_failure.step, .kind, and .reason (#65); WithTextLimit bounds every free text the engine records, default 4096 bytes (#68); option names follow what they configure (#66).

Breaking changes and migration

  • Protos: exclusion_group: "x" is mutexes: "x" (field 5 reserved). Hand-written configs: ExclusionGroup: "x" is Mutexes: []string{"x"}.
  • Options: engine.WithRetention is WithRetentionPolicy; durableotel.WithBaggage is WithBaggagePropagation. WithObserver and WithScheduleAnnotator are variadic; single-argument calls compile unchanged.
  • Failure types: durable.RootFailure, UnwindFailure, and FailureRecord are durable.Failure. res.RootFailure is res.Failure; inv.Failure().Root.X is inv.Failure().X; inv.Failure().UnwindFailures has no replacement on the invocation, and Result.UnwindFailures is gone. Read per-step unwind failures in a failure reducer through UnwindFailure(step).
  • Typed invocations gain Failure() and Logger(); third-party fakes of durable.Invocation must add Failure(), and fakes of durable.ReduceView must add Failure() and UnwindFailure(StepID).
  • Store SPI (store/driver): OperationRecord, StepRecord{Forward, Unwind}, OpWrite{StepID, Phase, Record}, Transition.Ops; Transition.UnwindFailure and RunRecord.UnwindFailures (the field) are gone, RunRecord.RootFailure is Failure, and RunRecord.UnwindFailures() is a derived view.
  • bbolt on-disk layout changed with no migration: a database written by v0.7.0 or earlier is not readable by this driver. Discard it.
  • Storage proto: the StepRecord and Failures messages are replaced by OperationRecord; buf's MESSAGE_NO_DELETE is excepted for the internal schema.

contrib/durableotel v0.8.0 is released in lockstep.

Modules

  • github.com/dangra/durable@v0.8.0
  • github.com/dangra/durable/contrib/durableotel@v0.8.0

What's Changed

  • codegen: typed invocation constructors and Reducer.Reduce for engine-free tests by @dangra in #64
  • durableotel: unwind spans carry the failure being unwound by @dangra in #65
  • Option names follow what they configure by @dangra in #66
  • Exclusion groups become mutexes by @dangra in #67
  • Bound the free text the engine records on a run by @dangra in #68
  • Operation records: one row per step phase, carrying its own failure by @dangra in #70
  • One Failure type; Status carries the Run's failure from the start of unwind by @dangra in #71
  • Failure reducer: a typed account of a failed run by @dangra in #72
  • Release v0.8.0 by @dangra in #73

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 15:05
7be40b4

v0.7.0: the lookup API, exclusion as an admission rule, and Invocation.Failure

Three contract changes and one new example, all shaped by what the engine, handler authors, and the flyd pilot need; dashboard-style listing APIs are gone.

  • Run lookup (#57, #58). A bound pipeline has three read methods: Schedule, GetRun(id), and GetActiveRun(resource). Engine.GetRun(id) finds a run across pipelines for callers holding only a RunID, with Status naming the pipeline for the typed upgrade. GetActiveRun is one indexed store read. The listing methods (ListActiveRuns, GetRuns, and their engine-level counterparts) are removed.

  • Exclusion groups are an admission rule (#59). A run's slot is always (PipelineID, ResourceID); the store never persists a group. Membership is resolved from the bound definitions at Engine.Start and checked at Schedule against every member's slot in one transaction. Adding, removing, or renaming a group converges by itself: in-flight runs finish, new admissions follow the current definitions from the first Schedule. This fixes a drift where a renamed group could admit two runs of one pipeline on one resource.

  • Recovery walks the slot index (#60). ListNonterminal in the bbolt driver costs the runs in flight rather than the retained history.

  • Unwind handlers read the failure from the invocation (#61). The durable.Failure parameter is gone; Invocation.Failure() *Failure is non-nil exactly in PhaseUnwind, so middleware sees it too. Typed invocations gain Failure() and Logger() (#62).

  • examples/snapshots (#62) is a pipeline assembled entirely from the generated http.HandlerFunc-style adapters: closures over a dependency struct, no handler types.

Breaking changes and migration

  • Unwind handlers: delete the third parameter and read inv.Failure() where it was used.

    // before
    func (h *reserve) Unwind(ctx context.Context, inv pb.ReserveInvocation, f durable.Failure) error
    // after
    func (h *reserve) Unwind(ctx context.Context, inv pb.ReserveInvocation) error {
        root := inv.Failure().Root // never nil during unwind

    pipelinedef.Step.UnwindFunc and the generated XxxFuncs.UnwindFunc change the same way. durabletest.InvocationConfig gains a Failure field.

  • Bound pipelines: Run(id) is GetRun(id), ActiveRun(resource) is GetActiveRun(resource); Active(), Runs(resource), and the engine-level listings are removed. Keep a RunID and poll it with Engine.GetRun.

  • Store SPI (store/driver): CreateRun(ctx, rec, excluding []PipelineID) takes the exclusion set; GetActiveRunID(ctx, pipeline, resource) is new; RunRecord.Group is gone. Third-party drivers must check every excluded pipeline's slot atomically with their own.

  • bbolt databases: proto field 4 of the run meta (slot_group) is reserved and ignored; a v0.6.0 database opens unchanged. A run created under a persisted group keeps running and is admitted against by the new rule.

contrib/durableotel v0.7.0 is released in lockstep.

Modules

  • github.com/dangra/durable@v0.7.0
  • github.com/dangra/durable/contrib/durableotel@v0.7.0

What's Changed

  • Bound pipeline lookups: GetRun, GetActiveRun, ListActiveRuns, GetRuns by @dangra in #57
  • Drop the listing APIs; GetActiveRun becomes one indexed read by @dangra in #58
  • Exclusion groups are an admission rule, never a persisted slot by @dangra in #59
  • bbolt: ListNonterminal walks the slot index by @dangra in #60
  • Unwind handlers read the failure through Invocation.Failure by @dangra in #61
  • examples/snapshots: a pipeline assembled from handler func adapters by @dangra in #62
  • Release v0.7.0 by @dangra in #63

Full Changelog: v0.6.0...v0.7.0

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 04:13
c04e274

v0.6.0: stores under store/, opened by URI

The storage side takes the database/sql shape (#55):

  • store.Open(uri) opens a store from a URI through drivers that register their scheme in init. store itself knows no driver: blank-import the ones you want and nothing else is linked into the binary.

    import (
        "github.com/dangra/durable/store"
        _ "github.com/dangra/durable/store/bbolt"
    )
    
    st, err := store.Open("bbolt:///var/lib/app/durable.db")
    eng := engine.New(st)
  • store/bbolt is the persistent driver (bbolt:///abs/path, bbolt:/abs/path, bbolt:rel/path; no host, no options yet, a query string is an error).

  • store/mem is the in-memory driver (mem:), promoted from a test double to a real store for CLI tools and other programs whose runs are ephemeral. It remains the executable reference the bbolt driver is differentially fuzzed against.

  • store/driver is the SPI backends implement.

Breaking changes and migration

  • github.com/dangra/durable/storedriver is github.com/dangra/durable/store/driver (package driver).
  • github.com/dangra/durable/bboltstore is github.com/dangra/durable/store/bbolt (package bbolt; alias one side if you also import go.etcd.io/bbolt). bbolt.Open(path) is unchanged, or use store.Open("bbolt:...").
  • durabletest.NewMemStore() is mem.New() in github.com/dangra/durable/store/mem, or store.Open("mem:"). durabletest keeps the fake Clock and the fake Invocation.
  • No wire or behavior change: a v0.5.0 bbolt database opens unchanged.

contrib/durableotel v0.6.0 is released in lockstep.

Modules

  • github.com/dangra/durable@v0.6.0
  • github.com/dangra/durable/contrib/durableotel@v0.6.0

What's Changed

  • Stores under store/: driver SPI, bbolt and mem drivers, Open by URI by @dangra in #55
  • Release v0.6.0 by @dangra in #56

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 22:05
754b118

v0.5.0: the package layout release

The module is now split by audience, and every import arrow points from the more specific package to the more general one (#47):

  • durable is the handler contract and nothing else: Invocation, Failure, Fail, AwaitRun/AwaitAll/AwaitAny, Handler and Middleware with their result classifiers, the step references, the Schedule options a step uses to fan out children, and aliases of the shared vocabulary. Handler files import only this package.
  • engine is the wiring side: engine.New, the With* options, Start/Stop/Stats, Bind, Pipeline, Run, Result, Status.
  • pipelinedef is the type-erased definition generated code builds and Engine.Bind validates.
  • kernel is the shared leaf (identities, phases, outcomes, parks, failure records); storedriver and observe no longer depend on the root.
  • durabletest.NewInvocation is a fake Invocation for unit-testing handlers without an engine or a store.

Breaking changes and migration

  • durable.NewEngine is engine.New; Engine, the With* options, Pipeline, Run, Result, Status, RunState, Clock, RetryPolicy, RetentionPolicy, ScheduleAnnotator are in engine. ErrEngineNotStarted/ErrEngineStarted are engine.ErrNotStarted/engine.ErrStarted.
  • Invocation and ReduceView are interfaces. Handler and middleware signatures take durable.Invocation by value, not *durable.Invocation.
  • NewDefinition, DefinitionConfig, StepConfig are pipelinedef.New, pipelinedef.Config, pipelinedef.Step; def.Bind(engine) is engine.Bind(def), which now validates and returns an error instead of the constructor panicking.
  • StepRef and StateStepRef have exported fields (Step, New); NewStepRef/NewStateStepRef are pipelinedef.StepRef/pipelinedef.StateStepRef.
  • Generated code must be regenerated with this version's protoc-gen-durable (buf generate).
  • Middleware classifiers gained AwaitTimeout, FailureCause, and FailureReason beside AwaitRequest and FailureInfo.

contrib/durableotel v0.5.0 is released in lockstep. The specification is at Draft 1.3.

Modules

  • github.com/dangra/durable@v0.5.0
  • github.com/dangra/durable/contrib/durableotel@v0.5.0

What's Changed

  • Spec Draft 1.2: parks, cancellation, drain, observability, API surface by @dangra in #46
  • Extract the shared vocabulary: durable/kernel by @dangra in #48
  • Invocation and ReduceView become interfaces by @dangra in #49
  • Move the engine to its own package: durable/engine by @dangra in #50
  • Extract the codegen adapter surface: durable/pipelinedef by @dangra in #51
  • Docs: describe the v0.5 package layout (spec Draft 1.3) by @dangra in #52
  • Schedule options and child-run errors belong to the handler contract by @dangra in #53
  • Release v0.5.0 by @dangra in #54

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 19:18
1648d8f

Modules

  • github.com/dangra/durable@v0.4.0
  • github.com/dangra/durable/contrib/durableotel@v0.4.0

What's Changed

  • durableotel: require durable v0.3.0 for release by @dangra in #35
  • Run.Wait: fail fast with ErrRunInProgress inside a handler by @dangra in #36
  • Multi-target parks and deadlines: AwaitAll, AwaitAny, WithAwaitTimeout by @dangra in #38
  • Await gate: push completions to parkers; fan-in benchmark by @dangra in #39
  • Tests: shared harness, engine_test.go split by concern by @dangra in #40
  • Freeze the pipeline registry at Start with internal/frozen by @dangra in #41
  • Lockstep releases: VERSION, scripts/release.sh, tag-release workflow by @dangra in #42
  • Release v0.4.0-rc.1 by @dangra in #43
  • release.sh: wait for in-flight check runs before tagging by @dangra in #44
  • Release v0.4.0 by @dangra in #45

Full Changelog: v0.3.0...v0.4.0

v0.4.0-rc.1

v0.4.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 05 Sep 19:06
2831005

Modules

  • github.com/dangra/durable@v0.4.0-rc.1
  • github.com/dangra/durable/contrib/durableotel@v0.4.0-rc.1

What's Changed

  • Add CI workflow and dependabot config by @dangra in #1
  • Add performance regression suite gated in CI by @dangra in #2
  • Add CoordinationMix perf scenarios; exact-deterministic write gates by @dangra in #3
  • Logging story: engine lifecycle events and Invocation.Logger by @dangra in #4
  • Metrics story: Observer lifecycle hooks and Engine.Stats by @dangra in #5
  • Extract keyed wait primitives into internal/watchset by @dangra in #6
  • Extract the concurrency-class gate into internal/tokenpool by @dangra in #7
  • Extract the run dispatcher into internal/dispatcher by @dangra in #8
  • Tidy-ups: bboltstore groupCommit helper, ULID generation to ids.go by @dangra in #9
  • Read the frozen pipelines map without locking in processRun by @dangra in #10
  • Fuzz the extracted primitives; hold them at 100% coverage in CI by @dangra in #11
  • Differentially fuzz the stores; harden identifier and text contracts by @dangra in #12
  • Model-stress crash-restart convergence by @dangra in #13
  • Unit-test perfcompare: the gate that guards the gates by @dangra in #14
  • Annotations: durable trace propagation and run metadata by @dangra in #15
  • Add contrib/durableotel: OpenTelemetry integration as a contrib module by @dangra in #16
  • durableotel: log correlation — stamp trace_id/span_id onto slog records by @dangra in #17
  • Close observability gaps found against flyd's FSM library by @dangra in #18
  • durableotel: opt-in W3C Baggage relay through the Run by @dangra in #19
  • Declare propagation once: engine-wide ScheduleAnnotator by @dangra in #20
  • Perf gate: wall-clock alarms require deterministic corroboration by @dangra in #21
  • README: drop the layout table, add the observability story by @dangra in #22
  • Godoc examples for the feature surface by @dangra in #23
  • Add the guided tour: docs/tour.md by @dangra in #24
  • Add the flagship example: examples/release-train by @dangra in #25
  • CI: split the fuzz smoke into its own parallel job by @dangra in #26
  • durableotel: require durable v0.2.0 for release by @dangra in #27
  • Tour: ctx.Err() is an ordinary error by @dangra in #28
  • Cancellation causes, verified yield attribution, and FailFastOnCancel by @dangra in #29
  • Tour follow-up + FailFastExcept: per-step cooperative escape hatch by @dangra in #30
  • Graceful drain: WithDrainTimeout on Engine.Stop by @dangra in #31
  • Extract the store SPI: durable/storedriver by @dangra in #32
  • Extract the lifecycle event surface: durable/observe by @dangra in #33
  • doc.go: the API in five groups by @dangra in #34
  • durableotel: require durable v0.3.0 for release by @dangra in #35
  • Run.Wait: fail fast with ErrRunInProgress inside a handler by @dangra in #36
  • Multi-target parks and deadlines: AwaitAll, AwaitAny, WithAwaitTimeout by @dangra in #38
  • Await gate: push completions to parkers; fan-in benchmark by @dangra in #39
  • Tests: shared harness, engine_test.go split by concern by @dangra in #40
  • Freeze the pipeline registry at Start with internal/frozen by @dangra in #41
  • Lockstep releases: VERSION, scripts/release.sh, tag-release workflow by @dangra in #42
  • Release v0.4.0-rc.1 by @dangra in #43

New Contributors

  • @dangra made their first contribution in #1

Full Changelog: https://github.com/dangra/durable/commits/v0.4.0-rc.1