👌 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:
parent
161d543229
commit
687b04a963
94
.github/workflows/release.yml
vendored
Normal file
94
.github/workflows/release.yml
vendored
Normal 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
51
RELEASING.md
Normal 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.
|
||||
10
package.json
10
package.json
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
36
scripts/sync-github-secrets.sh
Normal file
36
scripts/sync-github-secrets.sh
Normal 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
17
scripts/with-root-env.sh
Normal 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 "$@"
|
||||
Loading…
x
Reference in New Issue
Block a user