From 41e2145334e972249faabdf2153199ed388071bb Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:56:36 +0800 Subject: [PATCH] Publish verified Docker images to GHCR Make deployments pull immutable CI-built images while keeping test failures diagnosable before any package is published. --- .env.example | 3 +- .github/workflows/ci.yml | 43 +++++++++++++++++++ README.md | 12 +++++- build.gradle.kts | 5 +++ compose.yaml | 6 +-- docs/DEPLOYMENT.md | 12 ++++-- .../config/DeploymentConsistencyTest.kt | 9 ++-- 7 files changed, 74 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 79df78b..54abb0a 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,8 @@ PORT=8080 PUBLIC_BASE_URL=https://account.osglab.com INVITE_BASE_URL=https://osglab.com/i APP_STORE_URL=https://apps.apple.com/app/id0000000000 -IMAGE_TAG=local +ACCOUNT_IMAGE=ghcr.io/hkgood/osg-account-server +IMAGE_TAG=main ACCOUNT_BIND_PORT=18080 ACCOUNT_DOCKER_NETWORK=account-backend diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15e74d3..8068399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: pull_request: push: branches: [main] + tags: ["v*"] permissions: contents: read @@ -41,7 +42,49 @@ jobs: run: docker compose -f compose.yaml config --quiet - name: Test run: ./gradlew --no-daemon clean test + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports-${{ github.run_id }} + path: | + build/reports/tests/test + build/test-results/test + if-no-files-found: ignore - name: Build deployable JAR run: ./gradlew --no-daemon buildFatJar - name: Build container run: docker build -t osg-account-server:ci . + + publish: + if: github.event_name == 'push' + needs: verify + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository_owner }}/osg-account-server + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + - uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index e3af9c4..2e3c500 100644 --- a/README.md +++ b/README.md @@ -204,13 +204,21 @@ Deployment: # Create this only when the existing MySQL is containerized and the network is absent. docker network create --internal account-backend +# Authenticate once because the GHCR package is private. +echo "$GHCR_TOKEN" | docker login ghcr.io -u hkgood --password-stdin + # Put deployment values in an uncommitted .env or 1Panel secret/environment store. -docker compose config -docker compose up -d --build +docker compose config --quiet +docker compose pull +docker compose up -d docker compose ps curl --fail http://127.0.0.1:18080/health/ready ``` +Every successful `main` CI run publishes `ghcr.io/hkgood/osg-account-server:main` plus an immutable +`sha-` tag. Production should pin a tested immutable tag in `IMAGE_TAG`; use `main` only for +initial staging. `GHCR_TOKEN` needs package-read permission and must not be stored in `.env`. + If MySQL runs directly on the host or another private server, keep the external network declaration but set `DATABASE_URL` to a private hostname reachable from that network. The application also joins the separate egress network for Apple and provider HTTPS calls. Never publish MySQL port 3306 to the diff --git a/build.gradle.kts b/build.gradle.kts index 0b91447..65946db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { kotlin("jvm") version "2.4.10" @@ -85,6 +86,10 @@ dependencies { tasks.test { useJUnitPlatform() + testLogging { + events("failed", "skipped") + exceptionFormat = TestExceptionFormat.FULL + } finalizedBy(tasks.jacocoTestReport) } diff --git a/compose.yaml b/compose.yaml index 0fc32b1..5b1c9a3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,9 +1,7 @@ services: account-server: - build: - context: . - dockerfile: Dockerfile - image: osg-account-server:${IMAGE_TAG:-local} + image: ${ACCOUNT_IMAGE:-ghcr.io/hkgood/osg-account-server}:${IMAGE_TAG:-main} + pull_policy: always restart: unless-stopped init: true user: "10001:10001" diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index c5f8b81..4af229b 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -99,12 +99,16 @@ v3 WSS endpoint、资源 ID 和 API Key;DeepSeek 使用 HTTPS endpoint、已 ## 5. 构建与启动 -先检查变量插值。`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单: +GitHub CI 在测试通过后发布私有镜像 +`ghcr.io/hkgood/osg-account-server`。先使用仅有 `read:packages` 权限的部署令牌登录 GHCR; +令牌不要写入 `.env`、Compose、1Panel 截图或 shell 历史。然后检查变量插值并拉取镜像。 +`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单: ```bash ./gradlew test +echo "$GHCR_TOKEN" | docker login ghcr.io -u hkgood --password-stdin docker compose config --quiet -docker compose build --pull +docker compose pull docker compose up -d docker compose ps curl --fail http://127.0.0.1:18080/health/ready @@ -175,8 +179,8 @@ curl -i https://osglab.com/i/AbCdEf0123456789_-AbCd 更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建: ```bash -IMAGE_TAG= docker compose build -IMAGE_TAG= docker compose up -d +IMAGE_TAG=sha- docker compose pull +IMAGE_TAG=sha- docker compose up -d ``` Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响 diff --git a/src/test/kotlin/com/osglab/account/config/DeploymentConsistencyTest.kt b/src/test/kotlin/com/osglab/account/config/DeploymentConsistencyTest.kt index 42818e4..c521a56 100644 --- a/src/test/kotlin/com/osglab/account/config/DeploymentConsistencyTest.kt +++ b/src/test/kotlin/com/osglab/account/config/DeploymentConsistencyTest.kt @@ -24,6 +24,8 @@ class DeploymentConsistencyTest : FunSpec({ val compose = root.read("compose.yaml") compose shouldContain "127.0.0.1:\${ACCOUNT_BIND_PORT:-18080}:8080" + compose shouldContain "ghcr.io/hkgood/osg-account-server" + compose shouldContain "pull_policy: always" compose shouldContain "external: true" compose shouldContain "account-egress:" compose shouldContain "user: \"10001:10001\"" @@ -63,6 +65,8 @@ class DeploymentConsistencyTest : FunSpec({ ci shouldContain "docker compose -f compose.yaml config --quiet" ci shouldContain "./gradlew --no-daemon clean test" ci shouldContain "./gradlew --no-daemon buildFatJar" + ci shouldContain "docker/build-push-action@v6" + ci shouldContain "packages: write" ci shouldNotContain "3306:3306" ci shouldNotContain "TEST_DB_" } @@ -91,11 +95,6 @@ private val EXPECTED_PUBLIC_PATHS = setOf( "/v1/credits/balance", "/v1/credits/ledger", "/v1/credits/rates", - "/v1/credits/reservations", - "/v1/credits/reservations/{reservationId}", - "/v1/credits/reservations/{reservationId}/settle", - "/v1/credits/reservations/{reservationId}/release", - "/v1/credits/reservations/{reservationId}/refund", "/v1/referrals", "/v1/referrals/me", "/v1/referrals/code",