diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec22100..b03a2ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,10 @@ jobs: --health-timeout=5s --health-retries=5 + # Test against the durable in this checkout, not the one on Hex. + env: + DURABLE_PATH: ../durable + defaults: run: working-directory: durable_dashboard diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d67d9fd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,162 @@ +name: Release + +# Actions → Release → Run workflow, pick a package. It: +# +# 1. picks the version (the input, or the next one git-cliff derives from +# the package's conventional commits since its last tag), +# 2. bumps @version in mix.exs, regenerates the package's CHANGELOG.md, +# commits both to main and tags `-v`, +# 3. publishes to Hex (needs the HEX_API_KEY secret) and creates the GitHub +# release with that version's changelog as its notes. +# +# Publish is safe to retry with "Re-run failed jobs": it reuses the tag and +# skips what already went out. Release durable before a durable_dashboard +# that needs its changes, since the dashboard resolves durable from Hex. + +on: + workflow_dispatch: + inputs: + package: + description: Package + type: choice + options: [durable, durable_dashboard] + version: + description: "Version, e.g. 1.2.0 (blank: next from conventional commits)" + required: false + +concurrency: release + +env: + ELIXIR_VERSION: "1.20" + OTP_VERSION: "29" + PACKAGE: ${{ inputs.package }} + +jobs: + tag: + name: Tag + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + tag: ${{ steps.release.outputs.tag }} + version: ${{ steps.release.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - uses: taiki-e/install-action@v2 + with: + tool: git-cliff + + - name: Bump, changelog, commit and tag + id: release + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + cliff() { git cliff --include-path "$PACKAGE/**" --tag-pattern "^$PACKAGE-v" "$@"; } + + version=${INPUT_VERSION:-$(cliff --bumped-version)} + version=${version#"$PACKAGE-v"} + tag="$PACKAGE-v$version" + + if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'$version' is not a semantic version"; exit 1 + fi + if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then + echo "::error::$tag already exists"; exit 1 + fi + + sed -i -E "s/^([[:space:]]*@version )\"[^\"]+\"/\1\"$version\"/" "$PACKAGE/mix.exs" + grep -q "@version \"$version\"" "$PACKAGE/mix.exs" + cliff --tag "$tag" --output "$PACKAGE/CHANGELOG.md" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$PACKAGE/mix.exs" "$PACKAGE/CHANGELOG.md" + git commit -m "chore($PACKAGE): release $version" + git tag -a "$tag" -m "$PACKAGE $version" + git push --atomic origin HEAD:main "$tag" + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + + publish: + name: Publish + needs: tag + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAG: ${{ needs.tag.outputs.tag }} + VERSION: ${{ needs.tag.outputs.version }} + defaults: + run: + working-directory: ${{ inputs.package }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.tag.outputs.tag }} + fetch-depth: 0 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.ELIXIR_VERSION }} + otp-version: ${{ env.OTP_VERSION }} + + - name: Set up pnpm + if: inputs.package == 'durable_dashboard' + uses: pnpm/action-setup@v6 + with: + version: 9 + + - name: Set up Node + if: inputs.package == 'durable_dashboard' + uses: actions/setup-node@v6 + with: + node-version: "20" + cache: pnpm + cache-dependency-path: durable_dashboard/assets/pnpm-lock.yaml + + - name: Install JS dependencies + if: inputs.package == 'durable_dashboard' + working-directory: durable_dashboard/assets + run: pnpm install --frozen-lockfile + + # Against Hex deps, as users get it: catches a dashboard that needs an + # unreleased durable. + - name: Compile + run: | + mix deps.get + mix compile --warnings-as-errors + + - name: Publish to Hex + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} + run: | + if curl -sfo /dev/null "https://hex.pm/api/packages/$PACKAGE/releases/$VERSION"; then + echo "$PACKAGE $VERSION is already on Hex" + else + mix hex.publish --yes + fi + + - uses: taiki-e/install-action@v2 + with: + tool: git-cliff + + - name: GitHub release + working-directory: . + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "$TAG" > /dev/null 2>&1; then + echo "$TAG is already released"; exit 0 + fi + git cliff --include-path "$PACKAGE/**" --tag-pattern "^$PACKAGE-v" --current --strip all > "$RUNNER_TEMP/notes.md" + flags=() + if [[ $VERSION == *-* ]]; then flags+=(--prerelease); fi + gh release create "$TAG" --verify-tag --title "$PACKAGE $VERSION" --notes-file "$RUNNER_TEMP/notes.md" "${flags[@]}" diff --git a/README.md b/README.md index 04fc96f..cfa78e3 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,35 @@ commands work: `mix deps.get`, `mix compile`, `mix test`, `mix format`, ## Cross-package references -Packages link to each other via `path:` deps in dev. To publish, swap the -`path:` line for the Hex version: +`durable_dashboard` depends on `{:durable, "~> 1.0"}` from Hex, which is what +it ships with. To build it against the local checkout, set `DURABLE_PATH`: -```elixir -# durable_dashboard/mix.exs -{:durable, path: "../durable"} # dev -{:durable, "~> 0.1"} # release +```bash +cd durable_dashboard && DURABLE_PATH=../durable mix test ``` +The workspace commands and CI set it for you. + +## Releasing + +Each package is released on its own from GitHub Actions: **Actions → Release → +Run workflow**, then pick the package. Leave the version blank to take the +next one [git-cliff](https://git-cliff.org) derives from the package's +conventional commits since its last tag, or enter one. The workflow: + +1. bumps `@version`, regenerates the package's `CHANGELOG.md` (config in + [`cliff.toml`](cliff.toml)), commits both to `main`, and tags + `durable-v` or `durable_dashboard-v`; +2. publishes to Hex with the `HEX_API_KEY` repository secret; +3. creates the GitHub release, with that version's changelog as its notes. + +If publishing fails, "Re-run failed jobs" retries it for the same tag. Release +`durable` first when the dashboard needs its changes: the dashboard is +published against durable from Hex. + +Preview a changelog locally with +`git cliff --include-path "durable/**" --tag-pattern "^durable-v" --unreleased`. + ## Database A shared `docker-compose.yml` at the workspace root brings up the Postgres diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..c0e0be2 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,51 @@ +# git-cliff config (https://git-cliff.org/docs/configuration). +# +# Each package has its own CHANGELOG.md and tags (`durable-v1.2.3`, +# `durable_dashboard-v1.2.3`), so every run is scoped to one package: +# +# git cliff --include-path "durable/**" --tag-pattern "^durable-v" +# +# The release workflow (.github/workflows/release.yml) does this for you. + +[changelog] +header = """ +# Changelog + +All notable changes to this package. Versions follow [Semantic +Versioning](https://semver.org); entries come from [Conventional +Commits](https://www.conventionalcommits.org). + +""" +body = """ +{% set v = version | default(value="") | split(pat="-v") | last %}\ +{% if version and previous.version %}\ + ## [{{ v }}](https://github.com/wavezync/durable/compare/{{ previous.version }}...{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} +{% elif version %}\ + ## {{ v }} - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## Unreleased +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim }} + {% for commit in commits %} + - {% if commit.breaking %}**Breaking:** {% endif %}{{ commit.message | split(pat="\n") | first | upper_first | trim }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/wavezync/durable/commit/{{ commit.id }}))\ + {% endfor %} +{% endfor %}\n +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +protect_breaking_commits = true +sort_commits = "oldest" +commit_parsers = [ + { message = "^chore(\\(.+\\))?: (release|bump)", skip = true }, + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^docs", group = "Documentation" }, + # chore, ci, build, test, style: not user-facing. + { message = ".*", skip = true }, +] diff --git a/durable/CHANGELOG.md b/durable/CHANGELOG.md new file mode 100644 index 0000000..9c3cac2 --- /dev/null +++ b/durable/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this package. Versions follow [Semantic +Versioning](https://semver.org); entries come from [Conventional +Commits](https://www.conventionalcommits.org). + +## Unreleased + +### Features + +- Schema for operator controls, deadlines and history ([fd69e82](https://github.com/wavezync/durable/commit/fd69e82f93ae9e5c38965300f25cd0c6281512d0)) +- Telemetry events and metrics ([d3e7983](https://github.com/wavezync/durable/commit/d3e7983586ec8271ea037eb40c82ce6372157966)) +- Operator controls, workflow timeouts and run observability ([0164365](https://github.com/wavezync/durable/commit/0164365438d320aaf2a3e5abfcd7be25f4d7d176)) + +### Bug fixes + +- Stable ids for parallel and branch blocks ([333c152](https://github.com/wavezync/durable/commit/333c152301aa68608eee76e6403900fc64687c38)) +- Keep a run's first start time; dashboard fixes found by page tests ([7c4f55d](https://github.com/wavezync/durable/commit/7c4f55d963ad4316ab4c2e4f033174bcda13c5ca)) + +### Documentation + +- Workflow options, errors, telemetry and the operator API ([0cd4256](https://github.com/wavezync/durable/commit/0cd42560f0cee57acb6e5783d608abfe57251ca8)) + +## 0.1.0-rc - 2026-06-24 + +### Features + +- Collapse to LiveView-first with a single React island ([cfac43d](https://github.com/wavezync/durable/commit/cfac43d6ff4c1cf9a0a5d3900143ab792a9fa948)) +- Harden durable execution — sleep/wait, fencing, retries, child workflows ([6b56c35](https://github.com/wavezync/durable/commit/6b56c351073bc40c88d964e2ce01eb029e47d9c1)) +- Copy buttons, full-width step inspector, faceted Executions filters ([9341826](https://github.com/wavezync/durable/commit/9341826f912b2344dae330d7c8c3288aa0402773)) + +### Bug fixes + +- Resolve Elixir 1.20 gradual-type warnings and harden cold-build tests ([b1fbeb8](https://github.com/wavezync/durable/commit/b1fbeb8adff5d669ffc0eccdf3b2699b0187517d)) + +### Documentation + +- Refresh READMEs for the dashboard IA + new features ([c2be144](https://github.com/wavezync/durable/commit/c2be14465f272ddf367f826aa0fee7bcb14f439e)) + diff --git a/durable/README.md b/durable/README.md index 41d49a4..41b6e25 100644 --- a/durable/README.md +++ b/durable/README.md @@ -21,7 +21,7 @@ A durable, resumable workflow engine for Elixir. Similar to Temporal/Inngest. ```elixir def deps do - [{:durable, "~> 0.1.0-rc"}] + [{:durable, "~> 1.0"}] end ``` @@ -599,7 +599,7 @@ in-place — no `forward`, no extra endpoint. You get: - Settings view - ⌘K command palette — jump to any page, workflow, or recent run -See [`durable_dashboard/README.md`](durable_dashboard/) for the full +See the [durable_dashboard docs](https://hexdocs.pm/durable_dashboard) for the full option list and design notes. ## Guides diff --git a/durable/guides/ai_workflows.md b/durable/guides/ai_workflows.md index 0809581..ef98675 100644 --- a/durable/guides/ai_workflows.md +++ b/durable/guides/ai_workflows.md @@ -10,7 +10,7 @@ Build reliable AI agent workflows with automatic retries, state persistence, and # mix.exs defp deps do [ - {:durable, "~> 0.1.0-rc"}, + {:durable, "~> 1.0"}, {:req_llm, "~> 1.1"} ] end diff --git a/durable/mix.exs b/durable/mix.exs index da78a0a..87f7735 100644 --- a/durable/mix.exs +++ b/durable/mix.exs @@ -92,9 +92,10 @@ defmodule Durable.MixProject do [ main: "readme", source_url: @source_url, - source_ref: "v#{@version}", + source_ref: "durable-v#{@version}", extras: [ "README.md", + "CHANGELOG.md", "guides/ai_workflows.md", "guides/branching.md", "guides/compensations.md", @@ -121,7 +122,7 @@ defmodule Durable.MixProject do maintainers: @maintainers, licenses: @licenses, links: %{"GitHub" => @source_url, "Homepage" => @homepage_url}, - files: ~w(lib priv .formatter.exs mix.exs README.md LICENSE shared.exs) + files: ~w(lib priv .formatter.exs mix.exs README.md CHANGELOG.md LICENSE shared.exs) ] end end diff --git a/durable_dashboard/CHANGELOG.md b/durable_dashboard/CHANGELOG.md new file mode 100644 index 0000000..4bf4e7b --- /dev/null +++ b/durable_dashboard/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +All notable changes to this package. Versions follow [Semantic +Versioning](https://semver.org); entries come from [Conventional +Commits](https://www.conventionalcommits.org). + +## Unreleased + +### Features + +- V1 redesign — pages, design system, tooltips and query bar ([c05e87c](https://github.com/wavezync/durable/commit/c05e87c3ca40a800838800f7141b713e5d6e9292)) +- Workflow graph with joins and branch gateways ([2a65c4d](https://github.com/wavezync/durable/commit/2a65c4dba4f772b0863b60cadb30919ffdb72bf7)) + +### Bug fixes + +- Keep a run's first start time; dashboard fixes found by page tests ([7c4f55d](https://github.com/wavezync/durable/commit/7c4f55d963ad4316ab4c2e4f033174bcda13c5ca)) + +## 0.1.0-rc - 2026-06-24 + +### Features + +- Add durable_dashboard package ([d170094](https://github.com/wavezync/durable/commit/d170094e28b99f4b1b7ccf4dc66f5a83afef2668)) +- Collapse to LiveView-first with a single React island ([cfac43d](https://github.com/wavezync/durable/commit/cfac43d6ff4c1cf9a0a5d3900143ab792a9fa948)) +- Add setup/assets aliases and bundle assets on hex publish ([b0c8aa9](https://github.com/wavezync/durable/commit/b0c8aa99f9256bd75d617693050c2b90e5849af5)) +- Redesign shell around the instrument-panel direction ([54f6a09](https://github.com/wavezync/durable/commit/54f6a0918bd74a0efc0e07fe0c135f7148a0f7d2)) +- Shared UI primitives & display components ([c433ebb](https://github.com/wavezync/durable/commit/c433ebb32d83adcdcf74bc79b945f92a73307424)) +- Workflow flow-graph island ([d6f2f9a](https://github.com/wavezync/durable/commit/d6f2f9a16b55a69559a50feb0ff63e23428bfd58)) +- Workflow detail tabs ([7ea5c1c](https://github.com/wavezync/durable/commit/7ea5c1c2a58939aa1f277a24b50c6d5fe144318a)) +- Overview, executions, workflows & settings pages ([58fba94](https://github.com/wavezync/durable/commit/58fba946e8e109e1ce74be6c899a8472c3a99025)) +- Pulse brand mark, live ⌘K palette, topbar toolbar, wider inspector ([a1b2590](https://github.com/wavezync/durable/commit/a1b25909d18711fee16f97e7562dbde4980ef001)) +- Copy buttons, full-width step inspector, faceted Executions filters ([9341826](https://github.com/wavezync/durable/commit/9341826f912b2344dae330d7c8c3288aa0402773)) +- Workspace and dashboard precommit tooling ([daa10c9](https://github.com/wavezync/durable/commit/daa10c98679b7c04e3aefba36eb0796feaa7d1b0)) + +### Bug fixes + +- Unbreak overview links and reorder Recent workflows ([3ae6c8f](https://github.com/wavezync/durable/commit/3ae6c8f759054c02423df482a57ffcaf88e7ca40)) +- Resolve Elixir 1.20 gradual-type warnings and harden cold-build tests ([b1fbeb8](https://github.com/wavezync/durable/commit/b1fbeb8adff5d669ffc0eccdf3b2699b0187517d)) + +### Documentation + +- Refresh READMEs for the dashboard IA + new features ([c2be144](https://github.com/wavezync/durable/commit/c2be14465f272ddf367f826aa0fee7bcb14f439e)) + diff --git a/durable_dashboard/CLAUDE.md b/durable_dashboard/CLAUDE.md index 011ccdc..bd5a519 100644 --- a/durable_dashboard/CLAUDE.md +++ b/durable_dashboard/CLAUDE.md @@ -30,7 +30,8 @@ cd assets && pnpm install && pnpm build # Type / lint / format checks cd assets && pnpm exec tsc --noEmit && pnpm exec biome check src/ -# Elixir +# Elixir (durable comes from Hex unless DURABLE_PATH points at a checkout) +export DURABLE_PATH=../durable mix compile --warnings-as-errors mix test ``` diff --git a/durable_dashboard/README.md b/durable_dashboard/README.md index 893f8b2..807365c 100644 --- a/durable_dashboard/README.md +++ b/durable_dashboard/README.md @@ -31,8 +31,8 @@ Add `durable_dashboard` alongside `durable`: ```elixir def deps do [ - {:durable, "~> 0.1.0-rc"}, - {:durable_dashboard, "~> 0.1.0-rc"} + {:durable, "~> 1.0"}, + {:durable_dashboard, "~> 1.0"} ] end ``` diff --git a/durable_dashboard/mix.exs b/durable_dashboard/mix.exs index b6597f6..ebf8599 100644 --- a/durable_dashboard/mix.exs +++ b/durable_dashboard/mix.exs @@ -59,13 +59,7 @@ defmodule DurableDashboard.MixProject do defp deps do [ - # The Hex version is the committed default, so a plain `mix deps.get` and - # `mix hex.publish` resolve durable from Hex with no special steps. For - # monorepo co-development, comment it and uncomment the path dep below. - # Bump the requirement to the durable release this dashboard targets. - # (Same toggle pattern as elixir-nx/nx's torchx.) - # {:durable, "~> 1.0"}, - {:durable, path: "../durable"}, + durable_dep(), {:phoenix_live_view, "~> 1.1"}, {:phoenix, "~> 1.8"}, {:jason, "~> 1.4"}, @@ -75,6 +69,17 @@ defmodule DurableDashboard.MixProject do ] end + # durable comes from Hex, as it ships, so `mix hex.publish` needs nothing + # special. DURABLE_PATH=../durable builds against a local checkout instead; + # CI and the workspace root set it, so the dashboard is always tested with + # the durable beside it. + defp durable_dep do + case System.get_env("DURABLE_PATH") do + nil -> {:durable, "~> 1.0"} + path -> {:durable, path: path} + end + end + defp aliases do [ setup: ["deps.get", "assets.setup"], @@ -108,8 +113,8 @@ defmodule DurableDashboard.MixProject do [ main: "readme", source_url: @source_url, - source_ref: "v#{@version}", - extras: ["README.md"] + source_ref: "durable_dashboard-v#{@version}", + extras: ["README.md", "CHANGELOG.md"] ] end @@ -118,7 +123,7 @@ defmodule DurableDashboard.MixProject do maintainers: @maintainers, licenses: @licenses, links: %{"GitHub" => @source_url, "Homepage" => @homepage_url}, - files: ~w(lib priv .formatter.exs mix.exs README.md LICENSE shared.exs) + files: ~w(lib priv .formatter.exs mix.exs README.md CHANGELOG.md LICENSE shared.exs) ] end end diff --git a/mix.exs b/mix.exs index c055746..10e2052 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,8 @@ defmodule DurableWorkspace.MixProject do elixir: "~> 1.15", deps: [ # durable_dashboard depends on durable via Hex (its committed default), - # so override to resolve the whole workspace against the local checkout. + # so override to resolve the whole workspace against the local checkout + # (and set DURABLE_PATH for the per-package commands below). {:durable, path: "durable", override: true}, {:durable_dashboard, path: "durable_dashboard"} ], @@ -41,7 +42,8 @@ defmodule DurableWorkspace.MixProject do {_, code} = System.cmd("elixir", base ++ args, into: IO.binstream(:stdio, :line), - cd: app + cd: app, + env: [{"DURABLE_PATH", "../durable"}] ) if code > 0, do: System.at_exit(fn _ -> exit({:shutdown, 1}) end)