mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): provide prerelease plugin companions to package QA (#120107)
This commit is contained in:
@@ -418,6 +418,8 @@ jobs:
|
||||
include_release_path_suites: false
|
||||
include_openwebui: false
|
||||
include_live_suites: false
|
||||
enable_prepublish_plugin_registry: true
|
||||
published_upgrade_survivor_scenarios: ${{ (inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full') && 'reported-issues' || '' }}
|
||||
allow_unreleased_changelog: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}
|
||||
release_test_profile: ${{ inputs.release_profile }}
|
||||
shared_image_artifact_namespace: full-release
|
||||
|
||||
@@ -161,6 +161,24 @@ on:
|
||||
package_version:
|
||||
description: Package version
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.package_version }}
|
||||
prepublish_plugin_registry_artifact_name:
|
||||
description: Immutable prerelease plugin registry artifact name
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_name }}
|
||||
prepublish_plugin_registry_artifact_id:
|
||||
description: Immutable prerelease plugin registry artifact id
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}
|
||||
prepublish_plugin_registry_artifact_digest:
|
||||
description: Prerelease plugin registry artifact service digest
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_digest }}
|
||||
prepublish_plugin_registry_artifact_run_id:
|
||||
description: Prerelease plugin registry artifact producer run id
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
prepublish_plugin_registry_artifact_run_attempt:
|
||||
description: Prerelease plugin registry artifact producer run attempt
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
prepublish_plugin_registry_manifest_sha256:
|
||||
description: Prerelease plugin registry manifest SHA-256
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
shared_image_artifact_name:
|
||||
description: Immutable Docker image artifact name
|
||||
value: ${{ jobs.prepare_docker_e2e_image.outputs.image_artifact_name }}
|
||||
@@ -294,6 +312,41 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
enable_prepublish_plugin_registry:
|
||||
description: Build or reuse candidate-version official plugin packages for artifact/ref acceptance
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
prepublish_plugin_registry_artifact_name:
|
||||
description: Existing immutable prerelease plugin registry artifact name
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_artifact_id:
|
||||
description: Existing immutable prerelease plugin registry artifact id
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_artifact_digest:
|
||||
description: Existing prerelease plugin registry artifact service SHA-256 digest
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_artifact_run_id:
|
||||
description: Producer run id for the prerelease plugin registry artifact
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_artifact_run_attempt:
|
||||
description: Producer run attempt for the prerelease plugin registry artifact
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_manifest_sha256:
|
||||
description: SHA-256 of prepublish-plugin-registry.json
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
shared_image_policy:
|
||||
description: "Shared Docker image transport: existing-only or no-push-artifact"
|
||||
required: false
|
||||
@@ -545,6 +598,13 @@ jobs:
|
||||
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
|
||||
PACKAGE_SOURCE_SHA: ${{ inputs.package_source_sha }}
|
||||
PACKAGE_VERSION: ${{ inputs.package_version }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ENABLED: ${{ inputs.enable_prepublish_plugin_registry }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST: ${{ inputs.prepublish_plugin_registry_artifact_digest }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: ${{ inputs.prepublish_plugin_registry_artifact_id }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME: ${{ inputs.prepublish_plugin_registry_artifact_name }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT: ${{ inputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID: ${{ inputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: ${{ inputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
PROVIDED_BARE_IMAGE: ${{ inputs.docker_e2e_bare_image }}
|
||||
PROVIDED_FUNCTIONAL_IMAGE: ${{ inputs.docker_e2e_functional_image }}
|
||||
SHARED_IMAGE_ARTIFACT_NAMESPACE: ${{ inputs.shared_image_artifact_namespace }}
|
||||
@@ -616,6 +676,36 @@ jobs:
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ "$PREPUBLISH_PLUGIN_REGISTRY_ENABLED" == "true" ]]; then
|
||||
prepublish_plugin_registry_tuple_present=0
|
||||
for value in \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST" \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID" \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME" \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT" \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID" \
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256"; do
|
||||
if [[ -n "${value// }" ]]; then
|
||||
prepublish_plugin_registry_tuple_present=1
|
||||
fi
|
||||
done
|
||||
if [[ "$prepublish_plugin_registry_tuple_present" == "1" ]]; then
|
||||
[[ "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST" =~ ^[0-9a-f]{64}$ &&
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID" =~ ^[1-9][0-9]*$ &&
|
||||
-n "${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME// }" &&
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ &&
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ &&
|
||||
"$PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
|
||||
echo "Prerelease plugin registry selection requires the complete immutable artifact tuple." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME" == *"-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}" ]] || {
|
||||
echo "Prerelease plugin registry artifact name does not bind the producer run attempt." >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
image_tuple_present=0
|
||||
for value in \
|
||||
"$SHARED_IMAGE_ARTIFACT_DIGEST" \
|
||||
@@ -1138,6 +1228,8 @@ jobs:
|
||||
OPENCLAW_DOCKER_ALL_RELEASE_PROFILE: ${{ inputs.release_test_profile }}
|
||||
OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
|
||||
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_CANDIDATE_VERSION: ${{ needs.prepare_docker_e2e_image.outputs.package_version }}
|
||||
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
@@ -1217,6 +1309,49 @@ jobs:
|
||||
run-id: ${{ needs.prepare_docker_e2e_image.outputs.package_artifact_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Validate prerelease plugin registry artifact binding
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
env:
|
||||
ARTIFACT_DIGEST: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_digest }}
|
||||
ARTIFACT_ID: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}
|
||||
ARTIFACT_NAME: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_name }}
|
||||
ARTIFACT_RUN_ATTEMPT: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
ARTIFACT_RUN_ID: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash .release-harness/scripts/docker/shared-image-artifact.sh \
|
||||
verify-upload "Prerelease plugin registry" \
|
||||
"$ARTIFACT_ID" "$ARTIFACT_NAME" "$ARTIFACT_DIGEST" \
|
||||
"$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT"
|
||||
|
||||
- name: Download prerelease plugin registry artifact
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
artifact-ids: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}
|
||||
path: .artifacts/prepublish-plugin-registry
|
||||
run-id: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Validate prerelease plugin registry contents
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
env:
|
||||
CANDIDATE_VERSION: ${{ needs.prepare_docker_e2e_image.outputs.package_version }}
|
||||
EXPECTED_MANIFEST_SHA256: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
REQUIRED_PACKAGES_JSON: ${{ steps.plan.outputs.required_prepublish_plugin_packages }}
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node .release-harness/scripts/prepublish-plugin-registry-artifact.mjs verify \
|
||||
--artifact-dir .artifacts/prepublish-plugin-registry \
|
||||
--source-sha "$SELECTED_SHA" \
|
||||
--candidate-version "$CANDIDATE_VERSION" \
|
||||
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
||||
--required-packages-json "$REQUIRED_PACKAGES_JSON"
|
||||
|
||||
- name: Validate Docker E2E image artifact binding
|
||||
if: contains(matrix.profiles, inputs.release_test_profile) && inputs.shared_image_policy == 'no-push-artifact' && needs.prepare_docker_e2e_image.outputs.needs_e2e_image == '1'
|
||||
env:
|
||||
@@ -1339,6 +1474,11 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}"
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ inputs.enable_prepublish_plugin_registry }}" == "true" &&
|
||||
"${{ steps.plan.outputs.needs_prepublish_plugin_registry }}" == "1" &&
|
||||
-n "${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}" ]]; then
|
||||
export OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR=".artifacts/prepublish-plugin-registry"
|
||||
fi
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
@@ -1466,6 +1606,8 @@ jobs:
|
||||
OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
|
||||
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_CANDIDATE_VERSION: ${{ needs.prepare_docker_e2e_image.outputs.package_version }}
|
||||
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
@@ -1544,6 +1686,49 @@ jobs:
|
||||
run-id: ${{ needs.prepare_docker_e2e_image.outputs.package_artifact_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Validate targeted prerelease plugin registry artifact binding
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
env:
|
||||
ARTIFACT_DIGEST: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_digest }}
|
||||
ARTIFACT_ID: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}
|
||||
ARTIFACT_NAME: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_name }}
|
||||
ARTIFACT_RUN_ATTEMPT: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
ARTIFACT_RUN_ID: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash .release-harness/scripts/docker/shared-image-artifact.sh \
|
||||
verify-upload "Prerelease plugin registry" \
|
||||
"$ARTIFACT_ID" "$ARTIFACT_NAME" "$ARTIFACT_DIGEST" \
|
||||
"$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT"
|
||||
|
||||
- name: Download targeted prerelease plugin registry artifact
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
artifact-ids: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id }}
|
||||
path: .artifacts/prepublish-plugin-registry
|
||||
run-id: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Validate targeted prerelease plugin registry contents
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''
|
||||
env:
|
||||
CANDIDATE_VERSION: ${{ needs.prepare_docker_e2e_image.outputs.package_version }}
|
||||
EXPECTED_MANIFEST_SHA256: ${{ needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
REQUIRED_PACKAGES_JSON: ${{ steps.plan.outputs.required_prepublish_plugin_packages }}
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node .release-harness/scripts/prepublish-plugin-registry-artifact.mjs verify \
|
||||
--artifact-dir .artifacts/prepublish-plugin-registry \
|
||||
--source-sha "$SELECTED_SHA" \
|
||||
--candidate-version "$CANDIDATE_VERSION" \
|
||||
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
||||
--required-packages-json "$REQUIRED_PACKAGES_JSON"
|
||||
|
||||
- name: Validate Docker E2E image artifact binding
|
||||
if: inputs.shared_image_policy == 'no-push-artifact' && needs.prepare_docker_e2e_image.outputs.needs_e2e_image == '1'
|
||||
env:
|
||||
@@ -1655,6 +1840,7 @@ jobs:
|
||||
env:
|
||||
ARTIFACT_SUFFIX: ${{ steps.plan.outputs.artifact_suffix }}
|
||||
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ENABLED: ${{ inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != '' && '1' || '0' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export OPENCLAW_DOCKER_ALL_LANES="${DOCKER_E2E_LANES}"
|
||||
@@ -1667,6 +1853,9 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted-${ARTIFACT_SUFFIX}"
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-${ARTIFACT_SUFFIX}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "$PREPUBLISH_PLUGIN_REGISTRY_ENABLED" == "1" ]]; then
|
||||
export OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR=".artifacts/prepublish-plugin-registry"
|
||||
fi
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
@@ -1905,6 +2094,8 @@ jobs:
|
||||
needs_functional_image: ${{ inputs.shared_image_artifact_id != '' && '1' || steps.plan.outputs.needs_functional_image }}
|
||||
needs_live_image: ${{ steps.plan.outputs.needs_live_image }}
|
||||
needs_package: ${{ steps.plan.outputs.needs_package }}
|
||||
needs_prepublish_plugin_registry: ${{ steps.plan.outputs.needs_prepublish_plugin_registry }}
|
||||
required_prepublish_plugin_packages: ${{ steps.plan.outputs.required_prepublish_plugin_packages }}
|
||||
bare_exists: ${{ steps.image_exists.outputs.bare_exists }}
|
||||
functional_exists: ${{ steps.image_exists.outputs.functional_exists }}
|
||||
needs_registry_build: ${{ steps.image_exists.outputs.needs_build }}
|
||||
@@ -1917,6 +2108,12 @@ jobs:
|
||||
package_artifact_run_attempt: ${{ steps.upload_package.outputs.artifact-id && github.run_attempt || steps.input_package_artifact.outputs.run_attempt }}
|
||||
package_file_name: ${{ steps.package.outputs.file_name }}
|
||||
package_source_sha: ${{ steps.package.outputs.source_sha }}
|
||||
prepublish_plugin_registry_artifact_name: ${{ inputs.enable_prepublish_plugin_registry && (steps.upload_prepublish_plugin_registry.outputs.artifact-id && format('docker-e2e-prepublish-plugin-registry-{0}-{1}', github.run_id, github.run_attempt) || inputs.prepublish_plugin_registry_artifact_name) || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ inputs.enable_prepublish_plugin_registry && (steps.upload_prepublish_plugin_registry.outputs.artifact-id || inputs.prepublish_plugin_registry_artifact_id) || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ inputs.enable_prepublish_plugin_registry && (steps.upload_prepublish_plugin_registry.outputs.artifact-digest || inputs.prepublish_plugin_registry_artifact_digest) || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ inputs.enable_prepublish_plugin_registry && (steps.upload_prepublish_plugin_registry.outputs.artifact-id && github.run_id || inputs.prepublish_plugin_registry_artifact_run_id) || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ inputs.enable_prepublish_plugin_registry && (steps.upload_prepublish_plugin_registry.outputs.artifact-id && github.run_attempt || inputs.prepublish_plugin_registry_artifact_run_attempt) || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ inputs.enable_prepublish_plugin_registry && steps.prepublish_plugin_registry.outputs.manifest_sha256 || '' }}
|
||||
image_artifact_name: ${{ steps.image_artifact.outputs.artifact_name || inputs.shared_image_artifact_name }}
|
||||
image_archive_sha256: ${{ steps.image_artifact.outputs.archive_sha256 || inputs.shared_image_archive_sha256 }}
|
||||
image_artifact_id: ${{ steps.upload_image_artifact.outputs.artifact-id || inputs.shared_image_artifact_id }}
|
||||
@@ -1984,7 +2181,7 @@ jobs:
|
||||
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node environment
|
||||
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == '' && inputs.package_artifact_run_id == ''
|
||||
if: (steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == '' && inputs.package_artifact_run_id == '') || (inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == '')
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
@@ -2218,6 +2415,84 @@ jobs:
|
||||
path: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Validate prerelease plugin registry artifact identity
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id != ''
|
||||
env:
|
||||
ARTIFACT_DIGEST: ${{ inputs.prepublish_plugin_registry_artifact_digest }}
|
||||
ARTIFACT_ID: ${{ inputs.prepublish_plugin_registry_artifact_id }}
|
||||
ARTIFACT_NAME: ${{ inputs.prepublish_plugin_registry_artifact_name }}
|
||||
ARTIFACT_RUN_ATTEMPT: ${{ inputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash .release-harness/scripts/docker/shared-image-artifact.sh \
|
||||
verify-upload "Prerelease plugin registry" \
|
||||
"$ARTIFACT_ID" "$ARTIFACT_NAME" "$ARTIFACT_DIGEST" \
|
||||
"$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT"
|
||||
|
||||
- name: Download prerelease plugin registry artifact
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id != ''
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
artifact-ids: ${{ inputs.prepublish_plugin_registry_artifact_id }}
|
||||
path: .artifacts/prepublish-plugin-registry
|
||||
run-id: ${{ inputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Pack prerelease plugin registry artifact
|
||||
id: create_prepublish_plugin_registry
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == ''
|
||||
env:
|
||||
CANDIDATE_VERSION: ${{ steps.package.outputs.version }}
|
||||
REQUIRED_PACKAGES_JSON: ${{ steps.plan.outputs.required_prepublish_plugin_packages }}
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
result="$(
|
||||
node .release-harness/scripts/prepublish-plugin-registry-artifact.mjs create \
|
||||
--repo-root . \
|
||||
--artifact-dir .artifacts/prepublish-plugin-registry \
|
||||
--source-sha "$SELECTED_SHA" \
|
||||
--candidate-version "$CANDIDATE_VERSION" \
|
||||
--required-packages-json "$REQUIRED_PACKAGES_JSON"
|
||||
)"
|
||||
manifest_sha256="$(jq -er '.manifestSha256' <<< "$result")"
|
||||
echo "manifest_sha256=$manifest_sha256" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate prerelease plugin registry artifact
|
||||
id: prepublish_plugin_registry
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1'
|
||||
env:
|
||||
CANDIDATE_VERSION: ${{ steps.package.outputs.version }}
|
||||
EXPECTED_MANIFEST_SHA256: ${{ steps.create_prepublish_plugin_registry.outputs.manifest_sha256 || inputs.prepublish_plugin_registry_manifest_sha256 }}
|
||||
REQUIRED_PACKAGES_JSON: ${{ steps.plan.outputs.required_prepublish_plugin_packages }}
|
||||
SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
result="$(
|
||||
node .release-harness/scripts/prepublish-plugin-registry-artifact.mjs verify \
|
||||
--artifact-dir .artifacts/prepublish-plugin-registry \
|
||||
--source-sha "$SELECTED_SHA" \
|
||||
--candidate-version "$CANDIDATE_VERSION" \
|
||||
--manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
|
||||
--required-packages-json "$REQUIRED_PACKAGES_JSON"
|
||||
)"
|
||||
manifest_sha256="$(jq -er '.manifestSha256' <<< "$result")"
|
||||
echo "manifest_sha256=$manifest_sha256" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload prerelease plugin registry artifact
|
||||
id: upload_prepublish_plugin_registry
|
||||
if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == ''
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docker-e2e-prepublish-plugin-registry-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/prepublish-plugin-registry/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Resolve shared Docker E2E image tags
|
||||
id: image
|
||||
shell: bash
|
||||
@@ -2418,6 +2693,13 @@ jobs:
|
||||
PACKAGE_SHA256: ${{ steps.package.outputs.sha256 }}
|
||||
PACKAGE_SOURCE_SHA: ${{ steps.package.outputs.source_sha }}
|
||||
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST: ${{ steps.upload_prepublish_plugin_registry.outputs.artifact-digest || inputs.prepublish_plugin_registry_artifact_digest }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: ${{ steps.upload_prepublish_plugin_registry.outputs.artifact-id || inputs.prepublish_plugin_registry_artifact_id }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME: ${{ steps.upload_prepublish_plugin_registry.outputs.artifact-id && format('docker-e2e-prepublish-plugin-registry-{0}-{1}', github.run_id, github.run_attempt) || inputs.prepublish_plugin_registry_artifact_name }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT: ${{ steps.upload_prepublish_plugin_registry.outputs.artifact-id && github.run_attempt || inputs.prepublish_plugin_registry_artifact_run_attempt }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID: ${{ steps.upload_prepublish_plugin_registry.outputs.artifact-id && github.run_id || inputs.prepublish_plugin_registry_artifact_run_id }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: ${{ steps.prepublish_plugin_registry.outputs.manifest_sha256 }}
|
||||
NEEDS_PREPUBLISH_PLUGIN_REGISTRY: ${{ inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && '1' || '0' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -2439,6 +2721,18 @@ jobs:
|
||||
--arg imageArchiveSha256 "$IMAGE_ARCHIVE_SHA256" \
|
||||
'$ARGS.named')"
|
||||
jq -e 'all(.[]; type == "string" and length > 0)' <<< "$json" >/dev/null
|
||||
if [[ "$NEEDS_PREPUBLISH_PLUGIN_REGISTRY" == "1" ]]; then
|
||||
registry_json="$(jq -cn \
|
||||
--arg prepublishPluginRegistryArtifactName "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME" \
|
||||
--arg prepublishPluginRegistryArtifactId "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID" \
|
||||
--arg prepublishPluginRegistryArtifactDigest "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST" \
|
||||
--arg prepublishPluginRegistryArtifactRunId "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID" \
|
||||
--arg prepublishPluginRegistryArtifactRunAttempt "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT" \
|
||||
--arg prepublishPluginRegistryManifestSha256 "$PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256" \
|
||||
'$ARGS.named')"
|
||||
jq -e 'all(.[]; type == "string" and length > 0)' <<< "$registry_json" >/dev/null
|
||||
json="$(jq -cn --argjson base "$json" --argjson registry "$registry_json" '$base + $registry')"
|
||||
fi
|
||||
echo "json=$json" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker_e2e_image_ready:
|
||||
|
||||
@@ -331,8 +331,15 @@ jobs:
|
||||
RELEASE_PACKAGE_SPEC_INPUT: ${{ inputs.release_package_spec }}
|
||||
RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }}
|
||||
RELEASE_CODEX_PLUGIN_SPEC_INPUT: ${{ inputs.codex_plugin_spec }}
|
||||
CANDIDATE_ARTIFACT_JSON_INPUT: ${{ inputs.candidate_artifact_json }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${CANDIDATE_ARTIFACT_JSON_INPUT// }" ]] &&
|
||||
{ [[ -n "${RELEASE_PACKAGE_SPEC_INPUT// }" ]] ||
|
||||
[[ -n "${RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT// }" ]]; }; then
|
||||
echo "candidate_artifact_json cannot be combined with release package specs." >&2
|
||||
exit 1
|
||||
fi
|
||||
qa_live_matrix_enabled=true
|
||||
qa_live_buzz_enabled=true
|
||||
qa_live_telegram_enabled=true
|
||||
@@ -749,7 +756,29 @@ jobs:
|
||||
(.imageArtifactDigest | hex64) and
|
||||
(.imageArtifactRunId | tostring | digits) and
|
||||
(.imageArtifactRunAttempt | tostring | digits) and
|
||||
(.imageArchiveSha256 | hex64)' \
|
||||
(.imageArchiveSha256 | hex64) and
|
||||
(
|
||||
(
|
||||
(has("prepublishPluginRegistryArtifactName") | not) and
|
||||
(has("prepublishPluginRegistryArtifactId") | not) and
|
||||
(has("prepublishPluginRegistryArtifactDigest") | not) and
|
||||
(has("prepublishPluginRegistryArtifactRunId") | not) and
|
||||
(has("prepublishPluginRegistryArtifactRunAttempt") | not) and
|
||||
(has("prepublishPluginRegistryManifestSha256") | not)
|
||||
) or
|
||||
(
|
||||
(.prepublishPluginRegistryArtifactName | type == "string" and length > 0) and
|
||||
(.prepublishPluginRegistryArtifactId | tostring | digits) and
|
||||
(.prepublishPluginRegistryArtifactDigest | hex64) and
|
||||
(.prepublishPluginRegistryArtifactRunId | tostring | digits) and
|
||||
(.prepublishPluginRegistryArtifactRunAttempt | tostring | digits) and
|
||||
(.prepublishPluginRegistryManifestSha256 | hex64) and
|
||||
.prepublishPluginRegistryArtifactName ==
|
||||
("docker-e2e-prepublish-plugin-registry-" +
|
||||
(.prepublishPluginRegistryArtifactRunId | tostring) + "-" +
|
||||
(.prepublishPluginRegistryArtifactRunAttempt | tostring))
|
||||
)
|
||||
)' \
|
||||
<<< "$CANDIDATE_ARTIFACT_JSON" >/dev/null
|
||||
|
||||
install_smoke_release_checks:
|
||||
@@ -901,6 +930,13 @@ jobs:
|
||||
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
|
||||
package_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }}
|
||||
package_version: ${{ needs.prepare_release_package.outputs.package_version }}
|
||||
enable_prepublish_plugin_registry: ${{ needs.resolve_target.outputs.release_package_spec == '' }}
|
||||
prepublish_plugin_registry_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}
|
||||
codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }}
|
||||
shared_image_artifact_namespace: release-docker
|
||||
shared_image_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactName || '' }}
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
include_openwebui: true
|
||||
include_live_suites: true
|
||||
allow_unreleased_changelog: true
|
||||
enable_prepublish_plugin_registry: true
|
||||
shared_image_artifact_namespace: scheduled-live
|
||||
shared_image_policy: no-push-artifact
|
||||
secrets:
|
||||
|
||||
@@ -841,6 +841,13 @@ jobs:
|
||||
package_sha256: ${{ needs.resolve_package.outputs.package_sha256 }}
|
||||
package_source_sha: ${{ needs.resolve_package.outputs.package_source_sha }}
|
||||
package_version: ${{ needs.resolve_package.outputs.package_version }}
|
||||
enable_prepublish_plugin_registry: ${{ contains(fromJSON('["artifact","ref"]'), inputs.source) }}
|
||||
prepublish_plugin_registry_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}
|
||||
include_live_suites: ${{ needs.resolve_package.outputs.include_live_suites == 'true' }}
|
||||
live_models_only: false
|
||||
shared_image_artifact_namespace: ${{ inputs.shared_image_artifact_namespace }}
|
||||
|
||||
@@ -574,6 +574,13 @@ jobs:
|
||||
package_source_sha: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageSourceSha || '' }}
|
||||
package_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageSha256 || '' }}
|
||||
package_version: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageVersion || '' }}
|
||||
enable_prepublish_plugin_registry: true
|
||||
prepublish_plugin_registry_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}
|
||||
shared_image_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactName || '' }}
|
||||
shared_image_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactId || '' }}
|
||||
shared_image_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactDigest || '' }}
|
||||
|
||||
@@ -29,6 +29,10 @@ function githubOutputs(plan) {
|
||||
`needs_functional_image=${boolOutput(needs.functionalImage)}`,
|
||||
`needs_live_image=${boolOutput(needs.liveImage)}`,
|
||||
`needs_package=${boolOutput(needs.package)}`,
|
||||
`needs_prepublish_plugin_registry=${boolOutput(needs.prepublishPluginRegistry)}`,
|
||||
`required_prepublish_plugin_packages=${JSON.stringify(
|
||||
plan.requiredPrepublishPluginPackages ?? [],
|
||||
)}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,19 @@ export function resolveScenarioConfigSteps(scenario: string): Array<{
|
||||
intent: string;
|
||||
argv: string[];
|
||||
}>;
|
||||
export function resolveUpgradeSurvivorConfigSteps(scenario?: string): Array<{
|
||||
id: string;
|
||||
intent: string;
|
||||
argv: string[];
|
||||
}>;
|
||||
export function resolveUpgradeSurvivorConfigStepsForBaseline(
|
||||
scenario?: string,
|
||||
baselineVersion?: string | null,
|
||||
): Array<{
|
||||
id: string;
|
||||
intent: string;
|
||||
argv: string[];
|
||||
}>;
|
||||
export function resolveUpgradeSurvivorOpenClawCommand(
|
||||
argv: unknown,
|
||||
params?: Record<string, unknown>,
|
||||
|
||||
@@ -200,6 +200,15 @@ const recipe = [
|
||||
},
|
||||
];
|
||||
|
||||
export function resolveUpgradeSurvivorConfigSteps(scenario = "base") {
|
||||
const validateStep = recipe.at(-1);
|
||||
return [
|
||||
...recipe.slice(0, -1),
|
||||
...resolveScenarioConfigSteps(scenario),
|
||||
...(validateStep ? [validateStep] : []),
|
||||
];
|
||||
}
|
||||
|
||||
function selectedScenario() {
|
||||
return process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
|
||||
}
|
||||
@@ -252,6 +261,16 @@ function adaptStepForBaseline(step, baselineVersion, summary) {
|
||||
return step;
|
||||
}
|
||||
|
||||
export function resolveUpgradeSurvivorConfigStepsForBaseline(
|
||||
scenario = "base",
|
||||
baselineVersion = null,
|
||||
) {
|
||||
const summary = { skippedIntents: [] };
|
||||
return resolveUpgradeSurvivorConfigSteps(scenario)
|
||||
.map((step) => adaptStepForBaseline(step, baselineVersion, summary))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function resolveUpgradeSurvivorOpenClawCommand(argv, params = {}) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
if (platform === "win32") {
|
||||
@@ -331,7 +350,7 @@ function applyRecipe() {
|
||||
steps: [],
|
||||
};
|
||||
|
||||
for (const step of [...recipe.slice(0, -1), ...scenarioSteps, recipe.at(-1)]) {
|
||||
for (const step of resolveUpgradeSurvivorConfigSteps(scenario)) {
|
||||
const adaptedStep = adaptStepForBaseline(step, baselineVersion, summary);
|
||||
if (!adaptedStep) {
|
||||
continue;
|
||||
|
||||
@@ -3,6 +3,8 @@ set -Eeuo pipefail
|
||||
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
|
||||
SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}"
|
||||
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
@@ -16,7 +18,9 @@ export GATEWAY_AUTH_TOKEN_REF="upgrade-survivor-token"
|
||||
export OPENAI_API_KEY="sk-openclaw-upgrade-survivor"
|
||||
export DISCORD_BOT_TOKEN="upgrade-survivor-discord-token"
|
||||
export TELEGRAM_BOT_TOKEN="123456:upgrade-survivor-telegram-token"
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
if [ "$SCENARIO" = "feishu-channel" ]; then
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
fi
|
||||
export MATRIX_ACCESS_TOKEN="upgrade-survivor-matrix-token"
|
||||
export BRAVE_API_KEY="BSA_upgrade_survivor_brave_key"
|
||||
|
||||
@@ -43,7 +47,6 @@ PHASE_LOG="$ARTIFACT_ROOT/phases.jsonl"
|
||||
BASELINE_RAW="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE:?missing OPENCLAW_UPGRADE_SURVIVOR_BASELINE}"
|
||||
CANDIDATE_KIND="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND:-tarball}"
|
||||
CANDIDATE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
|
||||
SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}"
|
||||
UPDATE_RESTART_MODE="${OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE:-manual}"
|
||||
ROOT_MANAGED_VPS="${OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS:-0}"
|
||||
COMMAND_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_COMMAND_TIMEOUT:-900s}"
|
||||
@@ -365,16 +368,49 @@ TS
|
||||
echo "Seeded source-only plugin shadow: $shadow_root"
|
||||
}
|
||||
|
||||
configure_configured_plugin_install_fixture_registry() {
|
||||
configured_plugin_installs_enabled || return 0
|
||||
|
||||
local fixture_root="$ARTIFACT_ROOT/configured-plugin-installs-npm-fixture"
|
||||
configure_plugin_registry() {
|
||||
local fixture_root="$ARTIFACT_ROOT/plugin-registry"
|
||||
local package_dir="$fixture_root/package"
|
||||
local tarball="$fixture_root/openclaw-brave-plugin-2026.5.2.tgz"
|
||||
local port_file="$fixture_root/npm-registry-port"
|
||||
local log_file="$fixture_root/npm-registry.log"
|
||||
mkdir -p "$package_dir"
|
||||
FIXTURE_PACKAGE_DIR="$package_dir" node <<'NODE'
|
||||
local registry_args=()
|
||||
|
||||
if [ -n "${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR:-}" ]; then
|
||||
local manifest="$OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR/prepublish-plugin-registry.json"
|
||||
local registry_rows
|
||||
registry_rows="$(
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST="$manifest" node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const manifestPath = process.env.PREPUBLISH_PLUGIN_REGISTRY_MANIFEST;
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
if (!Array.isArray(manifest.packages) || manifest.packages.length === 0) {
|
||||
throw new Error("prepublish plugin registry manifest must contain packages");
|
||||
}
|
||||
for (const entry of manifest.packages) {
|
||||
if (
|
||||
typeof entry.name !== "string" ||
|
||||
typeof entry.version !== "string" ||
|
||||
typeof entry.tarball !== "string" ||
|
||||
path.basename(entry.tarball) !== entry.tarball
|
||||
) {
|
||||
throw new Error("invalid prepublish plugin registry package entry");
|
||||
}
|
||||
process.stdout.write(
|
||||
`${entry.name}\t${entry.version}\t${path.join(path.dirname(manifestPath), entry.tarball)}\n`,
|
||||
);
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
while IFS=$'\t' read -r package_name package_version package_tarball; do
|
||||
registry_args+=("$package_name" "$package_version" "$package_tarball")
|
||||
done <<<"$registry_rows"
|
||||
fi
|
||||
|
||||
if configured_plugin_installs_enabled; then
|
||||
mkdir -p "$package_dir"
|
||||
FIXTURE_PACKAGE_DIR="$package_dir" node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const root = process.env.FIXTURE_PACKAGE_DIR;
|
||||
@@ -424,13 +460,19 @@ fs.writeFileSync(
|
||||
`module.exports = { id: "brave", name: "Brave Fixture", register() {} };\n`,
|
||||
);
|
||||
NODE
|
||||
tar -czf "$tarball" -C "$fixture_root" package
|
||||
tar -czf "$tarball" -C "$fixture_root" package
|
||||
registry_args+=("@openclaw/brave-plugin" "2026.5.2" "$tarball")
|
||||
fi
|
||||
|
||||
if [ "${#registry_args[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$fixture_root"
|
||||
OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org \
|
||||
node scripts/e2e/lib/plugins/npm-registry-server.mjs \
|
||||
"$port_file" \
|
||||
"@openclaw/brave-plugin" \
|
||||
"2026.5.2" \
|
||||
"$tarball" \
|
||||
"${registry_args[@]}" \
|
||||
>"$log_file" 2>&1 &
|
||||
plugin_registry_pid="$!"
|
||||
|
||||
@@ -448,7 +490,7 @@ NODE
|
||||
done
|
||||
|
||||
openclaw_e2e_print_log "$log_file" >&2
|
||||
echo "Timed out waiting for configured plugin install npm fixture registry." >&2
|
||||
echo "Timed out waiting for upgrade survivor npm registry." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -1270,10 +1312,10 @@ phase assert-baseline assert_baseline_state
|
||||
phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink
|
||||
phase resolve-candidate resolve_candidate_version
|
||||
phase prepare-update-restart-probe prepare_update_restart_probe
|
||||
phase configure-plugin-registry configure_plugin_registry
|
||||
phase update-candidate update_candidate
|
||||
phase root-managed-vps-cli-usable assert_root_managed_vps_cli_usable
|
||||
phase assert-legacy-plugin-dependency-debris-before-doctor assert_legacy_plugin_dependency_debris_before_doctor
|
||||
phase configure-configured-plugin-install-fixture-registry configure_configured_plugin_install_fixture_registry
|
||||
phase doctor run_doctor
|
||||
phase assert-legacy-plugin-dependency-debris-cleaned assert_legacy_plugin_dependency_debris_cleaned
|
||||
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
|
||||
|
||||
@@ -56,6 +56,7 @@ LANE_ARTIFACT_SUFFIX="$(resolve_lane_artifact_suffix)"
|
||||
LANE_ARTIFACT_SUFFIX="${LANE_ARTIFACT_SUFFIX//[^A-Za-z0-9_.-]/_}"
|
||||
ARTIFACT_DIR="${OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/upgrade-survivor/$LANE_ARTIFACT_SUFFIX}"
|
||||
DOCKER_RUN_USER_ARGS=()
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARGS=()
|
||||
PROBE_ENV_ARGS=(
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_PROBE_TIMEOUT_MS="$PROBE_TIMEOUT_MS"
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_PROBE_ATTEMPT_TIMEOUT_MS="$PROBE_ATTEMPT_TIMEOUT_MS"
|
||||
@@ -71,6 +72,19 @@ if [ -n "${OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_DEGRADED:-}" ]; then
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_DEGRADED="$OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_DEGRADED"
|
||||
)
|
||||
fi
|
||||
if [ -n "${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR:-}" ]; then
|
||||
PREPUBLISH_PLUGIN_REGISTRY_DIR="$(
|
||||
cd "$OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR" && pwd
|
||||
)"
|
||||
if [ ! -f "$PREPUBLISH_PLUGIN_REGISTRY_DIR/prepublish-plugin-registry.json" ]; then
|
||||
echo "Prepublish plugin registry manifest is missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARGS=(
|
||||
-e OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR=/tmp/openclaw-prepublish-plugin-registry
|
||||
-v "$PREPUBLISH_PLUGIN_REGISTRY_DIR:/tmp/openclaw-prepublish-plugin-registry:ro"
|
||||
)
|
||||
fi
|
||||
cleanup_outer() {
|
||||
docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"
|
||||
}
|
||||
@@ -164,6 +178,7 @@ if [ "${OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE:-0}" = "1" ]; then
|
||||
"${PROBE_ENV_ARGS[@]}" \
|
||||
-v "$ARTIFACT_DIR:/tmp/openclaw-upgrade-survivor-artifacts" \
|
||||
-v "$HARNESS_ROOT_DIR/scripts/e2e/lib/upgrade-survivor/run.sh:/tmp/openclaw-upgrade-survivor-run.sh:ro" \
|
||||
"${PREPUBLISH_PLUGIN_REGISTRY_ARGS[@]}" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"${DOCKER_RUN_USER_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
@@ -192,6 +207,7 @@ docker_e2e_run_with_harness \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS="$STATUS_BUDGET_SECONDS" \
|
||||
"${PROBE_ENV_ARGS[@]}" \
|
||||
-v "$ARTIFACT_DIR:/tmp/openclaw-upgrade-survivor-artifacts" \
|
||||
"${PREPUBLISH_PLUGIN_REGISTRY_ARGS[@]}" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"${DOCKER_RUN_USER_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
@@ -224,7 +240,9 @@ export GATEWAY_AUTH_TOKEN_REF="upgrade-survivor-token"
|
||||
export OPENAI_API_KEY="sk-openclaw-upgrade-survivor"
|
||||
export DISCORD_BOT_TOKEN="upgrade-survivor-discord-token"
|
||||
export TELEGRAM_BOT_TOKEN="123456:upgrade-survivor-telegram-token"
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
if [ "${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}" = "feishu-channel" ]; then
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
fi
|
||||
export BRAVE_API_KEY="BSA_upgrade_survivor_brave_key"
|
||||
|
||||
UPDATE_RESTART_MODE="${OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE:-manual}"
|
||||
@@ -257,16 +275,49 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
configure_configured_plugin_install_fixture_registry() {
|
||||
[ "${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}" = "configured-plugin-installs" ] || return 0
|
||||
|
||||
local fixture_root="$OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_ROOT/configured-plugin-installs-npm-fixture"
|
||||
configure_plugin_registry() {
|
||||
local fixture_root="$OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_ROOT/plugin-registry"
|
||||
local package_dir="$fixture_root/package"
|
||||
local tarball="$fixture_root/openclaw-brave-plugin-2026.5.2.tgz"
|
||||
local port_file="$fixture_root/npm-registry-port"
|
||||
local log_file="$fixture_root/npm-registry.log"
|
||||
mkdir -p "$package_dir"
|
||||
FIXTURE_PACKAGE_DIR="$package_dir" node <<'"'"'NODE'"'"'
|
||||
local registry_args=()
|
||||
|
||||
if [ -n "${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR:-}" ]; then
|
||||
local manifest="$OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR/prepublish-plugin-registry.json"
|
||||
local registry_rows
|
||||
registry_rows="$(
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST="$manifest" node <<'"'"'NODE'"'"'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const manifestPath = process.env.PREPUBLISH_PLUGIN_REGISTRY_MANIFEST;
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
if (!Array.isArray(manifest.packages) || manifest.packages.length === 0) {
|
||||
throw new Error("prepublish plugin registry manifest must contain packages");
|
||||
}
|
||||
for (const entry of manifest.packages) {
|
||||
if (
|
||||
typeof entry.name !== "string" ||
|
||||
typeof entry.version !== "string" ||
|
||||
typeof entry.tarball !== "string" ||
|
||||
path.basename(entry.tarball) !== entry.tarball
|
||||
) {
|
||||
throw new Error("invalid prepublish plugin registry package entry");
|
||||
}
|
||||
process.stdout.write(
|
||||
`${entry.name}\t${entry.version}\t${path.join(path.dirname(manifestPath), entry.tarball)}\n`,
|
||||
);
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
while IFS=$'"'"'\t'"'"' read -r package_name package_version package_tarball; do
|
||||
registry_args+=("$package_name" "$package_version" "$package_tarball")
|
||||
done <<<"$registry_rows"
|
||||
fi
|
||||
|
||||
if [ "${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}" = "configured-plugin-installs" ]; then
|
||||
mkdir -p "$package_dir"
|
||||
FIXTURE_PACKAGE_DIR="$package_dir" node <<'"'"'NODE'"'"'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const root = process.env.FIXTURE_PACKAGE_DIR;
|
||||
@@ -316,13 +367,19 @@ fs.writeFileSync(
|
||||
`module.exports = { id: "brave", name: "Brave Fixture", register() {} };\n`,
|
||||
);
|
||||
NODE
|
||||
tar -czf "$tarball" -C "$fixture_root" package
|
||||
tar -czf "$tarball" -C "$fixture_root" package
|
||||
registry_args+=("@openclaw/brave-plugin" "2026.5.2" "$tarball")
|
||||
fi
|
||||
|
||||
if [ "${#registry_args[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$fixture_root"
|
||||
OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org \
|
||||
node scripts/e2e/lib/plugins/npm-registry-server.mjs \
|
||||
"$port_file" \
|
||||
"@openclaw/brave-plugin" \
|
||||
"2026.5.2" \
|
||||
"$tarball" \
|
||||
"${registry_args[@]}" \
|
||||
>"$log_file" 2>&1 &
|
||||
plugin_registry_pid="$!"
|
||||
|
||||
@@ -340,7 +397,7 @@ NODE
|
||||
done
|
||||
|
||||
openclaw_e2e_print_log "$log_file" >&2
|
||||
echo "Timed out waiting for configured plugin install npm fixture registry." >&2
|
||||
echo "Timed out waiting for upgrade survivor npm registry." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -364,6 +421,7 @@ if [ "$UPDATE_RESTART_MODE" = "auto-auth" ]; then
|
||||
prepare_update_restart_probe_current_install "$PORT" "$GATEWAY_LOG"
|
||||
fi
|
||||
|
||||
configure_plugin_registry
|
||||
echo "Running package update against the mounted tarball..."
|
||||
update_args=(update --tag "${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" --yes --json)
|
||||
if [ "$UPDATE_RESTART_MODE" != "auto-auth" ]; then
|
||||
@@ -384,7 +442,6 @@ if [ "$UPDATE_RESTART_MODE" = "auto-auth" ]; then
|
||||
echo "Skipping doctor repair until after restart proof."
|
||||
else
|
||||
echo "Running non-interactive doctor repair..."
|
||||
configure_configured_plugin_install_fixture_registry
|
||||
if ! openclaw_e2e_maybe_timeout "$command_timeout" openclaw doctor --fix --non-interactive >/tmp/openclaw-upgrade-survivor-doctor.log 2>&1; then
|
||||
echo "openclaw doctor failed" >&2
|
||||
openclaw_e2e_print_log /tmp/openclaw-upgrade-survivor-doctor.log >&2
|
||||
|
||||
@@ -43,12 +43,14 @@ export type DockerE2ePlan = {
|
||||
lanes: DockerE2ePlanLane[];
|
||||
mainLanes: DockerE2ePlanLane[];
|
||||
omittedUnsupportedLanes: string[];
|
||||
requiredPrepublishPluginPackages: string[];
|
||||
needs: {
|
||||
bareImage: boolean;
|
||||
e2eImage: boolean;
|
||||
functionalImage: boolean;
|
||||
liveImage: boolean;
|
||||
package: boolean;
|
||||
prepublishPluginRegistry: boolean;
|
||||
};
|
||||
profile: string;
|
||||
releaseProfile?: DockerE2eReleaseProfile;
|
||||
@@ -79,6 +81,7 @@ export function lanesNeedE2eImageKind(
|
||||
kind: DockerE2eImageKind,
|
||||
): boolean;
|
||||
export function lanesNeedOpenClawPackage(poolLanes: DockerE2eLane[]): boolean;
|
||||
export function requiredPrepublishPluginPackagesForLanes(poolLanes: DockerE2eLane[]): string[];
|
||||
export function findLaneByName(name: string): DockerE2eLane | undefined;
|
||||
export function resolveDockerE2ePlan(options: DockerE2ePlanOptions): {
|
||||
omittedUnsupportedLaneNames: string[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { resolveUpgradeSurvivorConfigStepsForBaseline } from "../e2e/lib/upgrade-survivor/config-recipe.mjs";
|
||||
import {
|
||||
BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS,
|
||||
DEFAULT_LIVE_RETRIES,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
releasePathChunkLanes,
|
||||
tailLanes,
|
||||
} from "./docker-e2e-scenarios.mjs";
|
||||
import officialExternalChannelCatalog from "./official-external-channel-catalog.json" with { type: "json" };
|
||||
|
||||
export { DEFAULT_LIVE_RETRIES };
|
||||
export { normalizeReleaseProfile };
|
||||
@@ -636,11 +638,72 @@ function unique(values) {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
|
||||
function upgradeSurvivorScenarioForLane(poolLane) {
|
||||
if (!poolLane.upgradeSurvivorScenario) {
|
||||
return undefined;
|
||||
}
|
||||
const match = /(?:^|\s)OPENCLAW_UPGRADE_SURVIVOR_SCENARIO=(?:'([^']+)'|"([^"]+)"|([^\s]+))/u.exec(
|
||||
poolLane.command,
|
||||
);
|
||||
return match?.[1] ?? match?.[2] ?? match?.[3] ?? poolLane.upgradeSurvivorScenario;
|
||||
}
|
||||
|
||||
function upgradeSurvivorBaselineVersionForLane(poolLane) {
|
||||
const match =
|
||||
/(?:^|\s)OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=(?:'([^']+)'|"([^"]+)"|([^\s]+))/u.exec(
|
||||
poolLane.command,
|
||||
);
|
||||
const spec = match?.[1] ?? match?.[2] ?? match?.[3];
|
||||
return /(?:^|\/|@)(\d{4}\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(spec ?? "")?.[1] ?? null;
|
||||
}
|
||||
|
||||
function configuredChannelIdsForLane(poolLane, scenario) {
|
||||
const channelIds = new Set();
|
||||
const baselineVersion = upgradeSurvivorBaselineVersionForLane(poolLane);
|
||||
for (const step of resolveUpgradeSurvivorConfigStepsForBaseline(scenario, baselineVersion)) {
|
||||
if (step.argv?.[0] !== "config" || step.argv?.[1] !== "set") {
|
||||
continue;
|
||||
}
|
||||
const match = /^channels\.([a-z0-9][a-z0-9-]*)$/u.exec(step.argv[2] ?? "");
|
||||
if (match) {
|
||||
channelIds.add(match[1]);
|
||||
}
|
||||
}
|
||||
return channelIds;
|
||||
}
|
||||
|
||||
export function requiredPrepublishPluginPackagesForLanes(poolLanes) {
|
||||
const configuredChannelIds = new Set();
|
||||
for (const poolLane of poolLanes) {
|
||||
const scenario = upgradeSurvivorScenarioForLane(poolLane);
|
||||
if (!scenario) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of configuredChannelIdsForLane(poolLane, scenario)) {
|
||||
configuredChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
return (officialExternalChannelCatalog.entries ?? [])
|
||||
.filter((entry) => {
|
||||
const channelId = entry.openclaw?.channel?.id;
|
||||
const install = entry.openclaw?.install;
|
||||
return (
|
||||
typeof entry.name === "string" &&
|
||||
configuredChannelIds.has(channelId) &&
|
||||
install?.defaultChoice === "npm" &&
|
||||
install?.npmSpec === entry.name
|
||||
);
|
||||
})
|
||||
.map((entry) => entry.name)
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function buildPlanJson(params) {
|
||||
const scheduledLanes = [...params.orderedLanes, ...params.orderedTailLanes];
|
||||
const imageKinds = unique(scheduledLanes.map((poolLane) => poolLane.e2eImageKind)).toSorted(
|
||||
(a, b) => a.localeCompare(b),
|
||||
);
|
||||
const requiredPrepublishPluginPackages = requiredPrepublishPluginPackagesForLanes(scheduledLanes);
|
||||
return {
|
||||
chunk: params.releaseChunk || undefined,
|
||||
credentials: unique(scheduledLanes.flatMap(laneCredentialRequirements)).toSorted((a, b) =>
|
||||
@@ -661,12 +724,14 @@ function buildPlanJson(params) {
|
||||
})),
|
||||
mainLanes: params.orderedLanes.map((poolLane) => poolLane.name),
|
||||
omittedUnsupportedLanes: params.omittedUnsupportedLaneNames,
|
||||
requiredPrepublishPluginPackages,
|
||||
needs: {
|
||||
bareImage: imageKinds.includes("bare"),
|
||||
e2eImage: imageKinds.length > 0,
|
||||
functionalImage: imageKinds.includes("functional"),
|
||||
liveImage: scheduledLanes.some((poolLane) => poolLane.needsLiveImage),
|
||||
package: lanesNeedOpenClawPackage(scheduledLanes),
|
||||
prepublishPluginRegistry: requiredPrepublishPluginPackages.length > 0,
|
||||
},
|
||||
profile: params.profile,
|
||||
releaseProfile: params.releaseProfile,
|
||||
|
||||
@@ -16,6 +16,7 @@ export type DockerE2eLane = {
|
||||
retryPatterns: RegExp[];
|
||||
stateScenario?: string;
|
||||
timeoutMs?: number;
|
||||
upgradeSurvivorScenario?: string;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,12 +16,22 @@ const OPENWEBUI_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
const RELEASE_OPENWEBUI_COMMAND =
|
||||
"OPENCLAW_OPENWEBUI_MODEL=openai/gpt-5.4-mini OPENCLAW_OPENWEBUI_PROVIDER_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui";
|
||||
export const BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS = 24;
|
||||
const upgradeSurvivorCommand = "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:upgrade-survivor";
|
||||
const rootManagedVpsUpgradeCommand =
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:root-managed-vps-upgrade";
|
||||
const updateRestartAuthCommand = liveDockerScriptCommand(
|
||||
"e2e/upgrade-survivor-docker.sh",
|
||||
'OPENCLAW_DOCKER_E2E_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest} OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}',
|
||||
const upgradeSurvivorCommand = upgradeSurvivorScriptCommand();
|
||||
const publishedUpgradeSurvivorCommand = upgradeSurvivorScriptCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
);
|
||||
const rootManagedVpsUpgradeCommand = upgradeSurvivorScriptCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.5.7}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
);
|
||||
const updateRestartAuthCommand = upgradeSurvivorScriptCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
);
|
||||
const updateMigrationCommand = upgradeSurvivorScriptCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.4.23}"; export OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-plugin-deps-cleanup}"',
|
||||
);
|
||||
const updateRunPackageSelfUpgradeCommand =
|
||||
"OPENCLAW_QA_ALLOW_UPDATE_RUN_SELF=1 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-run-package-self-upgrade";
|
||||
@@ -38,8 +48,18 @@ const LIVE_RETRY_PATTERNS = [
|
||||
|
||||
function liveDockerScriptCommand(script, envPrefix = "", options = {}) {
|
||||
const prefix = envPrefix ? `${envPrefix} ` : "";
|
||||
const shellPrelude = options.shellPrelude ? `${options.shellPrelude}; ` : "";
|
||||
const skipBuild = options.skipBuild === false ? "" : "OPENCLAW_SKIP_DOCKER_BUILD=1 ";
|
||||
return `${prefix}${skipBuild}bash -c 'harness="\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${LIVE_DOCKER_DEFAULT_HARNESS_DIR}}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/${script}"'`;
|
||||
return `${prefix}${skipBuild}bash -c '${shellPrelude}harness="\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${LIVE_DOCKER_DEFAULT_HARNESS_DIR}}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/${script}"'`;
|
||||
}
|
||||
|
||||
function upgradeSurvivorScriptCommand(envPrefix = "", shellPrelude = "") {
|
||||
const rootPrefix = 'OPENCLAW_DOCKER_E2E_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}"';
|
||||
return liveDockerScriptCommand(
|
||||
"e2e/upgrade-survivor-docker.sh",
|
||||
envPrefix ? `${rootPrefix} ${envPrefix}` : rootPrefix,
|
||||
{ shellPrelude },
|
||||
);
|
||||
}
|
||||
|
||||
function lane(name, command, options = {}) {
|
||||
@@ -60,6 +80,7 @@ function lane(name, command, options = {}) {
|
||||
resources: options.resources ?? [],
|
||||
stateScenario: options.stateScenario,
|
||||
timeoutMs: options.timeoutMs,
|
||||
upgradeSurvivorScenario: options.upgradeSurvivorScenario,
|
||||
weight: options.weight ?? 1,
|
||||
};
|
||||
}
|
||||
@@ -161,25 +182,25 @@ function createPackageUpdateMaintenanceLanes() {
|
||||
npmLane("upgrade-survivor", upgradeSurvivorCommand, {
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 20 * 60 * 1000,
|
||||
upgradeSurvivorScenario: "base",
|
||||
weight: 3,
|
||||
}),
|
||||
npmLane("published-upgrade-survivor", publishedUpgradeSurvivorCommand, {
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 25 * 60 * 1000,
|
||||
upgradeSurvivorScenario: "base",
|
||||
weight: 3,
|
||||
}),
|
||||
npmLane(
|
||||
"published-upgrade-survivor",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:published-upgrade-survivor",
|
||||
{
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 25 * 60 * 1000,
|
||||
weight: 3,
|
||||
},
|
||||
),
|
||||
npmLane("root-managed-vps-upgrade", rootManagedVpsUpgradeCommand, {
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 25 * 60 * 1000,
|
||||
upgradeSurvivorScenario: "base",
|
||||
weight: 3,
|
||||
}),
|
||||
npmLane("update-restart-auth", updateRestartAuthCommand, {
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 25 * 60 * 1000,
|
||||
upgradeSurvivorScenario: "base",
|
||||
weight: 3,
|
||||
}),
|
||||
npmLane("update-run-package-self-upgrade", updateRunPackageSelfUpgradeCommand, {
|
||||
@@ -519,9 +540,10 @@ export const mainLanes = [
|
||||
{ resources: ["npm"], stateScenario: "empty", weight: 3 },
|
||||
),
|
||||
...createPackageUpdateMaintenanceLanes(),
|
||||
npmLane("update-migration", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-migration", {
|
||||
npmLane("update-migration", updateMigrationCommand, {
|
||||
stateScenario: "upgrade-survivor",
|
||||
timeoutMs: 30 * 60 * 1000,
|
||||
upgradeSurvivorScenario: "plugin-deps-cleanup",
|
||||
weight: 3,
|
||||
}),
|
||||
lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export const PREPUBLISH_PLUGIN_REGISTRY_MANIFEST: "prepublish-plugin-registry.json";
|
||||
export function validatePrepublishPluginRegistryArtifact(params: {
|
||||
artifactDir: string;
|
||||
expectedCandidateVersion: string;
|
||||
expectedManifestSha256: string;
|
||||
expectedSourceSha: string;
|
||||
requiredPackages: string[];
|
||||
}): {
|
||||
manifest: {
|
||||
schema: string;
|
||||
schemaVersion: number;
|
||||
sourceSha: string;
|
||||
candidateVersion: string;
|
||||
packages: Array<{
|
||||
name: string;
|
||||
version: string;
|
||||
tarball: string;
|
||||
sha256: string;
|
||||
}>;
|
||||
};
|
||||
manifestPath: string;
|
||||
manifestSha256: string;
|
||||
};
|
||||
export function createPrepublishPluginRegistryArtifact(params: {
|
||||
repoRoot: string;
|
||||
outputDir: string;
|
||||
sourceSha: string;
|
||||
candidateVersion: string;
|
||||
requiredPackages: string[];
|
||||
}): ReturnType<typeof validatePrepublishPluginRegistryArtifact>;
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const PREPUBLISH_PLUGIN_REGISTRY_MANIFEST = "prepublish-plugin-registry.json";
|
||||
const SCHEMA = "openclaw.prepublish-plugin-registry/v1";
|
||||
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
||||
const SOURCE_SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
||||
const PACKAGE_NAME_PATTERN = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/u;
|
||||
const TARBALL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/u;
|
||||
|
||||
function readJson(file, label) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`cannot read ${label} ${file}: ${String(error)}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
function sha256File(file) {
|
||||
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
}
|
||||
|
||||
function normalizeRequiredPackages(value) {
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.some((name) => typeof name !== "string" || !PACKAGE_NAME_PATTERN.test(name))
|
||||
) {
|
||||
throw new Error("required packages must be an array of scoped npm package names");
|
||||
}
|
||||
const sorted = [...new Set(value)].toSorted((a, b) => a.localeCompare(b));
|
||||
if (sorted.length !== value.length) {
|
||||
throw new Error("required packages must not contain duplicates");
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function readTarballPackageJson(tarball) {
|
||||
try {
|
||||
return JSON.parse(
|
||||
execFileSync("tar", ["-xOf", tarball, "package/package.json"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`cannot read package/package.json from ${tarball}: ${String(error)}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateManifestShape(manifest) {
|
||||
if (
|
||||
!manifest ||
|
||||
typeof manifest !== "object" ||
|
||||
Array.isArray(manifest) ||
|
||||
manifest.schema !== SCHEMA ||
|
||||
manifest.schemaVersion !== 1 ||
|
||||
!SOURCE_SHA_PATTERN.test(manifest.sourceSha ?? "") ||
|
||||
typeof manifest.candidateVersion !== "string" ||
|
||||
manifest.candidateVersion.length === 0 ||
|
||||
/\s/u.test(manifest.candidateVersion) ||
|
||||
!Array.isArray(manifest.packages)
|
||||
) {
|
||||
throw new Error("prepublish plugin registry manifest has an invalid top-level shape");
|
||||
}
|
||||
const names = new Set();
|
||||
const tarballs = new Set();
|
||||
for (const entry of manifest.packages) {
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry !== "object" ||
|
||||
Array.isArray(entry) ||
|
||||
!PACKAGE_NAME_PATTERN.test(entry.name ?? "") ||
|
||||
entry.version !== manifest.candidateVersion ||
|
||||
!TARBALL_PATTERN.test(entry.tarball ?? "") ||
|
||||
path.basename(entry.tarball) !== entry.tarball ||
|
||||
!SHA256_PATTERN.test(entry.sha256 ?? "")
|
||||
) {
|
||||
throw new Error("prepublish plugin registry manifest contains an invalid package entry");
|
||||
}
|
||||
if (names.has(entry.name)) {
|
||||
throw new Error(
|
||||
`prepublish plugin registry manifest contains duplicate package ${entry.name}`,
|
||||
);
|
||||
}
|
||||
if (tarballs.has(entry.tarball)) {
|
||||
throw new Error(
|
||||
`prepublish plugin registry manifest contains duplicate tarball ${entry.tarball}`,
|
||||
);
|
||||
}
|
||||
names.add(entry.name);
|
||||
tarballs.add(entry.tarball);
|
||||
}
|
||||
const sortedNames = [...names].toSorted((a, b) => a.localeCompare(b));
|
||||
if (
|
||||
JSON.stringify(sortedNames) !== JSON.stringify(manifest.packages.map((entry) => entry.name))
|
||||
) {
|
||||
throw new Error("prepublish plugin registry manifest packages must be sorted by name");
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePrepublishPluginRegistryArtifact(params) {
|
||||
if (!SOURCE_SHA_PATTERN.test(params.expectedSourceSha ?? "")) {
|
||||
throw new Error("expectedSourceSha must be a full lowercase commit SHA");
|
||||
}
|
||||
if (
|
||||
typeof params.expectedCandidateVersion !== "string" ||
|
||||
params.expectedCandidateVersion.length === 0 ||
|
||||
/\s/u.test(params.expectedCandidateVersion)
|
||||
) {
|
||||
throw new Error("expectedCandidateVersion must be a non-empty package version");
|
||||
}
|
||||
if (!SHA256_PATTERN.test(params.expectedManifestSha256 ?? "")) {
|
||||
throw new Error("expectedManifestSha256 must be a lowercase SHA-256");
|
||||
}
|
||||
const artifactDir = path.resolve(params.artifactDir);
|
||||
const manifestPath = path.join(artifactDir, PREPUBLISH_PLUGIN_REGISTRY_MANIFEST);
|
||||
const manifestSha256 = sha256File(manifestPath);
|
||||
if (manifestSha256 !== params.expectedManifestSha256) {
|
||||
throw new Error("prepublish plugin registry manifest SHA-256 differs from the immutable tuple");
|
||||
}
|
||||
const manifest = readJson(manifestPath, "prepublish plugin registry manifest");
|
||||
validateManifestShape(manifest);
|
||||
if (manifest.sourceSha !== params.expectedSourceSha) {
|
||||
throw new Error("prepublish plugin registry source SHA differs from the selected release SHA");
|
||||
}
|
||||
if (manifest.candidateVersion !== params.expectedCandidateVersion) {
|
||||
throw new Error("prepublish plugin registry version differs from the root package candidate");
|
||||
}
|
||||
const requiredPackages = normalizeRequiredPackages(params.requiredPackages);
|
||||
if (
|
||||
JSON.stringify(manifest.packages.map((entry) => entry.name)) !==
|
||||
JSON.stringify(requiredPackages)
|
||||
) {
|
||||
throw new Error("prepublish plugin registry package set differs from the Docker plan");
|
||||
}
|
||||
const expectedFiles = new Set([
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST,
|
||||
...manifest.packages.map((entry) => entry.tarball),
|
||||
]);
|
||||
const actualFiles = fs.readdirSync(artifactDir, { withFileTypes: true });
|
||||
if (
|
||||
actualFiles.some((entry) => !entry.isFile()) ||
|
||||
actualFiles.length !== expectedFiles.size ||
|
||||
actualFiles.some((entry) => !expectedFiles.has(entry.name))
|
||||
) {
|
||||
throw new Error(
|
||||
"prepublish plugin registry artifact contains missing, extra, or non-file entries",
|
||||
);
|
||||
}
|
||||
for (const entry of manifest.packages) {
|
||||
const tarball = path.join(artifactDir, entry.tarball);
|
||||
if (sha256File(tarball) !== entry.sha256) {
|
||||
throw new Error(`prepublish plugin registry tarball SHA-256 mismatch for ${entry.name}`);
|
||||
}
|
||||
const packageJson = readTarballPackageJson(tarball);
|
||||
if (packageJson.name !== entry.name || packageJson.version !== entry.version) {
|
||||
throw new Error(`prepublish plugin registry tarball identity mismatch for ${entry.name}`);
|
||||
}
|
||||
}
|
||||
return { manifest, manifestPath, manifestSha256 };
|
||||
}
|
||||
|
||||
function findPublishablePlugin(repoRoot, packageName) {
|
||||
const extensionsDir = path.join(repoRoot, "extensions");
|
||||
const matches = fs
|
||||
.readdirSync(extensionsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.flatMap((entry) => {
|
||||
const packageFile = path.join(extensionsDir, entry.name, "package.json");
|
||||
if (!fs.existsSync(packageFile)) {
|
||||
return [];
|
||||
}
|
||||
const packageJson = readJson(packageFile, "plugin package");
|
||||
return packageJson.name === packageName
|
||||
? [{ packageDir: `extensions/${entry.name}`, packageJson }]
|
||||
: [];
|
||||
});
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(
|
||||
`expected exactly one extension package for ${packageName}; found ${matches.length}`,
|
||||
);
|
||||
}
|
||||
const match = matches[0];
|
||||
if (match.packageJson.openclaw?.release?.publishToNpm !== true) {
|
||||
throw new Error(`${packageName} is not marked openclaw.release.publishToNpm`);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
function runChecked(command, args, cwd) {
|
||||
// The CLI stdout is a JSON contract; child build and pack diagnostics belong on stderr.
|
||||
const result = spawnSync(command, args, { cwd, encoding: "utf8", stdio: ["ignore", 2, 2] });
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(" ")} failed with status ${String(result.status)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function createPrepublishPluginRegistryArtifact(params) {
|
||||
const repoRoot = path.resolve(params.repoRoot);
|
||||
const outputDir = path.resolve(params.outputDir);
|
||||
const requiredPackages = normalizeRequiredPackages(params.requiredPackages);
|
||||
if (!SOURCE_SHA_PATTERN.test(params.sourceSha ?? "")) {
|
||||
throw new Error("sourceSha must be a full lowercase commit SHA");
|
||||
}
|
||||
const actualSourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
if (actualSourceSha !== params.sourceSha) {
|
||||
throw new Error("repository HEAD differs from the requested artifact source SHA");
|
||||
}
|
||||
const trackedChanges = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=no"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
if (trackedChanges) {
|
||||
throw new Error("repository has tracked changes; refusing to package them under the HEAD SHA");
|
||||
}
|
||||
const rootPackage = readJson(path.join(repoRoot, "package.json"), "root package");
|
||||
if (rootPackage.version !== params.candidateVersion) {
|
||||
throw new Error("root package version differs from the requested candidate version");
|
||||
}
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
if (fs.readdirSync(outputDir).length !== 0) {
|
||||
throw new Error(`prepublish plugin registry output directory must be empty: ${outputDir}`);
|
||||
}
|
||||
const packages = [];
|
||||
for (const packageName of requiredPackages) {
|
||||
const { packageDir, packageJson } = findPublishablePlugin(repoRoot, packageName);
|
||||
if (packageJson.version !== params.candidateVersion) {
|
||||
throw new Error(`${packageName} version differs from the root package candidate`);
|
||||
}
|
||||
runChecked(
|
||||
process.execPath,
|
||||
[path.join(repoRoot, "scripts/lib/plugin-npm-runtime-build.mjs"), packageDir],
|
||||
repoRoot,
|
||||
);
|
||||
const before = new Set(fs.readdirSync(outputDir));
|
||||
runChecked(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(repoRoot, "scripts/lib/plugin-npm-package-manifest.mjs"),
|
||||
"--run",
|
||||
packageDir,
|
||||
"--",
|
||||
"npm",
|
||||
"pack",
|
||||
"--json",
|
||||
"--ignore-scripts",
|
||||
"--pack-destination",
|
||||
outputDir,
|
||||
],
|
||||
repoRoot,
|
||||
);
|
||||
const created = fs.readdirSync(outputDir).filter((name) => !before.has(name));
|
||||
if (created.length !== 1 || !TARBALL_PATTERN.test(created[0])) {
|
||||
throw new Error(`${packageName} pack must create exactly one .tgz`);
|
||||
}
|
||||
const tarball = created[0];
|
||||
const tarballPath = path.join(outputDir, tarball);
|
||||
const packedPackage = readTarballPackageJson(tarballPath);
|
||||
if (packedPackage.name !== packageName || packedPackage.version !== params.candidateVersion) {
|
||||
throw new Error(`${packageName} packed identity differs from the candidate`);
|
||||
}
|
||||
packages.push({
|
||||
name: packageName,
|
||||
version: params.candidateVersion,
|
||||
tarball,
|
||||
sha256: sha256File(tarballPath),
|
||||
});
|
||||
}
|
||||
const manifest = {
|
||||
schema: SCHEMA,
|
||||
schemaVersion: 1,
|
||||
sourceSha: params.sourceSha,
|
||||
candidateVersion: params.candidateVersion,
|
||||
packages,
|
||||
};
|
||||
const manifestPath = path.join(outputDir, PREPUBLISH_PLUGIN_REGISTRY_MANIFEST);
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
const manifestSha256 = sha256File(manifestPath);
|
||||
return validatePrepublishPluginRegistryArtifact({
|
||||
artifactDir: outputDir,
|
||||
expectedCandidateVersion: params.candidateVersion,
|
||||
expectedManifestSha256: manifestSha256,
|
||||
expectedSourceSha: params.sourceSha,
|
||||
requiredPackages,
|
||||
});
|
||||
}
|
||||
|
||||
function parseCliArgs(argv) {
|
||||
const args = [...argv];
|
||||
const command = args.shift();
|
||||
const options = new Map();
|
||||
while (args.length > 0) {
|
||||
const name = args.shift();
|
||||
const value = args.shift();
|
||||
if (!name?.startsWith("--") || value === undefined) {
|
||||
throw new Error(`invalid argument list near ${String(name)}`);
|
||||
}
|
||||
options.set(name, value);
|
||||
}
|
||||
const requiredPackages = JSON.parse(options.get("--required-packages-json") ?? "[]");
|
||||
return { command, options, requiredPackages };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { command, options, requiredPackages } = parseCliArgs(process.argv.slice(2));
|
||||
const common = {
|
||||
artifactDir: options.get("--artifact-dir"),
|
||||
expectedCandidateVersion: options.get("--candidate-version"),
|
||||
expectedManifestSha256: options.get("--manifest-sha256"),
|
||||
expectedSourceSha: options.get("--source-sha"),
|
||||
requiredPackages,
|
||||
};
|
||||
const result =
|
||||
command === "create"
|
||||
? createPrepublishPluginRegistryArtifact({
|
||||
repoRoot: options.get("--repo-root") ?? ".",
|
||||
outputDir: common.artifactDir,
|
||||
sourceSha: common.expectedSourceSha,
|
||||
candidateVersion: common.expectedCandidateVersion,
|
||||
requiredPackages,
|
||||
})
|
||||
: command === "verify"
|
||||
? validatePrepublishPluginRegistryArtifact(common)
|
||||
: undefined;
|
||||
if (!result) {
|
||||
throw new Error("usage: prepublish-plugin-registry-artifact.mjs <create|verify> [options]");
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
manifestPath: result.manifestPath,
|
||||
manifestSha256: result.manifestSha256,
|
||||
packages: result.manifest.packages.map((entry) => entry.name),
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const isMain =
|
||||
process.argv[1] &&
|
||||
fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
||||
if (isMain) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
resolveDockerE2ePlan,
|
||||
} from "./lib/docker-e2e-plan.mjs";
|
||||
import { sleep } from "./lib/sleep.mjs";
|
||||
import { validatePrepublishPluginRegistryArtifact } from "./prepublish-plugin-registry-artifact.mjs";
|
||||
|
||||
const SCRIPT_ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const ROOT_DIR = path.resolve(process.env.OPENCLAW_DOCKER_E2E_REPO_ROOT || SCRIPT_ROOT_DIR);
|
||||
@@ -351,6 +352,7 @@ function buildLaneRerunCommand(name, baseEnv) {
|
||||
["OPENCLAW_DOCKER_E2E_BARE_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE],
|
||||
["OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE],
|
||||
["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ],
|
||||
["OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR", baseEnv.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS],
|
||||
@@ -1540,6 +1542,20 @@ async function main() {
|
||||
allowFrozenTargetScenarioOmissions,
|
||||
candidatePackageRoot: ROOT_DIR,
|
||||
});
|
||||
if (plan.needs.prepublishPluginRegistry && process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR) {
|
||||
baseEnv.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR = path.resolve(
|
||||
process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR,
|
||||
);
|
||||
validatePrepublishPluginRegistryArtifact({
|
||||
artifactDir: baseEnv.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR,
|
||||
expectedCandidateVersion: process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_CANDIDATE_VERSION,
|
||||
expectedManifestSha256: process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256,
|
||||
expectedSourceSha: process.env.OPENCLAW_DOCKER_E2E_SELECTED_SHA,
|
||||
requiredPackages: plan.requiredPrepublishPluginPackages,
|
||||
});
|
||||
} else {
|
||||
delete baseEnv.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR;
|
||||
}
|
||||
if (omittedUnsupportedLaneNames.length > 0 && !allowFrozenTargetScenarioOmissions) {
|
||||
throw new Error(
|
||||
`frozen target scenario omissions require trusted workflow opt-in: ${omittedUnsupportedLaneNames.join(", ")}`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { spawn, spawnSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
@@ -167,12 +168,37 @@ describe("scripts/test-docker-all scheduler", () => {
|
||||
const root = tempDirs.make("openclaw-docker-plan-isolated-harness-");
|
||||
const scriptsDir = path.join(root, "scripts");
|
||||
const libDir = path.join(scriptsDir, "lib");
|
||||
const upgradeSurvivorDir = path.join(scriptsDir, "e2e/lib/upgrade-survivor");
|
||||
mkdirSync(libDir, { recursive: true });
|
||||
mkdirSync(upgradeSurvivorDir, { recursive: true });
|
||||
copyFileSync("package.json", path.join(root, "package.json"));
|
||||
copyFileSync("scripts/test-docker-all.mjs", path.join(scriptsDir, "test-docker-all.mjs"));
|
||||
for (const fileName of ["docker-e2e-plan.mjs", "docker-e2e-scenarios.mjs", "sleep.mjs"]) {
|
||||
copyFileSync(
|
||||
"scripts/prepublish-plugin-registry-artifact.mjs",
|
||||
path.join(scriptsDir, "prepublish-plugin-registry-artifact.mjs"),
|
||||
);
|
||||
copyFileSync(
|
||||
"scripts/windows-cmd-helpers.mjs",
|
||||
path.join(scriptsDir, "windows-cmd-helpers.mjs"),
|
||||
);
|
||||
for (const fileName of [
|
||||
"docker-e2e-plan.mjs",
|
||||
"docker-e2e-scenarios.mjs",
|
||||
"official-external-channel-catalog.json",
|
||||
"release-version.mjs",
|
||||
"sleep.mjs",
|
||||
]) {
|
||||
copyFileSync(path.join("scripts/lib", fileName), path.join(libDir, fileName));
|
||||
}
|
||||
copyFileSync(
|
||||
"scripts/e2e/lib/upgrade-survivor/config-recipe.mjs",
|
||||
path.join(upgradeSurvivorDir, "config-recipe.mjs"),
|
||||
);
|
||||
cpSync(
|
||||
"scripts/e2e/lib/upgrade-survivor/config-recipe",
|
||||
path.join(upgradeSurvivorDir, "config-recipe"),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
|
||||
@@ -2233,6 +2233,35 @@ docker_e2e_docker_run_cmd run demo
|
||||
}
|
||||
});
|
||||
|
||||
it("starts the upgrade survivor plugin registry before updates without ambient Feishu config", () => {
|
||||
const runner = readFileSync(UPGRADE_SURVIVOR_DOCKER_E2E_PATH, "utf8");
|
||||
const publishedRunner = readFileSync(UPGRADE_SURVIVOR_RUN_SCRIPT, "utf8");
|
||||
|
||||
expect(runner.indexOf("\nconfigure_plugin_registry\n")).toBeLessThan(
|
||||
runner.indexOf('\necho "Running package update against the mounted tarball..."\n'),
|
||||
);
|
||||
expect(
|
||||
publishedRunner.indexOf("phase configure-plugin-registry configure_plugin_registry"),
|
||||
).toBeLessThan(publishedRunner.indexOf("phase update-candidate update_candidate"));
|
||||
expect(runner).toContain(
|
||||
'if [ "${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}" = "feishu-channel" ]; then',
|
||||
);
|
||||
expect(publishedRunner).toContain('if [ "$SCENARIO" = "feishu-channel" ]; then');
|
||||
for (const script of [runner, publishedRunner]) {
|
||||
const emptyRegistryGuardIndex = script.indexOf('if [ "${#registry_args[@]}" -eq 0 ]; then');
|
||||
const fixtureDirectoryIndex = script.indexOf('mkdir -p "$fixture_root"');
|
||||
const registryServerIndex = script.indexOf(
|
||||
"OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org",
|
||||
);
|
||||
expect(emptyRegistryGuardIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(fixtureDirectoryIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(registryServerIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(emptyRegistryGuardIndex).toBeLessThan(fixtureDirectoryIndex);
|
||||
expect(fixtureDirectoryIndex).toBeLessThan(registryServerIndex);
|
||||
expect(script).not.toContain('\nexport FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"\n');
|
||||
}
|
||||
});
|
||||
|
||||
it("wraps package-backed scenario OpenClaw CLI calls with the shared timeout helper", () => {
|
||||
const paths = [
|
||||
CODEX_ON_DEMAND_DOCKER_E2E_PATH,
|
||||
|
||||
@@ -87,6 +87,26 @@ describe("Docker E2E helper CLIs", () => {
|
||||
expect(result.stderr).not.toContain("at file:");
|
||||
});
|
||||
|
||||
it("emits prerelease plugin registry planning outputs", () => {
|
||||
const root = tempDirs.make("openclaw-docker-e2e-helper-plan-");
|
||||
const file = path.join(root, "plan.json");
|
||||
writeFileSync(
|
||||
file,
|
||||
`${JSON.stringify({
|
||||
requiredPrepublishPluginPackages: ["@openclaw/discord", "@openclaw/feishu"],
|
||||
needs: { prepublishPluginRegistry: true },
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
const result = runHelper("scripts/docker-e2e.mjs", "github-outputs", file);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("needs_prepublish_plugin_registry=1");
|
||||
expect(result.stdout).toContain(
|
||||
'required_prepublish_plugin_packages=["@openclaw/discord","@openclaw/feishu"]',
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects oversized scheduler helper JSON artifacts without a Node stack trace", () => {
|
||||
const root = mkdtempSync(`${tmpdir()}/openclaw-docker-e2e-helper-`);
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Docker E2E Plan tests cover docker e2e plan script behavior.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
RELEASE_PATH_PROFILE,
|
||||
findLaneByName,
|
||||
parseLaneSelection,
|
||||
requiredPrepublishPluginPackagesForLanes,
|
||||
resolveDockerE2ePlan,
|
||||
} from "../../scripts/lib/docker-e2e-plan.mjs";
|
||||
import {
|
||||
allReleasePathLanes,
|
||||
BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS,
|
||||
mainLanes,
|
||||
} from "../../scripts/lib/docker-e2e-scenarios.mjs";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
@@ -81,6 +83,16 @@ function summarizeLane(lane: ReturnType<typeof planFor>["lanes"][number]) {
|
||||
};
|
||||
}
|
||||
|
||||
function trustedUpgradeSurvivorCommand(
|
||||
envPrefix = "",
|
||||
shellPrelude = "",
|
||||
harnessDir = ".",
|
||||
): string {
|
||||
const prefix = envPrefix ? `${envPrefix} ` : "";
|
||||
const prelude = shellPrelude ? `${shellPrelude}; ` : "";
|
||||
return `OPENCLAW_DOCKER_E2E_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" ${prefix}OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c '${prelude}harness="\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${harnessDir}}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/upgrade-survivor-docker.sh"'`;
|
||||
}
|
||||
|
||||
function publishedUpgradeSurvivorLane(
|
||||
name: string,
|
||||
baselineSpec: string,
|
||||
@@ -89,7 +101,10 @@ function publishedUpgradeSurvivorLane(
|
||||
return {
|
||||
command: `OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR="$PWD/.artifacts/upgrade-survivor/${name}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC='${baselineSpec}' ${
|
||||
scenario ? `OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='${scenario}' ` : ""
|
||||
}OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:published-upgrade-survivor`,
|
||||
}${trustedUpgradeSurvivorCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
)}`,
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name,
|
||||
@@ -102,7 +117,10 @@ function publishedUpgradeSurvivorLane(
|
||||
|
||||
function updateMigrationLane(name: string, baselineSpec: string): ReturnType<typeof summarizeLane> {
|
||||
return {
|
||||
command: `OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR="$PWD/.artifacts/upgrade-survivor/${name}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC='${baselineSpec}' OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='plugin-deps-cleanup' OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-migration`,
|
||||
command: `OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR="$PWD/.artifacts/upgrade-survivor/${name}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC='${baselineSpec}' OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='plugin-deps-cleanup' ${trustedUpgradeSurvivorCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.4.23}"; export OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-plugin-deps-cleanup}"',
|
||||
)}`,
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name,
|
||||
@@ -146,6 +164,26 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
expect(plan.lanes.map((lane) => lane.name)).toEqual(["update-run-package-self-upgrade"]);
|
||||
});
|
||||
|
||||
it("keeps trusted upgrade harness lanes without candidate package scripts", () => {
|
||||
const plan = planFor({
|
||||
candidatePackageRoot: writeCandidatePackage({}),
|
||||
selectedLaneNames: [
|
||||
"upgrade-survivor",
|
||||
"published-upgrade-survivor",
|
||||
"root-managed-vps-upgrade",
|
||||
"update-migration",
|
||||
"update-run-package-self-upgrade",
|
||||
],
|
||||
});
|
||||
|
||||
expect(plan.lanes.map((lane) => lane.name)).toEqual([
|
||||
"upgrade-survivor",
|
||||
"published-upgrade-survivor",
|
||||
"root-managed-vps-upgrade",
|
||||
"update-migration",
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails when the selected candidate package manifest is missing", () => {
|
||||
expect(() =>
|
||||
planFor({
|
||||
@@ -193,10 +231,21 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
expect(plan.needs.functionalImage).toBe(true);
|
||||
});
|
||||
|
||||
it("routes live Docker scripts through the nested trusted release harness", () => {
|
||||
const sourceLane = allReleasePathLanes({ releaseProfile: "beta" }).find(
|
||||
(candidate) => candidate.name === "live-codex-npm-plugin",
|
||||
);
|
||||
it("routes trusted Docker scripts through the nested release harness", () => {
|
||||
const trustedScripts = new Map([
|
||||
["live-codex-npm-plugin", "e2e/codex-npm-plugin-live-docker.sh"],
|
||||
["upgrade-survivor", "e2e/upgrade-survivor-docker.sh"],
|
||||
["published-upgrade-survivor", "e2e/upgrade-survivor-docker.sh"],
|
||||
["root-managed-vps-upgrade", "e2e/upgrade-survivor-docker.sh"],
|
||||
["update-migration", "e2e/upgrade-survivor-docker.sh"],
|
||||
]);
|
||||
const sourceLanes = [
|
||||
...new Map(
|
||||
[...allReleasePathLanes({ releaseProfile: "beta" }), ...mainLanes]
|
||||
.filter((candidate) => trustedScripts.has(candidate.name))
|
||||
.map((candidate) => [candidate.name, candidate]),
|
||||
).values(),
|
||||
];
|
||||
const tempRoot = tempDirs.make("openclaw-release-harness-");
|
||||
const nestedModule = join(
|
||||
tempRoot,
|
||||
@@ -206,9 +255,12 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
"docker-e2e-scenarios.mjs",
|
||||
);
|
||||
|
||||
expect(sourceLane?.command).toContain(
|
||||
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"',
|
||||
);
|
||||
expect(sourceLanes).toHaveLength(trustedScripts.size);
|
||||
for (const sourceLane of sourceLanes) {
|
||||
expect(sourceLane.command).toContain(
|
||||
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"',
|
||||
);
|
||||
}
|
||||
|
||||
mkdirSync(dirname(nestedModule), { recursive: true });
|
||||
copyFileSync("scripts/lib/docker-e2e-scenarios.mjs", nestedModule);
|
||||
@@ -221,10 +273,18 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
`
|
||||
import { pathToFileURL } from "node:url";
|
||||
const scenarios = await import(pathToFileURL(process.argv[1]).href);
|
||||
const lane = scenarios
|
||||
.allReleasePathLanes({ releaseProfile: "beta" })
|
||||
.find((candidate) => candidate.name === "live-codex-npm-plugin");
|
||||
process.stdout.write(JSON.stringify(lane));
|
||||
const names = ${JSON.stringify([...trustedScripts.keys()])};
|
||||
const lanes = [
|
||||
...new Map(
|
||||
[
|
||||
...scenarios.allReleasePathLanes({ releaseProfile: "beta" }),
|
||||
...scenarios.mainLanes,
|
||||
]
|
||||
.filter((candidate) => names.includes(candidate.name))
|
||||
.map((candidate) => [candidate.name, candidate]),
|
||||
).values(),
|
||||
];
|
||||
process.stdout.write(JSON.stringify(lanes));
|
||||
`,
|
||||
nestedModule,
|
||||
],
|
||||
@@ -236,12 +296,51 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
const lane = JSON.parse(laneJson) as { command: string };
|
||||
const lanes = JSON.parse(laneJson) as Array<{ command: string; name: string }>;
|
||||
|
||||
expect(lane.command).toContain(
|
||||
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.release-harness}"',
|
||||
expect(lanes).toHaveLength(trustedScripts.size);
|
||||
for (const lane of lanes) {
|
||||
expect(lane.command).toContain(
|
||||
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.release-harness}"',
|
||||
);
|
||||
expect(lane.command).toContain(`bash "$harness/scripts/${trustedScripts.get(lane.name)}"`);
|
||||
expect(lane.command).not.toContain(`pnpm test:docker:${lane.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves expanded survivor env through the trusted harness wrapper", () => {
|
||||
const root = tempDirs.make("openclaw-survivor-wrapper-");
|
||||
const harnessRoot = join(root, ".release-harness");
|
||||
const script = join(harnessRoot, "scripts/e2e/upgrade-survivor-docker.sh");
|
||||
const output = join(root, "survivor-env.txt");
|
||||
mkdirSync(dirname(script), { recursive: true });
|
||||
writeFileSync(
|
||||
script,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
'printf "%s|%s\\n" "$OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC" "$OPENCLAW_UPGRADE_SURVIVOR_SCENARIO" > "$OPENCLAW_TEST_OUTPUT"',
|
||||
].join("\n"),
|
||||
);
|
||||
expect(lane.command).toContain('bash "$harness/scripts/e2e/codex-npm-plugin-live-docker.sh"');
|
||||
chmodSync(script, 0o755);
|
||||
|
||||
const lane = requireFirstLane(
|
||||
planFor({
|
||||
selectedLaneNames: ["published-upgrade-survivor"],
|
||||
upgradeSurvivorBaselines: "2026.7.2",
|
||||
upgradeSurvivorScenarios: "feishu-channel",
|
||||
}),
|
||||
);
|
||||
execFileSync("/bin/bash", ["-c", lane.command], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_DOCKER_E2E_REPO_ROOT: root,
|
||||
OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR: harnessRoot,
|
||||
OPENCLAW_TEST_OUTPUT: output,
|
||||
},
|
||||
});
|
||||
|
||||
expect(readFileSync(output, "utf8")).toBe("openclaw@2026.7.2|feishu-channel\n");
|
||||
});
|
||||
|
||||
it("plans package-backed installer, Compose, and package artifact proofs", () => {
|
||||
@@ -287,6 +386,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: true,
|
||||
liveImage: false,
|
||||
package: true,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,6 +403,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: true,
|
||||
liveImage: true,
|
||||
package: true,
|
||||
prepublishPluginRegistry: true,
|
||||
});
|
||||
expect(plan.credentials).toEqual(["openai"]);
|
||||
expect(plan.lanes.map((lane) => lane.name)).not.toContain("install-e2e-openai");
|
||||
@@ -569,7 +670,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:upgrade-survivor",
|
||||
command: trustedUpgradeSurvivorCommand(),
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name: "upgrade-survivor",
|
||||
@@ -579,7 +680,10 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:published-upgrade-survivor",
|
||||
command: trustedUpgradeSurvivorCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
),
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name: "published-upgrade-survivor",
|
||||
@@ -589,7 +693,10 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:root-managed-vps-upgrade",
|
||||
command: trustedUpgradeSurvivorCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.5.7}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
),
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name: "root-managed-vps-upgrade",
|
||||
@@ -599,8 +706,10 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
command:
|
||||
'OPENCLAW_DOCKER_E2E_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest} OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s} OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/upgrade-survivor-docker.sh"\'',
|
||||
command: trustedUpgradeSurvivorCommand(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth",
|
||||
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
|
||||
),
|
||||
imageKind: "bare",
|
||||
live: false,
|
||||
name: "update-restart-auth",
|
||||
@@ -1237,6 +1346,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: false,
|
||||
liveImage: true,
|
||||
package: false,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1310,6 +1420,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: false,
|
||||
liveImage: true,
|
||||
package: true,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1442,6 +1553,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: true,
|
||||
liveImage: true,
|
||||
package: true,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1470,6 +1582,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: true,
|
||||
liveImage: false,
|
||||
package: true,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1539,6 +1652,43 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("derives prerelease npm companions from selected survivor recipes", () => {
|
||||
const basePlan = planFor({ selectedLaneNames: ["published-upgrade-survivor"] });
|
||||
expect(basePlan.requiredPrepublishPluginPackages).toEqual(["@openclaw/discord"]);
|
||||
expect(basePlan.needs.prepublishPluginRegistry).toBe(true);
|
||||
|
||||
const feishuPlan = planFor({
|
||||
selectedLaneNames: ["published-upgrade-survivor"],
|
||||
upgradeSurvivorBaselines: "2026.7.2",
|
||||
upgradeSurvivorScenarios: "base feishu-channel",
|
||||
});
|
||||
expect(feishuPlan.requiredPrepublishPluginPackages).toEqual([
|
||||
"@openclaw/discord",
|
||||
"@openclaw/feishu",
|
||||
]);
|
||||
expect(
|
||||
planFor({ selectedLaneNames: ["root-managed-vps-upgrade"] }).requiredPrepublishPluginPackages,
|
||||
).toEqual(["@openclaw/discord"]);
|
||||
expect(
|
||||
planFor({ selectedLaneNames: ["update-migration"] }).requiredPrepublishPluginPackages,
|
||||
).toEqual(["@openclaw/discord"]);
|
||||
const legacyFeishuPlan = planFor({
|
||||
selectedLaneNames: ["published-upgrade-survivor"],
|
||||
upgradeSurvivorBaselines: "2026.3.13",
|
||||
upgradeSurvivorScenarios: "feishu-channel",
|
||||
});
|
||||
expect(legacyFeishuPlan.requiredPrepublishPluginPackages).toEqual(["@openclaw/discord"]);
|
||||
const selfUpgradeLane = findLaneByName("update-run-package-self-upgrade");
|
||||
expect(selfUpgradeLane).toBeDefined();
|
||||
expect(requiredPrepublishPluginPackagesForLanes([selfUpgradeLane!])).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not request a prerelease plugin registry for unrelated lanes", () => {
|
||||
const plan = planFor({ selectedLaneNames: ["doctor-switch"] });
|
||||
expect(plan.requiredPrepublishPluginPackages).toEqual([]);
|
||||
expect(plan.needs.prepublishPluginRegistry).toBe(false);
|
||||
});
|
||||
|
||||
it("maps installer E2E to provider-specific package install lanes", () => {
|
||||
const selectedLaneNames = parseLaneSelection("install-e2e");
|
||||
const plan = planFor({ selectedLaneNames });
|
||||
@@ -1597,6 +1747,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
functionalImage: true,
|
||||
liveImage: false,
|
||||
package: true,
|
||||
prepublishPluginRegistry: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2293,6 +2293,35 @@ describe("package artifact reuse", () => {
|
||||
expect(workflow).toContain("OPENCLAW_DOCKER_E2E_REPO_ROOT:");
|
||||
expect(workflow).toContain("node .release-harness/scripts/test-docker-all.mjs --plan-json");
|
||||
expect(workflow).toContain("node .release-harness/scripts/docker-e2e.mjs github-outputs");
|
||||
expect(parsedWorkflow.on?.workflow_call?.inputs).toHaveProperty(
|
||||
"enable_prepublish_plugin_registry",
|
||||
);
|
||||
expect(workflow).toContain("Pack prerelease plugin registry artifact");
|
||||
expect(workflow).toContain("Validate prerelease plugin registry artifact");
|
||||
expect(workflow).toContain("Download targeted prerelease plugin registry artifact");
|
||||
expect(workflow).toContain("OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR");
|
||||
expect(workflow).toContain("prepublishPluginRegistryManifestSha256");
|
||||
expect(
|
||||
workflowStep(
|
||||
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
||||
"Pack prerelease plugin registry artifact",
|
||||
).id,
|
||||
).toBe("create_prepublish_plugin_registry");
|
||||
expect(
|
||||
workflowStep(
|
||||
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
||||
"Validate prerelease plugin registry artifact",
|
||||
).env?.EXPECTED_MANIFEST_SHA256,
|
||||
).toBe(
|
||||
"${{ steps.create_prepublish_plugin_registry.outputs.manifest_sha256 || inputs.prepublish_plugin_registry_manifest_sha256 }}",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1'",
|
||||
);
|
||||
expect(
|
||||
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image").outputs
|
||||
?.prepublish_plugin_registry_artifact_id,
|
||||
).toContain("inputs.enable_prepublish_plugin_registry");
|
||||
expect(workflow).toContain("bash .release-harness/scripts/ci-docker-pull-retry.sh");
|
||||
const prepareDockerImage = workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image");
|
||||
expect(workflowStep(prepareDockerImage, "Plan Docker E2E images").env).toEqual({
|
||||
@@ -2406,9 +2435,13 @@ describe("package artifact reuse", () => {
|
||||
|
||||
expect(prepare.uses).toBe("./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
||||
expect(prepare.with).toMatchObject({
|
||||
enable_prepublish_plugin_registry: true,
|
||||
prepare_only: true,
|
||||
shared_image_policy: "no-push-artifact",
|
||||
});
|
||||
expect(prepare.with?.published_upgrade_survivor_scenarios).toBe(
|
||||
"${{ (inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full') && 'reported-issues' || '' }}",
|
||||
);
|
||||
expect(pluginDispatch.run).toContain(
|
||||
'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")',
|
||||
);
|
||||
@@ -2418,6 +2451,14 @@ describe("package artifact reuse", () => {
|
||||
expect(workflow).toContain("Shared release candidate preparation ended with");
|
||||
});
|
||||
|
||||
it("enables prerelease plugin companions for scheduled ref validation", () => {
|
||||
const scheduled = workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "live_and_openwebui_checks");
|
||||
expect(scheduled.with).toMatchObject({
|
||||
enable_prepublish_plugin_registry: true,
|
||||
ref: "${{ github.sha }}",
|
||||
});
|
||||
});
|
||||
|
||||
it("gives memory extension shards enough CPU without lowering their planner cost", () => {
|
||||
const workflow = readFileSync(PLUGIN_PRERELEASE_WORKFLOW, "utf8");
|
||||
|
||||
@@ -3187,7 +3228,14 @@ describe("package artifact reuse", () => {
|
||||
expect(dockerAcceptanceJob.with).toMatchObject({
|
||||
allow_frozen_target_scenario_omissions:
|
||||
"${{ inputs.allow_frozen_target_scenario_omissions || false }}",
|
||||
enable_prepublish_plugin_registry:
|
||||
'${{ contains(fromJSON(\'["artifact","ref"]\'), inputs.source) }}',
|
||||
prepublish_plugin_registry_manifest_sha256:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
||||
});
|
||||
expect(workflow).toContain(
|
||||
"candidate_artifact_json cannot be combined with release package specs.",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"live_repo_e2e_release_checks:\n name: Run repo/live E2E validation\n needs: [resolve_target]",
|
||||
);
|
||||
@@ -5561,6 +5609,51 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
||||
});
|
||||
expect(valid.status, valid.stderr).toBe(0);
|
||||
|
||||
const registryCandidate = {
|
||||
...candidate,
|
||||
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-456-1",
|
||||
prepublishPluginRegistryArtifactId: "790",
|
||||
prepublishPluginRegistryArtifactDigest: "f".repeat(64),
|
||||
prepublishPluginRegistryArtifactRunId: "456",
|
||||
prepublishPluginRegistryArtifactRunAttempt: "1",
|
||||
prepublishPluginRegistryManifestSha256: "1".repeat(64),
|
||||
};
|
||||
const validRegistry = spawnSync("bash", ["-c", validation ?? ""], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CANDIDATE_ARTIFACT_JSON: JSON.stringify(registryCandidate),
|
||||
SELECTED_SHA: selectedSha,
|
||||
},
|
||||
});
|
||||
expect(validRegistry.status, validRegistry.stderr).toBe(0);
|
||||
|
||||
const partialRegistry = spawnSync("bash", ["-c", validation ?? ""], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CANDIDATE_ARTIFACT_JSON: JSON.stringify({
|
||||
...candidate,
|
||||
prepublishPluginRegistryArtifactId: "790",
|
||||
}),
|
||||
SELECTED_SHA: selectedSha,
|
||||
},
|
||||
});
|
||||
expect(partialRegistry.status).not.toBe(0);
|
||||
|
||||
const mismatchedRegistryName = spawnSync("bash", ["-c", validation ?? ""], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CANDIDATE_ARTIFACT_JSON: JSON.stringify({
|
||||
...registryCandidate,
|
||||
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-999-1",
|
||||
}),
|
||||
SELECTED_SHA: selectedSha,
|
||||
},
|
||||
});
|
||||
expect(mismatchedRegistryName.status).not.toBe(0);
|
||||
|
||||
const mismatched = spawnSync("bash", ["-c", validation ?? ""], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST,
|
||||
createPrepublishPluginRegistryArtifact,
|
||||
validatePrepublishPluginRegistryArtifact,
|
||||
} from "../../scripts/prepublish-plugin-registry-artifact.mjs";
|
||||
|
||||
const SOURCE_SHA = "a".repeat(40);
|
||||
const VERSION = "2026.8.1-beta.1";
|
||||
const PACKAGE_NAME = "@openclaw/discord";
|
||||
const TARBALL = "openclaw-discord-2026.8.1-beta.1.tgz";
|
||||
const SCRIPT = path.resolve("scripts/prepublish-plugin-registry-artifact.mjs");
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function sha256(file: string): string {
|
||||
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-registry-"));
|
||||
tempDirs.push(root);
|
||||
const packageRoot = path.join(root, "package");
|
||||
const artifactDir = path.join(root, "artifact");
|
||||
mkdirSync(packageRoot);
|
||||
mkdirSync(artifactDir);
|
||||
writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
`${JSON.stringify({ name: PACKAGE_NAME, version: VERSION })}\n`,
|
||||
);
|
||||
const tarballPath = path.join(artifactDir, TARBALL);
|
||||
execFileSync("tar", ["-czf", tarballPath, "-C", root, "package"]);
|
||||
const manifestPath = path.join(artifactDir, PREPUBLISH_PLUGIN_REGISTRY_MANIFEST);
|
||||
const manifest = {
|
||||
schema: "openclaw.prepublish-plugin-registry/v1",
|
||||
schemaVersion: 1,
|
||||
sourceSha: SOURCE_SHA,
|
||||
candidateVersion: VERSION,
|
||||
packages: [
|
||||
{
|
||||
name: PACKAGE_NAME,
|
||||
version: VERSION,
|
||||
tarball: TARBALL,
|
||||
sha256: sha256(tarballPath),
|
||||
},
|
||||
],
|
||||
};
|
||||
const writeManifest = () => {
|
||||
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
};
|
||||
writeManifest();
|
||||
return { artifactDir, manifest, manifestPath, tarballPath, writeManifest };
|
||||
}
|
||||
|
||||
function validate(paths: ReturnType<typeof fixture>, overrides = {}) {
|
||||
return validatePrepublishPluginRegistryArtifact({
|
||||
artifactDir: paths.artifactDir,
|
||||
expectedCandidateVersion: VERSION,
|
||||
expectedManifestSha256: sha256(paths.manifestPath),
|
||||
expectedSourceSha: SOURCE_SHA,
|
||||
requiredPackages: [PACKAGE_NAME],
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function firstPackage(paths: ReturnType<typeof fixture>) {
|
||||
const [entry] = paths.manifest.packages;
|
||||
if (!entry) {
|
||||
throw new Error("fixture manifest must contain one package");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function cliFixture() {
|
||||
const repoRoot = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-cli-"));
|
||||
tempDirs.push(repoRoot);
|
||||
const packageDir = path.join(repoRoot, "extensions", "discord");
|
||||
const scriptsDir = path.join(repoRoot, "scripts", "lib");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(repoRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: VERSION })}\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: PACKAGE_NAME,
|
||||
version: VERSION,
|
||||
openclaw: { release: { publishToNpm: true } },
|
||||
})}\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(scriptsDir, "plugin-npm-runtime-build.mjs"),
|
||||
'console.log("runtime build stdout");\n',
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(scriptsDir, "plugin-npm-package-manifest.mjs"),
|
||||
`import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
const repoRoot = process.cwd();
|
||||
const packageDir = process.argv[process.argv.indexOf("--run") + 1];
|
||||
const outputDir = process.argv[process.argv.indexOf("--pack-destination") + 1];
|
||||
const staging = path.join(repoRoot, ".pack-fixture");
|
||||
fs.mkdirSync(path.join(staging, "package"), { recursive: true });
|
||||
fs.copyFileSync(path.join(repoRoot, packageDir, "package.json"), path.join(staging, "package", "package.json"));
|
||||
execFileSync("tar", ["-czf", path.join(outputDir, "${TARBALL}"), "-C", staging, "package"]);
|
||||
console.log("package manifest stdout");
|
||||
`,
|
||||
);
|
||||
execFileSync("git", ["init"], { cwd: repoRoot });
|
||||
execFileSync("git", ["config", "user.email", "release-test@example.invalid"], {
|
||||
cwd: repoRoot,
|
||||
});
|
||||
execFileSync("git", ["config", "user.name", "Release Test"], { cwd: repoRoot });
|
||||
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: repoRoot });
|
||||
execFileSync("git", ["add", "."], { cwd: repoRoot });
|
||||
execFileSync("git", ["commit", "-m", "test: seed release source"], { cwd: repoRoot });
|
||||
const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
return { repoRoot, sourceSha };
|
||||
}
|
||||
|
||||
describe("prepublish plugin registry artifact", () => {
|
||||
it("validates the immutable manifest, package set, hashes, and packed identity", () => {
|
||||
const paths = fixture();
|
||||
const result = validate(paths);
|
||||
expect(result.manifest.packages.map((entry) => entry.name)).toEqual([PACKAGE_NAME]);
|
||||
});
|
||||
|
||||
it("requires the complete immutable identity tuple", () => {
|
||||
const paths = fixture();
|
||||
const common = {
|
||||
artifactDir: paths.artifactDir,
|
||||
expectedCandidateVersion: VERSION,
|
||||
expectedManifestSha256: sha256(paths.manifestPath),
|
||||
expectedSourceSha: SOURCE_SHA,
|
||||
requiredPackages: [PACKAGE_NAME],
|
||||
};
|
||||
for (const field of [
|
||||
"expectedCandidateVersion",
|
||||
"expectedManifestSha256",
|
||||
"expectedSourceSha",
|
||||
] as const) {
|
||||
expect(() =>
|
||||
validatePrepublishPluginRegistryArtifact({ ...common, [field]: undefined }),
|
||||
).toThrow(field);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses to create an artifact from tracked changes under the same HEAD", () => {
|
||||
const repoRoot = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-source-"));
|
||||
tempDirs.push(repoRoot);
|
||||
writeFileSync(
|
||||
path.join(repoRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: VERSION })}\n`,
|
||||
);
|
||||
execFileSync("git", ["init"], { cwd: repoRoot });
|
||||
execFileSync("git", ["config", "user.email", "release-test@example.invalid"], {
|
||||
cwd: repoRoot,
|
||||
});
|
||||
execFileSync("git", ["config", "user.name", "Release Test"], { cwd: repoRoot });
|
||||
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: repoRoot });
|
||||
execFileSync("git", ["add", "package.json"], { cwd: repoRoot });
|
||||
execFileSync("git", ["commit", "-m", "test: seed release source"], { cwd: repoRoot });
|
||||
const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
writeFileSync(
|
||||
path.join(repoRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: `${VERSION}-dirty` })}\n`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
createPrepublishPluginRegistryArtifact({
|
||||
repoRoot,
|
||||
outputDir: path.join(repoRoot, "artifact"),
|
||||
sourceSha,
|
||||
candidateVersion: VERSION,
|
||||
requiredPackages: [],
|
||||
}),
|
||||
).toThrow("tracked changes");
|
||||
});
|
||||
|
||||
it("keeps noisy package commands off the CLI JSON stdout contract", () => {
|
||||
const { repoRoot, sourceSha } = cliFixture();
|
||||
const artifactDir = path.join(repoRoot, "artifact");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
SCRIPT,
|
||||
"create",
|
||||
"--repo-root",
|
||||
repoRoot,
|
||||
"--artifact-dir",
|
||||
artifactDir,
|
||||
"--source-sha",
|
||||
sourceSha,
|
||||
"--candidate-version",
|
||||
VERSION,
|
||||
"--required-packages-json",
|
||||
JSON.stringify([PACKAGE_NAME]),
|
||||
],
|
||||
{ cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
manifestSha256: expect.stringMatching(/^[0-9a-f]{64}$/u),
|
||||
packages: [PACKAGE_NAME],
|
||||
});
|
||||
expect(result.stderr).toContain("runtime build stdout");
|
||||
expect(result.stderr).toContain("package manifest stdout");
|
||||
});
|
||||
|
||||
it("rejects traversal and duplicate package entries", () => {
|
||||
const traversal = fixture();
|
||||
firstPackage(traversal).tarball = "../escape.tgz";
|
||||
traversal.writeManifest();
|
||||
expect(() => validate(traversal)).toThrow("invalid package entry");
|
||||
|
||||
const duplicate = fixture();
|
||||
duplicate.manifest.packages.push({ ...firstPackage(duplicate) });
|
||||
duplicate.writeManifest();
|
||||
expect(() =>
|
||||
validate(duplicate, { requiredPackages: [PACKAGE_NAME, "@openclaw/feishu"] }),
|
||||
).toThrow("duplicate package");
|
||||
});
|
||||
|
||||
it("rejects missing and extra artifact files", () => {
|
||||
const missing = fixture();
|
||||
unlinkSync(missing.tarballPath);
|
||||
expect(() => validate(missing)).toThrow("missing, extra, or non-file");
|
||||
|
||||
const extra = fixture();
|
||||
writeFileSync(path.join(extra.artifactDir, "extra.txt"), "unexpected");
|
||||
expect(() => validate(extra)).toThrow("missing, extra, or non-file");
|
||||
});
|
||||
|
||||
it("rejects hash, identity, version, source SHA, and required-set mismatches", () => {
|
||||
const hash = fixture();
|
||||
writeFileSync(hash.tarballPath, "tampered");
|
||||
expect(() => validate(hash)).toThrow("tarball SHA-256 mismatch");
|
||||
|
||||
const identity = fixture();
|
||||
firstPackage(identity).name = "@openclaw/feishu";
|
||||
identity.writeManifest();
|
||||
expect(() => validate(identity, { requiredPackages: ["@openclaw/feishu"] })).toThrow(
|
||||
"tarball identity mismatch",
|
||||
);
|
||||
|
||||
const version = fixture();
|
||||
expect(() => validate(version, { expectedCandidateVersion: "2026.8.1-beta.2" })).toThrow(
|
||||
"version differs",
|
||||
);
|
||||
|
||||
const source = fixture();
|
||||
expect(() => validate(source, { expectedSourceSha: "b".repeat(40) })).toThrow(
|
||||
"source SHA differs",
|
||||
);
|
||||
|
||||
const required = fixture();
|
||||
expect(() => validate(required, { requiredPackages: ["@openclaw/feishu"] })).toThrow(
|
||||
"package set differs",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,13 @@ const WORKFLOW_CALL_ONLY_INPUTS = new Set([
|
||||
"package_source_sha",
|
||||
"package_sha256",
|
||||
"package_version",
|
||||
"enable_prepublish_plugin_registry",
|
||||
"prepublish_plugin_registry_artifact_name",
|
||||
"prepublish_plugin_registry_artifact_id",
|
||||
"prepublish_plugin_registry_artifact_digest",
|
||||
"prepublish_plugin_registry_artifact_run_id",
|
||||
"prepublish_plugin_registry_artifact_run_attempt",
|
||||
"prepublish_plugin_registry_manifest_sha256",
|
||||
"shared_image_artifact_name",
|
||||
"shared_image_artifact_id",
|
||||
"shared_image_artifact_digest",
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
CONFIG_COMMAND_TIMEOUT_MS,
|
||||
isReleaseBefore,
|
||||
resolveScenarioConfigSteps,
|
||||
resolveUpgradeSurvivorConfigSteps,
|
||||
resolveUpgradeSurvivorConfigStepsForBaseline,
|
||||
resolveUpgradeSurvivorOpenClawCommand,
|
||||
runUpgradeSurvivorOpenClawStep,
|
||||
} from "../../scripts/e2e/lib/upgrade-survivor/config-recipe.mjs";
|
||||
@@ -78,6 +80,20 @@ describe("upgrade survivor config recipe command resolution", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("inserts scenario config before final validation", () => {
|
||||
const steps = resolveUpgradeSurvivorConfigSteps("feishu-channel");
|
||||
expect(steps.find((step) => step.id === "channels-discord")).toBeDefined();
|
||||
expect(steps.find((step) => step.id === "channels-feishu")).toBeDefined();
|
||||
expect(steps.at(-1)?.id).toBe("validate");
|
||||
});
|
||||
|
||||
it("removes unsupported scenario config for older baselines", () => {
|
||||
const steps = resolveUpgradeSurvivorConfigStepsForBaseline("feishu-channel", "2026.3.13");
|
||||
expect(steps.find((step) => step.id === "channels-discord")).toBeDefined();
|
||||
expect(steps.find((step) => step.id === "channels-feishu")).toBeUndefined();
|
||||
expect(steps.at(-1)?.id).toBe("validate");
|
||||
});
|
||||
|
||||
it("bounds baseline config commands and reports spawn errors", () => {
|
||||
const calls: unknown[] = [];
|
||||
const timeoutError = Object.assign(new Error("spawnSync openclaw ETIMEDOUT"), {
|
||||
|
||||
Reference in New Issue
Block a user