From 2cce8a3175eaedec40096069b4f52261053f2627 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 8 Jun 2026 20:55:18 +0800 Subject: [PATCH] ci --- .github/workflows/build-image.yml | 181 ++++++++++++------ .github/workflows/build-release.yml | 278 +++++++++++++++++++++++----- 2 files changed, 353 insertions(+), 106 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index ab242f60..1b8c2727 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Docker image build (Server) +name: Docker Image on: workflow_dispatch: @@ -7,9 +7,23 @@ on: description: "Image version/tag to publish, for example v1.0.0-beta" required: false type: string + image_name: + description: "Image name without registry. Defaults to owner/repo." + required: false + type: string push: tags: ["v*"] +env: + REGISTRY: ghcr.io + DEFAULT_IMAGE_NAME: ${{ github.repository }} + DOCKERFILE: ./docker/Dockerfile + BUILD_CONTEXT: . + CACHE_SCOPE: docker-image + STABLE_FLOATING_TAG: latest + PRERELEASE_FLOATING_TAG: beta + PRERELEASE_PATTERN: (alpha|beta|rc) + permissions: contents: read packages: write @@ -19,88 +33,116 @@ permissions: jobs: build: name: Build (${{ matrix.arch }}) + runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - arch: amd64 platform: linux/amd64 - runner: ubuntu-24.04 - arch: arm64 platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 with: - fetch-tags: true fetch-depth: 0 + fetch-tags: true persist-credentials: false - - name: Set image metadata + - name: Set build metadata shell: bash env: INPUT_VERSION: ${{ github.event.inputs.version }} + INPUT_IMAGE_NAME: ${{ github.event.inputs.image_name }} run: | - POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" - INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}" + set -euo pipefail - echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - if [[ "${GITHUB_REF}" == refs/tags/* ]]; then - VERSION="${GITHUB_REF_NAME}" - elif [[ -n "$INPUT_VERSION" ]]; then - VERSION="$INPUT_VERSION" - elif [[ -n "$POINTED_TAG" ]]; then - VERSION="$POINTED_TAG" + pointed_tag="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" + input_version="${INPUT_VERSION//[[:space:]]/}" + image_name="${INPUT_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}" + + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + version="$GITHUB_REF_NAME" + elif [[ -n "$input_version" ]]; then + version="$input_version" + elif [[ -n "$pointed_tag" ]]; then + version="$pointed_tag" else - echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2 + echo "workflow_dispatch requires a version input when HEAD is not tagged" >&2 exit 1 fi - echo "VERSION=$VERSION" >> "$GITHUB_ENV" + if [[ ! -f "$DOCKERFILE" ]]; then + echo "Dockerfile not found: $DOCKERFILE" >&2 + exit 1 + fi + + if [[ ! -d "$BUILD_CONTEXT" ]]; then + echo "Docker context not found: $BUILD_CONTEXT" >&2 + exit 1 + fi + + { + echo "IMAGE=${REGISTRY}/${image_name,,}" + echo "VERSION=$version" + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + echo "VCS_REF=$GITHUB_SHA" + } >> "$GITHUB_ENV" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - - name: Log into registry + - name: Log in to registry uses: docker/login-action@v3 with: - registry: ghcr.io + registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push + - name: Build and push digest id: build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: - context: . - file: ./openflare-server/Dockerfile + context: ${{ env.BUILD_CONTEXT }} + file: ${{ env.DOCKERFILE }} platforms: ${{ matrix.platform }} outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true build-args: | VERSION=${{ env.VERSION }} - cache-from: type=gha,scope=docker-server-${{ matrix.arch }} - cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-server-${{ matrix.arch }} + BUILD_DATE=${{ env.BUILD_DATE }} + VCS_REF=${{ env.VCS_REF }} + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ env.VERSION }} + org.opencontainers.image.created=${{ env.BUILD_DATE }} + cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}-${{ matrix.arch }} + cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=${{ env.CACHE_SCOPE }}-${{ matrix.arch }} - name: Export digest shell: bash - run: | - mkdir -p /tmp/server-digests - touch "/tmp/server-digests/${DIGEST#sha256:}" env: DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -euo pipefail + mkdir -p /tmp/image-digests + touch "/tmp/image-digests/${DIGEST#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: - name: server-digests-${{ matrix.arch }} - path: /tmp/server-digests/* + name: image-digests-${{ matrix.arch }} + path: /tmp/image-digests/* if-no-files-found: error retention-days: 1 - name: Generate artifact attestation - uses: actions/attest-build-provenance@v3 + uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} @@ -108,79 +150,96 @@ jobs: merge: name: Merge multi-arch manifest - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest needs: build steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 with: - fetch-tags: true fetch-depth: 0 + fetch-tags: true persist-credentials: false - - name: Set image metadata + - name: Set build metadata shell: bash env: INPUT_VERSION: ${{ github.event.inputs.version }} + INPUT_IMAGE_NAME: ${{ github.event.inputs.image_name }} run: | - POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" - INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}" + set -euo pipefail - echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - if [[ "${GITHUB_REF}" == refs/tags/* ]]; then - VERSION="${GITHUB_REF_NAME}" - elif [[ -n "$INPUT_VERSION" ]]; then - VERSION="$INPUT_VERSION" - elif [[ -n "$POINTED_TAG" ]]; then - VERSION="$POINTED_TAG" + pointed_tag="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" + input_version="${INPUT_VERSION//[[:space:]]/}" + image_name="${INPUT_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}" + + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + version="$GITHUB_REF_NAME" + elif [[ -n "$input_version" ]]; then + version="$input_version" + elif [[ -n "$pointed_tag" ]]; then + version="$pointed_tag" else - echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2 + echo "workflow_dispatch requires a version input when HEAD is not tagged" >&2 exit 1 fi - echo "VERSION=$VERSION" >> "$GITHUB_ENV" + if [[ ! -f "$DOCKERFILE" ]]; then + echo "Dockerfile not found: $DOCKERFILE" >&2 + exit 1 + fi + + if [[ ! -d "$BUILD_CONTEXT" ]]; then + echo "Docker context not found: $BUILD_CONTEXT" >&2 + exit 1 + fi + + { + echo "IMAGE=${REGISTRY}/${image_name,,}" + echo "VERSION=$version" + } >> "$GITHUB_ENV" - name: Download digests uses: actions/download-artifact@v4 with: - path: /tmp/server-digests - pattern: server-digests-* + path: /tmp/image-digests + pattern: image-digests-* merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - - name: Log into registry + - name: Log in to registry uses: docker/login-action@v3 with: - registry: ghcr.io + registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push manifest list - working-directory: /tmp/server-digests + working-directory: /tmp/image-digests shell: bash run: | + set -euo pipefail shopt -s nullglob + references=() for digest in *; do references+=("${IMAGE}@sha256:${digest}") done - if [ ${#references[@]} -eq 0 ]; then - echo "No digests found in /tmp/server-digests" >&2 + if [[ ${#references[@]} -eq 0 ]]; then + echo "No digests found in /tmp/image-digests" >&2 exit 1 fi - if [[ "${VERSION}" =~ (alpha|beta|rc) ]]; then - FLOATING_TAG="beta" - else - FLOATING_TAG="latest" + floating_tag="$STABLE_FLOATING_TAG" + if [[ "$VERSION" =~ $PRERELEASE_PATTERN ]]; then + floating_tag="$PRERELEASE_FLOATING_TAG" fi docker buildx imagetools create \ -t "${IMAGE}:${VERSION}" \ - -t "${IMAGE}:${FLOATING_TAG}" \ + -t "${IMAGE}:${floating_tag}" \ "${references[@]}" - name: Inspect image diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3d977913..c24f0026 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -2,70 +2,258 @@ name: Build Release on: push: - tags: - - "v*" + tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: "Release version/tag to build, for example v1.0.0-beta" + required: true + type: string + +env: + APP_NAME: wavelet + GO_MAIN: ./main.go + GO_BUILD_TAGS: embed_frontend + GO_LDFLAGS: -s -w + NODE_VERSION: "22" + FRONTEND_DIR: frontend + FRONTEND_BUILD_COMMAND: pnpm build:embed + FRONTEND_OUT_DIR: frontend/out + EMBED_DIST_DIR: internal/router/dist + EXTRA_FILES: | + LICENSE + README.md + README_zh.md + config.example.yaml + DEPLOYMENT_zh.md permissions: contents: write - packages: write jobs: create-release: name: Create Release runs-on: ubuntu-latest + outputs: + version: ${{ steps.metadata.outputs.version }} + version_without_v: ${{ steps.metadata.outputs.version_without_v }} + build_date: ${{ steps.metadata.outputs.build_date }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true - - name: Create Release - uses: softprops/action-gh-release@v2 - - - name: Setup node - uses: actions/setup-node@v6 - with: - node-version: 22 - - - run: npx changelogithub + - name: Set release metadata + id: metadata + shell: bash env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + INPUT_VERSION: ${{ github.event.inputs.version }} + run: | + set -euo pipefail - build-matrix: - name: Release Go Binary + input_version="${INPUT_VERSION//[[:space:]]/}" + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + version="$GITHUB_REF_NAME" + elif [[ -n "$input_version" ]]; then + version="$input_version" + else + echo "workflow_dispatch requires a version input" >&2 + exit 1 + fi + + { + echo "version=$version" + echo "version_without_v=${version#v}" + echo "build_date=$(date -u +'%Y-%m-%d %H:%M:%S')" + } >> "$GITHUB_OUTPUT" + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.metadata.outputs.version }} + name: ${{ steps.metadata.outputs.version }} + generate_release_notes: true + prerelease: ${{ contains(steps.metadata.outputs.version, 'alpha') || contains(steps.metadata.outputs.version, 'beta') || contains(steps.metadata.outputs.version, 'rc') }} + + build-frontend: + name: Build Embedded Frontend runs-on: ubuntu-latest - strategy: - matrix: - goos: [linux, darwin, windows] - goarch: [amd64, arm64] - exclude: - - goos: windows - goarch: arm64 + needs: create-release steps: - name: Checkout uses: actions/checkout@v4 - - name: Extract version from Git Ref - id: extract_version - run: | - VERSION=$(echo "${{ github.ref }}" | sed 's/refs\/tags\/v//') - echo "VERSION=${VERSION}" >> $GITHUB_ENV - - - name: Release Go Binary - uses: wangyoucao577/go-release-action@v1 + - name: Setup Node + uses: actions/setup-node@v4 with: - pre_command: export CGO_ENABLED=0 - goos: ${{ matrix.goos }} - goarch: ${{ matrix.goarch }} - github_token: ${{ secrets.GITHUB_TOKEN }} - extra_files: | - LICENSE - README.md - ldflags: >- - -s -w - -X "github.com/krau/SaveAny-Bot/config.Version=${{ env.VERSION }}" - -X "github.com/krau/SaveAny-Bot/config.BuildTime=${{ format(github.event.repository.updated_at, 'yyyy-MM-dd HH:mm:ss') }}" - -X "github.com/krau/SaveAny-Bot/config.GitCommit=${{ github.sha }}" - binary_name: saveany-bot + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: ${{ env.FRONTEND_DIR }}/pnpm-lock.yaml + + - name: Enable pnpm + shell: bash + run: | + set -euo pipefail + corepack enable + corepack prepare pnpm@10.10.0 --activate + + - name: Install frontend dependencies + working-directory: ${{ env.FRONTEND_DIR }} + run: pnpm install --frozen-lockfile + + - name: Stamp frontend package metadata + working-directory: ${{ env.FRONTEND_DIR }} + shell: bash env: - VERSION: ${{ env.VERSION }} + VERSION: ${{ needs.create-release.outputs.version_without_v }} + BUILD_DATE: ${{ needs.create-release.outputs.build_date }} + run: | + set -euo pipefail + if [[ -f package.json ]]; then + node - <<'NODE' + const fs = require('fs'); + const path = './package.json'; + const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); + if (process.env.VERSION && Object.hasOwn(pkg, 'version')) { + pkg.version = process.env.VERSION; + } + if (process.env.BUILD_DATE && Object.hasOwn(pkg, 'buildDate')) { + pkg.buildDate = process.env.BUILD_DATE; + } + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); + NODE + fi + + - name: Build frontend + run: ${{ env.FRONTEND_BUILD_COMMAND }} + working-directory: ${{ env.FRONTEND_DIR }} + + - name: Prepare embed directory + shell: bash + run: | + set -euo pipefail + rm -rf "$EMBED_DIST_DIR" + mkdir -p "$(dirname "$EMBED_DIST_DIR")" + cp -R "$FRONTEND_OUT_DIR" "$EMBED_DIST_DIR" + + - name: Upload embedded frontend + uses: actions/upload-artifact@v4 + with: + name: embedded-frontend + path: ${{ env.EMBED_DIST_DIR }} + if-no-files-found: error + retention-days: 1 + + build-binaries: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} + runs-on: ubuntu-latest + needs: + - create-release + - build-frontend + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + archive: tar.gz + - goos: linux + goarch: arm64 + archive: tar.gz + - goos: darwin + goarch: amd64 + archive: tar.gz + - goos: darwin + goarch: arm64 + archive: tar.gz + - goos: windows + goarch: amd64 + archive: zip + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download embedded frontend + uses: actions/download-artifact@v4 + with: + name: embedded-frontend + path: ${{ env.EMBED_DIST_DIR }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build binary + shell: bash + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + run: | + set -euo pipefail + + mkdir -p dist + binary_name="$APP_NAME" + if [[ "$GOOS" == "windows" ]]; then + binary_name="${binary_name}.exe" + fi + + build_args=( + -trimpath + -ldflags "$GO_LDFLAGS" + -o "dist/$binary_name" + ) + + if [[ -n "$GO_BUILD_TAGS" ]]; then + build_args=(-tags "$GO_BUILD_TAGS" "${build_args[@]}") + fi + + go build "${build_args[@]}" "$GO_MAIN" + + - name: Package artifact + id: package + shell: bash + env: + VERSION: ${{ needs.create-release.outputs.version }} + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + ARCHIVE_FORMAT: ${{ matrix.archive }} + run: | + set -euo pipefail + + package_name="${APP_NAME}_${VERSION}_${GOOS}_${GOARCH}" + staging_dir="dist/$package_name" + mkdir -p "$staging_dir" + + if [[ "$GOOS" == "windows" ]]; then + cp "dist/${APP_NAME}.exe" "$staging_dir/" + else + cp "dist/${APP_NAME}" "$staging_dir/" + fi + + while IFS= read -r extra_file; do + [[ -z "$extra_file" ]] && continue + if [[ -e "$extra_file" ]]; then + cp -R "$extra_file" "$staging_dir/" + fi + done <<< "$EXTRA_FILES" + + if [[ "$ARCHIVE_FORMAT" == "zip" ]]; then + (cd dist && zip -r "${package_name}.zip" "$package_name") + artifact="dist/${package_name}.zip" + else + tar -C dist -czf "dist/${package_name}.tar.gz" "$package_name" + artifact="dist/${package_name}.tar.gz" + fi + + echo "artifact=$artifact" >> "$GITHUB_OUTPUT" + + - name: Upload release artifact + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.create-release.outputs.version }} + files: ${{ steps.package.outputs.artifact }}