diff --git a/.dockerignore b/.dockerignore index 80d0b6b3..83dd2de5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,9 @@ docker/ node_modules/ +# Vite output is rebuilt in the image's static_builder stage; never ship a stale host build +public/build/ + database/*.sqlite storage/app/* !storage/app/templates* diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 056e69a4..1e3e2e5c 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -3,9 +3,6 @@ name: Docker Build and Push on: release: types: [published] - schedule: - # Run nightly at 2 AM UTC - - cron: '0 2 * * *' workflow_dispatch: inputs: tag: @@ -13,10 +10,17 @@ on: required: true default: 'latest' +# Single source of truth for which major owns the moving stable tags +# (:latest, :{major}, :{major}.{minor}) and the temporary :nightly alias. +# Bump to "3" on BOTH the 2.x and 3.x branches when 3.0.0 GA is tagged, and +# drop the :nightly alias tag at the same time (end of the deprecation window). +env: + LATEST_MAJOR: "2" + jobs: php_syntax_errors: name: 1️⃣ PHP Code Style errors - if: github.event_name == 'release' + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Set up PHP @@ -35,7 +39,7 @@ jobs: tests: name: 2️⃣ PHP Tests - if: github.event_name == 'release' + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' needs: - php_syntax_errors runs-on: ubuntu-latest @@ -156,11 +160,20 @@ jobs: uses: docker/metadata-action@v6 with: images: invoiceshelf/invoiceshelf + # Pre-release semver (e.g. 3.0.0-beta.1) automatically gets ONLY the exact + # {{version}} tag — metadata-action withholds {{major}} / {{major}}.{{minor}}. + # The moving stable tags (:latest, etc.) are gated on LATEST_MAJOR so a 2.x + # patch can never steal :latest from 3.x after the GA flip, and vice-versa. tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') && startsWith(github.ref_name, format('{0}.', env.LATEST_MAJOR)) }} + type=raw,value=beta,enable=${{ contains(github.ref_name, '-beta') }} + type=raw,value=next,enable=${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') }} + # DEPRECATED transitional alias: keeps existing :nightly deployments converging + # onto stable until they migrate. Remove this line at the 3.0.0 GA flip. + type=raw,value=nightly,enable=${{ !contains(github.ref_name, '-') && startsWith(github.ref_name, format('{0}.', env.LATEST_MAJOR)) }} - name: Build and push Docker image uses: docker/build-push-action@v7 @@ -177,6 +190,8 @@ jobs: manual_docker_build: name: 🛠️ Manual Docker Build if: github.event_name == 'workflow_dispatch' + needs: + - tests runs-on: ubuntu-latest steps: - name: Checkout code @@ -201,67 +216,3 @@ jobs: tags: invoiceshelf/invoiceshelf:${{ github.event.inputs.tag }} cache-from: type=gha cache-to: type=gha,mode=max - - nightly_build: - name: 🌙 Nightly Docker Build - if: github.event_name == 'schedule' - runs-on: ubuntu-latest - strategy: - matrix: - branch: [2.x, develop] - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{ matrix.branch }} - fetch-depth: 2 - - - name: Check for recent changes - id: changes - run: | - # Check if there are commits in the last 24 hours - RECENT_COMMITS=$(git log --since="24 hours ago" --oneline | wc -l) - echo "recent_commits=$RECENT_COMMITS" >> $GITHUB_OUTPUT - if [ "$RECENT_COMMITS" -gt 0 ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - else - echo "has_changes=false" >> $GITHUB_OUTPUT - fi - - - name: Set up Docker Buildx - if: steps.changes.outputs.has_changes == 'true' - uses: docker/setup-buildx-action@v4 - - - name: Log in to Docker Hub - if: steps.changes.outputs.has_changes == 'true' - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Set Docker tag - if: steps.changes.outputs.has_changes == 'true' - id: tag - run: | - if [ "${{ matrix.branch }}" = "2.x" ]; then - echo "tag=nightly" >> $GITHUB_OUTPUT - elif [ "${{ matrix.branch }}" = "develop" ]; then - echo "tag=alpha" >> $GITHUB_OUTPUT - fi - - - name: Build and push Docker image - if: steps.changes.outputs.has_changes == 'true' - uses: docker/build-push-action@v7 - with: - context: . - file: docker/production/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: invoiceshelf/invoiceshelf:${{ steps.tag.outputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: No changes detected - if: steps.changes.outputs.has_changes == 'false' - run: | - echo "No commits found in the last 24 hours for ${{ matrix.branch }} branch. Skipping build." diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index b325f83b..f2581f10 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -37,7 +37,10 @@ FROM base AS production # Revert back to www-data, non-root user USER www-data - # Copy application files - COPY --from=static_builder --chown=www-data:www-data /var/www/html/public /var/www/html/public + # Copy application files first, then overlay the freshly-built frontend assets + # LAST. public/build is gitignored, so it's absent in CI but may exist in a local + # working tree — copying it after `COPY .` guarantees the stale host build can + # never clobber the fresh one produced by static_builder. COPY --chown=www-data:www-data . /var/www/html + COPY --from=static_builder --chown=www-data:www-data /var/www/html/public/build /var/www/html/public/build RUN composer install --prefer-dist --no-dev --optimize-autoloader