mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-10 03:27:54 +00:00
Merge pull request #422 from iflytek/feat/cli-auto-build
feat(cli): add automated build and publish workflow
This commit is contained in:
commit
9dfbed2ef1
5 changed files with 881 additions and 705 deletions
305
.github/workflows/release-cli.yml
vendored
Normal file
305
.github/workflows/release-cli.yml
vendored
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
name: Release CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['cli-v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to release (e.g. cli-v0.1.5)'
|
||||
required: true
|
||||
skip_npm:
|
||||
description: 'Skip npm publish'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release-cli-${{ github.event.inputs.tag || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.extract.outputs.version }}
|
||||
package_name: ${{ steps.extract.outputs.package_name }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Validate tag (workflow_dispatch only)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
|
||||
# Verify tag exists
|
||||
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
echo "ERROR: Tag '$TAG' does not exist in the repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify current checkout matches the tag
|
||||
TAG_SHA=$(git rev-parse "refs/tags/$TAG^{commit}")
|
||||
CURRENT_SHA=$(git rev-parse HEAD)
|
||||
|
||||
if [ "$TAG_SHA" != "$CURRENT_SHA" ]; then
|
||||
echo "ERROR: Current checkout SHA does not match tag '$TAG'" >&2
|
||||
echo " Tag SHA: $TAG_SHA" >&2
|
||||
echo " Current SHA: $CURRENT_SHA" >&2
|
||||
echo "" >&2
|
||||
echo "This indicates the checkout did not switch to the specified tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Tag '$TAG' validated (SHA: $TAG_SHA)"
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
- name: Extract version from tag
|
||||
id: extract
|
||||
working-directory: cli
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||
if [[ ! "$TAG" =~ ^cli-v([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]]; then
|
||||
echo "Invalid tag format: $TAG (expected cli-vX.Y.Z)"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${BASH_REMATCH[1]}"
|
||||
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '$VERSION';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "Version set to: $VERSION"
|
||||
echo "Package name: $PACKAGE_NAME"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: cli
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run linter
|
||||
working-directory: cli
|
||||
run: bun run lint
|
||||
|
||||
- name: Run type check
|
||||
working-directory: cli
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Run tests
|
||||
working-directory: cli
|
||||
run: bun test
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: cli
|
||||
run: bun run build
|
||||
|
||||
- name: Verify built CLI
|
||||
working-directory: cli
|
||||
run: |
|
||||
node dist/index.js version
|
||||
RUNTIME_VERSION=$(node dist/index.js version | sed -E 's/^SkillHub CLI //')
|
||||
if [ "$RUNTIME_VERSION" != "${{ steps.extract.outputs.version }}" ]; then
|
||||
echo "Version mismatch: runtime=$RUNTIME_VERSION, tag=${{ steps.extract.outputs.version }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-dist
|
||||
path: |
|
||||
cli/dist/
|
||||
cli/package.json
|
||||
cli/README.md
|
||||
cli/LICENSE
|
||||
retention-days: 7
|
||||
|
||||
publish-npm:
|
||||
needs: build-and-test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !inputs.skip_npm }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
- name: Set version from tag
|
||||
working-directory: cli
|
||||
run: |
|
||||
VERSION="${{ needs.build-and-test.outputs.version }}"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '$VERSION';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: cli
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: cli
|
||||
run: bun run build
|
||||
|
||||
- name: Check if version exists on npm
|
||||
id: check_npm
|
||||
env:
|
||||
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
|
||||
run: |
|
||||
PACKAGE_NAME="${{ needs.build-and-test.outputs.package_name }}"
|
||||
VERSION="${{ needs.build-and-test.outputs.version }}"
|
||||
|
||||
# Three-state check: success (exists) / 404 (missing) / error (fail job)
|
||||
set +e
|
||||
NPM_OUTPUT=$(npm view "${PACKAGE_NAME}@${VERSION}" version --registry "$NPM_REGISTRY" 2>&1)
|
||||
NPM_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ $NPM_EXIT_CODE -eq 0 ]; then
|
||||
# Success: version exists on registry
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Version $VERSION already exists on registry, skipping publish"
|
||||
elif echo "$NPM_OUTPUT" | grep -Eiq '(E404|404 Not Found|is not in this registry|Not found)'; then
|
||||
# Explicit 404: version does not exist
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Version $VERSION does not exist on registry, proceeding with publish"
|
||||
else
|
||||
# Uncertain state: network error, auth failure, registry error, etc.
|
||||
echo "ERROR: Failed to check npm registry (exit code: $NPM_EXIT_CODE)" >&2
|
||||
echo "Output: $NPM_OUTPUT" >&2
|
||||
echo "" >&2
|
||||
echo "This could be due to:" >&2
|
||||
echo " - Network connectivity issues" >&2
|
||||
echo " - Registry service errors (5xx)" >&2
|
||||
echo " - Authentication/authorization failures" >&2
|
||||
echo " - DNS or TLS problems" >&2
|
||||
echo "" >&2
|
||||
echo "Cannot safely determine if version exists. Failing job to prevent silent skip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure npm authentication
|
||||
if: steps.check_npm.outputs.exists == 'false'
|
||||
env:
|
||||
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
REGISTRY_HOST="${NPM_REGISTRY#http://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
||||
|
||||
cat > ~/.npmrc <<EOF
|
||||
registry=${NPM_REGISTRY}
|
||||
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
EOF
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check_npm.outputs.exists == 'false'
|
||||
working-directory: cli
|
||||
env:
|
||||
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
|
||||
run: |
|
||||
npm publish --access public --registry "$NPM_REGISTRY"
|
||||
echo "Published ${{ needs.build-and-test.outputs.package_name }}@${{ needs.build-and-test.outputs.version }}"
|
||||
|
||||
create-release:
|
||||
needs: [build-and-test, publish-npm]
|
||||
runs-on: ubuntu-latest
|
||||
# Only create the GitHub Release after npm publish has actually succeeded
|
||||
# (or was explicitly skipped via skip_npm=true). This prevents a
|
||||
# half-released state where Release exists but `npm install -g` fails.
|
||||
if: ${{ always() && needs.build-and-test.result == 'success' && (needs.publish-npm.result == 'success' || (inputs.skip_npm && needs.publish-npm.result == 'skipped')) }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cli-dist
|
||||
path: cli-release
|
||||
|
||||
- name: Create release archives
|
||||
run: |
|
||||
VERSION="${{ needs.build-and-test.outputs.version }}"
|
||||
|
||||
# Create tar.gz
|
||||
tar -czf "skillhub-cli-${VERSION}.tar.gz" -C cli-release .
|
||||
|
||||
# Create zip
|
||||
(cd cli-release && zip -r "../skillhub-cli-${VERSION}.zip" .)
|
||||
|
||||
# Generate checksums
|
||||
sha256sum "skillhub-cli-${VERSION}.tar.gz" > "skillhub-cli-${VERSION}.tar.gz.sha256"
|
||||
sha256sum "skillhub-cli-${VERSION}.zip" > "skillhub-cli-${VERSION}.zip.sha256"
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||
VERSION="${{ needs.build-and-test.outputs.version }}"
|
||||
PACKAGE_NAME="${{ needs.build-and-test.outputs.package_name }}"
|
||||
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "Release $TAG already exists, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate release notes
|
||||
cat > release-notes.md <<EOF
|
||||
# SkillHub CLI ${VERSION}
|
||||
|
||||
## Installation
|
||||
|
||||
### npm
|
||||
\`\`\`bash
|
||||
npm install -g ${PACKAGE_NAME}@${VERSION}
|
||||
\`\`\`
|
||||
|
||||
### From source
|
||||
Download and extract the archive, then:
|
||||
\`\`\`bash
|
||||
npm install -g .
|
||||
\`\`\`
|
||||
|
||||
## Verify installation
|
||||
\`\`\`bash
|
||||
skillhub version
|
||||
\`\`\`
|
||||
|
||||
## Changes
|
||||
See commit history for details.
|
||||
EOF
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "CLI ${VERSION}" \
|
||||
--notes-file release-notes.md \
|
||||
"skillhub-cli-${VERSION}.tar.gz" \
|
||||
"skillhub-cli-${VERSION}.tar.gz.sha256" \
|
||||
"skillhub-cli-${VERSION}.zip" \
|
||||
"skillhub-cli-${VERSION}.zip.sha256"
|
||||
32
Makefile
32
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset dev-logs dev-status build test check clean web-deps web-install web-install-ci dev-server dev-server-restart dev-web build-backend test-backend build-frontend test-frontend test-e2e-frontend test-e2e-smoke-frontend build-web test-web typecheck-web lint-web generate-api db-reset namespace-smoke validate-release-config staging staging-down staging-logs pr parallel-init parallel-sync parallel-up parallel-down docs-dev docs-build docs-preview cli-install test-cli build-cli lint-cli typecheck-cli
|
||||
.PHONY: build build-backend build-backend-app build-cli build-frontend build-web check clean cli-install db-reset dev dev-all dev-all-down dev-all-reset dev-down dev-logs dev-server dev-server-restart dev-status dev-web docs-build docs-dev docs-preview generate-api help lint-cli lint-web namespace-smoke parallel-down parallel-init parallel-sync parallel-up pr publish-cli publish-cli-major publish-cli-minor staging staging-down staging-logs test test-backend test-backend-app test-cli test-e2e-frontend test-e2e-smoke-frontend test-frontend test-web typecheck-cli typecheck-web validate-release-config web-deps web-install web-install-ci
|
||||
|
||||
DEV_DIR := .dev
|
||||
DEV_SERVER_PID := $(DEV_DIR)/server.pid
|
||||
|
|
@ -263,6 +263,9 @@ lint-web: ## 前端代码检查
|
|||
|
||||
# CLI 相关目标
|
||||
|
||||
cli-install: ## 安装 CLI 依赖
|
||||
cd cli && bun install --frozen-lockfile
|
||||
|
||||
build-cli: ## 构建 CLI
|
||||
cd cli && bun run build
|
||||
|
||||
|
|
@ -275,33 +278,15 @@ lint-cli: ## CLI 代码检查
|
|||
typecheck-cli: ## CLI 类型检查
|
||||
cd cli && bun run typecheck
|
||||
|
||||
publish-cli: ## 发布 CLI 到 npm(patch 版本)
|
||||
@if [ ! -f cli/.env.local ]; then \
|
||||
echo "Error: cli/.env.local not found."; \
|
||||
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
|
||||
exit 1; \
|
||||
fi
|
||||
publish-cli: ## 发布 CLI(patch 版本)- bump + tag + push,触发 CI 自动发布
|
||||
./scripts/publish-cli.sh patch
|
||||
|
||||
publish-cli-minor: ## 发布 CLI 到 npm(minor 版本)
|
||||
@if [ ! -f cli/.env.local ]; then \
|
||||
echo "Error: cli/.env.local not found."; \
|
||||
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
|
||||
exit 1; \
|
||||
fi
|
||||
publish-cli-minor: ## 发布 CLI(minor 版本)- bump + tag + push,触发 CI 自动发布
|
||||
./scripts/publish-cli.sh minor
|
||||
|
||||
publish-cli-major: ## 发布 CLI 到 npm(major 版本)
|
||||
@if [ ! -f cli/.env.local ]; then \
|
||||
echo "Error: cli/.env.local not found."; \
|
||||
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
|
||||
exit 1; \
|
||||
fi
|
||||
publish-cli-major: ## 发布 CLI(major 版本)- bump + tag + push,触发 CI 自动发布
|
||||
./scripts/publish-cli.sh major
|
||||
|
||||
publish-cli-dry: ## 发布 CLI 到 npm(dry run)
|
||||
DRY_RUN=true ./scripts/publish-cli.sh patch
|
||||
|
||||
db-reset: ## 重置数据库
|
||||
$(DEV_COMPOSE) down -v --remove-orphans
|
||||
$(DEV_COMPOSE) up -d --wait --remove-orphans postgres
|
||||
|
|
@ -408,6 +393,3 @@ docs-build: ## 构建文档站点
|
|||
|
||||
docs-preview: ## 预览构建后的文档站点
|
||||
cd docs/skillhub && npm run preview
|
||||
|
||||
cli-install: ## 安装 CLI 依赖
|
||||
cd cli && bun install --frozen-lockfile
|
||||
|
|
|
|||
146
cli/RELEASE.md
Normal file
146
cli/RELEASE.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# CLI Release Guide
|
||||
|
||||
## Overview
|
||||
|
||||
CLI releases are fully automated. Running `make publish-cli` on a clean `main` branch bumps the version, commits, creates a `cli-vX.Y.Z` tag, and pushes everything to origin. The GitHub Actions workflow [`release-cli.yml`](../.github/workflows/release-cli.yml) listens for the tag and handles build, test, npm publish, and GitHub Release creation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Repository Secrets
|
||||
|
||||
Configure in GitHub repository → Settings → Secrets and variables → Actions:
|
||||
|
||||
- `NPM_TOKEN`: npm token with publish permissions
|
||||
- Generate at https://www.npmjs.com/settings/YOUR_USERNAME/tokens
|
||||
- Use **Classic Automation Token** (bypasses 2FA automatically), or
|
||||
- **Granular Access Token** with "Allow bypass 2FA" enabled, scoped to the package
|
||||
|
||||
### Repository Variables (optional)
|
||||
|
||||
- `NPM_REGISTRY`: npm registry URL (default: `https://registry.npmjs.org`)
|
||||
|
||||
### Local Environment
|
||||
|
||||
- `node` and `npm` installed (the script uses `npm version` to bump)
|
||||
- `git` installed with push access to the repository
|
||||
- On the `main` branch with a clean working tree
|
||||
|
||||
### Package Configuration
|
||||
|
||||
In [`cli/package.json`](./package.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
### One-shot Release
|
||||
|
||||
From the repository root, on a clean `main` branch:
|
||||
|
||||
```bash
|
||||
make publish-cli # patch: 0.1.5 -> 0.1.6
|
||||
make publish-cli-minor # minor: 0.1.5 -> 0.2.0
|
||||
make publish-cli-major # major: 0.1.5 -> 1.0.0
|
||||
```
|
||||
|
||||
[`scripts/publish-cli.sh`](../scripts/publish-cli.sh) performs the following steps:
|
||||
|
||||
1. Verify the working tree is clean
|
||||
2. Require the current branch to be `main`, otherwise abort
|
||||
3. `git pull --ff-only` from `origin/main`
|
||||
4. Fetch remote tags and align `package.json` with the latest `cli-v*` tag
|
||||
5. Compute the new version via `npm version <bump>`
|
||||
6. Verify the new tag does not exist locally or on origin
|
||||
7. After interactive confirmation: commit the bump, create the `cli-vX.Y.Z` tag, push both commit and tag to origin
|
||||
|
||||
Pushing the tag triggers CI — no further manual action required.
|
||||
|
||||
### CI Workflow
|
||||
|
||||
[`release-cli.yml`](../.github/workflows/release-cli.yml) contains three jobs:
|
||||
|
||||
1. **build-and-test**
|
||||
- Extract version from tag name (`cli-v0.1.6` → `0.1.6`) and write it into `cli/package.json`
|
||||
- Install deps, lint, typecheck, test, build
|
||||
- Verify the built CLI's runtime version matches the tag
|
||||
|
||||
2. **publish-npm**
|
||||
- Skip if the target version already exists on the registry
|
||||
- Configure `~/.npmrc` and run `npm publish --access public`
|
||||
|
||||
3. **create-release**
|
||||
- Package `dist/` + README + LICENSE as `tar.gz` and `zip`
|
||||
- Generate SHA256 checksums
|
||||
- Create a GitHub Release and upload artifacts
|
||||
|
||||
### Verify Release
|
||||
|
||||
- Workflow: https://github.com/iflytek/skillhub/actions/workflows/release-cli.yml
|
||||
- Release: https://github.com/iflytek/skillhub/releases
|
||||
- npm: `npm view @astron-team/skillhub@<version>`
|
||||
|
||||
## Release Audit Trail
|
||||
|
||||
GitHub Actions automatically records on each workflow run page:
|
||||
|
||||
- **Triggering user** (the developer who pushed the tag, i.e. `github.actor`)
|
||||
- **Trigger event** (`push` tag or `workflow_dispatch`)
|
||||
- **Tag name and commit SHA**
|
||||
|
||||
The team can review the full audit trail in the Actions tab without any extra configuration.
|
||||
|
||||
## Manual Trigger
|
||||
|
||||
From the Actions UI:
|
||||
|
||||
1. Actions → Release CLI → "Run workflow"
|
||||
2. Enter an existing tag name matching `cli-vX.Y.Z`
|
||||
3. Optionally enable skip npm publish
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `releases must be cut from 'main'`
|
||||
|
||||
Switch back to `main`, pull the latest, and retry.
|
||||
|
||||
### `git working tree is not clean`
|
||||
|
||||
Commit or stash local changes first.
|
||||
|
||||
### `tag cli-vX.Y.Z already exists`
|
||||
|
||||
The previous release didn't clean up, or someone else released the same version. Check `git tag --list 'cli-v*'` and remote tags, then retry with a higher version.
|
||||
|
||||
### npm Publish Fails
|
||||
|
||||
- **403 with 2FA message**: `NPM_TOKEN` is not an Automation Token, or bypass 2FA is not enabled — regenerate with the correct type
|
||||
- **403 Forbidden**: Package scope doesn't match token permissions — confirm publish rights for the `@astron-team` org
|
||||
- **E404**: The registry doesn't host this scope — check `NPM_REGISTRY`
|
||||
|
||||
### Build / Test Fails
|
||||
|
||||
Reproduce locally:
|
||||
|
||||
```bash
|
||||
make lint-cli && make typecheck-cli && make test-cli && make build-cli
|
||||
```
|
||||
|
||||
Confirm the Bun version matches `packageManager` in [`cli/package.json`](./package.json).
|
||||
|
||||
### Version Mismatch (runtime ≠ tag)
|
||||
|
||||
CI runs `node dist/index.js version` and requires the output to match the tag. If the CLI's `version` command implementation changes, update the verification logic in [`release-cli.yml`](../.github/workflows/release-cli.yml) accordingly.
|
||||
|
||||
## Tag Naming Convention
|
||||
|
||||
- CLI releases: `cli-v*` (e.g., `cli-v0.1.6`)
|
||||
- Repository releases: `v*` (e.g., `v0.3.0`)
|
||||
|
||||
The two tag namespaces are independent, allowing CLI and server to version separately.
|
||||
|
|
@ -1,20 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Release entrypoint for the SkillHub CLI.
|
||||
#
|
||||
# This script bumps cli/package.json, commits the bump, creates a `cli-vX.Y.Z`
|
||||
# tag, and pushes it. The GitHub Actions workflow `release-cli.yml` picks up
|
||||
# the tag and performs the actual build + npm publish + GitHub Release.
|
||||
#
|
||||
# Prefer this over direct `npm publish`: avoids local network/TLS issues with
|
||||
# registry.npmjs.org, and keeps release provenance tied to CI.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
CLI_DIR="$REPO_ROOT/cli"
|
||||
ENV_LOCAL="$CLI_DIR/.env.local"
|
||||
PACKAGE_JSON="$CLI_DIR/package.json"
|
||||
PKG_INFO_TS="$CLI_DIR/src/generated/pkg-info.ts"
|
||||
DIST_ENTRY="$CLI_DIR/dist/index.js"
|
||||
|
||||
log_stage() {
|
||||
echo "[publish-cli] $1"
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [patch|minor|major|skip]" >&2
|
||||
echo "Usage: $0 [patch|minor|major]" >&2
|
||||
}
|
||||
|
||||
confirm() {
|
||||
|
|
@ -24,222 +30,141 @@ confirm() {
|
|||
[[ "$answer" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
verify_version_sync() {
|
||||
local expected_version="$1"
|
||||
local generated_version
|
||||
local runtime_version
|
||||
|
||||
generated_version="$(node - "$PKG_INFO_TS" <<'NODE'
|
||||
const fs = require('fs')
|
||||
const path = process.argv[2]
|
||||
const contents = fs.readFileSync(path, 'utf8')
|
||||
const match = contents.match(/export const PKG_VERSION = ["']([^"']+)["']/)
|
||||
if (!match) process.exit(1)
|
||||
process.stdout.write(match[1])
|
||||
NODE
|
||||
)"
|
||||
|
||||
runtime_version="$(node "$DIST_ENTRY" version | sed -E 's/^SkillHub CLI //')"
|
||||
|
||||
if [[ "$generated_version" != "$expected_version" ]]; then
|
||||
echo "generated PKG_VERSION mismatch: expected $expected_version, got $generated_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$runtime_version" != "$expected_version" ]]; then
|
||||
echo "built CLI version mismatch: expected $expected_version, got $runtime_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_version_not_published() {
|
||||
local package_name="$1"
|
||||
local version="$2"
|
||||
local view_output
|
||||
|
||||
if view_output="$(npm view "${package_name}@${version}" version --registry "$NPM_REGISTRY" 2>&1)"; then
|
||||
echo "${package_name}@${version} already exists on $NPM_REGISTRY" >&2
|
||||
echo "Update cli/package.json to the latest published version before running this release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$view_output" | grep -Eiq '(E404|404 Not Found|is not in this registry|Not found)'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "failed to verify whether ${package_name}@${version} exists on $NPM_REGISTRY" >&2
|
||||
echo "$view_output" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
next_package_version() {
|
||||
local version="$1"
|
||||
local bump_type="$2"
|
||||
|
||||
node - "$version" "$bump_type" <<'NODE'
|
||||
const version = process.argv[2]
|
||||
const bumpType = process.argv[3]
|
||||
const parts = version.split('.').map(Number)
|
||||
|
||||
if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0)) {
|
||||
throw new Error(`unsupported package version: ${version}`)
|
||||
}
|
||||
|
||||
if (bumpType === 'patch') {
|
||||
parts[2] += 1
|
||||
} else if (bumpType === 'minor') {
|
||||
parts[1] += 1
|
||||
parts[2] = 0
|
||||
} else if (bumpType === 'major') {
|
||||
parts[0] += 1
|
||||
parts[1] = 0
|
||||
parts[2] = 0
|
||||
} else if (bumpType !== 'skip') {
|
||||
throw new Error(`unsupported bump type: ${bumpType}`)
|
||||
}
|
||||
|
||||
process.stdout.write(parts.join('.'))
|
||||
NODE
|
||||
}
|
||||
|
||||
BUMP_TYPE="${1:-patch}"
|
||||
if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" && "$BUMP_TYPE" != "skip" ]]; then
|
||||
if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ENV_LOCAL" ]]; then
|
||||
echo "cli/.env.local not found" >&2
|
||||
echo "Copy cli/.env.example to cli/.env.local" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "loading environment"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_LOCAL"
|
||||
set +a
|
||||
|
||||
: "${NPM_REGISTRY:=https://registry.npmjs.org}"
|
||||
: "${DRY_RUN:=false}"
|
||||
|
||||
if [[ -z "${NPM_TOKEN:-}" ]]; then
|
||||
echo "NPM_TOKEN is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${NPM_ORG:-}" ]]; then
|
||||
echo "NPM_ORG is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "validating package metadata"
|
||||
PACKAGE_NAME="$(node -p "require('$PACKAGE_JSON').name ?? ''")"
|
||||
PACKAGE_VERSION="$(node -p "require('$PACKAGE_JSON').version ?? ''")"
|
||||
PACKAGE_ACCESS="$(node -p "require('$PACKAGE_JSON').publishConfig?.access ?? ''")"
|
||||
|
||||
if [[ "$PACKAGE_NAME" != "@${NPM_ORG}/"* ]]; then
|
||||
echo "package name must match @${NPM_ORG}/* (got $PACKAGE_NAME)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$PACKAGE_VERSION" ]]; then
|
||||
echo "package version is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGE_ACCESS" != "public" ]]; then
|
||||
echo "publishConfig.access must be public" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "checking git working tree"
|
||||
if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
|
||||
echo "git working tree is not clean" >&2
|
||||
echo "git working tree is not clean — commit or stash changes first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$BUMP_TYPE" == "skip" ]]; then
|
||||
NEW_VERSION="$PACKAGE_VERSION"
|
||||
else
|
||||
NEW_VERSION="$(next_package_version "$PACKAGE_VERSION" "$BUMP_TYPE")"
|
||||
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$CURRENT_BRANCH" != "main" ]]; then
|
||||
echo "releases must be cut from 'main' (current: '$CURRENT_BRANCH')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "checking registry version"
|
||||
assert_version_not_published "$PACKAGE_NAME" "$NEW_VERSION"
|
||||
log_stage "pulling latest from origin/$CURRENT_BRANCH"
|
||||
git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH"
|
||||
|
||||
if [[ "$BUMP_TYPE" != "skip" ]]; then
|
||||
if ! confirm "Proceed with version bump ($BUMP_TYPE)?"; then
|
||||
echo "version bump cancelled" >&2
|
||||
exit 1
|
||||
log_stage "fetching tags from origin"
|
||||
git -C "$REPO_ROOT" fetch --tags --prune origin
|
||||
|
||||
log_stage "checking for unpushed release artifacts"
|
||||
UNPUSHED_COMMITS="$(git -C "$REPO_ROOT" log --oneline origin/"$CURRENT_BRANCH"..HEAD 2>/dev/null || true)"
|
||||
|
||||
# Detect local cli-v* tags that don't exist on origin.
|
||||
# This catches both: (a) commit not pushed + tag not pushed, and
|
||||
# (b) commit pushed but tag push failed (where --no-merged would miss it).
|
||||
UNPUSHED_RELEASE_TAGS=""
|
||||
while IFS= read -r local_tag; do
|
||||
[[ -z "$local_tag" ]] && continue
|
||||
if ! git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$local_tag" >/dev/null 2>&1; then
|
||||
UNPUSHED_RELEASE_TAGS="${UNPUSHED_RELEASE_TAGS:+$UNPUSHED_RELEASE_TAGS
|
||||
}$local_tag"
|
||||
fi
|
||||
done < <(git -C "$REPO_ROOT" tag --list 'cli-v*' 2>/dev/null)
|
||||
|
||||
if [[ -n "$UNPUSHED_COMMITS" ]] || [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo "" >&2
|
||||
echo "ERROR: Detected unpushed release artifacts from a previous failed push:" >&2
|
||||
echo "" >&2
|
||||
|
||||
if [[ -n "$UNPUSHED_COMMITS" ]]; then
|
||||
echo " Unpushed commits:" >&2
|
||||
echo "$UNPUSHED_COMMITS" | sed 's/^/ /' >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
|
||||
log_stage "bumping version ($BUMP_TYPE)"
|
||||
(
|
||||
cd "$CLI_DIR"
|
||||
npm version "$BUMP_TYPE" --no-git-tag-version
|
||||
)
|
||||
fi
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo " Unpushed tags:" >&2
|
||||
echo "$UNPUSHED_RELEASE_TAGS" | sed 's/^/ /' >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
|
||||
ACTUAL_VERSION="$(node -p "require('$PACKAGE_JSON').version ?? ''")"
|
||||
if [[ "$ACTUAL_VERSION" != "$NEW_VERSION" ]]; then
|
||||
echo "npm version produced $ACTUAL_VERSION, expected $NEW_VERSION" >&2
|
||||
echo "Choose a recovery option:" >&2
|
||||
echo "" >&2
|
||||
echo " 1. Retry push (if the previous failure was temporary, e.g., network issue):" >&2
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
FIRST_TAG="$(echo "$UNPUSHED_RELEASE_TAGS" | head -n1)"
|
||||
echo " git push origin $CURRENT_BRANCH $FIRST_TAG" >&2
|
||||
else
|
||||
echo " git push origin $CURRENT_BRANCH" >&2
|
||||
fi
|
||||
echo "" >&2
|
||||
echo " 2. Rollback and retry release (if you want to start fresh):" >&2
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo " git tag -d $UNPUSHED_RELEASE_TAGS" | tr '\n' ' ' | sed 's/ $/\n/' >&2
|
||||
fi
|
||||
echo " git reset --hard origin/$CURRENT_BRANCH" >&2
|
||||
echo " # Then re-run: make publish-cli" >&2
|
||||
echo "" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "running preflight build"
|
||||
(
|
||||
cd "$CLI_DIR"
|
||||
bun run build
|
||||
)
|
||||
|
||||
log_stage "running preflight tests"
|
||||
(
|
||||
cd "$CLI_DIR"
|
||||
bun run test
|
||||
)
|
||||
|
||||
log_stage "verifying built version"
|
||||
verify_version_sync "$NEW_VERSION"
|
||||
|
||||
log_stage "running preflight pack"
|
||||
(
|
||||
cd "$CLI_DIR"
|
||||
npm pack --dry-run
|
||||
)
|
||||
|
||||
log_stage "ready to publish $PACKAGE_NAME@$NEW_VERSION"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_stage "DRY_RUN=true, skipping npm publish"
|
||||
exit 0
|
||||
log_stage "resolving baseline version from latest cli-v* tag"
|
||||
LATEST_TAG="$(git -C "$REPO_ROOT" tag --list 'cli-v*' --sort=-version:refname | head -n1)"
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
BASE_VERSION="${LATEST_TAG#cli-v}"
|
||||
CURRENT_PKG_VERSION="$(node -p "require('$PACKAGE_JSON').version")"
|
||||
if [[ "$BASE_VERSION" != "$CURRENT_PKG_VERSION" ]]; then
|
||||
log_stage "syncing package.json $CURRENT_PKG_VERSION -> $BASE_VERSION (from $LATEST_TAG)"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('$PACKAGE_JSON', 'utf8'));
|
||||
pkg.version = '$BASE_VERSION';
|
||||
fs.writeFileSync('$PACKAGE_JSON', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
fi
|
||||
else
|
||||
log_stage "no cli-v* tags found, bumping from package.json"
|
||||
fi
|
||||
|
||||
if ! confirm "Publish $PACKAGE_NAME@$NEW_VERSION to $NPM_REGISTRY?"; then
|
||||
echo "publish cancelled" >&2
|
||||
exit 2
|
||||
log_stage "bumping version ($BUMP_TYPE)"
|
||||
NPM_VERSION_OUTPUT="$(cd "$CLI_DIR" && npm version "$BUMP_TYPE" --no-git-tag-version)"
|
||||
NEW_VERSION="${NPM_VERSION_OUTPUT#v}"
|
||||
|
||||
if [[ -z "$NEW_VERSION" ]]; then
|
||||
echo "failed to parse version from npm output: $NPM_VERSION_OUTPUT" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NPM_CONFIG_FILE="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "$NPM_CONFIG_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
TAG="cli-v${NEW_VERSION}"
|
||||
|
||||
REGISTRY_HOST="${NPM_REGISTRY#http://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
||||
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
echo "tag $TAG already exists locally" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"$NPM_CONFIG_FILE" <<EOF
|
||||
registry=${NPM_REGISTRY}
|
||||
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
EOF
|
||||
if git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
|
||||
echo "tag $TAG already exists on origin" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "publishing package"
|
||||
(
|
||||
cd "$CLI_DIR"
|
||||
NPM_CONFIG_USERCONFIG="$NPM_CONFIG_FILE" npm publish --access public --registry "$NPM_REGISTRY"
|
||||
)
|
||||
log_stage "new version: $NEW_VERSION (tag: $TAG)"
|
||||
|
||||
log_stage "publish completed"
|
||||
if ! confirm "Commit, tag, and push $TAG to origin?"; then
|
||||
echo "release cancelled — reverting package.json" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "committing version bump"
|
||||
git -C "$REPO_ROOT" add "$PACKAGE_JSON"
|
||||
git -C "$REPO_ROOT" commit -m "chore(cli): bump version to $NEW_VERSION"
|
||||
|
||||
log_stage "creating tag $TAG"
|
||||
git -C "$REPO_ROOT" tag "$TAG"
|
||||
|
||||
log_stage "pushing commit and tag to origin (atomic)"
|
||||
git -C "$REPO_ROOT" push --atomic origin "$CURRENT_BRANCH" "$TAG"
|
||||
|
||||
log_stage "release triggered — CI workflow will build and publish"
|
||||
log_stage "watch progress at: https://github.com/iflytek/skillhub/actions/workflows/release-cli.yml"
|
||||
|
|
|
|||
|
|
@ -1,517 +1,335 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Integration tests for scripts/publish-cli.sh.
|
||||
#
|
||||
# The script bumps cli/package.json, commits, tags `cli-vX.Y.Z`, and pushes
|
||||
# both refs to origin. These tests build a self-contained fake repo for each
|
||||
# scenario, using a real bare repository as origin so git fetch / pull / push
|
||||
# are actually exercised. `npm` is stubbed to keep `npm version` deterministic.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
PUBLISH_SCRIPT="$REPO_ROOT/scripts/publish-cli.sh"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
CLI_DIR="$TMP_DIR/cli"
|
||||
SCRIPTS_DIR="$TMP_DIR/scripts"
|
||||
|
||||
TMP_DIRS=()
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
local d
|
||||
for d in "${TMP_DIRS[@]+"${TMP_DIRS[@]}"}"; do
|
||||
rm -rf "$d"
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$CLI_DIR" "$SCRIPTS_DIR"
|
||||
cp "$PUBLISH_SCRIPT" "$SCRIPTS_DIR/publish-cli.sh"
|
||||
cat >"$CLI_DIR/package.json" <<'EOF'
|
||||
new_tmp() {
|
||||
local d
|
||||
d="$(mktemp -d)"
|
||||
TMP_DIRS+=("$d")
|
||||
echo "$d"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# init_repo <repo_dir> [initial_pkg_version] [tag_to_seed ...]
|
||||
#
|
||||
# Builds a minimal repo with:
|
||||
# - cli/package.json at the requested version
|
||||
# - scripts/publish-cli.sh (the script under test)
|
||||
# - bin/npm stub that implements `npm version <bump> --no-git-tag-version`
|
||||
# - a sibling bare repo as `origin`
|
||||
# - HEAD on `main` with the init commit already pushed
|
||||
# - optional pre-seeded `cli-v*` tags (created locally AND on origin)
|
||||
init_repo() {
|
||||
local repo="$1"
|
||||
local version="${2:-0.1.0}"
|
||||
local tags=()
|
||||
if [[ $# -gt 2 ]]; then
|
||||
tags=("${@:3}")
|
||||
fi
|
||||
|
||||
local origin="$repo.origin.git"
|
||||
TMP_DIRS+=("$origin")
|
||||
|
||||
mkdir -p "$repo/cli" "$repo/scripts" "$repo/bin"
|
||||
cp "$PUBLISH_SCRIPT" "$repo/scripts/publish-cli.sh"
|
||||
|
||||
cat >"$repo/cli/package.json" <<EOF
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.1.0",
|
||||
"version": "$version",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
|
||||
if REPO_ROOT="$TMP_DIR" bash "$SCRIPTS_DIR/publish-cli.sh" >"$TMP_DIR/stdout.log" 2>"$TMP_DIR/stderr.log"; then
|
||||
echo "expected script to fail when cli/.env.local is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "cli/.env.local not found" "$TMP_DIR/stderr.log"
|
||||
grep -F "Copy cli/.env.example to cli/.env.local" "$TMP_DIR/stderr.log"
|
||||
|
||||
cat >"$CLI_DIR/.env.local" <<'EOF'
|
||||
NPM_TOKEN=test-token
|
||||
NPM_ORG=astron-team
|
||||
EOF
|
||||
|
||||
cat >"$CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "astron-team/skillhub",
|
||||
"version": "0.1.0",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
|
||||
if REPO_ROOT="$TMP_DIR" bash "$SCRIPTS_DIR/publish-cli.sh" >"$TMP_DIR/stdout.log" 2>"$TMP_DIR/stderr.log"; then
|
||||
echo "expected script to fail for invalid package scope" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "loading environment" "$TMP_DIR/stdout.log"
|
||||
grep -F "validating package metadata" "$TMP_DIR/stdout.log"
|
||||
grep -F "package name must match @astron-team/*" "$TMP_DIR/stderr.log"
|
||||
|
||||
cat >"$CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.1.0",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
|
||||
git -C "$TMP_DIR" init -q
|
||||
git -C "$TMP_DIR" config user.name "Test User"
|
||||
git -C "$TMP_DIR" config user.email "test@example.com"
|
||||
git -C "$TMP_DIR" add cli/.env.local cli/package.json
|
||||
git -C "$TMP_DIR" commit -q -m "init"
|
||||
|
||||
touch "$TMP_DIR/dirty-file.txt"
|
||||
|
||||
if REPO_ROOT="$TMP_DIR" bash "$SCRIPTS_DIR/publish-cli.sh" >"$TMP_DIR/stdout.log" 2>"$TMP_DIR/stderr.log"; then
|
||||
echo "expected script to fail when git working tree is dirty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "checking git working tree" "$TMP_DIR/stdout.log"
|
||||
grep -F "git working tree is not clean" "$TMP_DIR/stderr.log"
|
||||
|
||||
CONFLICT_DIR="$(mktemp -d)"
|
||||
cleanup_conflict() {
|
||||
rm -rf "$CONFLICT_DIR"
|
||||
}
|
||||
CONFLICT_STDOUT="$(mktemp)"
|
||||
CONFLICT_STDERR="$(mktemp)"
|
||||
trap 'cleanup; cleanup_conflict; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR"' EXIT
|
||||
|
||||
CONFLICT_CLI_DIR="$CONFLICT_DIR/cli"
|
||||
CONFLICT_SCRIPTS_DIR="$CONFLICT_DIR/scripts"
|
||||
CONFLICT_BIN_DIR="$CONFLICT_DIR/bin"
|
||||
CONFLICT_CALLS="$CONFLICT_DIR/calls.log"
|
||||
mkdir -p "$CONFLICT_CLI_DIR" "$CONFLICT_SCRIPTS_DIR" "$CONFLICT_BIN_DIR"
|
||||
cp "$PUBLISH_SCRIPT" "$CONFLICT_SCRIPTS_DIR/publish-cli.sh"
|
||||
cat >"$CONFLICT_CLI_DIR/.env.local" <<'EOF'
|
||||
NPM_TOKEN=test-token
|
||||
NPM_ORG=astron-team
|
||||
DRY_RUN=true
|
||||
EOF
|
||||
cat >"$CONFLICT_CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.1.4",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
cat >"$CONFLICT_BIN_DIR/bun" <<EOF
|
||||
# Stub `npm`: only `npm version <patch|minor|major> --no-git-tag-version` is
|
||||
# supported. Mutates package.json in cwd and echoes `vX.Y.Z` (matching real
|
||||
# npm behaviour the script depends on).
|
||||
cat >"$repo/bin/npm" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "bun %s\\n" "\$*" >>"$CONFLICT_CALLS"
|
||||
exit 1
|
||||
EOF
|
||||
cat >"$CONFLICT_BIN_DIR/npm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "npm %s\\n" "\$*" >>"$CONFLICT_CALLS"
|
||||
case "\$1" in
|
||||
version)
|
||||
node - "\$PWD/package.json" <<'NODE'
|
||||
const fs = require("fs")
|
||||
const path = process.argv[2]
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"))
|
||||
pkg.version = "0.1.5"
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\\n")
|
||||
if [[ "${1:-}" != "version" ]]; then
|
||||
echo "npm stub: unsupported subcommand: $*" >&2
|
||||
exit 1
|
||||
fi
|
||||
BUMP="$2"
|
||||
node - "$PWD/package.json" "$BUMP" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const path = process.argv[2];
|
||||
const bump = process.argv[3];
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
const parts = pkg.version.split(".").map(Number);
|
||||
if (bump === "patch") parts[2] += 1;
|
||||
else if (bump === "minor") { parts[1] += 1; parts[2] = 0; }
|
||||
else if (bump === "major") { parts[0] += 1; parts[1] = 0; parts[2] = 0; }
|
||||
else throw new Error("unexpected bump: " + bump);
|
||||
const next = parts.join(".");
|
||||
pkg.version = next;
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
||||
console.log("v" + next);
|
||||
NODE
|
||||
;;
|
||||
view)
|
||||
if [[ "\$2" == "@astron-team/skillhub@0.1.5" ]]; then
|
||||
echo "0.1.5"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$CONFLICT_BIN_DIR/bun" "$CONFLICT_BIN_DIR/npm"
|
||||
chmod +x "$repo/bin/npm"
|
||||
|
||||
git -C "$CONFLICT_DIR" init -q
|
||||
git -C "$CONFLICT_DIR" config user.name "Test User"
|
||||
git -C "$CONFLICT_DIR" config user.email "test@example.com"
|
||||
git -C "$CONFLICT_DIR" add cli/.env.local cli/package.json scripts/publish-cli.sh bin/bun bin/npm
|
||||
git -C "$CONFLICT_DIR" commit -q -m "init"
|
||||
# Ignore test scaffolding files so they don't make `git status` dirty.
|
||||
cat >"$repo/.gitignore" <<EOF
|
||||
stdout.log
|
||||
stderr.log
|
||||
git-push-log.txt
|
||||
bin-git/
|
||||
EOF
|
||||
|
||||
if printf 'y\n' | REPO_ROOT="$CONFLICT_DIR" PATH="$CONFLICT_BIN_DIR:$PATH" bash "$CONFLICT_SCRIPTS_DIR/publish-cli.sh" patch >"$CONFLICT_STDOUT" 2>"$CONFLICT_STDERR"; then
|
||||
echo "expected script to fail when bumped version already exists on npm" >&2
|
||||
exit 1
|
||||
fi
|
||||
git init -q --bare "$origin"
|
||||
git -C "$repo" init -q -b main
|
||||
git -C "$repo" config user.name "Test User"
|
||||
git -C "$repo" config user.email "test@example.com"
|
||||
git -C "$repo" remote add origin "$origin"
|
||||
git -C "$repo" add cli/package.json scripts/publish-cli.sh bin/npm .gitignore
|
||||
git -C "$repo" commit -q -m "init"
|
||||
git -C "$repo" push -q -u origin main
|
||||
|
||||
grep -F "checking registry version" "$CONFLICT_STDOUT"
|
||||
grep -F "@astron-team/skillhub@0.1.5 already exists" "$CONFLICT_STDERR"
|
||||
grep -F "Update cli/package.json to the latest published version" "$CONFLICT_STDERR"
|
||||
if grep -Fq "bun run build" "$CONFLICT_CALLS"; then
|
||||
echo "build should not run when bumped version already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq "npm version" "$CONFLICT_CALLS"; then
|
||||
echo "version bump should not run when bumped version already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -F '"version": "0.1.4"' "$CONFLICT_CLI_DIR/package.json"
|
||||
|
||||
REGISTRY_ERROR_DIR="$(mktemp -d)"
|
||||
cleanup_registry_error() {
|
||||
rm -rf "$REGISTRY_ERROR_DIR"
|
||||
local tag
|
||||
for tag in "${tags[@]+"${tags[@]}"}"; do
|
||||
git -C "$repo" tag "$tag"
|
||||
git -C "$repo" push -q origin "$tag"
|
||||
done
|
||||
}
|
||||
REGISTRY_ERROR_STDOUT="$(mktemp)"
|
||||
REGISTRY_ERROR_STDERR="$(mktemp)"
|
||||
trap 'cleanup; cleanup_conflict; cleanup_registry_error; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR"' EXIT
|
||||
|
||||
REGISTRY_ERROR_CLI_DIR="$REGISTRY_ERROR_DIR/cli"
|
||||
REGISTRY_ERROR_SCRIPTS_DIR="$REGISTRY_ERROR_DIR/scripts"
|
||||
REGISTRY_ERROR_BIN_DIR="$REGISTRY_ERROR_DIR/bin"
|
||||
REGISTRY_ERROR_CALLS="$REGISTRY_ERROR_DIR/calls.log"
|
||||
mkdir -p "$REGISTRY_ERROR_CLI_DIR" "$REGISTRY_ERROR_SCRIPTS_DIR" "$REGISTRY_ERROR_BIN_DIR"
|
||||
cp "$PUBLISH_SCRIPT" "$REGISTRY_ERROR_SCRIPTS_DIR/publish-cli.sh"
|
||||
cat >"$REGISTRY_ERROR_CLI_DIR/.env.local" <<'EOF'
|
||||
NPM_TOKEN=test-token
|
||||
NPM_ORG=astron-team
|
||||
DRY_RUN=true
|
||||
EOF
|
||||
cat >"$REGISTRY_ERROR_CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.3.0",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
# run_publish <repo> <bump> [stdin]
|
||||
# Writes stdout to $repo/stdout.log and stderr to $repo/stderr.log.
|
||||
# Prints the exit code on stdout.
|
||||
run_publish() {
|
||||
local repo="$1"
|
||||
local bump="$2"
|
||||
local input="${3-}"
|
||||
local status=0
|
||||
if [[ -n "$input" ]]; then
|
||||
printf '%s' "$input" | env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE \
|
||||
REPO_ROOT="$repo" PATH="$repo/bin:$PATH" \
|
||||
bash "$repo/scripts/publish-cli.sh" "$bump" \
|
||||
>"$repo/stdout.log" 2>"$repo/stderr.log" || status=$?
|
||||
else
|
||||
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE \
|
||||
REPO_ROOT="$repo" PATH="$repo/bin:$PATH" \
|
||||
bash "$repo/scripts/publish-cli.sh" "$bump" \
|
||||
>"$repo/stdout.log" 2>"$repo/stderr.log" || status=$?
|
||||
fi
|
||||
echo "$status"
|
||||
}
|
||||
EOF
|
||||
cat >"$REGISTRY_ERROR_BIN_DIR/bun" <<EOF
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 1: invalid bump type → usage error, exit non-zero
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] invalid bump type"
|
||||
REPO1="$(new_tmp)"
|
||||
init_repo "$REPO1"
|
||||
status="$(run_publish "$REPO1" "foo")"
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit for invalid bump type"
|
||||
grep -F "Usage:" "$REPO1/stderr.log" >/dev/null
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 2: dirty working tree → abort before any side effect
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] dirty working tree aborts"
|
||||
REPO2="$(new_tmp)"
|
||||
init_repo "$REPO2"
|
||||
touch "$REPO2/dirty.txt"
|
||||
status="$(run_publish "$REPO2" "patch")"
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit for dirty tree"
|
||||
grep -F "checking git working tree" "$REPO2/stdout.log" >/dev/null
|
||||
grep -F "git working tree is not clean" "$REPO2/stderr.log" >/dev/null
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 3: not on main branch → abort
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] non-main branch aborts"
|
||||
REPO3="$(new_tmp)"
|
||||
init_repo "$REPO3"
|
||||
git -C "$REPO3" checkout -q -b feature/x
|
||||
status="$(run_publish "$REPO3" "patch")"
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when not on main"
|
||||
grep -F "releases must be cut from 'main'" "$REPO3/stderr.log" >/dev/null
|
||||
grep -F "feature/x" "$REPO3/stderr.log" >/dev/null
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 4: package.json behind latest cli-v* tag → baseline sync, then bump
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] baseline sync from latest cli-v* tag, then bump + push"
|
||||
REPO4="$(new_tmp)"
|
||||
init_repo "$REPO4" "0.1.0" "cli-v0.2.0"
|
||||
status="$(run_publish "$REPO4" "patch" $'y\n')"
|
||||
[[ "$status" -eq 0 ]] || { cat "$REPO4/stderr.log" >&2; fail "expected success, got $status"; }
|
||||
grep -F "syncing package.json 0.1.0 -> 0.2.0 (from cli-v0.2.0)" "$REPO4/stdout.log" >/dev/null
|
||||
grep -F "bumping version (patch)" "$REPO4/stdout.log" >/dev/null
|
||||
grep -F "new version: 0.2.1 (tag: cli-v0.2.1)" "$REPO4/stdout.log" >/dev/null
|
||||
grep -F '"version": "0.2.1"' "$REPO4/cli/package.json" >/dev/null
|
||||
git -C "$REPO4" rev-parse "cli-v0.2.1" >/dev/null \
|
||||
|| fail "local tag cli-v0.2.1 missing"
|
||||
git -C "$REPO4" log --oneline | grep -F "chore(cli): bump version to 0.2.1" >/dev/null
|
||||
git -C "$REPO4.origin.git" rev-parse "cli-v0.2.1" >/dev/null \
|
||||
|| fail "origin tag cli-v0.2.1 missing — atomic push not delivered"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 5: no cli-v* tags → fall back to package.json
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] no cli-v* tags falls back to package.json"
|
||||
REPO5="$(new_tmp)"
|
||||
init_repo "$REPO5" "0.1.0"
|
||||
status="$(run_publish "$REPO5" "minor" $'y\n')"
|
||||
[[ "$status" -eq 0 ]] || { cat "$REPO5/stderr.log" >&2; fail "expected success, got $status"; }
|
||||
grep -F "no cli-v* tags found, bumping from package.json" "$REPO5/stdout.log" >/dev/null
|
||||
grep -F "new version: 0.2.0 (tag: cli-v0.2.0)" "$REPO5/stdout.log" >/dev/null
|
||||
git -C "$REPO5.origin.git" rev-parse "cli-v0.2.0" >/dev/null \
|
||||
|| fail "origin tag cli-v0.2.0 missing"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 6: confirmation cancel → revert package.json, no commit, no tag
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] confirmation cancel reverts everything"
|
||||
REPO6="$(new_tmp)"
|
||||
init_repo "$REPO6" "0.1.0" "cli-v0.1.0"
|
||||
INITIAL_HEAD="$(git -C "$REPO6" rev-parse HEAD)"
|
||||
status="$(run_publish "$REPO6" "patch" $'n\n')"
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit on cancel"
|
||||
grep -F "release cancelled" "$REPO6/stderr.log" >/dev/null
|
||||
grep -F '"version": "0.1.0"' "$REPO6/cli/package.json" >/dev/null \
|
||||
|| fail "package.json not reverted to 0.1.0 after cancel"
|
||||
[[ "$(git -C "$REPO6" rev-parse HEAD)" == "$INITIAL_HEAD" ]] \
|
||||
|| fail "HEAD advanced after cancel — extra commit was made"
|
||||
if git -C "$REPO6" rev-parse -q --verify "refs/tags/cli-v0.1.1" >/dev/null 2>&1; then
|
||||
fail "tag cli-v0.1.1 must not exist after cancel"
|
||||
fi
|
||||
[[ -z "$(git -C "$REPO6" status --porcelain)" ]] \
|
||||
|| fail "working tree not clean after cancel — revert incomplete"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 7: happy path — atomic push delivers branch + tag together
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] happy path pushes branch and tag atomically"
|
||||
REPO7="$(new_tmp)"
|
||||
init_repo "$REPO7" "0.5.0"
|
||||
status="$(run_publish "$REPO7" "patch" $'y\n')"
|
||||
[[ "$status" -eq 0 ]] || { cat "$REPO7/stderr.log" >&2; fail "expected success, got $status"; }
|
||||
ORIGIN7="$REPO7.origin.git"
|
||||
git -C "$ORIGIN7" rev-parse "cli-v0.5.1" >/dev/null \
|
||||
|| fail "origin missing tag cli-v0.5.1"
|
||||
ORIGIN_HEAD="$(git -C "$ORIGIN7" rev-parse main)"
|
||||
LOCAL_HEAD="$(git -C "$REPO7" rev-parse main)"
|
||||
[[ "$ORIGIN_HEAD" == "$LOCAL_HEAD" ]] \
|
||||
|| fail "origin/main HEAD did not advance to match local main"
|
||||
TAG_COMMIT="$(git -C "$ORIGIN7" rev-parse "cli-v0.5.1^{commit}")"
|
||||
[[ "$TAG_COMMIT" == "$ORIGIN_HEAD" ]] \
|
||||
|| fail "origin tag cli-v0.5.1 does not point to origin/main HEAD"
|
||||
grep -F "release triggered" "$REPO7/stdout.log" >/dev/null
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 8: push failure → script exits non-zero (commit + tag stay local)
|
||||
#
|
||||
# Use a git wrapper that fails only on `git push`, so pull/fetch succeed
|
||||
# but the final push does not.
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] push failure surfaces error"
|
||||
REPO8="$(new_tmp)"
|
||||
init_repo "$REPO8" "0.6.0"
|
||||
mkdir -p "$REPO8/bin-git"
|
||||
cat >"$REPO8/bin-git/git" <<'WRAPPER'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "bun %s\\n" "\$*" >>"$REGISTRY_ERROR_CALLS"
|
||||
exit 1
|
||||
EOF
|
||||
cat >"$REGISTRY_ERROR_BIN_DIR/npm" <<EOF
|
||||
if [[ "$*" == *"push"* ]]; then
|
||||
echo "fatal: could not read from remote repository." >&2
|
||||
exit 128
|
||||
fi
|
||||
exec /usr/bin/git "$@"
|
||||
WRAPPER
|
||||
chmod +x "$REPO8/bin-git/git"
|
||||
status=0
|
||||
printf 'y\n' | env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE \
|
||||
REPO_ROOT="$REPO8" PATH="$REPO8/bin-git:$REPO8/bin:$PATH" \
|
||||
bash "$REPO8/scripts/publish-cli.sh" "patch" \
|
||||
>"$REPO8/stdout.log" 2>"$REPO8/stderr.log" || status=$?
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when push fails"
|
||||
git -C "$REPO8" rev-parse "cli-v0.6.1" >/dev/null \
|
||||
|| fail "local tag cli-v0.6.1 missing after push failure"
|
||||
git -C "$REPO8" log --oneline | grep -F "chore(cli): bump version to 0.6.1" >/dev/null \
|
||||
|| fail "local bump commit missing after push failure"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 9: unpushed detection catches "branch pushed, tag not pushed" state
|
||||
#
|
||||
# Simulate: commit is on origin/main, local tag exists but was never pushed.
|
||||
# The old `--no-merged` approach would miss this because the tagged commit is
|
||||
# already reachable from origin/main. The new ls-remote approach catches it.
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] unpushed detection catches tag-only failure"
|
||||
REPO9="$(new_tmp)"
|
||||
init_repo "$REPO9" "0.7.0"
|
||||
cd "$REPO9/cli"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '0.7.1';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
cd "$REPO9"
|
||||
git -C "$REPO9" add cli/package.json
|
||||
git -C "$REPO9" commit -q -m "chore(cli): bump version to 0.7.1"
|
||||
git -C "$REPO9" tag "cli-v0.7.1"
|
||||
git -C "$REPO9" push -q origin main
|
||||
# Tag NOT pushed — simulates atomic push partial failure recovery
|
||||
status="$(run_publish "$REPO9" "patch")"
|
||||
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when unpushed tag detected"
|
||||
grep -F "Unpushed tags" "$REPO9/stderr.log" >/dev/null \
|
||||
|| { cat "$REPO9/stderr.log" >&2; fail "expected unpushed tag warning"; }
|
||||
grep -F "cli-v0.7.1" "$REPO9/stderr.log" >/dev/null \
|
||||
|| fail "expected cli-v0.7.1 in unpushed tag warning"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Test 10: --atomic flag is actually passed to git push
|
||||
#
|
||||
# Use a git wrapper to capture the push command and verify --atomic is present.
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[test] push uses --atomic flag"
|
||||
REPO10="$(new_tmp)"
|
||||
init_repo "$REPO10" "0.8.0"
|
||||
mkdir -p "$REPO10/bin-git"
|
||||
cat >"$REPO10/bin-git/git" <<'WRAPPER'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "npm %s\\n" "\$*" >>"$REGISTRY_ERROR_CALLS"
|
||||
case "\$1" in
|
||||
view)
|
||||
echo "npm ERR! code E500" >&2
|
||||
echo "npm ERR! registry temporarily unavailable" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$REGISTRY_ERROR_BIN_DIR/bun" "$REGISTRY_ERROR_BIN_DIR/npm"
|
||||
|
||||
git -C "$REGISTRY_ERROR_DIR" init -q
|
||||
git -C "$REGISTRY_ERROR_DIR" config user.name "Test User"
|
||||
git -C "$REGISTRY_ERROR_DIR" config user.email "test@example.com"
|
||||
git -C "$REGISTRY_ERROR_DIR" add cli/.env.local cli/package.json scripts/publish-cli.sh bin/bun bin/npm
|
||||
git -C "$REGISTRY_ERROR_DIR" commit -q -m "init"
|
||||
|
||||
if REPO_ROOT="$REGISTRY_ERROR_DIR" PATH="$REGISTRY_ERROR_BIN_DIR:$PATH" bash "$REGISTRY_ERROR_SCRIPTS_DIR/publish-cli.sh" skip >"$REGISTRY_ERROR_STDOUT" 2>"$REGISTRY_ERROR_STDERR"; then
|
||||
echo "expected script to fail when registry lookup fails unexpectedly" >&2
|
||||
exit 1
|
||||
if [[ "$*" == *"push"* ]]; then
|
||||
echo "GIT_PUSH_ARGS: $*" >> "$REPO_ROOT/git-push-log.txt"
|
||||
fi
|
||||
exec /usr/bin/git "$@"
|
||||
WRAPPER
|
||||
chmod +x "$REPO10/bin-git/git"
|
||||
status=0
|
||||
printf 'y\n' | env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE \
|
||||
REPO_ROOT="$REPO10" PATH="$REPO10/bin-git:$REPO10/bin:$PATH" \
|
||||
bash "$REPO10/scripts/publish-cli.sh" "patch" \
|
||||
>"$REPO10/stdout.log" 2>"$REPO10/stderr.log" || status=$?
|
||||
[[ "$status" -eq 0 ]] || { cat "$REPO10/stderr.log" >&2; fail "expected success, got $status"; }
|
||||
grep -F -- "--atomic" "$REPO10/git-push-log.txt" >/dev/null \
|
||||
|| { cat "$REPO10/git-push-log.txt" >&2; fail "git push did not include --atomic flag"; }
|
||||
|
||||
grep -F "checking registry version" "$REGISTRY_ERROR_STDOUT"
|
||||
grep -F "failed to verify whether @astron-team/skillhub@0.3.0 exists" "$REGISTRY_ERROR_STDERR"
|
||||
grep -F "npm ERR! code E500" "$REGISTRY_ERROR_STDERR"
|
||||
if grep -Fq "npm version" "$REGISTRY_ERROR_CALLS"; then
|
||||
echo "version bump should not run when registry lookup fails" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq "bun run build" "$REGISTRY_ERROR_CALLS"; then
|
||||
echo "build should not run when registry lookup fails" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUCCESS_DIR="$(mktemp -d)"
|
||||
cleanup_success() {
|
||||
rm -rf "$SUCCESS_DIR"
|
||||
}
|
||||
SUCCESS_STDOUT="$(mktemp)"
|
||||
SUCCESS_STDERR="$(mktemp)"
|
||||
trap 'cleanup; cleanup_success; rm -f "$SUCCESS_STDOUT" "$SUCCESS_STDERR"' EXIT
|
||||
|
||||
trap 'cleanup; cleanup_conflict; cleanup_registry_error; cleanup_success; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR" "$SUCCESS_STDOUT" "$SUCCESS_STDERR"' EXIT
|
||||
|
||||
SUCCESS_CLI_DIR="$SUCCESS_DIR/cli"
|
||||
SUCCESS_SCRIPTS_DIR="$SUCCESS_DIR/scripts"
|
||||
SUCCESS_BIN_DIR="$SUCCESS_DIR/bin"
|
||||
SUCCESS_CALLS="$SUCCESS_DIR/calls.log"
|
||||
mkdir -p "$SUCCESS_CLI_DIR" "$SUCCESS_SCRIPTS_DIR" "$SUCCESS_BIN_DIR"
|
||||
cp "$PUBLISH_SCRIPT" "$SUCCESS_SCRIPTS_DIR/publish-cli.sh"
|
||||
cat >"$SUCCESS_CLI_DIR/.env.local" <<'EOF'
|
||||
NPM_TOKEN=test-token
|
||||
NPM_ORG=astron-team
|
||||
DRY_RUN=true
|
||||
EOF
|
||||
cat >"$SUCCESS_CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.1.0",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
mkdir -p "$SUCCESS_CLI_DIR/dist"
|
||||
cat >"$SUCCESS_BIN_DIR/bun" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "bun %s\\n" "\$*" >>"$SUCCESS_CALLS"
|
||||
case "\$1 \$2" in
|
||||
"run build")
|
||||
mkdir -p dist src/generated
|
||||
node - "\$PWD/package.json" "\$PWD/src/generated/pkg-info.ts" "\$PWD/dist/index.js" <<'NODE'
|
||||
const fs = require("fs")
|
||||
const pkgPath = process.argv[2]
|
||||
const generatedPath = process.argv[3]
|
||||
const distPath = process.argv[4]
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
||||
fs.writeFileSync(generatedPath, [
|
||||
"// Generated by scripts/generate-pkg-info.ts - do not edit by hand.",
|
||||
"export const PKG_NAME = " + JSON.stringify(pkg.name),
|
||||
"export const PKG_VERSION = " + JSON.stringify(pkg.version),
|
||||
""
|
||||
].join("\\n"))
|
||||
fs.writeFileSync(distPath, "#!/usr/bin/env node\\nconsole.log(\\"SkillHub CLI " + pkg.version + "\\")\\n")
|
||||
NODE
|
||||
;;
|
||||
"run test")
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
cat >"$SUCCESS_BIN_DIR/npm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "npm %s\\n" "\$*" >>"$SUCCESS_CALLS"
|
||||
case "\$1" in
|
||||
pack)
|
||||
;;
|
||||
view)
|
||||
echo "npm ERR! code E404" >&2
|
||||
echo "npm ERR! 404 Not Found" >&2
|
||||
exit 1
|
||||
;;
|
||||
version)
|
||||
node - "\$PWD/package.json" "\$2" <<'NODE'
|
||||
const fs = require("fs")
|
||||
const path = process.argv[2]
|
||||
const bump = process.argv[3]
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"))
|
||||
const parts = pkg.version.split(".").map(Number)
|
||||
if (bump === "patch") parts[2] += 1
|
||||
else if (bump === "minor") { parts[1] += 1; parts[2] = 0 }
|
||||
else if (bump === "major") { parts[0] += 1; parts[1] = 0; parts[2] = 0 }
|
||||
else throw new Error('unexpected bump: ' + bump)
|
||||
pkg.version = parts.join(".")
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n")
|
||||
NODE
|
||||
;;
|
||||
publish)
|
||||
echo "publish should not run in dry run" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$SUCCESS_BIN_DIR/bun" "$SUCCESS_BIN_DIR/npm"
|
||||
|
||||
git -C "$SUCCESS_DIR" init -q
|
||||
git -C "$SUCCESS_DIR" config user.name "Test User"
|
||||
git -C "$SUCCESS_DIR" config user.email "test@example.com"
|
||||
git -C "$SUCCESS_DIR" add cli/.env.local cli/package.json cli/dist scripts/publish-cli.sh bin/bun bin/npm
|
||||
git -C "$SUCCESS_DIR" commit -q -m "init"
|
||||
|
||||
if printf 'y\n' | REPO_ROOT="$SUCCESS_DIR" PATH="$SUCCESS_BIN_DIR:$PATH" bash "$SUCCESS_SCRIPTS_DIR/publish-cli.sh" patch >"$SUCCESS_STDOUT" 2>"$SUCCESS_STDERR"; then
|
||||
:
|
||||
else
|
||||
status=$?
|
||||
cat "$SUCCESS_STDOUT" >&2 || true
|
||||
cat "$SUCCESS_STDERR" >&2 || true
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
grep -F "running preflight build" "$SUCCESS_STDOUT"
|
||||
grep -F "running preflight tests" "$SUCCESS_STDOUT"
|
||||
grep -F "verifying built version" "$SUCCESS_STDOUT"
|
||||
grep -F "running preflight pack" "$SUCCESS_STDOUT"
|
||||
grep -F "checking registry version" "$SUCCESS_STDOUT"
|
||||
grep -F "bumping version (patch)" "$SUCCESS_STDOUT"
|
||||
grep -F "ready to publish @astron-team/skillhub@0.1.1" "$SUCCESS_STDOUT"
|
||||
grep -F "DRY_RUN=true, skipping npm publish" "$SUCCESS_STDOUT"
|
||||
grep -F "bun run build" "$SUCCESS_CALLS"
|
||||
grep -F "bun run test" "$SUCCESS_CALLS"
|
||||
grep -F "npm pack --dry-run" "$SUCCESS_CALLS"
|
||||
grep -F "npm version patch --no-git-tag-version" "$SUCCESS_CALLS"
|
||||
if grep -Fq "npm publish" "$SUCCESS_CALLS"; then
|
||||
echo "expected npm publish to be skipped in dry run" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F '"version": "0.1.1"' "$SUCCESS_CLI_DIR/package.json"
|
||||
grep -F 'export const PKG_VERSION = "0.1.1"' "$SUCCESS_CLI_DIR/src/generated/pkg-info.ts"
|
||||
node "$SUCCESS_CLI_DIR/dist/index.js" version | grep -F "SkillHub CLI 0.1.1"
|
||||
|
||||
SUCCESS_VERSION_LINE="$(grep -nF "npm version patch --no-git-tag-version" "$SUCCESS_CALLS" | cut -d: -f1)"
|
||||
SUCCESS_BUILD_LINE="$(grep -nF "bun run build" "$SUCCESS_CALLS" | cut -d: -f1)"
|
||||
if [[ "$SUCCESS_VERSION_LINE" -ge "$SUCCESS_BUILD_LINE" ]]; then
|
||||
echo "expected version bump to happen before build" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CANCEL_DIR="$(mktemp -d)"
|
||||
cleanup_cancel() {
|
||||
rm -rf "$CANCEL_DIR"
|
||||
}
|
||||
CANCEL_STDOUT="$(mktemp)"
|
||||
CANCEL_STDERR="$(mktemp)"
|
||||
trap 'cleanup; cleanup_success; cleanup_cancel; rm -f "$SUCCESS_STDOUT" "$SUCCESS_STDERR" "$CANCEL_STDOUT" "$CANCEL_STDERR"' EXIT
|
||||
|
||||
trap 'cleanup; cleanup_conflict; cleanup_registry_error; cleanup_success; cleanup_cancel; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR" "$SUCCESS_STDOUT" "$SUCCESS_STDERR" "$CANCEL_STDOUT" "$CANCEL_STDERR"' EXIT
|
||||
|
||||
CANCEL_CLI_DIR="$CANCEL_DIR/cli"
|
||||
CANCEL_SCRIPTS_DIR="$CANCEL_DIR/scripts"
|
||||
CANCEL_BIN_DIR="$CANCEL_DIR/bin"
|
||||
CANCEL_CALLS="$CANCEL_DIR/calls.log"
|
||||
mkdir -p "$CANCEL_CLI_DIR" "$CANCEL_SCRIPTS_DIR" "$CANCEL_BIN_DIR"
|
||||
cp "$PUBLISH_SCRIPT" "$CANCEL_SCRIPTS_DIR/publish-cli.sh"
|
||||
cat >"$CANCEL_CLI_DIR/.env.local" <<'EOF'
|
||||
NPM_TOKEN=test-token
|
||||
NPM_ORG=astron-team
|
||||
DRY_RUN=false
|
||||
EOF
|
||||
cat >"$CANCEL_CLI_DIR/package.json" <<'EOF'
|
||||
{
|
||||
"name": "@astron-team/skillhub",
|
||||
"version": "0.2.0",
|
||||
"bin": { "skillhub": "./dist/index.js" },
|
||||
"files": ["dist", "README.md", "LICENSE"],
|
||||
"publishConfig": { "access": "public" }
|
||||
}
|
||||
EOF
|
||||
mkdir -p "$CANCEL_CLI_DIR/dist"
|
||||
cat >"$CANCEL_BIN_DIR/bun" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "bun %s\\n" "\$*" >>"$CANCEL_CALLS"
|
||||
case "\$1 \$2" in
|
||||
"run build")
|
||||
mkdir -p dist src/generated
|
||||
node - "\$PWD/package.json" "\$PWD/src/generated/pkg-info.ts" "\$PWD/dist/index.js" <<'NODE'
|
||||
const fs = require("fs")
|
||||
const pkgPath = process.argv[2]
|
||||
const generatedPath = process.argv[3]
|
||||
const distPath = process.argv[4]
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
||||
fs.writeFileSync(generatedPath, [
|
||||
"// Generated by scripts/generate-pkg-info.ts - do not edit by hand.",
|
||||
"export const PKG_NAME = " + JSON.stringify(pkg.name),
|
||||
"export const PKG_VERSION = " + JSON.stringify(pkg.version),
|
||||
""
|
||||
].join("\\n"))
|
||||
fs.writeFileSync(distPath, "#!/usr/bin/env node\\nconsole.log(\\"SkillHub CLI " + pkg.version + "\\")\\n")
|
||||
NODE
|
||||
;;
|
||||
"run test")
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
cat >"$CANCEL_BIN_DIR/npm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "npm %s\\n" "\$*" >>"$CANCEL_CALLS"
|
||||
case "\$1" in
|
||||
pack)
|
||||
;;
|
||||
view)
|
||||
echo "npm ERR! code E404" >&2
|
||||
echo "npm ERR! 404 Not Found" >&2
|
||||
exit 1
|
||||
;;
|
||||
version)
|
||||
node - "\$PWD/package.json" "\$2" <<'NODE'
|
||||
const fs = require("fs")
|
||||
const path = process.argv[2]
|
||||
const bump = process.argv[3]
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"))
|
||||
const parts = pkg.version.split(".").map(Number)
|
||||
if (bump === "patch") parts[2] += 1
|
||||
else if (bump === "minor") { parts[1] += 1; parts[2] = 0 }
|
||||
else if (bump === "major") { parts[0] += 1; parts[1] = 0; parts[2] = 0 }
|
||||
else throw new Error('unexpected bump: ' + bump)
|
||||
pkg.version = parts.join(".")
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n")
|
||||
NODE
|
||||
;;
|
||||
publish)
|
||||
echo "npm publish should not be called after cancellation" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$CANCEL_BIN_DIR/bun" "$CANCEL_BIN_DIR/npm"
|
||||
|
||||
git -C "$CANCEL_DIR" init -q
|
||||
git -C "$CANCEL_DIR" config user.name "Test User"
|
||||
git -C "$CANCEL_DIR" config user.email "test@example.com"
|
||||
git -C "$CANCEL_DIR" add cli/.env.local cli/package.json cli/dist scripts/publish-cli.sh bin/bun bin/npm
|
||||
git -C "$CANCEL_DIR" commit -q -m "init"
|
||||
|
||||
if printf 'y\nn\n' | REPO_ROOT="$CANCEL_DIR" PATH="$CANCEL_BIN_DIR:$PATH" bash "$CANCEL_SCRIPTS_DIR/publish-cli.sh" patch >"$CANCEL_STDOUT" 2>"$CANCEL_STDERR"; then
|
||||
CANCEL_EXIT_CODE=0
|
||||
else
|
||||
CANCEL_EXIT_CODE=$?
|
||||
fi
|
||||
|
||||
if [[ "$CANCEL_EXIT_CODE" -eq 0 ]]; then
|
||||
echo "expected script to exit with non-zero when publish is cancelled" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$CANCEL_EXIT_CODE" -ne 2 ]]; then
|
||||
echo "expected exit code 2 when publish is cancelled, got $CANCEL_EXIT_CODE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F "ready to publish @astron-team/skillhub@0.2.1" "$CANCEL_STDOUT"
|
||||
grep -F "verifying built version" "$CANCEL_STDOUT"
|
||||
grep -F "publish cancelled" "$CANCEL_STDERR"
|
||||
if grep -Fq "npm publish" "$CANCEL_CALLS"; then
|
||||
echo "npm publish should not be called after cancellation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -F '"version": "0.2.1"' "$CANCEL_CLI_DIR/package.json"
|
||||
grep -F 'export const PKG_VERSION = "0.2.1"' "$CANCEL_CLI_DIR/src/generated/pkg-info.ts"
|
||||
node "$CANCEL_CLI_DIR/dist/index.js" version | grep -F "SkillHub CLI 0.2.1"
|
||||
echo "all tests passed"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue