Merge ci.yml + publish.yml + deploy.yml into pipeline.yml with sequential stages build -> publish(npm) -> deploy: - build: install/test/typecheck/pack:check (runs on PR and push) - publish: npm publish (needs build; release/tag/dispatch only) - deploy: SSH install on ubuntu@openclaw.svc.plus (needs publish) Version now flows from publish job output to deploy, removing the workflow_run cross-workflow trigger. Vault roles/secrets unchanged. Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
407 lines
15 KiB
YAML
407 lines
15 KiB
YAML
name: Pipeline
|
||
|
||
# 单一流水线,三个串联 stage:build -> publish(npm) -> deploy。
|
||
# build : 安装/测试/类型检查/包内容校验(PR 与 push 都跑)。
|
||
# publish : 发布到 npm(仅 release / 版本 tag / 手动触发;needs build)。
|
||
# deploy : SSH 安装到 ubuntu@openclaw.svc.plus(needs publish)。
|
||
on:
|
||
push:
|
||
branches:
|
||
- main
|
||
tags:
|
||
- "[0-9]+.[0-9]+.[0-9]+"
|
||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||
pull_request:
|
||
release:
|
||
types:
|
||
- published
|
||
workflow_dispatch:
|
||
inputs:
|
||
version:
|
||
description: "Plugin version to install (e.g. 2026.6.1). Leave blank to use package.json."
|
||
required: false
|
||
default: ""
|
||
force:
|
||
description: "Reinstall even if the same version is already installed."
|
||
required: false
|
||
default: "false"
|
||
type: choice
|
||
options:
|
||
- "false"
|
||
- "true"
|
||
|
||
env:
|
||
VAULT_ADDR: https://vault.svc.plus
|
||
|
||
concurrency:
|
||
group: pipeline-${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
jobs:
|
||
# ───────────────────────── Stage 1: build ─────────────────────────
|
||
build:
|
||
name: Build
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||
|
||
- name: Setup Node
|
||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||
with:
|
||
node-version: 22
|
||
|
||
- name: Setup pnpm
|
||
run: |
|
||
corepack enable
|
||
corepack prepare pnpm@10.28.2 --activate
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Test
|
||
run: pnpm test
|
||
|
||
- name: Typecheck
|
||
run: pnpm typecheck
|
||
|
||
- name: Verify npm package contents
|
||
run: pnpm pack:check
|
||
|
||
# ──────────────────────── Stage 2: publish ────────────────────────
|
||
publish:
|
||
name: Publish to npm
|
||
needs: build
|
||
if: >-
|
||
github.event_name == 'release' ||
|
||
github.event_name == 'workflow_dispatch' ||
|
||
startsWith(github.ref, 'refs/tags/')
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
outputs:
|
||
version: ${{ steps.meta.outputs.version }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||
|
||
- name: Load Vault secrets
|
||
id: vault
|
||
uses: hashicorp/vault-action@v2
|
||
with:
|
||
url: ${{ env.VAULT_ADDR }}
|
||
method: jwt
|
||
role: github-actions-openclaw-multi-session-plugins
|
||
jwtGithubAudience: vault
|
||
secrets: |
|
||
kv/data/github-actions/openclaw-multi-session-plugins NPM_TOKEN | NPM_TOKEN
|
||
|
||
- name: Setup Node
|
||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||
with:
|
||
node-version: 22
|
||
registry-url: https://registry.npmjs.org/
|
||
|
||
- name: Setup pnpm
|
||
run: |
|
||
corepack enable
|
||
corepack prepare pnpm@10.28.2 --activate
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Resolve package metadata
|
||
id: meta
|
||
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Verify npm publish access
|
||
shell: bash
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
name="$(node -p "require('./package.json').name")"
|
||
version="$(node -p "require('./package.json').version")"
|
||
user="$(npm whoami 2>/dev/null || true)"
|
||
if [ -z "${user}" ]; then
|
||
echo "::error::NPM_TOKEN is not valid for npm publish. Create an npm automation token for an account that can publish ${name}, then store it in Vault as NPM_TOKEN."
|
||
exit 1
|
||
fi
|
||
if npm view "${name}" name >/dev/null 2>&1; then
|
||
echo "::notice::Publishing ${name}@${version} as npm user ${user}; package already exists."
|
||
else
|
||
echo "::notice::Publishing ${name}@${version} as npm user ${user}; npm will create this public package on first publish."
|
||
fi
|
||
|
||
- name: Check published version
|
||
id: published
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
name="$(node -p "require('./package.json').name")"
|
||
version="$(node -p "require('./package.json').version")"
|
||
if npm view "${name}@${version}" version >/dev/null 2>&1; then
|
||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||
echo "${name}@${version} is already published; skipping npm publish."
|
||
else
|
||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- name: Publish
|
||
if: steps.published.outputs.exists != 'true'
|
||
run: npm publish --provenance --access public
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ steps.vault.outputs.NPM_TOKEN }}
|
||
|
||
# ───────────────────────── Stage 3: deploy ────────────────────────
|
||
deploy:
|
||
name: Update plugin on ubuntu@openclaw.svc.plus
|
||
needs: publish
|
||
runs-on: ubuntu-latest
|
||
concurrency:
|
||
group: openclaw-deploy
|
||
cancel-in-progress: false
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
env:
|
||
SSH_HOST: ubuntu@openclaw.svc.plus
|
||
PLUGIN_NAME: openclaw-multi-session-plugins
|
||
steps:
|
||
- name: Checkout source
|
||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||
|
||
- name: Load Vault secrets
|
||
id: vault
|
||
uses: hashicorp/vault-action@v2
|
||
with:
|
||
url: ${{ env.VAULT_ADDR }}
|
||
method: jwt
|
||
role: github-actions-openclaw-multi-session-plugins
|
||
jwtGithubAudience: vault
|
||
secrets: |
|
||
kv/data/github-actions/openclaw-multi-session-plugins OPENCLAW_SSH_KEY | OPENCLAW_SSH_KEY ;
|
||
kv/data/github-actions/openclaw-multi-session-plugins OPENCLAW_SSH_KEY_B64 | OPENCLAW_SSH_KEY_B64 ;
|
||
kv/data/github-actions/openclaw-multi-session-plugins SINGLE_NODE_VPS_SSH_PRIVATE_KEY | SINGLE_NODE_VPS_SSH_PRIVATE_KEY ;
|
||
kv/data/github-actions/openclaw-multi-session-plugins SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64 | SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64
|
||
|
||
- name: Resolve target version
|
||
id: version
|
||
env:
|
||
INPUT_VERSION: ${{ inputs.version }}
|
||
PUBLISH_VERSION: ${{ needs.publish.outputs.version }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${INPUT_VERSION}" ]; then
|
||
value="${INPUT_VERSION}"
|
||
elif [ -n "${PUBLISH_VERSION}" ]; then
|
||
value="${PUBLISH_VERSION}"
|
||
else
|
||
value="$(node -p "require('./package.json').version")"
|
||
fi
|
||
value="${value##*/}"
|
||
value="${value#v}"
|
||
if ! [[ "${value}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo "::error::Resolved value '${value}' is not a valid X.Y.Z version"
|
||
exit 1
|
||
fi
|
||
echo "value=${value}" >> "$GITHUB_OUTPUT"
|
||
echo "Resolved plugin version: ${value}"
|
||
|
||
- name: Resolve install source
|
||
id: install
|
||
env:
|
||
VERSION: ${{ steps.version.outputs.value }}
|
||
FORCE: ${{ inputs.force || 'false' }}
|
||
run: |
|
||
set -euo pipefail
|
||
PACKAGE="${PLUGIN_NAME}@${VERSION}"
|
||
if [ "${FORCE}" != "true" ] && npm view "${PACKAGE}" version >/dev/null 2>&1; then
|
||
PUBLISHED="$(npm view "${PACKAGE}" version)"
|
||
echo "::notice::${PLUGIN_NAME}@${PUBLISHED} is available on npm"
|
||
echo "source=npm" >> "$GITHUB_OUTPUT"
|
||
echo "install_spec=${PACKAGE}" >> "$GITHUB_OUTPUT"
|
||
else
|
||
install_spec="/tmp/${PLUGIN_NAME}-${VERSION}-${GITHUB_SHA}.tgz"
|
||
echo "::warning::Building and installing ${install_spec} from the checked-out source"
|
||
echo "source=archive" >> "$GITHUB_OUTPUT"
|
||
echo "install_spec=${install_spec}" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- name: Setup Node
|
||
if: steps.install.outputs.source == 'archive'
|
||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||
with:
|
||
node-version: 22
|
||
|
||
- name: Setup pnpm
|
||
if: steps.install.outputs.source == 'archive'
|
||
run: |
|
||
corepack enable
|
||
corepack prepare pnpm@10.28.2 --activate
|
||
|
||
- name: Build archive install source
|
||
id: archive
|
||
if: steps.install.outputs.source == 'archive'
|
||
env:
|
||
VERSION: ${{ steps.version.outputs.value }}
|
||
run: |
|
||
set -euo pipefail
|
||
pnpm install --frozen-lockfile
|
||
pnpm pack
|
||
tarball="${PLUGIN_NAME}-${VERSION}.tgz"
|
||
test -f "${tarball}"
|
||
echo "tarball=${tarball}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Configure SSH key
|
||
env:
|
||
OPENCLAW_SSH_KEY: ${{ steps.vault.outputs.OPENCLAW_SSH_KEY }}
|
||
OPENCLAW_SSH_KEY_B64: ${{ steps.vault.outputs.OPENCLAW_SSH_KEY_B64 }}
|
||
SINGLE_NODE_VPS_SSH_PRIVATE_KEY: ${{ steps.vault.outputs.SINGLE_NODE_VPS_SSH_PRIVATE_KEY }}
|
||
SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64: ${{ steps.vault.outputs.SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64 }}
|
||
run: |
|
||
set -euo pipefail
|
||
SSH_KEY=""
|
||
if [ -n "${OPENCLAW_SSH_KEY_B64:-}" ]; then
|
||
SSH_KEY="$(printf '%s' "${OPENCLAW_SSH_KEY_B64}" | base64 -d)"
|
||
elif [ -n "${OPENCLAW_SSH_KEY:-}" ]; then
|
||
SSH_KEY="${OPENCLAW_SSH_KEY}"
|
||
fi
|
||
if [ -z "${SSH_KEY}" ] && [ -n "${SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64:-}" ]; then
|
||
SSH_KEY="$(printf '%s' "${SINGLE_NODE_VPS_SSH_PRIVATE_KEY_B64}" | base64 -d)"
|
||
elif [ -z "${SSH_KEY}" ] && [ -n "${SINGLE_NODE_VPS_SSH_PRIVATE_KEY:-}" ]; then
|
||
SSH_KEY="${SINGLE_NODE_VPS_SSH_PRIVATE_KEY}"
|
||
fi
|
||
|
||
if [ -z "${SSH_KEY}" ]; then
|
||
echo "::error::Neither OPENCLAW_SSH_KEY nor SINGLE_NODE_VPS_SSH_PRIVATE_KEY is set."
|
||
exit 1
|
||
fi
|
||
install -m 700 -d ~/.ssh
|
||
printf '%s\n' "${SSH_KEY}" > ~/.ssh/openclaw_ed25519
|
||
chmod 600 ~/.ssh/openclaw_ed25519
|
||
ssh-keyscan -H openclaw.svc.plus >> ~/.ssh/known_hosts 2>/dev/null || true
|
||
|
||
- name: Verify SSH connectivity
|
||
run: |
|
||
ssh -i ~/.ssh/openclaw_ed25519 -o BatchMode=yes -o ConnectTimeout=10 \
|
||
"${SSH_HOST}" 'echo "connected to $(hostname) as $(whoami)"'
|
||
|
||
- name: Upload archive install source
|
||
if: steps.install.outputs.source == 'archive'
|
||
run: |
|
||
scp -i ~/.ssh/openclaw_ed25519 -o BatchMode=yes \
|
||
"${{ steps.archive.outputs.tarball }}" "${SSH_HOST}:${{ steps.install.outputs.install_spec }}"
|
||
|
||
- name: Install or update plugin on remote host
|
||
env:
|
||
VERSION: ${{ steps.version.outputs.value }}
|
||
INSTALL_SPEC: ${{ steps.install.outputs.install_spec }}
|
||
INSTALL_SOURCE: ${{ steps.install.outputs.source }}
|
||
FORCE: ${{ inputs.force || 'false' }}
|
||
run: |
|
||
ssh -i ~/.ssh/openclaw_ed25519 -o BatchMode=yes -o ServerAliveInterval=30 \
|
||
"${SSH_HOST}" bash -s -- "${PLUGIN_NAME}" "${VERSION}" "${INSTALL_SPEC}" "${INSTALL_SOURCE}" "${FORCE}" <<'REMOTE'
|
||
set -euo pipefail
|
||
PLUGIN_NAME="$1"
|
||
VERSION="$2"
|
||
INSTALL_SPEC="$3"
|
||
INSTALL_SOURCE="$4"
|
||
FORCE="$5"
|
||
PACKAGE="${PLUGIN_NAME}@${VERSION}"
|
||
STATE_DIR="/tmp/openclaw-deploy"
|
||
mkdir -p "${STATE_DIR}"
|
||
|
||
echo "==> Installing ${PACKAGE} from ${INSTALL_SOURCE} on $(hostname) (force=${FORCE})"
|
||
echo "==> Install spec: ${INSTALL_SPEC}"
|
||
|
||
# Record the previously installed version for rollback.
|
||
PREVIOUS_VERSION=""
|
||
if command -v openclaw >/dev/null 2>&1; then
|
||
PREVIOUS_VERSION="$(npm ls -g "${PLUGIN_NAME}" --depth=0 2>/dev/null \
|
||
| awk -F'[@:]' '/'"${PLUGIN_NAME}"'@/ {print $2; exit}' || true)"
|
||
fi
|
||
echo "==> Previously installed version: ${PREVIOUS_VERSION:-<none>}"
|
||
|
||
# Skip when the requested version is already present unless forced.
|
||
if [ "${FORCE}" != "true" ] && [ "${PREVIOUS_VERSION}" = "${VERSION}" ]; then
|
||
echo "==> ${PACKAGE} already installed and force=false; nothing to do"
|
||
exit 0
|
||
fi
|
||
|
||
printf '%s\n' "${PREVIOUS_VERSION}" > "${STATE_DIR}/previous-version"
|
||
|
||
rollback() {
|
||
local rc=$?
|
||
echo "::remote-error::Install failed (exit ${rc}); attempting rollback"
|
||
local prev
|
||
prev="$(cat "${STATE_DIR}/previous-version" 2>/dev/null || true)"
|
||
if [ -n "${prev}" ] && [ "${prev}" != "${VERSION}" ]; then
|
||
echo "::remote-warning::Reinstalling ${PLUGIN_NAME}@${prev}"
|
||
npm install -g "${PLUGIN_NAME}@${prev}" || true
|
||
if command -v openclaw >/dev/null 2>&1; then
|
||
openclaw plugins enable "${PLUGIN_NAME}" || true
|
||
fi
|
||
else
|
||
echo "::remote-warning::No previous version recorded; leaving host as-is"
|
||
fi
|
||
exit "${rc}"
|
||
}
|
||
trap rollback ERR
|
||
|
||
install_plugin() {
|
||
if command -v openclaw >/dev/null 2>&1; then
|
||
openclaw plugins install --force "${INSTALL_SPEC}" \
|
||
|| openclaw plugins install "${INSTALL_SPEC}" \
|
||
|| openclaw plugins update "${INSTALL_SPEC}" \
|
||
|| npm install -g "${INSTALL_SPEC}"
|
||
else
|
||
npm install -g "${INSTALL_SPEC}"
|
||
fi
|
||
}
|
||
|
||
install_plugin
|
||
|
||
if command -v openclaw >/dev/null 2>&1; then
|
||
openclaw plugins enable "${PLUGIN_NAME}" || true
|
||
systemctl --user restart openclaw-gateway.service || true
|
||
fi
|
||
|
||
# Verify the installed version matches the requested version.
|
||
GLOBAL_ROOT="$(npm root -g)"
|
||
INSTALLED=""
|
||
if [ -f "${GLOBAL_ROOT}/${PLUGIN_NAME}/package.json" ]; then
|
||
INSTALLED="$(node -p "require('${GLOBAL_ROOT}/${PLUGIN_NAME}/package.json').version")"
|
||
fi
|
||
if [ "${INSTALLED}" != "${VERSION}" ]; then
|
||
echo "::remote-error::Verification failed: expected ${VERSION}, found ${INSTALLED:-<none>}"
|
||
exit 1
|
||
fi
|
||
|
||
trap - ERR
|
||
rm -f "${STATE_DIR}/previous-version"
|
||
|
||
echo "==> Installed plugin state:"
|
||
if command -v openclaw >/dev/null 2>&1; then
|
||
openclaw plugins info "${PLUGIN_NAME}" || true
|
||
fi
|
||
npm ls -g "${PLUGIN_NAME}" || true
|
||
echo "==> ${PACKAGE} is now active on $(hostname)"
|
||
REMOTE
|
||
|
||
- name: Summarize deploy
|
||
if: always()
|
||
env:
|
||
VERSION: ${{ steps.version.outputs.value }}
|
||
run: |
|
||
if [ "${{ job.status }}" = "success" ]; then
|
||
echo "::notice::openclaw-multi-session-plugins@${VERSION} deployed to ubuntu@openclaw.svc.plus"
|
||
else
|
||
echo "::error::Deploy to ubuntu@openclaw.svc.plus failed for openclaw-multi-session-plugins@${VERSION}"
|
||
exit 1
|
||
fi
|