Files
Aether/.github/workflows/nightly.yml
T
elky 579f2c7cc1 feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
2026-09-04 03:45:52 +08:00

614 lines
21 KiB
YAML

name: Nightly Release
on:
# 02:17 Asia/Shanghai (18:17 UTC) every day.
schedule:
- cron: '17 18 * * *'
workflow_dispatch:
# Checks and builds only need read access. Publishing jobs opt into write access
# below so a failed build cannot modify the existing nightly release.
permissions:
actions: read
contents: read
# A rolling tag and image are shared by scheduled and manually retried runs.
# Keep GitHub Release immutability disabled for this repository: the tag and
# assets intentionally move after each successful daily build.
concurrency:
group: nightly-main
cancel-in-progress: false
env:
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_DEV_DEBUG: '0'
CARGO_PROFILE_TEST_DEBUG: '0'
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
GHCR_IMAGE: ghcr.io/fawney19/aether
jobs:
source:
name: Resolve main snapshot
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
sha: ${{ steps.snapshot.outputs.sha }}
short_sha: ${{ steps.snapshot.outputs.short_sha }}
date: ${{ steps.snapshot.outputs.date }}
steps:
- name: Require main branch
id: snapshot
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
echo "Nightly releases must run from refs/heads/main (got ${GITHUB_REF})." >&2
exit 1
fi
sha="${GITHUB_SHA}"
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
echo "short_sha=${sha:0:7}" >> "${GITHUB_OUTPUT}"
echo "date=$(date -u +'%Y-%m-%d')" >> "${GITHUB_OUTPUT}"
echo "Building main at ${sha}."
# Keep the scheduled backend coverage in one place so it cannot drift from PR CI.
rust_ci:
name: Rust CI
needs: source
uses: ./.github/workflows/rust-ci.yml
rust_extended:
name: Rust extended checks
needs: source
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: 1.95.0
- name: Show Rust toolchain
run: rustc -Vv
- name: Rust cache
uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2
with:
shared-key: nightly-rust-1.95-${{ runner.os }}
workspaces: . -> target
- name: Setup sccache
uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- name: Check all workspace targets
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: 'true'
run: cargo check --workspace --all-targets --all-features --locked
- name: Run workspace doctests
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: 'true'
run: cargo test --workspace --all-features --doc --locked
- name: Show sccache stats
if: always()
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: 'true'
run: sccache --show-stats
frontend:
name: Frontend checks and build
needs: source
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
cache: npm
cache-dependency-path: |
frontend/package-lock.json
aether-vscodex/web/package-lock.json
# The frontend prebuild synchronizes the embedded VSCodex UI by running
# its build from a separate package. Install that package explicitly so
# vue-tsc can resolve vite/client, vitest/globals, and node types in a
# clean runner.
- name: Install VSCodex web dependencies
working-directory: aether-vscodex/web
run: npm ci
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Lint
working-directory: frontend
run: npx --no-install eslint .
- name: Type-check
working-directory: frontend
run: npm run type-check
- name: Run unit tests
working-directory: frontend
run: npm run test:run
- name: Build nightly frontend
working-directory: frontend
env:
AETHER_BUILD_VERSION: nightly-${{ needs.source.outputs.short_sha }}
AETHER_VERSION: nightly
run: npm run build
- name: Upload frontend artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: nightly-frontend-dist
path: frontend/dist/
if-no-files-found: error
overwrite: true
retention-days: 7
repository_health:
name: Repository health checks
needs: source
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
- name: Check generated format coverage matrix
run: python3 docs/api/generate_format_field_coverage.py --check
- name: Test pressure report checker
run: node --test tools/pressure/check_gateway_stage_report.test.js
checks:
name: Nightly check gate
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- source
- rust_ci
- rust_extended
- frontend
- repository_health
steps:
- name: Verify check jobs
shell: bash
run: |
set -euo pipefail
failed=0
echo "source=${{ needs.source.result }}"
echo "rust_ci=${{ needs.rust_ci.result }}"
echo "rust_extended=${{ needs.rust_extended.result }}"
echo "frontend=${{ needs.frontend.result }}"
echo "repository_health=${{ needs.repository_health.result }}"
for result in \
"${{ needs.source.result }}" \
"${{ needs.rust_ci.result }}" \
"${{ needs.rust_extended.result }}" \
"${{ needs.frontend.result }}" \
"${{ needs.repository_health.result }}"; do
if [[ "${result}" != "success" ]]; then
failed=1
fi
done
if [[ "${failed}" -ne 0 ]]; then
echo 'One or more nightly checks failed or were cancelled.' >&2
exit 1
fi
build:
name: Build ${{ matrix.name }}
needs: [source, checks]
if: ${{ needs.checks.result == 'success' }}
runs-on: ${{ matrix.os }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- name: linux-amd64
target: x86_64-unknown-linux-musl
platform: linux
arch: amd64
os: ubuntu-latest
use_cross: true
- name: linux-arm64
target: aarch64-unknown-linux-musl
platform: linux
arch: arm64
os: ubuntu-latest
use_cross: true
- name: macos-amd64
target: x86_64-apple-darwin
platform: macos
arch: amd64
os: macos-15-intel
use_cross: false
- name: macos-arm64
target: aarch64-apple-darwin
platform: macos
arch: arm64
os: macos-15
use_cross: false
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: 1.95.0
targets: ${{ matrix.target }}
- name: Rust cache
uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2
with:
shared-key: nightly-release-${{ matrix.target }}
workspaces: . -> target
- name: Install cross
if: matrix.use_cross
uses: taiki-e/install-action@1ae7257be536a92d9218a6b343dc6e6ba650f7e1 # cross
- name: Build release binary
env:
AETHER_BUILD_VERSION: nightly-${{ needs.source.outputs.short_sha }}
AETHER_VERSION: nightly
AETHER_BUILD_TYPE: release
CARGO_TERM_COLOR: always
shell: bash
run: |
if [[ "${{ matrix.use_cross }}" == "true" ]]; then
cross build --release --locked -p aether-gateway --target "${{ matrix.target }}"
else
cargo build --release --locked -p aether-gateway --target "${{ matrix.target }}"
fi
- name: Upload binary artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: nightly-gateway-${{ matrix.platform }}-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/aether-gateway
if-no-files-found: error
overwrite: true
retention-days: 7
docker:
name: Publish nightly GHCR image
needs: [source, checks, build]
if: ${{ needs.checks.result == 'success' && needs.build.result == 'success' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
packages: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Download Linux binaries and frontend
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
pattern: nightly-*
path: artifacts
merge-multiple: false
- name: Prepare Docker build context
shell: bash
run: |
set -euo pipefail
mkdir -p dist/frontend
cp artifacts/nightly-gateway-linux-amd64/aether-gateway dist/aether-gateway-amd64
cp artifacts/nightly-gateway-linux-arm64/aether-gateway dist/aether-gateway-arm64
chmod 0755 dist/aether-gateway-amd64 dist/aether-gateway-arm64
cp -R artifacts/nightly-frontend-dist/. dist/frontend/
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6 # v3
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push nightly image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ./Dockerfile.app
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.GHCR_IMAGE }}:nightly
${{ env.GHCR_IMAGE }}:nightly-${{ needs.source.outputs.sha }}
labels: |
org.opencontainers.image.title=Aether
org.opencontainers.image.version=nightly
org.opencontainers.image.revision=${{ needs.source.outputs.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
package:
name: Package nightly archives
needs: [source, checks, build]
if: ${{ needs.checks.result == 'success' && needs.build.result == 'success' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.source.outputs.sha }}
- name: Download nightly artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
pattern: nightly-*
path: artifacts
merge-multiple: false
- name: Build nightly release packages
shell: bash
env:
SOURCE_REF: ${{ needs.source.outputs.sha }}
run: |
set -euo pipefail
VERSION="nightly"
mkdir -p package release-assets
for platform in linux macos; do
for arch in amd64 arm64; do
bundle="aether-${VERSION}-${platform}-${arch}"
root="package/${bundle}"
mkdir -p "${root}/bin" "${root}/frontend"
install -m 0755 \
"artifacts/nightly-gateway-${platform}-${arch}/aether-gateway" \
"${root}/bin/aether-gateway"
cp -R artifacts/nightly-frontend-dist/. "${root}/frontend/"
sed \
-e "s/^SOURCE_REF=\"\${AETHER_SOURCE_REF:-main}\"/SOURCE_REF=\"\${AETHER_SOURCE_REF:-${SOURCE_REF}}\"/" \
-e "s/^VERSION=\"\${AETHER_VERSION:-}\"/VERSION=\"\${AETHER_VERSION:-${VERSION}}\"/" \
install.sh > "${root}/install.sh"
chmod 0755 "${root}/install.sh"
install -m 0755 update.sh "${root}/update.sh"
install -m 0644 docker-compose.yml "${root}/docker-compose.yml"
install -m 0644 docker-compose.single-node.yml "${root}/docker-compose.single-node.yml"
install -m 0644 .env.example "${root}/.env.example"
install -m 0755 generate_keys.sh "${root}/generate_keys.sh"
install -m 0644 README.md "${root}/README.md"
install -m 0644 LICENSE "${root}/LICENSE"
tar -C package -czf "release-assets/${bundle}.tar.gz" "${bundle}"
done
done
sed \
-e "s/^SOURCE_REF=\"\${AETHER_SOURCE_REF:-main}\"/SOURCE_REF=\"\${AETHER_SOURCE_REF:-${SOURCE_REF}}\"/" \
-e "s/^VERSION=\"\${AETHER_VERSION:-}\"/VERSION=\"\${AETHER_VERSION:-${VERSION}}\"/" \
install.sh > release-assets/install.sh
chmod 0755 release-assets/install.sh
(cd release-assets && sha256sum *.tar.gz > SHA256SUMS)
test "$(find release-assets -maxdepth 1 -name '*.tar.gz' | wc -l)" -eq 4
test "$(wc -l < release-assets/SHA256SUMS)" -eq 4
(cd release-assets && sha256sum -c SHA256SUMS)
for archive in release-assets/*.tar.gz; do
tar -tzf "${archive}" >/dev/null
done
- name: Upload nightly package artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: nightly-release-assets
path: release-assets/*
if-no-files-found: error
overwrite: true
retention-days: 7
github_release:
name: Publish nightly GitHub Release
needs: [source, checks, docker, package]
if: ${{ needs.checks.result == 'success' && needs.docker.result == 'success' && needs.package.result == 'success' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
steps:
- name: Download nightly package artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: nightly-release-assets
path: release-assets
- name: Update rolling nightly release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
RELEASE_TAG: nightly
SOURCE_SHA: ${{ needs.source.outputs.sha }}
SOURCE_SHORT_SHA: ${{ needs.source.outputs.short_sha }}
RELEASE_DATE: ${{ needs.source.outputs.date }}
run: |
set -euo pipefail
release_title="Aether Nightly ${RELEASE_DATE} (${SOURCE_SHORT_SHA})"
notes_file="${RUNNER_TEMP}/nightly-release-notes.md"
cat > "${notes_file}" <<EOF
## Aether nightly
This rolling prerelease was built from [main commit ${SOURCE_SHORT_SHA}](https://github.com/${REPOSITORY}/commit/${SOURCE_SHA}).
- Source branch: main
- Source commit: ${SOURCE_SHA}
- Build date (UTC): ${RELEASE_DATE}
- Container image: ${GHCR_IMAGE}:nightly
- Commit image: ${GHCR_IMAGE}:nightly-${SOURCE_SHA}
The nightly tag and assets are replaced by the next successful daily build.
EOF
# Create a draft on the first run. Later runs repair the same rolling
# release on retry if any upload or metadata update is interrupted.
if ! gh release view "${RELEASE_TAG}" --repo "${REPOSITORY}" >/dev/null 2>&1; then
gh release create "${RELEASE_TAG}" \
--repo "${REPOSITORY}" \
--draft \
--prerelease \
--latest=false \
--target "${SOURCE_SHA}" \
--title "${release_title}" \
--notes-file "${notes_file}"
fi
# Upload archives first, then the checksum/installer metadata. This
# keeps a failed upload from leaving a checksum that describes files
# which have not reached the Release yet.
gh release upload "${RELEASE_TAG}" release-assets/*.tar.gz \
--repo "${REPOSITORY}" \
--clobber
gh release upload "${RELEASE_TAG}" \
release-assets/SHA256SUMS \
release-assets/install.sh \
--repo "${REPOSITORY}" \
--clobber
# target_commitish does not move an existing git tag. Move the ref
# only after the complete asset set is available.
if gh api "repos/${REPOSITORY}/git/ref/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
gh api -X PATCH "repos/${REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \
-f "sha=${SOURCE_SHA}" \
-F 'force=true' >/dev/null
else
gh api -X POST "repos/${REPOSITORY}/git/refs" \
-f "ref=refs/tags/${RELEASE_TAG}" \
-f "sha=${SOURCE_SHA}" >/dev/null
fi
gh release edit "${RELEASE_TAG}" \
--repo "${REPOSITORY}" \
--draft=false \
--prerelease \
--latest=false \
--target "${SOURCE_SHA}" \
--title "${release_title}" \
--notes-file "${notes_file}"
expected_assets=(
aether-nightly-linux-amd64.tar.gz
aether-nightly-linux-arm64.tar.gz
aether-nightly-macos-amd64.tar.gz
aether-nightly-macos-arm64.tar.gz
SHA256SUMS
install.sh
)
asset_names="$(gh release view "${RELEASE_TAG}" --repo "${REPOSITORY}" --json assets --jq '.assets[].name')"
for expected_asset in "${expected_assets[@]}"; do
if ! grep -Fxq "${expected_asset}" <<<"${asset_names}"; then
echo "Published release is missing asset ${expected_asset}." >&2
exit 1
fi
done
resolved_sha=""
for attempt in {1..10}; do
resolved_sha="$(gh api "repos/${REPOSITORY}/commits/${RELEASE_TAG}" --jq '.sha' 2>/dev/null || true)"
if [[ "${resolved_sha}" == "${SOURCE_SHA}" ]]; then
break
fi
sleep 2
done
if [[ "${resolved_sha}" != "${SOURCE_SHA}" ]]; then
echo "nightly tag resolved to ${resolved_sha}, expected ${SOURCE_SHA}." >&2
exit 1
fi
release_state="$(gh release view "${RELEASE_TAG}" --repo "${REPOSITORY}" --json isDraft,isPrerelease --jq '[.isDraft, .isPrerelease] | @tsv')"
if [[ "${release_state}" != $'false\ttrue' ]]; then
echo "nightly release has unexpected state: ${release_state}" >&2
exit 1
fi
echo "Published ${RELEASE_TAG} for ${SOURCE_SHA}."
summary:
name: Nightly summary
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- source
- rust_ci
- rust_extended
- frontend
- repository_health
- checks
- build
- docker
- package
- github_release
steps:
- name: Verify nightly pipeline
shell: bash
run: |
set -euo pipefail
failed=0
for entry in \
"source=${{ needs.source.result }}" \
"rust_ci=${{ needs.rust_ci.result }}" \
"rust_extended=${{ needs.rust_extended.result }}" \
"frontend=${{ needs.frontend.result }}" \
"repository_health=${{ needs.repository_health.result }}" \
"checks=${{ needs.checks.result }}" \
"build=${{ needs.build.result }}" \
"docker=${{ needs.docker.result }}" \
"package=${{ needs.package.result }}" \
"github_release=${{ needs.github_release.result }}"; do
echo "${entry}"
if [[ "${entry#*=}" != "success" ]]; then
failed=1
fi
done
if [[ "${failed}" -ne 0 ]]; then
echo 'Nightly pipeline did not publish a new release.' >&2
exit 1
fi