Files
Aether/.github/workflows/release.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

471 lines
16 KiB
YAML

name: Release Aether
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
actions: read
contents: read
concurrency:
group: release-aether-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
GHCR_IMAGE: fawney19/aether
DOCKERHUB_IMAGE: fawney19/aether
jobs:
preflight:
name: Release preflight
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.classify.outputs.publish }}
version_tag: ${{ steps.classify.outputs.version_tag }}
prerelease: ${{ steps.classify.outputs.prerelease }}
make_latest: ${{ steps.classify.outputs.make_latest }}
steps:
- name: Classify release tag
id: classify
shell: bash
run: |
set -euo pipefail
echo "publish=false" >> "${GITHUB_OUTPUT}"
echo "version_tag=" >> "${GITHUB_OUTPUT}"
echo "prerelease=false" >> "${GITHUB_OUTPUT}"
echo "make_latest=false" >> "${GITHUB_OUTPUT}"
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
echo "Manual release build; publish jobs will be skipped."
exit 0
fi
tag="${GITHUB_REF_NAME}"
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|rc)\.[0-9]+)?$ ]]; then
echo "Unsupported release tag: ${tag}" >&2
echo "Expected vX.Y.Z, vX.Y.Z-beta.N, or vX.Y.Z-rc.N." >&2
exit 1
fi
echo "version_tag=${tag}" >> "${GITHUB_OUTPUT}"
if [[ "${tag}" == *-* ]]; then
echo "prerelease=true" >> "${GITHUB_OUTPUT}"
else
echo "make_latest=true" >> "${GITHUB_OUTPUT}"
fi
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
echo "publish=true" >> "${GITHUB_OUTPUT}"
else
echo "Manual release build for ${tag}; publish jobs will be skipped."
fi
frontend:
name: Build frontend
needs: preflight
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
frontend/package-lock.json
aether-vscodex/web/package-lock.json
- name: Build aether-vscodex web
working-directory: aether-vscodex/web
run: |
npm ci
npm run build
- name: Install & build
working-directory: frontend
run: |
npm ci
npm run build
- name: Upload frontend artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: frontend-dist
path: frontend/dist/
if-no-files-found: error
retention-days: 1
vscodex:
name: Build VS Code Codex extension
needs: preflight
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
aether-vscodex/package-lock.json
aether-vscodex/web/package-lock.json
aether-vscodex/vscode-extension/package-lock.json
- name: Install module test dependencies
working-directory: aether-vscodex
run: npm ci
- name: Build the embedded Web UI
working-directory: aether-vscodex/web
run: |
npm ci
npm run build
- name: Install extension dependencies
working-directory: aether-vscodex/vscode-extension
run: npm ci
- name: Check and compile the extension
working-directory: aether-vscodex/vscode-extension
run: |
npm run check
npm run build
- name: Run module tests
working-directory: aether-vscodex
run: npm test
- name: Run Web UI tests
working-directory: aether-vscodex/web
run: npm test
- name: Package VSIX
working-directory: aether-vscodex/vscode-extension
shell: bash
run: |
set -euo pipefail
version="$(node -p "require('./package.json').version")"
npx --yes @vscode/vsce package --no-update-package-json --allow-missing-repository
source_vsix="codex-remote-collab-${version}.vsix"
test -f "${source_vsix}"
mv "${source_vsix}" "aether-vscodex-${version}.vsix"
unzip -l "aether-vscodex-${version}.vsix" | grep 'extension/node_modules/ws/index.js' >/dev/null
- name: Upload VSIX artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: aether-vscodex-vsix
path: aether-vscodex/vscode-extension/aether-vscodex-*.vsix
if-no-files-found: error
retention-days: 7
build:
name: Build ${{ matrix.name }}
needs: preflight
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
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
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
targets: ${{ matrix.target }}
- name: Rust cache
uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2
with:
shared-key: release-${{ matrix.target }}
workspaces: . -> target
- name: Install cross
if: matrix.use_cross
uses: taiki-e/install-action@1ae7257be536a92d9218a6b343dc6e6ba650f7e1 # cross
- name: Build
env:
AETHER_VERSION: ${{ needs.preflight.outputs.version_tag }}
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: aether-gateway-${{ matrix.platform }}-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/aether-gateway
if-no-files-found: error
retention-days: 1
docker:
name: Docker multi-arch
needs: [preflight, frontend, build]
if: needs.preflight.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
attestations: write
contents: read
id-token: write
packages: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Download all artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
path: artifacts
- name: Prepare dist layout
run: |
mkdir -p dist
cp artifacts/aether-gateway-linux-amd64/aether-gateway dist/aether-gateway-amd64
cp artifacts/aether-gateway-linux-arm64/aether-gateway dist/aether-gateway-arm64
chmod +x dist/aether-gateway-amd64 dist/aether-gateway-arm64
cp -r artifacts/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@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: |
${{ env.REGISTRY }}/${{ env.GHCR_IMAGE }}
docker.io/${{ env.DOCKERHUB_IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ needs.preflight.outputs.make_latest == 'true' }}
type=raw,value=latest,enable=${{ needs.preflight.outputs.make_latest == 'true' }}
type=raw,value=beta,enable=${{ contains(github.ref_name, '-beta.') }}
type=raw,value=rc,enable=${{ contains(github.ref_name, '-rc.') }}
type=sha,prefix=
flavor: |
latest=false
- name: Build and push
id: push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ./Dockerfile.app
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
- name: Attest GHCR image provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Attest Docker Hub image provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: docker.io/${{ env.DOCKERHUB_IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
create-storage-record: false
package:
name: Release tarballs
needs: [preflight, frontend, build]
runs-on: ubuntu-latest
permissions:
actions: read
attestations: write
contents: read
id-token: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Download all artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
path: artifacts
- name: Build release packages
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
VERSION="${GITHUB_REF_NAME}"
SOURCE_REF="${GITHUB_REF_NAME}"
else
VERSION="snapshot-${GITHUB_SHA::7}"
SOURCE_REF="${GITHUB_SHA}"
fi
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/aether-gateway-${platform}-${arch}/aether-gateway" "${root}/bin/aether-gateway"
cp -R artifacts/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 +x release-assets/install.sh
(cd release-assets && sha256sum *.tar.gz > SHA256SUMS)
- name: Attest release package provenance
id: attest-release
if: needs.preflight.outputs.publish == 'true'
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
release-assets/*.tar.gz
release-assets/install.sh
release-assets/SHA256SUMS
- name: Bundle release package provenance
if: needs.preflight.outputs.publish == 'true'
env:
ATTESTATION_BUNDLE: ${{ steps.attest-release.outputs.bundle-path }}
run: install -m 0644 "${ATTESTATION_BUNDLE}" release-assets/AETHER_RELEASE_PROVENANCE.sigstore.json
- name: Upload release package artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: release-assets
path: release-assets/*
if-no-files-found: error
retention-days: 7
github-release:
name: GitHub Release assets
needs: [preflight, docker, package, vscodex]
if: needs.preflight.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
steps:
- name: Download release package artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: release-assets
path: release-assets
- name: Download VSIX artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: aether-vscodex-vsix
path: release-assets
- name: Delete stale draft releases for tag
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
REPOSITORY: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
draft_ids="$(gh api "repos/${REPOSITORY}/releases" --paginate --jq '.[] | select(.tag_name == env.RELEASE_TAG and .draft == true) | .id')"
if [[ -z "${draft_ids}" ]]; then
echo "No stale draft releases for ${RELEASE_TAG}"
exit 0
fi
while IFS= read -r release_id; do
[[ -z "${release_id}" ]] && continue
echo "Deleting stale draft release ${release_id} for ${RELEASE_TAG}"
gh api -X DELETE "repos/${REPOSITORY}/releases/${release_id}"
done <<< "${draft_ids}"
- name: Publish GitHub Release assets
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
generate_release_notes: true
prerelease: ${{ needs.preflight.outputs.prerelease }}
make_latest: ${{ needs.preflight.outputs.make_latest }}
files: |
release-assets/*.tar.gz
release-assets/AETHER_RELEASE_PROVENANCE.sigstore.json
release-assets/SHA256SUMS
release-assets/install.sh
release-assets/*.vsix