mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): ship selectable local console trial kit
This commit is contained in:
+139
-15
@@ -23,6 +23,14 @@ on:
|
|||||||
- local
|
- local
|
||||||
- cluster
|
- cluster
|
||||||
- all
|
- all
|
||||||
|
local_alpha_variant:
|
||||||
|
description: Select the Local Alpha deployment surface without changing the default headless image
|
||||||
|
required: true
|
||||||
|
default: headless
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- headless
|
||||||
|
- console
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -384,9 +392,20 @@ jobs:
|
|||||||
run: >-
|
run: >-
|
||||||
docker build
|
docker build
|
||||||
--file deploy/containers/ql3-local-application/Dockerfile
|
--file deploy/containers/ql3-local-application/Dockerfile
|
||||||
|
--target runtime
|
||||||
--build-arg SOURCE_REVISION=${{ github.sha }}
|
--build-arg SOURCE_REVISION=${{ github.sha }}
|
||||||
--tag "${IMAGE}"
|
--tag "${IMAGE}"
|
||||||
.
|
.
|
||||||
|
- name: Build the opt-in Local Console image
|
||||||
|
env:
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
run: >-
|
||||||
|
docker build
|
||||||
|
--file deploy/containers/ql3-local-application/Dockerfile
|
||||||
|
--target runtime-console
|
||||||
|
--build-arg SOURCE_REVISION=${{ github.sha }}
|
||||||
|
--tag "${CONSOLE_IMAGE}"
|
||||||
|
.
|
||||||
- name: Build the native short-lived Local operator image
|
- name: Build the native short-lived Local operator image
|
||||||
env:
|
env:
|
||||||
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
||||||
@@ -404,6 +423,9 @@ jobs:
|
|||||||
node scripts/ql3-image-os-vulnerability-policy.cjs \
|
node scripts/ql3-image-os-vulnerability-policy.cjs \
|
||||||
--image=local-operator \
|
--image=local-operator \
|
||||||
--output=${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml
|
--output=${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml
|
||||||
|
node scripts/ql3-image-os-vulnerability-policy.cjs \
|
||||||
|
--image=local-console \
|
||||||
|
--output=${{ runner.temp }}/ql3-local-console-${{ matrix.image_arch }}.trivyignore.yaml
|
||||||
- name: Reject unexcepted high or critical local OS vulnerabilities
|
- name: Reject unexcepted high or critical local OS vulnerabilities
|
||||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||||
with:
|
with:
|
||||||
@@ -434,6 +456,21 @@ jobs:
|
|||||||
timeout: '10m0s'
|
timeout: '10m0s'
|
||||||
cache: 'false'
|
cache: 'false'
|
||||||
trivyignores: ${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml
|
trivyignores: ${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml
|
||||||
|
- name: Reject unexcepted high or critical Local Console OS vulnerabilities
|
||||||
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||||
|
with:
|
||||||
|
version: 'v0.70.0'
|
||||||
|
image-ref: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
scanners: 'vuln'
|
||||||
|
vuln-type: 'os'
|
||||||
|
severity: 'HIGH,CRITICAL'
|
||||||
|
ignore-unfixed: 'false'
|
||||||
|
exit-code: '1'
|
||||||
|
format: 'table'
|
||||||
|
hide-progress: 'true'
|
||||||
|
timeout: '10m0s'
|
||||||
|
cache: 'false'
|
||||||
|
trivyignores: ${{ runner.temp }}/ql3-local-console-${{ matrix.image_arch }}.trivyignore.yaml
|
||||||
- name: Verify non-root identity and architecture
|
- name: Verify non-root identity and architecture
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
@@ -456,6 +493,17 @@ jobs:
|
|||||||
echo "unexpected Local operator image contract: ${actual}" >&2
|
echo "unexpected Local operator image contract: ${actual}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
- name: Verify the opt-in Local Console identity
|
||||||
|
env:
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
EXPECTED: ${{ matrix.image_arch }} 65532:65532 edge-application-api,standalone-application-api offline-loopback
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
actual="$(docker image inspect --format '{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.profile"}} {{index .Config.Labels "io.qinglong.local.console"}}' "${CONSOLE_IMAGE}")"
|
||||||
|
if [[ "${actual}" != "${EXPECTED}" ]]; then
|
||||||
|
echo "unexpected Local Console image contract: ${actual}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- name: Reconcile the exact production package inventory
|
- name: Reconcile the exact production package inventory
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
@@ -484,6 +532,20 @@ jobs:
|
|||||||
"${OPERATOR_IMAGE}"
|
"${OPERATOR_IMAGE}"
|
||||||
scripts/ql3-local-operator-image-inventory.cjs
|
scripts/ql3-local-operator-image-inventory.cjs
|
||||||
--inventory-root=/opt/qinglong/node_modules
|
--inventory-root=/opt/qinglong/node_modules
|
||||||
|
- name: Reconcile the opt-in Local Console package inventory
|
||||||
|
env:
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
run: >-
|
||||||
|
docker run --rm --read-only
|
||||||
|
--network none
|
||||||
|
--cap-drop ALL
|
||||||
|
--security-opt no-new-privileges
|
||||||
|
--volume "${{ github.workspace }}:/audit:ro"
|
||||||
|
--workdir /audit
|
||||||
|
--entrypoint node
|
||||||
|
"${CONSOLE_IMAGE}"
|
||||||
|
scripts/ql3-local-console-image-inventory.cjs
|
||||||
|
--inventory-root=/opt/qinglong/node_modules
|
||||||
- name: Generate and reconcile the reviewed CycloneDX SBOM
|
- name: Generate and reconcile the reviewed CycloneDX SBOM
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
@@ -520,6 +582,24 @@ jobs:
|
|||||||
scripts/ql3-cluster-image-sbom.cjs \
|
scripts/ql3-cluster-image-sbom.cjs \
|
||||||
--image=local-operator \
|
--image=local-operator \
|
||||||
--inventory-root=/opt/qinglong/node_modules
|
--inventory-root=/opt/qinglong/node_modules
|
||||||
|
- name: Generate and reconcile the Local Console CycloneDX SBOM
|
||||||
|
env:
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
run: |
|
||||||
|
node scripts/ql3-cluster-image-sbom.cjs \
|
||||||
|
--image=local-console \
|
||||||
|
--output="${{ runner.temp }}/ql3-local-console.cdx.json"
|
||||||
|
docker run --rm --read-only \
|
||||||
|
--network none \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--security-opt no-new-privileges \
|
||||||
|
--volume "${{ github.workspace }}:/audit:ro" \
|
||||||
|
--workdir /audit \
|
||||||
|
--entrypoint node \
|
||||||
|
"${CONSOLE_IMAGE}" \
|
||||||
|
scripts/ql3-cluster-image-sbom.cjs \
|
||||||
|
--image=local-console \
|
||||||
|
--inventory-root=/opt/qinglong/node_modules
|
||||||
- name: Exercise the entrypoint under the router stress envelope
|
- name: Exercise the entrypoint under the router stress envelope
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
@@ -535,6 +615,21 @@ jobs:
|
|||||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m
|
||||||
"${IMAGE}"
|
"${IMAGE}"
|
||||||
--help
|
--help
|
||||||
|
- name: Exercise the opt-in Console entrypoint under the router stress envelope
|
||||||
|
env:
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
run: >-
|
||||||
|
docker run --rm --read-only
|
||||||
|
--network none
|
||||||
|
--cap-drop ALL
|
||||||
|
--security-opt no-new-privileges
|
||||||
|
--memory=128m
|
||||||
|
--memory-swap=128m
|
||||||
|
--cpus=0.5
|
||||||
|
--pids-limit=64
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m
|
||||||
|
"${CONSOLE_IMAGE}"
|
||||||
|
--help
|
||||||
- name: Exercise the short-lived Local operator entrypoint
|
- name: Exercise the short-lived Local operator entrypoint
|
||||||
env:
|
env:
|
||||||
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
||||||
@@ -576,11 +671,23 @@ jobs:
|
|||||||
node scripts/ql3-local-alpha-trial-kit-live-contract.cjs \
|
node scripts/ql3-local-alpha-trial-kit-live-contract.cjs \
|
||||||
--application-image="${IMAGE}" \
|
--application-image="${IMAGE}" \
|
||||||
--operator-image="${OPERATOR_IMAGE}" \
|
--operator-image="${OPERATOR_IMAGE}" \
|
||||||
--profile=edge
|
--profile=edge \
|
||||||
|
--variant=headless
|
||||||
node scripts/ql3-local-alpha-trial-kit-live-contract.cjs \
|
node scripts/ql3-local-alpha-trial-kit-live-contract.cjs \
|
||||||
--application-image="${IMAGE}" \
|
--application-image="${IMAGE}" \
|
||||||
--operator-image="${OPERATOR_IMAGE}" \
|
--operator-image="${OPERATOR_IMAGE}" \
|
||||||
--profile=standalone
|
--profile=standalone \
|
||||||
|
--variant=headless
|
||||||
|
- name: Run the Local Console loopback journey
|
||||||
|
env:
|
||||||
|
IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
|
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
||||||
|
run: |
|
||||||
|
node scripts/ql3-local-alpha-trial-kit-live-contract.cjs \
|
||||||
|
--application-image="${IMAGE}" \
|
||||||
|
--operator-image="${OPERATOR_IMAGE}" \
|
||||||
|
--profile=edge \
|
||||||
|
--variant=console
|
||||||
- name: Run authenticated Local API cancellation through real Linux processes
|
- name: Run authenticated Local API cancellation through real Linux processes
|
||||||
env:
|
env:
|
||||||
QL3_LOCAL_API_CANCELLATION_LIVE: '1'
|
QL3_LOCAL_API_CANCELLATION_LIVE: '1'
|
||||||
@@ -599,16 +706,27 @@ jobs:
|
|||||||
- name: Materialize and offline-audit the native Local Alpha trial kit
|
- name: Materialize and offline-audit the native Local Alpha trial kit
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
HEADLESS_IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
|
CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }}
|
||||||
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }}
|
||||||
|
TRIAL_VARIANT: ${{ inputs.local_alpha_variant }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
|
if [[ "${TRIAL_VARIANT}" == 'console' ]]; then
|
||||||
|
IMAGE="${CONSOLE_IMAGE}"
|
||||||
|
APPLICATION_SBOM="${RUNNER_TEMP}/ql3-local-console.cdx.json"
|
||||||
|
else
|
||||||
|
IMAGE="${HEADLESS_IMAGE}"
|
||||||
|
APPLICATION_SBOM="${RUNNER_TEMP}/ql3-local-application.cdx.json"
|
||||||
|
fi
|
||||||
|
BUNDLE_ROOT="${RUNNER_TEMP}/ql3-alpha-local-${TRIAL_VARIANT}-${{ matrix.image_arch }}"
|
||||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||||
--mode=record-verification \
|
--mode=record-verification \
|
||||||
--application-image="${IMAGE}" \
|
--application-image="${IMAGE}" \
|
||||||
--operator-image="${OPERATOR_IMAGE}" \
|
--operator-image="${OPERATOR_IMAGE}" \
|
||||||
--architecture=${{ matrix.image_arch }} \
|
--architecture=${{ matrix.image_arch }} \
|
||||||
|
--variant="${TRIAL_VARIANT}" \
|
||||||
--source-revision=${{ github.sha }} \
|
--source-revision=${{ github.sha }} \
|
||||||
--repository=${{ github.repository }} \
|
--repository=${{ github.repository }} \
|
||||||
--workflow-ref="${{ github.workflow_ref }}" \
|
--workflow-ref="${{ github.workflow_ref }}" \
|
||||||
@@ -623,19 +741,24 @@ jobs:
|
|||||||
--application-image="${IMAGE}" \
|
--application-image="${IMAGE}" \
|
||||||
--operator-image="${OPERATOR_IMAGE}" \
|
--operator-image="${OPERATOR_IMAGE}" \
|
||||||
--architecture=${{ matrix.image_arch }} \
|
--architecture=${{ matrix.image_arch }} \
|
||||||
|
--variant="${TRIAL_VARIANT}" \
|
||||||
--source-revision=${{ github.sha }} \
|
--source-revision=${{ github.sha }} \
|
||||||
--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json" \
|
--application-sbom="${APPLICATION_SBOM}" \
|
||||||
--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json" \
|
--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json" \
|
||||||
--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json" \
|
--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json" \
|
||||||
--readme=docs/operations/ql3-local-alpha-trial-kit.md \
|
--readme=docs/operations/ql3-local-alpha-trial-kit.md \
|
||||||
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
--output="${BUNDLE_ROOT}"
|
||||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||||
--mode=audit --bundle="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
--mode=audit --bundle="${BUNDLE_ROOT}"
|
||||||
QUICKSTART_CONTAINER="ql3-alpha-quickstart-${{ matrix.image_arch }}"
|
QUICKSTART_CONTAINER="ql3-alpha-${TRIAL_VARIANT}-quickstart-${{ matrix.image_arch }}"
|
||||||
QUICKSTART_ROOT="${RUNNER_TEMP}/ql3-alpha-quickstart-${{ matrix.image_arch }}"
|
QUICKSTART_ROOT="${RUNNER_TEMP}/ql3-alpha-${TRIAL_VARIANT}-quickstart-${{ matrix.image_arch }}"
|
||||||
trap 'docker rm --force "${QUICKSTART_CONTAINER}" >/dev/null 2>&1 || true' EXIT
|
trap 'docker rm --force "${QUICKSTART_CONTAINER}" >/dev/null 2>&1 || true' EXIT
|
||||||
sh "${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}/quickstart.sh" \
|
sh "${BUNDLE_ROOT}/quickstart.sh" \
|
||||||
edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"
|
edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"
|
||||||
|
if [[ "${TRIAL_VARIANT}" == 'console' ]]; then
|
||||||
|
test "$(curl --silent --output /dev/null --write-out '%{http_code}' http://127.0.0.1:5700/)" = '200'
|
||||||
|
test "$(curl --silent --output /dev/null --write-out '%{http_code}' http://127.0.0.1:5700/api/v3/projects/default/tasks)" = '401'
|
||||||
|
fi
|
||||||
docker stop --time 30 "${QUICKSTART_CONTAINER}"
|
docker stop --time 30 "${QUICKSTART_CONTAINER}"
|
||||||
docker rm "${QUICKSTART_CONTAINER}"
|
docker rm "${QUICKSTART_CONTAINER}"
|
||||||
test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"
|
test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"
|
||||||
@@ -645,8 +768,8 @@ jobs:
|
|||||||
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ql3-alpha-${{ github.sha }}-local-${{ matrix.image_arch }}
|
name: ql3-alpha-${{ github.sha }}-local-${{ inputs.local_alpha_variant }}-${{ matrix.image_arch }}
|
||||||
path: ${{ runner.temp }}/ql3-alpha-local-${{ matrix.image_arch }}
|
path: ${{ runner.temp }}/ql3-alpha-local-${{ inputs.local_alpha_variant }}-${{ matrix.image_arch }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
@@ -1777,12 +1900,12 @@ jobs:
|
|||||||
- name: Download the exact amd64 Local Alpha trial kit
|
- name: Download the exact amd64 Local Alpha trial kit
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: ql3-alpha-${{ github.sha }}-local-amd64
|
name: ql3-alpha-${{ github.sha }}-local-${{ inputs.local_alpha_variant }}-amd64
|
||||||
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/amd64
|
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/amd64
|
||||||
- name: Download the exact arm64 Local Alpha trial kit
|
- name: Download the exact arm64 Local Alpha trial kit
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: ql3-alpha-${{ github.sha }}-local-arm64
|
name: ql3-alpha-${{ github.sha }}-local-${{ inputs.local_alpha_variant }}-arm64
|
||||||
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/arm64
|
path: ${{ runner.temp }}/ql3-alpha-local-milestone-input/arm64
|
||||||
- name: Close both native trial kits into one milestone index
|
- name: Close both native trial kits into one milestone index
|
||||||
run: |
|
run: |
|
||||||
@@ -1792,6 +1915,7 @@ jobs:
|
|||||||
--mode=finalize \
|
--mode=finalize \
|
||||||
--amd64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/amd64" \
|
--amd64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/amd64" \
|
||||||
--arm64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/arm64" \
|
--arm64-bundle="${RUNNER_TEMP}/ql3-alpha-local-milestone-input/arm64" \
|
||||||
|
--variant=${{ inputs.local_alpha_variant }} \
|
||||||
--source-revision=${{ github.sha }} \
|
--source-revision=${{ github.sha }} \
|
||||||
--repository=${{ github.repository }} \
|
--repository=${{ github.repository }} \
|
||||||
--workflow-ref="${{ github.workflow_ref }}" \
|
--workflow-ref="${{ github.workflow_ref }}" \
|
||||||
@@ -1807,7 +1931,7 @@ jobs:
|
|||||||
- name: Upload the closed Local Alpha milestone index
|
- name: Upload the closed Local Alpha milestone index
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ql3-alpha-${{ github.sha }}-local-milestone
|
name: ql3-alpha-${{ github.sha }}-local-${{ inputs.local_alpha_variant }}-milestone
|
||||||
path: ${{ runner.temp }}/ql3-alpha-local-milestone
|
path: ${{ runner.temp }}/ql3-alpha-local-milestone
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
@@ -1830,7 +1954,7 @@ jobs:
|
|||||||
- name: Download the closed Local Alpha milestone
|
- name: Download the closed Local Alpha milestone
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: ql3-alpha-${{ github.sha }}-local-milestone
|
name: ql3-alpha-${{ github.sha }}-local-${{ inputs.local_alpha_variant }}-milestone
|
||||||
path: ${{ runner.temp }}/ql3-alpha-stage-input/local
|
path: ${{ runner.temp }}/ql3-alpha-stage-input/local
|
||||||
- name: Download the closed Cluster Alpha milestone
|
- name: Download the closed Cluster Alpha milestone
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ WORKDIR /workspace
|
|||||||
|
|
||||||
COPY packages/ql3-ai packages/ql3-ai
|
COPY packages/ql3-ai packages/ql3-ai
|
||||||
COPY packages/ql3-local-admin packages/ql3-local-admin
|
COPY packages/ql3-local-admin packages/ql3-local-admin
|
||||||
|
COPY packages/ql3-local-api packages/ql3-local-api
|
||||||
COPY packages/ql3-local-application packages/ql3-local-application
|
COPY packages/ql3-local-application packages/ql3-local-application
|
||||||
COPY packages/ql3-local-command-file packages/ql3-local-command-file
|
COPY packages/ql3-local-command-file packages/ql3-local-command-file
|
||||||
COPY packages/ql3-local-execution packages/ql3-local-execution
|
COPY packages/ql3-local-execution packages/ql3-local-execution
|
||||||
|
COPY packages/ql3-local-owner-console packages/ql3-local-owner-console
|
||||||
COPY packages/ql3-local-process packages/ql3-local-process
|
COPY packages/ql3-local-process packages/ql3-local-process
|
||||||
COPY packages/ql3-local-secret packages/ql3-local-secret
|
COPY packages/ql3-local-secret packages/ql3-local-secret
|
||||||
COPY packages/ql3-local-sqlite packages/ql3-local-sqlite
|
COPY packages/ql3-local-sqlite packages/ql3-local-sqlite
|
||||||
@@ -37,12 +39,16 @@ RUN ln -s /opt/qinglong/node_modules node_modules \
|
|||||||
/opt/qinglong/node_modules/@qinglong/ai \
|
/opt/qinglong/node_modules/@qinglong/ai \
|
||||||
&& ln -s /workspace/packages/ql3-local-admin \
|
&& ln -s /workspace/packages/ql3-local-admin \
|
||||||
/opt/qinglong/node_modules/@qinglong/local-admin \
|
/opt/qinglong/node_modules/@qinglong/local-admin \
|
||||||
|
&& ln -s /workspace/packages/ql3-local-api \
|
||||||
|
/opt/qinglong/node_modules/@qinglong/local-api \
|
||||||
&& ln -s /workspace/packages/ql3-local-application \
|
&& ln -s /workspace/packages/ql3-local-application \
|
||||||
/opt/qinglong/node_modules/@qinglong/local-application \
|
/opt/qinglong/node_modules/@qinglong/local-application \
|
||||||
&& ln -s /workspace/packages/ql3-local-command-file \
|
&& ln -s /workspace/packages/ql3-local-command-file \
|
||||||
/opt/qinglong/node_modules/@qinglong/local-command-file \
|
/opt/qinglong/node_modules/@qinglong/local-command-file \
|
||||||
&& ln -s /workspace/packages/ql3-local-execution \
|
&& ln -s /workspace/packages/ql3-local-execution \
|
||||||
/opt/qinglong/node_modules/@qinglong/local-execution \
|
/opt/qinglong/node_modules/@qinglong/local-execution \
|
||||||
|
&& ln -s /workspace/packages/ql3-local-owner-console \
|
||||||
|
/opt/qinglong/node_modules/@qinglong/local-owner-console \
|
||||||
&& ln -s /workspace/packages/ql3-local-process \
|
&& ln -s /workspace/packages/ql3-local-process \
|
||||||
/opt/qinglong/node_modules/@qinglong/local-process \
|
/opt/qinglong/node_modules/@qinglong/local-process \
|
||||||
&& ln -s /workspace/packages/ql3-local-secret \
|
&& ln -s /workspace/packages/ql3-local-secret \
|
||||||
@@ -59,6 +65,8 @@ RUN ln -s /opt/qinglong/node_modules node_modules \
|
|||||||
-p packages/ql3-local-process/tsconfig.json \
|
-p packages/ql3-local-process/tsconfig.json \
|
||||||
&& /opt/qinglong/node_modules/.bin/tsc \
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
-p packages/ql3-local-sqlite/tsconfig.json \
|
-p packages/ql3-local-sqlite/tsconfig.json \
|
||||||
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
|
-p packages/ql3-local-owner-console/tsconfig.json \
|
||||||
&& /opt/qinglong/node_modules/.bin/tsc \
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
-p packages/ql3-ai/tsconfig.json \
|
-p packages/ql3-ai/tsconfig.json \
|
||||||
&& /opt/qinglong/node_modules/.bin/tsc \
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
@@ -68,7 +76,9 @@ RUN ln -s /opt/qinglong/node_modules node_modules \
|
|||||||
&& /opt/qinglong/node_modules/.bin/tsc \
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
-p packages/ql3-local-execution/tsconfig.json \
|
-p packages/ql3-local-execution/tsconfig.json \
|
||||||
&& /opt/qinglong/node_modules/.bin/tsc \
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
-p packages/ql3-local-application/tsconfig.json
|
-p packages/ql3-local-application/tsconfig.json \
|
||||||
|
&& /opt/qinglong/node_modules/.bin/tsc \
|
||||||
|
-p packages/ql3-local-api/tsconfig.json
|
||||||
|
|
||||||
FROM runtime-dependency-manifest AS external-dependencies
|
FROM runtime-dependency-manifest AS external-dependencies
|
||||||
|
|
||||||
@@ -130,12 +140,85 @@ RUN rm -rf node_modules/.bin \
|
|||||||
--exclude=@qinglong/ai \
|
--exclude=@qinglong/ai \
|
||||||
&& rm /tmp/ql3-prune-runtime-artifact.cjs
|
&& rm /tmp/ql3-prune-runtime-artifact.cjs
|
||||||
|
|
||||||
FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime
|
FROM external-dependencies AS console-assembled
|
||||||
|
|
||||||
|
COPY scripts/ql3-prune-runtime-artifact.cjs /tmp/ql3-prune-runtime-artifact.cjs
|
||||||
|
|
||||||
|
RUN mkdir -p \
|
||||||
|
node_modules/@qinglong/runtime-core \
|
||||||
|
node_modules/@qinglong/local-admin \
|
||||||
|
node_modules/@qinglong/local-api \
|
||||||
|
node_modules/@qinglong/local-application \
|
||||||
|
node_modules/@qinglong/local-command-file \
|
||||||
|
node_modules/@qinglong/local-execution \
|
||||||
|
node_modules/@qinglong/local-owner-console \
|
||||||
|
node_modules/@qinglong/local-process \
|
||||||
|
node_modules/@qinglong/local-secret \
|
||||||
|
node_modules/@qinglong/local-sqlite
|
||||||
|
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-runtime-core/package.json \
|
||||||
|
node_modules/@qinglong/runtime-core/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-runtime-core/dist \
|
||||||
|
node_modules/@qinglong/runtime-core/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-admin/package.json \
|
||||||
|
node_modules/@qinglong/local-admin/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-admin/dist \
|
||||||
|
node_modules/@qinglong/local-admin/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-api/package.json \
|
||||||
|
node_modules/@qinglong/local-api/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-api/dist \
|
||||||
|
node_modules/@qinglong/local-api/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-api/assets \
|
||||||
|
node_modules/@qinglong/local-api/assets
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-application/package.json \
|
||||||
|
node_modules/@qinglong/local-application/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-application/dist \
|
||||||
|
node_modules/@qinglong/local-application/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-command-file/package.json \
|
||||||
|
node_modules/@qinglong/local-command-file/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-command-file/dist \
|
||||||
|
node_modules/@qinglong/local-command-file/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-execution/package.json \
|
||||||
|
node_modules/@qinglong/local-execution/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-execution/dist \
|
||||||
|
node_modules/@qinglong/local-execution/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-owner-console/package.json \
|
||||||
|
node_modules/@qinglong/local-owner-console/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-owner-console/dist \
|
||||||
|
node_modules/@qinglong/local-owner-console/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-process/package.json \
|
||||||
|
node_modules/@qinglong/local-process/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-process/dist \
|
||||||
|
node_modules/@qinglong/local-process/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-secret/package.json \
|
||||||
|
node_modules/@qinglong/local-secret/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-secret/dist \
|
||||||
|
node_modules/@qinglong/local-secret/dist
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-sqlite/package.json \
|
||||||
|
node_modules/@qinglong/local-sqlite/package.json
|
||||||
|
COPY --from=workspace /workspace/packages/ql3-local-sqlite/dist \
|
||||||
|
node_modules/@qinglong/local-sqlite/dist
|
||||||
|
|
||||||
|
RUN rm -rf node_modules/.bin \
|
||||||
|
&& node /tmp/ql3-prune-runtime-artifact.cjs node_modules/@qinglong \
|
||||||
|
@qinglong/local-api/config \
|
||||||
|
@qinglong/local-api/process \
|
||||||
|
--exclude=@qinglong/ai \
|
||||||
|
--retain-js=local-api/assets/console/console.js \
|
||||||
|
&& rm /tmp/ql3-prune-runtime-artifact.cjs
|
||||||
|
|
||||||
|
FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime-platform
|
||||||
|
|
||||||
RUN apk add --no-cache --upgrade \
|
RUN apk add --no-cache --upgrade \
|
||||||
libcrypto3=3.5.8-r0 \
|
libcrypto3=3.5.8-r0 \
|
||||||
libssl3=3.5.8-r0
|
libssl3=3.5.8-r0
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /opt/qinglong
|
||||||
|
|
||||||
|
FROM runtime-platform AS runtime
|
||||||
|
|
||||||
ARG SOURCE_REVISION=uncommitted
|
ARG SOURCE_REVISION=uncommitted
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="QingLong 3.0 Local Application" \
|
LABEL org.opencontainers.image.title="QingLong 3.0 Local Application" \
|
||||||
@@ -152,12 +235,35 @@ LABEL org.opencontainers.image.title="QingLong 3.0 Local Application" \
|
|||||||
io.qinglong.local.sqlite-write-contract="52" \
|
io.qinglong.local.sqlite-write-contract="52" \
|
||||||
io.qinglong.local.compose-selection="1"
|
io.qinglong.local.compose-selection="1"
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
WORKDIR /opt/qinglong
|
|
||||||
|
|
||||||
COPY --from=assembled --chown=65532:65532 /opt/qinglong ./
|
COPY --from=assembled --chown=65532:65532 /opt/qinglong ./
|
||||||
|
|
||||||
USER 65532:65532
|
USER 65532:65532
|
||||||
|
|
||||||
ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js"]
|
ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js"]
|
||||||
|
|
||||||
|
FROM runtime-platform AS runtime-console
|
||||||
|
|
||||||
|
ARG SOURCE_REVISION=uncommitted
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="QingLong 3.0 Local Console Application" \
|
||||||
|
org.opencontainers.image.description="QingLong 3.0 opt-in offline Console for Edge and Standalone" \
|
||||||
|
org.opencontainers.image.source="https://github.com/whyour/qinglong" \
|
||||||
|
org.opencontainers.image.revision="${SOURCE_REVISION}" \
|
||||||
|
org.opencontainers.image.licenses="Apache-2.0" \
|
||||||
|
org.opencontainers.image.version="3.0.0-alpha.2" \
|
||||||
|
io.qinglong.profile="edge-application-api,standalone-application-api" \
|
||||||
|
io.qinglong.ai="excluded" \
|
||||||
|
io.qinglong.local.console="offline-loopback" \
|
||||||
|
io.qinglong.local.application-config="2,3,4" \
|
||||||
|
io.qinglong.local.sqlite-contract-min="51" \
|
||||||
|
io.qinglong.local.sqlite-contract-max="52" \
|
||||||
|
io.qinglong.local.sqlite-write-contract="52" \
|
||||||
|
io.qinglong.local.compose-selection="1"
|
||||||
|
|
||||||
|
COPY --from=console-assembled --chown=65532:65532 /opt/qinglong ./
|
||||||
|
|
||||||
|
USER 65532:65532
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-api/dist/cli.js"]
|
||||||
|
|
||||||
|
FROM runtime AS default-runtime
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-28):
|
最新增量证据(2026-08-28):
|
||||||
|
|
||||||
- D-417/ADR-0512(已实现,Console Trial Kit 交付待后续切片):开发约二十天后的阶段产物从“可直接启动的 headless runtime”继续推进到首个可操作 Web 产品面。既有可选 `@qinglong/local-api` 现在内置 `/`、`/console.css`、`/console.js` 三文件离线 Console,总计 43,252 bytes,无前端框架、CDN、网络字体、遥测或 Web Storage;它复用同一 Node 进程、loopback listener、SQLite authority、Bearer/Project Policy/durable audit/re-confirm 链,不新增 package、sidecar、数据库连接、timer、watcher 或 cache。部署者可以按 Project 查看 Task revision/content fence、显式确认启动,查看 durable Run、Workflow Step 数与 Event sequence,并对未终止 Run 提交显式 cancellation request;取消请求不会伪装成停止终态。Credential 只在当前页面内存,服务端固定 CSP/COOP/CORP/Permissions Policy/no-referrer/nosniff/DENY/no-store,远程设备只能由操作者建立 SSH tunnel,不开放 LAN/public listener。默认 Edge/Standalone 与 D-416 Trial Kit 继续 headless、零增量;Console 只进入 opt-in `edge-application-api|standalone-application-api`,两档 artifact 为 `3,953,346 / 3,953,490 bytes`、467 files、12 packages、90 loaded modules,低于 6 MiB/640-file 门。Local API 48/48、pruner 11/11、完整 backend `1,637 total / 1,635 pass / 2 conditional skip / 0 fail`、18-package clean build/test 退出 0;package boundary 保持 18 packages 且无 single-source/shallow package,Cluster dependency、122-module Edge import 与全部 14 档 Local artifact audit 均 compatible。桌面 1440×960 与手机 390×844 已检查 Task、Run/Event spine 和显式确认工作态。该切片形成可构建、可运行的 source/application-api 阶段制品,但当前 v3 Trial Kit 尚未携带 Console;把 Console image/quickstart 变成路由/NAS 可下载物、固定低性能物理 Edge 容量数据和 public release 仍是后续门。
|
- D-418/ADR-0513(已实现,首份真实双架构产物待授权):二十天研发形成的 headless 用户旅程与 opt-in Console 已闭合为部署者可选择的 Local Alpha 交付链。手动门新增默认 `headless` 的 `local_alpha_variant=headless|console`;一次授权 run 只生成所选变体,不把两套 Application 塞入同一 archive。默认 Docker/runtime 与路由/NAS 继续使用原 8 个内部 package、无 listener 的 headless 镜像;独立 `runtime-console` target 增加 Local API/Console,固定 12-package、6 MiB/640-file、AI-excluded 闭包。Console quickstart 仅支持 Linux Docker host,使用 host network 让容器内仍严格绑定 `127.0.0.1:5700` 的服务可由宿主访问,远程只能显式 SSH tunnel,不放宽为 `0.0.0.0` 或 public ingress。Trial Kit、verification、Local milestone 与 stage index 分别升级为 `qinglong/alpha-local-trial-kit@v4`、`qinglong/alpha-local-trial-kit-verification@v2`、`qinglong/alpha-local-milestone@v2`、`qinglong/alpha-stage-index@v2`,variant 贯穿 archive/SBOM/evidence/artifact name/deployment selection,拒绝跨变体与跨架构混用。普通双架构 CI 同时构建、Trivy 扫描、库存/SBOM 对账和实跑两种镜像;Console 必须完成 fresh Owner、loopback 首页 200、未认证 API 401 与 graceful stop。实际大 archive 仍只由维护者显式 `produce_alpha_artifacts=true` 生成;没有同 run/attempt 的双架构 milestone 时,单个 archive 仍只是工程中间物,不冒充 public release、生产 HA、签名或 LTS。
|
||||||
|
|
||||||
|
- D-417/ADR-0512(已实现,部署交付由 D-418 闭合):开发约二十天后的阶段产物从“可直接启动的 headless runtime”继续推进到首个可操作 Web 产品面。既有可选 `@qinglong/local-api` 现在内置 `/`、`/console.css`、`/console.js` 三文件离线 Console,总计 43,252 bytes,无前端框架、CDN、网络字体、遥测或 Web Storage;它复用同一 Node 进程、loopback listener、SQLite authority、Bearer/Project Policy/durable audit/re-confirm 链,不新增 package、sidecar、数据库连接、timer、watcher 或 cache。部署者可以按 Project 查看 Task revision/content fence、显式确认启动,查看 durable Run、Workflow Step 数与 Event sequence,并对未终止 Run 提交显式 cancellation request;取消请求不会伪装成停止终态。Credential 只在当前页面内存,服务端固定 CSP/COOP/CORP/Permissions Policy/no-referrer/nosniff/DENY/no-store,远程设备只能由操作者建立 SSH tunnel,不开放 LAN/public listener。默认 Edge/Standalone 与 D-416 Trial Kit 继续 headless、零增量;Console 只进入 opt-in `edge-application-api|standalone-application-api`,两档 artifact 为 `3,953,346 / 3,953,490 bytes`、467 files、12 packages、90 loaded modules,低于 6 MiB/640-file 门。Local API 48/48、pruner 11/11、完整 backend `1,637 total / 1,635 pass / 2 conditional skip / 0 fail`、18-package clean build/test 退出 0;package boundary 保持 18 packages 且无 single-source/shallow package,Cluster dependency、122-module Edge import 与全部 14 档 Local artifact audit 均 compatible。桌面 1440×960 与手机 390×844 已检查 Task、Run/Event spine 和显式确认工作态。D-417 当时只形成 source/application-api 制品;其 Console image/quickstart 下载交付已由 D-418 完成,固定低性能物理 Edge 容量数据和 public release 仍是后续门。
|
||||||
|
|
||||||
- D-416/ADR-0511(已实现,首份真实 v3 Trial Kit 待授权):阶段产物的判定从“可下载、可验真”推进到“部署者可直接试运行”。Local Trial Kit schema 升为 `qinglong/alpha-local-trial-kit@v3`,每个 amd64/arm64 bundle 新增由唯一模板生成并绑定 archive、两个 exact image ID、source 和 architecture 的 `quickstart.sh`;离线 auditor 会重新渲染 canonical journey,拒绝即使重算 manifest/checksum 的本地脚本改写。目标路由/NAS 或单节点只需 POSIX shell、`sha256sum` 和 Docker,即可在一个尚不存在的隔离目录完成全包 checksum、镜像 load/identity 复核、128 MiB 短生命周期 operator fresh setup、首 Owner 建立,并按 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 启动 Application、等待 `active` 后输出 logs/stop/remove 指令。显式 artifact job 在 upload 前必须执行将要上传的 exact quickstart、检查 SQLite/Owner delivery 并 graceful stop;普通 push 不生成大 archive。聚焦 materializer/milestone/stage/workflow 回归为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`,18-package clean build/test 退出 0;package boundary 保持 `18 packages / singleSourcePackages=[] / shallowSourcePackages=[]`,Cluster dependency、122-module Edge import、镜像/版本与 14 档 Local artifact 均 compatible,基础 Edge/Standalone 仍为 `2,669,390 / 2,669,468 bytes`、325 files、58 modules。历史同源 arm64 镜像对已本地物化为 v3 八文件开发 bundle,184,648,192-byte archive 完成 checksum、canonical audit、load 与 exact identity;Docker Desktop 在 Owner directory ownership 处按既有 POSIX 不等价失败关闭,未冒充原生 Linux 成功,正式 native exact-script 证据由手动 artifact job 产生。该切片不安装宿主 Node.js,不把 Operator 变成 daemon,不增加镜像 layer、workspace package、端口或稳态 RSS,也不触碰 2.x/migration。当前产物是无外部 listener、AI-excluded 的 headless Alpha runtime,不冒充 2.x Web UI、公开 release、生产 HA 或 LTS;首份真实双架构 v3 下载物仍需维护者授权 `produce_alpha_artifacts=true`。
|
- D-416/ADR-0511(已实现,首份真实 v3 Trial Kit 待授权):阶段产物的判定从“可下载、可验真”推进到“部署者可直接试运行”。Local Trial Kit schema 升为 `qinglong/alpha-local-trial-kit@v3`,每个 amd64/arm64 bundle 新增由唯一模板生成并绑定 archive、两个 exact image ID、source 和 architecture 的 `quickstart.sh`;离线 auditor 会重新渲染 canonical journey,拒绝即使重算 manifest/checksum 的本地脚本改写。目标路由/NAS 或单节点只需 POSIX shell、`sha256sum` 和 Docker,即可在一个尚不存在的隔离目录完成全包 checksum、镜像 load/identity 复核、128 MiB 短生命周期 operator fresh setup、首 Owner 建立,并按 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 启动 Application、等待 `active` 后输出 logs/stop/remove 指令。显式 artifact job 在 upload 前必须执行将要上传的 exact quickstart、检查 SQLite/Owner delivery 并 graceful stop;普通 push 不生成大 archive。聚焦 materializer/milestone/stage/workflow 回归为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`,18-package clean build/test 退出 0;package boundary 保持 `18 packages / singleSourcePackages=[] / shallowSourcePackages=[]`,Cluster dependency、122-module Edge import、镜像/版本与 14 档 Local artifact 均 compatible,基础 Edge/Standalone 仍为 `2,669,390 / 2,669,468 bytes`、325 files、58 modules。历史同源 arm64 镜像对已本地物化为 v3 八文件开发 bundle,184,648,192-byte archive 完成 checksum、canonical audit、load 与 exact identity;Docker Desktop 在 Owner directory ownership 处按既有 POSIX 不等价失败关闭,未冒充原生 Linux 成功,正式 native exact-script 证据由手动 artifact job 产生。该切片不安装宿主 Node.js,不把 Operator 变成 daemon,不增加镜像 layer、workspace package、端口或稳态 RSS,也不触碰 2.x/migration。当前产物是无外部 listener、AI-excluded 的 headless Alpha runtime,不冒充 2.x Web UI、公开 release、生产 HA 或 LTS;首份真实双架构 v3 下载物仍需维护者授权 `produce_alpha_artifacts=true`。
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ D-416 的 Local Trial Kit 和基础 `edge|standalone` Application 仍保持 head
|
|||||||
- `application-api` Edge/Standalone 制品当前为 3,953,346 / 3,953,490 bytes、467 files、12 packages,远低于 6 MiB/640-file budget;加载闭包为 90 modules,实测 import RSS delta 约 14.0 MiB;
|
- `application-api` Edge/Standalone 制品当前为 3,953,346 / 3,953,490 bytes、467 files、12 packages,远低于 6 MiB/640-file budget;加载闭包为 90 modules,实测 import RSS delta 约 14.0 MiB;
|
||||||
- 基础 headless Edge/Standalone、D-416 Trial Kit、Cluster Profile 和 AI/MCP Profile 不因本切片新增 Console 进程或端口;
|
- 基础 headless Edge/Standalone、D-416 Trial Kit、Cluster Profile 和 AI/MCP Profile 不因本切片新增 Console 进程或端口;
|
||||||
- Console 视觉和交互已在 1440×960 与 390×844 视口验证,工作态覆盖 Task detail、运行确认、Run evidence/Event sequence 和取消入口;
|
- Console 视觉和交互已在 1440×960 与 390×844 视口验证,工作态覆盖 Task detail、运行确认、Run evidence/Event sequence 和取消入口;
|
||||||
- 当前仍缺少面向部署用户的 Console Docker image/Trial Kit 选择、真实低性能物理 Edge 容量数据,以及受保护 public release。
|
- 本 ADR 当时缺少的 Console Docker image/Trial Kit 选择已由 ADR-0513 闭合;真实低性能物理 Edge 容量数据和受保护 public release 仍未完成。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# ADR-0513:可选择的 Local Console Trial Kit
|
||||||
|
|
||||||
|
- 状态:Accepted
|
||||||
|
- 日期:2026-08-28
|
||||||
|
- 决策:D-418
|
||||||
|
- 关联:ADR-0503、ADR-0506、ADR-0508、ADR-0510、ADR-0511、ADR-0512
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
D-416 已形成可执行的 headless Local Alpha Trial Kit,D-417 已形成有界、离线、loopback-only 的 Local Web Console,但部署用户仍不能下载一套同时包含 Console、短生命周期管理 authority 和 canonical quickstart 的闭合产物。开发约二十天后,阶段成果必须能够由 amd64/arm64 的路由器、NAS 或单节点用户下载、验真、启动和回退,而不能只停留在源码包或单元测试。
|
||||||
|
|
||||||
|
同时,Console 不应增加默认低配设备的镜像体积、listener 或稳态资源成本;也不能为了容器访问而把 Local API 从 `127.0.0.1` 放宽为 LAN/public listener。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
### 1. Local Trial Kit 提供两个互斥变体
|
||||||
|
|
||||||
|
显式 artifact run 新增 `local_alpha_variant=headless|console`,默认 `headless`。一次授权运行只生成所选变体的 amd64/arm64 Trial Kit 和对应 Local milestone,不把两套 Application 镜像塞入同一个 archive:
|
||||||
|
|
||||||
|
- `headless`:沿用最小 Local Application,稳态无 listener,适合低配路由器/NAS;
|
||||||
|
- `console`:使用独立 `runtime-console` image target,携带 `@qinglong/local-api` 与 `@qinglong/local-owner-console`,适合需要浏览器操作面的 Linux 单节点。
|
||||||
|
|
||||||
|
两者共享同一个短生命周期 Local operator。默认 Docker build 仍落在 headless runtime,Console 不进入 2.x、Cluster 或基础 Edge/Standalone 闭包。
|
||||||
|
|
||||||
|
### 2. Console 保持 loopback-only
|
||||||
|
|
||||||
|
Console quickstart 只允许 Linux Docker host,Application 使用 host network,但进程配置仍严格绑定 `127.0.0.1:5700`。这样宿主浏览器可访问 loopback,同时没有把 API 改为 `0.0.0.0`。远程设备必须由操作者建立 SSH tunnel,不得直接向 LAN 或公网暴露端口。
|
||||||
|
|
||||||
|
### 3. 产物协议显式升级并绑定变体
|
||||||
|
|
||||||
|
- Trial Kit:`qinglong/alpha-local-trial-kit@v4`,manifest schemaVersion 5;
|
||||||
|
- verification evidence:`qinglong/alpha-local-trial-kit-verification@v2`;
|
||||||
|
- Local milestone:`qinglong/alpha-local-milestone@v2`;
|
||||||
|
- Alpha stage index:`qinglong/alpha-stage-index@v2`。
|
||||||
|
|
||||||
|
manifest、verification、SBOM、archive 名、artifact 名、milestone 和 stage deployment selection 都必须记录同一个 `variant`。Headless archive 保留兼容文件名;Console 使用 `qinglong3-local-console-trial-kit-<arch>.docker.tar`。跨变体复制、混合双架构或用 headless SBOM 冒充 Console 一律失败关闭。
|
||||||
|
|
||||||
|
### 4. 阶段可用必须有真实运行证据
|
||||||
|
|
||||||
|
普通 push/PR 构建并扫描两个 image target,复核 exact package inventory/SBOM,并在原生 amd64/arm64 Linux 上执行:
|
||||||
|
|
||||||
|
- headless Edge 与 Standalone fresh setup、首 Owner、active、SIGTERM drain;
|
||||||
|
- Console Edge fresh setup、首 Owner、loopback 首页 HTTP 200、未认证 API HTTP 401、SIGTERM drain;
|
||||||
|
- 128 MiB/64 PID Edge 入口约束和 AI-excluded closure。
|
||||||
|
|
||||||
|
只有显式 `produce_alpha_artifacts=true` 的 workflow 才能生成大 archive;上传前必须从即将上传的目录运行 exact `quickstart.sh`。没有同 run/attempt 的双架构 Local milestone 时,单个 archive 仍只是中间产物。
|
||||||
|
|
||||||
|
## 被拒绝的替代方案
|
||||||
|
|
||||||
|
### 一个 archive 同时携带 headless 与 Console
|
||||||
|
|
||||||
|
拒绝。低配用户会为未选择的 UI 支付下载和存储成本,部署选择也难以从 SBOM 和 image identity 中可靠判定。
|
||||||
|
|
||||||
|
### Console 取代默认 Local Application
|
||||||
|
|
||||||
|
拒绝。它会让所有路由设备承担额外 package、静态资产和 listener,违背按能力付费与默认最小运行时原则。
|
||||||
|
|
||||||
|
### 将 Local API 改为 `0.0.0.0`
|
||||||
|
|
||||||
|
拒绝。Alpha 尚未关闭 TLS、可信代理、CSRF 和远程会话门;容器便利性不能扩大网络 authority。
|
||||||
|
|
||||||
|
### 每个变体一次生成两套 artifact
|
||||||
|
|
||||||
|
拒绝。单次显式授权只产生被维护者选择的变体,避免大 archive 存储翻倍,并保持 milestone 语义单一。
|
||||||
|
|
||||||
|
## 影响
|
||||||
|
|
||||||
|
- 低配设备默认仍下载和运行 headless Application,Console 对其为零成本;
|
||||||
|
- Console image 有独立 12-package、6 MiB/640-file 上限、SBOM、OS vulnerability policy 与双架构 live gate;
|
||||||
|
- Local milestone 和 stage index 升级 schema,旧 v1/v3 索引不会被新 auditor 静默接受;
|
||||||
|
- 该决策提供 Alpha/Linux 试运行产物,不等于 public release、生产 ingress、HA、签名或 LTS;
|
||||||
|
- 首份真实可下载双架构产物仍需维护者显式授权 workflow,普通 CI 成功只证明产物链可生成。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- Trial Kit materializer/auditor 覆盖 headless 与 Console 的 archive、SBOM、quickstart 和 mutation 拒绝;
|
||||||
|
- Local milestone 与 stage index 覆盖变体绑定、双架构隔离和 Console deployment selection;
|
||||||
|
- Dockerfile audit 固定 headless/Console 两套精确 package closure、Console asset 保留与 loopback label;
|
||||||
|
- 原生 Linux CI 对两个架构运行 Console HTTP 200/401 和生命周期门;
|
||||||
|
- 本地聚焦门为 `70/70`,完整 backend 为 `1646 total / 1644 pass / 2 conditional skip / 0 fail`,18-package clean build/test 退出 0,14 档 Local artifact audit 全部 `compatible=true`;
|
||||||
|
- 本机实际镜像库存为 headless `10 packages / 425 files / 3,638,399 bytes`、Console `12 packages / 455 files / 3,831,208 bytes`,两者均在 128 MiB/64 PID entrypoint 门内且排除 AI;
|
||||||
|
- macOS Docker Desktop 的 bind mount 不能提供与原生 Linux 等价的 POSIX Owner UID/mode 证据,Console fresh lifecycle 因此在 owner-private directory 门失败关闭;不得把该宿主限制冒充应用通过,最终双架构证据由推送后的原生 Linux CI 关闭;
|
||||||
|
- GitNexus 变更审计、镜像静态门和 Docker 实物门在提交前执行。
|
||||||
+2
-1
@@ -515,7 +515,8 @@
|
|||||||
| [ADR-0509](./ADR-0509-cluster-alpha-bundle-and-milestone-closure.md) | Cluster Alpha Bundle 与跨架构里程碑闭合 | Accepted(首份实际 Cluster milestone artifact 待维护者授权) |
|
| [ADR-0509](./ADR-0509-cluster-alpha-bundle-and-milestone-closure.md) | Cluster Alpha Bundle 与跨架构里程碑闭合 | Accepted(首份实际 Cluster milestone artifact 待维护者授权) |
|
||||||
| [ADR-0510](./ADR-0510-cross-profile-alpha-stage-delivery-index.md) | 跨 Profile Alpha 阶段交付索引 | Accepted(首份实际 stage index 待维护者授权) |
|
| [ADR-0510](./ADR-0510-cross-profile-alpha-stage-delivery-index.md) | 跨 Profile Alpha 阶段交付索引 | Accepted(首份实际 stage index 待维护者授权) |
|
||||||
| [ADR-0511](./ADR-0511-runnable-local-alpha-quickstart.md) | 可直接试运行的 Local Alpha Quickstart | Accepted(首份实际 v3 Trial Kit 待维护者授权) |
|
| [ADR-0511](./ADR-0511-runnable-local-alpha-quickstart.md) | 可直接试运行的 Local Alpha Quickstart | Accepted(首份实际 v3 Trial Kit 待维护者授权) |
|
||||||
| [ADR-0512](./ADR-0512-bounded-offline-local-web-console.md) | 有界、离线的 Local Web Console | Accepted(Console Trial Kit 交付待后续切片) |
|
| [ADR-0512](./ADR-0512-bounded-offline-local-web-console.md) | 有界、离线的 Local Web Console | Accepted(Trial Kit 交付已由 ADR-0513 闭合) |
|
||||||
|
| [ADR-0513](./ADR-0513-selectable-local-console-trial-kit.md) | 可选择的 Local Console Trial Kit | Accepted(首份实际双架构产物待维护者授权) |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,16 @@
|
|||||||
|
|
||||||
当维护者显式选择 `alpha_artifact_scope=all` 时,还会生成 `Alpha stage index`。它把同一次 run 的 Local/Cluster milestone 交叉绑定,并为 Edge、Standalone、Cluster 给出目标架构的最小 artifact 选择;这是阶段交付导航,不是正式 release catalog。只生成 Local 或 Cluster 时,各自 milestone 仍可独立成立,不制造一个不完整的总索引。
|
当维护者显式选择 `alpha_artifact_scope=all` 时,还会生成 `Alpha stage index`。它把同一次 run 的 Local/Cluster milestone 交叉绑定,并为 Edge、Standalone、Cluster 给出目标架构的最小 artifact 选择;这是阶段交付导航,不是正式 release catalog。只生成 Local 或 Cluster 时,各自 milestone 仍可独立成立,不制造一个不完整的总索引。
|
||||||
|
|
||||||
## 当前阶段实物(2026-08-27)
|
## 当前阶段实物(2026-08-28)
|
||||||
|
|
||||||
在下面保留的历史 exact-image 证据之外,2026-08-28 的源码阶段已经形成两条可独立验收的产品线:
|
在下面保留的历史 exact-image 证据之外,2026-08-28 的源码阶段已把 headless 用户旅程与 opt-in Console 合并为一条可选择的交付链:
|
||||||
|
|
||||||
| 阶段产物 | 当前可用能力 | 仍缺少 |
|
| 阶段产物 | 当前可用能力 | 仍缺少 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| D-416 Local Alpha Trial Kit v3 | POSIX shell + Docker 一条命令完成 checksum、load、fresh setup、首 Owner、headless Application active/stop | 首份远端双架构实际 artifact 仍需维护者授权 `produce_alpha_artifacts=true` |
|
| D-418 headless Trial Kit v4 | 默认低配变体;POSIX shell + Docker 一条命令完成 checksum、load、fresh setup、首 Owner、Application active/stop;无 listener | 首份远端双架构实际 artifact 仍需维护者授权 `produce_alpha_artifacts=true` |
|
||||||
| D-417 opt-in Local Web Console | `application-api` 制品可在同进程/同 SQLite/loopback listener 上查看 Task/Run/Event 并显式运行或取消;Edge/Standalone 制品审计通过 | 尚未进入 Trial Kit Docker image/quickstart,不能冒充当前路由/NAS 下载物 |
|
| D-418 Console Trial Kit v4 | 显式 Linux-only 变体;同一 quickstart 关闭 Owner ceremony 后启动 loopback Console,支持 Task/Run/Event、显式运行与取消;CI 验证首页 200、未认证 API 401 | 仍是 Alpha、无 public ingress/TLS/签名;首份实际 archive 同样需维护者授权 |
|
||||||
|
|
||||||
这两个边界共同防止把“20 天代码和测试”冒充“用户已经能下载并完整操作”:D-416 已关闭 fresh headless 用户旅程,D-417 已关闭 source/application-api 操作界面,但两者尚未合并成实际可下载的 Console Trial Kit。操作说明见 [Local Web Console](./ql3-local-web-console.md)。
|
D-418 防止把“20 天代码和测试”冒充“用户已经能下载并完整操作”:源码与普通 CI 已具备生成、审计和实跑两种 Trial Kit 的能力,但只有显式 artifact run 生成且被同 run 的双架构 milestone 收录后,才是可下载阶段产物。操作说明见 [Local Alpha Trial Kit](./ql3-local-alpha-trial-kit.md) 与 [Local Web Console](./ql3-local-web-console.md)。
|
||||||
|
|
||||||
提交 `4239464af6937d56528a0a2c573d12329bc7ca55` 已形成最新 owner-private arm64 工程候选:
|
提交 `4239464af6937d56528a0a2c573d12329bc7ca55` 已形成最新 owner-private arm64 工程候选:
|
||||||
|
|
||||||
@@ -35,24 +35,24 @@
|
|||||||
|
|
||||||
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
|
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
|
||||||
|
|
||||||
ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加了 source-bound `verification-evidence.json`;ADR-0511 进一步把当前格式升级为 `@v3`,新增不依赖宿主 Node.js 的 canonical `quickstart.sh`,把可验证镜像闭合为可执行的 fresh 用户旅程。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit、`4239464a` v1 archive 与未携带 quickstart 的 v2 均为历史工程证据,不能通过 v3 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成并实际执行 exact-image 双架构可下载 archive。
|
ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加 source-bound verification,ADR-0511 的 `@v3` 增加 canonical quickstart,ADR-0513 的 `@v4` 再把 `headless|console` 变体绑定到 image、SBOM、verification、milestone 和 stage index。旧 runtime-only、v1/v2/v3 bundle 均为历史工程证据,不能通过 v4 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成并实际执行所选变体的双架构可下载 archive。
|
||||||
|
|
||||||
## 生成
|
## 生成
|
||||||
|
|
||||||
在 GitHub Actions 手动运行 `QingLong 3.0 CI`,选择目标 `next` 提交,设置 `produce_alpha_artifacts=true`,并明确选择 `alpha_artifact_scope=local|cluster|all`。普通 push/PR 不上传大镜像,避免每次开发提交都制造伪里程碑和额外存储成本。
|
在 GitHub Actions 手动运行 `QingLong 3.0 CI`,选择目标 `next` 提交,设置 `produce_alpha_artifacts=true`,明确选择 `alpha_artifact_scope=local|cluster|all`,并为 Local 选择 `local_alpha_variant=headless|console`(默认 headless)。普通 push/PR 不上传大镜像,避免每次开发提交都制造伪里程碑和额外存储成本。
|
||||||
|
|
||||||
成功后同一次 run 生成、保留 30 天:
|
成功后同一次 run 生成、保留 30 天:
|
||||||
|
|
||||||
- `ql3-alpha-<commit>-local-amd64` 与 `ql3-alpha-<commit>-local-arm64`;
|
- `ql3-alpha-<commit>-local-<variant>-amd64` 与 `ql3-alpha-<commit>-local-<variant>-arm64`;
|
||||||
- `ql3-alpha-<commit>-local-milestone`;
|
- `ql3-alpha-<commit>-local-<variant>-milestone`;
|
||||||
- `ql3-alpha-<commit>-control-<arch>`、`control-ai-<arch>`、`admin-<arch>`、`worker-<arch>`。
|
- `ql3-alpha-<commit>-control-<arch>`、`control-ai-<arch>`、`admin-<arch>`、`worker-<arch>`。
|
||||||
- `ql3-alpha-<commit>-cluster-milestone`。
|
- `ql3-alpha-<commit>-cluster-milestone`。
|
||||||
- 仅 `alpha_artifact_scope=all`:`ql3-alpha-<commit>-stage-index`。
|
- 仅 `alpha_artifact_scope=all`:`ql3-alpha-<commit>-stage-index`。
|
||||||
|
|
||||||
Local artifact 含:
|
Local artifact 含:
|
||||||
|
|
||||||
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
|
- 一个包含所选 Application 与短生命周期 operator 的 archive;headless 为 `qinglong3-local-trial-kit-<arch>.docker.tar`,Console 为 `qinglong3-local-console-trial-kit-<arch>.docker.tar`,共享 Node 基础层在 archive 中去重;
|
||||||
- schema 为 `qinglong/alpha-local-trial-kit@v3` 的 `manifest.json`,通过 `archive/images/sboms/quickstart/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
- schema 为 `qinglong/alpha-local-trial-kit@v4` 的 `manifest.json`,通过 `variant/archive/images/sboms/quickstart/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
||||||
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active;
|
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active;
|
||||||
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
||||||
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
||||||
@@ -60,7 +60,7 @@ Local artifact 含:
|
|||||||
|
|
||||||
Cluster artifact 是每角色/架构一个六文件闭包:native Docker archive、精确 CycloneDX SBOM、workflow-bound verification evidence、README、`qinglong/alpha-cluster-image@v1` manifest 和覆盖全部内容文件的 `SHA256SUMS`。完整 CI 成功后,八个 bundle 由 `qinglong/alpha-cluster-milestone@v1` 小型索引闭合;索引本身不重复存放大 archive。
|
Cluster artifact 是每角色/架构一个六文件闭包:native Docker archive、精确 CycloneDX SBOM、workflow-bound verification evidence、README、`qinglong/alpha-cluster-image@v1` manifest 和覆盖全部内容文件的 `SHA256SUMS`。完整 CI 成功后,八个 bundle 由 `qinglong/alpha-cluster-milestone@v1` 小型索引闭合;索引本身不重复存放大 archive。
|
||||||
|
|
||||||
Stage index 是 `qinglong/alpha-stage-index@v1` 三文件闭包。它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把路由/NAS 的单 Local Trial Kit 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
Local milestone 是 `qinglong/alpha-local-milestone@v2` 三文件闭包,绑定一个 variant 的双架构 Trial Kit。Stage index 是 `qinglong/alpha-stage-index@v2` 三文件闭包;它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把 Local variant/Profile 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
||||||
|
|
||||||
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ sh quickstart.sh edge /opt/qinglong3-alpha-data
|
|||||||
|
|
||||||
也可以选择 `standalone` 和自定义容器名。quickstart 会先执行 `SHA256SUMS`,再核对 exact
|
也可以选择 `standalone` 和自定义容器名。quickstart 会先执行 `SHA256SUMS`,再核对 exact
|
||||||
镜像身份并完成 fresh setup、首 Owner 与 Application active;成功后输出 logs、stop 和
|
镜像身份并完成 fresh setup、首 Owner 与 Application active;成功后输出 logs、stop 和
|
||||||
remove 命令。当前是无外部 listener、AI-excluded 的 headless runtime 阶段成果,不是
|
remove 命令。Headless 不开放 listener;Console 只在 Linux host 的 `127.0.0.1:5700`
|
||||||
2.x Web UI 的替代品。
|
提供 Alpha 操作面,远程访问必须经 SSH tunnel。两者都不是 2.x Web UI 的生产替代品。
|
||||||
|
|
||||||
## 手工验证与最小 smoke
|
## 手工验证与最小 smoke
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
| 设备或环境 | 选择 | 稳态组件 | 需要下载 |
|
| 设备或环境 | 选择 | 稳态组件 | 需要下载 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 低配路由器、NAS、单机 | `deploymentSelections.local` | 仅 Local Application;Operator 只在 setup/upgrade/recovery 短暂运行 | 目标架构的一个 Local Trial Kit |
|
| 低配路由器、NAS、单机 | `deploymentSelections.local`;默认选择 `headless`,需要浏览器操作面时显式选择 `console` | 仅 Local Application;Operator 只在 setup/upgrade/recovery 短暂运行 | 目标架构、目标 variant 的一个 Local Trial Kit |
|
||||||
| 临时 K3s/Kubernetes 集群 | `deploymentSelections.cluster` | control、admin、worker;AI 按需增加 control-ai | 目标架构的三个 required artifact;需要 AI 时再下载 optional artifact |
|
| 临时 K3s/Kubernetes 集群 | `deploymentSelections.cluster` | control、admin、worker;AI 按需增加 control-ai | 目标架构的三个 required artifact;需要 AI 时再下载 optional artifact |
|
||||||
|
|
||||||
不得把四个 Cluster 角色部署到低配路由器,也不需要为一台 amd64 主机下载 arm64 归档。索引固定列出十个可选择 artifact,但每个部署者只下载目标 Profile、架构和角色所需的子集。
|
不得把四个 Cluster 角色部署到低配路由器,也不需要为一台 amd64 主机下载 arm64 归档。索引固定列出十个可选择 artifact,但每个部署者只下载目标 Profile、架构和角色所需的子集。
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
1. 在本目录运行 `sha256sum --check SHA256SUMS`。
|
1. 在本目录运行 `sha256sum --check SHA256SUMS`。
|
||||||
2. 检查 `manifest.json`:
|
2. 检查 `manifest.json`:
|
||||||
- schema 为 `qinglong/alpha-stage-index@v1`;
|
- schema 为 `qinglong/alpha-stage-index@v2`;
|
||||||
|
- `deploymentSelections.local.variant` 与 Local milestone、artifact 名和 Profile 一致;
|
||||||
- maturity 为 `alpha_stage_delivery_not_public_release`;
|
- maturity 为 `alpha_stage_delivery_not_public_release`;
|
||||||
- source revision、run ID/attempt 是准备验证的显式 workflow run;
|
- source revision、run ID/attempt 是准备验证的显式 workflow run;
|
||||||
- `milestones.local` 与 `milestones.cluster` 分别指向同一提交的 milestone artifact。
|
- `milestones.local` 与 `milestones.cluster` 分别指向同一提交的 milestone artifact。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- 不提供公开 GHCR tag、Cosign 签名、GitHub attestation、catalog、生产升级或长期支持承诺;
|
- 不提供公开 GHCR tag、Cosign 签名、GitHub attestation、catalog、生产升级或长期支持承诺;
|
||||||
- Cluster/Kubernetes 仍按独立 Integration Candidate 与 Public Release Set 门验收。
|
- Cluster/Kubernetes 仍按独立 Integration Candidate 与 Public Release Set 门验收。
|
||||||
|
|
||||||
单个架构 artifact 提前上传并不代表 milestone 成功。没有 `ql3-alpha-<sourceRevision>-local-milestone` 索引、索引 workflow 不是成功终态、run/attempt 不一致或索引审计失败时,已有的大归档只能作为失败运行的中间文件,不得交付用户。
|
单个架构 artifact 提前上传并不代表 milestone 成功。没有 `ql3-alpha-<sourceRevision>-local-<variant>-milestone` 索引、索引 workflow 不是成功终态、run/attempt/variant 不一致或索引审计失败时,已有的大归档只能作为失败运行的中间文件,不得交付用户。
|
||||||
|
|
||||||
## 选择并验证下载物
|
## 选择并验证下载物
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 打开 `manifest.json`,确认:
|
2. 打开 `manifest.json`,确认:
|
||||||
- `schema` 为 `qinglong/alpha-local-milestone@v1`;
|
- `schema` 为 `qinglong/alpha-local-milestone@v2`;
|
||||||
|
- `variant` 为 `headless` 或 `console`,且两个架构记录都使用同一变体;
|
||||||
- `sourceRevision` 是准备试用的完整 40 位提交;
|
- `sourceRevision` 是准备试用的完整 40 位提交;
|
||||||
- `workflow.event` 为 `workflow_dispatch`,`workflow.job` 为 `local-alpha-milestone`;
|
- `workflow.event` 为 `workflow_dispatch`,`workflow.job` 为 `local-alpha-milestone`;
|
||||||
- GitHub Actions 中对应 `runId/runAttempt` 的整条 `QingLong 3.0 CI` 为成功终态;
|
- GitHub Actions 中对应 `runId/runAttempt` 的整条 `QingLong 3.0 CI` 为成功终态;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本目录是绑定一个 QingLong 3.0 源码提交、一个 Linux 架构和一次显式 GitHub milestone run 的阶段试运行套件,不是公开 release 或生产升级承诺。它同时包含常驻 Application 镜像和短生命周期 operator 镜像;两者共享的 OCI layer 只在同一个 Docker archive 中保存一次。
|
本目录是绑定一个 QingLong 3.0 源码提交、一个 Linux 架构和一次显式 GitHub milestone run 的阶段试运行套件,不是公开 release 或生产升级承诺。它同时包含常驻 Application 镜像和短生命周期 operator 镜像;两者共享的 OCI layer 只在同一个 Docker archive 中保存一次。
|
||||||
|
|
||||||
一套 Trial Kit 只有被同一 run 的 `ql3-alpha-<sourceRevision>-local-milestone` 跨架构索引收录后才是可交付阶段产物。单个矩阵 job 提前上传、另一架构或完整 CI 随后失败时留下的 artifact 只是中间文件。先按 milestone `manifest.json` 选择本机架构并核对本 bundle manifest digest,再执行下述离线验收。
|
一套 Trial Kit 只有被同一 run 的 `ql3-alpha-<sourceRevision>-local-<variant>-milestone` 跨架构索引收录后才是可交付阶段产物。单个矩阵 job 提前上传、另一架构或完整 CI 随后失败时留下的 artifact 只是中间文件。先按 milestone `manifest.json` 选择 `variant` 和本机架构并核对本 bundle manifest digest,再执行下述离线验收。
|
||||||
|
|
||||||
## 适用范围
|
## 适用范围
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
- fresh、隔离的测试数据目录;
|
- fresh、隔离的测试数据目录;
|
||||||
- 离线导入、设备兼容验证和 3.0 Alpha 用户旅程验证。
|
- 离线导入、设备兼容验证和 3.0 Alpha 用户旅程验证。
|
||||||
|
|
||||||
|
`headless` 是默认且最小的低配设备变体,不打开端口。`console` 是显式选择的 Linux-only 变体,携带离线 Web Console,并仅通过宿主 `127.0.0.1:5700` 提供操作面。二者是独立 archive,不应同时下载;远程 Console 只允许经 SSH tunnel 访问,不得暴露到 LAN 或公网。
|
||||||
|
|
||||||
不要把它直接用于生产数据、2.x 唯一数据目录或生产 Secret。Cluster/Kubernetes 节点应使用 Cluster Integration Candidate;本套件不包含 PostgreSQL HA、Worker 或 Cluster Admin。
|
不要把它直接用于生产数据、2.x 唯一数据目录或生产 Secret。Cluster/Kubernetes 节点应使用 Cluster Integration Candidate;本套件不包含 PostgreSQL HA、Worker 或 Cluster Admin。
|
||||||
|
|
||||||
## 离线验收
|
## 离线验收
|
||||||
@@ -23,7 +25,8 @@ sha256sum --check SHA256SUMS
|
|||||||
|
|
||||||
`manifest.json` 必须满足:
|
`manifest.json` 必须满足:
|
||||||
|
|
||||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v3`;
|
- `schema` 为 `qinglong/alpha-local-trial-kit@v4`;
|
||||||
|
- `variant` 为 `headless` 或 `console`,并与 milestone、application SBOM 和 artifact 名一致;
|
||||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||||
- `architecture` 与主机相同;
|
- `architecture` 与主机相同;
|
||||||
- `maturity` 为 `alpha_candidate_not_public_release`。
|
- `maturity` 为 `alpha_candidate_not_public_release`。
|
||||||
@@ -42,7 +45,7 @@ node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
|||||||
|
|
||||||
## 一条命令完成 Fresh 试运行
|
## 一条命令完成 Fresh 试运行
|
||||||
|
|
||||||
v3 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
|
v4 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
|
||||||
shell、`sha256sum` 和已启动的 Docker。必须选择一个尚不存在、与 2.x/生产数据完全
|
shell、`sha256sum` 和已启动的 Docker。必须选择一个尚不存在、与 2.x/生产数据完全
|
||||||
隔离的绝对路径:
|
隔离的绝对路径:
|
||||||
|
|
||||||
@@ -66,11 +69,12 @@ Owner delivery 保留在新数据目录的 `owner-delivery/`,operator command
|
|||||||
删除数据目录;确认不再需要诊断后由操作者显式删除该 fresh 测试目录。脚本拒绝既有目录,
|
删除数据目录;确认不再需要诊断后由操作者显式删除该 fresh 测试目录。脚本拒绝既有目录,
|
||||||
不能用于升级、迁移或接管 2.x。
|
不能用于升级、迁移或接管 2.x。
|
||||||
|
|
||||||
当前 Application 是无外部 listener、AI-excluded 的 headless Alpha runtime。该试运行可
|
Headless Application 是无外部 listener、AI-excluded 的最小 Alpha runtime。Console 变体同样
|
||||||
验证 3.0 SQLite、Owner authority、调度/插件基础与生命周期,但还不是 2.x Web UI 的
|
AI-excluded,但 quickstart 会在 Linux 上使用 host network,让容器内仍严格绑定
|
||||||
可替代版本。D-417 已在 opt-in `application-api` Profile 提供首个 Local Web Console,
|
`127.0.0.1:5700` 的 Local API 可由宿主浏览器访问。成功后打开
|
||||||
但本 v3 Trial Kit 尚未携带该 Profile;不要期待 quickstart 启动后出现 Web 端口。Console
|
`http://127.0.0.1:5700/`;远程主机必须建立 SSH tunnel。两种变体都只用于 fresh Alpha,
|
||||||
当前使用方式与边界见 [Local Web Console](./ql3-local-web-console.md)。
|
不是 2.x Web UI 的生产替代版本。Console 能力和凭据边界见
|
||||||
|
[Local Web Console](./ql3-local-web-console.md)。
|
||||||
|
|
||||||
## 手工加载与最小 smoke
|
## 手工加载与最小 smoke
|
||||||
|
|
||||||
@@ -78,6 +82,7 @@ Owner delivery 保留在新数据目录的 `owner-delivery/`,operator command
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker load --input qinglong3-local-trial-kit-<arch>.docker.tar
|
docker load --input qinglong3-local-trial-kit-<arch>.docker.tar
|
||||||
|
# Console 变体使用 qinglong3-local-console-trial-kit-<arch>.docker.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
以 manifest 中 `images.application.reference` 和 `images.operator.reference` 为准,分别核对 `docker image inspect` 返回的 image ID。然后执行无网络、只读 smoke:
|
以 manifest 中 `images.application.reference` 和 `images.operator.reference` 为准,分别核对 `docker image inspect` 返回的 image ID。然后执行无网络、只读 smoke:
|
||||||
@@ -98,7 +103,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
|||||||
|
|
||||||
## Fresh 试运行边界
|
## Fresh 试运行边界
|
||||||
|
|
||||||
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。v3 artifact job 还必须从将要上传的目录实际执行 `quickstart.sh` 并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。Console 还必须证明首页返回 200、未认证 API 返回 401。v4 artifact job 必须从将要上传的目录实际执行 `quickstart.sh` 并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||||
|
|
||||||
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const { sha256File } = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
|||||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SCHEMA = 'qinglong/alpha-stage-index@v1';
|
const SCHEMA = 'qinglong/alpha-stage-index@v2';
|
||||||
const FILES = Object.freeze({
|
const FILES = Object.freeze({
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
@@ -118,8 +118,10 @@ function stageArtifactName(sourceRevision) {
|
|||||||
return `ql3-alpha-${sourceRevision}-stage-index`;
|
return `ql3-alpha-${sourceRevision}-stage-index`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function milestoneArtifactName(sourceRevision, product) {
|
function milestoneArtifactName(sourceRevision, product, variant) {
|
||||||
return `ql3-alpha-${sourceRevision}-${product}-milestone`;
|
return product === 'local'
|
||||||
|
? `ql3-alpha-${sourceRevision}-local-${variant}-milestone`
|
||||||
|
: `ql3-alpha-${sourceRevision}-cluster-milestone`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateWorkflow(document, sourceRevision) {
|
function validateWorkflow(document, sourceRevision) {
|
||||||
@@ -189,8 +191,15 @@ function readMilestones(localMilestoneRoot, clusterMilestoneRoot) {
|
|||||||
function expectedSelections(local, cluster) {
|
function expectedSelections(local, cluster) {
|
||||||
return {
|
return {
|
||||||
local: {
|
local: {
|
||||||
profiles: ['edge', 'standalone'],
|
variant: local.variant,
|
||||||
intent: 'fresh_non_production_trial',
|
profiles:
|
||||||
|
local.variant === 'console'
|
||||||
|
? ['edge-application-api', 'standalone-application-api']
|
||||||
|
: ['edge', 'standalone'],
|
||||||
|
intent:
|
||||||
|
local.variant === 'console'
|
||||||
|
? 'fresh_loopback_console_non_production_trial'
|
||||||
|
: 'fresh_non_production_trial',
|
||||||
architectures: Object.fromEntries(
|
architectures: Object.fromEntries(
|
||||||
ARCHITECTURES.map((architecture) => [
|
ARCHITECTURES.map((architecture) => [
|
||||||
architecture,
|
architecture,
|
||||||
@@ -224,18 +233,19 @@ function expectedSelections(local, cluster) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateMilestoneRecord(record, product, sourceRevision) {
|
function validateMilestoneRecord(record, product, sourceRevision, variant) {
|
||||||
const expectedMaturity =
|
const expectedMaturity =
|
||||||
product === 'local'
|
product === 'local'
|
||||||
? 'alpha_candidate_not_public_release'
|
? 'alpha_candidate_not_public_release'
|
||||||
: 'cluster_integration_candidate_not_public_release';
|
: 'cluster_integration_candidate_not_public_release';
|
||||||
const expectedSchema =
|
const expectedSchema =
|
||||||
product === 'local'
|
product === 'local'
|
||||||
? 'qinglong/alpha-local-milestone@v1'
|
? 'qinglong/alpha-local-milestone@v2'
|
||||||
: 'qinglong/alpha-cluster-milestone@v1';
|
: 'qinglong/alpha-cluster-milestone@v1';
|
||||||
if (
|
if (
|
||||||
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
||||||
record.artifactName !== milestoneArtifactName(sourceRevision, product) ||
|
record.artifactName !==
|
||||||
|
milestoneArtifactName(sourceRevision, product, variant) ||
|
||||||
record.schema !== expectedSchema ||
|
record.schema !== expectedSchema ||
|
||||||
record.maturity !== expectedMaturity ||
|
record.maturity !== expectedMaturity ||
|
||||||
!exactKeys(record.manifest, ['file', 'sha256', 'bytes']) ||
|
!exactKeys(record.manifest, ['file', 'sha256', 'bytes']) ||
|
||||||
@@ -293,7 +303,7 @@ function auditAlphaStageIndex(options) {
|
|||||||
'deploymentSelections',
|
'deploymentSelections',
|
||||||
'readme',
|
'readme',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 1 ||
|
manifest.schemaVersion !== 2 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_stage_delivery_not_public_release' ||
|
manifest.maturity !== 'alpha_stage_delivery_not_public_release' ||
|
||||||
manifest.product !== 'qinglong3' ||
|
manifest.product !== 'qinglong3' ||
|
||||||
@@ -321,11 +331,13 @@ function auditAlphaStageIndex(options) {
|
|||||||
manifest.milestones.local,
|
manifest.milestones.local,
|
||||||
'local',
|
'local',
|
||||||
manifest.sourceRevision,
|
manifest.sourceRevision,
|
||||||
|
milestones.local.variant,
|
||||||
);
|
);
|
||||||
validateMilestoneRecord(
|
validateMilestoneRecord(
|
||||||
manifest.milestones.cluster,
|
manifest.milestones.cluster,
|
||||||
'cluster',
|
'cluster',
|
||||||
manifest.sourceRevision,
|
manifest.sourceRevision,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
const milestoneManifests = {
|
const milestoneManifests = {
|
||||||
local: fileRecord(
|
local: fileRecord(
|
||||||
@@ -365,12 +377,15 @@ function auditAlphaStageIndex(options) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
schema: 'qinglong/alpha-stage-index-audit@v1',
|
schema: 'qinglong/alpha-stage-index-audit@v2',
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
workflowRunId: manifest.workflow.runId,
|
workflowRunId: manifest.workflow.runId,
|
||||||
workflowRunAttempt: manifest.workflow.runAttempt,
|
workflowRunAttempt: manifest.workflow.runAttempt,
|
||||||
profiles: ['edge', 'standalone', 'cluster'],
|
profiles: [
|
||||||
|
...manifest.deploymentSelections.local.profiles,
|
||||||
|
'cluster',
|
||||||
|
],
|
||||||
artifactCount: 10,
|
artifactCount: 10,
|
||||||
compatible: true,
|
compatible: true,
|
||||||
});
|
});
|
||||||
@@ -441,7 +456,7 @@ function finalizeAlphaStageIndex(options) {
|
|||||||
path.join(normalized.outputRoot, FILES.readme),
|
path.join(normalized.outputRoot, FILES.readme),
|
||||||
);
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 2,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_stage_delivery_not_public_release',
|
maturity: 'alpha_stage_delivery_not_public_release',
|
||||||
product: 'qinglong3',
|
product: 'qinglong3',
|
||||||
@@ -461,6 +476,7 @@ function finalizeAlphaStageIndex(options) {
|
|||||||
artifactName: milestoneArtifactName(
|
artifactName: milestoneArtifactName(
|
||||||
normalized.sourceRevision,
|
normalized.sourceRevision,
|
||||||
'local',
|
'local',
|
||||||
|
normalized.local.variant,
|
||||||
),
|
),
|
||||||
schema: normalized.local.schema,
|
schema: normalized.local.schema,
|
||||||
maturity: normalized.local.maturity,
|
maturity: normalized.local.maturity,
|
||||||
@@ -473,6 +489,7 @@ function finalizeAlphaStageIndex(options) {
|
|||||||
artifactName: milestoneArtifactName(
|
artifactName: milestoneArtifactName(
|
||||||
normalized.sourceRevision,
|
normalized.sourceRevision,
|
||||||
'cluster',
|
'cluster',
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
schema: normalized.cluster.schema,
|
schema: normalized.cluster.schema,
|
||||||
maturity: normalized.cluster.maturity,
|
maturity: normalized.cluster.maturity,
|
||||||
@@ -542,7 +559,7 @@ function auditAlphaStageIndexWorkflow(root = DEFAULT_ROOT) {
|
|||||||
`if: ${condition}`,
|
`if: ${condition}`,
|
||||||
' - local-alpha-milestone\n',
|
' - local-alpha-milestone\n',
|
||||||
' - cluster-alpha-milestone\n',
|
' - cluster-alpha-milestone\n',
|
||||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-milestone`,
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`,
|
||||||
`name: ql3-alpha-${'${{ github.sha }}'}-cluster-milestone`,
|
`name: ql3-alpha-${'${{ github.sha }}'}-cluster-milestone`,
|
||||||
'scripts/ql3-alpha-stage-index.cjs',
|
'scripts/ql3-alpha-stage-index.cjs',
|
||||||
'--mode=finalize',
|
'--mode=finalize',
|
||||||
|
|||||||
@@ -76,6 +76,30 @@ const IMAGE_PROFILES = Object.freeze({
|
|||||||
'drizzle-orm': '1.0.0-rc.4',
|
'drizzle-orm': '1.0.0-rc.4',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
'local-console': Object.freeze({
|
||||||
|
id: 'local-console',
|
||||||
|
buildManifestPath: 'deploy/containers/ql3-local-application/package.json',
|
||||||
|
buildLockPath: 'deploy/containers/ql3-local-application/package-lock.json',
|
||||||
|
imageManifestPath:
|
||||||
|
'deploy/containers/ql3-local-application/runtime-dependencies/package.json',
|
||||||
|
imageLockPath:
|
||||||
|
'deploy/containers/ql3-local-application/runtime-dependencies/package-lock.json',
|
||||||
|
internalManifestPaths: Object.freeze([
|
||||||
|
'packages/ql3-runtime-core/package.json',
|
||||||
|
'packages/ql3-local-admin/package.json',
|
||||||
|
'packages/ql3-local-api/package.json',
|
||||||
|
'packages/ql3-local-application/package.json',
|
||||||
|
'packages/ql3-local-command-file/package.json',
|
||||||
|
'packages/ql3-local-execution/package.json',
|
||||||
|
'packages/ql3-local-owner-console/package.json',
|
||||||
|
'packages/ql3-local-process/package.json',
|
||||||
|
'packages/ql3-local-secret/package.json',
|
||||||
|
'packages/ql3-local-sqlite/package.json',
|
||||||
|
]),
|
||||||
|
buildOnlyDependencies: Object.freeze({
|
||||||
|
'drizzle-orm': '1.0.0-rc.4',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
'local-operator': Object.freeze({
|
'local-operator': Object.freeze({
|
||||||
id: 'local-operator',
|
id: 'local-operator',
|
||||||
buildManifestPath: 'deploy/containers/ql3-local-operator/package.json',
|
buildManifestPath: 'deploy/containers/ql3-local-operator/package.json',
|
||||||
@@ -132,7 +156,7 @@ function resolveImageProfile(value = 'control') {
|
|||||||
const profile = IMAGE_PROFILES[value];
|
const profile = IMAGE_PROFILES[value];
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'image profile must be exactly control, control-ai, admin, local, local-operator or worker',
|
'image profile must be exactly control, control-ai, admin, local, local-console, local-operator or worker',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return profile;
|
return profile;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const IMAGES = Object.freeze([
|
|||||||
'control',
|
'control',
|
||||||
'control-ai',
|
'control-ai',
|
||||||
'local',
|
'local',
|
||||||
|
'local-console',
|
||||||
'local-operator',
|
'local-operator',
|
||||||
'worker',
|
'worker',
|
||||||
]);
|
]);
|
||||||
@@ -109,6 +110,7 @@ function auditImageOsVulnerabilityPolicy(
|
|||||||
control: 0,
|
control: 0,
|
||||||
'control-ai': 0,
|
'control-ai': 0,
|
||||||
local: 0,
|
local: 0,
|
||||||
|
'local-console': 0,
|
||||||
'local-operator': 0,
|
'local-operator': 0,
|
||||||
worker: 0,
|
worker: 0,
|
||||||
}),
|
}),
|
||||||
@@ -120,6 +122,7 @@ function auditImageOsVulnerabilityPolicy(
|
|||||||
control: 0,
|
control: 0,
|
||||||
'control-ai': 0,
|
'control-ai': 0,
|
||||||
local: 0,
|
local: 0,
|
||||||
|
'local-console': 0,
|
||||||
'local-operator': 0,
|
'local-operator': 0,
|
||||||
worker: 0,
|
worker: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ const path = require('node:path');
|
|||||||
const {
|
const {
|
||||||
auditLocalAlphaTrialKit,
|
auditLocalAlphaTrialKit,
|
||||||
sha256File,
|
sha256File,
|
||||||
|
VARIANTS,
|
||||||
} = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
} = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
||||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SCHEMA = 'qinglong/alpha-local-milestone@v1';
|
const SCHEMA = 'qinglong/alpha-local-milestone@v2';
|
||||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
const FILES = Object.freeze({
|
const FILES = Object.freeze({
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
@@ -126,8 +127,8 @@ function checksumContents(root, names) {
|
|||||||
.join('\n')}\n`;
|
.join('\n')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function artifactName(sourceRevision, architecture) {
|
function artifactName(sourceRevision, architecture, variant = 'headless') {
|
||||||
return `ql3-alpha-${sourceRevision}-local-${architecture}`;
|
return `ql3-alpha-${sourceRevision}-local-${variant}-${architecture}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateIdentity(options) {
|
function validateIdentity(options) {
|
||||||
@@ -150,6 +151,7 @@ function validateFinalizeOptions(options) {
|
|||||||
const outputRoot = path.resolve(options.outputRoot || '');
|
const outputRoot = path.resolve(options.outputRoot || '');
|
||||||
const parent = path.dirname(outputRoot);
|
const parent = path.dirname(outputRoot);
|
||||||
if (
|
if (
|
||||||
|
!VARIANTS.includes(options.variant) ||
|
||||||
!path.isAbsolute(outputRoot) ||
|
!path.isAbsolute(outputRoot) ||
|
||||||
fs.existsSync(outputRoot) ||
|
fs.existsSync(outputRoot) ||
|
||||||
fs.realpathSync(parent) !== parent
|
fs.realpathSync(parent) !== parent
|
||||||
@@ -179,6 +181,7 @@ function validateFinalizeOptions(options) {
|
|||||||
'milestone README',
|
'milestone README',
|
||||||
),
|
),
|
||||||
sourceRevision: options.sourceRevision,
|
sourceRevision: options.sourceRevision,
|
||||||
|
variant: options.variant,
|
||||||
repository: options.repository,
|
repository: options.repository,
|
||||||
workflowRef: options.workflowRef,
|
workflowRef: options.workflowRef,
|
||||||
workflowSha: options.workflowSha,
|
workflowSha: options.workflowSha,
|
||||||
@@ -196,12 +199,17 @@ function bundleRecord(options, architecture) {
|
|||||||
report.architecture !== architecture ||
|
report.architecture !== architecture ||
|
||||||
report.sourceRevision !== options.sourceRevision ||
|
report.sourceRevision !== options.sourceRevision ||
|
||||||
report.workflowRunId !== options.runId ||
|
report.workflowRunId !== options.runId ||
|
||||||
report.workflowRunAttempt !== options.runAttempt
|
report.workflowRunAttempt !== options.runAttempt ||
|
||||||
|
report.variant !== options.variant
|
||||||
) {
|
) {
|
||||||
fail(`${architecture} trial kit is detached from the milestone run`);
|
fail(`${architecture} trial kit is detached from the milestone run`);
|
||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
artifactName: artifactName(options.sourceRevision, architecture),
|
artifactName: artifactName(
|
||||||
|
options.sourceRevision,
|
||||||
|
architecture,
|
||||||
|
options.variant,
|
||||||
|
),
|
||||||
architecture,
|
architecture,
|
||||||
bundleManifest: fileRecord(
|
bundleManifest: fileRecord(
|
||||||
path.join(bundleRoot, 'manifest.json'),
|
path.join(bundleRoot, 'manifest.json'),
|
||||||
@@ -226,7 +234,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
|||||||
'verificationSha256',
|
'verificationSha256',
|
||||||
]) ||
|
]) ||
|
||||||
record.artifactName !==
|
record.artifactName !==
|
||||||
artifactName(manifest.sourceRevision, architecture) ||
|
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
||||||
record.architecture !== architecture ||
|
record.architecture !== architecture ||
|
||||||
!exactKeys(record.bundleManifest, ['file', 'sha256', 'bytes']) ||
|
!exactKeys(record.bundleManifest, ['file', 'sha256', 'bytes']) ||
|
||||||
record.bundleManifest.file !== 'manifest.json' ||
|
record.bundleManifest.file !== 'manifest.json' ||
|
||||||
@@ -298,16 +306,18 @@ function auditLocalAlphaMilestone(options) {
|
|||||||
'schema',
|
'schema',
|
||||||
'maturity',
|
'maturity',
|
||||||
'product',
|
'product',
|
||||||
|
'variant',
|
||||||
'version',
|
'version',
|
||||||
'sourceRevision',
|
'sourceRevision',
|
||||||
'workflow',
|
'workflow',
|
||||||
'artifacts',
|
'artifacts',
|
||||||
'readme',
|
'readme',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 1 ||
|
manifest.schemaVersion !== 2 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||||
manifest.product !== 'local' ||
|
manifest.product !== 'local' ||
|
||||||
|
!VARIANTS.includes(manifest.variant) ||
|
||||||
typeof manifest.version !== 'string' ||
|
typeof manifest.version !== 'string' ||
|
||||||
manifest.version.length < 3 ||
|
manifest.version.length < 3 ||
|
||||||
manifest.version.length > 64 ||
|
manifest.version.length > 64 ||
|
||||||
@@ -373,9 +383,10 @@ function auditLocalAlphaMilestone(options) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
schema: 'qinglong/alpha-local-milestone-audit@v1',
|
schema: 'qinglong/alpha-local-milestone-audit@v2',
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
|
variant: manifest.variant,
|
||||||
workflowRunId: manifest.workflow.runId,
|
workflowRunId: manifest.workflow.runId,
|
||||||
workflowRunAttempt: manifest.workflow.runAttempt,
|
workflowRunAttempt: manifest.workflow.runAttempt,
|
||||||
architectures: [...ARCHITECTURES],
|
architectures: [...ARCHITECTURES],
|
||||||
@@ -422,10 +433,11 @@ function finalizeLocalAlphaMilestone(options) {
|
|||||||
path.join(normalized.outputRoot, FILES.readme),
|
path.join(normalized.outputRoot, FILES.readme),
|
||||||
);
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 2,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
|
variant: normalized.variant,
|
||||||
version: [...versions][0],
|
version: [...versions][0],
|
||||||
sourceRevision: normalized.sourceRevision,
|
sourceRevision: normalized.sourceRevision,
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -492,6 +504,10 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
|
|||||||
'- local',
|
'- local',
|
||||||
'- cluster',
|
'- cluster',
|
||||||
'- all',
|
'- all',
|
||||||
|
'local_alpha_variant:',
|
||||||
|
'default: headless',
|
||||||
|
'- headless',
|
||||||
|
'- console',
|
||||||
"github.run_id || 'validation'",
|
"github.run_id || 'validation'",
|
||||||
"cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}",
|
"cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}",
|
||||||
];
|
];
|
||||||
@@ -512,12 +528,13 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
|
|||||||
' name: Finalize the Local Alpha milestone',
|
' name: Finalize the Local Alpha milestone',
|
||||||
' needs:',
|
' needs:',
|
||||||
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
|
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
|
||||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-amd64`,
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-amd64`,
|
||||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-arm64`,
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-arm64`,
|
||||||
'scripts/ql3-local-alpha-milestone.cjs',
|
'scripts/ql3-local-alpha-milestone.cjs',
|
||||||
'--mode=finalize',
|
'--mode=finalize',
|
||||||
|
'--variant=${{ inputs.local_alpha_variant }}',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
`name: ql3-alpha-${'${{ github.sha }}'}-local-milestone`,
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`,
|
||||||
'retention-days: 30',
|
'retention-days: 30',
|
||||||
'overwrite: false',
|
'overwrite: false',
|
||||||
];
|
];
|
||||||
@@ -587,6 +604,7 @@ function parseArguments(argv) {
|
|||||||
'run-attempt',
|
'run-attempt',
|
||||||
'run-id',
|
'run-id',
|
||||||
'source-revision',
|
'source-revision',
|
||||||
|
'variant',
|
||||||
'workflow-ref',
|
'workflow-ref',
|
||||||
'workflow-sha',
|
'workflow-sha',
|
||||||
];
|
];
|
||||||
@@ -605,6 +623,7 @@ function parseArguments(argv) {
|
|||||||
},
|
},
|
||||||
readme: path.resolve(values.readme),
|
readme: path.resolve(values.readme),
|
||||||
sourceRevision: values['source-revision'],
|
sourceRevision: values['source-revision'],
|
||||||
|
variant: values.variant,
|
||||||
repository: values.repository,
|
repository: values.repository,
|
||||||
workflowRef: values['workflow-ref'],
|
workflowRef: values['workflow-ref'],
|
||||||
workflowSha: values['workflow-sha'],
|
workflowSha: values['workflow-sha'],
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
|
|||||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v3';
|
const SCHEMA = 'qinglong/alpha-local-trial-kit@v4';
|
||||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v2';
|
||||||
const QUICKSTART_TEMPLATE = path.join(
|
const QUICKSTART_TEMPLATE = path.join(
|
||||||
DEFAULT_ROOT,
|
DEFAULT_ROOT,
|
||||||
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||||
);
|
);
|
||||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
|
const VARIANTS = Object.freeze(['headless', 'console']);
|
||||||
const ARCHIVE_MIN_BYTES = 1024;
|
const ARCHIVE_MIN_BYTES = 1024;
|
||||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||||
const MAX_README_BYTES = 512 * 1024;
|
const MAX_README_BYTES = 512 * 1024;
|
||||||
@@ -43,6 +44,13 @@ const VERIFICATION = Object.freeze({
|
|||||||
standaloneFreshLifecycle: 'passed',
|
standaloneFreshLifecycle: 'passed',
|
||||||
localApiCancellation: 'passed',
|
localApiCancellation: 'passed',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function verificationGates(variant) {
|
||||||
|
return Object.freeze({
|
||||||
|
...VERIFICATION,
|
||||||
|
consoleLiveJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
||||||
|
});
|
||||||
|
}
|
||||||
const WORKFLOW_IDENTITY = Object.freeze({
|
const WORKFLOW_IDENTITY = Object.freeze({
|
||||||
repository: 'whyour/qinglong',
|
repository: 'whyour/qinglong',
|
||||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||||
@@ -162,11 +170,13 @@ function validateImageReference(value, label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeImageInspection(inspection, options) {
|
function normalizeImageInspection(inspection, options) {
|
||||||
const { architecture, reference, revision, role, version } = options;
|
const { architecture, reference, revision, role, variant, version } = options;
|
||||||
const labels = inspection?.Config?.Labels;
|
const labels = inspection?.Config?.Labels;
|
||||||
const expectedTitle =
|
const expectedTitle =
|
||||||
role === 'application'
|
role === 'application'
|
||||||
? 'QingLong 3.0 Local Application'
|
? variant === 'console'
|
||||||
|
? 'QingLong 3.0 Local Console Application'
|
||||||
|
: 'QingLong 3.0 Local Application'
|
||||||
: 'QingLong 3.0 Local Operator';
|
: 'QingLong 3.0 Local Operator';
|
||||||
if (
|
if (
|
||||||
!SHA256_PATTERN.test(inspection?.Id || '') ||
|
!SHA256_PATTERN.test(inspection?.Id || '') ||
|
||||||
@@ -183,8 +193,14 @@ function normalizeImageInspection(inspection, options) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
role === 'application' &&
|
role === 'application' &&
|
||||||
(labels?.['io.qinglong.profile'] !== 'edge,standalone' ||
|
(labels?.['io.qinglong.profile'] !==
|
||||||
labels?.['io.qinglong.ai'] !== 'excluded')
|
(variant === 'console'
|
||||||
|
? 'edge-application-api,standalone-application-api'
|
||||||
|
: 'edge,standalone') ||
|
||||||
|
labels?.['io.qinglong.ai'] !== 'excluded' ||
|
||||||
|
(variant === 'console'
|
||||||
|
? labels?.['io.qinglong.local.console'] !== 'offline-loopback'
|
||||||
|
: labels?.['io.qinglong.local.console'] !== undefined))
|
||||||
) {
|
) {
|
||||||
fail('application image profile is incompatible');
|
fail('application image profile is incompatible');
|
||||||
}
|
}
|
||||||
@@ -259,12 +275,14 @@ function validateVerificationEvidence(document, expected) {
|
|||||||
'version',
|
'version',
|
||||||
'sourceRevision',
|
'sourceRevision',
|
||||||
'architecture',
|
'architecture',
|
||||||
|
'variant',
|
||||||
'applicationImageId',
|
'applicationImageId',
|
||||||
'operatorImageId',
|
'operatorImageId',
|
||||||
]) ||
|
]) ||
|
||||||
document.subject.version !== expected.version ||
|
document.subject.version !== expected.version ||
|
||||||
document.subject.sourceRevision !== expected.sourceRevision ||
|
document.subject.sourceRevision !== expected.sourceRevision ||
|
||||||
document.subject.architecture !== expected.architecture ||
|
document.subject.architecture !== expected.architecture ||
|
||||||
|
document.subject.variant !== expected.variant ||
|
||||||
document.subject.applicationImageId !== expected.applicationImageId ||
|
document.subject.applicationImageId !== expected.applicationImageId ||
|
||||||
document.subject.operatorImageId !== expected.operatorImageId ||
|
document.subject.operatorImageId !== expected.operatorImageId ||
|
||||||
document.subject.applicationImageId === document.subject.operatorImageId ||
|
document.subject.applicationImageId === document.subject.operatorImageId ||
|
||||||
@@ -284,8 +302,9 @@ function validateVerificationEvidence(document, expected) {
|
|||||||
document.workflow.job !== WORKFLOW_IDENTITY.job ||
|
document.workflow.job !== WORKFLOW_IDENTITY.job ||
|
||||||
!DECIMAL_ID_PATTERN.test(document.workflow.runId || '') ||
|
!DECIMAL_ID_PATTERN.test(document.workflow.runId || '') ||
|
||||||
!ATTEMPT_PATTERN.test(document.workflow.runAttempt || '') ||
|
!ATTEMPT_PATTERN.test(document.workflow.runAttempt || '') ||
|
||||||
!exactKeys(document.gates, Object.keys(VERIFICATION)) ||
|
!exactKeys(document.gates, Object.keys(verificationGates(expected.variant))) ||
|
||||||
JSON.stringify(document.gates) !== JSON.stringify(VERIFICATION)
|
JSON.stringify(document.gates) !==
|
||||||
|
JSON.stringify(verificationGates(expected.variant))
|
||||||
) {
|
) {
|
||||||
fail('trial kit verification evidence is incompatible');
|
fail('trial kit verification evidence is incompatible');
|
||||||
}
|
}
|
||||||
@@ -298,6 +317,7 @@ function validateVerificationOptions(options) {
|
|||||||
const parent = path.dirname(output);
|
const parent = path.dirname(output);
|
||||||
if (
|
if (
|
||||||
!ARCHITECTURES.includes(options.architecture) ||
|
!ARCHITECTURES.includes(options.architecture) ||
|
||||||
|
!VARIANTS.includes(options.variant) ||
|
||||||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
||||||
!path.isAbsolute(output) ||
|
!path.isAbsolute(output) ||
|
||||||
fs.existsSync(output) ||
|
fs.existsSync(output) ||
|
||||||
@@ -316,6 +336,7 @@ function validateVerificationOptions(options) {
|
|||||||
root,
|
root,
|
||||||
output,
|
output,
|
||||||
architecture: options.architecture,
|
architecture: options.architecture,
|
||||||
|
variant: options.variant,
|
||||||
sourceRevision: options.sourceRevision,
|
sourceRevision: options.sourceRevision,
|
||||||
applicationImage: validateImageReference(
|
applicationImage: validateImageReference(
|
||||||
options.applicationImage,
|
options.applicationImage,
|
||||||
@@ -343,6 +364,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
|||||||
reference: normalized.applicationImage,
|
reference: normalized.applicationImage,
|
||||||
revision: normalized.sourceRevision,
|
revision: normalized.sourceRevision,
|
||||||
role: 'application',
|
role: 'application',
|
||||||
|
variant: normalized.variant,
|
||||||
version: release.version,
|
version: release.version,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -353,6 +375,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
|||||||
reference: normalized.operatorImage,
|
reference: normalized.operatorImage,
|
||||||
revision: normalized.sourceRevision,
|
revision: normalized.sourceRevision,
|
||||||
role: 'operator',
|
role: 'operator',
|
||||||
|
variant: normalized.variant,
|
||||||
version: release.version,
|
version: release.version,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -364,6 +387,7 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
|||||||
version: release.version,
|
version: release.version,
|
||||||
sourceRevision: normalized.sourceRevision,
|
sourceRevision: normalized.sourceRevision,
|
||||||
architecture: normalized.architecture,
|
architecture: normalized.architecture,
|
||||||
|
variant: normalized.variant,
|
||||||
applicationImageId: application.id,
|
applicationImageId: application.id,
|
||||||
operatorImageId: operator.id,
|
operatorImageId: operator.id,
|
||||||
},
|
},
|
||||||
@@ -376,15 +400,17 @@ function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
|
|||||||
runId: normalized.runId,
|
runId: normalized.runId,
|
||||||
runAttempt: normalized.runAttempt,
|
runAttempt: normalized.runAttempt,
|
||||||
},
|
},
|
||||||
gates: { ...VERIFICATION },
|
gates: { ...verificationGates(normalized.variant) },
|
||||||
};
|
};
|
||||||
validateVerificationEvidence(evidence, evidence.subject);
|
validateVerificationEvidence(evidence, evidence.subject);
|
||||||
writeExclusive(normalized.output, `${JSON.stringify(evidence, null, 2)}\n`);
|
writeExclusive(normalized.output, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||||
return evidence;
|
return evidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
function archiveName(architecture) {
|
function archiveName(architecture, variant = 'headless') {
|
||||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
return variant === 'console'
|
||||||
|
? `qinglong3-local-console-trial-kit-${architecture}.docker.tar`
|
||||||
|
: `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderQuickstart(identity) {
|
function renderQuickstart(identity) {
|
||||||
@@ -404,6 +430,7 @@ function renderQuickstart(identity) {
|
|||||||
'@@ARCHITECTURE@@': identity.architecture,
|
'@@ARCHITECTURE@@': identity.architecture,
|
||||||
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
||||||
'@@ARCHIVE@@': identity.archive.file,
|
'@@ARCHIVE@@': identity.archive.file,
|
||||||
|
'@@VARIANT@@': identity.variant,
|
||||||
});
|
});
|
||||||
let rendered = template;
|
let rendered = template;
|
||||||
for (const [token, value] of Object.entries(replacements)) {
|
for (const [token, value] of Object.entries(replacements)) {
|
||||||
@@ -442,6 +469,7 @@ function validateCreateOptions(options) {
|
|||||||
const parent = path.dirname(outputRoot);
|
const parent = path.dirname(outputRoot);
|
||||||
if (
|
if (
|
||||||
!ARCHITECTURES.includes(options.architecture) ||
|
!ARCHITECTURES.includes(options.architecture) ||
|
||||||
|
!VARIANTS.includes(options.variant) ||
|
||||||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
||||||
!path.isAbsolute(outputRoot) ||
|
!path.isAbsolute(outputRoot) ||
|
||||||
fs.existsSync(outputRoot) ||
|
fs.existsSync(outputRoot) ||
|
||||||
@@ -453,6 +481,7 @@ function validateCreateOptions(options) {
|
|||||||
root,
|
root,
|
||||||
outputRoot,
|
outputRoot,
|
||||||
architecture: options.architecture,
|
architecture: options.architecture,
|
||||||
|
variant: options.variant,
|
||||||
sourceRevision: options.sourceRevision,
|
sourceRevision: options.sourceRevision,
|
||||||
applicationImage: validateImageReference(
|
applicationImage: validateImageReference(
|
||||||
options.applicationImage,
|
options.applicationImage,
|
||||||
@@ -501,7 +530,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
);
|
);
|
||||||
validateSbom(applicationSbom, {
|
validateSbom(applicationSbom, {
|
||||||
root: normalized.root,
|
root: normalized.root,
|
||||||
profile: 'local',
|
profile: normalized.variant === 'console' ? 'local-console' : 'local',
|
||||||
version: release.version,
|
version: release.version,
|
||||||
});
|
});
|
||||||
validateSbom(operatorSbom, {
|
validateSbom(operatorSbom, {
|
||||||
@@ -516,6 +545,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
reference: normalized.applicationImage,
|
reference: normalized.applicationImage,
|
||||||
revision: normalized.sourceRevision,
|
revision: normalized.sourceRevision,
|
||||||
role: 'application',
|
role: 'application',
|
||||||
|
variant: normalized.variant,
|
||||||
version: release.version,
|
version: release.version,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -526,6 +556,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
reference: normalized.operatorImage,
|
reference: normalized.operatorImage,
|
||||||
revision: normalized.sourceRevision,
|
revision: normalized.sourceRevision,
|
||||||
role: 'operator',
|
role: 'operator',
|
||||||
|
variant: normalized.variant,
|
||||||
version: release.version,
|
version: release.version,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -534,6 +565,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
version: release.version,
|
version: release.version,
|
||||||
sourceRevision: normalized.sourceRevision,
|
sourceRevision: normalized.sourceRevision,
|
||||||
architecture: normalized.architecture,
|
architecture: normalized.architecture,
|
||||||
|
variant: normalized.variant,
|
||||||
applicationImageId: application.id,
|
applicationImageId: application.id,
|
||||||
operatorImageId: operator.id,
|
operatorImageId: operator.id,
|
||||||
});
|
});
|
||||||
@@ -542,7 +574,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
try {
|
try {
|
||||||
fs.mkdirSync(normalized.outputRoot, { mode: 0o700 });
|
fs.mkdirSync(normalized.outputRoot, { mode: 0o700 });
|
||||||
created = true;
|
created = true;
|
||||||
const archive = archiveName(normalized.architecture);
|
const archive = archiveName(normalized.architecture, normalized.variant);
|
||||||
const archivePath = path.join(normalized.outputRoot, archive);
|
const archivePath = path.join(normalized.outputRoot, archive);
|
||||||
saveImages(
|
saveImages(
|
||||||
[normalized.applicationImage, normalized.operatorImage],
|
[normalized.applicationImage, normalized.operatorImage],
|
||||||
@@ -576,6 +608,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
const manifestIdentity = {
|
const manifestIdentity = {
|
||||||
sourceRevision: normalized.sourceRevision,
|
sourceRevision: normalized.sourceRevision,
|
||||||
architecture: normalized.architecture,
|
architecture: normalized.architecture,
|
||||||
|
variant: normalized.variant,
|
||||||
archive: { file: archive },
|
archive: { file: archive },
|
||||||
images: { application, operator },
|
images: { application, operator },
|
||||||
};
|
};
|
||||||
@@ -585,13 +618,14 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
0o700,
|
0o700,
|
||||||
);
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 4,
|
schemaVersion: 5,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
version: release.version,
|
version: release.version,
|
||||||
sourceRevision: normalized.sourceRevision,
|
sourceRevision: normalized.sourceRevision,
|
||||||
architecture: normalized.architecture,
|
architecture: normalized.architecture,
|
||||||
|
variant: normalized.variant,
|
||||||
archive: fileRecord(normalized.outputRoot, archive),
|
archive: fileRecord(normalized.outputRoot, archive),
|
||||||
images: { application, operator },
|
images: { application, operator },
|
||||||
sboms: {
|
sboms: {
|
||||||
@@ -679,6 +713,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'version',
|
'version',
|
||||||
'sourceRevision',
|
'sourceRevision',
|
||||||
'architecture',
|
'architecture',
|
||||||
|
'variant',
|
||||||
'archive',
|
'archive',
|
||||||
'images',
|
'images',
|
||||||
'sboms',
|
'sboms',
|
||||||
@@ -686,13 +721,14 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'readme',
|
'readme',
|
||||||
'verification',
|
'verification',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 4 ||
|
manifest.schemaVersion !== 5 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||||
manifest.product !== 'local' ||
|
manifest.product !== 'local' ||
|
||||||
typeof manifest.version !== 'string' ||
|
typeof manifest.version !== 'string' ||
|
||||||
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
||||||
!ARCHITECTURES.includes(manifest.architecture) ||
|
!ARCHITECTURES.includes(manifest.architecture) ||
|
||||||
|
!VARIANTS.includes(manifest.variant) ||
|
||||||
!exactKeys(manifest.images, ['application', 'operator']) ||
|
!exactKeys(manifest.images, ['application', 'operator']) ||
|
||||||
!exactKeys(manifest.sboms, ['application', 'operator'])
|
!exactKeys(manifest.sboms, ['application', 'operator'])
|
||||||
) {
|
) {
|
||||||
@@ -703,7 +739,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
if (manifest.images.application.id === manifest.images.operator.id) {
|
if (manifest.images.application.id === manifest.images.operator.id) {
|
||||||
fail('trial kit images must be distinct');
|
fail('trial kit images must be distinct');
|
||||||
}
|
}
|
||||||
const expectedArchive = archiveName(manifest.architecture);
|
const expectedArchive = archiveName(manifest.architecture, manifest.variant);
|
||||||
validateFileRecord(manifest.archive, expectedArchive, bundleRoot);
|
validateFileRecord(manifest.archive, expectedArchive, bundleRoot);
|
||||||
if (manifest.archive.bytes < ARCHIVE_MIN_BYTES) {
|
if (manifest.archive.bytes < ARCHIVE_MIN_BYTES) {
|
||||||
fail('Docker archive is unexpectedly small');
|
fail('Docker archive is unexpectedly small');
|
||||||
@@ -738,7 +774,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
path.join(bundleRoot, FILES.applicationSbom),
|
path.join(bundleRoot, FILES.applicationSbom),
|
||||||
'application SBOM',
|
'application SBOM',
|
||||||
),
|
),
|
||||||
'local',
|
manifest.variant === 'console' ? 'local-console' : 'local',
|
||||||
manifest.version,
|
manifest.version,
|
||||||
);
|
);
|
||||||
validateOfflineSbom(
|
validateOfflineSbom(
|
||||||
@@ -755,6 +791,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
architecture: manifest.architecture,
|
architecture: manifest.architecture,
|
||||||
|
variant: manifest.variant,
|
||||||
applicationImageId: manifest.images.application.id,
|
applicationImageId: manifest.images.application.id,
|
||||||
operatorImageId: manifest.images.operator.id,
|
operatorImageId: manifest.images.operator.id,
|
||||||
},
|
},
|
||||||
@@ -800,10 +837,11 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
schema: 'qinglong/alpha-local-trial-kit-audit@v1',
|
schema: 'qinglong/alpha-local-trial-kit-audit@v2',
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
architecture: manifest.architecture,
|
architecture: manifest.architecture,
|
||||||
|
variant: manifest.variant,
|
||||||
archiveSha256: manifest.archive.sha256,
|
archiveSha256: manifest.archive.sha256,
|
||||||
applicationImageId: manifest.images.application.id,
|
applicationImageId: manifest.images.application.id,
|
||||||
operatorImageId: manifest.images.operator.id,
|
operatorImageId: manifest.images.operator.id,
|
||||||
@@ -845,6 +883,7 @@ function parseArguments(argv) {
|
|||||||
'run-attempt',
|
'run-attempt',
|
||||||
'run-id',
|
'run-id',
|
||||||
'source-revision',
|
'source-revision',
|
||||||
|
'variant',
|
||||||
'workflow-ref',
|
'workflow-ref',
|
||||||
'workflow-sha',
|
'workflow-sha',
|
||||||
];
|
];
|
||||||
@@ -857,6 +896,7 @@ function parseArguments(argv) {
|
|||||||
mode: 'record-verification',
|
mode: 'record-verification',
|
||||||
output: path.resolve(values.output),
|
output: path.resolve(values.output),
|
||||||
architecture: values.architecture,
|
architecture: values.architecture,
|
||||||
|
variant: values.variant,
|
||||||
sourceRevision: values['source-revision'],
|
sourceRevision: values['source-revision'],
|
||||||
applicationImage: values['application-image'],
|
applicationImage: values['application-image'],
|
||||||
operatorImage: values['operator-image'],
|
operatorImage: values['operator-image'],
|
||||||
@@ -880,6 +920,7 @@ function parseArguments(argv) {
|
|||||||
'output',
|
'output',
|
||||||
'readme',
|
'readme',
|
||||||
'source-revision',
|
'source-revision',
|
||||||
|
'variant',
|
||||||
'verification-evidence',
|
'verification-evidence',
|
||||||
];
|
];
|
||||||
if (
|
if (
|
||||||
@@ -891,6 +932,7 @@ function parseArguments(argv) {
|
|||||||
mode: 'create',
|
mode: 'create',
|
||||||
outputRoot: path.resolve(values.output),
|
outputRoot: path.resolve(values.output),
|
||||||
architecture: values.architecture,
|
architecture: values.architecture,
|
||||||
|
variant: values.variant,
|
||||||
sourceRevision: values['source-revision'],
|
sourceRevision: values['source-revision'],
|
||||||
applicationImage: values['application-image'],
|
applicationImage: values['application-image'],
|
||||||
operatorImage: values['operator-image'],
|
operatorImage: values['operator-image'],
|
||||||
@@ -933,6 +975,7 @@ module.exports = Object.freeze({
|
|||||||
SCHEMA,
|
SCHEMA,
|
||||||
VERIFICATION,
|
VERIFICATION,
|
||||||
VERIFICATION_SCHEMA,
|
VERIFICATION_SCHEMA,
|
||||||
|
VARIANTS,
|
||||||
archiveName,
|
archiveName,
|
||||||
auditLocalAlphaTrialKit,
|
auditLocalAlphaTrialKit,
|
||||||
createLocalAlphaTrialKit,
|
createLocalAlphaTrialKit,
|
||||||
|
|||||||
@@ -20,27 +20,30 @@ function fail(message) {
|
|||||||
function argumentsFrom(argv) {
|
function argumentsFrom(argv) {
|
||||||
const values = {};
|
const values = {};
|
||||||
for (const argument of argv) {
|
for (const argument of argv) {
|
||||||
const match = /^--(application-image|operator-image|profile)=(.+)$/u.exec(
|
const match =
|
||||||
argument,
|
/^--(application-image|operator-image|profile|variant)=(.+)$/u.exec(
|
||||||
);
|
argument,
|
||||||
|
);
|
||||||
if (!match || Object.hasOwn(values, match[1]))
|
if (!match || Object.hasOwn(values, match[1]))
|
||||||
fail('arguments are invalid');
|
fail('arguments are invalid');
|
||||||
values[match[1]] = match[2];
|
values[match[1]] = match[2];
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
Object.keys(values).length !== 3 ||
|
Object.keys(values).length !== 4 ||
|
||||||
!IMAGE_PATTERN.test(values['application-image'] ?? '') ||
|
!IMAGE_PATTERN.test(values['application-image'] ?? '') ||
|
||||||
!IMAGE_PATTERN.test(values['operator-image'] ?? '') ||
|
!IMAGE_PATTERN.test(values['operator-image'] ?? '') ||
|
||||||
!['edge', 'standalone'].includes(values.profile)
|
!['edge', 'standalone'].includes(values.profile) ||
|
||||||
|
!['headless', 'console'].includes(values.variant)
|
||||||
) {
|
) {
|
||||||
fail(
|
fail(
|
||||||
'usage: --application-image=... --operator-image=... --profile=edge|standalone',
|
'usage: --application-image=... --operator-image=... --profile=edge|standalone --variant=headless|console',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
applicationImage: values['application-image'],
|
applicationImage: values['application-image'],
|
||||||
operatorImage: values['operator-image'],
|
operatorImage: values['operator-image'],
|
||||||
profile: values.profile,
|
profile: values.profile,
|
||||||
|
variant: values.variant,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,41 +64,54 @@ function docker(args, options = {}) {
|
|||||||
return result.stdout.trim();
|
return result.stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function inspectImages(applicationImage, operatorImage) {
|
function inspectImages(applicationImage, operatorImage, variant) {
|
||||||
const application = docker([
|
const application = JSON.parse(
|
||||||
'image',
|
docker(['image', 'inspect', applicationImage]),
|
||||||
'inspect',
|
)[0];
|
||||||
'--format',
|
const operator = JSON.parse(docker(['image', 'inspect', operatorImage]))[0];
|
||||||
'{{.Id}} {{.Architecture}} {{.Config.User}} {{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "org.opencontainers.image.version"}}',
|
const applicationLabels = application?.Config?.Labels;
|
||||||
applicationImage,
|
const operatorLabels = operator?.Config?.Labels;
|
||||||
]).split(' ');
|
const expectedTitle =
|
||||||
const operator = docker([
|
variant === 'console'
|
||||||
'image',
|
? 'QingLong 3.0 Local Console Application'
|
||||||
'inspect',
|
: 'QingLong 3.0 Local Application';
|
||||||
'--format',
|
const expectedProfile =
|
||||||
'{{.Id}} {{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.lifecycle"}} {{index .Config.Labels "io.qinglong.authority"}} {{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "org.opencontainers.image.version"}}',
|
variant === 'console'
|
||||||
operatorImage,
|
? 'edge-application-api,standalone-application-api'
|
||||||
]).split(' ');
|
: 'edge,standalone';
|
||||||
if (
|
if (
|
||||||
!/^sha256:[0-9a-f]{64}$/u.test(application[0] ?? '') ||
|
!/^sha256:[0-9a-f]{64}$/u.test(application?.Id ?? '') ||
|
||||||
!/^sha256:[0-9a-f]{64}$/u.test(operator[0] ?? '') ||
|
!/^sha256:[0-9a-f]{64}$/u.test(operator?.Id ?? '') ||
|
||||||
application[1] !== operator[1] ||
|
application?.Architecture !== operator?.Architecture ||
|
||||||
!['amd64', 'arm64'].includes(application[1]) ||
|
!['amd64', 'arm64'].includes(application?.Architecture) ||
|
||||||
application[2] !== '65532:65532' ||
|
application?.Config?.User !== '65532:65532' ||
|
||||||
operator[2] !== '65532:65532' ||
|
operator?.Config?.User !== '65532:65532' ||
|
||||||
operator[3] !== 'short-lived' ||
|
applicationLabels?.['org.opencontainers.image.title'] !== expectedTitle ||
|
||||||
operator[4] !== 'local-owner-management' ||
|
applicationLabels?.['io.qinglong.profile'] !== expectedProfile ||
|
||||||
!/^[0-9a-f]{40}$/u.test(application[3] ?? '') ||
|
applicationLabels?.['io.qinglong.ai'] !== 'excluded' ||
|
||||||
application[3] !== operator[5] ||
|
(variant === 'console'
|
||||||
application[4] !== operator[6] ||
|
? applicationLabels?.['io.qinglong.local.console'] !==
|
||||||
!/^3\.0\.0-alpha\.[0-9]+$/u.test(application[4] ?? '')
|
'offline-loopback'
|
||||||
|
: applicationLabels?.['io.qinglong.local.console'] !== undefined) ||
|
||||||
|
operatorLabels?.['io.qinglong.lifecycle'] !== 'short-lived' ||
|
||||||
|
operatorLabels?.['io.qinglong.authority'] !== 'local-owner-management' ||
|
||||||
|
!/^[0-9a-f]{40}$/u.test(
|
||||||
|
applicationLabels?.['org.opencontainers.image.revision'] ?? '',
|
||||||
|
) ||
|
||||||
|
applicationLabels?.['org.opencontainers.image.revision'] !==
|
||||||
|
operatorLabels?.['org.opencontainers.image.revision'] ||
|
||||||
|
applicationLabels?.['org.opencontainers.image.version'] !==
|
||||||
|
operatorLabels?.['org.opencontainers.image.version'] ||
|
||||||
|
!/^3\.0\.0-alpha\.[0-9]+$/u.test(
|
||||||
|
applicationLabels?.['org.opencontainers.image.version'] ?? '',
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
fail('image identity, architecture or authority labels drifted');
|
fail('image identity, architecture or authority labels drifted');
|
||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
architecture: application[1],
|
architecture: application.Architecture,
|
||||||
applicationId: application[0],
|
applicationId: application.Id,
|
||||||
operatorId: operator[0],
|
operatorId: operator.Id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +342,55 @@ function writeApplicationConfig(state) {
|
|||||||
},
|
},
|
||||||
ai: { deployment: 'excluded' },
|
ai: { deployment: 'excluded' },
|
||||||
});
|
});
|
||||||
|
if (state.variant === 'console') {
|
||||||
|
writePrivateJson(path.join(state.root, 'local-api.json'), {
|
||||||
|
schema: 'qinglong/local-api-process@v1',
|
||||||
|
deploymentRoot: '/var/lib/qinglong3',
|
||||||
|
applicationConfigFilePath:
|
||||||
|
'/var/lib/qinglong3/local-application.json',
|
||||||
|
ownerPepperKeyringDirectory: '/var/lib/qinglong3/owner-peppers',
|
||||||
|
listener: { host: '127.0.0.1', port: 5700 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(milliseconds) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consoleSurfaceContract() {
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const root = await fetch('http://127.0.0.1:5700/', {
|
||||||
|
redirect: 'manual',
|
||||||
|
signal: AbortSignal.timeout(2_000),
|
||||||
|
});
|
||||||
|
const api = await fetch(
|
||||||
|
'http://127.0.0.1:5700/api/v3/projects/default/tasks',
|
||||||
|
{
|
||||||
|
redirect: 'manual',
|
||||||
|
signal: AbortSignal.timeout(2_000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await root.body?.cancel();
|
||||||
|
await api.body?.cancel();
|
||||||
|
if (root.status !== 200 || api.status !== 401) {
|
||||||
|
fail(
|
||||||
|
`Console HTTP contract drifted: root=${root.status}, unauthenticatedApi=${api.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Object.freeze({
|
||||||
|
listener: '127.0.0.1:5700',
|
||||||
|
rootStatus: 200,
|
||||||
|
unauthenticatedApiStatus: 401,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError || new Error('Console listener did not become ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runApplication(state) {
|
async function runApplication(state) {
|
||||||
@@ -344,7 +409,7 @@ async function runApplication(state) {
|
|||||||
'--user',
|
'--user',
|
||||||
`${state.uid}:${state.gid}`,
|
`${state.uid}:${state.gid}`,
|
||||||
'--network',
|
'--network',
|
||||||
'none',
|
state.variant === 'console' ? 'host' : 'none',
|
||||||
'--cap-drop',
|
'--cap-drop',
|
||||||
'ALL',
|
'ALL',
|
||||||
'--security-opt',
|
'--security-opt',
|
||||||
@@ -363,13 +428,18 @@ async function runApplication(state) {
|
|||||||
`${state.root}:/var/lib/qinglong3`,
|
`${state.root}:/var/lib/qinglong3`,
|
||||||
state.applicationImage,
|
state.applicationImage,
|
||||||
'--config',
|
'--config',
|
||||||
'/var/lib/qinglong3/local-application.json',
|
state.variant === 'console'
|
||||||
|
? '/var/lib/qinglong3/local-api.json'
|
||||||
|
: '/var/lib/qinglong3/local-application.json',
|
||||||
],
|
],
|
||||||
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
||||||
);
|
);
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
let active = false;
|
let active = false;
|
||||||
|
let surfaceError;
|
||||||
|
let surface = Object.freeze({ status: 'not_applicable' });
|
||||||
|
let surfacePromise = Promise.resolve();
|
||||||
child.stdout.setEncoding('utf8');
|
child.stdout.setEncoding('utf8');
|
||||||
child.stderr.setEncoding('utf8');
|
child.stderr.setEncoding('utf8');
|
||||||
child.stdout.on('data', (chunk) => {
|
child.stdout.on('data', (chunk) => {
|
||||||
@@ -385,7 +455,17 @@ async function runApplication(state) {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
active = true;
|
active = true;
|
||||||
docker(['stop', '--time', '30', name]);
|
surfacePromise = (async () => {
|
||||||
|
try {
|
||||||
|
if (state.variant === 'console') {
|
||||||
|
surface = await consoleSurfaceContract();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
surfaceError = error;
|
||||||
|
} finally {
|
||||||
|
docker(['stop', '--time', '30', name]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
child.stderr.on('data', (chunk) => {
|
child.stderr.on('data', (chunk) => {
|
||||||
@@ -403,6 +483,8 @@ async function runApplication(state) {
|
|||||||
resolve({ code, signal });
|
resolve({ code, signal });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await surfacePromise;
|
||||||
|
if (surfaceError) throw surfaceError;
|
||||||
const events = stdout
|
const events = stdout
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@@ -426,7 +508,7 @@ async function runApplication(state) {
|
|||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Object.freeze({ active: true, gracefulStop: true });
|
return Object.freeze({ active: true, gracefulStop: true, surface });
|
||||||
} finally {
|
} finally {
|
||||||
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
|
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
|
||||||
}
|
}
|
||||||
@@ -441,7 +523,11 @@ async function main() {
|
|||||||
fail('a POSIX identity is required');
|
fail('a POSIX identity is required');
|
||||||
}
|
}
|
||||||
const options = argumentsFrom(process.argv.slice(2));
|
const options = argumentsFrom(process.argv.slice(2));
|
||||||
const images = inspectImages(options.applicationImage, options.operatorImage);
|
const images = inspectImages(
|
||||||
|
options.applicationImage,
|
||||||
|
options.operatorImage,
|
||||||
|
options.variant,
|
||||||
|
);
|
||||||
const root = fs.realpathSync(
|
const root = fs.realpathSync(
|
||||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
|
||||||
);
|
);
|
||||||
@@ -479,8 +565,9 @@ async function main() {
|
|||||||
fail('durable SQLite result is invalid');
|
fail('durable SQLite result is invalid');
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${JSON.stringify({
|
`${JSON.stringify({
|
||||||
schemaVersion: 1,
|
schemaVersion: 2,
|
||||||
schema: 'qinglong/local-alpha-trial-kit-live@v1',
|
schema: 'qinglong/local-alpha-trial-kit-live@v2',
|
||||||
|
variant: options.variant,
|
||||||
profile: options.profile,
|
profile: options.profile,
|
||||||
architecture: images.architecture,
|
architecture: images.architecture,
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const MAX_FILES = 640;
|
||||||
|
const MAX_BYTES = 6 * 1024 * 1024;
|
||||||
|
const EXPECTED_PACKAGES = Object.freeze([
|
||||||
|
'@qinglong/local-admin',
|
||||||
|
'@qinglong/local-api',
|
||||||
|
'@qinglong/local-application',
|
||||||
|
'@qinglong/local-command-file',
|
||||||
|
'@qinglong/local-execution',
|
||||||
|
'@qinglong/local-owner-console',
|
||||||
|
'@qinglong/local-process',
|
||||||
|
'@qinglong/local-secret',
|
||||||
|
'@qinglong/local-sqlite',
|
||||||
|
'@qinglong/runtime-core',
|
||||||
|
'croner',
|
||||||
|
'semver',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
throw new Error(`QingLong local Console image inventory failed: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryRoot(argv) {
|
||||||
|
if (argv.length !== 1 || !argv[0].startsWith('--inventory-root=')) {
|
||||||
|
fail('usage: --inventory-root=/absolute/node_modules');
|
||||||
|
}
|
||||||
|
const root = argv[0].slice('--inventory-root='.length);
|
||||||
|
if (
|
||||||
|
!path.isAbsolute(root) ||
|
||||||
|
path.normalize(root) !== root ||
|
||||||
|
root === path.parse(root).root
|
||||||
|
) {
|
||||||
|
fail('inventory root is invalid');
|
||||||
|
}
|
||||||
|
const stat = fs.lstatSync(root);
|
||||||
|
if (
|
||||||
|
!stat.isDirectory() ||
|
||||||
|
stat.isSymbolicLink() ||
|
||||||
|
fs.realpathSync(root) !== root
|
||||||
|
) {
|
||||||
|
fail('inventory root must be a canonical directory');
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageNames(root) {
|
||||||
|
const result = [];
|
||||||
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||||
|
if (entry.name.startsWith('.')) continue;
|
||||||
|
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
||||||
|
fail(`unexpected root entry ${entry.name}`);
|
||||||
|
}
|
||||||
|
if (!entry.name.startsWith('@')) {
|
||||||
|
result.push(entry.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scope = path.join(root, entry.name);
|
||||||
|
for (const child of fs.readdirSync(scope, { withFileTypes: true })) {
|
||||||
|
if (
|
||||||
|
child.name.startsWith('.') ||
|
||||||
|
!child.isDirectory() ||
|
||||||
|
child.isSymbolicLink()
|
||||||
|
) {
|
||||||
|
fail(`unexpected scoped entry ${entry.name}/${child.name}`);
|
||||||
|
}
|
||||||
|
result.push(`${entry.name}/${child.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage(root) {
|
||||||
|
const pending = [root];
|
||||||
|
let files = 0;
|
||||||
|
let bytes = 0;
|
||||||
|
while (pending.length > 0) {
|
||||||
|
const directory = pending.pop();
|
||||||
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const filePath = path.join(directory, entry.name);
|
||||||
|
const stat = fs.lstatSync(filePath);
|
||||||
|
if (stat.isSymbolicLink()) fail('runtime inventory contains a symlink');
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
pending.push(filePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!stat.isFile()) fail('runtime inventory contains a special file');
|
||||||
|
files += 1;
|
||||||
|
bytes += stat.size;
|
||||||
|
if (files > MAX_FILES) fail('runtime file budget exceeded');
|
||||||
|
if (bytes > MAX_BYTES) fail('runtime byte budget exceeded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.freeze({ files, bytes });
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const root = inventoryRoot(process.argv.slice(2));
|
||||||
|
const packages = packageNames(root);
|
||||||
|
if (JSON.stringify(packages) !== JSON.stringify(EXPECTED_PACKAGES)) {
|
||||||
|
fail(`package closure drifted: ${packages.join(',')}`);
|
||||||
|
}
|
||||||
|
for (const packageName of packages) {
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(root, packageName, 'package.json'), 'utf8'),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
manifest.name !== packageName ||
|
||||||
|
typeof manifest.version !== 'string'
|
||||||
|
) {
|
||||||
|
fail(`package identity drifted: ${packageName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const measured = usage(root);
|
||||||
|
process.stdout.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
image: 'local-console',
|
||||||
|
packages,
|
||||||
|
packageCount: packages.length,
|
||||||
|
files: measured.files,
|
||||||
|
bytes: measured.bytes,
|
||||||
|
maxFiles: MAX_FILES,
|
||||||
|
maxBytes: MAX_BYTES,
|
||||||
|
ai: 'excluded',
|
||||||
|
listener: 'loopback-only',
|
||||||
|
compatible: true,
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(
|
||||||
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -29,6 +29,18 @@ const RUNTIME_DEPENDENCIES = Object.freeze({
|
|||||||
});
|
});
|
||||||
const BUILD_PACKAGES = Object.freeze([
|
const BUILD_PACKAGES = Object.freeze([
|
||||||
'ql3-ai',
|
'ql3-ai',
|
||||||
|
'ql3-local-admin',
|
||||||
|
'ql3-local-api',
|
||||||
|
'ql3-local-application',
|
||||||
|
'ql3-local-command-file',
|
||||||
|
'ql3-local-execution',
|
||||||
|
'ql3-local-owner-console',
|
||||||
|
'ql3-local-process',
|
||||||
|
'ql3-local-secret',
|
||||||
|
'ql3-local-sqlite',
|
||||||
|
'ql3-runtime-core',
|
||||||
|
]);
|
||||||
|
const RUNTIME_PACKAGES = Object.freeze([
|
||||||
'ql3-local-admin',
|
'ql3-local-admin',
|
||||||
'ql3-local-application',
|
'ql3-local-application',
|
||||||
'ql3-local-command-file',
|
'ql3-local-command-file',
|
||||||
@@ -38,8 +50,8 @@ const BUILD_PACKAGES = Object.freeze([
|
|||||||
'ql3-local-sqlite',
|
'ql3-local-sqlite',
|
||||||
'ql3-runtime-core',
|
'ql3-runtime-core',
|
||||||
]);
|
]);
|
||||||
const RUNTIME_PACKAGES = Object.freeze(
|
const CONSOLE_RUNTIME_PACKAGES = Object.freeze(
|
||||||
BUILD_PACKAGES.filter((name) => name !== 'ql3-ai'),
|
[...RUNTIME_PACKAGES, 'ql3-local-api', 'ql3-local-owner-console'].sort(),
|
||||||
);
|
);
|
||||||
|
|
||||||
function readJson(filePath) {
|
function readJson(filePath) {
|
||||||
@@ -174,6 +186,15 @@ function counts(values) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dockerStage(contents, name) {
|
||||||
|
const marker = new RegExp(`^FROM [^\\n]+ AS ${name}$`, 'm');
|
||||||
|
const match = marker.exec(contents);
|
||||||
|
if (!match) return '';
|
||||||
|
const remaining = contents.slice(match.index + match[0].length);
|
||||||
|
const next = /^FROM [^\n]+ AS [a-z0-9-]+$/m.exec(remaining);
|
||||||
|
return next ? remaining.slice(0, next.index) : remaining;
|
||||||
|
}
|
||||||
|
|
||||||
function auditDockerfile(contents, findings) {
|
function auditDockerfile(contents, findings) {
|
||||||
const escapedBuildNodeImage = BUILD_NODE_IMAGE.replace(
|
const escapedBuildNodeImage = BUILD_NODE_IMAGE.replace(
|
||||||
/[.*+?^${}()|[\]\\]/g,
|
/[.*+?^${}()|[\]\\]/g,
|
||||||
@@ -188,7 +209,7 @@ function auditDockerfile(contents, findings) {
|
|||||||
'gm',
|
'gm',
|
||||||
);
|
);
|
||||||
const exactRuntimeBasePattern = new RegExp(
|
const exactRuntimeBasePattern = new RegExp(
|
||||||
`^FROM ${escapedRuntimeNodeImage} AS runtime$`,
|
`^FROM ${escapedRuntimeNodeImage} AS runtime-platform$`,
|
||||||
'gm',
|
'gm',
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
@@ -221,8 +242,13 @@ function auditDockerfile(contents, findings) {
|
|||||||
addFinding(findings, 'UNREVIEWED_RUNTIME_SURFACE');
|
addFinding(findings, 'UNREVIEWED_RUNTIME_SURFACE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceStage = dockerStage(contents, 'workspace');
|
||||||
|
const assembledStage = dockerStage(contents, 'assembled');
|
||||||
|
const consoleAssembledStage = dockerStage(contents, 'console-assembled');
|
||||||
|
const runtimeStage = dockerStage(contents, 'runtime');
|
||||||
|
const consoleRuntimeStage = dockerStage(contents, 'runtime-console');
|
||||||
const buildCopies = captures(
|
const buildCopies = captures(
|
||||||
contents,
|
workspaceStage,
|
||||||
/^COPY packages\/(ql3-[a-z-]+) packages\/\1$/gm,
|
/^COPY packages\/(ql3-[a-z-]+) packages\/\1$/gm,
|
||||||
).sort();
|
).sort();
|
||||||
if (!sameJson(buildCopies, [...BUILD_PACKAGES].sort())) {
|
if (!sameJson(buildCopies, [...BUILD_PACKAGES].sort())) {
|
||||||
@@ -230,7 +256,7 @@ function auditDockerfile(contents, findings) {
|
|||||||
}
|
}
|
||||||
const runtimeCopyCounts = counts(
|
const runtimeCopyCounts = counts(
|
||||||
captures(
|
captures(
|
||||||
contents,
|
assembledStage,
|
||||||
/^COPY --from=workspace \/workspace\/packages\/(ql3-[a-z-]+)\/(?:package\.json|dist) /gm,
|
/^COPY --from=workspace \/workspace\/packages\/(ql3-[a-z-]+)\/(?:package\.json|dist) /gm,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -244,6 +270,28 @@ function auditDockerfile(contents, findings) {
|
|||||||
) {
|
) {
|
||||||
addFinding(findings, 'RUNTIME_INTERNAL_PACKAGE_CLOSURE_DRIFT');
|
addFinding(findings, 'RUNTIME_INTERNAL_PACKAGE_CLOSURE_DRIFT');
|
||||||
}
|
}
|
||||||
|
const consoleRuntimeCopyCounts = counts(
|
||||||
|
captures(
|
||||||
|
consoleAssembledStage,
|
||||||
|
/^COPY --from=workspace \/workspace\/packages\/(ql3-[a-z-]+)\/(?:package\.json|dist) /gm,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!sameJson(
|
||||||
|
sortedObject(consoleRuntimeCopyCounts),
|
||||||
|
sortedObject(
|
||||||
|
Object.fromEntries(
|
||||||
|
CONSOLE_RUNTIME_PACKAGES.map((name) => [name, 2]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ||
|
||||||
|
!consoleAssembledStage.includes(
|
||||||
|
'COPY --from=workspace /workspace/packages/ql3-local-api/assets \\\n' +
|
||||||
|
' node_modules/@qinglong/local-api/assets',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addFinding(findings, 'CONSOLE_RUNTIME_INTERNAL_PACKAGE_CLOSURE_DRIFT');
|
||||||
|
}
|
||||||
if (contents.includes('COPY --from=workspace /workspace/packages/ql3-ai/')) {
|
if (contents.includes('COPY --from=workspace /workspace/packages/ql3-ai/')) {
|
||||||
addFinding(findings, 'AI_PRESENT_IN_RUNTIME_STAGE');
|
addFinding(findings, 'AI_PRESENT_IN_RUNTIME_STAGE');
|
||||||
}
|
}
|
||||||
@@ -264,20 +312,51 @@ function auditDockerfile(contents, findings) {
|
|||||||
addFinding(findings, 'RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED');
|
addFinding(findings, 'RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!contents.includes('USER 65532:65532') ||
|
!consoleAssembledStage.includes(
|
||||||
!contents.includes(
|
'RUN rm -rf node_modules/.bin \\\n' +
|
||||||
|
' && node /tmp/ql3-prune-runtime-artifact.cjs node_modules/@qinglong \\\n' +
|
||||||
|
' @qinglong/local-api/config \\\n' +
|
||||||
|
' @qinglong/local-api/process \\\n' +
|
||||||
|
' --exclude=@qinglong/ai \\\n' +
|
||||||
|
' --retain-js=local-api/assets/console/console.js \\\n' +
|
||||||
|
' && rm /tmp/ql3-prune-runtime-artifact.cjs',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addFinding(findings, 'CONSOLE_RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!runtimeStage.includes('USER 65532:65532') ||
|
||||||
|
!runtimeStage.includes(
|
||||||
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js"]',
|
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js"]',
|
||||||
) ||
|
) ||
|
||||||
!contents.includes('io.qinglong.ai="excluded"') ||
|
!runtimeStage.includes('io.qinglong.ai="excluded"') ||
|
||||||
!contents.includes('io.qinglong.profile="edge,standalone"') ||
|
!runtimeStage.includes('io.qinglong.profile="edge,standalone"') ||
|
||||||
!contents.includes('io.qinglong.local.application-config="2,3,4"') ||
|
!runtimeStage.includes('io.qinglong.local.application-config="2,3,4"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
!runtimeStage.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-contract-max="52"') ||
|
!runtimeStage.includes('io.qinglong.local.sqlite-contract-max="52"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-write-contract="52"') ||
|
!runtimeStage.includes('io.qinglong.local.sqlite-write-contract="52"') ||
|
||||||
!contents.includes('io.qinglong.local.compose-selection="1"')
|
!runtimeStage.includes('io.qinglong.local.compose-selection="1"')
|
||||||
) {
|
) {
|
||||||
addFinding(findings, 'RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
addFinding(findings, 'RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!consoleRuntimeStage.includes('USER 65532:65532') ||
|
||||||
|
!consoleRuntimeStage.includes(
|
||||||
|
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/local-api/dist/cli.js"]',
|
||||||
|
) ||
|
||||||
|
!consoleRuntimeStage.includes(
|
||||||
|
'org.opencontainers.image.title="QingLong 3.0 Local Console Application"',
|
||||||
|
) ||
|
||||||
|
!consoleRuntimeStage.includes(
|
||||||
|
'io.qinglong.profile="edge-application-api,standalone-application-api"',
|
||||||
|
) ||
|
||||||
|
!consoleRuntimeStage.includes(
|
||||||
|
'io.qinglong.local.console="offline-loopback"',
|
||||||
|
) ||
|
||||||
|
!consoleRuntimeStage.includes('io.qinglong.ai="excluded"')
|
||||||
|
) {
|
||||||
|
addFinding(findings, 'CONSOLE_RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function auditWorkflow(contents, findings) {
|
function auditWorkflow(contents, findings) {
|
||||||
@@ -295,6 +374,9 @@ function auditWorkflow(contents, findings) {
|
|||||||
'pnpm audit:local-image:ql3',
|
'pnpm audit:local-image:ql3',
|
||||||
'docker build',
|
'docker build',
|
||||||
'--file deploy/containers/ql3-local-application/Dockerfile',
|
'--file deploy/containers/ql3-local-application/Dockerfile',
|
||||||
|
'--target runtime',
|
||||||
|
'--target runtime-console',
|
||||||
|
'qinglong3-local-console:ci-${{ matrix.image_arch }}',
|
||||||
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1',
|
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1',
|
||||||
'actual="$(docker image inspect --format \'{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}\' "${IMAGE}")"',
|
'actual="$(docker image inspect --format \'{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}\' "${IMAGE}")"',
|
||||||
'io.qinglong.local.application-config',
|
'io.qinglong.local.application-config',
|
||||||
@@ -309,10 +391,18 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--memory=128m',
|
'--memory=128m',
|
||||||
'--pids-limit=64',
|
'--pids-limit=64',
|
||||||
'scripts/ql3-local-image-inventory.cjs',
|
'scripts/ql3-local-image-inventory.cjs',
|
||||||
|
'scripts/ql3-local-console-image-inventory.cjs',
|
||||||
'--inventory-root=/opt/qinglong/node_modules',
|
'--inventory-root=/opt/qinglong/node_modules',
|
||||||
'node ../../scripts/ql3-build-package-closure.cjs',
|
'node ../../scripts/ql3-build-package-closure.cjs',
|
||||||
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=edge',
|
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=edge',
|
||||||
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=standalone',
|
'node scripts/ql3-local-image-live-contract.cjs --image="${IMAGE}" --profile=standalone',
|
||||||
|
'node scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
||||||
|
'--variant=headless',
|
||||||
|
'--variant=console',
|
||||||
|
'--image=local-console',
|
||||||
|
'io.qinglong.local.console',
|
||||||
|
'--variant="${TRIAL_VARIANT}"',
|
||||||
|
'inputs.local_alpha_variant',
|
||||||
];
|
];
|
||||||
for (const value of required) {
|
for (const value of required) {
|
||||||
if (!job.includes(value)) {
|
if (!job.includes(value)) {
|
||||||
@@ -356,6 +446,14 @@ function auditLocalImageContract(root) {
|
|||||||
...Object.keys(RUNTIME_DEPENDENCIES),
|
...Object.keys(RUNTIME_DEPENDENCIES),
|
||||||
].sort(),
|
].sort(),
|
||||||
),
|
),
|
||||||
|
consoleRuntimePackages: Object.freeze(
|
||||||
|
[
|
||||||
|
...CONSOLE_RUNTIME_PACKAGES.map(
|
||||||
|
(name) => `@qinglong/${name.slice(4)}`,
|
||||||
|
),
|
||||||
|
...Object.keys(RUNTIME_DEPENDENCIES),
|
||||||
|
].sort(),
|
||||||
|
),
|
||||||
findings: Object.freeze(findings),
|
findings: Object.freeze(findings),
|
||||||
compatible: findings.length === 0,
|
compatible: findings.length === 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,10 +199,10 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--mode=record-verification',
|
'--mode=record-verification',
|
||||||
'--mode=create',
|
'--mode=create',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
'/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
'sh "${BUNDLE_ROOT}/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
||||||
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
||||||
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
||||||
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
'--application-sbom="${APPLICATION_SBOM}"',
|
||||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
||||||
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
||||||
'--readme=docs/operations/ql3-local-alpha-trial-kit.md',
|
'--readme=docs/operations/ql3-local-alpha-trial-kit.md',
|
||||||
@@ -213,6 +213,8 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--job=${{ github.job }}',
|
'--job=${{ github.job }}',
|
||||||
'--run-id=${{ github.run_id }}',
|
'--run-id=${{ github.run_id }}',
|
||||||
'--run-attempt=${{ github.run_attempt }}',
|
'--run-attempt=${{ github.run_attempt }}',
|
||||||
|
'--variant="${TRIAL_VARIANT}"',
|
||||||
|
'inputs.local_alpha_variant',
|
||||||
];
|
];
|
||||||
for (const value of required) {
|
for (const value of required) {
|
||||||
if (!contents.includes(value))
|
if (!contents.includes(value))
|
||||||
|
|||||||
@@ -760,17 +760,22 @@ if (require.main === module) {
|
|||||||
try {
|
try {
|
||||||
if (process.argv.length < 4) {
|
if (process.argv.length < 4) {
|
||||||
fail(
|
fail(
|
||||||
'usage: ql3-prune-runtime-artifact.cjs node_modules/@qinglong @qinglong/profile-entry [...]',
|
'usage: ql3-prune-runtime-artifact.cjs node_modules/@qinglong @qinglong/profile-entry [...] [--exclude=@qinglong/package] [--retain-js=package/path.js]',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const arguments = process.argv.slice(3);
|
const arguments = process.argv.slice(3);
|
||||||
const report = pruneRuntimeArtifact(process.argv[2], {
|
const report = pruneRuntimeArtifact(process.argv[2], {
|
||||||
entrySpecifiers: arguments.filter(
|
entrySpecifiers: arguments.filter(
|
||||||
(argument) => !argument.startsWith('--exclude='),
|
(argument) =>
|
||||||
|
!argument.startsWith('--exclude=') &&
|
||||||
|
!argument.startsWith('--retain-js='),
|
||||||
),
|
),
|
||||||
excludedInternalPackages: arguments
|
excludedInternalPackages: arguments
|
||||||
.filter((argument) => argument.startsWith('--exclude='))
|
.filter((argument) => argument.startsWith('--exclude='))
|
||||||
.map((argument) => argument.slice('--exclude='.length)),
|
.map((argument) => argument.slice('--exclude='.length)),
|
||||||
|
retainedJavaScriptFiles: arguments
|
||||||
|
.filter((argument) => argument.startsWith('--retain-js='))
|
||||||
|
.map((argument) => argument.slice('--retain-js='.length)),
|
||||||
});
|
});
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${JSON.stringify({ schemaVersion: 1, ...report })}\n`,
|
`${JSON.stringify({ schemaVersion: 1, ...report })}\n`,
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ const CONTAINER_ROOTS = Object.freeze([
|
|||||||
'deploy/containers/ql3-local-application',
|
'deploy/containers/ql3-local-application',
|
||||||
'deploy/containers/ql3-worker',
|
'deploy/containers/ql3-worker',
|
||||||
]);
|
]);
|
||||||
|
const CONTAINER_DOCKER_VERSION_LABEL_COUNTS = Object.freeze({
|
||||||
|
'deploy/containers/ql3-cluster-control': 1,
|
||||||
|
'deploy/containers/ql3-cluster-admin': 1,
|
||||||
|
'deploy/containers/ql3-local-application': 2,
|
||||||
|
'deploy/containers/ql3-worker': 1,
|
||||||
|
});
|
||||||
const DEPLOYMENT_ROOTS = Object.freeze([
|
const DEPLOYMENT_ROOTS = Object.freeze([
|
||||||
'deploy/kubernetes/ql3-cluster',
|
'deploy/kubernetes/ql3-cluster',
|
||||||
'deploy/kubernetes/ql3-worker',
|
'deploy/kubernetes/ql3-worker',
|
||||||
@@ -257,7 +263,7 @@ function auditReleaseVersionContract(rootInput = DEFAULT_ROOT) {
|
|||||||
versionOccurrences(
|
versionOccurrences(
|
||||||
dockerfile,
|
dockerfile,
|
||||||
`org.opencontainers.image.version=\"${identity.version}\"`,
|
`org.opencontainers.image.version=\"${identity.version}\"`,
|
||||||
) !== 1
|
) !== CONTAINER_DOCKER_VERSION_LABEL_COUNTS[containerRoot]
|
||||||
) {
|
) {
|
||||||
fail(`container Dockerfile release identity drifted: ${containerRoot}`);
|
fail(`container Dockerfile release identity drifted: ${containerRoot}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ OPERATOR_ID='@@OPERATOR_ID@@'
|
|||||||
ARCHITECTURE='@@ARCHITECTURE@@'
|
ARCHITECTURE='@@ARCHITECTURE@@'
|
||||||
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
||||||
ARCHIVE='@@ARCHIVE@@'
|
ARCHIVE='@@ARCHIVE@@'
|
||||||
|
VARIANT='@@VARIANT@@'
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
|
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
|
||||||
@@ -37,6 +38,18 @@ case "$profile" in
|
|||||||
;;
|
;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
|
case "$VARIANT" in
|
||||||
|
headless)
|
||||||
|
network_mode=none
|
||||||
|
application_config=local-application.json
|
||||||
|
;;
|
||||||
|
console)
|
||||||
|
[ "$(uname -s)" = Linux ] || fail 'Console variant requires a Linux Docker host'
|
||||||
|
network_mode=host
|
||||||
|
application_config=local-api.json
|
||||||
|
;;
|
||||||
|
*) fail 'embedded Trial Kit variant is invalid' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
case "$data_root" in
|
case "$data_root" in
|
||||||
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
||||||
@@ -92,6 +105,11 @@ EOF
|
|||||||
cat >"$data_root/local-application.json" <<EOF
|
cat >"$data_root/local-application.json" <<EOF
|
||||||
{"schema":"qinglong/local-application-process@v2","instanceId":"alpha-trial-local","profile":"$profile","storage":{"mode":"fresh","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","busyTimeoutMs":100},"runtime":{"receiptRoot":"/var/lib/qinglong3/receipts","artifactRoot":"/var/lib/qinglong3/artifacts","secretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json"},"pluginPackages":{"stagingRoot":"/var/lib/qinglong3/plugin-staging","activationRoot":"/var/lib/qinglong3/plugin-activation","recoverySource":{"mode":"disabled"},"pageSize":4,"maxPages":4,"taskPublicationPageSize":4,"taskPublicationMaxPages":4},"ai":{"deployment":"excluded"}}
|
{"schema":"qinglong/local-application-process@v2","instanceId":"alpha-trial-local","profile":"$profile","storage":{"mode":"fresh","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","busyTimeoutMs":100},"runtime":{"receiptRoot":"/var/lib/qinglong3/receipts","artifactRoot":"/var/lib/qinglong3/artifacts","secretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json"},"pluginPackages":{"stagingRoot":"/var/lib/qinglong3/plugin-staging","activationRoot":"/var/lib/qinglong3/plugin-activation","recoverySource":{"mode":"disabled"},"pageSize":4,"maxPages":4,"taskPublicationPageSize":4,"taskPublicationMaxPages":4},"ai":{"deployment":"excluded"}}
|
||||||
EOF
|
EOF
|
||||||
|
if [ "$VARIANT" = console ]; then
|
||||||
|
cat >"$data_root/local-api.json" <<EOF
|
||||||
|
{"schema":"qinglong/local-api-process@v1","deploymentRoot":"/var/lib/qinglong3","applicationConfigFilePath":"/var/lib/qinglong3/local-application.json","ownerPepperKeyringDirectory":"/var/lib/qinglong3/owner-peppers","listener":{"host":"127.0.0.1","port":5700}}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
chmod 0600 "$data_root"/*.json
|
chmod 0600 "$data_root"/*.json
|
||||||
|
|
||||||
uid=$(id -u)
|
uid=$(id -u)
|
||||||
@@ -129,12 +147,12 @@ trap cleanup EXIT
|
|||||||
trap 'exit 130' HUP INT TERM
|
trap 'exit 130' HUP INT TERM
|
||||||
|
|
||||||
container_id=$(docker run --detach --name "$container_name" \
|
container_id=$(docker run --detach --name "$container_name" \
|
||||||
--restart unless-stopped --read-only --user "$uid:$gid" --network none \
|
--restart unless-stopped --read-only --user "$uid:$gid" --network "$network_mode" \
|
||||||
--cap-drop ALL --security-opt no-new-privileges \
|
--cap-drop ALL --security-opt no-new-privileges \
|
||||||
--memory "$memory" --memory-swap "$memory" --cpus 0.5 --pids-limit "$pids" \
|
--memory "$memory" --memory-swap "$memory" --cpus 0.5 --pids-limit "$pids" \
|
||||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
|
||||||
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
|
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
|
||||||
"$APPLICATION_IMAGE" --config /var/lib/qinglong3/local-application.json)
|
"$APPLICATION_IMAGE" --config "/var/lib/qinglong3/$application_config")
|
||||||
printf '%s\n' "$container_id" >"$data_root/container.id"
|
printf '%s\n' "$container_id" >"$data_root/container.id"
|
||||||
chmod 0600 "$data_root/container.id"
|
chmod 0600 "$data_root/container.id"
|
||||||
|
|
||||||
@@ -153,10 +171,15 @@ done
|
|||||||
umask "$old_umask"
|
umask "$old_umask"
|
||||||
|
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
"QingLong 3.0 Local Alpha is active ($profile, $ARCHITECTURE)." \
|
"QingLong 3.0 Local Alpha is active ($VARIANT, $profile, $ARCHITECTURE)." \
|
||||||
"Data root: $data_root" \
|
"Data root: $data_root" \
|
||||||
"Owner deliveries: $data_root/owner-delivery" \
|
"Owner deliveries: $data_root/owner-delivery" \
|
||||||
"Logs: docker logs $container_name" \
|
"Logs: docker logs $container_name" \
|
||||||
"Stop: docker stop --time 30 $container_name" \
|
"Stop: docker stop --time 30 $container_name" \
|
||||||
"Remove container: docker rm $container_name" \
|
"Remove container: docker rm $container_name" \
|
||||||
'The fresh data root is retained until you remove it explicitly.'
|
'The fresh data root is retained until you remove it explicitly.'
|
||||||
|
if [ "$VARIANT" = console ]; then
|
||||||
|
printf '%s\n' \
|
||||||
|
'Console: http://127.0.0.1:5700/' \
|
||||||
|
'Remote access: create an SSH tunnel to 127.0.0.1:5700; do not expose the port on LAN or the public Internet.'
|
||||||
|
fi
|
||||||
|
|||||||
@@ -78,18 +78,19 @@ function writeMilestone(directory, manifest) {
|
|||||||
fs.writeFileSync(path.join(directory, 'SHA256SUMS'), checksums(directory));
|
fs.writeFileSync(path.join(directory, 'SHA256SUMS'), checksums(directory));
|
||||||
}
|
}
|
||||||
|
|
||||||
function localManifest(attempt = runAttempt) {
|
function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 2,
|
||||||
schema: 'qinglong/alpha-local-milestone@v1',
|
schema: 'qinglong/alpha-local-milestone@v2',
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
|
variant,
|
||||||
version,
|
version,
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
workflow: workflow('local-alpha-milestone', attempt),
|
workflow: workflow('local-alpha-milestone', attempt),
|
||||||
artifacts: {
|
artifacts: {
|
||||||
amd64: {
|
amd64: {
|
||||||
artifactName: `ql3-alpha-${revision}-local-amd64`,
|
artifactName: `ql3-alpha-${revision}-local-${variant}-amd64`,
|
||||||
architecture: 'amd64',
|
architecture: 'amd64',
|
||||||
bundleManifest: {
|
bundleManifest: {
|
||||||
file: 'manifest.json',
|
file: 'manifest.json',
|
||||||
@@ -102,7 +103,7 @@ function localManifest(attempt = runAttempt) {
|
|||||||
verificationSha256: digest('5'),
|
verificationSha256: digest('5'),
|
||||||
},
|
},
|
||||||
arm64: {
|
arm64: {
|
||||||
artifactName: `ql3-alpha-${revision}-local-arm64`,
|
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
|
||||||
architecture: 'arm64',
|
architecture: 'arm64',
|
||||||
bundleManifest: {
|
bundleManifest: {
|
||||||
file: 'manifest.json',
|
file: 'manifest.json',
|
||||||
@@ -163,7 +164,10 @@ function fixture(t, options = {}) {
|
|||||||
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
||||||
const localMilestoneRoot = path.join(fixtureRoot, 'local');
|
const localMilestoneRoot = path.join(fixtureRoot, 'local');
|
||||||
const clusterMilestoneRoot = path.join(fixtureRoot, 'cluster');
|
const clusterMilestoneRoot = path.join(fixtureRoot, 'cluster');
|
||||||
writeMilestone(localMilestoneRoot, localManifest(options.localAttempt));
|
writeMilestone(
|
||||||
|
localMilestoneRoot,
|
||||||
|
localManifest(options.localAttempt, options.variant || 'headless'),
|
||||||
|
);
|
||||||
writeMilestone(clusterMilestoneRoot, clusterManifest(options.clusterAttempt));
|
writeMilestone(clusterMilestoneRoot, clusterManifest(options.clusterAttempt));
|
||||||
const readme = path.join(fixtureRoot, 'README-source.md');
|
const readme = path.join(fixtureRoot, 'README-source.md');
|
||||||
fs.writeFileSync(readme, '# QingLong 3.0 Alpha stage index\n');
|
fs.writeFileSync(readme, '# QingLong 3.0 Alpha stage index\n');
|
||||||
@@ -196,11 +200,11 @@ function finalizeOptions(paths) {
|
|||||||
test('closes Local and Cluster milestones into one deployment-facing stage index', (t) => {
|
test('closes Local and Cluster milestones into one deployment-facing stage index', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = finalizeAlphaStageIndex(finalizeOptions(paths));
|
const manifest = finalizeAlphaStageIndex(finalizeOptions(paths));
|
||||||
assert.equal(manifest.schema, 'qinglong/alpha-stage-index@v1');
|
assert.equal(manifest.schema, 'qinglong/alpha-stage-index@v2');
|
||||||
assert.deepEqual(Object.keys(manifest.milestones), ['local', 'cluster']);
|
assert.deepEqual(Object.keys(manifest.milestones), ['local', 'cluster']);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
manifest.deploymentSelections.local.architectures.amd64.requiredArtifacts,
|
manifest.deploymentSelections.local.architectures.amd64.requiredArtifacts,
|
||||||
[`ql3-alpha-${revision}-local-amd64`],
|
[`ql3-alpha-${revision}-local-headless-amd64`],
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
manifest.deploymentSelections.cluster.architectures.arm64.requiredArtifacts,
|
manifest.deploymentSelections.cluster.architectures.arm64.requiredArtifacts,
|
||||||
@@ -224,6 +228,34 @@ test('closes Local and Cluster milestones into one deployment-facing stage index
|
|||||||
assert.deepEqual(report.profiles, ['edge', 'standalone', 'cluster']);
|
assert.deepEqual(report.profiles, ['edge', 'standalone', 'cluster']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('indexes the Console milestone as a distinct loopback deployment selection', (t) => {
|
||||||
|
const paths = fixture(t, { variant: 'console' });
|
||||||
|
const manifest = finalizeAlphaStageIndex(finalizeOptions(paths));
|
||||||
|
assert.equal(
|
||||||
|
manifest.milestones.local.artifactName,
|
||||||
|
`ql3-alpha-${revision}-local-console-milestone`,
|
||||||
|
);
|
||||||
|
assert.equal(manifest.deploymentSelections.local.variant, 'console');
|
||||||
|
assert.deepEqual(manifest.deploymentSelections.local.profiles, [
|
||||||
|
'edge-application-api',
|
||||||
|
'standalone-application-api',
|
||||||
|
]);
|
||||||
|
assert.equal(
|
||||||
|
manifest.deploymentSelections.local.intent,
|
||||||
|
'fresh_loopback_console_non_production_trial',
|
||||||
|
);
|
||||||
|
const report = auditAlphaStageIndex({
|
||||||
|
stageRoot: paths.outputRoot,
|
||||||
|
localMilestoneRoot: paths.localMilestoneRoot,
|
||||||
|
clusterMilestoneRoot: paths.clusterMilestoneRoot,
|
||||||
|
});
|
||||||
|
assert.deepEqual(report.profiles, [
|
||||||
|
'edge-application-api',
|
||||||
|
'standalone-application-api',
|
||||||
|
'cluster',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects Local and Cluster milestones from different workflow attempts', (t) => {
|
test('rejects Local and Cluster milestones from different workflow attempts', (t) => {
|
||||||
const paths = fixture(t, { clusterAttempt: '2' });
|
const paths = fixture(t, { clusterAttempt: '2' });
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|||||||
@@ -128,6 +128,40 @@ test('generates the AI-excluded local application image closure', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generates the bounded AI-excluded local Console image closure', () => {
|
||||||
|
const document = createClusterImageSbom({ root, image: 'local-console' });
|
||||||
|
const report = auditClusterImageSbom(document, {
|
||||||
|
root,
|
||||||
|
image: 'local-console',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(report, {
|
||||||
|
image: 'local-console',
|
||||||
|
root: `pkg:npm/%40qinglong/local-application-image@${version}`,
|
||||||
|
components: 12,
|
||||||
|
externalComponents: 2,
|
||||||
|
internalComponents: 10,
|
||||||
|
dependencyNodes: 13,
|
||||||
|
inventoryVerified: false,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
document.components.some(
|
||||||
|
(component) => component.name === '@qinglong/local-api',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
document.components.some(
|
||||||
|
(component) => component.name === '@qinglong/local-owner-console',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
document.components.some((component) => component.name === '@qinglong/ai'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('generates the headless Worker image runtime closure', () => {
|
test('generates the headless Worker image runtime closure', () => {
|
||||||
const document = createClusterImageSbom({ root, image: 'worker' });
|
const document = createClusterImageSbom({ root, image: 'worker' });
|
||||||
const report = auditClusterImageSbom(document, {
|
const report = auditClusterImageSbom(document, {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ test('accepts the empty fail-closed production exception policy', () => {
|
|||||||
control: 0,
|
control: 0,
|
||||||
'control-ai': 0,
|
'control-ai': 0,
|
||||||
local: 0,
|
local: 0,
|
||||||
|
'local-console': 0,
|
||||||
'local-operator': 0,
|
'local-operator': 0,
|
||||||
worker: 0,
|
worker: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const revision = 'a'.repeat(40);
|
|||||||
const runId = '33047425710';
|
const runId = '33047425710';
|
||||||
const runAttempt = '2';
|
const runAttempt = '2';
|
||||||
|
|
||||||
function imageInspection(role, architecture, idCharacter) {
|
function imageInspection(role, architecture, idCharacter, variant = 'headless') {
|
||||||
return {
|
return {
|
||||||
Id: `sha256:${idCharacter.repeat(64)}`,
|
Id: `sha256:${idCharacter.repeat(64)}`,
|
||||||
Os: 'linux',
|
Os: 'linux',
|
||||||
@@ -39,15 +39,23 @@ function imageInspection(role, architecture, idCharacter) {
|
|||||||
Labels: {
|
Labels: {
|
||||||
'org.opencontainers.image.title':
|
'org.opencontainers.image.title':
|
||||||
role === 'application'
|
role === 'application'
|
||||||
? 'QingLong 3.0 Local Application'
|
? variant === 'console'
|
||||||
|
? 'QingLong 3.0 Local Console Application'
|
||||||
|
: 'QingLong 3.0 Local Application'
|
||||||
: 'QingLong 3.0 Local Operator',
|
: 'QingLong 3.0 Local Operator',
|
||||||
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
|
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
|
||||||
'org.opencontainers.image.revision': revision,
|
'org.opencontainers.image.revision': revision,
|
||||||
'org.opencontainers.image.version': version,
|
'org.opencontainers.image.version': version,
|
||||||
...(role === 'application'
|
...(role === 'application'
|
||||||
? {
|
? {
|
||||||
'io.qinglong.profile': 'edge,standalone',
|
'io.qinglong.profile':
|
||||||
|
variant === 'console'
|
||||||
|
? 'edge-application-api,standalone-application-api'
|
||||||
|
: 'edge,standalone',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
|
...(variant === 'console'
|
||||||
|
? { 'io.qinglong.local.console': 'offline-loopback' }
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
'io.qinglong.lifecycle': 'short-lived',
|
'io.qinglong.lifecycle': 'short-lived',
|
||||||
@@ -60,6 +68,7 @@ function imageInspection(role, architecture, idCharacter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createBundle(fixtureRoot, architecture, options = {}) {
|
function createBundle(fixtureRoot, architecture, options = {}) {
|
||||||
|
const variant = options.variant || 'headless';
|
||||||
const bundleFixture = path.join(fixtureRoot, architecture);
|
const bundleFixture = path.join(fixtureRoot, architecture);
|
||||||
fs.mkdirSync(bundleFixture);
|
fs.mkdirSync(bundleFixture);
|
||||||
const applicationSbom = path.join(bundleFixture, 'application.json');
|
const applicationSbom = path.join(bundleFixture, 'application.json');
|
||||||
@@ -69,7 +78,12 @@ function createBundle(fixtureRoot, architecture, options = {}) {
|
|||||||
const outputRoot = path.join(bundleFixture, 'bundle');
|
const outputRoot = path.join(bundleFixture, 'bundle');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
applicationSbom,
|
applicationSbom,
|
||||||
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
|
`${JSON.stringify(
|
||||||
|
createClusterImageSbom({
|
||||||
|
root,
|
||||||
|
image: variant === 'console' ? 'local-console' : 'local',
|
||||||
|
}),
|
||||||
|
)}\n`,
|
||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
operatorSbom,
|
operatorSbom,
|
||||||
@@ -88,7 +102,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
|
|||||||
const adapters = {
|
const adapters = {
|
||||||
inspectImage(image) {
|
inspectImage(image) {
|
||||||
const role = image.includes('operator') ? 'operator' : 'application';
|
const role = image.includes('operator') ? 'operator' : 'application';
|
||||||
return imageInspection(role, architecture, characters[role]);
|
return imageInspection(role, architecture, characters[role], variant);
|
||||||
},
|
},
|
||||||
saveImages(images, archivePath) {
|
saveImages(images, archivePath) {
|
||||||
assert.deepEqual(images, [applicationImage, operatorImage]);
|
assert.deepEqual(images, [applicationImage, operatorImage]);
|
||||||
@@ -104,6 +118,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
|
|||||||
root,
|
root,
|
||||||
output: verificationEvidence,
|
output: verificationEvidence,
|
||||||
architecture,
|
architecture,
|
||||||
|
variant,
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
applicationImage,
|
applicationImage,
|
||||||
operatorImage,
|
operatorImage,
|
||||||
@@ -123,6 +138,7 @@ function createBundle(fixtureRoot, architecture, options = {}) {
|
|||||||
root,
|
root,
|
||||||
outputRoot,
|
outputRoot,
|
||||||
architecture,
|
architecture,
|
||||||
|
variant,
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
applicationImage,
|
applicationImage,
|
||||||
operatorImage,
|
operatorImage,
|
||||||
@@ -152,9 +168,16 @@ function fixture(t, options = {}) {
|
|||||||
readme,
|
readme,
|
||||||
outputRoot: path.join(fixtureRoot, 'milestone'),
|
outputRoot: path.join(fixtureRoot, 'milestone'),
|
||||||
bundles: {
|
bundles: {
|
||||||
amd64: createBundle(fixtureRoot, 'amd64', options.amd64),
|
amd64: createBundle(fixtureRoot, 'amd64', {
|
||||||
arm64: createBundle(fixtureRoot, 'arm64', options.arm64),
|
...options.amd64,
|
||||||
|
variant: options.variant || 'headless',
|
||||||
|
}),
|
||||||
|
arm64: createBundle(fixtureRoot, 'arm64', {
|
||||||
|
...options.arm64,
|
||||||
|
variant: options.variant || 'headless',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
variant: options.variant || 'headless',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +188,7 @@ function finalizeOptions(paths) {
|
|||||||
bundles: paths.bundles,
|
bundles: paths.bundles,
|
||||||
readme: paths.readme,
|
readme: paths.readme,
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
|
variant: paths.variant,
|
||||||
repository: 'whyour/qinglong',
|
repository: 'whyour/qinglong',
|
||||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||||
workflowSha: revision,
|
workflowSha: revision,
|
||||||
@@ -177,16 +201,17 @@ function finalizeOptions(paths) {
|
|||||||
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v1');
|
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v2');
|
||||||
|
assert.equal(manifest.variant, 'headless');
|
||||||
assert.equal(manifest.sourceRevision, revision);
|
assert.equal(manifest.sourceRevision, revision);
|
||||||
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
manifest.artifacts.amd64.artifactName,
|
manifest.artifacts.amd64.artifactName,
|
||||||
`ql3-alpha-${revision}-local-amd64`,
|
`ql3-alpha-${revision}-local-headless-amd64`,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
manifest.artifacts.arm64.artifactName,
|
manifest.artifacts.arm64.artifactName,
|
||||||
`ql3-alpha-${revision}-local-arm64`,
|
`ql3-alpha-${revision}-local-headless-arm64`,
|
||||||
);
|
);
|
||||||
assert.notEqual(
|
assert.notEqual(
|
||||||
manifest.artifacts.amd64.archiveSha256,
|
manifest.artifacts.amd64.archiveSha256,
|
||||||
@@ -204,6 +229,25 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
|
|||||||
assert.deepEqual(report.architectures, ['amd64', 'arm64']);
|
assert.deepEqual(report.architectures, ['amd64', 'arm64']);
|
||||||
assert.equal(report.workflowRunId, runId);
|
assert.equal(report.workflowRunId, runId);
|
||||||
assert.equal(report.workflowRunAttempt, runAttempt);
|
assert.equal(report.workflowRunAttempt, runAttempt);
|
||||||
|
assert.equal(report.variant, 'headless');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finalizes Console trial kits as a separately named milestone', (t) => {
|
||||||
|
const paths = fixture(t, { variant: 'console' });
|
||||||
|
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||||
|
assert.equal(manifest.variant, 'console');
|
||||||
|
assert.equal(
|
||||||
|
manifest.artifacts.amd64.artifactName,
|
||||||
|
`ql3-alpha-${revision}-local-console-amd64`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
manifest.artifacts.arm64.artifactName,
|
||||||
|
`ql3-alpha-${revision}-local-console-arm64`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }).variant,
|
||||||
|
'console',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects a trial kit from another run attempt before publishing', (t) => {
|
test('rejects a trial kit from another run attempt before publishing', (t) => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const revision = 'a'.repeat(40);
|
|||||||
function imageInspection(
|
function imageInspection(
|
||||||
role,
|
role,
|
||||||
idCharacter = role === 'application' ? '1' : '2',
|
idCharacter = role === 'application' ? '1' : '2',
|
||||||
|
variant = 'headless',
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
Id: `sha256:${idCharacter.repeat(64)}`,
|
Id: `sha256:${idCharacter.repeat(64)}`,
|
||||||
@@ -37,15 +38,23 @@ function imageInspection(
|
|||||||
Labels: {
|
Labels: {
|
||||||
'org.opencontainers.image.title':
|
'org.opencontainers.image.title':
|
||||||
role === 'application'
|
role === 'application'
|
||||||
? 'QingLong 3.0 Local Application'
|
? variant === 'console'
|
||||||
|
? 'QingLong 3.0 Local Console Application'
|
||||||
|
: 'QingLong 3.0 Local Application'
|
||||||
: 'QingLong 3.0 Local Operator',
|
: 'QingLong 3.0 Local Operator',
|
||||||
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
|
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
|
||||||
'org.opencontainers.image.revision': revision,
|
'org.opencontainers.image.revision': revision,
|
||||||
'org.opencontainers.image.version': version,
|
'org.opencontainers.image.version': version,
|
||||||
...(role === 'application'
|
...(role === 'application'
|
||||||
? {
|
? {
|
||||||
'io.qinglong.profile': 'edge,standalone',
|
'io.qinglong.profile':
|
||||||
|
variant === 'console'
|
||||||
|
? 'edge-application-api,standalone-application-api'
|
||||||
|
: 'edge,standalone',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
|
...(variant === 'console'
|
||||||
|
? { 'io.qinglong.local.console': 'offline-loopback' }
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
'io.qinglong.lifecycle': 'short-lived',
|
'io.qinglong.lifecycle': 'short-lived',
|
||||||
@@ -57,7 +66,7 @@ function imageInspection(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixture(t) {
|
function fixture(t, variant = 'headless') {
|
||||||
const fixtureRoot = fs.realpathSync(
|
const fixtureRoot = fs.realpathSync(
|
||||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-bundle-')),
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-bundle-')),
|
||||||
);
|
);
|
||||||
@@ -71,7 +80,12 @@ function fixture(t) {
|
|||||||
const readme = path.join(fixtureRoot, 'README-source.md');
|
const readme = path.join(fixtureRoot, 'README-source.md');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
applicationSbom,
|
applicationSbom,
|
||||||
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
|
`${JSON.stringify(
|
||||||
|
createClusterImageSbom({
|
||||||
|
root,
|
||||||
|
image: variant === 'console' ? 'local-console' : 'local',
|
||||||
|
}),
|
||||||
|
)}\n`,
|
||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
operatorSbom,
|
operatorSbom,
|
||||||
@@ -89,8 +103,8 @@ function fixture(t) {
|
|||||||
outputRoot: path.join(fixtureRoot, 'bundle'),
|
outputRoot: path.join(fixtureRoot, 'bundle'),
|
||||||
};
|
};
|
||||||
createLocalAlphaTrialKitVerificationEvidence(
|
createLocalAlphaTrialKitVerificationEvidence(
|
||||||
verificationOptions(paths),
|
verificationOptions(paths, { variant }),
|
||||||
adapters(),
|
adapters({}, variant),
|
||||||
);
|
);
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
@@ -100,6 +114,7 @@ function verificationOptions(paths, overrides = {}) {
|
|||||||
root,
|
root,
|
||||||
output: paths.verificationEvidence,
|
output: paths.verificationEvidence,
|
||||||
architecture: 'arm64',
|
architecture: 'arm64',
|
||||||
|
variant: 'headless',
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
applicationImage: 'qinglong3-local-application:test-arm64',
|
applicationImage: 'qinglong3-local-application:test-arm64',
|
||||||
operatorImage: 'qinglong3-local-operator:test-arm64',
|
operatorImage: 'qinglong3-local-operator:test-arm64',
|
||||||
@@ -114,11 +129,12 @@ function verificationOptions(paths, overrides = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOptions(paths) {
|
function createOptions(paths, variant = 'headless') {
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
outputRoot: paths.outputRoot,
|
outputRoot: paths.outputRoot,
|
||||||
architecture: 'arm64',
|
architecture: 'arm64',
|
||||||
|
variant,
|
||||||
sourceRevision: revision,
|
sourceRevision: revision,
|
||||||
applicationImage: 'qinglong3-local-application:test-arm64',
|
applicationImage: 'qinglong3-local-application:test-arm64',
|
||||||
operatorImage: 'qinglong3-local-operator:test-arm64',
|
operatorImage: 'qinglong3-local-operator:test-arm64',
|
||||||
@@ -129,12 +145,12 @@ function createOptions(paths) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function adapters(overrides = {}) {
|
function adapters(overrides = {}, variant = 'headless') {
|
||||||
return {
|
return {
|
||||||
inspectImage(image) {
|
inspectImage(image) {
|
||||||
return image.includes('operator')
|
return image.includes('operator')
|
||||||
? imageInspection('operator')
|
? imageInspection('operator', '2', variant)
|
||||||
: imageInspection('application');
|
: imageInspection('application', '1', variant);
|
||||||
},
|
},
|
||||||
saveImages(images, archivePath) {
|
saveImages(images, archivePath) {
|
||||||
assert.deepEqual(images, [
|
assert.deepEqual(images, [
|
||||||
@@ -150,7 +166,8 @@ function adapters(overrides = {}) {
|
|||||||
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v3');
|
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v4');
|
||||||
|
assert.equal(manifest.variant, 'headless');
|
||||||
assert.equal(manifest.sourceRevision, revision);
|
assert.equal(manifest.sourceRevision, revision);
|
||||||
assert.equal(manifest.architecture, 'arm64');
|
assert.equal(manifest.architecture, 'arm64');
|
||||||
assert.equal(manifest.images.application.architecture, 'arm64');
|
assert.equal(manifest.images.application.architecture, 'arm64');
|
||||||
@@ -164,13 +181,14 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
|
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
|
||||||
assert.match(
|
assert.match(
|
||||||
quickstartContents,
|
quickstartContents,
|
||||||
/QingLong 3\.0 Local Alpha is active \(\$profile, \$ARCHITECTURE\)/,
|
/QingLong 3\.0 Local Alpha is active \(\$VARIANT, \$profile, \$ARCHITECTURE\)/,
|
||||||
);
|
);
|
||||||
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
|
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
|
||||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||||
assert.equal(report.compatible, true);
|
assert.equal(report.compatible, true);
|
||||||
assert.equal(report.sourceRevision, revision);
|
assert.equal(report.sourceRevision, revision);
|
||||||
assert.equal(report.workflowRunId, '32990652047');
|
assert.equal(report.workflowRunId, '32990652047');
|
||||||
|
assert.equal(report.variant, 'headless');
|
||||||
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
||||||
'README.md',
|
'README.md',
|
||||||
'SHA256SUMS',
|
'SHA256SUMS',
|
||||||
@@ -183,6 +201,57 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('materializes a distinct loopback Console trial kit without widening the headless archive', (t) => {
|
||||||
|
const paths = fixture(t, 'console');
|
||||||
|
const manifest = createLocalAlphaTrialKit(
|
||||||
|
createOptions(paths, 'console'),
|
||||||
|
adapters({}, 'console'),
|
||||||
|
);
|
||||||
|
assert.equal(manifest.variant, 'console');
|
||||||
|
const applicationSbom = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
applicationSbom.metadata.properties.find(
|
||||||
|
(property) => property.name === 'qinglong:image-profile',
|
||||||
|
).value,
|
||||||
|
'local-console',
|
||||||
|
);
|
||||||
|
const verification = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(paths.outputRoot, 'verification-evidence.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(verification.gates.consoleLiveJourney, 'passed');
|
||||||
|
const quickstartContents = fs.readFileSync(
|
||||||
|
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.match(quickstartContents, /VARIANT='console'/);
|
||||||
|
assert.match(quickstartContents, /network_mode=host/);
|
||||||
|
assert.match(quickstartContents, /application_config=local-api\.json/);
|
||||||
|
assert.match(quickstartContents, /"host":"127\.0\.0\.1","port":5700/);
|
||||||
|
assert.match(quickstartContents, /Console: http:\/\/127\.0\.0\.1:5700\//);
|
||||||
|
assert.match(quickstartContents, /do not expose the port on LAN/);
|
||||||
|
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||||
|
assert.equal(report.compatible, true);
|
||||||
|
assert.equal(report.variant, 'console');
|
||||||
|
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
||||||
|
'README.md',
|
||||||
|
'SHA256SUMS',
|
||||||
|
'manifest.json',
|
||||||
|
'qinglong3-local-application.cdx.json',
|
||||||
|
'qinglong3-local-console-trial-kit-arm64.docker.tar',
|
||||||
|
'qinglong3-local-operator.cdx.json',
|
||||||
|
'quickstart.sh',
|
||||||
|
'verification-evidence.json',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('fails closed and removes a partial output on incompatible image identity', (t) => {
|
test('fails closed and removes a partial output on incompatible image identity', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const options = createOptions(paths);
|
const options = createOptions(paths);
|
||||||
@@ -352,7 +421,7 @@ exit 1
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
run.stdout,
|
run.stdout,
|
||||||
/QingLong 3\.0 Local Alpha is active \(edge, arm64\)/,
|
/QingLong 3\.0 Local Alpha is active \(headless, edge, arm64\)/,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
|
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
|
||||||
@@ -439,6 +508,7 @@ test('CLI grammar is exact and separates create from offline audit', () => {
|
|||||||
'--application-image=qinglong3-local-application:test-arm64',
|
'--application-image=qinglong3-local-application:test-arm64',
|
||||||
'--operator-image=qinglong3-local-operator:test-arm64',
|
'--operator-image=qinglong3-local-operator:test-arm64',
|
||||||
'--architecture=arm64',
|
'--architecture=arm64',
|
||||||
|
'--variant=headless',
|
||||||
`--source-revision=${revision}`,
|
`--source-revision=${revision}`,
|
||||||
'--repository=whyour/qinglong',
|
'--repository=whyour/qinglong',
|
||||||
'--workflow-ref=whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
'--workflow-ref=whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '../..');
|
||||||
|
const script = path.join(root, 'scripts/ql3-local-console-image-inventory.cjs');
|
||||||
|
const packages = [
|
||||||
|
'@qinglong/local-admin',
|
||||||
|
'@qinglong/local-api',
|
||||||
|
'@qinglong/local-application',
|
||||||
|
'@qinglong/local-command-file',
|
||||||
|
'@qinglong/local-execution',
|
||||||
|
'@qinglong/local-owner-console',
|
||||||
|
'@qinglong/local-process',
|
||||||
|
'@qinglong/local-secret',
|
||||||
|
'@qinglong/local-sqlite',
|
||||||
|
'@qinglong/runtime-core',
|
||||||
|
'croner',
|
||||||
|
'semver',
|
||||||
|
];
|
||||||
|
|
||||||
|
function inventoryFixture(t) {
|
||||||
|
const inventoryRoot = fs.realpathSync(
|
||||||
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-console-inventory-')),
|
||||||
|
);
|
||||||
|
t.after(() => fs.rmSync(inventoryRoot, { recursive: true, force: true }));
|
||||||
|
for (const packageName of packages) {
|
||||||
|
const packageRoot = path.join(inventoryRoot, packageName);
|
||||||
|
fs.mkdirSync(packageRoot, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(packageRoot, 'package.json'),
|
||||||
|
`${JSON.stringify({
|
||||||
|
name: packageName,
|
||||||
|
version: '3.0.0-alpha.1',
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return inventoryRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInventory(inventoryRoot) {
|
||||||
|
return spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[script, `--inventory-root=${inventoryRoot}`],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('accepts the exact bounded Local Console package closure', (t) => {
|
||||||
|
const inventoryRoot = inventoryFixture(t);
|
||||||
|
const run = runInventory(inventoryRoot);
|
||||||
|
assert.equal(run.status, 0, run.stderr);
|
||||||
|
const report = JSON.parse(run.stdout);
|
||||||
|
assert.equal(report.image, 'local-console');
|
||||||
|
assert.equal(report.packageCount, 12);
|
||||||
|
assert.equal(report.ai, 'excluded');
|
||||||
|
assert.equal(report.listener, 'loopback-only');
|
||||||
|
assert.equal(report.compatible, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects one unreviewed runtime package', (t) => {
|
||||||
|
const inventoryRoot = inventoryFixture(t);
|
||||||
|
const extra = path.join(inventoryRoot, 'unreviewed');
|
||||||
|
fs.mkdirSync(extra);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(extra, 'package.json'),
|
||||||
|
'{"name":"unreviewed","version":"1.0.0"}\n',
|
||||||
|
);
|
||||||
|
const run = runInventory(inventoryRoot);
|
||||||
|
assert.notEqual(run.status, 0);
|
||||||
|
assert.match(run.stderr, /package closure drifted/);
|
||||||
|
});
|
||||||
@@ -63,6 +63,50 @@ test('accepts the exact AI-excluded local application image contract', () => {
|
|||||||
'croner',
|
'croner',
|
||||||
'semver',
|
'semver',
|
||||||
]);
|
]);
|
||||||
|
assert.deepEqual(report.consoleRuntimePackages, [
|
||||||
|
'@qinglong/local-admin',
|
||||||
|
'@qinglong/local-api',
|
||||||
|
'@qinglong/local-application',
|
||||||
|
'@qinglong/local-command-file',
|
||||||
|
'@qinglong/local-execution',
|
||||||
|
'@qinglong/local-owner-console',
|
||||||
|
'@qinglong/local-process',
|
||||||
|
'@qinglong/local-secret',
|
||||||
|
'@qinglong/local-sqlite',
|
||||||
|
'@qinglong/runtime-core',
|
||||||
|
'croner',
|
||||||
|
'semver',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects Console assets, loopback identity or retained JavaScript drift', () => {
|
||||||
|
const current = fixture();
|
||||||
|
try {
|
||||||
|
const dockerfilePath = path.join(current.target, 'Dockerfile');
|
||||||
|
const dockerfile = fs
|
||||||
|
.readFileSync(dockerfilePath, 'utf8')
|
||||||
|
.replace(
|
||||||
|
'--retain-js=local-api/assets/console/console.js',
|
||||||
|
'--retain-js=local-api/assets/console/unreviewed.js',
|
||||||
|
)
|
||||||
|
.replace('io.qinglong.local.console="offline-loopback"', 'io.qinglong.local.console="public"');
|
||||||
|
fs.writeFileSync(dockerfilePath, dockerfile);
|
||||||
|
const report = auditLocalImageContract(current.root);
|
||||||
|
assert.equal(report.compatible, false);
|
||||||
|
assert.ok(
|
||||||
|
report.findings.some(
|
||||||
|
({ code }) =>
|
||||||
|
code === 'CONSOLE_RUNTIME_NONESSENTIAL_FILES_NOT_REMOVED',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
report.findings.some(
|
||||||
|
({ code }) => code === 'CONSOLE_RUNTIME_IDENTITY_OR_LABEL_DRIFT',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
current.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects a mutable runtime base image', () => {
|
test('rejects a mutable runtime base image', () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
@@ -575,6 +576,37 @@ test('rejects invalid or missing explicit JavaScript assets before mutation', ()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('CLI retains one reviewed JavaScript asset outside dist', () => {
|
||||||
|
const current = fixture();
|
||||||
|
try {
|
||||||
|
const application = writePackage(
|
||||||
|
current.root,
|
||||||
|
'local-api',
|
||||||
|
'@qinglong/local-api',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(application.dist, 'index.js'), "'use strict';\n");
|
||||||
|
const assets = path.join(application.packageRoot, 'assets/console');
|
||||||
|
fs.mkdirSync(assets, { recursive: true });
|
||||||
|
const retained = path.join(assets, 'console.js');
|
||||||
|
fs.writeFileSync(retained, "'use strict';\n");
|
||||||
|
const run = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
path.resolve(__dirname, '../../scripts/ql3-prune-runtime-artifact.cjs'),
|
||||||
|
current.root,
|
||||||
|
'@qinglong/local-api',
|
||||||
|
'--retain-js=local-api/assets/console/console.js',
|
||||||
|
],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
assert.equal(run.status, 0, run.stderr);
|
||||||
|
assert.equal(fs.existsSync(retained), true);
|
||||||
|
assert.equal(JSON.parse(run.stdout).schemaVersion, 1);
|
||||||
|
} finally {
|
||||||
|
current.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects symbolic links before making partial changes', () => {
|
test('rejects symbolic links before making partial changes', () => {
|
||||||
const current = fixture();
|
const current = fixture();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -109,6 +109,25 @@ test('audits one source-derived QingLong 3 release identity', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('requires version identity on both headless and Console runtime targets', (t) => {
|
||||||
|
const fixture = createFixture(t);
|
||||||
|
const dockerfilePath = path.join(
|
||||||
|
fixture,
|
||||||
|
'deploy/containers/ql3-local-application/Dockerfile',
|
||||||
|
);
|
||||||
|
const dockerfile = fs.readFileSync(dockerfilePath, 'utf8');
|
||||||
|
const label = `org.opencontainers.image.version="${SOURCE_VERSION}"`;
|
||||||
|
assert.equal(dockerfile.split(label).length - 1, 2);
|
||||||
|
fs.writeFileSync(
|
||||||
|
dockerfilePath,
|
||||||
|
dockerfile.replace(label, 'org.opencontainers.image.version="drifted"'),
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditReleaseVersionContract(fixture),
|
||||||
|
/container Dockerfile release identity drifted/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('plans the exact governed version surface without touching legacy 2.x', () => {
|
test('plans the exact governed version surface without touching legacy 2.x', () => {
|
||||||
const plan = createVersionTransitionPlan({
|
const plan = createVersionTransitionPlan({
|
||||||
root,
|
root,
|
||||||
@@ -116,7 +135,7 @@ test('plans the exact governed version surface without touching legacy 2.x', ()
|
|||||||
targetVersion: TARGET_VERSION,
|
targetVersion: TARGET_VERSION,
|
||||||
});
|
});
|
||||||
assert.equal(plan.fileCount, 66);
|
assert.equal(plan.fileCount, 66);
|
||||||
assert.equal(plan.replacementCount, 86);
|
assert.equal(plan.replacementCount, 87);
|
||||||
assert.equal(plan.legacyRootPackageVersion, LEGACY_VERSION);
|
assert.equal(plan.legacyRootPackageVersion, LEGACY_VERSION);
|
||||||
assert.equal(plan.legacyRootExcluded, true);
|
assert.equal(plan.legacyRootExcluded, true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
Reference in New Issue
Block a user