From 18a3b5312eb4fd0d0a5401838fc5ecd6f2d2b2c3 Mon Sep 17 00:00:00 2001 From: Satsuki Akiba Date: Thu, 12 Jun 2025 09:00:16 +0900 Subject: [PATCH] Add ci --- .github/workflows/ci.yml | 111 +++++++++++++++++++++++ .github/workflows/docker-build.yml | 114 ++++++++++++++++++++++++ .github/workflows/release.yml | 138 +++++++++++++++++++++++++++++ build-amd64.sh | 20 +++++ build-docker-image.sh | 72 ++++++++++++--- publish-docker.sh | 87 ++++++++++++++++-- 6 files changed, 526 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/release.yml create mode 100755 build-amd64.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c3650f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Install admin panel dependencies + run: cd admin-panel && bun install + + - name: Type check + run: bunx tsc --noEmit + + - name: Lint + run: bun run lint + + - name: Format check + run: bun run format:check + + - name: Build frontend + run: bun run build:frontend + + - name: Build admin panel + run: bun run build:admin + + - name: Test build artifacts + run: | + ls -la frontend/dist/ + ls -la admin-panel/dist/ + echo "✅ Build artifacts created successfully" + + docker-test: + runs-on: ubuntu-latest + needs: lint-and-test + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image (test only) + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: false + tags: voice-rss-summary:test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Test Docker image + run: | + echo "Testing Docker image functionality..." + + # Create minimal test environment + mkdir -p test-data test-public + echo "# Test feed" > feed_urls.txt + echo "OPENAI_API_KEY=test" > .env + echo "VOICEVOX_HOST=http://localhost:50021" >> .env + + # Run container for a short time to test startup + docker run --rm --name test-container \ + -v "$(pwd)/feed_urls.txt:/app/feed_urls.txt:ro" \ + -v "$(pwd)/.env:/app/.env:ro" \ + -v "$(pwd)/test-public:/app/public" \ + -v "$(pwd)/test-data:/app/data" \ + voice-rss-summary:test \ + timeout 30 bun run server.ts || true + + echo "✅ Docker image test completed" + + security-scan: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..4d4de73 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,114 @@ +name: Build and Publish Docker Images + +on: + push: + branches: [ main, develop ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + platforms: + description: 'Platforms to build (comma-separated)' + required: false + default: 'linux/amd64,linux/arm64' + type: string + push_to_registry: + description: 'Push to registry' + required: false + default: true + type: boolean + +env: + REGISTRY: ghcr.io + IMAGE_NAME_1: ${{ github.repository_owner }}/voice-rss-summary + IMAGE_NAME_2: ${{ github.repository_owner }}/voicersssummary + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1 }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2 }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Determine platforms + id: platforms + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "platforms=${{ github.event.inputs.platforms }}" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "pull_request" ]; then + echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + else + echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT + fi + + - name: Determine push setting + id: push + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "push=${{ github.event.inputs.push_to_registry }}" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "pull_request" ]; then + echo "push=false" >> $GITHUB_OUTPUT + else + echo "push=true" >> $GITHUB_OUTPUT + fi + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: ${{ steps.platforms.outputs.platforms }} + push: ${{ steps.push.outputs.push }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUILDKIT_INLINE_CACHE=1 + + - name: Generate summary + if: always() + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Event**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Ref**: ${{ github.ref }}" >> $GITHUB_STEP_SUMMARY + echo "- **Platforms**: ${{ steps.platforms.outputs.platforms }}" >> $GITHUB_STEP_SUMMARY + echo "- **Push to registry**: ${{ steps.push.outputs.push }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Images built:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c3593f4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,138 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release' + required: true + type: string + +env: + REGISTRY: ghcr.io + +jobs: + create-release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + release_id: ${{ steps.create_release.outputs.id }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get tag + id: get_tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Generate changelog + id: changelog + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${{ github.event.inputs.tag }}^" 2>/dev/null || echo "") + else + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + fi + + if [ -n "$PREVIOUS_TAG" ]; then + echo "## Changes since $PREVIOUS_TAG" > changelog.md + git log --pretty=format:"- %s (%h)" "$PREVIOUS_TAG"..HEAD >> changelog.md + else + echo "## Initial Release" > changelog.md + echo "First release of Voice RSS Summary" >> changelog.md + fi + + echo "" >> changelog.md + echo "## Docker Images" >> changelog.md + echo "- \`ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${{ steps.get_tag.outputs.tag }}\`" >> changelog.md + echo "- \`ghcr.io/${{ github.repository_owner }}/voicersssummary:${{ steps.get_tag.outputs.tag }}\`" >> changelog.md + echo "" >> changelog.md + echo "## Usage" >> changelog.md + echo "\`\`\`bash" >> changelog.md + echo "# Pull and run the latest image" >> changelog.md + echo "docker run -p 3000:3000 -p 3001:3001 ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${{ steps.get_tag.outputs.tag }}" >> changelog.md + echo "" >> changelog.md + echo "# Or clone the repository and run locally" >> changelog.md + echo "git clone https://github.com/${{ github.repository }}.git" >> changelog.md + echo "cd VoiceRSSSummary" >> changelog.md + echo "git checkout ${{ steps.get_tag.outputs.tag }}" >> changelog.md + echo "./run-docker.sh container-name ${{ steps.get_tag.outputs.tag }} --from-ghcr" >> changelog.md + echo "\`\`\`" >> changelog.md + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.get_tag.outputs.tag }} + release_name: Release ${{ steps.get_tag.outputs.tag }} + body_path: changelog.md + draft: false + prerelease: ${{ contains(steps.get_tag.outputs.tag, '-') }} + + wait-for-docker: + runs-on: ubuntu-latest + needs: create-release + permissions: + packages: read + + steps: + - name: Get tag + id: get_tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Wait for Docker images + run: | + echo "Waiting for Docker images to be available..." + TAG="${{ steps.get_tag.outputs.tag }}" + + for i in {1..30}; do + echo "Attempt $i: Checking if images are available..." + + if docker manifest inspect ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${TAG} >/dev/null 2>&1; then + echo "✅ Docker images are available!" + exit 0 + fi + + echo "Images not yet available, waiting 30 seconds..." + sleep 30 + done + + echo "❌ Timeout waiting for Docker images" + exit 1 + + - name: Test Docker image + run: | + TAG="${{ steps.get_tag.outputs.tag }}" + echo "Testing Docker image: ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${TAG}" + + # Pull the image + docker pull ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${TAG} + + # Run a quick test + docker run --rm --name test-container \ + ghcr.io/${{ github.repository_owner }}/voice-rss-summary:${TAG} \ + timeout 10 bun --version || true + + echo "✅ Docker image test completed" \ No newline at end of file diff --git a/build-amd64.sh b/build-amd64.sh new file mode 100755 index 0000000..274d917 --- /dev/null +++ b/build-amd64.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +# Quick script to build AMD64 image and publish to GHCR +# Usage: ./build-amd64.sh [tag] + +TAG="${1:-latest}" + +echo "🔨 Building and publishing AMD64 image..." +echo "Tag: ${TAG}" +echo "" + +# Build and push AMD64 image in one command +./publish-docker.sh "${TAG}" --platform=linux/amd64 --build-and-push + +echo "" +echo "✅ AMD64 image built and published successfully!" +echo "" +echo "To run on AMD64 systems:" +echo " docker run --platform linux/amd64 -p 3000:3000 -p 3001:3001 ghcr.io/anosatsuk124/voice-rss-summary:${TAG}" \ No newline at end of file diff --git a/build-docker-image.sh b/build-docker-image.sh index fa2ba03..1b2e009 100755 --- a/build-docker-image.sh +++ b/build-docker-image.sh @@ -2,14 +2,47 @@ set -euo pipefail # Build Docker image for Voice RSS Summary project -# Usage: ./build-docker-image.sh [tag] [build-args...] +# Usage: ./build-docker-image.sh [tag] [--platform=platform] [build-args...] IMAGE_NAME="voice-rss-summary" -TAG="${1:-latest}" +TAG="latest" +PLATFORM="" +BUILD_ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --platform=*) + PLATFORM="${1#*=}" + shift + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + -*) + BUILD_ARGS+=("$1") + shift + ;; + *) + if [[ -z "${TAG_SET:-}" ]]; then + TAG="$1" + TAG_SET=true + else + BUILD_ARGS+=("$1") + fi + shift + ;; + esac +done + FULL_TAG="${IMAGE_NAME}:${TAG}" echo "Building Docker image: ${FULL_TAG}" echo "Build context: $(pwd)" +if [[ -n "$PLATFORM" ]]; then + echo "Target platform: ${PLATFORM}" +fi # Check if Dockerfile exists if [[ ! -f "Dockerfile" ]]; then @@ -18,15 +51,34 @@ if [[ ! -f "Dockerfile" ]]; then fi # Build with build cache and progress output -exec docker build \ - --tag "${FULL_TAG}" \ - --progress=plain \ - --build-arg BUILDKIT_INLINE_CACHE=1 \ - "${@:2}" \ - . +DOCKER_CMD=(docker build --tag "${FULL_TAG}" --progress=plain --build-arg BUILDKIT_INLINE_CACHE=1) + +# Add platform if specified +if [[ -n "$PLATFORM" ]]; then + DOCKER_CMD+=(--platform "$PLATFORM") +fi + +# Add any additional build args +if [[ ${#BUILD_ARGS[@]} -gt 0 ]]; then + DOCKER_CMD+=("${BUILD_ARGS[@]}") +fi + +# Add build context +DOCKER_CMD+=(.) + +echo "Running: ${DOCKER_CMD[*]}" +"${DOCKER_CMD[@]}" # Display image info -echo "\nBuild completed successfully!" +echo "" +echo "Build completed successfully!" echo "Image: ${FULL_TAG}" +if [[ -n "$PLATFORM" ]]; then + echo "Platform: ${PLATFORM}" +fi echo "Size: $(docker images --format 'table {{.Size}}' "${FULL_TAG}" | tail -n +2)" -echo "\nTo run the container, use: ./run-docker.sh" +echo "" +echo "To run the container, use: ./run-docker.sh" +if [[ -n "$PLATFORM" && "$PLATFORM" != "linux/amd64" ]]; then + echo "Note: Cross-platform image built. May need to push to registry for deployment." +fi diff --git a/publish-docker.sh b/publish-docker.sh index 4723cce..6fe9858 100755 --- a/publish-docker.sh +++ b/publish-docker.sh @@ -2,10 +2,52 @@ set -euo pipefail # Publish Docker image to GitHub Container Registry -# Usage: ./publish-docker.sh [tag] [username] +# Usage: ./publish-docker.sh [tag] [username] [--platform=platform] [--build-and-push] + +GITHUB_USERNAME="anosatsuk124" +TAG="latest" +PLATFORM="" +BUILD_AND_PUSH=false +BUILD_ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --platform=*) + PLATFORM="${1#*=}" + shift + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + --build-and-push) + BUILD_AND_PUSH=true + shift + ;; + --username=*) + GITHUB_USERNAME="${1#*=}" + shift + ;; + -*) + BUILD_ARGS+=("$1") + shift + ;; + *) + if [[ -z "${TAG_SET:-}" ]]; then + TAG="$1" + TAG_SET=true + elif [[ -z "${USERNAME_SET:-}" ]]; then + GITHUB_USERNAME="$1" + USERNAME_SET=true + else + BUILD_ARGS+=("$1") + fi + shift + ;; + esac +done -GITHUB_USERNAME="${2:-anosatsuk124}" -TAG="${1:-latest}" LOCAL_IMAGE="voice-rss-summary:${TAG}" GHCR_IMAGE_1="ghcr.io/${GITHUB_USERNAME}/voice-rss-summary:${TAG}" GHCR_IMAGE_2="ghcr.io/${GITHUB_USERNAME}/voicersssummary:${TAG}" @@ -13,11 +55,37 @@ GHCR_IMAGE_2="ghcr.io/${GITHUB_USERNAME}/voicersssummary:${TAG}" echo "Publishing Docker image to GitHub Container Registry" echo "Local image: ${LOCAL_IMAGE}" echo "GHCR images: ${GHCR_IMAGE_1}, ${GHCR_IMAGE_2}" +if [[ -n "$PLATFORM" ]]; then + echo "Target platform: ${PLATFORM}" +fi + +# Build image if requested +if [[ "$BUILD_AND_PUSH" == "true" ]]; then + echo "Building image first..." + BUILD_CMD=(./build-docker-image.sh "$TAG") + if [[ -n "$PLATFORM" ]]; then + BUILD_CMD+=(--platform "$PLATFORM") + fi + if [[ ${#BUILD_ARGS[@]} -gt 0 ]]; then + BUILD_CMD+=("${BUILD_ARGS[@]}") + fi + + echo "Running: ${BUILD_CMD[*]}" + if ! "${BUILD_CMD[@]}"; then + echo "Error: Failed to build image" + exit 1 + fi +fi # Check if local image exists if ! docker image inspect "${LOCAL_IMAGE}" >/dev/null 2>&1; then echo "Error: Local Docker image '${LOCAL_IMAGE}' not found" - echo "Build it first with: ./build-docker-image.sh ${TAG}" + if [[ -n "$PLATFORM" ]]; then + echo "Build it first with: ./build-docker-image.sh ${TAG} --platform=${PLATFORM}" + else + echo "Build it first with: ./build-docker-image.sh ${TAG}" + fi + echo "Or use --build-and-push flag to build and push in one command" exit 1 fi @@ -62,9 +130,16 @@ echo "" echo "Images available at:" echo " - ${GHCR_IMAGE_1}" echo " - ${GHCR_IMAGE_2}" +if [[ -n "$PLATFORM" ]]; then + echo "Platform: ${PLATFORM}" +fi echo "" echo "To run from GHCR:" -echo " docker run -p 3000:3000 -p 3001:3001 ${GHCR_IMAGE_1}" +if [[ -n "$PLATFORM" && "$PLATFORM" != "$(uname -m)" ]]; then + echo " docker run --platform ${PLATFORM} -p 3000:3000 -p 3001:3001 ${GHCR_IMAGE_1}" +else + echo " docker run -p 3000:3000 -p 3001:3001 ${GHCR_IMAGE_1}" +fi echo "" -echo "To update run-docker.sh to use GHCR:" +echo "To use with run-docker.sh:" echo " ./run-docker.sh container-name ${TAG} --from-ghcr" \ No newline at end of file