mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(release): restore direct package Telegram acceptance (#129912)
* fix(release): provision package acceptance plugin registry * fix(release): accept registry tuple in package dispatch * fix(release): default Docker advisory input * fix(release): bind artifact registry to package source * fix(release): preserve package advisory dispatch * fix(ci): bound hosted release gate checks Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> * test(ci): update release-gate manifest fixture * fix(ci): avoid duplicate release gate lint * test(release): restore Docker helper fixture path --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
+40
-12
@@ -545,12 +545,13 @@ jobs:
|
||||
OPENCLAW_CI_CHANGED_PATHS_JSON: ${{ steps.changed_scope.outputs.changed_paths_json || 'null' }}
|
||||
OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }}
|
||||
OPENCLAW_CI_HISTORICAL_TARGET: ${{ steps.historical_target.outputs.eligible || 'false' }}
|
||||
OPENCLAW_CI_RELEASE_GATE: ${{ inputs.release_gate && 'true' || 'false' }}
|
||||
OPENCLAW_CI_RELEASE_CANDIDATE_TARGET: ${{ steps.release_candidate_target.outputs.eligible || 'false' }}
|
||||
OPENCLAW_CI_TARGET_CONTEXT_TARGET: ${{ steps.target_context_target.outputs.eligible || 'false' }}
|
||||
OPENCLAW_CI_WORKFLOW_REVISION: ${{ github.sha }}
|
||||
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
|
||||
OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }}
|
||||
OPENCLAW_CI_RUNNER_BACKEND: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}
|
||||
OPENCLAW_CI_RUNNER_BACKEND: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}
|
||||
run: |
|
||||
manifest_node_args=()
|
||||
if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ]; then
|
||||
@@ -668,6 +669,7 @@ jobs:
|
||||
})();
|
||||
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
|
||||
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
|
||||
const releaseGate = parseBoolean(process.env.OPENCLAW_CI_RELEASE_GATE);
|
||||
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
|
||||
const runNodeFastOnly =
|
||||
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_ONLY);
|
||||
@@ -755,6 +757,24 @@ jobs:
|
||||
});
|
||||
}
|
||||
}
|
||||
if (releaseGate) {
|
||||
checksFastCoreTasks.push(
|
||||
...Array.from({ length: 5 }, (_, index) => {
|
||||
const stripe = index + 1;
|
||||
return {
|
||||
check_name: `checks-fast-release-lint-core-${stripe}`,
|
||||
runtime: "node",
|
||||
stripe,
|
||||
task: `release-lint-core-${stripe}`,
|
||||
};
|
||||
}),
|
||||
{
|
||||
check_name: "checks-fast-release-lint-extensions",
|
||||
runtime: "node",
|
||||
task: "release-lint-extensions",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const compactPullRequest = isCanonicalRepository && eventName === "pull_request";
|
||||
const dockerSeedLanes =
|
||||
@@ -1930,11 +1950,11 @@ jobs:
|
||||
max-parallel: 12
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
env:
|
||||
CHECKOUT_BASE_SHA: ${{ matrix.task == 'baseline-ratchets' && needs.preflight.outputs.diff_base_revision || '' }}
|
||||
CHECKOUT_BASE_SHA: ${{ (matrix.task == 'baseline-ratchets' || startsWith(matrix.task, 'release-lint-')) && needs.preflight.outputs.diff_base_revision || '' }}
|
||||
steps:
|
||||
- *linux_node_checkout_step
|
||||
- name: Prepare release-gate ratchet merge tree
|
||||
if: matrix.task == 'baseline-ratchets' && github.event_name == 'workflow_dispatch' && inputs.release_gate
|
||||
if: (matrix.task == 'baseline-ratchets' || startsWith(matrix.task, 'release-lint-')) && github.event_name == 'workflow_dispatch' && inputs.release_gate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }}
|
||||
@@ -1976,7 +1996,6 @@ jobs:
|
||||
fi
|
||||
git checkout --detach "$merge_sha"
|
||||
echo "RATCHET_BASE_REF=${frozen_base_sha}" >> "$GITHUB_ENV"
|
||||
echo "RATCHET_RELEASE_MERGE_TREE=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.ci-harness/.github/actions/setup-node-env
|
||||
@@ -2046,9 +2065,14 @@ jobs:
|
||||
fi
|
||||
pnpm check:max-lines-ratchet --base "$base_ref"
|
||||
pnpm check:assertion-safety --base "$base_ref"
|
||||
if [[ "${RATCHET_RELEASE_MERGE_TREE:-}" == "true" ]]; then
|
||||
node --import tsx scripts/run-oxlint-shards.mts --only=core --only=extensions --threads=1
|
||||
fi
|
||||
;;
|
||||
release-lint-core-*)
|
||||
stripe="${TASK#release-lint-core-}"
|
||||
node --import tsx scripts/run-oxlint-shards.mts \
|
||||
--only=core --split-core --core-stripe="${stripe}/5" --threads=1
|
||||
;;
|
||||
release-lint-extensions)
|
||||
node --import tsx scripts/run-oxlint-shards.mts --only=extensions --threads=1
|
||||
;;
|
||||
bun-launcher)
|
||||
OPENCLAW_E2E_SKIP_BUILD=1 OPENCLAW_TEST_BUN_LAUNCHER=1 pnpm test test/openclaw-launcher.e2e.test.ts
|
||||
@@ -2636,9 +2660,10 @@ jobs:
|
||||
FROZEN_TARGET: ${{ needs.preflight.outputs.frozen_target }}
|
||||
HISTORICAL_TARGET: ${{ needs.preflight.outputs.compatibility_target }}
|
||||
FORMAT_CHECK: ${{ needs.preflight.outputs.run_format_check }}
|
||||
RELEASE_GATE: ${{ inputs.release_gate && 'true' || 'false' }}
|
||||
RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }}
|
||||
RUN_UI_TESTS: ${{ needs.preflight.outputs.run_ui_tests }}
|
||||
RUNNER_BACKEND: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}
|
||||
RUNNER_BACKEND: ${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}
|
||||
OPENCLAW_LOCAL_CHECK: "0"
|
||||
TASK: ${{ matrix.task }}
|
||||
PR_BASE_SHA: ${{ github.event_name == 'pull_request' && needs.preflight.outputs.diff_base_revision || '' }}
|
||||
@@ -2735,9 +2760,12 @@ jobs:
|
||||
lint_args=(--threads=8)
|
||||
if [ "$RUNNER_BACKEND" = "github" ]; then
|
||||
# Five dedicated hosted jobs own the aggregated core Programs. Keep
|
||||
# extension preparation, optional UI checks, and formatting in
|
||||
# this sixth lane so each 4-core runner stays below ~6m.
|
||||
# extension preparation in this sixth lane except when the exact
|
||||
# merge-tree release lint job already owns it.
|
||||
lint_args=(--only=extensions --only=scripts --threads=1)
|
||||
if [ "$RELEASE_GATE" = "true" ]; then
|
||||
lint_args=(--only=scripts --threads=1)
|
||||
fi
|
||||
export GOMAXPROCS=2
|
||||
elif [ "$(nproc)" -lt 8 ]; then
|
||||
# Fork PRs build each semantic Program once. Bound both oxlint
|
||||
@@ -2875,7 +2903,7 @@ jobs:
|
||||
contents: read
|
||||
name: check-lint-core-${{ matrix.stripe }}
|
||||
needs: [preflight]
|
||||
if: ${{ needs.preflight.outputs.run_check == 'true' && (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
if: ${{ needs.preflight.outputs.run_check == 'true' && (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
@@ -2910,7 +2938,7 @@ jobs:
|
||||
contents: read
|
||||
name: check-test-types-core-${{ matrix.stripe }}
|
||||
needs: [preflight]
|
||||
if: ${{ needs.preflight.outputs.run_check == 'true' && (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
if: ${{ needs.preflight.outputs.run_check == 'true' && (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
# Measured 232-269s per stripe on hosted 4-core; hybrid attempt 1 rides
|
||||
# the 8 vCPU class like the compact large rows, retries stay hosted.
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && github.run_attempt > 1) && 'ubuntu-24.04' || github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association)) && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
|
||||
@@ -409,11 +409,15 @@ jobs:
|
||||
echo "Artifact-backed Telegram E2E requires the complete prerelease plugin registry tuple." >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_registry_name="docker-e2e-prepublish-plugin-registry-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}"
|
||||
if [[ "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME" != "$expected_registry_name" ]]; then
|
||||
echo "Prerelease plugin registry artifact name does not match its producer run." >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_registry_suffix="-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}"
|
||||
case "$PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME" in
|
||||
"docker-e2e-prepublish-plugin-registry${expected_registry_suffix}" | \
|
||||
"package-acceptance-telegram-plugin-registry${expected_registry_suffix}") ;;
|
||||
*)
|
||||
echo "Prerelease plugin registry artifact name does not match its producer run." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
case "${PROVIDER_MODE}" in
|
||||
mock-openai | live-frontier) ;;
|
||||
|
||||
@@ -1307,6 +1307,7 @@ jobs:
|
||||
package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.package_mode != 'published') && 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 }}
|
||||
prepublish_plugin_registry_json: ${{ needs.prepare_release_package.outputs.prepublish_plugin_registry_json }}
|
||||
suite_profile: custom
|
||||
docker_lanes: release-typed-onboarding doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update plugin-binding-command-escape
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
|
||||
@@ -249,6 +249,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
prepublish_plugin_registry_json:
|
||||
description: Immutable prerelease plugin registry tuple from the package producer
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
suite_profile:
|
||||
description: "Acceptance profile: smoke, package, telegram, product, full, or custom"
|
||||
required: false
|
||||
@@ -428,6 +433,7 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.19.0"
|
||||
PACKAGE_ARTIFACT_NAME: package-under-test-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
TELEGRAM_PLUGIN_REGISTRY_ARTIFACT_NAME: package-acceptance-telegram-plugin-registry-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
||||
jobs:
|
||||
resolve_package:
|
||||
@@ -448,6 +454,7 @@ jobs:
|
||||
package_source_sha: ${{ steps.resolve.outputs.package_source_sha }}
|
||||
package_sha256: ${{ steps.resolve.outputs.sha256 }}
|
||||
package_version: ${{ steps.resolve.outputs.package_version }}
|
||||
prepublish_plugin_registry_json: ${{ steps.registry_identity.outputs.json }}
|
||||
published_upgrade_survivor_baseline: ${{ steps.upgrade_survivor_baselines.outputs.baseline }}
|
||||
published_upgrade_survivor_baselines: ${{ steps.upgrade_survivor_baselines.outputs.baselines }}
|
||||
published_upgrade_survivor_scenarios: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
@@ -471,6 +478,80 @@ jobs:
|
||||
install-bun: ${{ inputs.source == 'ref' && 'true' || 'false' }}
|
||||
install-deps: "true"
|
||||
|
||||
- name: Validate prerelease plugin registry input
|
||||
id: registry_input
|
||||
env:
|
||||
CANDIDATE_ARTIFACT_JSON: ${{ inputs.candidate_artifact_json || '' }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_JSON: ${{ inputs.prepublish_plugin_registry_json || '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
normalize_registry_tuple() {
|
||||
local json="$1"
|
||||
local source="$2"
|
||||
[[ -n "${json// }" ]] || return 0
|
||||
jq -cer \
|
||||
--arg source "$source" \
|
||||
'
|
||||
def digits: test("^[1-9][0-9]*$");
|
||||
def hex64: test("^[a-f0-9]{64}$");
|
||||
def fields: [
|
||||
"prepublishPluginRegistryArtifactName", "prepublishPluginRegistryArtifactId",
|
||||
"prepublishPluginRegistryArtifactDigest", "prepublishPluginRegistryArtifactRunId",
|
||||
"prepublishPluginRegistryArtifactRunAttempt", "prepublishPluginRegistryManifestSha256"
|
||||
];
|
||||
def tuple:
|
||||
. as $input
|
||||
| reduce fields[] as $field ({}; .[$field] = $input[$field])
|
||||
| .prepublishPluginRegistryArtifactId |= tostring
|
||||
| .prepublishPluginRegistryArtifactRunId |= tostring
|
||||
| .prepublishPluginRegistryArtifactRunAttempt |= tostring;
|
||||
if type != "object" then error("not an object") else . end
|
||||
| . as $input
|
||||
| [fields[] as $field | select($input | has($field))] as $present
|
||||
| if ($present | length) == 0 and $source == "candidate" then empty
|
||||
elif ($present | length) != 6 or
|
||||
($source == "direct" and ((keys | sort) != (fields | sort)))
|
||||
then error("incomplete or unexpected fields")
|
||||
else tuple
|
||||
end
|
||||
| . as $tuple
|
||||
| select(
|
||||
(
|
||||
.prepublishPluginRegistryArtifactName ==
|
||||
("docker-e2e-prepublish-plugin-registry-" +
|
||||
$tuple.prepublishPluginRegistryArtifactRunId + "-" +
|
||||
$tuple.prepublishPluginRegistryArtifactRunAttempt) or
|
||||
.prepublishPluginRegistryArtifactName ==
|
||||
("package-acceptance-telegram-plugin-registry-" +
|
||||
$tuple.prepublishPluginRegistryArtifactRunId + "-" +
|
||||
$tuple.prepublishPluginRegistryArtifactRunAttempt)
|
||||
) and
|
||||
([
|
||||
.prepublishPluginRegistryArtifactId,
|
||||
.prepublishPluginRegistryArtifactRunId,
|
||||
.prepublishPluginRegistryArtifactRunAttempt
|
||||
] | all(.[]; digits)) and
|
||||
([
|
||||
.prepublishPluginRegistryArtifactDigest,
|
||||
.prepublishPluginRegistryManifestSha256
|
||||
] | all(.[]; hex64))
|
||||
)
|
||||
' <<< "$json" 2>/dev/null || {
|
||||
echo "Prerelease plugin registry JSON must contain one complete immutable tuple." >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
candidate_registry="$(normalize_registry_tuple "$CANDIDATE_ARTIFACT_JSON" candidate)"
|
||||
direct_registry="$(normalize_registry_tuple "$PREPUBLISH_PLUGIN_REGISTRY_JSON" direct)"
|
||||
if [[ -n "$candidate_registry" && -n "$direct_registry" &&
|
||||
"$candidate_registry" != "$direct_registry" ]]; then
|
||||
echo "Prerelease plugin registry inputs disagree." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'json=%s\n' "${direct_registry:-$candidate_registry}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate package artifact input identity
|
||||
id: input_artifact
|
||||
if: inputs.source == 'artifact'
|
||||
@@ -558,6 +639,8 @@ jobs:
|
||||
PACKAGE_FILE_NAME: ${{ inputs.package_file_name }}
|
||||
PACKAGE_SOURCE_SHA: ${{ inputs.package_source_sha }}
|
||||
PACKAGE_VERSION: ${{ inputs.package_version }}
|
||||
PREPUBLISH_PLUGIN_REGISTRY_JSON: ${{ steps.registry_input.outputs.json }}
|
||||
TELEGRAM_MODE: ${{ inputs.telegram_mode }}
|
||||
TRUSTED_SOURCE_ID: ${{ inputs.trusted_source_id }}
|
||||
OPENCLAW_TRUSTED_PACKAGE_TOKEN: ${{ secrets.OPENCLAW_TRUSTED_PACKAGE_TOKEN }}
|
||||
shell: bash
|
||||
@@ -578,6 +661,15 @@ jobs:
|
||||
}
|
||||
fi
|
||||
|
||||
set --
|
||||
if [[ -z "$PREPUBLISH_PLUGIN_REGISTRY_JSON" &&
|
||||
"$TELEGRAM_MODE" != "none" &&
|
||||
("$SOURCE" == "ref" || "$SOURCE" == "artifact") ]]; then
|
||||
set -- \
|
||||
--plugin-registry-output-dir .artifacts/package-acceptance-telegram-plugin-registry \
|
||||
--required-plugin-packages-json '["@openclaw/codex"]'
|
||||
fi
|
||||
|
||||
node --import tsx scripts/resolve-openclaw-package-candidate.mts \
|
||||
--source "$SOURCE" \
|
||||
--package-ref "$PACKAGE_REF" \
|
||||
@@ -589,7 +681,8 @@ jobs:
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz \
|
||||
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
--github-output "$GITHUB_OUTPUT" \
|
||||
"$@"
|
||||
echo "package_file_name=openclaw-current.tgz" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$SOURCE" == "artifact" ]]; then
|
||||
jq -e \
|
||||
@@ -747,6 +840,44 @@ jobs:
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Telegram prerelease plugin registry artifact
|
||||
id: upload_telegram_registry
|
||||
if: steps.resolve.outputs.plugin_registry_manifest_sha256 != ''
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ env.TELEGRAM_PLUGIN_REGISTRY_ARTIFACT_NAME }}
|
||||
path: .artifacts/package-acceptance-telegram-plugin-registry/
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Prepare prerelease plugin registry identity
|
||||
id: registry_identity
|
||||
env:
|
||||
GENERATED_ARTIFACT_DIGEST: ${{ steps.upload_telegram_registry.outputs.artifact-digest }}
|
||||
GENERATED_ARTIFACT_ID: ${{ steps.upload_telegram_registry.outputs.artifact-id }}
|
||||
GENERATED_MANIFEST_SHA256: ${{ steps.resolve.outputs.plugin_registry_manifest_sha256 }}
|
||||
SUPPLIED_REGISTRY_JSON: ${{ steps.registry_input.outputs.json }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$GENERATED_ARTIFACT_ID" ]]; then
|
||||
[[ -z "$SUPPLIED_REGISTRY_JSON" ]] || {
|
||||
echo "Package Acceptance generated a registry despite a supplied immutable tuple." >&2
|
||||
exit 1
|
||||
}
|
||||
SUPPLIED_REGISTRY_JSON="$(
|
||||
jq -cn \
|
||||
--arg prepublishPluginRegistryArtifactName "$TELEGRAM_PLUGIN_REGISTRY_ARTIFACT_NAME" \
|
||||
--arg prepublishPluginRegistryArtifactId "$GENERATED_ARTIFACT_ID" \
|
||||
--arg prepublishPluginRegistryArtifactDigest "$GENERATED_ARTIFACT_DIGEST" \
|
||||
--arg prepublishPluginRegistryArtifactRunId "$GITHUB_RUN_ID" \
|
||||
--arg prepublishPluginRegistryArtifactRunAttempt "$GITHUB_RUN_ATTEMPT" \
|
||||
--arg prepublishPluginRegistryManifestSha256 "$GENERATED_MANIFEST_SHA256" \
|
||||
'$ARGS.named'
|
||||
)"
|
||||
fi
|
||||
printf 'json=%s\n' "$SUPPLIED_REGISTRY_JSON" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Summarize package candidate
|
||||
env:
|
||||
INPUT_ARTIFACT_DIGEST: ${{ steps.input_artifact.outputs.artifact_digest }}
|
||||
@@ -909,7 +1040,7 @@ jobs:
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
|
||||
with: &docker_acceptance_inputs
|
||||
advisory: ${{ inputs.advisory }}
|
||||
advisory: ${{ inputs.advisory || false }}
|
||||
ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}
|
||||
include_repo_e2e: false
|
||||
include_release_path_suites: ${{ needs.resolve_package.outputs.include_release_path_suites == 'true' }}
|
||||
@@ -929,12 +1060,12 @@ jobs:
|
||||
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 || '' }}
|
||||
prepublish_plugin_registry_artifact_name: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_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 }}
|
||||
@@ -1023,12 +1154,12 @@ 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 }}
|
||||
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 || '' }}
|
||||
prepublish_plugin_registry_artifact_name: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '' }}
|
||||
prepublish_plugin_registry_artifact_id: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}
|
||||
prepublish_plugin_registry_artifact_digest: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}
|
||||
prepublish_plugin_registry_artifact_run_id: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}
|
||||
prepublish_plugin_registry_artifact_run_attempt: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}
|
||||
prepublish_plugin_registry_manifest_sha256: ${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}
|
||||
package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}
|
||||
harness_ref: ${{ inputs.workflow_ref }}
|
||||
provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }}
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ Barnacle treats bug-labeled issues as verification candidates rather than inacti
|
||||
|
||||
Ordinary manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI/native app i18n. Node 22 compatibility runs in Full Release Validation and manual dispatches only; push and pull request CI skip it. The exact-head `release_gate` fallback instead keeps the pull request's macOS, iOS, and generated-native-locale scope, including conservative release screenshot capture for screenshot-pipeline owners. Automatic source PRs and release gates verify native extraction inventory and Android/Apple localization safety without requiring translated or platform-generated output in the same PR. The serialized Native App Locale Refresh workflow rebuilds those artifacts in one isolated PR and enables exact-head auto-merge after required checks pass. Full native parity remains blocking for generated-artifact PRs, generated-scope release gates, ordinary manual CI, Full Release Validation, and release prep. Control UI locale parity remains advisory on automatic PR and `main` runs and blocking on manual/release CI. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true` without setting `release_gate`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
|
||||
|
||||
PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. The max-lines entry chains the environment-variable budget with the same fork-point ref before the assertion-safety check, so production source growth cannot first surface on `main`. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated.
|
||||
PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. The max-lines entry chains the environment-variable budget with the same fork-point ref before the assertion-safety check, so production source growth cannot first surface on `main`. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated. Release-gate merge-tree lint uses the same five core stripes as hosted PR CI plus one extension stripe, so no single hosted runner owns the full type-aware lint workload.
|
||||
|
||||
```bash
|
||||
gh workflow run ci.yml --ref release/YYYY.M.PATCH
|
||||
|
||||
@@ -192,7 +192,7 @@ Options:
|
||||
--output-name <name> Output tarball filename. Default: ${DEFAULT_OUTPUT_NAME}
|
||||
--metadata <file> Write package metadata JSON.
|
||||
--plugin-registry-output-dir <dir>
|
||||
Build an immutable registry for source=ref before cleanup.
|
||||
Build an immutable registry for source=ref, npm, or artifact.
|
||||
--required-plugin-packages-json <json>
|
||||
Scoped package names to include in that registry.
|
||||
--github-output <file> Append tarball, sha256, package name/version outputs.`;
|
||||
@@ -1660,6 +1660,7 @@ async function resolveCandidate(options: PackageCandidateOptions) {
|
||||
let packageTrustedReason = "";
|
||||
let packageTrustedSourceId = "";
|
||||
let packageWorktreeDir = "";
|
||||
let packageBuildSourceSha: string | undefined;
|
||||
let pluginRegistrySource: Awaited<ReturnType<typeof preparePackageSourceWorktree>> | undefined;
|
||||
let artifactMetadata: ArtifactMetadata = {};
|
||||
let pluginRegistryIdentity:
|
||||
@@ -1705,12 +1706,6 @@ async function resolveCandidate(options: PackageCandidateOptions) {
|
||||
packOutput,
|
||||
options.outputName || DEFAULT_OUTPUT_NAME,
|
||||
);
|
||||
if (options.pluginRegistryOutputDir) {
|
||||
pluginRegistrySource = await preparePackageSourceWorktree(options.packageRef);
|
||||
packageWorktreeDir = pluginRegistrySource.sourceDir;
|
||||
packageRef = options.packageRef;
|
||||
await installPackageSourceDeps(pluginRegistrySource.sourceDir);
|
||||
}
|
||||
} else if (options.source === "url" || options.source === "trusted-url") {
|
||||
if (!options.packageUrl) {
|
||||
throw new Error(`${options.source} requires --package-url`);
|
||||
@@ -1749,15 +1744,43 @@ async function resolveCandidate(options: PackageCandidateOptions) {
|
||||
: "";
|
||||
const input = await findSingleTarball(options.artifactDir);
|
||||
await fs.copyFile(input, target);
|
||||
packageBuildSourceSha = await readPackageBuildSourceSha(target);
|
||||
if (packageSourceSha && packageBuildSourceSha && packageSourceSha !== packageBuildSourceSha) {
|
||||
throw new Error(
|
||||
`artifact packageSourceSha ${packageSourceSha} does not match package build-info commit ${packageBuildSourceSha}`,
|
||||
);
|
||||
}
|
||||
if (!packageSourceSha && packageBuildSourceSha) {
|
||||
packageSourceSha = packageBuildSourceSha;
|
||||
packageTrustedReason = "package-build-info";
|
||||
}
|
||||
if (options.pluginRegistryOutputDir) {
|
||||
if (!packageBuildSourceSha) {
|
||||
throw new Error(
|
||||
"source=artifact requires a valid package build-info commit for prerelease plugin registry creation",
|
||||
);
|
||||
}
|
||||
packageTrustedReason ||= "package-build-info";
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`source must be one of: ref, npm, url, trusted-url, artifact. Got: ${options.source}`,
|
||||
);
|
||||
}
|
||||
if (options.pluginRegistryOutputDir && !pluginRegistrySource) {
|
||||
throw new Error(
|
||||
"--plugin-registry-output-dir is only supported with source=ref or source=npm",
|
||||
if (options.source !== "npm" && options.source !== "artifact") {
|
||||
throw new Error(
|
||||
"--plugin-registry-output-dir is only supported with source=ref, source=npm, or source=artifact",
|
||||
);
|
||||
}
|
||||
if (options.source === "npm") {
|
||||
packageRef = options.packageRef;
|
||||
}
|
||||
pluginRegistrySource = await preparePackageSourceWorktree(
|
||||
options.source === "npm" ? packageRef : packageSourceSha,
|
||||
);
|
||||
packageWorktreeDir = pluginRegistrySource.sourceDir;
|
||||
await installPackageSourceDeps(pluginRegistrySource.sourceDir);
|
||||
}
|
||||
if (options.pluginRegistryOutputDir && pluginRegistrySource) {
|
||||
const requiredPackages = JSON.parse(options.requiredPluginPackagesJson) as string[];
|
||||
@@ -1798,7 +1821,7 @@ async function resolveCandidate(options: PackageCandidateOptions) {
|
||||
);
|
||||
const pkg = await readPackageJson(target);
|
||||
if (!packageSourceSha) {
|
||||
packageSourceSha = await readPackageBuildSourceSha(target);
|
||||
packageSourceSha = packageBuildSourceSha ?? (await readPackageBuildSourceSha(target));
|
||||
if (packageSourceSha && !packageTrustedReason) {
|
||||
packageTrustedReason = "package-build-info";
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ function evaluateWorkflowExpression(
|
||||
headRepository?: string;
|
||||
matrix?: Record<string, unknown>;
|
||||
repository: string;
|
||||
runCheck?: boolean;
|
||||
runnerBackend?: "" | "blacksmith" | "github" | "hybrid";
|
||||
runAttempt: number;
|
||||
},
|
||||
@@ -136,6 +137,13 @@ function evaluateWorkflowExpression(
|
||||
: {},
|
||||
},
|
||||
matrix: context.matrix ?? {},
|
||||
needs: {
|
||||
preflight: {
|
||||
outputs: {
|
||||
run_check: String(context.runCheck ?? true),
|
||||
},
|
||||
},
|
||||
},
|
||||
vars: {
|
||||
OPENCLAW_CI_RUNNER_BACKEND: context.runnerBackend ?? "",
|
||||
},
|
||||
@@ -216,6 +224,7 @@ function runCiManifestFixture(options: {
|
||||
qaSmokePlan?: boolean;
|
||||
formatCheck?: boolean;
|
||||
releaseCandidateCompatibility?: boolean;
|
||||
releaseGate?: boolean;
|
||||
targetContextCompatibility?: boolean;
|
||||
nodeFastOnly?: boolean;
|
||||
nodeFastPluginContracts?: boolean;
|
||||
@@ -404,6 +413,7 @@ function runCiManifestFixture(options: {
|
||||
(options.eventName ?? "workflow_dispatch") === "workflow_dispatch"
|
||||
? "true"
|
||||
: "false",
|
||||
OPENCLAW_CI_RELEASE_GATE: String(options.releaseGate ?? false),
|
||||
OPENCLAW_CI_RELEASE_CANDIDATE_TARGET:
|
||||
options.releaseCandidateCompatibility === true ? "true" : "false",
|
||||
OPENCLAW_CI_TARGET_CONTEXT_TARGET:
|
||||
@@ -6468,6 +6478,13 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
);
|
||||
const checkShardRun = checkShardStep.run;
|
||||
const hostedCoreLint = workflow.jobs["check-lint-hosted-core-shard"];
|
||||
const hostedCoreTypes = workflow.jobs["check-test-types-hosted-core-shard"];
|
||||
const manualDispatch = {
|
||||
eventName: "workflow_dispatch",
|
||||
repository: "openclaw/openclaw",
|
||||
runnerBackend: "blacksmith",
|
||||
runAttempt: 1,
|
||||
} as const;
|
||||
const untrustedForkPullRequest = {
|
||||
authorAssociation: "NONE",
|
||||
eventName: "pull_request",
|
||||
@@ -6478,8 +6495,11 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
} as const;
|
||||
|
||||
expect(manifestStep.env.OPENCLAW_CI_RUNNER_BACKEND).toBe(
|
||||
"${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}",
|
||||
"${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}",
|
||||
);
|
||||
expect(
|
||||
evaluateWorkflowExpression(manifestStep.env.OPENCLAW_CI_RUNNER_BACKEND, manualDispatch),
|
||||
).toBe("github");
|
||||
expect(
|
||||
evaluateWorkflowExpression(
|
||||
manifestStep.env.OPENCLAW_CI_RUNNER_BACKEND,
|
||||
@@ -6489,9 +6509,15 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(
|
||||
evaluateWorkflowExpression(checkShardStep.env.RUNNER_BACKEND, untrustedForkPullRequest),
|
||||
).toBe("github");
|
||||
expect(evaluateWorkflowExpression(checkShardStep.env.RUNNER_BACKEND, manualDispatch)).toBe(
|
||||
"github",
|
||||
);
|
||||
expect(checkShardStep.env.RELEASE_GATE).toBe("${{ inputs.release_gate && 'true' || 'false' }}");
|
||||
expect(manifestStep.run).toContain("runnerBackend: process.env.OPENCLAW_CI_RUNNER_BACKEND");
|
||||
expect(checkShardRun).toContain('if [ "$RUNNER_BACKEND" = "github" ]; then');
|
||||
expect(checkShardRun).toContain("lint_args=(--only=extensions --only=scripts --threads=1)");
|
||||
expect(checkShardRun).toContain('if [ "$RELEASE_GATE" = "true" ]; then');
|
||||
expect(checkShardRun).toContain("lint_args=(--only=scripts --threads=1)");
|
||||
expect(checkShardRun).toContain('elif [ "$(nproc)" -lt 8 ]; then');
|
||||
expect(checkShardRun).toContain("lint_args=(--threads=1)");
|
||||
expect(checkShardRun).not.toContain("lint_args=(--split-core --threads=1)");
|
||||
@@ -6504,9 +6530,11 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(hostedCoreLint.if).toContain(
|
||||
"github.event.pull_request.head.repo.full_name != github.repository",
|
||||
);
|
||||
expect(workflow.jobs["check-test-types-hosted-core-shard"].if).toContain(
|
||||
expect(hostedCoreTypes.if).toContain(
|
||||
"github.event.pull_request.head.repo.full_name != github.repository",
|
||||
);
|
||||
expect(evaluateWorkflowExpression(hostedCoreLint.if, manualDispatch)).toBe(true);
|
||||
expect(evaluateWorkflowExpression(hostedCoreTypes.if, manualDispatch)).toBe(true);
|
||||
expect(hostedCoreLint["runs-on"]).toBe("ubuntu-24.04");
|
||||
expect(hostedCoreLint.strategy).toEqual({
|
||||
"fail-fast": false,
|
||||
@@ -6544,14 +6572,14 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
"pull-requests": "read",
|
||||
});
|
||||
expect(checksFastJob.env.CHECKOUT_BASE_SHA).toBe(
|
||||
"${{ matrix.task == 'baseline-ratchets' && needs.preflight.outputs.diff_base_revision || '' }}",
|
||||
"${{ (matrix.task == 'baseline-ratchets' || startsWith(matrix.task, 'release-lint-')) && needs.preflight.outputs.diff_base_revision || '' }}",
|
||||
);
|
||||
expect(checkout.run).toContain(
|
||||
'fetch_refs+=("+${CHECKOUT_BASE_SHA}:refs/remotes/origin/ci-ratchet-base")',
|
||||
);
|
||||
expect(checkout.run).toContain('"${fetch_refs[@]}" || return 1');
|
||||
expect(releaseGateMerge.if).toBe(
|
||||
"matrix.task == 'baseline-ratchets' && github.event_name == 'workflow_dispatch' && inputs.release_gate",
|
||||
"(matrix.task == 'baseline-ratchets' || startsWith(matrix.task, 'release-lint-')) && github.event_name == 'workflow_dispatch' && inputs.release_gate",
|
||||
);
|
||||
expect(checksFastRun.run).toContain("baseline-ratchets)");
|
||||
expect(checksFastRun.run).toContain("coercion-helpers)");
|
||||
@@ -6603,9 +6631,6 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(releaseGateMerge.run).toContain(
|
||||
'echo "RATCHET_BASE_REF=${frozen_base_sha}" >> "$GITHUB_ENV"',
|
||||
);
|
||||
expect(releaseGateMerge.run).toContain(
|
||||
'echo "RATCHET_RELEASE_MERGE_TREE=true" >> "$GITHUB_ENV"',
|
||||
);
|
||||
expect(checksFastRun.run).not.toContain("PROTOCOL_MANUAL_BASE_SHA");
|
||||
expect(checksFastRun.run).toContain(
|
||||
'"+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/protocol-since-base"',
|
||||
@@ -6631,10 +6656,10 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
);
|
||||
expect(maxLinesRatchet).toContain("checkEnvVarCount(envVarCountArgs(argv), root);");
|
||||
expect(checksFastRun.run).toContain(
|
||||
'if [[ "${RATCHET_RELEASE_MERGE_TREE:-}" == "true" ]]; then',
|
||||
'--only=core --split-core --core-stripe="${stripe}/5" --threads=1',
|
||||
);
|
||||
expect(checksFastRun.run).toContain(
|
||||
"node --import tsx scripts/run-oxlint-shards.mts --only=core --only=extensions --threads=1",
|
||||
"node --import tsx scripts/run-oxlint-shards.mts --only=extensions --threads=1",
|
||||
);
|
||||
expect(checksFastRun.run).not.toContain(
|
||||
"node scripts/run-oxlint.mjs src ui/src packages extensions",
|
||||
@@ -6665,6 +6690,34 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
task: "coercion-helpers",
|
||||
},
|
||||
]);
|
||||
|
||||
const releaseGate = runCiManifestFixture({
|
||||
bundledPlanner: true,
|
||||
eventName: "workflow_dispatch",
|
||||
historicalCompatibility: false,
|
||||
releaseGate: true,
|
||||
});
|
||||
expect(releaseGate.status, releaseGate.output).toBe(0);
|
||||
expect(
|
||||
JSON.parse(
|
||||
expectDefined(releaseGate.outputs.checks_fast_core_matrix, "release-gate checks matrix"),
|
||||
).include.filter((entry: { task: string }) => entry.task.startsWith("release-lint-")),
|
||||
).toEqual([
|
||||
...Array.from({ length: 5 }, (_, index) => {
|
||||
const stripe = index + 1;
|
||||
return {
|
||||
check_name: `checks-fast-release-lint-core-${stripe}`,
|
||||
runtime: "node",
|
||||
stripe,
|
||||
task: `release-lint-core-${stripe}`,
|
||||
};
|
||||
}),
|
||||
{
|
||||
check_name: "checks-fast-release-lint-extensions",
|
||||
runtime: "node",
|
||||
task: "release-lint-extensions",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses target-owned CI plans and capabilities for older release checkouts", () => {
|
||||
|
||||
@@ -4759,6 +4759,29 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
|
||||
'-v "$harness_root/test/e2e/qa-lab:/app/test/e2e/qa-lab:ro"',
|
||||
'-v "$harness_root/test/helpers:/app/test/helpers:ro"',
|
||||
]);
|
||||
|
||||
const script = repoRootShell`
|
||||
export DOCKER_E2E_HARNESS_ROOT_DIR=/trusted-harness
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
|
||||
docker_e2e_harness_mount_args
|
||||
for ((index = 1; index < \${#DOCKER_E2E_HARNESS_ARGS[@]}; index += 2)); do
|
||||
printf "%s\\n" "\${DOCKER_E2E_HARNESS_ARGS[$index]}"
|
||||
done
|
||||
`;
|
||||
const mounts = execFileSync("bash", ["-lc", script], { encoding: "utf8" }).trim().split("\n");
|
||||
|
||||
expect(mounts).toEqual([
|
||||
"/trusted-harness/scripts/e2e:/app/scripts/e2e:ro",
|
||||
"/trusted-harness/scripts/lib:/app/scripts/lib:ro",
|
||||
"/trusted-harness/packages/gateway-client/src:/app/packages/gateway-client/src:ro",
|
||||
"/trusted-harness/packages/normalization-core/package.json:/app/packages/normalization-core/package.json:ro",
|
||||
"/trusted-harness/packages/normalization-core/src:/app/packages/normalization-core/src:ro",
|
||||
"/trusted-harness/tsconfig.json:/app/tsconfig.json:ro",
|
||||
"/trusted-harness/test/e2e/qa-lab:/app/test/e2e/qa-lab:ro",
|
||||
"/trusted-harness/test/helpers:/app/test/helpers:ro",
|
||||
"/trusted-harness/scripts/prepublish-plugin-registry-artifact.mjs:/app/scripts/prepublish-plugin-registry-artifact.mjs:ro",
|
||||
"/trusted-harness/scripts/windows-cmd-helpers.mjs:/app/scripts/windows-cmd-helpers.mjs:ro",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves pnpm lookup paths for scheduled Docker child lanes", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Package Acceptance Workflow tests cover package acceptance workflow script behavior.
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
mkdirSync,
|
||||
@@ -813,6 +814,101 @@ function runPackageAcceptanceProfile(params: {
|
||||
return { outputs, result };
|
||||
}
|
||||
|
||||
function runPackageAcceptanceRegistryInputValidation(params: {
|
||||
candidateArtifactJson?: string;
|
||||
prepublishPluginRegistryJson?: string;
|
||||
}) {
|
||||
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
||||
const script = workflowStep(job, "Validate prerelease plugin registry input").run;
|
||||
if (!script) {
|
||||
throw new Error("Expected package acceptance registry input validation script");
|
||||
}
|
||||
const workdir = tempDirs.make("package-acceptance-registry-input-");
|
||||
const outputPath = resolve(workdir, "github-output");
|
||||
const result = spawnSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
CANDIDATE_ARTIFACT_JSON: params.candidateArtifactJson ?? "",
|
||||
GITHUB_OUTPUT: outputPath,
|
||||
PATH: process.env.PATH,
|
||||
PREPUBLISH_PLUGIN_REGISTRY_JSON: params.prepublishPluginRegistryJson ?? "",
|
||||
},
|
||||
});
|
||||
const output = result.status === 0 ? readFileSync(outputPath, "utf8") : "";
|
||||
return { output, result };
|
||||
}
|
||||
|
||||
function packageAcceptanceRegistryTuple(overrides: Record<string, string> = {}) {
|
||||
return {
|
||||
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-123-2",
|
||||
prepublishPluginRegistryArtifactId: "456",
|
||||
prepublishPluginRegistryArtifactDigest: "a".repeat(64),
|
||||
prepublishPluginRegistryArtifactRunId: "123",
|
||||
prepublishPluginRegistryArtifactRunAttempt: "2",
|
||||
prepublishPluginRegistryManifestSha256: "b".repeat(64),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function runPackageAcceptanceResolveScript(params: {
|
||||
prepublishPluginRegistryJson?: string;
|
||||
source: "artifact" | "npm" | "ref" | "trusted-url" | "url";
|
||||
telegramMode: "mock-openai" | "none";
|
||||
}) {
|
||||
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
||||
const script = workflowStep(job, "Resolve package candidate").run;
|
||||
if (!script) {
|
||||
throw new Error("Expected package acceptance resolve script");
|
||||
}
|
||||
const workdir = tempDirs.make("package-acceptance-resolve-");
|
||||
const binDir = resolve(workdir, "bin");
|
||||
const capturePath = resolve(workdir, "node-args");
|
||||
const outputPath = resolve(workdir, "github-output");
|
||||
const artifactDir = resolve(workdir, ".artifacts/package-candidate-input");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
mkdirSync(artifactDir, { recursive: true });
|
||||
const nodePath = resolve(binDir, "node");
|
||||
const artifactPath = resolve(artifactDir, "openclaw-current.tgz");
|
||||
const artifactBody = "package acceptance artifact";
|
||||
writeFileSync(artifactPath, artifactBody);
|
||||
writeFileSync(
|
||||
nodePath,
|
||||
`#!/bin/sh
|
||||
printf "%s\\n" "$@" > "$CAPTURE_PATH"
|
||||
if [ "$SOURCE" = "artifact" ]; then
|
||||
mkdir -p .artifacts/docker-e2e-package
|
||||
printf '{"name":"openclaw","sha256":"%s","packageSourceSha":"%s","version":"%s"}\\n' \
|
||||
"$PACKAGE_SHA256" "$PACKAGE_SOURCE_SHA" "$PACKAGE_VERSION" \
|
||||
> .artifacts/docker-e2e-package/package-candidate.json
|
||||
fi
|
||||
`,
|
||||
);
|
||||
chmodSync(nodePath, 0o755);
|
||||
const result = spawnSync("bash", ["-c", script], {
|
||||
cwd: workdir,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
CAPTURE_PATH: capturePath,
|
||||
GITHUB_OUTPUT: outputPath,
|
||||
OPENCLAW_TRUSTED_PACKAGE_TOKEN: "",
|
||||
PACKAGE_FILE_NAME: "openclaw-current.tgz",
|
||||
PACKAGE_REF: "HEAD",
|
||||
PACKAGE_SHA256: createHash("sha256").update(artifactBody).digest("hex"),
|
||||
PACKAGE_SOURCE_SHA: "a".repeat(40),
|
||||
PACKAGE_SPEC: "openclaw@beta",
|
||||
PACKAGE_URL: "https://example.invalid/openclaw.tgz",
|
||||
PACKAGE_VERSION: "2026.8.26",
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
PREPUBLISH_PLUGIN_REGISTRY_JSON: params.prepublishPluginRegistryJson ?? "",
|
||||
SOURCE: params.source,
|
||||
TELEGRAM_MODE: params.telegramMode,
|
||||
TRUSTED_SOURCE_ID: "",
|
||||
},
|
||||
});
|
||||
const args = result.status === 0 ? readFileSync(capturePath, "utf8") : "";
|
||||
return { args, result };
|
||||
}
|
||||
|
||||
function runNpmTelegramInputValidation(overrides: Record<string, string>) {
|
||||
const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
||||
const script = workflowStep(job, "Validate inputs and secrets").run;
|
||||
@@ -2605,6 +2701,17 @@ describe("package acceptance workflow", () => {
|
||||
description: "Acceptance profile: smoke, package, telegram, product, full, or custom",
|
||||
options: ["smoke", "package", "telegram", "product", "full", "custom"],
|
||||
});
|
||||
const dispatchInputs = parsedWorkflow.on?.workflow_dispatch?.inputs;
|
||||
const callInputs = parsedWorkflow.on?.workflow_call?.inputs;
|
||||
expect(dispatchInputs?.prepublish_plugin_registry_json).toBeUndefined();
|
||||
expect(dispatchInputs?.advisory).toEqual(callInputs?.advisory);
|
||||
expect(callInputs?.advisory).toEqual({
|
||||
description: "Treat acceptance failures as advisory for the caller",
|
||||
required: false,
|
||||
default: false,
|
||||
type: "boolean",
|
||||
});
|
||||
expect(Object.keys(dispatchInputs ?? {})).toHaveLength(25);
|
||||
expect(parsedWorkflow.on?.workflow_dispatch?.inputs?.telegram_advisory).toBeUndefined();
|
||||
expect(parsedWorkflow.on?.workflow_call?.inputs?.suite_profile).toMatchObject({
|
||||
default: "package",
|
||||
@@ -2684,17 +2791,17 @@ describe("package acceptance workflow", () => {
|
||||
);
|
||||
const registryInputs = {
|
||||
prepublish_plugin_registry_artifact_name:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '' }}",
|
||||
prepublish_plugin_registry_artifact_id:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
|
||||
prepublish_plugin_registry_artifact_digest:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}",
|
||||
prepublish_plugin_registry_artifact_run_id:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}",
|
||||
prepublish_plugin_registry_artifact_run_attempt:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}",
|
||||
prepublish_plugin_registry_manifest_sha256:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
||||
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
||||
};
|
||||
expect(packageTelegram.with).toMatchObject(registryInputs);
|
||||
const registryInputSchema = Object.fromEntries(
|
||||
@@ -2713,7 +2820,11 @@ describe("package acceptance workflow", () => {
|
||||
"Prerelease plugin registry inputs require an artifact-backed OpenClaw package.",
|
||||
);
|
||||
expect(npmTelegramWorkflow).toContain(
|
||||
'expected_registry_name="docker-e2e-prepublish-plugin-registry-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}"',
|
||||
'expected_registry_suffix="-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}"',
|
||||
);
|
||||
expect(npmTelegramWorkflow).toContain(
|
||||
'"docker-e2e-prepublish-plugin-registry${expected_registry_suffix}" | \\\n' +
|
||||
' "package-acceptance-telegram-plugin-registry${expected_registry_suffix}"',
|
||||
);
|
||||
expect(npmTelegramWorkflow).not.toContain(
|
||||
"Prerelease plugin registry and package artifacts must come from the same workflow run attempt.",
|
||||
@@ -2729,6 +2840,17 @@ describe("package acceptance workflow", () => {
|
||||
expect(dockerAcceptance.with?.ref).toBe(
|
||||
"${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}",
|
||||
);
|
||||
expect(dockerAcceptance.with?.advisory).toBe("${{ inputs.advisory || false }}");
|
||||
expect(dockerAcceptanceRegistry.with?.advisory).toBe("${{ inputs.advisory || false }}");
|
||||
expect(dockerAcceptance.with?.prepublish_plugin_registry_artifact_name).toContain(
|
||||
"startsWith(",
|
||||
);
|
||||
expect(dockerAcceptance.with?.prepublish_plugin_registry_artifact_name).toContain(
|
||||
"'docker-e2e-prepublish-plugin-registry-'",
|
||||
);
|
||||
expect(packageTelegram.with?.prepublish_plugin_registry_artifact_name).not.toContain(
|
||||
"startsWith(",
|
||||
);
|
||||
expect(npm12Install.if).toBe("inputs.suite_profile != 'telegram'");
|
||||
expect(dockerAcceptance.if).toBe(
|
||||
"inputs.suite_profile != 'telegram' && inputs.shared_image_policy == 'no-push-artifact'",
|
||||
@@ -2771,6 +2893,101 @@ describe("package acceptance workflow", () => {
|
||||
expect(workflow).toContain("Published upgrade survivor scenarios:");
|
||||
});
|
||||
|
||||
it("normalizes one closed prerelease registry tuple before child workflows", () => {
|
||||
const tuple = packageAcceptanceRegistryTuple();
|
||||
const direct = runPackageAcceptanceRegistryInputValidation({
|
||||
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
||||
});
|
||||
expect(direct.result.status, direct.result.stderr).toBe(0);
|
||||
expect(direct.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
||||
|
||||
const candidate = runPackageAcceptanceRegistryInputValidation({
|
||||
candidateArtifactJson: JSON.stringify({
|
||||
imageArtifactName: "image-123-2",
|
||||
...tuple,
|
||||
}),
|
||||
});
|
||||
expect(candidate.result.status, candidate.result.stderr).toBe(0);
|
||||
expect(candidate.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
||||
|
||||
const identical = runPackageAcceptanceRegistryInputValidation({
|
||||
candidateArtifactJson: JSON.stringify(tuple),
|
||||
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
||||
});
|
||||
expect(identical.result.status, identical.result.stderr).toBe(0);
|
||||
expect(identical.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
||||
});
|
||||
|
||||
it("rejects partial or ambiguous prerelease registry tuples before child workflows", () => {
|
||||
const tuple = packageAcceptanceRegistryTuple();
|
||||
const partial = runPackageAcceptanceRegistryInputValidation({
|
||||
prepublishPluginRegistryJson: JSON.stringify({
|
||||
prepublishPluginRegistryArtifactId: tuple.prepublishPluginRegistryArtifactId,
|
||||
}),
|
||||
});
|
||||
expect(partial.result.status).toBe(1);
|
||||
expect(partial.result.stderr).toContain(
|
||||
"Prerelease plugin registry JSON must contain one complete immutable tuple.",
|
||||
);
|
||||
|
||||
const ambiguous = runPackageAcceptanceRegistryInputValidation({
|
||||
candidateArtifactJson: JSON.stringify(tuple),
|
||||
prepublishPluginRegistryJson: JSON.stringify(
|
||||
packageAcceptanceRegistryTuple({
|
||||
prepublishPluginRegistryArtifactId: "789",
|
||||
}),
|
||||
),
|
||||
});
|
||||
expect(ambiguous.result.status).toBe(1);
|
||||
expect(ambiguous.result.stderr).toContain("Prerelease plugin registry inputs disagree.");
|
||||
});
|
||||
|
||||
it("generates a Telegram-only registry only for direct ref or artifact candidates", () => {
|
||||
for (const source of ["ref", "artifact"] as const) {
|
||||
const generated = runPackageAcceptanceResolveScript({
|
||||
source,
|
||||
telegramMode: "mock-openai",
|
||||
});
|
||||
expect(generated.result.status, generated.result.stderr).toBe(0);
|
||||
expect(generated.args).toContain("--plugin-registry-output-dir\n");
|
||||
expect(generated.args).toContain(".artifacts/package-acceptance-telegram-plugin-registry\n");
|
||||
expect(generated.args).toContain('--required-plugin-packages-json\n["@openclaw/codex"]\n');
|
||||
}
|
||||
|
||||
for (const source of ["npm", "url", "trusted-url"] as const) {
|
||||
const skipped = runPackageAcceptanceResolveScript({
|
||||
source,
|
||||
telegramMode: "mock-openai",
|
||||
});
|
||||
expect(skipped.result.status, skipped.result.stderr).toBe(0);
|
||||
expect(skipped.args).not.toContain("--plugin-registry-output-dir");
|
||||
}
|
||||
|
||||
const telegramDisabled = runPackageAcceptanceResolveScript({
|
||||
source: "ref",
|
||||
telegramMode: "none",
|
||||
});
|
||||
expect(telegramDisabled.result.status, telegramDisabled.result.stderr).toBe(0);
|
||||
expect(telegramDisabled.args).not.toContain("--plugin-registry-output-dir");
|
||||
});
|
||||
|
||||
it("reuses a supplied registry and keeps generated artifact names distinct from Docker", () => {
|
||||
const tuple = packageAcceptanceRegistryTuple();
|
||||
const reused = runPackageAcceptanceResolveScript({
|
||||
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
||||
source: "ref",
|
||||
telegramMode: "mock-openai",
|
||||
});
|
||||
expect(reused.result.status, reused.result.stderr).toBe(0);
|
||||
expect(reused.args).not.toContain("--plugin-registry-output-dir");
|
||||
|
||||
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
||||
expect(workflow).toContain(
|
||||
"package-acceptance-telegram-plugin-registry-${{ github.run_id }}-${{ github.run_attempt }}",
|
||||
);
|
||||
expect(workflow).toContain('"docker-e2e-prepublish-plugin-registry-" +');
|
||||
});
|
||||
|
||||
it("selects one normalized Telegram scenario without enabling broad acceptance lanes", () => {
|
||||
const { outputs, result } = runPackageAcceptanceProfile({
|
||||
suiteProfile: "telegram",
|
||||
@@ -4544,6 +4761,8 @@ describe("package artifact reuse", () => {
|
||||
"${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.package_mode != 'published') && 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 }}",
|
||||
prepublish_plugin_registry_json:
|
||||
"${{ needs.prepare_release_package.outputs.prepublish_plugin_registry_json }}",
|
||||
suite_profile: "custom",
|
||||
});
|
||||
expect(releaseChecksTargetSummary.env).toMatchObject({
|
||||
@@ -4556,7 +4775,7 @@ describe("package artifact reuse", () => {
|
||||
enable_prepublish_plugin_registry:
|
||||
'${{ contains(fromJSON(\'["artifact","ref"]\'), inputs.source) }}',
|
||||
prepublish_plugin_registry_manifest_sha256:
|
||||
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
||||
"${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
||||
});
|
||||
expect(workflow).toContain(
|
||||
"candidate_artifact_json cannot be combined with release package specs.",
|
||||
@@ -5872,6 +6091,14 @@ describe("package artifact reuse", () => {
|
||||
|
||||
expect(runNpmTelegramInputValidation(packageTuple).status).toBe(0);
|
||||
expect(runNpmTelegramInputValidation({ ...packageTuple, ...registryTuple }).status).toBe(0);
|
||||
expect(
|
||||
runNpmTelegramInputValidation({
|
||||
...packageTuple,
|
||||
...registryTuple,
|
||||
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME:
|
||||
"package-acceptance-telegram-plugin-registry-123-1",
|
||||
}).status,
|
||||
).toBe(0);
|
||||
|
||||
const partial = runNpmTelegramInputValidation({
|
||||
...packageTuple,
|
||||
|
||||
@@ -588,13 +588,14 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
||||
"${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }}",
|
||||
OPENCLAW_CI_EVENT_NAME: "${{ github.event_name }}",
|
||||
OPENCLAW_CI_HISTORICAL_TARGET: "${{ steps.historical_target.outputs.eligible || 'false' }}",
|
||||
OPENCLAW_CI_RELEASE_GATE: "${{ inputs.release_gate && 'true' || 'false' }}",
|
||||
OPENCLAW_CI_RELEASE_CANDIDATE_TARGET:
|
||||
"${{ steps.release_candidate_target.outputs.eligible || 'false' }}",
|
||||
OPENCLAW_CI_TARGET_CONTEXT_TARGET:
|
||||
"${{ steps.target_context_target.outputs.eligible || 'false' }}",
|
||||
OPENCLAW_CI_REPOSITORY: "${{ github.repository }}",
|
||||
OPENCLAW_CI_RUNNER_BACKEND:
|
||||
"${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}",
|
||||
"${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 'github' || vars.OPENCLAW_CI_RUNNER_BACKEND }}",
|
||||
OPENCLAW_CI_RUN_ANDROID:
|
||||
"${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
|
||||
OPENCLAW_CI_RUN_CONTROL_UI_I18N:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Resolve Openclaw Package Candidate tests cover resolve openclaw package candidate script behavior.
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
downloadUrl,
|
||||
findSingleTarballForTest,
|
||||
loadTrustedPackageSource,
|
||||
main,
|
||||
moveNewestPackedTarballForTest,
|
||||
parseArgs,
|
||||
readArtifactPackageCandidateMetadata,
|
||||
@@ -22,8 +23,10 @@ import {
|
||||
runCommandForTest,
|
||||
validateOpenClawPackageSpec,
|
||||
} from "../../scripts/resolve-openclaw-package-candidate.mts";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const autoTempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
type LookupAddress = { address: string; family: number };
|
||||
|
||||
@@ -42,6 +45,99 @@ async function missing(file: string): Promise<boolean> {
|
||||
);
|
||||
}
|
||||
|
||||
async function createPackageTarball(
|
||||
dir: string,
|
||||
buildInfo?: string | { commit: string },
|
||||
): Promise<string> {
|
||||
const root = path.join(dir, "package");
|
||||
await mkdir(path.join(root, "dist"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.8.1" }),
|
||||
);
|
||||
if (buildInfo !== undefined) {
|
||||
await writeFile(
|
||||
path.join(root, "dist", "build-info.json"),
|
||||
typeof buildInfo === "string" ? buildInfo : JSON.stringify(buildInfo),
|
||||
);
|
||||
}
|
||||
const tarball = path.join(dir, "openclaw.tgz");
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile("tar", ["-czf", tarball, "-C", dir, "package"], (error) => {
|
||||
if (error) {
|
||||
reject(toLintErrorObject(error, "Non-Error rejection"));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return tarball;
|
||||
}
|
||||
|
||||
async function createArtifactFixture(
|
||||
prefix: string,
|
||||
{
|
||||
buildInfo,
|
||||
packageSourceSha,
|
||||
}: { buildInfo?: string | { commit: string }; packageSourceSha?: string },
|
||||
) {
|
||||
const dir = autoTempDirs.make(prefix);
|
||||
const artifactDir = path.join(dir, "artifact");
|
||||
const binDir = path.join(dir, "bin");
|
||||
const gitLog = path.join(dir, "git.log");
|
||||
const nodeLog = path.join(dir, "node.log");
|
||||
await mkdir(artifactDir);
|
||||
await mkdir(binDir);
|
||||
await createPackageTarball(artifactDir, buildInfo);
|
||||
if (packageSourceSha !== undefined) {
|
||||
await writeFile(
|
||||
path.join(artifactDir, "package-candidate.json"),
|
||||
JSON.stringify({ packageSourceSha }),
|
||||
);
|
||||
}
|
||||
await writeFile(
|
||||
path.join(binDir, "git"),
|
||||
`#!/bin/sh
|
||||
printf '%s\\n' "$*" >> "$FAKE_GIT_LOG"
|
||||
exit 99
|
||||
`,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(binDir, "node"),
|
||||
`#!/bin/sh
|
||||
printf '%s\\n' "$*" >> "$FAKE_NODE_LOG"
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
await chmod(path.join(binDir, "git"), 0o755);
|
||||
await chmod(path.join(binDir, "node"), 0o755);
|
||||
return {
|
||||
artifactDir,
|
||||
binDir,
|
||||
dir,
|
||||
gitLog,
|
||||
nodeLog,
|
||||
registryDir: path.join(dir, "registry"),
|
||||
};
|
||||
}
|
||||
|
||||
async function withArtifactFixtureCommands<T>(
|
||||
fixture: Awaited<ReturnType<typeof createArtifactFixture>>,
|
||||
run: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.FAKE_GIT_LOG = fixture.gitLog;
|
||||
process.env.FAKE_NODE_LOG = fixture.nodeLog;
|
||||
process.env.PATH = `${fixture.binDir}:${previousPath}`;
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
delete process.env.FAKE_GIT_LOG;
|
||||
delete process.env.FAKE_NODE_LOG;
|
||||
}
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
@@ -1267,6 +1363,191 @@ describe("resolve-openclaw-package-candidate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["without a registry", false],
|
||||
["with a registry", true],
|
||||
])("rejects artifact provenance mismatches %s before side effects", async (_label, registry) => {
|
||||
const metadataSha = "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2";
|
||||
const buildInfoSha = "77df743c0c8d6d80ee4f77d84a798e62749be7f3";
|
||||
const fixture = await createArtifactFixture("openclaw-artifact-provenance-mismatch-", {
|
||||
buildInfo: { commit: buildInfoSha.toUpperCase() },
|
||||
packageSourceSha: metadataSha.toUpperCase(),
|
||||
});
|
||||
|
||||
await withArtifactFixtureCommands(fixture, async () => {
|
||||
await expect(
|
||||
main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
fixture.artifactDir,
|
||||
"--output-dir",
|
||||
path.join(fixture.dir, "output"),
|
||||
...(registry
|
||||
? [
|
||||
"--plugin-registry-output-dir",
|
||||
fixture.registryDir,
|
||||
"--required-plugin-packages-json",
|
||||
'["@openclaw/codex"]',
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
`artifact packageSourceSha ${metadataSha} does not match package build-info commit ${buildInfoSha}`,
|
||||
);
|
||||
});
|
||||
await expect(missing(fixture.gitLog)).resolves.toBe(true);
|
||||
await expect(missing(fixture.registryDir)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("uses normalized artifact build-info provenance when metadata is absent", async () => {
|
||||
const sourceSha = "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2";
|
||||
const fixture = await createArtifactFixture("openclaw-artifact-build-info-fallback-", {
|
||||
buildInfo: { commit: sourceSha.toUpperCase() },
|
||||
});
|
||||
const metadataPath = path.join(fixture.dir, "resolved.json");
|
||||
|
||||
await withArtifactFixtureCommands(fixture, async () => {
|
||||
await main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
fixture.artifactDir,
|
||||
"--output-dir",
|
||||
path.join(fixture.dir, "output"),
|
||||
"--metadata",
|
||||
metadataPath,
|
||||
]);
|
||||
});
|
||||
|
||||
const metadata = JSON.parse(await readFile(metadataPath, "utf8")) as Record<string, unknown>;
|
||||
expect(metadata.packageSourceSha).toBe(sourceSha);
|
||||
expect(metadata.packageTrustedReason).toBe("package-build-info");
|
||||
});
|
||||
|
||||
it("requires artifact build-info only when preparing a registry", async () => {
|
||||
const sourceSha = "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2";
|
||||
const fixture = await createArtifactFixture("openclaw-artifact-missing-build-info-", {
|
||||
packageSourceSha: sourceSha,
|
||||
});
|
||||
const metadataPath = path.join(fixture.dir, "resolved.json");
|
||||
|
||||
await withArtifactFixtureCommands(fixture, async () => {
|
||||
await main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
fixture.artifactDir,
|
||||
"--output-dir",
|
||||
path.join(fixture.dir, "output-without-registry"),
|
||||
"--metadata",
|
||||
metadataPath,
|
||||
]);
|
||||
await expect(
|
||||
main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
fixture.artifactDir,
|
||||
"--output-dir",
|
||||
path.join(fixture.dir, "output-with-registry"),
|
||||
"--plugin-registry-output-dir",
|
||||
fixture.registryDir,
|
||||
"--required-plugin-packages-json",
|
||||
'["@openclaw/codex"]',
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
"source=artifact requires a valid package build-info commit for prerelease plugin registry creation",
|
||||
);
|
||||
});
|
||||
|
||||
const metadata = JSON.parse(await readFile(metadataPath, "utf8")) as Record<string, unknown>;
|
||||
expect(metadata.packageSourceSha).toBe(sourceSha);
|
||||
await expect(missing(fixture.gitLog)).resolves.toBe(true);
|
||||
await expect(missing(fixture.registryDir)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("rejects malformed artifact build-info before package validation", async () => {
|
||||
const fixture = await createArtifactFixture("openclaw-artifact-malformed-build-info-", {
|
||||
buildInfo: "{not-json",
|
||||
packageSourceSha: "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2",
|
||||
});
|
||||
|
||||
await withArtifactFixtureCommands(fixture, async () => {
|
||||
await expect(
|
||||
main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
fixture.artifactDir,
|
||||
"--output-dir",
|
||||
path.join(fixture.dir, "output"),
|
||||
]),
|
||||
).rejects.toBeInstanceOf(SyntaxError);
|
||||
});
|
||||
await expect(missing(fixture.nodeLog)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("validates the normalized artifact source SHA before registry preparation", async () => {
|
||||
const dir = autoTempDirs.make("openclaw-artifact-registry-source-");
|
||||
const artifactDir = path.join(dir, "artifact");
|
||||
const binDir = path.join(dir, "bin");
|
||||
const gitLog = path.join(dir, "git.log");
|
||||
const sourceSha = "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2";
|
||||
await mkdir(artifactDir);
|
||||
await mkdir(binDir);
|
||||
await createPackageTarball(artifactDir, { commit: sourceSha });
|
||||
await writeFile(
|
||||
path.join(artifactDir, "package-candidate.json"),
|
||||
JSON.stringify({ packageSourceSha: sourceSha.toUpperCase() }),
|
||||
);
|
||||
const fakeGit = path.join(binDir, "git");
|
||||
await writeFile(
|
||||
fakeGit,
|
||||
`#!/bin/sh
|
||||
printf '%s\\n' "$*" >> "$FAKE_GIT_LOG"
|
||||
case "$1" in
|
||||
fetch) exit 0 ;;
|
||||
rev-parse) printf '%s\\n' "$FAKE_SOURCE_SHA" ;;
|
||||
merge-base) exit 1 ;;
|
||||
tag | for-each-ref) exit 0 ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
`,
|
||||
);
|
||||
await chmod(fakeGit, 0o755);
|
||||
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.FAKE_GIT_LOG = gitLog;
|
||||
process.env.FAKE_SOURCE_SHA = sourceSha;
|
||||
process.env.PATH = `${binDir}:${previousPath}`;
|
||||
try {
|
||||
await expect(
|
||||
main([
|
||||
"--source",
|
||||
"artifact",
|
||||
"--artifact-dir",
|
||||
artifactDir,
|
||||
"--output-dir",
|
||||
path.join(dir, "output"),
|
||||
"--plugin-registry-output-dir",
|
||||
path.join(dir, "registry"),
|
||||
"--required-plugin-packages-json",
|
||||
'["@openclaw/codex"]',
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
`package_ref ${sourceSha} resolved to ${sourceSha}, which is not reachable from an OpenClaw branch or release tag`,
|
||||
);
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
delete process.env.FAKE_GIT_LOG;
|
||||
delete process.env.FAKE_SOURCE_SHA;
|
||||
}
|
||||
await expect(readFile(gitLog, "utf8")).resolves.toContain(
|
||||
`rev-parse --verify ${sourceSha}^{commit}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes whitespace-only artifact package source SHAs to absent", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-candidate-empty-sha-"));
|
||||
tempDirs.push(dir);
|
||||
@@ -1341,22 +1622,8 @@ describe("resolve-openclaw-package-candidate", () => {
|
||||
it("reads the source SHA from packed npm build metadata", async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-build-info-"));
|
||||
tempDirs.push(dir);
|
||||
const root = path.join(dir, "package");
|
||||
await mkdir(path.join(root, "dist"), { recursive: true });
|
||||
await writeFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }));
|
||||
await writeFile(
|
||||
path.join(root, "dist", "build-info.json"),
|
||||
JSON.stringify({ commit: "66CE632B9B7C5C7FDD3E66C739687D51638AD6E2" }),
|
||||
);
|
||||
const tarball = path.join(dir, "openclaw.tgz");
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile("tar", ["-czf", tarball, "-C", dir, "package"], (error) => {
|
||||
if (error) {
|
||||
reject(toLintErrorObject(error, "Non-Error rejection"));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
const tarball = await createPackageTarball(dir, {
|
||||
commit: "66CE632B9B7C5C7FDD3E66C739687D51638AD6E2",
|
||||
});
|
||||
|
||||
await expect(readPackageBuildSourceSha(tarball)).resolves.toBe(
|
||||
|
||||
Reference in New Issue
Block a user