From 685646f88ea6ced3867741744b7099007edd596f Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 28 Aug 2026 11:14:15 +0800 Subject: [PATCH 1/2] ci(github): add release workflows for server/web - add github workflow for server release build and publish - add github workflow for web release package and deploy - adapt release metadata from gitea context to github context - upload release assets via github release action --- .github/workflows/server-build-and-deploy.yml | 95 +++++++++++++++ .github/workflows/web-build-and-deploy.yml | 109 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 .github/workflows/server-build-and-deploy.yml create mode 100644 .github/workflows/web-build-and-deploy.yml diff --git a/.github/workflows/server-build-and-deploy.yml b/.github/workflows/server-build-and-deploy.yml new file mode 100644 index 0000000..04bf611 --- /dev/null +++ b/.github/workflows/server-build-and-deploy.yml @@ -0,0 +1,95 @@ +name: Server Build and Deploy + +on: + release: + types: [published] + +permissions: + contents: write + +env: + APP_NAME: ahsarah-guide-server + +jobs: + build-and-release: + runs-on: ubuntu-latest + defaults: + run: + working-directory: server + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 (Corretto) + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: corretto + cache: gradle + + - name: Build with Gradle + run: > + ./gradlew bootJar -x test + -PartefactVersion="${{ github.event.release.tag_name }}" + -PbuildChannel=stable + -Pvendor=${{ vars.VENDOR }} + + - name: Resolve JAR file path + id: jar + run: | + JAR_PATH=$(find build/libs -name '*.jar' | head -1) + echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT" + + - name: Prepare release asset + id: asset + run: | + TAG="${{ github.event.release.tag_name }}" + FILE="${{ steps.jar.outputs.file }}" + ASSET_NAME="${APP_NAME}-${TAG}.jar" + cp "${FILE}" "../${ASSET_NAME}" + echo "path=${ASSET_NAME}" >> "$GITHUB_OUTPUT" + + - name: Upload JAR to GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.asset.outputs.path }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Generate image tags + id: meta + run: | + DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}" + echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + echo "tag_latest=${DOCKERHUB_USER}/${APP_NAME}:latest" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: server + file: server/Dockerfile.ci + build-args: | + JAR_FILE=${{ steps.jar.outputs.file }} + IMAGE_VERSION=${{ github.event.release.tag_name }} + IMAGE_VENDOR=${{ vars.VENDOR }} + BUILD_DATE=${{ github.event.release.published_at }} + push: true + tags: | + ${{ steps.meta.outputs.tag_version }} + ${{ steps.meta.outputs.tag_latest }} + + - name: Push README to Docker Hub + uses: christian-korneck/update-container-description-action@v1 + env: + DOCKER_USER: ${{ vars.DOCKER_HUB_USERNAME }} + DOCKER_PASS: ${{ secrets.DOCKER_HUB_TOKEN }} + with: + destination_container_repo: ${{ vars.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }} + provider: dockerhub + readme_file: server/README.md diff --git a/.github/workflows/web-build-and-deploy.yml b/.github/workflows/web-build-and-deploy.yml new file mode 100644 index 0000000..ff7d205 --- /dev/null +++ b/.github/workflows/web-build-and-deploy.yml @@ -0,0 +1,109 @@ +name: Web Build and Deploy + +on: + release: + types: [published] + +jobs: + build: + name: Build release archive + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build release archive + run: pnpm build:tar + + - name: Clean up previous build artefacts on server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.DEPLOY_HOST }} + username: ${{ vars.DEPLOY_USER }} + port: ${{ vars.DEPLOY_PORT }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + command: rm -rf /tmp/dist.tar.gz /tmp/dist.tar.gz/ + + - name: Upload artefact to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ vars.DEPLOY_HOST }} + username: ${{ vars.DEPLOY_USER }} + port: ${{ vars.DEPLOY_PORT }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + source: "dist.tar.gz" + target: "/tmp/" + + upload-release-asset: + name: Upload to GitHub Release + needs: build + runs-on: ubuntu-latest + + steps: + - name: Download artefact from server + env: + DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} + DEPLOY_USER: ${{ vars.DEPLOY_USER }} + DEPLOY_PORT: ${{ vars.DEPLOY_PORT }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + set -e + SSH_DIR="${RUNNER_TEMP}/ssh" + mkdir -p "$SSH_DIR" + chmod 700 "$SSH_DIR" + printf '%s\n' "$DEPLOY_SSH_KEY" > "$SSH_DIR/key" + chmod 600 "$SSH_DIR/key" + scp -i "$SSH_DIR/key" \ + -P "$DEPLOY_PORT" \ + -o StrictHostKeyChecking=accept-new \ + -o UserKnownHostsFile="$SSH_DIR/known_hosts" \ + "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/dist.tar.gz" \ + ./dist.tar.gz + rm -rf "$SSH_DIR" + + - name: Upload release asset to GitHub + uses: softprops/action-gh-release@v2 + with: + files: dist.tar.gz + + deploy-to-server: + name: Deploy to onixbyte.cn + needs: [build, upload-release-asset] + runs-on: ubuntu-latest + + steps: + - name: Extract archive and deploy + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.DEPLOY_HOST }} + username: ${{ vars.DEPLOY_USER }} + port: ${{ vars.DEPLOY_PORT }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script_stop: true + script: | + set -e + DEPLOY_PATH="${{ vars.DEPLOY_PATH }}" + mkdir -p "$DEPLOY_PATH" + rm -rf "$DEPLOY_PATH"/* + tar -xzf /tmp/dist.tar.gz -C "$DEPLOY_PATH" --strip-components=1 + chown -R caddy:caddy "$DEPLOY_PATH" + rm -f /tmp/dist.tar.gz From aca203020e8ffd3145db03dff141f830e1a1fb5f Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 1 Sep 2026 16:08:22 +0800 Subject: [PATCH 2/2] refactor(server): source build version from git tag - Stamp version, channel and vendor into META-INF/build-info.properties - Read build metadata at runtime from BuildProperties instead of expanded YAML - Remove app.common placeholders from application.yaml - Derive the OpenAPI info version from the packaged build version - Point the OCI image source label at the GitHub repository - Drop redundant repository blocks from settings.gradle.kts --- server/Dockerfile.ci | 2 +- server/build.gradle.kts | 27 +++++++++++-------- server/settings.gradle.kts | 13 --------- .../ahsarahguide/config/AppConfig.java | 3 --- .../config/OpenApiConfiguration.java | 21 ++++++++++++++- .../ahsarahguide/manager/AppManager.java | 14 +++++----- .../properties/AppProperties.java | 11 -------- server/src/main/resources/application.yaml | 5 ---- 8 files changed, 44 insertions(+), 52 deletions(-) delete mode 100644 server/src/main/java/com/onixbyte/ahsarahguide/properties/AppProperties.java diff --git a/server/Dockerfile.ci b/server/Dockerfile.ci index 521c3e6..ed9e0d1 100644 --- a/server/Dockerfile.ci +++ b/server/Dockerfile.ci @@ -12,7 +12,7 @@ LABEL org.opencontainers.image.title="ahsarah-guide-server" \ org.opencontainers.image.description="REST API backend for Ahsarah Guide" \ org.opencontainers.image.version="${IMAGE_VERSION}" \ org.opencontainers.image.vendor="${IMAGE_VENDOR}" \ - org.opencontainers.image.source="https://onixbyte.dev/onixbyte/ahsarah-guide" \ + org.opencontainers.image.source="https://github.com/onixbyte/ahsarah-guide" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.created="${BUILD_DATE}" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 8e0e7b0..1937210 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -23,6 +23,22 @@ java { } } +// Generate META-INF/build-info.properties at build time. The build version comes +// from the project version (driven by the git tag in CI); channel and vendor are +// stamped alongside it so the runtime no longer needs template placeholders. +springBoot { + buildInfo { + properties { + additional.set( + mapOf( + "channel" to buildChannel, + "vendor" to vendor + ) + ) + } + } +} + dependencies { // BOMs are imported as platforms, so they cannot be bundled. implementation(platform(libs.aws.sdk.bom)) @@ -48,17 +64,6 @@ dependencies { testRuntimeOnly(libs.bundles.testRuntime) } -tasks.processResources { - filesMatching("application.yaml") { - println("appVersion = ${artefactVersion}, channel = ${buildChannel}, vendor = ${vendor}") - expand( - "appVersion" to artefactVersion, - "channel" to buildChannel, - "vendor" to vendor - ) - } -} - tasks.test { useJUnitPlatform() } diff --git a/server/settings.gradle.kts b/server/settings.gradle.kts index 1ca35a5..da5da34 100644 --- a/server/settings.gradle.kts +++ b/server/settings.gradle.kts @@ -1,14 +1 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositories { - mavenCentral() - } -} - rootProject.name = "ahsarah-guide-server" diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/config/AppConfig.java b/server/src/main/java/com/onixbyte/ahsarahguide/config/AppConfig.java index 7246ebe..cacb6f0 100644 --- a/server/src/main/java/com/onixbyte/ahsarahguide/config/AppConfig.java +++ b/server/src/main/java/com/onixbyte/ahsarahguide/config/AppConfig.java @@ -2,10 +2,8 @@ package com.onixbyte.ahsarahguide.config; import com.onixbyte.ahsarahguide.interceptor.GitHubWebhookInterceptor; import com.onixbyte.ahsarahguide.interceptor.TrafficInterceptor; -import com.onixbyte.ahsarahguide.properties.AppProperties; import com.onixbyte.ahsarahguide.security.resolver.CurrentUserArgumentResolver; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -14,7 +12,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @Configuration -@EnableConfigurationProperties(AppProperties.class) public class AppConfig implements WebMvcConfigurer { private final TrafficInterceptor trafficInterceptor; diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/config/OpenApiConfiguration.java b/server/src/main/java/com/onixbyte/ahsarahguide/config/OpenApiConfiguration.java index e937cad..8e020fa 100644 --- a/server/src/main/java/com/onixbyte/ahsarahguide/config/OpenApiConfiguration.java +++ b/server/src/main/java/com/onixbyte/ahsarahguide/config/OpenApiConfiguration.java @@ -4,13 +4,16 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Contact; import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.models.OpenAPI; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @OpenAPIDefinition( info = @Info( title = "Ahsarah Guide Server", description = "API for Ahsarah Guide", - version = "1.4.0", contact = @Contact( name = "Zihlu Wang", email = "zihlu.wang@onixbyte.com" @@ -23,4 +26,20 @@ import org.springframework.context.annotation.Configuration; ) @Configuration public class OpenApiConfiguration { + + /** + * Overrides the OpenAPI info version with the packaged build version so the + * docs always match the released artefact instead of a hard-coded value. + * + * @param buildProperties the build-info properties generated at build time + * @return customiser that stamps the build version onto the API info + */ + @Bean + public OpenApiCustomizer versionCustomizer(BuildProperties buildProperties) { + return openApi -> { + if (openApi.getInfo() != null) { + openApi.getInfo().setVersion(buildProperties.getVersion()); + } + }; + } } diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/manager/AppManager.java b/server/src/main/java/com/onixbyte/ahsarahguide/manager/AppManager.java index 042932e..ecef32f 100644 --- a/server/src/main/java/com/onixbyte/ahsarahguide/manager/AppManager.java +++ b/server/src/main/java/com/onixbyte/ahsarahguide/manager/AppManager.java @@ -1,15 +1,15 @@ package com.onixbyte.ahsarahguide.manager; -import com.onixbyte.ahsarahguide.properties.AppProperties; +import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Component; @Component public class AppManager { - private final AppProperties appProperties; + private final BuildProperties buildProperties; - public AppManager(AppProperties appProperties) { - this.appProperties = appProperties; + public AppManager(BuildProperties buildProperties) { + this.buildProperties = buildProperties; } /** @@ -19,9 +19,9 @@ public class AppManager { */ public String getVersion() { return "v%s-%s by @%s".formatted( - appProperties.version(), - appProperties.channel(), - appProperties.vendor() + buildProperties.getVersion(), + buildProperties.get("channel"), + buildProperties.get("vendor") ); } } diff --git a/server/src/main/java/com/onixbyte/ahsarahguide/properties/AppProperties.java b/server/src/main/java/com/onixbyte/ahsarahguide/properties/AppProperties.java deleted file mode 100644 index 31b81fd..0000000 --- a/server/src/main/java/com/onixbyte/ahsarahguide/properties/AppProperties.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.onixbyte.ahsarahguide.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "app.common") -public record AppProperties( - String version, - String channel, - String vendor -) { -} diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 5329469..ae7fd15 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -53,8 +53,3 @@ logging: org.hibernate: orm.connections.pooling: off -app: - common: - version: ${appVersion} - channel: ${channel} - vendor: ${vendor}