mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
ci: add package acceptance workflow
This commit is contained in:
@@ -142,6 +142,41 @@ image. Release-path normal mode remains max three Docker chunk jobs:
|
||||
- `package-update`
|
||||
- `plugins-integrations`
|
||||
|
||||
## Package Acceptance
|
||||
|
||||
Use the manual `Package Acceptance` workflow when the question is "does this
|
||||
installable package work as a product?" rather than "does this source diff pass
|
||||
Vitest?"
|
||||
|
||||
Good defaults:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml --ref main \
|
||||
-f source=npm \
|
||||
-f package_spec=openclaw@beta \
|
||||
-f suite_profile=product
|
||||
```
|
||||
|
||||
Profiles:
|
||||
|
||||
- `smoke`: quick package install/channel/agent + gateway/config lanes.
|
||||
- `package`: package, update, and plugin lanes; no OpenWebUI.
|
||||
- `product`: package profile plus MCP channels, cron/subagent cleanup, OpenAI
|
||||
web search, and OpenWebUI.
|
||||
- `full`: Docker release-path chunks with OpenWebUI.
|
||||
- `custom`: exact `docker_lanes` list for a focused rerun.
|
||||
|
||||
Candidate sources:
|
||||
|
||||
- `source=npm`: `openclaw@beta`, `openclaw@latest`, or an exact release version.
|
||||
- `source=ref`: pack the trusted ref in the workflow.
|
||||
- `source=url`: HTTPS `.tgz` plus required `package_sha256`.
|
||||
- `source=artifact`: download one `.tgz` from `artifact_run_id`/`artifact_name`.
|
||||
|
||||
Use `telegram_mode=mock-openai` or `telegram_mode=live-frontier` only with
|
||||
`source=npm`; that path reuses the published npm Telegram E2E workflow and the
|
||||
`qa-live-shared` environment.
|
||||
|
||||
Docker E2E images never copy repo sources as the app under test: the bare image
|
||||
is a Node/Git runner, and the functional image installs the same prebuilt npm
|
||||
tarball that bare lanes mount. `scripts/package-openclaw-for-docker.mjs` is the
|
||||
|
||||
6
.github/actions/docker-e2e-plan/action.yml
vendored
6
.github/actions/docker-e2e-plan/action.yml
vendored
@@ -26,6 +26,10 @@ inputs:
|
||||
description: Whether to download/pull artifacts required by the plan.
|
||||
required: false
|
||||
default: "true"
|
||||
package-artifact-name:
|
||||
description: Workflow artifact name containing openclaw-current.tgz.
|
||||
required: false
|
||||
default: docker-e2e-package
|
||||
outputs:
|
||||
credentials:
|
||||
description: Comma-separated credential groups required by selected lanes.
|
||||
@@ -108,7 +112,7 @@ runs:
|
||||
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: docker-e2e-package
|
||||
name: ${{ inputs.package-artifact-name }}
|
||||
path: .artifacts/docker-e2e-package
|
||||
|
||||
- name: Pull shared bare Docker E2E image
|
||||
|
||||
30
.github/workflows/npm-telegram-beta-e2e.yml
vendored
30
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -20,6 +20,29 @@ on:
|
||||
description: Optional comma-separated Telegram scenario ids
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
package_spec:
|
||||
description: Published OpenClaw package spec to test
|
||||
required: true
|
||||
type: string
|
||||
provider_mode:
|
||||
description: QA provider mode
|
||||
required: false
|
||||
default: mock-openai
|
||||
type: string
|
||||
scenario:
|
||||
description: Optional comma-separated Telegram scenario ids
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
secrets:
|
||||
OPENAI_API_KEY:
|
||||
required: false
|
||||
OPENCLAW_QA_CONVEX_SITE_URL:
|
||||
required: false
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -90,6 +113,13 @@ jobs:
|
||||
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "${PROVIDER_MODE}" in
|
||||
mock-openai | live-frontier) ;;
|
||||
*)
|
||||
echo "provider_mode must be mock-openai or live-frontier; got: ${PROVIDER_MODE}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
require_var() {
|
||||
local key="$1"
|
||||
|
||||
@@ -28,6 +28,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Existing workflow artifact containing openclaw-current.tgz; blank packs the selected ref
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
include_live_suites:
|
||||
description: Whether to run live-provider coverage
|
||||
required: false
|
||||
@@ -69,6 +74,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Existing workflow artifact containing openclaw-current.tgz; blank packs the selected ref
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
include_live_suites:
|
||||
description: Whether to run live-provider coverage
|
||||
required: false
|
||||
@@ -477,6 +487,7 @@ jobs:
|
||||
mode: chunk
|
||||
chunk: ${{ matrix.chunk_id }}
|
||||
include-openwebui: ${{ inputs.include_openwebui }}
|
||||
package-artifact-name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
|
||||
|
||||
- name: Run Docker E2E chunk
|
||||
shell: bash
|
||||
@@ -603,6 +614,7 @@ jobs:
|
||||
mode: targeted
|
||||
lanes: ${{ inputs.docker_lanes }}
|
||||
include-openwebui: ${{ inputs.include_openwebui }}
|
||||
package-artifact-name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
|
||||
|
||||
- name: Run targeted Docker E2E lanes
|
||||
shell: bash
|
||||
@@ -713,23 +725,6 @@ jobs:
|
||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Resolve shared Docker E2E image tags
|
||||
id: image
|
||||
shell: bash
|
||||
env:
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repository="${GITHUB_REPOSITORY,,}"
|
||||
bare_image="ghcr.io/${repository}-docker-e2e-bare:${SELECTED_SHA}"
|
||||
functional_image="ghcr.io/${repository}-docker-e2e-functional:${SELECTED_SHA}"
|
||||
image="$functional_image"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
echo "bare_image=$bare_image" >> "$GITHUB_OUTPUT"
|
||||
echo "functional_image=$functional_image" >> "$GITHUB_OUTPUT"
|
||||
echo "Shared Docker E2E bare image: \`$bare_image\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Shared Docker E2E functional image: \`$functional_image\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Plan Docker E2E images
|
||||
id: plan
|
||||
uses: ./.github/actions/docker-e2e-plan
|
||||
@@ -741,15 +736,22 @@ jobs:
|
||||
hydrate-artifacts: "false"
|
||||
|
||||
- name: Setup Node environment
|
||||
if: steps.plan.outputs.needs_package == '1'
|
||||
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == ''
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Download provided OpenClaw Docker E2E package
|
||||
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name != ''
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.package_artifact_name }}
|
||||
path: .artifacts/docker-e2e-package
|
||||
|
||||
- name: Pack OpenClaw package for Docker E2E
|
||||
if: steps.plan.outputs.needs_package == '1'
|
||||
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -758,14 +760,60 @@ jobs:
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz
|
||||
|
||||
- name: Upload OpenClaw Docker E2E package
|
||||
- name: Validate OpenClaw Docker E2E package
|
||||
id: package
|
||||
if: steps.plan.outputs.needs_package == '1'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .artifacts/docker-e2e-package
|
||||
target=".artifacts/docker-e2e-package/openclaw-current.tgz"
|
||||
if [[ ! -f "$target" ]]; then
|
||||
mapfile -t tgzs < <(find .artifacts/docker-e2e-package -type f -name '*.tgz' | sort)
|
||||
if [[ "${#tgzs[@]}" -ne 1 ]]; then
|
||||
echo "Expected exactly one package tarball in .artifacts/docker-e2e-package; found ${#tgzs[@]}." >&2
|
||||
printf '%s\n' "${tgzs[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "${tgzs[0]}" "$target"
|
||||
fi
|
||||
node scripts/check-openclaw-package-tarball.mjs "$target"
|
||||
digest="$(sha256sum "$target" | awk '{print $1}')"
|
||||
tag="pkg-${digest:0:32}"
|
||||
echo "sha256=$digest" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "Docker E2E package: \`$target\`"
|
||||
echo "Docker E2E package SHA-256: \`$digest\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload OpenClaw Docker E2E package
|
||||
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: docker-e2e-package
|
||||
path: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Resolve shared Docker E2E image tags
|
||||
id: image
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_TAG: ${{ steps.package.outputs.tag }}
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repository="${GITHUB_REPOSITORY,,}"
|
||||
image_tag="${PACKAGE_TAG:-$SELECTED_SHA}"
|
||||
bare_image="ghcr.io/${repository}-docker-e2e-bare:${image_tag}"
|
||||
functional_image="ghcr.io/${repository}-docker-e2e-functional:${image_tag}"
|
||||
image="$functional_image"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
echo "bare_image=$bare_image" >> "$GITHUB_OUTPUT"
|
||||
echo "functional_image=$functional_image" >> "$GITHUB_OUTPUT"
|
||||
echo "Shared Docker E2E bare image: \`$bare_image\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Shared Docker E2E functional image: \`$functional_image\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: steps.plan.outputs.needs_e2e_image == '1'
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
|
||||
309
.github/workflows/package-acceptance.yml
vendored
Normal file
309
.github/workflows/package-acceptance.yml
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
name: Package Acceptance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source:
|
||||
description: Package candidate source
|
||||
required: true
|
||||
default: npm
|
||||
type: choice
|
||||
options:
|
||||
- npm
|
||||
- ref
|
||||
- url
|
||||
- artifact
|
||||
ref:
|
||||
description: Trusted repo ref for workflow scripts, or package source when source=ref
|
||||
required: true
|
||||
default: main
|
||||
type: string
|
||||
package_spec:
|
||||
description: Published package spec when source=npm
|
||||
required: false
|
||||
default: openclaw@beta
|
||||
type: string
|
||||
package_url:
|
||||
description: HTTPS .tgz URL when source=url
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_sha256:
|
||||
description: Expected package SHA-256; required for source=url
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
artifact_run_id:
|
||||
description: GitHub Actions run id when source=artifact
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
artifact_name:
|
||||
description: Artifact name containing one .tgz when source=artifact
|
||||
required: false
|
||||
default: package-under-test
|
||||
type: string
|
||||
suite_profile:
|
||||
description: Acceptance profile
|
||||
required: true
|
||||
default: package
|
||||
type: choice
|
||||
options:
|
||||
- smoke
|
||||
- package
|
||||
- product
|
||||
- full
|
||||
- custom
|
||||
docker_lanes:
|
||||
description: Comma/space separated Docker lanes when suite_profile=custom
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
telegram_mode:
|
||||
description: Optional published-npm Telegram QA lane
|
||||
required: true
|
||||
default: none
|
||||
type: choice
|
||||
options:
|
||||
- none
|
||||
- mock-openai
|
||||
- live-frontier
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: package-acceptance-${{ github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.x"
|
||||
PNPM_VERSION: "10.33.0"
|
||||
PACKAGE_ARTIFACT_NAME: package-under-test
|
||||
|
||||
jobs:
|
||||
resolve_package:
|
||||
name: Resolve package candidate
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
docker_lanes: ${{ steps.profile.outputs.docker_lanes }}
|
||||
include_live_suites: ${{ steps.profile.outputs.include_live_suites }}
|
||||
include_openwebui: ${{ steps.profile.outputs.include_openwebui }}
|
||||
include_release_path_suites: ${{ steps.profile.outputs.include_release_path_suites }}
|
||||
package_artifact_name: ${{ steps.profile.outputs.package_artifact_name }}
|
||||
package_sha256: ${{ steps.resolve.outputs.sha256 }}
|
||||
package_version: ${{ steps.resolve.outputs.package_version }}
|
||||
telegram_enabled: ${{ steps.profile.outputs.telegram_enabled }}
|
||||
telegram_mode: ${{ steps.profile.outputs.telegram_mode }}
|
||||
steps:
|
||||
- name: Checkout package workflow ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
install-bun: ${{ inputs.source == 'ref' && 'true' || 'false' }}
|
||||
install-deps: ${{ inputs.source == 'ref' && 'true' || 'false' }}
|
||||
|
||||
- name: Download package artifact input
|
||||
if: inputs.source == 'artifact'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
|
||||
ARTIFACT_NAME: ${{ inputs.artifact_name }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${ARTIFACT_RUN_ID// }" ]]; then
|
||||
echo "artifact_run_id is required when source=artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${ARTIFACT_NAME// }" ]]; then
|
||||
echo "artifact_name is required when source=artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p .artifacts/package-candidate-input
|
||||
gh run download "$ARTIFACT_RUN_ID" -n "$ARTIFACT_NAME" -D .artifacts/package-candidate-input
|
||||
|
||||
- name: Resolve package candidate
|
||||
id: resolve
|
||||
env:
|
||||
SOURCE: ${{ inputs.source }}
|
||||
PACKAGE_SPEC: ${{ inputs.package_spec }}
|
||||
PACKAGE_URL: ${{ inputs.package_url }}
|
||||
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
artifact_dir=""
|
||||
if [[ "$SOURCE" == "artifact" ]]; then
|
||||
artifact_dir=".artifacts/package-candidate-input"
|
||||
fi
|
||||
|
||||
node scripts/resolve-openclaw-package-candidate.mjs \
|
||||
--source "$SOURCE" \
|
||||
--package-spec "$PACKAGE_SPEC" \
|
||||
--package-url "$PACKAGE_URL" \
|
||||
--package-sha256 "$PACKAGE_SHA256" \
|
||||
--artifact-dir "${artifact_dir:-.}" \
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz \
|
||||
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Select acceptance profile
|
||||
id: profile
|
||||
env:
|
||||
SOURCE: ${{ inputs.source }}
|
||||
SUITE_PROFILE: ${{ inputs.suite_profile }}
|
||||
CUSTOM_DOCKER_LANES: ${{ inputs.docker_lanes }}
|
||||
TELEGRAM_MODE: ${{ inputs.telegram_mode }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
include_release_path_suites=false
|
||||
include_openwebui=false
|
||||
include_live_suites=false
|
||||
docker_lanes=""
|
||||
|
||||
case "$SUITE_PROFILE" in
|
||||
smoke)
|
||||
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
|
||||
;;
|
||||
package)
|
||||
docker_lanes="install-e2e npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps plugins plugin-update"
|
||||
;;
|
||||
product)
|
||||
docker_lanes="install-e2e npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
|
||||
include_openwebui=true
|
||||
;;
|
||||
full)
|
||||
include_release_path_suites=true
|
||||
include_openwebui=true
|
||||
;;
|
||||
custom)
|
||||
docker_lanes="$CUSTOM_DOCKER_LANES"
|
||||
if [[ -z "${docker_lanes// }" ]]; then
|
||||
echo "docker_lanes is required when suite_profile=custom." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$docker_lanes" == *"openwebui"* ]]; then
|
||||
include_openwebui=true
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unknown suite_profile: $SUITE_PROFILE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
telegram_enabled=false
|
||||
if [[ "$TELEGRAM_MODE" != "none" ]]; then
|
||||
if [[ "$SOURCE" != "npm" ]]; then
|
||||
echo "telegram_mode requires source=npm because the Telegram workflow installs a published package spec." >&2
|
||||
exit 1
|
||||
fi
|
||||
telegram_enabled=true
|
||||
fi
|
||||
|
||||
{
|
||||
echo "docker_lanes=$docker_lanes"
|
||||
echo "include_release_path_suites=$include_release_path_suites"
|
||||
echo "include_openwebui=$include_openwebui"
|
||||
echo "include_live_suites=$include_live_suites"
|
||||
echo "telegram_enabled=$telegram_enabled"
|
||||
echo "telegram_mode=$TELEGRAM_MODE"
|
||||
echo "package_artifact_name=${PACKAGE_ARTIFACT_NAME}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload package-under-test artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.PACKAGE_ARTIFACT_NAME }}
|
||||
path: |
|
||||
.artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
.artifacts/docker-e2e-package/package-candidate.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Summarize package candidate
|
||||
env:
|
||||
PACKAGE_SHA256: ${{ steps.resolve.outputs.sha256 }}
|
||||
PACKAGE_VERSION: ${{ steps.resolve.outputs.package_version }}
|
||||
SOURCE: ${{ inputs.source }}
|
||||
SUITE_PROFILE: ${{ inputs.suite_profile }}
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "## Package acceptance"
|
||||
echo
|
||||
echo "- Source: \`${SOURCE}\`"
|
||||
echo "- Version: \`${PACKAGE_VERSION}\`"
|
||||
echo "- SHA-256: \`${PACKAGE_SHA256}\`"
|
||||
echo "- Profile: \`${SUITE_PROFILE}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
docker_acceptance:
|
||||
name: Docker product acceptance
|
||||
needs: resolve_package
|
||||
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
include_repo_e2e: false
|
||||
include_release_path_suites: ${{ needs.resolve_package.outputs.include_release_path_suites == 'true' }}
|
||||
include_openwebui: ${{ needs.resolve_package.outputs.include_openwebui == 'true' }}
|
||||
docker_lanes: ${{ needs.resolve_package.outputs.docker_lanes }}
|
||||
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
|
||||
include_live_suites: ${{ needs.resolve_package.outputs.include_live_suites == 'true' }}
|
||||
live_models_only: false
|
||||
secrets: inherit
|
||||
|
||||
npm_telegram:
|
||||
name: Published npm Telegram acceptance
|
||||
needs: resolve_package
|
||||
if: needs.resolve_package.outputs.telegram_enabled == 'true'
|
||||
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
|
||||
with:
|
||||
package_spec: ${{ inputs.package_spec }}
|
||||
provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }}
|
||||
secrets: inherit
|
||||
|
||||
summary:
|
||||
name: Verify package acceptance
|
||||
needs: [resolve_package, docker_acceptance, npm_telegram]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify package acceptance results
|
||||
env:
|
||||
DOCKER_RESULT: ${{ needs.docker_acceptance.result }}
|
||||
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
|
||||
RESOLVE_RESULT: ${{ needs.resolve_package.result }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
failed=0
|
||||
for item in \
|
||||
"resolve_package=${RESOLVE_RESULT}" \
|
||||
"docker_acceptance=${DOCKER_RESULT}" \
|
||||
"npm_telegram=${NPM_TELEGRAM_RESULT}"
|
||||
do
|
||||
name="${item%%=*}"
|
||||
result="${item#*=}"
|
||||
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
|
||||
echo "::error::${name} ended with ${result}"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
exit "$failed"
|
||||
11
docs/ci.md
11
docs/ci.md
File diff suppressed because one or more lines are too long
@@ -151,6 +151,42 @@ runs the same lanes before release approval.
|
||||
- GitHub Actions exposes this lane as the manual maintainer workflow
|
||||
`NPM Telegram Beta E2E`. It does not run on merge. The workflow uses the
|
||||
`qa-live-shared` environment and Convex CI credential leases.
|
||||
- GitHub Actions also exposes `Package Acceptance` for side-run product proof
|
||||
against one candidate package. It accepts a trusted ref, published npm spec,
|
||||
HTTPS tarball URL plus SHA-256, or tarball artifact from another run, uploads
|
||||
the normalized `openclaw-current.tgz` as `package-under-test`, then runs the
|
||||
existing Docker E2E scheduler with smoke, package, product, full, or custom
|
||||
lane profiles. Published npm candidates can additionally run the Telegram QA
|
||||
workflow.
|
||||
- Latest beta product proof:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml --ref main \
|
||||
-f source=npm \
|
||||
-f package_spec=openclaw@beta \
|
||||
-f suite_profile=product
|
||||
```
|
||||
|
||||
- Exact tarball URL proof requires a digest:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml --ref main \
|
||||
-f source=url \
|
||||
-f package_url=https://registry.npmjs.org/openclaw/-/openclaw-VERSION.tgz \
|
||||
-f package_sha256=<sha256> \
|
||||
-f suite_profile=package
|
||||
```
|
||||
|
||||
- Artifact proof downloads a tarball artifact from another Actions run:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml --ref main \
|
||||
-f source=artifact \
|
||||
-f artifact_run_id=<run-id> \
|
||||
-f artifact_name=<artifact-name> \
|
||||
-f suite_profile=smoke
|
||||
```
|
||||
|
||||
- `pnpm test:docker:bundled-channel-deps`
|
||||
- Packs and installs the current OpenClaw build in Docker, starts the Gateway
|
||||
with OpenAI configured, then enables bundled channel/plugins via config
|
||||
|
||||
@@ -57,6 +57,22 @@ OpenClaw has three public release lanes:
|
||||
Provide `npm_telegram_package_spec` only after a package has been published
|
||||
and the post-publish Telegram E2E should run too.
|
||||
Example: `gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D`
|
||||
- Run the manual `Package Acceptance` workflow when you want side-channel proof
|
||||
for a package candidate while release work continues. Use `source=npm` for
|
||||
`openclaw@beta`, `openclaw@latest`, or an exact release version; `source=ref`
|
||||
to pack a trusted branch/tag/SHA; `source=url` for an HTTPS tarball with a
|
||||
required SHA-256; or `source=artifact` for a tarball uploaded by another
|
||||
GitHub Actions run. The workflow resolves the candidate to
|
||||
`package-under-test`, reuses the Docker E2E release scheduler against that
|
||||
tarball, and can optionally run published-npm Telegram QA.
|
||||
Example: `gh workflow run package-acceptance.yml --ref main -f source=npm -f package_spec=openclaw@beta -f suite_profile=product`
|
||||
Common profiles:
|
||||
- `smoke`: install/channel/agent, gateway network, and config reload lanes
|
||||
- `package`: package/update/plugin lanes without OpenWebUI
|
||||
- `product`: package profile plus MCP channels, cron/subagent cleanup,
|
||||
OpenAI web search, and OpenWebUI
|
||||
- `full`: Docker release-path chunks with OpenWebUI
|
||||
- `custom`: exact `docker_lanes` selection for a focused rerun
|
||||
- Run the manual `CI` workflow directly when you only need full normal CI
|
||||
coverage for the release candidate. Manual CI dispatches bypass changed
|
||||
scoping and force the Linux Node shards, bundled-plugin shards, channel
|
||||
|
||||
330
scripts/resolve-openclaw-package-candidate.mjs
Normal file
330
scripts/resolve-openclaw-package-candidate.mjs
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env node
|
||||
// Normalizes package-acceptance inputs into the tarball shape consumed by Docker E2E.
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const DEFAULT_OUTPUT_NAME = "openclaw-current.tgz";
|
||||
export const OPENCLAW_PACKAGE_SPEC_RE =
|
||||
/^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$/u;
|
||||
|
||||
function usage() {
|
||||
return `Usage: node scripts/resolve-openclaw-package-candidate.mjs --source <ref|npm|url|artifact> --output-dir <dir> [options]
|
||||
|
||||
Options:
|
||||
--package-spec <spec> Published npm spec for source=npm.
|
||||
--package-url <url> HTTPS tarball URL for source=url.
|
||||
--package-sha256 <sha256> Expected tarball SHA-256 for source=url or source=artifact.
|
||||
--artifact-dir <dir> Directory containing exactly one .tgz for source=artifact.
|
||||
--output-name <name> Output tarball filename. Default: ${DEFAULT_OUTPUT_NAME}
|
||||
--metadata <file> Write package metadata JSON.
|
||||
--github-output <file> Append tarball, sha256, package name/version outputs.`;
|
||||
}
|
||||
|
||||
export function parseArgs(argv) {
|
||||
const options = {
|
||||
artifactDir: "",
|
||||
githubOutput: "",
|
||||
metadata: "",
|
||||
outputDir: "",
|
||||
outputName: DEFAULT_OUTPUT_NAME,
|
||||
packageSha256: "",
|
||||
packageSpec: "",
|
||||
packageUrl: "",
|
||||
source: "",
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
const readValue = (name) => {
|
||||
const value = argv[(index += 1)];
|
||||
if (value === undefined) {
|
||||
throw new Error(`${name} requires a value`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
if (arg === "--artifact-dir") {
|
||||
options.artifactDir = readValue(arg);
|
||||
} else if (arg === "--github-output") {
|
||||
options.githubOutput = readValue(arg);
|
||||
} else if (arg === "--metadata") {
|
||||
options.metadata = readValue(arg);
|
||||
} else if (arg === "--output-dir") {
|
||||
options.outputDir = readValue(arg);
|
||||
} else if (arg === "--output-name") {
|
||||
options.outputName = readValue(arg);
|
||||
} else if (arg === "--package-sha256") {
|
||||
options.packageSha256 = readValue(arg).toLowerCase();
|
||||
} else if (arg === "--package-spec") {
|
||||
options.packageSpec = readValue(arg);
|
||||
} else if (arg === "--package-url") {
|
||||
options.packageUrl = readValue(arg);
|
||||
} else if (arg === "--source") {
|
||||
options.source = readValue(arg);
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
options.help = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function validateOpenClawPackageSpec(spec) {
|
||||
if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) {
|
||||
throw new Error(
|
||||
`package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd ?? ROOT_DIR,
|
||||
stdio: options.capture ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (options.capture) {
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += String(chunk);
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += String(chunk);
|
||||
});
|
||||
}
|
||||
child.on("error", reject);
|
||||
child.on("close", (status, signal) => {
|
||||
if (status === 0) {
|
||||
resolve(stdout);
|
||||
return;
|
||||
}
|
||||
const detail = stderr.trim() ? `\n${stderr.trim()}` : "";
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with ${status ?? signal}${detail}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function walkFiles(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const absolute = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkFiles(absolute)));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(absolute);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function sha256(file) {
|
||||
const hash = createHash("sha256");
|
||||
const handle = await fs.open(file, "r");
|
||||
try {
|
||||
for await (const chunk of handle.createReadStream()) {
|
||||
hash.update(chunk);
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function assertSha256(value) {
|
||||
if (!/^[a-f0-9]{64}$/u.test(value)) {
|
||||
throw new Error(`package_sha256 must be a lowercase or uppercase 64-character SHA-256 digest`);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertExpectedSha256(file, expected) {
|
||||
if (!expected) {
|
||||
return await sha256(file);
|
||||
}
|
||||
assertSha256(expected);
|
||||
const actual = await sha256(file);
|
||||
if (actual !== expected.toLowerCase()) {
|
||||
throw new Error(`package SHA-256 mismatch: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
async function findSingleTarball(dir) {
|
||||
const files = (await walkFiles(path.resolve(ROOT_DIR, dir)))
|
||||
.filter((file) => /\.t(?:ar\.)?gz$/u.test(path.basename(file)))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
if (files.length !== 1) {
|
||||
throw new Error(
|
||||
`source=artifact requires exactly one .tgz under ${dir}; found ${files.length}: ${files.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return files[0];
|
||||
}
|
||||
|
||||
async function moveNewestPackedTarball(outputDir, packOutput, outputName) {
|
||||
let filename = "";
|
||||
try {
|
||||
const parsed = JSON.parse(packOutput);
|
||||
if (Array.isArray(parsed)) {
|
||||
filename = parsed.find((entry) => typeof entry?.filename === "string")?.filename ?? "";
|
||||
}
|
||||
} catch {}
|
||||
if (!filename) {
|
||||
for (const line of packOutput.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
if (/^openclaw-.*\.tgz$/u.test(trimmed)) {
|
||||
filename = trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!filename) {
|
||||
const entries = await fs.readdir(outputDir);
|
||||
filename = entries
|
||||
.filter((entry) => /^openclaw-.*\.tgz$/u.test(entry))
|
||||
.toSorted((a, b) => a.localeCompare(b))
|
||||
.at(-1);
|
||||
}
|
||||
if (!filename) {
|
||||
throw new Error(`npm pack produced no OpenClaw tarball in ${outputDir}`);
|
||||
}
|
||||
const packed = path.join(outputDir, filename);
|
||||
const target = path.join(outputDir, outputName);
|
||||
if (packed !== target) {
|
||||
await fs.rm(target, { force: true });
|
||||
await fs.rename(packed, target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
async function downloadUrl(url, target) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== "https:") {
|
||||
throw new Error(`package_url must use https: ${url}`);
|
||||
}
|
||||
const response = await fetch(parsed);
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`failed to download package_url: HTTP ${response.status}`);
|
||||
}
|
||||
await pipeline(response.body, createWriteStream(target));
|
||||
}
|
||||
|
||||
async function readPackageJson(tarball) {
|
||||
const raw = await run("tar", ["-xOf", tarball, "package/package.json"], { capture: true });
|
||||
const pkg = JSON.parse(raw);
|
||||
return {
|
||||
name: typeof pkg.name === "string" ? pkg.name : "",
|
||||
version: typeof pkg.version === "string" ? pkg.version : "",
|
||||
};
|
||||
}
|
||||
|
||||
async function appendGithubOutputs(file, outputs) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const body = Object.entries(outputs)
|
||||
.map(([key, value]) => `${key}=${String(value).replace(/\n/gu, " ")}`)
|
||||
.join("\n");
|
||||
await fs.appendFile(file, `${body}\n`);
|
||||
}
|
||||
|
||||
async function resolveCandidate(options) {
|
||||
const outputDir = path.resolve(ROOT_DIR, options.outputDir);
|
||||
const target = path.join(outputDir, options.outputName || DEFAULT_OUTPUT_NAME);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.rm(target, { force: true });
|
||||
|
||||
if (options.source === "ref") {
|
||||
await run("node", [
|
||||
"scripts/package-openclaw-for-docker.mjs",
|
||||
"--output-dir",
|
||||
outputDir,
|
||||
"--output-name",
|
||||
options.outputName || DEFAULT_OUTPUT_NAME,
|
||||
]);
|
||||
} else if (options.source === "npm") {
|
||||
validateOpenClawPackageSpec(options.packageSpec);
|
||||
const packOutput = await run(
|
||||
"npm",
|
||||
["pack", options.packageSpec, "--ignore-scripts", "--json", "--pack-destination", outputDir],
|
||||
{ capture: true },
|
||||
);
|
||||
await moveNewestPackedTarball(outputDir, packOutput, options.outputName || DEFAULT_OUTPUT_NAME);
|
||||
} else if (options.source === "url") {
|
||||
if (!options.packageUrl) {
|
||||
throw new Error("source=url requires --package-url");
|
||||
}
|
||||
if (!options.packageSha256) {
|
||||
throw new Error("source=url requires --package-sha256");
|
||||
}
|
||||
await downloadUrl(options.packageUrl, target);
|
||||
} else if (options.source === "artifact") {
|
||||
if (!options.artifactDir) {
|
||||
throw new Error("source=artifact requires --artifact-dir");
|
||||
}
|
||||
const input = await findSingleTarball(options.artifactDir);
|
||||
await fs.copyFile(input, target);
|
||||
} else {
|
||||
throw new Error(`source must be one of: ref, npm, url, artifact. Got: ${options.source}`);
|
||||
}
|
||||
|
||||
const digest = await assertExpectedSha256(target, options.packageSha256);
|
||||
await run("node", ["scripts/check-openclaw-package-tarball.mjs", target]);
|
||||
const pkg = await readPackageJson(target);
|
||||
const metadata = {
|
||||
name: pkg.name,
|
||||
packageSpec: options.packageSpec || "",
|
||||
sha256: digest,
|
||||
source: options.source,
|
||||
tarball: path.relative(ROOT_DIR, target),
|
||||
version: pkg.version,
|
||||
};
|
||||
|
||||
if (pkg.name !== "openclaw") {
|
||||
throw new Error(`package candidate must be named "openclaw"; got: ${pkg.name || "<missing>"}`);
|
||||
}
|
||||
if (!pkg.version) {
|
||||
throw new Error("package candidate package.json has no version");
|
||||
}
|
||||
|
||||
if (options.metadata) {
|
||||
await fs.mkdir(path.dirname(path.resolve(ROOT_DIR, options.metadata)), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.resolve(ROOT_DIR, options.metadata),
|
||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
await appendGithubOutputs(options.githubOutput, {
|
||||
package_name: pkg.name,
|
||||
package_version: pkg.version,
|
||||
sha256: digest,
|
||||
tarball: metadata.tarball,
|
||||
});
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
const options = parseArgs(argv);
|
||||
if (options.help) {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
if (!options.outputDir) {
|
||||
throw new Error("--output-dir is required");
|
||||
}
|
||||
const metadata = await resolveCandidate(options);
|
||||
console.log(JSON.stringify(metadata, null, 2));
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
console.error(usage());
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
65
test/scripts/package-acceptance-workflow.test.ts
Normal file
65
test/scripts/package-acceptance-workflow.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const PACKAGE_ACCEPTANCE_WORKFLOW = ".github/workflows/package-acceptance.yml";
|
||||
const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml";
|
||||
const DOCKER_E2E_PLAN_ACTION = ".github/actions/docker-e2e-plan/action.yml";
|
||||
const NPM_TELEGRAM_WORKFLOW = ".github/workflows/npm-telegram-beta-e2e.yml";
|
||||
|
||||
describe("package acceptance workflow", () => {
|
||||
it("resolves candidate package sources before reusing Docker E2E lanes", () => {
|
||||
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
||||
|
||||
expect(workflow).toContain("name: Package Acceptance");
|
||||
expect(workflow).toContain("source:");
|
||||
expect(workflow).toContain("- npm");
|
||||
expect(workflow).toContain("- ref");
|
||||
expect(workflow).toContain("- url");
|
||||
expect(workflow).toContain("- artifact");
|
||||
expect(workflow).toContain("scripts/resolve-openclaw-package-candidate.mjs");
|
||||
expect(workflow).toContain('gh run download "$ARTIFACT_RUN_ID"');
|
||||
expect(workflow).toContain("name: ${{ env.PACKAGE_ARTIFACT_NAME }}");
|
||||
expect(workflow).toContain(
|
||||
"uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}",
|
||||
);
|
||||
});
|
||||
|
||||
it("offers bounded product profiles and keeps Telegram published-npm only", () => {
|
||||
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
||||
|
||||
expect(workflow).toContain("suite_profile:");
|
||||
expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload");
|
||||
expect(workflow).toContain("install-e2e npm-onboard-channel-agent doctor-switch");
|
||||
expect(workflow).toContain("include_release_path_suites=true");
|
||||
expect(workflow).toContain("telegram_mode requires source=npm");
|
||||
expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml");
|
||||
});
|
||||
});
|
||||
|
||||
describe("package artifact reuse", () => {
|
||||
it("lets reusable Docker E2E consume an already resolved package artifact", () => {
|
||||
const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
||||
const action = readFileSync(DOCKER_E2E_PLAN_ACTION, "utf8");
|
||||
|
||||
expect(workflow).toContain("package_artifact_name:");
|
||||
expect(workflow).toContain("Download provided OpenClaw Docker E2E package");
|
||||
expect(workflow).toContain("inputs.package_artifact_name != ''");
|
||||
expect(workflow).toContain('image_tag="${PACKAGE_TAG:-$SELECTED_SHA}"');
|
||||
expect(workflow).toContain(
|
||||
"package-artifact-name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}",
|
||||
);
|
||||
expect(action).toContain("package-artifact-name:");
|
||||
expect(action).toContain("name: ${{ inputs.package-artifact-name }}");
|
||||
});
|
||||
|
||||
it("allows the npm Telegram lane to run from reusable package acceptance", () => {
|
||||
const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");
|
||||
|
||||
expect(workflow).toContain("workflow_call:");
|
||||
expect(workflow).toContain("provider_mode:");
|
||||
expect(workflow).toContain("provider_mode must be mock-openai or live-frontier");
|
||||
});
|
||||
});
|
||||
51
test/scripts/resolve-openclaw-package-candidate.test.ts
Normal file
51
test/scripts/resolve-openclaw-package-candidate.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseArgs,
|
||||
validateOpenClawPackageSpec,
|
||||
} from "../../scripts/resolve-openclaw-package-candidate.mjs";
|
||||
|
||||
describe("resolve-openclaw-package-candidate", () => {
|
||||
it("accepts only OpenClaw release package specs for npm candidates", () => {
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@beta")).not.toThrow();
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@latest")).not.toThrow();
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27")).not.toThrow();
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-1")).not.toThrow();
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-beta.2")).not.toThrow();
|
||||
|
||||
expect(() => validateOpenClawPackageSpec("@evil/openclaw@1.0.0")).toThrow(
|
||||
"package_spec must be openclaw@beta",
|
||||
);
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@canary")).toThrow(
|
||||
"package_spec must be openclaw@beta",
|
||||
);
|
||||
expect(() => validateOpenClawPackageSpec("openclaw@2026.04.27")).toThrow(
|
||||
"package_spec must be openclaw@beta",
|
||||
);
|
||||
});
|
||||
|
||||
it("parses optional empty workflow inputs without rejecting the command line", () => {
|
||||
expect(
|
||||
parseArgs([
|
||||
"--source",
|
||||
"npm",
|
||||
"--package-spec",
|
||||
"openclaw@beta",
|
||||
"--package-url",
|
||||
"",
|
||||
"--package-sha256",
|
||||
"",
|
||||
"--artifact-dir",
|
||||
".",
|
||||
"--output-dir",
|
||||
".artifacts/docker-e2e-package",
|
||||
]),
|
||||
).toMatchObject({
|
||||
artifactDir: ".",
|
||||
outputDir: ".artifacts/docker-e2e-package",
|
||||
packageSha256: "",
|
||||
packageSpec: "openclaw@beta",
|
||||
packageUrl: "",
|
||||
source: "npm",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user