Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.5.1 (Next)

- [#49](https://github.com/dblock/ruby-gem-release/pull/49): Add ordered lifecycle hooks to standard and semantic workflows, trusted preparation hooks, structured hook failure details, a consumer customization example, and real-Git coverage - [@dblock](https://github.com/dblock).
- Your contribution here.

## 0.5.0 (2026/11/10)
Expand Down
4 changes: 3 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Execution platform, registry, and hosting are independent choices. Built-in exte

Both interfaces install dependencies, capture context, validate, prepare files, resolve the version, test, checkpoint and preserve artifacts, publish, prepare the next development iteration, and notify success. Validation raises on invalid state, including nothing to release; normal completion succeeds, while returning `false` also fails. Errors stop downstream operations and reach the failure callback after successful Ruby setup.

The standard publication callback commits prepared edits, builds reproducibly, tags locally and creates a recovery bundle, verifies or uploads the gem, atomically pushes its commit and tag, and creates or updates the hosted release. Git checks for concurrent branch changes before publication and again before pushing.
The checkpoint task commits prepared edits, builds reproducibly, tags locally, and creates a recovery bundle. After artifacts are preserved, publication repeats commit, build, and tag preparation before verifying or uploading the gem, atomically pushing its commit and tag, and creating or updating the hosted release. Git checks for concurrent branch changes before publication and again before pushing.

Every release action must be idempotent for the same checkout and inputs, without recovery flags, artifact discovery, restored context, or a completion journal. Tasks reconcile their own outputs; a fresh workflow uses its current checkout and inputs rather than discovering an older unfinished release. Registry publication and Git pushes cannot be atomic together, so both interfaces preserve artifacts before publication for manual inspection. See [rerunning releases](RELEASING.md#verify-and-recover).

Expand All @@ -37,6 +37,8 @@ Capture rejects non-default branches and dirty checkouts after dependency instal

Resolution validates the prepared version; publishing tasks enforce their own postconditions. RubyGems accepts an existing version only when its checksum matches. Git reuses matching release and development trees without moving tags or rewinding remote descendants. Hosting replaces differing metadata and assets. Development increments are anchored to the supplied release version, not a recorded completion marker.

Lifecycle hooks add checks and project edits without replacing the release sequence. Preparation hooks run inside trusted staging; other hooks validate or observe. See the [hook guide](docs/workflow/hooks.md).

Version objects update exactly one constant assignment without evaluating the file. Changelog objects use `Commonmarker` to find sections, then edit original source to preserve formatting. Handwritten formats reject empty notes before writing; malformed content fails rather than skipping. See [version](docs/version/ruby_constant.md), [changelog](docs/changelog/base.md), and [Git](docs/source_control/git.md) contracts.

## Contexts and State
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ For a real-world example of adding release automation to an existing gem, see [r
Replace only what you need. The [standard workflow](docs/workflow/ruby_gem.md) preserves ordering; [Detailed Usage](docs/usage.md) provides file examples and complete manual callbacks.

- **Project conventions:** Choose [version objects](docs/usage.md#version-objects) and [changelog formats](docs/usage.md#changelog-objects), including custom implementations.
- **Stage customization:** Add ordered [lifecycle hooks](docs/workflow/hooks.md) for validation, trusted preparation edits, publication observers, and notifications without replacing the pipeline.
- **Release decisions and callbacks:** Customize [validation, preparation, publication, and notifications](docs/tasks.md). Callbacks receive an [execution context](docs/execution_context.md) with release data, step history, and the configured workflow.
- **Semantic releases:** Opt into [Conventional Commits, release-history-based versions, and generated notes](docs/workflow/semantic_ruby_gem.md) without changing the execution platform or registry.
- **Packages and registries:** Override [package construction](docs/package/ruby_gem.md) or supply a [registry destination](docs/destination.md) independently of hosted releases.
Expand Down
6 changes: 5 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

Upgrade the gem and pinned workflow or action together. Apply the relevant changes below; see the linked PRs for details.

## Upgrading to 0.5.0 (Unreleased)
## Upgrading To 0.5.1

Additive [lifecycle hooks](docs/workflow/hooks.md) are available on both workflows without replacing existing callbacks. Existing success/failure callbacks and explicit replacement interfaces remain supported. Preparation hooks run inside trusted Git staging; publication order and build behavior are unchanged.

## Upgrading To 0.5.0

From 0.4.x, update both the gem and pinned Actions reference. Install only one workflow or manual task configuration.

Expand Down
8 changes: 7 additions & 1 deletion docs/execution_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ end

## Release

`ctx.release` is the immutable [`Release`](release.md) value used by changelog, package, Git, and hosting operations. It can be `nil` in a failure callback when release capture did not complete. Other callbacks require valid release data.
`ctx.release` is the immutable [`Release`](release.md) value used by changelog, package, Git, and hosting operations. It can be `nil` in a failure callback when release capture did not complete, or in a before-capture hook. Other callbacks require valid release data.

## Lifecycle Stage

[Lifecycle hooks](workflow/hooks.md) also expose the stage, timing, and hook position as `ctx.stage`, `ctx.hook`, and `ctx.hook_index`. Failure hooks expose the original error as `ctx.failure`; ordinary callback contexts leave these fields `nil`.

## Step

Expand Down Expand Up @@ -44,3 +48,5 @@ Without a state file, callbacks installed by the same `Tasks` instance share in-
## Failure

`failure.phase` identifies the failed step, while `failure.context.step.id` identifies the currently executing failure callback. `failure.error` retains the captured exception interface. Release data may be absent, but execution metadata remains available after an early failure. Failure callbacks cannot run before Ruby setup completes.

Captured workflow errors also expose `stage`, `hook`, and `hook_index`, identifying the built-in stage or particular failing hook across processes. These are absent when failure occurred outside an observed lifecycle stage.
4 changes: 4 additions & 0 deletions docs/package/ruby_gem.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ Builds use `SOURCE_DATE_EPOCH` for the gemspec date and archive timestamps. Work

Reproduce a release from the same source commit: equivalent files in another commit can have a different timestamp. Toolchains, generated files, gemspecs, and signing inputs must also remain stable. No previous archive is needed; partial output is overwritten. `built?(release)` verifies an existing archive and manifest without building.

The standard Actions workflow builds at checkpoint and again during publication. Each build writes the same reproducible archive and digest manifest; direct builds overwrite prior output.

## Artifact

`artifact(release)` verifies the built name, version, and digest and returns a copy named `NAME-VERSION.gem`. Registry destinations and hosted releases use those exact bytes.

Verification checks the retained archive against the manifest and validates its gem metadata before publication. `artifact(release)` copies the verified canonical archive to its name-and-version path.

## Errors

Build failures, version mismatches, and changed artifacts raise. The package does not contact a registry or upload files; see [`Destination::RubyGems`](../destination/ruby_gems.md) for publication and duplicate-version safeguards.
2 changes: 2 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The composite action first invokes `release:workflow:configure` to validate and

The provider constructs callback contexts: by default, the provider-neutral `ExecutionContext` locally or its [`GitHub`](execution_context/github.md) subclass on Actions. Explicit `provider:` injection overrides automatic detection. GitHub-specific step statuses and run fields are not part of the local context.

Standard workflows provide additive [lifecycle hooks](workflow/hooks.md) for checks and project edits. Manual callbacks replace task behavior and do not automatically get the standard workflow's inner hooks.

## Validate

`tasks.validate` checks release preconditions after context capture and before preparation edits. It receives an [ExecutionContext](execution_context.md) containing the same initial or explicitly selected release value as `prepare`. Raise on invalid state; normal completion, including `nil`, succeeds. Returning `false` also fails rather than skipping. Without a callback, validation passes.
Expand Down
10 changes: 5 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ RubyGemRelease::Workflow::RubyGem.new(
).install_tasks
```

This validates, prepares, builds once, publishes the verified gem, pushes commit and tag atomically, creates the hosted release, and prepares the next patch version.
This validates and prepares the release, publishes the verified gem, pushes the release commit and tag, creates the hosted release, and prepares the next patch version.

The default releases the checked-in version; optional `version` selects another through the version object's `set!`. The initial version remains in the context. Custom preparation callbacks own their file updates.

Configure optional `success:` and `failure:` lambdas on `Workflow::RubyGem.new` for notifications; success receives an execution context after the next development iteration succeeds, while failure receives a `RubyGemRelease::ReleaseFailure` with a phase, captured error when available, and execution context.

Before edits, `release:validate` requires pending notes or an existing heading for the selected release. Otherwise it raises, even with an explicit version. Override with a `validate:` lambda that raises on failure; returning `false` also fails. Without a changelog, validation passes.

Use [lifecycle hooks](workflow/hooks.md) to add checks or project edits without replacing callbacks. Preparation hooks run inside trusted Git staging; see the [example](../examples/lifecycle_hooks/Rakefile).

The package uses `GEMSPEC_PATH` or the only root gemspec. Repository, branch, and artifact directory use the existing checkout and workflow defaults. Missing or ambiguous selections fail instead of guessing.

## Object Overrides
Expand All @@ -56,7 +58,7 @@ Set `changelog: nil` for projects without a changelog; version incrementing and

## Custom Callbacks

This is the full equivalent of the standard workflow. Keep this ordering to preserve artifact identity, concurrency checks, and package-before-tag publication:
For a genuinely different sequence, configure replacement callbacks explicitly. Individual project customizations should use lifecycle hooks instead. Keep this ordering to preserve artifact identity, concurrency checks, and package-before-tag publication:

```ruby
require 'ruby-gem-release/workflow/ruby_gem'
Expand Down Expand Up @@ -86,9 +88,7 @@ RubyGemRelease::Tasks.install(workflow: workflow, package: package, destination:
end
tasks.publish do |ctx|
release = ctx.release
git.commit!(release)
package.build!(release)
git.tag!(release)
workflow.checkpoint!(ctx)
destination.publish!(release, package: package)
git.push!(release)
github.publish!(release, package: package)
Expand Down
46 changes: 46 additions & 0 deletions docs/workflow/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Lifecycle Hooks

Both the [standard](ruby_gem.md) and [semantic](semantic_ruby_gem.md) workflows support ordered `before` and `after` hooks. Register them before `install_tasks`:

```ruby
workflow.before(:validate) { |ctx| validate_project_notes(ctx) }
workflow.before(:prepare_release) { |ctx| remove_contribution_placeholders(ctx) }
workflow.after(:prepare_release) { |ctx| update_stable_readme(ctx) }
workflow.after(:prepare_next) { |ctx| restore_template_and_development_readme(ctx) }

workflow.install_tasks
```

For each stage, before hooks run in registration order, then the built-in operation, then after hooks in registration order if it succeeded. Hooks add behavior; they do not replace versioning, changelog, publication, Git, hosting, or validation.

## Context

Hooks receive an immutable execution context with the configured `ctx.workflow` and current `ctx.release`. `ctx.stage`, `ctx.hook`, and `ctx.hook_index` identify the stage, timing, and one-based position. Before capture, `ctx.release` is `nil`; semantic hooks see `ctx.analysis` and the selected version after validation. Next-version hooks see `ctx.release.next_version` after development preparation.

## Order

All stages below support both hook timings. This is the Actions sequence; a direct Rake task runs only its own stages.

- Setup: `capture` → `validate` → `prepare_release` → `resolve_version`. The prepared-release `test` command follows; it is not a hook stage.
- Checkpoint: `checkpoint` wraps `commit_release` → `build_package` → `tag_release`.
- Publication: Actions preserve artifacts, then run `authenticate` → `commit_release` → `build_package` → `tag_release` → `publish_registry` → `push_release` → `publish_hosted_release` (if hosting is enabled).
- Next development: `prepare_next` → `commit_next` → `push_next` → `success`.
- Failure: `failure` runs through the existing failure-reporting task.

Actions run the checkpoint before publication, so `commit_release`, `build_package`, and `tag_release` hooks run twice. Standalone `release:publish` authenticates and runs those three stages once. `push_release` pushes the release commit and tag; `push_next` pushes only the development commit. Artifact preservation and authentication are not part of the checkpoint.

## Safe Edits and Retries

Only `prepare_release` and `prepare_next` are file-editing stages. Both hook timings run inside `git.stage_changes!`, before the release or next-development commit. If a hook raises, later hooks and that stage's after hooks do not run; the release stops, but partial working files are not rolled back.

Use other hooks to validate or observe. Publication rebuilds the package after the Actions checkpoint, so edits to generated artifacts may be replaced. Preserve existing publication ordering and concurrency checks when replacing workflow operations.

Hooks may run again when a task or Actions job is retried; make them idempotent. Authentication hooks are skipped when the destination confirms publication, hosting hooks are skipped when hosting is disabled, and semantic development hooks are skipped when `bump: false`. Each Actions process reloads the Rakefile and registers hooks again; callbacks are not serialized.

## Callbacks and Failures

Existing `success:` and `failure:` callbacks run inside their stages, between before and after hooks. Use `before(:validate)` or `after(:validate)` to add checks; the existing `validate:` option still replaces validation.

Hook exceptions keep their original class and message and are recorded as `failure.error.stage`, `.hook`, and `.hook_index`. Built-in failures have no hook timing or index. `failure.phase` remains the enclosing task or Actions step.

See the [consumer example](../../examples/lifecycle_hooks/Rakefile) for project-local edits using the standard workflow.
14 changes: 9 additions & 5 deletions docs/workflow/ruby_gem.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Default validation requires `released?(release.version)` or `pending?`. This per

## Package

The optional `package:` defaults to [`Package::RubyGem`](../package/ruby_gem.md), supplying capture, prepared-version checks, and reproducible builds. Implement `name`, `version`, `build!(release)`, `artifact(release)`, and `built?(release)` for equivalent verification and publication. `gemspec` remains optional for capture.
The optional `package:` defaults to [`Package::RubyGem`](../package/ruby_gem.md), supplying capture, prepared-version checks, and reproducible builds. Implement `version`, `build!(release)`, `artifact(release)`, and `built?(release)` for equivalent verification and publication. `gemspec` remains optional for capture.

## Destination

Expand All @@ -38,12 +38,16 @@ The optional `hosting:` defaults to [`Hosting::GitHubRelease`](../hosting/github

`install_tasks` registers preparation, publication, next development iteration, success, failure, and workflow-context tasks. It does not immediately release or change project files. Use it instead of manual `Tasks.install` callback configuration, not alongside another installation.

Preparation updates the changelog with the selected version. Checkpoint commits, builds, and tags locally before artifact preservation. Publication authenticates unless the destination already proves the exact artifact is published, repeats checkpoint, verifies or uploads the package, pushes commit and tag atomically, and updates hosting. The next development iteration calculates from `release.version`, updates the changelog, and reconciles its commit and push.
Preparation updates the changelog with the selected version. In Actions, the checkpoint commits, builds, and tags before artifacts are preserved. Publication then authenticates as needed, repeats those three operations, publishes the package, pushes the release commit and tag, and updates hosting. The next development iteration prepares, commits, and pushes the next version.

Pass optional `success:` and `failure:` callbacks to the constructor for result notifications. Success receives an [ExecutionContext](../execution_context.md) with the published release and recorded next development version after the next development iteration succeeds. Failure receives a `RubyGemRelease::ReleaseFailure` with `phase`, `error`, and `context` properties; `failure.context.release` may be absent. A captured Ruby exception is reconstructed with its original class name, message, and stack trace; failures only reported by GitHub Actions have no `error`. A failure callback error does not turn a failed workflow into a successful one.
Pass optional `success:` and `failure:` callbacks for notifications. Success receives an execution context after the next development iteration completes; failure receives a `RubyGemRelease::ReleaseFailure`. Early failures may not have a release context, and failure callbacks do not turn a failed workflow into a successful one.

Callbacks can access this configured workflow through `ctx.workflow`. Public readers expose the configured `version`, `changelog`, `package`, `destination`, `git`, `hosting`, and `provider` objects; they are reconstructed through the Rakefile in each Ruby process, not serialized across Actions steps.
Contexts expose the configured `ctx.workflow`. Each process reconstructs the workflow from the Rakefile; it is not serialized across Actions steps.

## Lifecycle Hooks

Use `before(stage)` and `after(stage)` hooks to add checks or project edits without replacing callbacks. Preparation hooks run inside trusted Git staging. See the [hook guide](hooks.md) for ordering, safety, and an example.

## Errors

Errors propagate through the tasks and stop the sequence. The default objects preserve concurrency checks, artifact verification, recovery bundles, and package-before-tag publication. Callbacks receive execution snapshots; the release value inside the failure context is `nil` if capture failed. Overrides must preserve the publication order and artifact identity.
Errors stop the sequence. Default objects preserve concurrency checks, artifact verification, recovery bundles, and package-before-tag ordering. A failure context may not have a release if capture failed; custom operations must preserve publication order and artifact identity.
Loading
Loading