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:
+2
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -99,12 +99,16 @@ v3 WSS endpoint、资源 ID 和 API Key;DeepSeek 使用 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",
|
||||||
|
|||||||
Reference in New Issue
Block a user