diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7860a35..2017bb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,19 +12,14 @@ jobs: steps: - name: 'Check out code' uses: actions/checkout@v4 - - name: 'Make release' - id: 'make-release' + # Drafting logic lives in scripts/deploy.sh so CI and manual releases stay + # in sync (single source of truth). See CONTRIBUTING.md. + - name: 'Draft the release' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release create "${VERSION}" \ - --repo "${GITHUB_REPOSITORY}" \ - --draft \ - --title "${VERSION}" \ - --target ${{ github.sha }} - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + OWNER_REPO: '${{ github.repository }}' + REV: '${{ github.sha }}' + run: ./scripts/deploy.sh release-draft nix-cache: name: 'Populate Nix Caches' @@ -55,13 +50,8 @@ jobs: if: failure() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release delete "${VERSION}" \ - --repo "${GITHUB_REPOSITORY}" \ - --yes \ - --cleanup-tag || true + OWNER_REPO: '${{ github.repository }}' + run: ./scripts/deploy.sh release-abort dockerhub: name: 'Build and Publish Docker Image' @@ -85,13 +75,8 @@ jobs: if: failure() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release delete "${VERSION}" \ - --repo "${GITHUB_REPOSITORY}" \ - --yes \ - --cleanup-tag || true + OWNER_REPO: '${{ github.repository }}' + run: ./scripts/deploy.sh release-abort cut-release: name: 'Cut Release' @@ -104,12 +89,10 @@ jobs: ref: ${{ github.sha }} fetch-depth: 0 + # Finalizing logic lives in scripts/deploy.sh so CI and manual releases + # stay in sync (single source of truth). See CONTRIBUTING.md. - name: 'Finalize Release' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release edit "${VERSION}" \ - --repo "${GITHUB_REPOSITORY}" \ - --draft=false + OWNER_REPO: '${{ github.repository }}' + run: ./scripts/deploy.sh release-cut diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5976a91..1637cd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,12 +79,14 @@ where `` comes from `package/version`. ### Prerequisites -Both subcommands run from a checkout of the revision you are releasing, with the +The subcommands run from a checkout of the revision you are releasing, with the following tools on `PATH`: - `nix-cache` needs `nix` and `cachix`. It fetches `kup` from `github:runtimeverification/kup` itself, so you do not install kup separately. - `docker` needs `docker`. +- `release-draft`, `release-cut`, and `release-abort` need `gh`, authenticated + via `gh auth login` or a `GH_TOKEN`/`GITHUB_TOKEN` in the environment. Each subcommand checks its tools up front and exits with a clear message if one is missing, before starting the build. @@ -102,14 +104,16 @@ further setup. | ----------- | ------------------------------ | | `nix-cache` | `CACHIX_PUBLIC_TOKEN`, `CACHIX_PRIVATE_KFB_TOKEN` | | `docker` | `DOCKERHUB_PASSWORD` | +| `release-*` | none — `gh` supplies its own authentication | `OWNER_REPO` and `REV` default to the current checkout's `origin` remote and `HEAD`. Override them if you are publishing a revision other than the one checked out. `DOCKERHUB_USERNAME`, `DOCKERHUB_NAMESPACE`, and `DOCKERHUB_REPO` default to the release account and repository; override them only to publish elsewhere. -Both subcommands refuse to run if the working tree is not clean. CI builds from a -fresh checkout, so a manual build must too: uncommitted changes and untracked +The `nix-cache` and `docker` subcommands refuse to run if the working tree is not +clean. CI builds from a fresh checkout, so a manual build must too: uncommitted +changes and untracked files would otherwise be baked into the published image or flake build without matching the `REV` they are published under. Commit, stash, or clean the tree first. `git status --porcelain` ignores `.gitignore`d paths, so runtime artifacts @@ -118,17 +122,18 @@ deliberately want to publish an uncommitted state. ### Manual release while CI is down -The workflow also drafts, cleans up, and finalizes the GitHub release around the -deployment jobs. When you deploy by hand, run those `gh` steps yourself in this -order. `nix-cache` runs once per architecture (the workflow uses an `x86_64` and -an `ARM64` runner), so run it on a machine of each architecture that should be +`deploy.sh` wraps the GitHub-release steps too, so a manual release runs the same +subcommands the workflow does, in the same order. Run them from a checkout of the +`main` commit you are releasing: `release-draft` targets `REV` (the current +`HEAD` by default), and kup resolves komet-node versions against `main`, so a +release built from an unmerged commit will not be installable with `kup`. +`nix-cache` runs once per architecture (the workflow uses an `x86_64` and an +`ARM64` runner), so run it on a machine of each architecture that should be cached. ```sh -VERSION=v$(cat package/version) - # 1. Draft the release. -gh release create "$VERSION" --draft --title "$VERSION" --target "$(git rev-parse HEAD)" +./scripts/deploy.sh release-draft # 2. Deploy. Export the secrets first (see the table above). # Run nix-cache once per architecture; docker once. @@ -136,8 +141,8 @@ gh release create "$VERSION" --draft --title "$VERSION" --target "$(git rev-pars ./scripts/deploy.sh docker # 3. Finalize the release once every deployment has succeeded. -gh release edit "$VERSION" --draft=false +./scripts/deploy.sh release-cut # If a deployment fails, remove the draft instead: -# gh release delete "$VERSION" --yes --cleanup-tag +# ./scripts/deploy.sh release-abort ``` diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 2e3bcd5..27cab2f 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -13,13 +13,16 @@ set -euo pipefail # unavailable. See CONTRIBUTING.md ("Deploying a release") for the runbook. # # Subcommands: -# nix-cache Build komet-node and push it to both Nix caches (the -# k-framework closure + the k-framework-binary kup binary). -# docker Build the Docker image, smoke-test it, and push to Docker Hub. -# all Run nix-cache then docker. +# nix-cache Build komet-node and push it to both Nix caches (the +# k-framework closure + the k-framework-binary kup binary). +# docker Build the Docker image, smoke-test it, and push to Docker Hub. +# all Run nix-cache then docker. +# release-draft Create the draft GitHub release for this version and revision. +# release-cut Publish (un-draft) the release once its artifacts are up. +# release-abort Delete the draft release and its tag (failure cleanup). # # Usage: -# scripts/deploy.sh +# scripts/deploy.sh REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${REPO_ROOT}" @@ -156,15 +159,51 @@ deploy_docker() { docker image push "${tag}" } +# --- release lifecycle ----------------------------------------------------- +# The GitHub release is drafted before the artifacts are published and finalized +# after, so it only becomes visible once its Docker image and cache pins exist. +# These wrap the `gh` calls the Release workflow makes, keeping the release +# lifecycle in the same single source of truth as the deploy steps. `gh` +# authenticates via its own login or GH_TOKEN/GITHUB_TOKEN; no secret is read +# here directly. The release is drafted against REV, which must be the commit you +# deployed -- and, for a real release, a commit on `main`, since kup resolves +# komet-node versions against `main`. +release_tag() { echo "v${KOMET_NODE_VERSION}"; } + +deploy_release_draft() { + require_cmd gh git + local tag; tag="$(release_tag)" + echo ":: drafting release ${tag} targeting ${REV}" + gh release create "${tag}" --repo "${OWNER_REPO}" --draft --title "${tag}" --target "${REV}" +} + +deploy_release_cut() { + require_cmd gh git + local tag; tag="$(release_tag)" + echo ":: publishing release ${tag}" + gh release edit "${tag}" --repo "${OWNER_REPO}" --draft=false +} + +deploy_release_abort() { + require_cmd gh git + local tag; tag="$(release_tag)" + echo ":: deleting drafted release ${tag}" + # Best-effort: the cleanup path must never fail on its own account. + gh release delete "${tag}" --repo "${OWNER_REPO}" --yes --cleanup-tag || true +} + usage() { - sed -n '8,22p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + sed -n '8,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' } main() { case "${1:-}" in - nix-cache) deploy_nix_cache ;; - docker) deploy_docker ;; - all) deploy_nix_cache; deploy_docker ;; + nix-cache) deploy_nix_cache ;; + docker) deploy_docker ;; + all) deploy_nix_cache; deploy_docker ;; + release-draft) deploy_release_draft ;; + release-cut) deploy_release_cut ;; + release-abort) deploy_release_abort ;; -h | --help | help | "") usage [ -n "${1:-}" ] # exit non-zero when no subcommand was given