feat: ship verified platform upgrades with channel bundle releases

Package production dependencies per architecture for online and offline upgrades.
Validate dependencies and isolated backend startup before readiness, repair once
when permitted, and preserve service/data through failed preparation or rollback.

Publish release bundles with master images and expose an opt-in develop build
sharing the development index across Alpine and Debian images.
This commit is contained in:
whyour
2026-10-18 22:19:49 +08:00
parent a36a77086a
commit e8b6a30bd9
53 changed files with 5271 additions and 122 deletions
+115
View File
@@ -8,6 +8,9 @@ on:
# CLI is verified separately; its shared Bash upgrade engine ships in images.
- "!cli/**"
- "cli/src/internal/maintenance/upgradeArtifacts.cjs"
- "cli/src/internal/maintenance/platformBundle.cjs"
- "cli/src/internal/maintenance/upgradePrestart.cjs"
- "cli/src/internal/maintenance/panelUpgrade.cjs"
- "!.github/workflows/cli-package.yml"
branches:
- "master"
@@ -16,6 +19,10 @@ on:
- "v*"
workflow_dispatch:
inputs:
build_development_bundles:
description: Develop only - build and publish shared develop/debian-dev upgrade packages
type: boolean
default: false
static_sync_only:
description: Synchronize an existing static snapshot to GitLab and verify Gitee
type: boolean
@@ -33,6 +40,14 @@ on:
permissions:
contents: read
# Keep master image tags and their bundle indexes in one serialized release.
# A rerun may intentionally replace a version, including from the same commit.
# Manual development bundle releases are serialized independently of master.
# Ordinary develop pushes and Git-tag builds remain independent.
concurrency:
group: version-images-${{ github.ref == 'refs/heads/master' && 'master' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/develop' && inputs.build_development_bundles && 'develop-bundles') || format('{0}-{1}', github.run_id, github.run_attempt) }}
cancel-in-progress: false
jobs:
validate:
if: ${{ !inputs.static_sync_only }}
@@ -203,6 +218,8 @@ jobs:
build-alpine:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static
outputs:
image-digest: ${{ steps.image.outputs.digest }}
runs-on: ubuntu-22.04
permissions:
packages: write
@@ -268,6 +285,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Build and push (Alpine)
id: image
uses: docker/build-push-action@v7
with:
build-args: |
@@ -289,6 +307,8 @@ jobs:
build-debian:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static
outputs:
image-digest: ${{ steps.image.outputs.digest }}
runs-on: ubuntu-22.04
permissions:
packages: write
@@ -353,6 +373,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Build and push (Debian)
id: image
uses: docker/build-push-action@v7
with:
build-args: |
@@ -563,3 +584,97 @@ jobs:
echo '::error::Could not check the published npm version; refusing to publish.'
exit 1
fi
build-upgrade-bundles:
# Development packages are opt-in; both channels use this run's image digests.
if: ${{ !inputs.static_sync_only && (github.ref == 'refs/heads/master' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/develop' && inputs.build_development_bundles)) }}
needs: [build-alpine, build-debian]
runs-on: ubuntu-22.04
timeout-minutes: 90
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- { platform: linux/amd64, libc: musl, name: x64-musl }
- { platform: linux/arm/v6, libc: musl, name: armv6-musl }
- { platform: linux/arm/v7, libc: musl, name: armv7-musl }
- { platform: linux/arm64, libc: musl, name: arm64-musl }
- { platform: linux/ppc64le, libc: musl, name: ppc64-musl }
- { platform: linux/s390x, libc: musl, name: s390x-musl }
- { platform: linux/386, libc: musl, name: ia32-musl }
- { platform: linux/amd64, libc: glibc, name: x64-glibc }
- { platform: linux/arm/v6, libc: glibc, name: armv6-glibc }
- { platform: linux/arm/v7, libc: glibc, name: armv7-glibc }
- { platform: linux/arm64, libc: glibc, name: arm64-glibc }
- { platform: linux/ppc64le, libc: glibc, name: ppc64-glibc }
- { platform: linux/s390x, libc: glibc, name: s390x-glibc }
steps:
- uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v4
with:
cache-image: false
- uses: docker/setup-buildx-action@v4
- name: Build and probe the immutable platform package
env:
ALPINE_DIGEST: ${{ needs.build-alpine.outputs.image-digest }}
DEBIAN_DIGEST: ${{ needs.build-debian.outputs.image-digest }}
GH_TOKEN: ${{ github.token }}
BUNDLE_LIBC: ${{ matrix.libc }}
BUNDLE_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
digest="$ALPINE_DIGEST"
if [ "$BUNDLE_LIBC" = glibc ]; then digest="$DEBIAN_DIGEST"; fi
test -n "$digest"
image="ghcr.io/${GITHUB_REPOSITORY}@${digest}"
if [ "$BUNDLE_PLATFORM/$BUNDLE_LIBC" = linux/arm/v6/glibc ]; then
# This amd64 helper executes a genuine Raspbian ARMv6 userspace under
# ARM1176 QEMU. It is a CI validator, not a user runtime or bundle.
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
validator="ghcr.io/${GITHUB_REPOSITORY}:upgrade-armv6-glibc-validator-${GITHUB_SHA}"
mkdir -p output/validator
docker buildx build --platform linux/amd64 --build-arg "SOURCE_IMAGE=$image" --file docker/Dockerfile.armv6-glibc --target runtime --tag "$validator" --push --metadata-file output/validator/image.json .
validator_digest="$(jq -er '."containerimage.digest"' output/validator/image.json)"
docker buildx build --platform linux/amd64 --build-arg "BASE_IMAGE=${validator%:*}@${validator_digest}" --file docker/Dockerfile.upgrade-bundle-armv6-glibc --output type=local,dest=output/upgrade .
else
docker buildx build --platform "$BUNDLE_PLATFORM" --build-arg "BASE_IMAGE=$image" --file docker/Dockerfile.upgrade-bundle --output type=local,dest=output/upgrade .
fi
- uses: actions/upload-artifact@v6
with:
name: upgrade-${{ matrix.name }}-${{ github.sha }}
path: output/upgrade/*
if-no-files-found: error
publish-upgrade-bundles:
if: ${{ !inputs.static_sync_only && (github.ref == 'refs/heads/master' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/develop' && inputs.build_development_bundles)) }}
needs: build-upgrade-bundles
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: upgrade-channel-${{ github.ref_name }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20'
- uses: pnpm/action-setup@v6
with:
version: '8.3.1'
- run: pnpm install --prod --frozen-lockfile
- run: npm --prefix cli ci
- uses: actions/download-artifact@v7
with:
pattern: upgrade-*-${{ github.sha }}
path: output/upgrade
merge-multiple: true
- run: node scripts/build-bundle-installer.cjs output/upgrade/install-bundle.cjs
- name: Publish complete packages before advancing the channel
env:
GH_TOKEN: ${{ github.token }}
QL_BUILD_DEVELOPMENT_BUNDLES: ${{ inputs.build_development_bundles }}
run: node scripts/publish-upgrade-bundles.cjs output/upgrade "$GITHUB_SHA" "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME"