diff --git a/.docket/ledger.jsonl b/.docket/ledger.jsonl index a89ec41..483301f 100644 --- a/.docket/ledger.jsonl +++ b/.docket/ledger.jsonl @@ -130,3 +130,4 @@ {"schema":2,"kind":"decision","id":"d130","text":"A correction appended with its id already set skips the write-time refusals.","state":"adopted","ts":"2026-09-23T09:15:42+00:00","author":"claude-code","session":"","branch":"feat/ledger-corrections","scope":["docket/ledger.py","docket/corrections.py","docket/rebase.py"],"rationale":"A correction valid on its own branch can be refused against the other branch's corrected state, which leaves a rebase half-applied.","supports":[["d129"]],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"A hand-written numbered correction line bypasses the refusals; validation still bounds its fields.","pinned":false,"choice":"append runs the no-op drop and the echo and question-text refusals only for a correction the CLI submits unnumbered. A pre-numbered correction, as rebase appends, passes on validation alone, the same as a pre-numbered record.","alternatives":["Run the refusals on every appended correction, including rebased ones."],"decided_by":""} {"schema":2,"kind":"decision","id":"d131","text":"Ledger filtering uses one query language parsed in Python, and the viewer calls the CLI to apply it.","state":"adopted","ts":"2026-09-23T10:15:32+00:00","author":"claude-code","session":"","branch":"main","scope":["graph/**","docket/cli/graph.py","docket/cli/query.py","docket/where.py"],"rationale":"One parser keeps the CLI and viewer in agreement and keeps filtering in Python, where the viewer decision put it. A submitted filter costs one Python start, about 150 ms.","supports":[["d36"]],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"Live filtering on field terms would need a Go parser later, and the viewer's layout change moves every key hint users know.","pinned":false,"choice":"docket/where.py parses field:value terms (kind, state, scope, is, author, branch, after, before) plus bare text. list and graph take --where. The viewer sends a submitted query to a hidden docket _filter-ids command and keeps the returned ids; bare words still filter live in Go. The viewer gains a three-line bottom pane for filter, status, and key hints, with a ? help overlay.","alternatives":["Parse the query in both Python and Go, kept in step by a shared fixture table.","Python pre-computes matchable fields and Go runs a reduced matcher.","Toggle keys or a filter form in the viewer instead of a query prompt."],"decided_by":""} {"schema":2,"kind":"decision","id":"d132","text":"The graph viewer receives its filter callback in the DOCKET_GRAPH_FILTER_CMD environment variable.","state":"adopted","ts":"2026-09-23T14:02:55+00:00","author":"claude-code","session":"","branch":"feat/ledger-filtering","scope":["graph/main.go","docket/cli/graph.py"],"rationale":"The viewer binary ships separately from the CLI. An older binary exits 2 on an unknown flag and ignores an unknown variable, so the variable keeps docket graph working across an update skew.","supports":[["d131"]],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"The callback is invisible in the viewer's --help; anyone running the binary by hand must know the variable.","pinned":false,"choice":"docket graph sets DOCKET_GRAPH_FILTER_CMD to a JSON argv ([python, bin/docket, _filter-ids, --]) for the viewer process; the viewer appends the query as one argument.","alternatives":["Pass the argv as a --filter-cmd flag, as the spec first named it."],"decided_by":""} +{"schema":2,"kind":"decision","id":"d133","text":"A pushed v* tag publishes the release through CI; the local recipe stops at the tag push.","state":"adopted","ts":"2026-09-23T14:57:17+00:00","author":"claude-code","session":"","branch":"ci/tag-release","scope":[".github/workflows/installer.yml","justfile"],"rationale":"Two uploaders built the same assets; the CI upload failed with 'already exists' from v0.19.0 on. One uploader on a clean runner gives reproducible assets and drops the local gh requirement.","supports":[["d120"]],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"docket update offers a version only after the workflow finishes, a few minutes after the push; a failed workflow leaves a pushed tag with no release until it is rerun.","pinned":false,"choice":"just release bumps, tests, commits, tags, and pushes. .github/workflows/installer.yml runs on v* tag pushes: it checks the tag against VERSION, runs the tests, builds the installer and graph binaries, and creates the GitHub release with roll_changelog notes, or replaces the assets of an existing one on a rerun.","alternatives":["Keep building and creating the release locally with gh, and have CI rebuild and re-upload on release published."],"decided_by":""} diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index ed21591..d468a93 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -1,8 +1,8 @@ -name: Installer release binaries +name: Release on: - release: - types: [published] + push: + tags: ["v*"] workflow_dispatch: inputs: version: @@ -24,6 +24,11 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Check the tag against VERSION + if: github.event_name == 'push' + run: | + test "v$(cat VERSION)" = "$GITHUB_REF_NAME" \ + || { echo "tag $GITHUB_REF_NAME does not match VERSION $(cat VERSION)"; exit 1; } - name: Test native installer working-directory: installer run: go test ./... @@ -38,11 +43,11 @@ jobs: run: python3 -m unittest installer/test_bootstrap.py - name: Build release assets env: - RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + RELEASE_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} run: python3 installer/release.py --output-dir dist/installer --version "$RELEASE_VERSION" - name: Build graph viewer release assets env: - RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + RELEASE_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} run: python3 graph/release.py --output-dir dist/graph --version "$RELEASE_VERSION" - uses: actions/upload-artifact@v4 with: @@ -50,9 +55,18 @@ jobs: path: | dist/installer/* dist/graph/* - - name: Upload assets to the published release - if: github.event_name == 'release' + # docket update reads releases/latest, so the release exists only once + # every asset is attached: a rerun after a partial failure replaces them. + - name: Publish the GitHub release + if: github.event_name == 'push' env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$RELEASE_TAG" dist/installer/* dist/graph/* --clobber + run: | + version="${GITHUB_REF_NAME#v}" + python3 scripts/roll_changelog.py "$version" --notes > notes.md + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" dist/installer/* dist/graph/* --clobber + else + gh release create "$GITHUB_REF_NAME" --title "docket $version" --notes-file notes.md \ + --latest --verify-tag dist/installer/* dist/graph/* + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f73cc9..d53e901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- A pushed `v*` tag now publishes the release. The Release workflow tests the tagged tree, builds the installer and graph viewer binaries, and creates the GitHub release with its changelog notes. `just release` stops after it pushes the tag, and no longer needs `gh`. + ## [0.20.0] - 2026-09-23 ### Added diff --git a/justfile b/justfile index 733486b..34aaef1 100644 --- a/justfile +++ b/justfile @@ -150,7 +150,7 @@ where: fi ./bin/docket where -# bump, roll the changelog, test, commit, tag, push and publish the GitHub release +# bump, roll the changelog, test, commit, tag, and push; CI publishes the release [group('release')] release new: #!/usr/bin/env bash @@ -184,30 +184,23 @@ release new: git tag -a "v{{new}}" -m "docket {{new}}" just publish "{{new}}" -# push a tag from `just release`, build its assets, create the GitHub release +# push a tag from `just release`; the Release workflow builds and publishes it [group('release')] publish new: #!/usr/bin/env bash set -euo pipefail - # Separate from release so a failed publish is one command to retry, and so - # a tag cut on a machine without gh can be published from another. docket - # update reads releases/latest, so a tag alone reaches nobody: v0.16.0 was - # tagged and pushed while every installed copy was still offered 0.15.0. - command -v gh >/dev/null || { echo "gh is not installed; cannot publish v{{new}}"; exit 1; } + # Separate from release so a failed push is one command to retry. The tag + # push triggers .github/workflows/installer.yml, which tests, builds every + # asset on a clean runner, and creates the GitHub release. docket update + # reads releases/latest, so installed copies see the version only once + # that workflow finishes. git rev-parse -q --verify "refs/tags/v{{new}}" >/dev/null \ || { echo "no tag v{{new}}; run just release {{new}} first"; exit 1; } test "$(cat VERSION)" = "{{new}}" \ || { echo "VERSION says $(cat VERSION), not {{new}}"; exit 1; } git push origin HEAD --follow-tags - # Built here, not reused from a previous run: an asset must come from the - # tree the tag names, and dist/ survives a checkout. - rm -rf dist - python3 installer/release.py - python3 graph/release.py - notes="$(mktemp)" - trap 'rm -f "$notes"' EXIT - python3 scripts/roll_changelog.py "{{new}}" --notes > "$notes" - gh release create "v{{new}}" --title "docket {{new}}" --notes-file "$notes" --latest \ - dist/installer/* dist/graph/* - rm -rf dist + echo "pushed v{{new}}; the Release workflow publishes it" + if command -v gh >/dev/null; then + echo "watch it with: gh run watch \$(gh run list --workflow installer.yml --limit 1 --json databaseId --jq '.[0].databaseId')" + fi echo "published v{{new}}; docket update now offers it"