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