mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Introduce install checkpoint support and resume logic for updates, plus related locking and validation. Adds InstallCheckpoint model, AppJsonContext serialization, and UpdatePaths helpers for deployment lock, apply-in-progress lock and install-checkpoint path. UpdateEngineService gains checkpoint load/save/delete, incoming-state validation, resume logic for PLONDS and legacy updates, apply lock handling, and safer cleanup; ApplyPendingPlondsUpdateAsync and ApplyPendingUpdate flow updated accordingly. Add DeploymentLock contract and extend UpdateState with pause/resume/cancel helpers. Tests updated to cover stale/valid checkpoint resume and legacy/PLONDS flows. CI: enhance ddss-publish to detect release channel, validate S3 assets, prepare and atomically publish channel pointer; add ddss-rollback workflow to publish rollbacks; adjust plonds-build concurrency and release events.
382 lines
14 KiB
YAML
382 lines
14 KiB
YAML
name: DDSS
|
|
|
|
concurrency:
|
|
group: ddss-${{ github.event_name }}-${{ github.event.workflow_run.id || github.event.inputs.tag || github.run_id }}
|
|
cancel-in-progress: false
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows:
|
|
- PLONDS
|
|
types:
|
|
- completed
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Release tag'
|
|
required: true
|
|
type: string
|
|
|
|
env:
|
|
DOTNET_VERSION: '10.0.x'
|
|
|
|
jobs:
|
|
publish:
|
|
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
actions: read
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
submodules: recursive
|
|
|
|
- name: Resolve release tag and channel
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
RAW_TAG="${{ github.event.inputs.tag }}"
|
|
if [[ "$RAW_TAG" == v* ]]; then
|
|
TAG="$RAW_TAG"
|
|
else
|
|
TAG="v$RAW_TAG"
|
|
fi
|
|
else
|
|
gh run download "${{ github.event.workflow_run.id }}" -n plonds-run-metadata -D plonds-run-metadata
|
|
TAG="$(tr -d '\r\n' < plonds-run-metadata/tag.txt)"
|
|
fi
|
|
|
|
echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV"
|
|
IS_PRERELEASE="$(gh release view "$TAG" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease')"
|
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
|
CHANNEL="preview"
|
|
else
|
|
CHANNEL="stable"
|
|
fi
|
|
echo "RELEASE_CHANNEL=${CHANNEL}" >> "$GITHUB_ENV"
|
|
echo "DDSS_CHANNEL_POINTER_KEY=lanmountain/update/meta/channels/${CHANNEL}/ddss-latest.json" >> "$GITHUB_ENV"
|
|
PUBLIC_BASE="${{ vars.S3_PUBLIC_BASE_URL }}"
|
|
if [[ -z "$PUBLIC_BASE" ]]; then
|
|
PUBLIC_BASE="https://cn-nb1.rains3.com/lmdesktop/lanmountain/update"
|
|
fi
|
|
PUBLIC_BASE="${PUBLIC_BASE%/}"
|
|
echo "S3_PUBLIC_BASE_URL=${PUBLIC_BASE}" >> "$GITHUB_ENV"
|
|
echo "S3_BASE_URL=${PUBLIC_BASE}/releases/${TAG}/assets" >> "$GITHUB_ENV"
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
dotnet-quality: preview
|
|
|
|
- name: Prepare signing key
|
|
env:
|
|
UPDATE_PRIVATE_KEY_PEM: ${{ secrets.UPDATE_PRIVATE_KEY_PEM }}
|
|
PLONDS_SIGNING_KEY: ${{ secrets.PLONDS_SIGNING_KEY }}
|
|
PDC_SIGNING_KEY: ${{ secrets.PDC_SIGNING_KEY }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
KEY="${PLONDS_SIGNING_KEY:-}"
|
|
if [[ -z "$KEY" ]]; then KEY="${UPDATE_PRIVATE_KEY_PEM:-}"; fi
|
|
if [[ -z "$KEY" ]]; then KEY="${PDC_SIGNING_KEY:-}"; fi
|
|
if [[ -z "$KEY" ]]; then
|
|
echo "No signing key is configured."
|
|
exit 1
|
|
fi
|
|
printf '%s' "$KEY" > update-private-key.pem
|
|
echo "UPDATE_PRIVATE_KEY_PATH=$PWD/update-private-key.pem" >> "$GITHUB_ENV"
|
|
|
|
- name: Build PLONDS tool
|
|
run: dotnet build PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Plonds.Tool.csproj -c Release
|
|
|
|
- name: Download release assets
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p release-assets
|
|
gh release download "$RELEASE_TAG" -D release-assets
|
|
find release-assets -maxdepth 1 -type f | sort
|
|
|
|
- name: Prepare PLONDS static output
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
rm -rf plonds-static
|
|
mkdir -p plonds-static
|
|
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
|
gh run download "${{ github.event.workflow_run.id }}" -n plonds-static -D plonds-static || true
|
|
fi
|
|
if [[ ! -d plonds-static/repo/sha256 && -f release-assets/plonds-static.zip ]]; then
|
|
unzip -q release-assets/plonds-static.zip -d plonds-static
|
|
fi
|
|
if [[ ! -d plonds-static/repo/sha256 || ! -d plonds-static/meta/channels || ! -d plonds-static/manifests ]]; then
|
|
echo "PLONDS static output is missing. Run the PLONDS workflow for this release first."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload release assets to Rainyun S3
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
aws --version
|
|
for file in release-assets/*; do
|
|
[[ -f "$file" ]] || continue
|
|
name="$(basename "$file")"
|
|
if [[ "$name" == "ddss.json" || "$name" == "ddss.json.sig" ]]; then
|
|
continue
|
|
fi
|
|
key="lanmountain/update/releases/${RELEASE_TAG}/assets/${name}"
|
|
sha256="$(sha256sum "$file" | awk '{print $1}')"
|
|
existing_sha="$(aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api head-object --bucket "$S3_BUCKET" --key "$key" --query 'Metadata.sha256' --output text 2>/dev/null || true)"
|
|
if [[ "$existing_sha" == "$sha256" ]]; then
|
|
echo "Skip existing asset: $name"
|
|
continue
|
|
fi
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api put-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$key" \
|
|
--body "$file" \
|
|
--metadata "sha256=$sha256"
|
|
done
|
|
|
|
- name: Upload PLONDS static output to Rainyun S3
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3 sync \
|
|
plonds-static/ \
|
|
"s3://$S3_BUCKET/lanmountain/update/" \
|
|
--only-show-errors
|
|
|
|
- name: Mirror installers to Rainyun S3
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
version="${RELEASE_TAG#v}"
|
|
for file in release-assets/*; do
|
|
[[ -f "$file" ]] || continue
|
|
name="$(basename "$file")"
|
|
platform=""
|
|
case "$name" in
|
|
*.exe)
|
|
if [[ "$name" == *x86* ]]; then platform="windows-x86"; else platform="windows-x64"; fi
|
|
;;
|
|
*.deb)
|
|
platform="linux-x64"
|
|
;;
|
|
*.dmg)
|
|
if [[ "$name" == *arm64* ]]; then platform="macos-arm64"; else platform="macos-x64"; fi
|
|
;;
|
|
esac
|
|
[[ -n "$platform" ]] || continue
|
|
key="lanmountain/update/installers/${platform}/${version}/${name}"
|
|
sha256="$(sha256sum "$file" | awk '{print $1}')"
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api put-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$key" \
|
|
--body "$file" \
|
|
--metadata "sha256=$sha256"
|
|
done
|
|
|
|
- name: Build DDSS manifest
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p ddss-output
|
|
dotnet run --project PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Plonds.Tool.csproj --configuration Release -- \
|
|
build-ddss \
|
|
--release-tag "$RELEASE_TAG" \
|
|
--assets-dir release-assets \
|
|
--output-dir ddss-output \
|
|
--private-key "$UPDATE_PRIVATE_KEY_PATH" \
|
|
--repository "${{ github.repository }}" \
|
|
--s3-base-url "$S3_BASE_URL"
|
|
|
|
- name: Validate DDSS asset references in Rainyun S3
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
keys=$(jq -r '.assets[]?.mirrors[]?.url // empty' ddss-output/ddss.json \
|
|
| sed -n 's#^.*/lanmountain/update/\(.*\)$#lanmountain/update/\1#p' \
|
|
| sort -u)
|
|
|
|
if [[ -z "$keys" ]]; then
|
|
echo "No S3-backed asset URLs found in ddss.json"
|
|
exit 1
|
|
fi
|
|
|
|
while IFS= read -r key; do
|
|
[[ -n "$key" ]] || continue
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api head-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$key" >/dev/null
|
|
done <<< "$keys"
|
|
|
|
- name: Upload DDSS manifest to release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
gh release upload "$RELEASE_TAG" ddss-output/ddss.json ddss-output/ddss.json.sig --clobber
|
|
|
|
- name: Upload DDSS manifest to Rainyun S3 staging
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
for file in ddss-output/ddss.json ddss-output/ddss.json.sig; do
|
|
name="$(basename "$file")"
|
|
key="lanmountain/update/releases/${RELEASE_TAG}/assets/${name}"
|
|
sha256="$(sha256sum "$file" | awk '{print $1}')"
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api put-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$key" \
|
|
--body "$file" \
|
|
--metadata "sha256=$sha256"
|
|
done
|
|
|
|
- name: Prepare DDSS channel pointer
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pointer_file="ddss-output/ddss-latest.json"
|
|
cat > "$pointer_file" <<'JSON'
|
|
{
|
|
"schemaVersion": 1,
|
|
"channel": "__CHANNEL__",
|
|
"releaseTag": "__TAG__",
|
|
"version": "__VERSION__",
|
|
"updatedAt": "__UPDATED_AT__",
|
|
"manifest": {
|
|
"url": "__MANIFEST_URL__",
|
|
"signatureUrl": "__SIG_URL__"
|
|
}
|
|
}
|
|
JSON
|
|
|
|
manifest_url="${S3_BASE_URL}/ddss.json"
|
|
sig_url="${S3_BASE_URL}/ddss.json.sig"
|
|
version="${RELEASE_TAG#v}"
|
|
updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
sed -i "s|__CHANNEL__|${RELEASE_CHANNEL}|g" "$pointer_file"
|
|
sed -i "s|__TAG__|${RELEASE_TAG}|g" "$pointer_file"
|
|
sed -i "s|__VERSION__|${version}|g" "$pointer_file"
|
|
sed -i "s|__UPDATED_AT__|${updated_at}|g" "$pointer_file"
|
|
sed -i "s|__MANIFEST_URL__|${manifest_url}|g" "$pointer_file"
|
|
sed -i "s|__SIG_URL__|${sig_url}|g" "$pointer_file"
|
|
|
|
jq -e . "$pointer_file" >/dev/null
|
|
|
|
- name: Atomically publish DDSS channel pointer
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pointer_file="ddss-output/ddss-latest.json"
|
|
staging_key="lanmountain/update/releases/${RELEASE_TAG}/assets/ddss-latest.json"
|
|
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api put-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$staging_key" \
|
|
--body "$pointer_file"
|
|
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api put-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$DDSS_CHANNEL_POINTER_KEY" \
|
|
--body "$pointer_file"
|
|
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api head-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$DDSS_CHANNEL_POINTER_KEY" >/dev/null
|
|
|
|
curl -fsSI "$S3_PUBLIC_BASE_URL/meta/channels/${RELEASE_CHANNEL}/ddss-latest.json" >/dev/null
|
|
|
|
- name: Verify Rainyun S3 PLONDS output
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.S3_REGION }}
|
|
AWS_REGION: ${{ vars.S3_REGION }}
|
|
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mapfile -t required < <(
|
|
{
|
|
find plonds-static/meta/channels -path '*/latest.json' -type f | sort | head -n 1
|
|
find plonds-static/meta/distributions -name '*.json' -type f | sort | head -n 1
|
|
find plonds-static/manifests -name 'plonds-filemap.json' -type f | sort | head -n 1
|
|
find plonds-static/manifests -name 'plonds-filemap.json.sig' -type f | sort | head -n 1
|
|
find plonds-static/repo/sha256 -type f | sort | head -n 1
|
|
} | sed '/^$/d'
|
|
)
|
|
|
|
if [[ "${#required[@]}" -lt 5 ]]; then
|
|
echo "Not enough PLONDS static files to verify."
|
|
exit 1
|
|
fi
|
|
|
|
for path in "${required[@]}"; do
|
|
rel="${path#plonds-static/}"
|
|
key="lanmountain/update/${rel}"
|
|
aws --endpoint-url "$S3_ENDPOINT" --region "$AWS_REGION" s3api head-object \
|
|
--bucket "$S3_BUCKET" \
|
|
--key "$key" >/dev/null
|
|
curl -fsSI "$S3_PUBLIC_BASE_URL/$rel" >/dev/null
|
|
done
|