diff --git a/README.md b/README.md index b1ec53b..d63688f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,18 @@ The build emits `dist/`. A packed npm artifact can be made locally with: It validates the requested version against `package.json`, then writes an npm tarball and `SHA256SUMS` to `release/`. +## Publish a GitHub Release + +```bash +./scripts/publish-github-release.sh v0.1.0 +``` + +This runs the same local packaging command, then requires a clean, up-to-date +`main` and an authenticated [`gh`](https://cli.github.com/) CLI. It creates and +pushes the annotated version tag when needed and uploads the npm tarball and +`SHA256SUMS` to GitHub Releases. The requested version must match +`package.json`. + ## Contracts relevant to the alpha - Workspaces have durable UUID identities; paths are addresses, not identity. diff --git a/scripts/publish-github-release.sh b/scripts/publish-github-release.sh new file mode 100755 index 0000000..d882eef --- /dev/null +++ b/scripts/publish-github-release.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +VERSION="${1:-}" +REPOSITORY="mirivlad/verstak-sdk" +RELEASE_SCRIPT="${VERSTAK_RELEASE_SCRIPT:-$ROOT/scripts/release.sh}" +RELEASE_DIR="${VERSTAK_RELEASE_DIR:-$ROOT/release}" +GIT_BIN="${GIT_BIN:-git}" +GH_BIN="${GH_BIN:-gh}" + +cd "$ROOT" + +if [[ -z "$VERSION" || ! "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "usage: $0 " >&2 + echo "example: $0 v0.1.0" >&2 + exit 2 +fi +if ! command -v "$GH_BIN" >/dev/null; then + echo "gh CLI is required to publish a GitHub Release" >&2 + exit 1 +fi +if [[ "$("$GIT_BIN" branch --show-current)" != "main" ]]; then + echo "GitHub releases must be published from main" >&2 + exit 1 +fi +if [[ -n "$("$GIT_BIN" status --porcelain)" ]]; then + echo "working tree must be clean before publishing a release" >&2 + exit 1 +fi + +"$GH_BIN" auth status +"$GIT_BIN" fetch origin main --tags +HEAD="$("$GIT_BIN" rev-parse HEAD)" +if [[ "$HEAD" != "$("$GIT_BIN" rev-parse origin/main)" ]]; then + echo "local main must match origin/main before publishing a release" >&2 + exit 1 +fi + +"$RELEASE_SCRIPT" "$VERSION" + +shopt -s nullglob +ASSETS=("$RELEASE_DIR"/*.tar.gz "$RELEASE_DIR"/*.tgz) +if [[ -f "$RELEASE_DIR/SHA256SUMS" ]]; then + ASSETS+=("$RELEASE_DIR/SHA256SUMS") +fi +if [[ ${#ASSETS[@]} -eq 0 ]]; then + echo "no release assets found in $RELEASE_DIR" >&2 + exit 1 +fi + +if "$GIT_BIN" rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then + if [[ "$("$GIT_BIN" rev-parse "${VERSION}^{commit}")" != "$HEAD" ]]; then + echo "existing tag $VERSION does not point at HEAD" >&2 + exit 1 + fi +else + "$GIT_BIN" tag -a "$VERSION" -m "Release $VERSION" + "$GIT_BIN" push origin "refs/tags/$VERSION" +fi + +if "$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" >/dev/null 2>&1; then + "$GH_BIN" release upload "$VERSION" "${ASSETS[@]}" --repo "$REPOSITORY" --clobber +else + "$GH_BIN" release create "$VERSION" "${ASSETS[@]}" \ + --repo "$REPOSITORY" \ + --title "Verstak Plugin SDK $VERSION" \ + --generate-notes \ + --latest \ + --verify-tag +fi + +echo "GitHub release:" +"$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" --json url --jq .url diff --git a/scripts/test-publish-github-release.sh b/scripts/test-publish-github-release.sh new file mode 100755 index 0000000..7658f5e --- /dev/null +++ b/scripts/test-publish-github-release.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PUBLISHER="$ROOT/scripts/publish-github-release.sh" +VERSION="v0.0.0-test" +REPOSITORY="mirivlad/verstak-sdk" +ASSET_NAME="verstak-plugin-sdk-0.0.0-test.tgz" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +if [[ ! -x "$PUBLISHER" ]]; then + echo "publisher is missing or not executable: $PUBLISHER" >&2 + exit 1 +fi + +mkdir -p "$WORK/bin" "$WORK/release" "$WORK/state" +LOG="$WORK/log" +export LOG + +cat > "$WORK/release.sh" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +printf 'release:%s\n' "$1" >> "$LOG" +printf 'package\n' > "$VERSTAK_RELEASE_DIR/verstak-plugin-sdk-${1#v}.tgz" +printf 'checksum\n' > "$VERSTAK_RELEASE_DIR/SHA256SUMS" +SCRIPT +chmod +x "$WORK/release.sh" + +cat > "$WORK/bin/git" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +if [[ "$PWD" != "${EXPECTED_ROOT:?}" ]]; then + echo "publisher did not enter repository root: $PWD" >&2 + exit 1 +fi +case "${1:-}" in + status) exit 0 ;; + branch) echo main ;; + fetch) printf 'fetch\n' >> "$LOG" ;; + rev-parse) + if [[ "${2:-}" == "-q" ]]; then + if [[ -f "$TEST_STATE/tag" ]]; then echo test-commit; exit 0; fi + exit 1 + fi + echo test-commit + ;; + tag) touch "$TEST_STATE/tag"; printf 'tag:%s\n' "${3:-}" >> "$LOG" ;; + push) printf 'push:%s:%s\n' "${2:-}" "${3:-}" >> "$LOG" ;; + *) echo "unexpected git invocation: $*" >&2; exit 1 ;; +esac +SCRIPT +chmod +x "$WORK/bin/git" + +cat > "$WORK/bin/gh" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +case "${1:-}:${2:-}" in + auth:status) printf 'auth\n' >> "$LOG" ;; + release:view) + if [[ -f "$TEST_STATE/release" ]]; then echo https://github.example/release; else exit 1; fi + ;; + release:create) touch "$TEST_STATE/release"; printf 'create:%s\n' "$*" >> "$LOG" ;; + release:upload) printf 'upload:%s\n' "$*" >> "$LOG" ;; + *) echo "unexpected gh invocation: $*" >&2; exit 1 ;; +esac +SCRIPT +chmod +x "$WORK/bin/gh" + +run_publisher() { + VERSTAK_RELEASE_SCRIPT="$WORK/release.sh" \ + VERSTAK_RELEASE_DIR="$WORK/release" \ + GIT_BIN="$WORK/bin/git" \ + GH_BIN="$WORK/bin/gh" \ + EXPECTED_ROOT="$ROOT" \ + TEST_STATE="$WORK/state" \ + "$PUBLISHER" "$VERSION" +} + +run_publisher +grep -Fqx "release:$VERSION" "$LOG" +grep -Fqx "tag:$VERSION" "$LOG" +grep -Fqx "push:origin:refs/tags/$VERSION" "$LOG" +grep -F "release create $VERSION" "$LOG" >/dev/null +grep -F "$ASSET_NAME" "$LOG" >/dev/null +grep -F "SHA256SUMS" "$LOG" >/dev/null + +run_publisher +grep -F "release upload $VERSION" "$LOG" >/dev/null + +echo "GitHub release publisher test passed"