ci(release): add runtime builder runner workflow
This commit is contained in:
@@ -77,12 +77,53 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
|
||||
build-production-runtime-images:
|
||||
name: Build Production Runtime Images
|
||||
runs-on: runtime-builder
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
archive_url="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz"
|
||||
wget --header="Authorization: token ${GITHUB_TOKEN}" -O /tmp/repo.tar.gz "$archive_url"
|
||||
tar -xzf /tmp/repo.tar.gz --strip-components=1 -C .
|
||||
rm -f /tmp/repo.tar.gz
|
||||
|
||||
- name: Build runtime image artifact
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
chmod +x scripts/build_release_images.sh
|
||||
scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Upload runtime image artifact
|
||||
shell: sh
|
||||
env:
|
||||
PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
|
||||
PRODUCTION_SSH_USER: ${{ secrets.PRODUCTION_SSH_USER }}
|
||||
PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
|
||||
run: |
|
||||
set -eu
|
||||
test -n "$PRODUCTION_SSH_HOST"
|
||||
test -n "$PRODUCTION_SSH_USER"
|
||||
test -n "$PRODUCTION_SSH_KEY"
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "$PRODUCTION_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H "$PRODUCTION_SSH_HOST" >> ~/.ssh/known_hosts
|
||||
scp "dist/release-images/xiaoxia-runtime-images-${GITHUB_REF_NAME}.tar" \
|
||||
"$PRODUCTION_SSH_USER@$PRODUCTION_SSH_HOST:/var/lib/xiaoxia-saas-production/runtime-images-${GITHUB_REF_NAME}.tar"
|
||||
|
||||
deploy-production:
|
||||
name: Deploy Production
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker:27-cli
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: build-production-runtime-images
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Dedicated Gitea Runtime Builder Runner
|
||||
|
||||
> Goal: build API/Worker runtime image tar on a machine that does not host production services.
|
||||
|
||||
## Required machine
|
||||
|
||||
Minimum recommended spec:
|
||||
|
||||
- 2 vCPU minimum, 4 vCPU preferred.
|
||||
- 4GiB RAM minimum, 8GiB preferred.
|
||||
- 40GiB disk minimum.
|
||||
- Docker Engine installed.
|
||||
- Network access to Gitea and production SSH.
|
||||
|
||||
Do not install this runner on the current production host.
|
||||
|
||||
## Runner label
|
||||
|
||||
The production runtime image workflow targets:
|
||||
|
||||
```yaml
|
||||
runs-on: runtime-builder
|
||||
```
|
||||
|
||||
Register the new runner with a label named `runtime-builder`. Keep the existing production/staging runner separate.
|
||||
|
||||
## Required Gitea secrets
|
||||
|
||||
Configure repository or organization secrets:
|
||||
|
||||
- `PRODUCTION_SSH_HOST`
|
||||
- `PRODUCTION_SSH_USER`
|
||||
- `PRODUCTION_SSH_KEY`
|
||||
|
||||
The key must allow uploading to:
|
||||
|
||||
```text
|
||||
/var/lib/xiaoxia-saas-production/runtime-images-<tag>.tar
|
||||
```
|
||||
|
||||
Do not store SSH private keys in the repository.
|
||||
|
||||
## Workflow behavior
|
||||
|
||||
On `v*` tag push:
|
||||
|
||||
1. `build-production-runtime-images` runs on `runtime-builder`.
|
||||
2. It executes `scripts/build_release_images.sh <tag>`.
|
||||
3. It uploads `dist/release-images/xiaoxia-runtime-images-<tag>.tar` to production.
|
||||
4. `deploy-production` runs after the build job succeeds.
|
||||
5. `deploy-production` loads `/var/lib/xiaoxia-saas-production/runtime-images-<tag>.tar` and restarts API/Worker/Web.
|
||||
|
||||
If the tar is missing, production deploy must fail.
|
||||
|
||||
## Preflight checks on runner
|
||||
|
||||
Run on the new runner before registration:
|
||||
|
||||
```bash
|
||||
docker version
|
||||
docker ps --format '{{.Names}}' | grep -Eq '^(xiaoxia-(api|web|worker|postgres|redis)-production|gitea)$' && exit 1 || true
|
||||
```
|
||||
|
||||
The second command must not find production containers.
|
||||
|
||||
## Validation release
|
||||
|
||||
After runner registration:
|
||||
|
||||
1. Push a test tag only after `develop` is green.
|
||||
2. Confirm runtime image build job runs on the dedicated runner.
|
||||
3. Confirm production deploy waits for the image job.
|
||||
4. Run:
|
||||
|
||||
```bash
|
||||
python scripts/smoke_public_auth_flow.py
|
||||
python scripts/smoke_public_upload_flow.py
|
||||
```
|
||||
|
||||
5. Record release result in the Phase progress document.
|
||||
@@ -154,6 +154,18 @@ def test_subscription_api_is_not_wired_to_ui_until_backend_exists():
|
||||
assert importers == []
|
||||
|
||||
|
||||
def test_gitea_production_deploy_requires_runtime_builder_job():
|
||||
workflow = Path(".gitea/workflows/deploy.yml").read_text(encoding="utf-8")
|
||||
build_section = workflow.split("build-production-runtime-images:", 1)[1].split("deploy-production:", 1)[0]
|
||||
production_section = workflow.split("deploy-production:", 1)[1]
|
||||
|
||||
assert "runs-on: runtime-builder" in build_section
|
||||
assert "scripts/build_release_images.sh \"${GITHUB_REF_NAME}\"" in build_section
|
||||
assert "PRODUCTION_SSH_HOST" in build_section
|
||||
assert "runtime-images-${GITHUB_REF_NAME}.tar" in build_section
|
||||
assert "needs: build-production-runtime-images" in production_section
|
||||
|
||||
|
||||
def test_build_host_runbook_requires_off_production_runtime_builds():
|
||||
runbook = Path("docs/BUILD-HOST-RUNBOOK.md").read_text(encoding="utf-8")
|
||||
|
||||
@@ -164,6 +176,17 @@ def test_build_host_runbook_requires_off_production_runtime_builds():
|
||||
assert "python scripts/smoke_public_upload_flow.py" in runbook
|
||||
|
||||
|
||||
def test_runtime_builder_runner_runbook_matches_workflow():
|
||||
runbook = Path("docs/GITEA-RUNTIME-BUILDER-RUNNER.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "runs-on: runtime-builder" in runbook
|
||||
assert "PRODUCTION_SSH_HOST" in runbook
|
||||
assert "PRODUCTION_SSH_USER" in runbook
|
||||
assert "PRODUCTION_SSH_KEY" in runbook
|
||||
assert "Do not install this runner on the current production host" in runbook
|
||||
assert "production deploy waits for the image job" in runbook
|
||||
|
||||
|
||||
def test_deployment_docs_forbid_production_runtime_builds():
|
||||
docs = Path("docs/DEPLOYMENT.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user