diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bcbadd9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,119 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to package, for example v0.4.0' + required: true + type: string + +permissions: + contents: write + +jobs: + package: + name: Package ${{ matrix.os }} / ${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + arch: x64 + - os: macos-latest + platform: macos + arch: arm64 + - os: windows-latest + platform: windows + arch: x64 + runs-on: ${{ matrix.os }} + env: + WASM_PACK_VERSION: 0.12.1 + PACKAGE_NAME: source_map_parser_node + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + - name: Set up Rust + shell: bash + run: | + rustup toolchain install stable --profile minimal + rustup default stable + rustup target add wasm32-unknown-unknown + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Install wasm-pack + shell: bash + run: | + if ! command -v wasm-pack >/dev/null 2>&1; then + cargo install wasm-pack --version "$WASM_PACK_VERSION" --locked + fi + - name: Install Node dependencies + working-directory: crates/node_sdk + run: pnpm install --no-frozen-lockfile + - name: Run Rust tests + run: cargo test --workspace --exclude source_map_parser_node --all-features + - name: Run WASM tests + run: wasm-pack test --node crates/node_sdk + - name: Build Node/WASM package + working-directory: crates/node_sdk + run: pnpm run build + - name: Run Node SDK tests + working-directory: crates/node_sdk + run: pnpm test + - name: Stage release package + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then TAG="${{ github.event.inputs.tag }}"; fi + STAGE="release/${PACKAGE_NAME}-${{ matrix.platform }}-${{ matrix.arch }}" + mkdir -p "$STAGE/bin" "$STAGE/dist" "$STAGE/skills" + cp -R crates/node_sdk/dist/. "$STAGE/dist/" + cp -R crates/node_sdk/bin/. "$STAGE/bin/" + cp -R crates/node_sdk/skills/. "$STAGE/skills/" + cp crates/node_sdk/README.md "$STAGE/README.md" + cp LICENSE "$STAGE/LICENSE" + cp crates/node_sdk/package.json "$STAGE/package.json" + printf '%s\n' "$TAG" > "$STAGE/VERSION" + ARCHIVE="release/${PACKAGE_NAME}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz" + tar -czf "$ARCHIVE" -C release "${PACKAGE_NAME}-${{ matrix.platform }}-${{ matrix.arch }}" + if command -v sha256sum >/dev/null 2>&1; then + (cd release && sha256sum "$(basename "$ARCHIVE")" > "$(basename "$ARCHIVE").sha256") + else + shasum -a 256 "$ARCHIVE" | sed "s#release/##" > "$ARCHIVE.sha256" + fi + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PACKAGE_NAME }}-${{ matrix.platform }}-${{ matrix.arch }} + path: | + release/${{ env.PACKAGE_NAME }}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz + release/${{ env.PACKAGE_NAME }}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz.sha256 + + publish-github-release: + needs: package + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: release-artifacts + merge-multiple: true + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + files: | + release-artifacts/*.tar.gz + release-artifacts/*.sha256 + generate_release_notes: true diff --git a/crates/node_sdk/README.md b/crates/node_sdk/README.md index c8a623c..f26995a 100644 --- a/crates/node_sdk/README.md +++ b/crates/node_sdk/README.md @@ -191,3 +191,37 @@ pnpm test # 运行 vitest 测试 ## 许可证 MIT License + +## CLI 安装与更新 + +从 npm 全局安装后会提供 `source-map-parser` 命令,并内置跨平台更新命令: + +```bash +npm install -g source_map_parser_node +source-map-parser update +# 安装指定版本 +source-map-parser update --version 0.4.0 +``` + +如果你不想依赖 npm,也可以在 Linux/macOS 或带 bash 的 Windows 环境中使用随 Node 包一起分发的 GitHub Release 安装脚本。脚本会自动识别当前系统与 CPU 架构,从 tag 对应的 Release 资产中下载匹配的包: + +```bash +# 本地依赖安装后使用 +bash ./node_modules/source_map_parser_node/bin/install.sh + +# 全局 npm 安装后使用 +bash "$(npm root -g)/source_map_parser_node/bin/install.sh" --version v0.4.0 +``` + +CLI 也可以通过 GitHub Release 方式自更新: + +```bash +source-map-parser update --from github +source-map-parser update --from github --version v0.4.0 +``` + +> Windows 用户推荐使用 npm 更新;GitHub 安装脚本需要 Git Bash、MSYS2 或 WSL 等 bash 环境。 + +## Tag Release 流程 + +推送 `v*.*.*` tag 后,GitHub Actions 会在 Linux、macOS、Windows 三类环境中构建并测试 Node/WASM 包,然后把按平台命名的 `source_map_parser_node--.tar.gz` 上传到对应 GitHub Release。Release 包和 npm 包都会携带同一份 `bin/install.sh`,用户可以通过该脚本或 `source-map-parser update --from github` 下载使用。 diff --git a/crates/node_sdk/bin/install.sh b/crates/node_sdk/bin/install.sh new file mode 100755 index 0000000..b2d255d --- /dev/null +++ b/crates/node_sdk/bin/install.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="MasonChow/source-map-parser" +PACKAGE_NAME="source_map_parser_node" +VERSION="latest" +INSTALL_DIR="${SOURCE_MAP_PARSER_INSTALL_DIR:-$HOME/.source-map-parser}" +BIN_DIR="${SOURCE_MAP_PARSER_BIN_DIR:-$HOME/.local/bin}" + +usage() { + cat < Release tag to install. Defaults to latest. + --install-dir Package installation directory. Defaults to ~/.source-map-parser. + --bin-dir Directory for the source-map-parser shim. Defaults to ~/.local/bin. + -h, --help Show this help. +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) VERSION="${2:?missing value for --version}"; shift 2 ;; + --install-dir) INSTALL_DIR="${2:?missing value for --install-dir}"; shift 2 ;; + --bin-dir) BIN_DIR="${2:?missing value for --bin-dir}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage; exit 1 ;; + esac +done + +need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; } +need curl +need tar +need node + +case "$(uname -s)" in + Linux*) OS="linux" ;; + Darwin*) OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) OS="windows" ;; + *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;; +esac +case "$(uname -m)" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) ARCH="$(uname -m)" ;; +esac + +if [[ "$VERSION" == "latest" ]]; then + API_URL="https://api.github.com/repos/${REPO}/releases/latest" +else + API_URL="https://api.github.com/repos/${REPO}/releases/tags/${VERSION}" +fi + +ASSET="${PACKAGE_NAME}-${OS}-${ARCH}.tar.gz" +DOWNLOAD_URL="$(curl -fsSL "$API_URL" | ASSET="$ASSET" node -e ' +let data = ""; +process.stdin.on("data", (chunk) => { data += chunk; }); +process.stdin.on("end", () => { + const release = JSON.parse(data); + const asset = (release.assets || []).find((item) => item.name === process.env.ASSET); + if (!asset) process.exit(2); + console.log(asset.browser_download_url); +}); +')" || { + echo "Could not find release asset ${ASSET}." >&2 + exit 1 +} +CHECKSUM_URL="${DOWNLOAD_URL}.sha256" + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT +curl -fL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET" +if curl -fsL "$CHECKSUM_URL" -o "$TMP_DIR/$ASSET.sha256"; then + if command -v sha256sum >/dev/null 2>&1; then + (cd "$TMP_DIR" && sha256sum -c "$ASSET.sha256") + else + (cd "$TMP_DIR" && shasum -a 256 -c "$ASSET.sha256") + fi +else + echo "Checksum file not found; continuing without checksum verification." >&2 +fi +mkdir -p "$INSTALL_DIR" "$BIN_DIR" +tar -xzf "$TMP_DIR/$ASSET" -C "$INSTALL_DIR" --strip-components=1 +cat > "$BIN_DIR/source-map-parser" < "$BIN_DIR/source-map-parser.cmd" <] [--from npm|github] + source-map-parser --help + +Commands: + update Update the global source_map_parser_node CLI/package. + +Options: + --version Install a specific npm version or GitHub tag. Defaults to latest. + --from Update through npm or the GitHub install script. Defaults to npm. +`); +} + +function readOptionValue(argv, index, optionName) { + const value = argv[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for ${optionName}`); + } + return value; +} + +function parseArgs(argv) { + const args = { command: argv[2], version: 'latest', from: 'npm', help: false }; + for (let i = 3; i < argv.length; i += 1) { + const value = argv[i]; + if (value === '--version') { + args.version = readOptionValue(argv, i, '--version'); + i += 1; + } else if (value === '--from') { + args.from = readOptionValue(argv, i, '--from'); + i += 1; + } else if (value === '--help' || value === '-h') { + args.help = true; + } else { + throw new Error(`Unknown argument: ${value}`); + } + } + return args; +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { stdio: 'inherit', ...options }); + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function updateFromNpm(version) { + const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const spec = version === 'latest' ? PACKAGE_NAME : `${PACKAGE_NAME}@${version}`; + run(npmCommand, ['install', '--global', spec]); +} + +function updateFromGitHub(version) { + if (process.platform === 'win32') { + console.error('GitHub install script update requires bash. Use: source-map-parser update --from npm'); + process.exit(1); + } + run('bash', [INSTALL_SCRIPT_PATH, '--version', version]); +} + +try { + const args = parseArgs(process.argv); + if (!args.command || args.help || args.command === '--help' || args.command === '-h') { + printHelp(); + process.exit(0); + } + if (args.command !== 'update') throw new Error(`Unknown command: ${args.command}`); + if (!['npm', 'github'].includes(args.from)) throw new Error('--from must be npm or github'); + if (args.from === 'github') updateFromGitHub(args.version); + else updateFromNpm(args.version); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} diff --git a/crates/node_sdk/package.json b/crates/node_sdk/package.json index 6527f77..e769d00 100644 --- a/crates/node_sdk/package.json +++ b/crates/node_sdk/package.json @@ -9,6 +9,7 @@ "type": "module", "files": [ "dist/", + "bin/", "skills/", "README.md", "LICENSE" @@ -36,5 +37,8 @@ "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", "vitest": "^2.0.5" + }, + "bin": { + "source-map-parser": "./bin/source-map-parser.mjs" } -} \ No newline at end of file +}