👌 IMPROVE: automate package releases

Keep GitHub releases and npm publishing aligned with package.json while making deploy.sh the single source of truth for release validation.
This commit is contained in:
kumarabhirup 2026-03-19 17:41:52 -07:00
parent 161d543229
commit 687b04a963
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
6 changed files with 253 additions and 18 deletions

94
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,94 @@
name: Release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.23.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Resolve release version
id: version
run: |
echo "value=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
- name: Check npm publish status
id: npm
run: |
VERSION="${{ steps.version.outputs.value }}"
if npm view "denchclaw@${VERSION}" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "denchclaw@${VERSION} is already published"
else
echo "published=false" >> "$GITHUB_OUTPUT"
echo "denchclaw@${VERSION} is not published yet"
fi
- name: Check GitHub release status
id: github_release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="v${{ steps.version.outputs.value }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "${TAG} release already exists"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "${TAG} release does not exist yet"
fi
- name: Validate release checks
if: steps.npm.outputs.published != 'true'
env:
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
run: pnpm run deploy:check
- name: Publish packages to npm
if: steps.npm.outputs.published != 'true'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
run: pnpm run deploy -- --skip-tests --skip-build
- name: Create GitHub release
if: steps.github_release.outputs.exists != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="v${{ steps.version.outputs.value }}"
gh release create "$TAG" \
--target "$GITHUB_SHA" \
--title "$TAG" \
--generate-notes

51
RELEASING.md Normal file
View File

@ -0,0 +1,51 @@
# Releasing
`package.json` is the release source of truth for `denchclaw`.
## Main flow
1. Bump the root package version in `package.json`.
2. Push or merge that commit to `main`.
3. GitHub Actions runs `.github/workflows/release.yml`.
4. If that version is not already on npm, the workflow runs the same `deploy.sh` test and build checks in validation mode.
5. Only after those checks pass does the workflow publish `denchclaw` to npm.
6. The workflow creates a matching GitHub release named `v<version>`.
If the npm package already exists, the workflow skips publishing. If the GitHub release already exists, the workflow skips creating it. This makes reruns safe.
## Local commands
- `pnpm run deploy`
- `pnpm run deploy:check`
- `pnpm run deploy:patch`
- `pnpm run deploy:minor`
- `pnpm run deploy:major`
- `pnpm run github:sync-secrets`
The deploy commands load `.env` automatically when it exists.
Examples:
```bash
pnpm run deploy:check
pnpm run deploy
pnpm run deploy -- --dry-run --version 2.3.15
pnpm run deploy:patch
```
## GitHub Actions secrets
The release workflow expects:
- `POSTHOG_KEY`
- `NPM_TOKEN`
To sync the current local `.env` values into GitHub repository secrets:
```bash
pnpm run github:sync-secrets
```
## Better long-term option
The workflow supports `NPM_TOKEN` today because that matches the current local deploy process. For better security, configure npm trusted publishing for `.github/workflows/release.yml` and then remove `NPM_TOKEN`. The deploy script already supports GitHub Actions OIDC when no `NPM_TOKEN` is present.

View File

@ -53,10 +53,12 @@
"web:dev": "pnpm --dir apps/web dev",
"web:install": "pnpm --dir apps/web install",
"web:prepack": "cp -r apps/web/public apps/web/.next/standalone/apps/web/public && cp -r apps/web/.next/static apps/web/.next/standalone/apps/web/.next/static && node scripts/flatten-standalone-deps.mjs",
"deploy": "set -a && source .env && set +a && bash scripts/deploy.sh",
"deploy:major": "set -a && source .env && set +a && bash scripts/deploy.sh --bump major",
"deploy:minor": "set -a && source .env && set +a && bash scripts/deploy.sh --bump minor",
"deploy:patch": "set -a && source .env && set +a && bash scripts/deploy.sh --bump patch"
"deploy:check": "bash scripts/with-root-env.sh bash scripts/deploy.sh --skip-publish",
"deploy": "bash scripts/with-root-env.sh bash scripts/deploy.sh",
"deploy:major": "bash scripts/with-root-env.sh bash scripts/deploy.sh --bump major",
"deploy:minor": "bash scripts/with-root-env.sh bash scripts/deploy.sh --bump minor",
"deploy:patch": "bash scripts/with-root-env.sh bash scripts/deploy.sh --bump patch",
"github:sync-secrets": "bash scripts/with-root-env.sh bash scripts/sync-github-secrets.sh"
},
"dependencies": {
"@clack/prompts": "^1.0.1",

View File

@ -10,10 +10,14 @@
#
# Flags:
# --skip-tests Skip running tests before build/publish.
# --skip-publish Run all validation/build checks but do not publish.
# --skip-npx-smoke Skip post-publish npx binary verification.
#
# Environment:
# NPM_TOKEN Required. npm auth token for publishing.
# NPM_TOKEN Optional. npm auth token for publishing.
# Required only when actually publishing outside GitHub Actions.
# If omitted in GitHub Actions, npm trusted publishing via OIDC
# can be used instead.
set -euo pipefail
@ -29,6 +33,14 @@ cd "$ROOT_DIR"
die() { echo "error: $*" >&2; exit 1; }
run_npm() {
if [[ ${#NPM_FLAGS[@]} -gt 0 ]]; then
npm "$@" "${NPM_FLAGS[@]}"
else
npm "$@"
fi
}
current_version() {
node -p "require('./package.json').version"
}
@ -126,6 +138,7 @@ EXPLICIT_VERSION=""
DRY_RUN=false
SKIP_BUILD=false
SKIP_TESTS=false
SKIP_PUBLISH=false
SKIP_NPX_SMOKE=false
set_mode() {
@ -138,6 +151,9 @@ set_mode() {
while [[ $# -gt 0 ]]; do
case $1 in
--)
shift
;;
--version)
set_mode "version"
EXPLICIT_VERSION="${2:?--version requires a semver argument (x.y.z)}"
@ -163,6 +179,10 @@ while [[ $# -gt 0 ]]; do
SKIP_TESTS=true
shift
;;
--skip-publish)
SKIP_PUBLISH=true
shift
;;
--skip-npx-smoke)
SKIP_NPX_SMOKE=true
shift
@ -179,16 +199,22 @@ done
# ── auth ─────────────────────────────────────────────────────────────────────
if [[ -z "${NPM_TOKEN:-}" ]]; then
die "NPM_TOKEN environment variable is required"
fi
NPM_FLAGS=()
# Write a temporary .npmrc for auth (npm_config_ env vars can't encode
# registry-scoped keys because they contain slashes and colons).
NPMRC_TEMP="${ROOT_DIR}/.npmrc.deploy"
trap 'rm -f "$NPMRC_TEMP"' EXIT
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$NPMRC_TEMP"
NPM_FLAGS=(--userconfig "$NPMRC_TEMP")
if [[ "$SKIP_PUBLISH" == true ]]; then
:
elif [[ -n "${NPM_TOKEN:-}" ]]; then
# Write a temporary .npmrc for auth (npm_config_ env vars can't encode
# registry-scoped keys because they contain slashes and colons).
NPMRC_TEMP="${ROOT_DIR}/.npmrc.deploy"
trap 'rm -f "$NPMRC_TEMP"' EXIT
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$NPMRC_TEMP"
NPM_FLAGS=(--userconfig "$NPMRC_TEMP")
elif [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then
echo "using npm trusted publishing via GitHub Actions OIDC"
else
die "NPM_TOKEN environment variable is required outside GitHub Actions"
fi
# ── compute version ─────────────────────────────────────────────────────────
@ -213,7 +239,11 @@ case "$MODE" in
esac
if npm_version_exists "$VERSION"; then
die "version $VERSION already exists on npm. Use --bump <major|minor|patch> or --version <x.y.z>."
if [[ "$SKIP_PUBLISH" == true ]]; then
echo "version $VERSION already exists on npm; continuing because --skip-publish was requested"
else
die "version $VERSION already exists on npm. Use --bump <major|minor|patch> or --version <x.y.z>."
fi
fi
if [[ "$DRY_RUN" == true ]]; then
@ -223,7 +253,7 @@ fi
# ── set version ──────────────────────────────────────────────────────────────
npm version "$VERSION" --no-git-tag-version --allow-same-version "${NPM_FLAGS[@]}"
run_npm version "$VERSION" --no-git-tag-version --allow-same-version
# ── pre-flight: tests ────────────────────────────────────────────────────────
@ -307,13 +337,18 @@ for mod in $SERVER_EXTERNAL; do
done
echo "standalone node_modules verified ($CHECKED)"
if [[ "$SKIP_PUBLISH" == true ]]; then
echo "pre-publish checks passed; skipping publish"
exit 0
fi
# ── publish ──────────────────────────────────────────────────────────────────
# Always tag as "latest" — npm skips the latest tag for prerelease versions
# by default, but we want `npm i -g denchclaw` to always resolve to
# the most recently published version.
echo "publishing ${PACKAGE_NAME}@${VERSION}..."
npm publish --access public --tag latest "${NPM_FLAGS[@]}"
run_npm publish --access public --tag latest
# ── publish alias package (dench → denchclaw) ────────────────────────────────
@ -329,7 +364,7 @@ if [[ -d "$ALIAS_DIR" ]]; then
fs.writeFileSync('${ALIAS_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "publishing ${ALIAS_PACKAGE_NAME}@${VERSION}..."
if (cd "$ALIAS_DIR" && npm publish --access public --tag latest "${NPM_FLAGS[@]}" 2>/dev/null); then
if (cd "$ALIAS_DIR" && run_npm publish --access public --tag latest 2>/dev/null); then
ALIAS_PUBLISHED=true
echo "published ${ALIAS_PACKAGE_NAME}@${VERSION}"
else

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"
die() { echo "error: $*" >&2; exit 1; }
if ! command -v gh >/dev/null 2>&1; then
die "gh CLI is required to sync repository secrets"
fi
if ! gh auth status >/dev/null 2>&1; then
die "run 'gh auth login' before syncing GitHub secrets"
fi
if [[ -z "${POSTHOG_KEY:-}" ]]; then
die "POSTHOG_KEY environment variable is required"
fi
gh secret set POSTHOG_KEY --body "$POSTHOG_KEY"
echo "synced POSTHOG_KEY"
if [[ -n "${NPM_TOKEN:-}" ]]; then
gh secret set NPM_TOKEN --body "$NPM_TOKEN"
echo "synced NPM_TOKEN"
else
echo "skipped NPM_TOKEN (not set)"
fi
echo ""
echo "GitHub Actions secrets are ready for the release workflow."
echo "If you configure npm trusted publishing later, you can remove NPM_TOKEN."

17
scripts/with-root-env.sh Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"
if [[ -f ".env" ]]; then
set -a
# shellcheck disable=SC1091
source ".env"
set +a
fi
exec "$@"