Publish verified Docker images to GHCR

Make deployments pull immutable CI-built images while keeping test failures diagnosable before any package is published.
This commit is contained in:
Rocky
2026-08-16 14:56:36 +08:00
parent 0af35d44f4
commit 41e2145334
7 changed files with 74 additions and 16 deletions
+2 -1
View File
@@ -4,7 +4,8 @@ PORT=8080
PUBLIC_BASE_URL=https://account.osglab.com PUBLIC_BASE_URL=https://account.osglab.com
INVITE_BASE_URL=https://osglab.com/i INVITE_BASE_URL=https://osglab.com/i
APP_STORE_URL=https://apps.apple.com/app/id0000000000 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_BIND_PORT=18080
ACCOUNT_DOCKER_NETWORK=account-backend ACCOUNT_DOCKER_NETWORK=account-backend
+43
View File
@@ -4,6 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: [main] branches: [main]
tags: ["v*"]
permissions: permissions:
contents: read contents: read
@@ -41,7 +42,49 @@ jobs:
run: docker compose -f compose.yaml config --quiet run: docker compose -f compose.yaml config --quiet
- name: Test - name: Test
run: ./gradlew --no-daemon clean 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 - name: Build deployable JAR
run: ./gradlew --no-daemon buildFatJar run: ./gradlew --no-daemon buildFatJar
- name: Build container - name: Build container
run: docker build -t osg-account-server:ci . 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
+10 -2
View File
@@ -204,13 +204,21 @@ Deployment:
# Create this only when the existing MySQL is containerized and the network is absent. # Create this only when the existing MySQL is containerized and the network is absent.
docker network create --internal account-backend 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. # Put deployment values in an uncommitted .env or 1Panel secret/environment store.
docker compose config docker compose config --quiet
docker compose up -d --build docker compose pull
docker compose up -d
docker compose ps docker compose ps
curl --fail http://127.0.0.1:18080/health/ready 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-<commit>` 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 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 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 the separate egress network for Apple and provider HTTPS calls. Never publish MySQL port 3306 to the
+5
View File
@@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins { plugins {
kotlin("jvm") version "2.4.10" kotlin("jvm") version "2.4.10"
@@ -85,6 +86,10 @@ dependencies {
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
testLogging {
events("failed", "skipped")
exceptionFormat = TestExceptionFormat.FULL
}
finalizedBy(tasks.jacocoTestReport) finalizedBy(tasks.jacocoTestReport)
} }
+2 -4
View File
@@ -1,9 +1,7 @@
services: services:
account-server: account-server:
build: image: ${ACCOUNT_IMAGE:-ghcr.io/hkgood/osg-account-server}:${IMAGE_TAG:-main}
context: . pull_policy: always
dockerfile: Dockerfile
image: osg-account-server:${IMAGE_TAG:-local}
restart: unless-stopped restart: unless-stopped
init: true init: true
user: "10001:10001" user: "10001:10001"
+8 -4
View File
@@ -99,12 +99,16 @@ v3 WSS endpoint、资源 ID 和 API KeyDeepSeek 使用 HTTPS endpoint、已
## 5. 构建与启动 ## 5. 构建与启动
先检查变量插值。`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单: GitHub CI 在测试通过后发布私有镜像
`ghcr.io/hkgood/osg-account-server`。先使用仅有 `read:packages` 权限的部署令牌登录 GHCR
令牌不要写入 `.env`、Compose、1Panel 截图或 shell 历史。然后检查变量插值并拉取镜像。
`docker compose config` 会展开秘密,不要把输出上传或粘贴到工单:
```bash ```bash
./gradlew test ./gradlew test
echo "$GHCR_TOKEN" | docker login ghcr.io -u hkgood --password-stdin
docker compose config --quiet docker compose config --quiet
docker compose build --pull docker compose pull
docker compose up -d docker compose up -d
docker compose ps docker compose ps
curl --fail http://127.0.0.1:18080/health/ready curl --fail http://127.0.0.1:18080/health/ready
@@ -175,8 +179,8 @@ curl -i https://osglab.com/i/AbCdEf0123456789_-AbCd
更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建: 更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建:
```bash ```bash
IMAGE_TAG=<release-tag> docker compose build IMAGE_TAG=sha-<commit> docker compose pull
IMAGE_TAG=<release-tag> docker compose up -d IMAGE_TAG=sha-<commit> docker compose up -d
``` ```
Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响 Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响
@@ -24,6 +24,8 @@ class DeploymentConsistencyTest : FunSpec({
val compose = root.read("compose.yaml") val compose = root.read("compose.yaml")
compose shouldContain "127.0.0.1:\${ACCOUNT_BIND_PORT:-18080}:8080" 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 "external: true"
compose shouldContain "account-egress:" compose shouldContain "account-egress:"
compose shouldContain "user: \"10001:10001\"" compose shouldContain "user: \"10001:10001\""
@@ -63,6 +65,8 @@ class DeploymentConsistencyTest : FunSpec({
ci shouldContain "docker compose -f compose.yaml config --quiet" ci shouldContain "docker compose -f compose.yaml config --quiet"
ci shouldContain "./gradlew --no-daemon clean test" ci shouldContain "./gradlew --no-daemon clean test"
ci shouldContain "./gradlew --no-daemon buildFatJar" ci shouldContain "./gradlew --no-daemon buildFatJar"
ci shouldContain "docker/build-push-action@v6"
ci shouldContain "packages: write"
ci shouldNotContain "3306:3306" ci shouldNotContain "3306:3306"
ci shouldNotContain "TEST_DB_" ci shouldNotContain "TEST_DB_"
} }
@@ -91,11 +95,6 @@ private val EXPECTED_PUBLIC_PATHS = setOf(
"/v1/credits/balance", "/v1/credits/balance",
"/v1/credits/ledger", "/v1/credits/ledger",
"/v1/credits/rates", "/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",
"/v1/referrals/me", "/v1/referrals/me",
"/v1/referrals/code", "/v1/referrals/code",