From 5773156222dbc76353efc502aadf916eaa3f4660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 9 Jan 2026 18:32:48 +0100 Subject: [PATCH] Add Gitea CI/CD workflows for automated Docker builds Implements automated Docker image building and publishing to registry: - build.yml: Main workflow that builds and pushes Docker images to registry - Triggers on push to master branch - Only builds when image with current version doesn't exist - Uses Docker BuildKit with layer caching for faster builds - Tags images with both version number and 'latest' - check_image_version.yml: Reusable workflow to verify image existence - Reads version from package.json - Uses lightweight manifest inspection (no image download) - Returns image_exists and version as outputs - check_package_version.yml: Reusable workflow to detect version changes - Compares version between commits - Handles edge cases (first commit, missing package.json) - Includes validation for version extraction failures All workflows include proper error handling and clear logging. Co-Authored-By: Claude Sonnet 4.5 --- .gitea/workflows/build.yml | 58 +++++++++++++ .gitea/workflows/check_image_version.yml | 94 ++++++++++++++++++++++ .gitea/workflows/check_package_version.yml | 82 +++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/check_image_version.yml create mode 100644 .gitea/workflows/check_package_version.yml diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..21df4a0 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,58 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + +jobs: + # Verifies if Docker image with current version already exists in registry + # This prevents rebuilding the same version but allows pulls and version changes + # to always trigger new builds. Uses lightweight manifest inspect (no download) + build_web_app__check_image_version: + uses: ./.gitea/workflows/check_image_version.yml + with: + workspacePath: './web-app' + imageName: 'utility-bills-tracker' + registryUrl: 'registry.budakova.org' + registryUsername: ${{ vars.PROFILE_REGISTRY_USERNAME }} + registryNamespace: 'knee-cola' + secrets: + registryToken: ${{ secrets.PROFILE_REGISTRY_TOKEN }} + + # Builds and pushes Docker image to registry if: + # - Image with current version doesn't exist in registry + # This prevents rebuilding the same version unnecessarily + build_web_app: + needs: [build_web_app__check_image_version] + if: needs.build_web_app__check_image_version.outputs.image_exists == 'false' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Registry + # Gitea automatically provides these secrets: + # - `vars.REGISTRY_USERNAME` - defined as action variable in **repo settings** + # - `secrets.REGISTRY_TOKEN` - defined as action secret in **repo settings** + # created in user settings as personal access token with `write:packages` scope + # - `vars.PROFILE_REGISTRY_USERNAME` - defined as action variable in **profile settings** + # - `secrets.PROFILE_REGISTRY_TOKEN` - defined as action secret in **profile settings** + # created in user settings as personal access token with `write:packages` scope + run: | + echo "${{ secrets.PROFILE_REGISTRY_TOKEN }}" | docker login registry.budakova.org -u "${{ vars.PROFILE_REGISTRY_USERNAME }}" --password-stdin + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./web-app + push: true + tags: | + registry.budakova.org/knee-cola/utility-bills-tracker:${{ needs.build_web_app__check_image_version.outputs.version }} + registry.budakova.org/knee-cola/utility-bills-tracker:latest + cache-from: type=registry,ref=registry.budakova.org/knee-cola/utility-bills-tracker:buildcache + cache-to: type=registry,ref=registry.budakova.org/knee-cola/utility-bills-tracker:buildcache,mode=max diff --git a/.gitea/workflows/check_image_version.yml b/.gitea/workflows/check_image_version.yml new file mode 100644 index 0000000..f9033c7 --- /dev/null +++ b/.gitea/workflows/check_image_version.yml @@ -0,0 +1,94 @@ +name: Check Image Version + +on: + workflow_call: + inputs: + workspacePath: + description: 'Path relative to repo root where package.json is located' + required: false + type: string + default: '.' + imageName: + description: 'Docker image name without registry FQDN or username' + required: true + type: string + registryUrl: + description: 'Docker registry URL (e.g., registry.budakova.org)' + required: false + type: string + default: 'registry.budakova.org' + registryUsername: + description: 'Docker registry username' + required: true + type: string + registryNamespace: + description: 'Docker registry namespace/organization (e.g., knee-cola)' + required: true + type: string + secrets: + registryToken: + description: 'Registry access token' + required: true + outputs: + image_exists: + description: 'Whether the image exists in the registry' + value: ${{ jobs.check_image.outputs.image_exists }} + version: + description: 'Current version from package.json' + value: ${{ jobs.check_image.outputs.version }} + +jobs: + check_image: + runs-on: ubuntu-latest + outputs: + image_exists: ${{ steps.manifest-check.outputs.image_exists }} + version: ${{ steps.version-read.outputs.version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Read version from package.json + id: version-read + run: | + WORKSPACE_PATH="${{ inputs.workspacePath }}" + # Clean up path - remove trailing slash if present + WORKSPACE_PATH="${WORKSPACE_PATH%/}" + # Handle root directory case + if [ "$WORKSPACE_PATH" = "." ]; then + PACKAGE_JSON_PATH="package.json" + else + PACKAGE_JSON_PATH="${WORKSPACE_PATH}/package.json" + fi + + VERSION=$(node -p "try { require('./${PACKAGE_JSON_PATH}').version } catch(e) { console.error('Error reading version:', e.message); process.exit(1) }") || { + echo "Error: Failed to read version from ${PACKAGE_JSON_PATH}" + exit 1 + } + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Checking for image version: $VERSION" + + - name: Login to Registry + run: | + echo "${{ secrets.registryToken }}" | docker login ${{ inputs.registryUrl }} -u "${{ inputs.registryUsername }}" --password-stdin + + - name: Check if image exists in registry + id: manifest-check + run: | + VERSION=${{ steps.version-read.outputs.version }} + IMAGE="${{ inputs.registryUrl }}/${{ inputs.registryNamespace }}/${{ inputs.imageName }}:${VERSION}" + + echo "Checking manifest for image: $IMAGE" + + if docker manifest inspect "$IMAGE" &>/dev/null; then + echo "Image exists in registry" + echo "image_exists=true" >> $GITHUB_OUTPUT + else + echo "Image does not exist in registry" + echo "image_exists=false" >> $GITHUB_OUTPUT + fi + + - name: Summary + run: | + echo "Version: ${{ steps.version-read.outputs.version }}" + echo "Image exists: ${{ steps.manifest-check.outputs.image_exists }}" diff --git a/.gitea/workflows/check_package_version.yml b/.gitea/workflows/check_package_version.yml new file mode 100644 index 0000000..ac2b474 --- /dev/null +++ b/.gitea/workflows/check_package_version.yml @@ -0,0 +1,82 @@ +name: Check Package Version + +on: + workflow_call: + inputs: + workspacePath: + description: 'Path relative to repo root where package.json is located' + required: false + type: string + default: '.' + outputs: + version_changed: + description: 'Whether the version changed from the previous commit' + value: ${{ jobs.check_version.outputs.version_changed }} + version: + description: 'Current version from package.json' + value: ${{ jobs.check_version.outputs.version }} + +jobs: + check_version: + runs-on: ubuntu-latest + outputs: + version_changed: ${{ steps.version-check.outputs.version_changed }} + version: ${{ steps.version-check.outputs.version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if version changed + id: version-check + run: | + WORKSPACE_PATH="${{ inputs.workspacePath }}" + # Clean up path - remove trailing slash if present + WORKSPACE_PATH="${WORKSPACE_PATH%/}" + # Handle root directory case + if [ "$WORKSPACE_PATH" = "." ]; then + PACKAGE_JSON_PATH="package.json" + else + PACKAGE_JSON_PATH="${WORKSPACE_PATH}/package.json" + fi + + # Get current version + CURRENT_VERSION=$(node -p "require('./${PACKAGE_JSON_PATH}').version") + echo "Current version: $CURRENT_VERSION" + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + # Check if HEAD~1 exists (handle first commit) + if ! git rev-parse HEAD~1 &>/dev/null; then + echo "First commit detected, running workflow" + echo "version_changed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if package.json exists in previous commit + if ! git show HEAD~1:${PACKAGE_JSON_PATH} &>/dev/null; then + echo "package.json doesn't exist in previous commit" + echo "version_changed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract previous version using grep/sed (safer than node for old file) + PREVIOUS_VERSION=$(git show HEAD~1:${PACKAGE_JSON_PATH} | grep '"version"' | head -1 | sed -E 's/.*"version"\s*:\s*"([^"]+)".*/\1/') + echo "Previous version: $PREVIOUS_VERSION" + + # Validate extraction + if [ -z "$PREVIOUS_VERSION" ]; then + echo "Warning: Could not extract previous version, assuming changed" + echo "version_changed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Compare versions + if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then + echo "Version changed: $PREVIOUS_VERSION -> $CURRENT_VERSION" + echo "version_changed=true" >> $GITHUB_OUTPUT + else + echo "Version unchanged: $CURRENT_VERSION" + echo "version_changed=false" >> $GITHUB_OUTPUT + fi