mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
* fix(ota): isolate updates in persistent workspaces Stage online and offline update archives under /root/.kvmcache/nanokvm-update-* and validate storage, manifests, and archive contents before changing the installed application. * fix(ota): harden storage safety and release gates Preserve the last rollback backup when update storage is insufficient, and verify the actual application mount point before installation. Move the shared transfer sentinel from /tmp to /run, enforce device package limits in release verification, and run that verification in package CI.
297 lines
10 KiB
YAML
297 lines
10 KiB
YAML
name: NanoKVM Package
|
|
|
|
# Builds nanokvm_<version>.tar.gz and the latest.json manifest that the
|
|
# on-device updater consumes (server/service/application/). Pull requests get a
|
|
# uniquely identified, short-lived test artifact. Release publication is a
|
|
# separate, explicit workflow that calls this one for an existing tag.
|
|
#
|
|
# This workflow does NOT publish to cdn.sipeed.com. Uploading latest.json is what
|
|
# actually offers the update to every device in the field, so that step stays
|
|
# manual and deliberate.
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- .github/workflows/create-tag.yml
|
|
- .github/workflows/package.yml
|
|
- .github/workflows/release.yml
|
|
- kvmapp/**
|
|
- scripts/**
|
|
- server/**
|
|
- support/**
|
|
- tools/nanokvm_update_edid/**
|
|
- web/**
|
|
- Makefile
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: Version to package, e.g. 2.4.4
|
|
required: true
|
|
type: string
|
|
workflow_call:
|
|
inputs:
|
|
version:
|
|
description: Numeric version to package
|
|
required: true
|
|
type: string
|
|
ref:
|
|
description: Existing tag or commit to check out
|
|
required: true
|
|
type: string
|
|
outputs:
|
|
artifact_name:
|
|
description: Uploaded Actions artifact name
|
|
value: ${{ jobs.package.outputs.artifact_name }}
|
|
version:
|
|
description: Packaged version
|
|
value: ${{ jobs.package.outputs.version }}
|
|
|
|
concurrency:
|
|
group: package-${{ github.event.pull_request.number || inputs.ref || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
package:
|
|
name: Build package
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 180
|
|
outputs:
|
|
artifact_name: ${{ steps.version.outputs.artifact_name }}
|
|
version: ${{ steps.version.outputs.version }}
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
|
|
steps:
|
|
- name: Checkout event ref
|
|
if: inputs.ref == ''
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# package.sh derives SOURCE_DATE_EPOCH from the commit date.
|
|
fetch-depth: 0
|
|
# Build scripts from a pull request must not inherit checkout's token.
|
|
persist-credentials: false
|
|
|
|
- name: Checkout requested ref
|
|
if: inputs.ref != ''
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
ref: ${{ inputs.ref }}
|
|
|
|
- name: Resolve version
|
|
id: version
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
BUILD_SHA=$(git rev-parse HEAD)
|
|
if [ "$EVENT_NAME" = "pull_request" ]; then
|
|
VERSION="0.${PR_NUMBER}.${GITHUB_RUN_NUMBER}"
|
|
SOURCE_SHA="$HEAD_SHA"
|
|
elif [ -n "$INPUT_VERSION" ]; then
|
|
VERSION="$INPUT_VERSION"
|
|
SOURCE_SHA="$BUILD_SHA"
|
|
else
|
|
VERSION="$REF_NAME"
|
|
SOURCE_SHA="$BUILD_SHA"
|
|
fi
|
|
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
echo "::error::invalid version '$VERSION', expected MAJOR.MINOR.PATCH"
|
|
exit 1
|
|
fi
|
|
if [ -z "$SOURCE_SHA" ]; then
|
|
echo "::error::could not resolve source commit"
|
|
exit 1
|
|
fi
|
|
SHORT_SHA=$(printf '%s' "$SOURCE_SHA" | cut -c1-12)
|
|
if [ "$EVENT_NAME" = "pull_request" ]; then
|
|
ARTIFACT_NAME="nanokvm-pr-${PR_NUMBER}-${SHORT_SHA}-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}"
|
|
else
|
|
ARTIFACT_NAME="nanokvm-${VERSION}-${SHORT_SHA}-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}"
|
|
fi
|
|
{
|
|
echo "artifact_name=$ARTIFACT_NAME"
|
|
echo "build_sha=$BUILD_SHA"
|
|
echo "source_sha=$SOURCE_SHA"
|
|
echo "version=$VERSION"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "Packaging version $VERSION as $ARTIFACT_NAME"
|
|
|
|
- name: Free up disk space
|
|
run: |
|
|
df -h /
|
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
|
df -h /
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Set up pnpm
|
|
run: npm install --global pnpm@11
|
|
|
|
- name: Build frontend
|
|
run: make web
|
|
|
|
- name: Log in to GHCR
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
GHCR_USER: ${{ github.actor }}
|
|
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
ok=0
|
|
for attempt in 1 2 3; do
|
|
if printf '%s' "$GHCR_TOKEN" \
|
|
| docker login ghcr.io -u "$GHCR_USER" --password-stdin; then
|
|
ok=1
|
|
break
|
|
fi
|
|
echo "login attempt $attempt failed, retrying in 15s"
|
|
sleep 15
|
|
done
|
|
if [ "$ok" -ne 1 ]; then
|
|
echo "::error::could not log in to ghcr.io after 3 attempts"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Pull builder image
|
|
id: image
|
|
env:
|
|
OWNER: ${{ github.repository_owner }}
|
|
run: |
|
|
# GHCR only accepts lowercase repository paths.
|
|
# shellcheck disable=SC2153 # OWNER is supplied through the step environment.
|
|
owner=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
|
|
IMAGE_REPO="ghcr.io/$owner/nanokvm-builder"
|
|
TAGGED_REF="${IMAGE_REPO}:latest"
|
|
ok=0
|
|
for attempt in 1 2 3; do
|
|
if docker pull "$TAGGED_REF"; then
|
|
ok=1
|
|
break
|
|
fi
|
|
echo "pull attempt $attempt failed, retrying in 15s"
|
|
sleep 15
|
|
done
|
|
if [ "$ok" -ne 1 ]; then
|
|
echo "::error::could not pull $TAGGED_REF - run the 'Builder Image' workflow first and ensure PR builds can pull it without credentials"
|
|
exit 1
|
|
fi
|
|
RESOLVED_REF=$(docker image inspect --format='{{index .RepoDigests 0}}' "$TAGGED_REF")
|
|
case "$RESOLVED_REF" in
|
|
"$IMAGE_REPO"@sha256:*) ;;
|
|
*)
|
|
echo "::error::could not resolve immutable digest for $TAGGED_REF (got '$RESOLVED_REF')"
|
|
exit 1
|
|
;;
|
|
esac
|
|
echo "Pulled $RESOLVED_REF"
|
|
echo "ref=$RESOLVED_REF" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build riscv64 artifacts
|
|
run: |
|
|
make release-build \
|
|
DOCKER_TTY= \
|
|
IMAGE_NAME="${{ steps.image.outputs.ref }}"
|
|
|
|
- name: Assemble package
|
|
run: make package VERSION="${{ steps.version.outputs.version }}"
|
|
|
|
- name: Compare against the published release
|
|
# Informational only: highlights what changed relative to what devices
|
|
# are currently running. Never blocks the build.
|
|
continue-on-error: true
|
|
run: |
|
|
./scripts/compare-release.sh \
|
|
"build/release/nanokvm_${{ steps.version.outputs.version }}.tar.gz"
|
|
|
|
- name: Write build provenance
|
|
env:
|
|
ARTIFACT_NAME: ${{ steps.version.outputs.artifact_name }}
|
|
BUILDER_IMAGE: ${{ steps.image.outputs.ref }}
|
|
BUILD_SHA: ${{ steps.version.outputs.build_sha }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
SOURCE_SHA: ${{ steps.version.outputs.source_sha }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
TARBALL="build/release/nanokvm_${VERSION}.tar.gz"
|
|
TARBALL_NAME=$(basename "$TARBALL")
|
|
SHA256_HEX=$(sha256sum "$TARBALL" | cut -d' ' -f1)
|
|
SHA512_HEX=$(sha512sum "$TARBALL" | cut -d' ' -f1)
|
|
SHA512_BASE64=$(jq -er '.sha512 | select(type == "string" and length > 0)' \
|
|
build/release/latest.json)
|
|
ACTUAL_BASE64=$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)
|
|
if [ "$SHA512_BASE64" != "$ACTUAL_BASE64" ]; then
|
|
echo "::error::latest.json sha512 does not match $TARBALL"
|
|
exit 1
|
|
fi
|
|
printf '%s %s\n' "$SHA256_HEX" "$TARBALL_NAME" > build/release/sha256.txt
|
|
{
|
|
echo "artifact=${ARTIFACT_NAME}"
|
|
echo "version=${VERSION}"
|
|
echo "event=${GITHUB_EVENT_NAME}"
|
|
echo "pull_request=${PR_NUMBER}"
|
|
echo "source_sha=${SOURCE_SHA}"
|
|
echo "build_sha=${BUILD_SHA}"
|
|
echo "builder_image=${BUILDER_IMAGE}"
|
|
echo "run_id=${GITHUB_RUN_ID}"
|
|
echo "run_attempt=${GITHUB_RUN_ATTEMPT}"
|
|
echo "run_url=https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
echo "tarball=${TARBALL_NAME}"
|
|
echo "sha256_hex=${SHA256_HEX}"
|
|
echo "sha512_hex=${SHA512_HEX}"
|
|
echo "sha512_base64=${SHA512_BASE64}"
|
|
} > build/release/BUILD_INFO.txt
|
|
|
|
- name: Verify release assets
|
|
run: |
|
|
./scripts/verify-release-assets.sh \
|
|
build/release "${{ steps.version.outputs.version }}"
|
|
|
|
- name: Summary
|
|
env:
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
{
|
|
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
|
echo "> [!WARNING]"
|
|
echo "> PR TEST ONLY — DO NOT PUBLISH TO CDN"
|
|
echo
|
|
fi
|
|
echo "### nanokvm_${VERSION}.tar.gz"
|
|
echo
|
|
echo '```text'
|
|
cat build/release/BUILD_INFO.txt
|
|
echo '```'
|
|
echo
|
|
echo '```json'
|
|
cat build/release/latest.json
|
|
echo '```'
|
|
echo
|
|
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
|
echo "Upload only the inner tarball through NanoKVM's manual offline update UI."
|
|
else
|
|
echo "Publishing to \`cdn.sipeed.com/nanokvm/\` is a separate manual step."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ steps.version.outputs.artifact_name }}
|
|
path: |
|
|
build/release/nanokvm_${{ steps.version.outputs.version }}.tar.gz
|
|
build/release/latest.json
|
|
build/release/sha256.txt
|
|
build/release/BUILD_INFO.txt
|
|
if-no-files-found: error
|
|
retention-days: ${{ github.event_name == 'pull_request' && 7 || 90 }}
|