BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

A real changelog and release, from the terminal

Cutting a release shouldn't mean remembering the version-bump rules, the tag format, and the build flags. One OrchStep task computes the next version, writes the changelog, tags, and builds — and shows you the plan before it runs.

Jun 20, 2026 OrchStep Team 6 minROLE: OSS MaintainerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/changelog-release
VIEW SOURCE

Cutting a release is a sequence of small, error-prone decisions. Is this a patch or a minor? What was the last version — was it 1.3.0 or 1.3.1? Did I tag before or after the build? Get the order wrong and you've pushed a tag for a version that doesn't exist yet. So releases become the thing only one maintainer does, on one machine, from memory.

None of those decisions are hard. They're just unwritten. A release is: compute the next version from the bump level, generate the changelog, tag it, build the artifacts. Write that down as a task and anyone can cut a release — after seeing exactly what it'll do.

The release task

The version math is real logic, not string-mashing, so it goes in a transform step — sandboxed JavaScript that returns the next version as a structured value the rest of the task reads:

orchstep.yml
name: release
defaults:
  level: patch          # patch | minor | major
  current: "1.3.0"

tasks:
  # `orchstep run release --var level=minor --dry-run`
  release:
    desc: "Bump version, changelog, tag, and build"
    steps:
      - name: bump
        func: transform
        do: |
          const p = vars.current.split(".").map(Number);
          if (vars.level === "major") { p[0]++; p[1] = 0; p[2] = 0; }
          else if (vars.level === "minor") { p[1]++; p[2] = 0; }
          else { p[2]++; }
          return { version: p.join(".") };

      - name: changelog
        func: shell
        do: echo "generating changelog for v{{ steps.bump.version }}"
        outputs:
          notes: "CHANGELOG-{{ steps.bump.version }}.md"

      - name: tag
        func: shell
        do: echo "git tag v{{ steps.bump.version }} (from {{ steps.changelog.notes }})"

      - name: build
        func: shell
        do: echo "building release artifacts for v{{ steps.bump.version }}"

(The shell steps echo so the demo runs anywhere — swap in your changelog tool, git tag, and build command to make it live.)

Semver as code, not as a guess

The bump step encodes the rule everyone says they follow: major resets minor and patch, minor resets patch, patch increments. Once it's written down, --var level=minor against 1.3.0 deterministically produces 1.4.0 — every time, on every machine. The downstream steps read steps.bump.version, so the version is computed once and flows through tag and build identically. No copy-paste of a number you typed at the top.

Order is encoded too: changelog, then tag, then build, top to bottom. You can't accidentally tag before the changelog exists, because the steps run in the order they're written.

Dry-run first, every time

The reason this is safe to hand to anyone: you can see the whole release before a single command fires. A dry-run resolves the bump, fills in every version reference, and prints the plan:

orchstep run release --var level=minor --dry-run

You read it — "yes, 1.4.0, tag v1.4.0, changelog CHANGELOG-1.4.0.md" — and only then drop --dry-run:

orchstep run release --var level=minor

What you actually gained

ConcernBy-hand releaseOrchStep release task
Next versionmental semver matha transform step, deterministic
Step orderremember tag-after-buildencoded top to bottom
Version reused everywhereretype the numbersteps.bump.version flows through
Preview before taggingnone--dry-run prints the plan
Who can cut a releasethe one who remembersanyone who can read the task

Where OrchStep is not the answer

If a full release tool already owns your pipeline — semantic-release, GoReleaser, release-please — keep it; those do far more (artifact signing, multi-registry publish, PR automation). This pattern is for the project that has no release process yet, or one that lives in a maintainer's head — a readable, dry-runnable first step you can grow later.

Where to go next

No real release process yet — just one maintainer who remembers the steps? Write them down as a dry-runnable task. Install the binary and start with the demo above.

#RELEASE#CHANGELOG#VERSIONING#TRANSFORM
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY