mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-08-04 23:22:12 +00:00
Port of #708 to 3.x. This branch carries the identical bug: the changelog is written to /tmp/changelog.txt while curl is told to read `changelog.txt`, a relative path resolved against the checkout. The next 3.x release would fail exactly as 2.4.2 did — published, zip uploaded, images built, and never registered, so no install offered it. It has not bitten here only because no 3.x release has been cut since the automation landed; 3.0.0-alpha.1 predates it, the same reason 2.4.2 was the first to hit it on 2.x. Registration moves into its own job that downloads the published asset rather than reusing the build job's working directory, so it can also be re-run on demand for an existing tag instead of needing production shell access to repair. A missing WEBSITE_RELEASE_TOKEN is now fatal on a real release rather than a warning, and the registration is verified against the download endpoint afterwards, which fails unless the row exists and its zip is retrievable. Both blocks are byte-identical to what merged on 2.x, so the two trees do not drift and the next change ports cleanly. Checked for this branch specifically: the verify probe returns 200 for 3.0.0-alpha.1, so it holds on the insider channel; that release ships the same InvoiceShelf.zip asset name; and the "-" suffix already derives CHANNEL=insider without change. LATEST_MAJOR stays "2" — 2.x owns the moving :latest tags until 3.0.0 GA.
329 lines
12 KiB
YAML
329 lines
12 KiB
YAML
name: Docker Build and Push
|
||
|
||
on:
|
||
release:
|
||
types: [published]
|
||
workflow_dispatch:
|
||
inputs:
|
||
tag:
|
||
description: 'Docker tag'
|
||
required: true
|
||
default: 'latest'
|
||
register_tag:
|
||
description: 'Release tag to (re-)register on the updater, e.g. 2.4.2. Leave blank to skip.'
|
||
required: false
|
||
default: ''
|
||
|
||
# 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' || github.event_name == 'workflow_dispatch'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Set up PHP
|
||
uses: shivammathur/setup-php@v2
|
||
with:
|
||
php-version: 8.4
|
||
|
||
- name: Checkout code
|
||
uses: actions/checkout@v6
|
||
|
||
- name: Install dependencies
|
||
uses: ramsey/composer-install@4.0.0
|
||
|
||
- name: Check source code for syntax errors
|
||
run: ./vendor/bin/pint --test
|
||
|
||
tests:
|
||
name: 2️⃣ PHP Tests
|
||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||
needs:
|
||
- php_syntax_errors
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
php-version:
|
||
- 8.4
|
||
env:
|
||
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v6
|
||
|
||
- name: Setup PHP Action
|
||
uses: shivammathur/setup-php@v2
|
||
with:
|
||
php-version: ${{ matrix.php-version }}
|
||
extensions: ${{ env.extensions }}
|
||
coverage: none
|
||
tools: pecl, composer
|
||
|
||
- name: Install Composer dependencies
|
||
uses: ramsey/composer-install@4.0.0
|
||
|
||
- name: Apply tests ${{ matrix.php-version }} (parallel)
|
||
run: php artisan test --parallel --exclude-group=modules
|
||
|
||
- name: Apply module tests ${{ matrix.php-version }} (serial)
|
||
run: php artisan test --group=modules
|
||
|
||
release_artifact_build:
|
||
name: 🏗️ Build / Upload - Release File
|
||
if: github.event_name == 'release'
|
||
needs:
|
||
- tests
|
||
runs-on: ubuntu-latest
|
||
env:
|
||
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v6
|
||
|
||
- name: Setup PHP
|
||
uses: shivammathur/setup-php@v2
|
||
with:
|
||
php-version: 8.4
|
||
extensions: ${{ env.extensions }}
|
||
coverage: none
|
||
|
||
- name: Install Composer dependencies
|
||
uses: ramsey/composer-install@4.0.0
|
||
with:
|
||
composer-options: --no-dev
|
||
|
||
- name: Install pnpm
|
||
uses: pnpm/action-setup@v6
|
||
|
||
- name: Use Node.js 24
|
||
uses: actions/setup-node@v6
|
||
with:
|
||
node-version: 24
|
||
cache: pnpm
|
||
|
||
- name: Install
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Compile Front-end
|
||
run: pnpm build
|
||
|
||
- name: Build Dist
|
||
run: |
|
||
make clean dist
|
||
|
||
- name: Upload package
|
||
uses: svenstaro/upload-release-action@v2
|
||
with:
|
||
repo_token: ${{ github.token }}
|
||
file: InvoiceShelf.zip
|
||
asset_name: InvoiceShelf.zip
|
||
tag: ${{ github.ref }}
|
||
overwrite: true
|
||
|
||
|
||
# Registration lives in its own job, and fetches the published asset rather than
|
||
# reusing the build job's working directory. That decoupling is deliberate: the
|
||
# previous in-line step read `changelog.txt` relative to the checkout while writing
|
||
# it to /tmp, and the mismatch was undetectable until a real release ran. As its own
|
||
# job it can also be re-run on demand for an existing tag, so a failure here no longer
|
||
# requires production shell access to repair. Idempotent per version — the endpoint
|
||
# upserts. Release fields are passed via env (not inline ${{ }}) to avoid shell
|
||
# injection from the release body.
|
||
register_release:
|
||
name: 📡 Register release on the updater
|
||
# always() is required because release_artifact_build is skipped on a manual
|
||
# dispatch, and a job needing a skipped job is skipped too.
|
||
if: >-
|
||
always() &&
|
||
((github.event_name == 'release' && needs.release_artifact_build.result == 'success') ||
|
||
(github.event_name == 'workflow_dispatch' && inputs.register_tag != ''))
|
||
needs:
|
||
- release_artifact_build
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Resolve the tag being registered
|
||
id: resolve
|
||
env:
|
||
EVENT: ${{ github.event_name }}
|
||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||
INPUT_TAG: ${{ inputs.register_tag }}
|
||
run: |
|
||
if [ "$EVENT" = "release" ]; then TAG="$RELEASE_TAG"; else TAG="$INPUT_TAG"; fi
|
||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||
|
||
# config/installer.php is read at the tag being registered, not at HEAD, so a
|
||
# re-registration reports the requirements that release actually shipped with.
|
||
- name: Checkout code at that tag
|
||
uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ steps.resolve.outputs.tag }}
|
||
|
||
- name: Setup PHP
|
||
uses: shivammathur/setup-php@v2
|
||
with:
|
||
php-version: 8.4
|
||
coverage: none
|
||
|
||
- name: Download the published release asset
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
TAG: ${{ steps.resolve.outputs.tag }}
|
||
run: gh release download "$TAG" --repo "$GITHUB_REPOSITORY" -p InvoiceShelf.zip --clobber
|
||
|
||
- name: Register on the updater
|
||
id: register
|
||
env:
|
||
CI_RELEASE_TOKEN: ${{ secrets.WEBSITE_RELEASE_TOKEN }}
|
||
GH_TOKEN: ${{ github.token }}
|
||
EVENT: ${{ github.event_name }}
|
||
TAG: ${{ steps.resolve.outputs.tag }}
|
||
run: |
|
||
if [ -z "$CI_RELEASE_TOKEN" ]; then
|
||
# A release that silently skips registration looks entirely successful while
|
||
# reaching nobody, so on a real release this is fatal. A manual dispatch
|
||
# without the secret is legitimate, so warn there instead.
|
||
if [ "$EVENT" = "release" ]; then
|
||
echo "::error::WEBSITE_RELEASE_TOKEN is not set — $TAG would never be offered to installs."
|
||
exit 1
|
||
fi
|
||
echo "::warning::WEBSITE_RELEASE_TOKEN not set — skipping updater registration for $TAG"
|
||
echo "registered=false" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
fi
|
||
echo "registered=true" >> "$GITHUB_OUTPUT"
|
||
|
||
MIN_PHP=$(php -r '$c=require "config/installer.php"; echo $c["core"]["minPhpVersion"] ?? "";')
|
||
EXTS=$(php -r '$c=require "config/installer.php"; echo implode(", ", $c["requirements"]["php"] ?? []);')
|
||
|
||
# Read the notes and pre-release flag from the release itself, so this behaves
|
||
# identically whether triggered by a publish or re-run later by hand.
|
||
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq '.body' > /tmp/changelog.txt
|
||
PRERELEASE=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isPrerelease --jq '.isPrerelease')
|
||
PUBLISHED=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json publishedAt --jq '.publishedAt')
|
||
|
||
case "$TAG" in
|
||
*-*) CHANNEL=insider ;;
|
||
*) [ "$PRERELEASE" = "true" ] && CHANNEL=insider || CHANNEL=stable ;;
|
||
esac
|
||
|
||
echo "Registering $TAG (channel=$CHANNEL, min_php=$MIN_PHP) on the updater"
|
||
curl -fsS --retry 3 --retry-delay 5 -X POST https://invoiceshelf.com/api/releases \
|
||
-H "Authorization: Bearer $CI_RELEASE_TOKEN" \
|
||
-F "version=$TAG" \
|
||
-F "channel=$CHANNEL" \
|
||
-F "released_at=$PUBLISHED" \
|
||
-F "min_php_version=$MIN_PHP" \
|
||
-F "extensions=$EXTS" \
|
||
-F "changelog=</tmp/changelog.txt" \
|
||
-F "description=</tmp/changelog.txt" \
|
||
-F "release_file=@InvoiceShelf.zip"
|
||
|
||
# Posting a 2xx is not proof an install can actually fetch the release. This
|
||
# endpoint 404s unless the Release row exists AND its zip is retrievable from
|
||
# storage, so it verifies the whole chain — and would have caught the failure
|
||
# this job was rewritten for.
|
||
- name: Verify the release is being served
|
||
if: steps.register.outputs.registered == 'true'
|
||
env:
|
||
TAG: ${{ steps.resolve.outputs.tag }}
|
||
run: |
|
||
if ! curl -fsI --retry 3 --retry-delay 5 "https://invoiceshelf.com/releases/download/$TAG" > /dev/null; then
|
||
echo "::error::$TAG was accepted by the updater but is not being served — installs will not receive it."
|
||
exit 1
|
||
fi
|
||
echo "$TAG is registered and downloadable."
|
||
|
||
release_docker_build:
|
||
name: 🐳 Release Docker Build
|
||
if: github.event_name == 'release'
|
||
needs:
|
||
- tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v6
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v4
|
||
|
||
- name: Log in to Docker Hub
|
||
uses: docker/login-action@v4
|
||
with:
|
||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||
|
||
- name: Extract metadata
|
||
id: meta
|
||
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=${{ !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
|
||
with:
|
||
context: .
|
||
file: docker/production/Dockerfile
|
||
platforms: linux/amd64,linux/arm64
|
||
push: true
|
||
tags: ${{ steps.meta.outputs.tags }}
|
||
labels: ${{ steps.meta.outputs.labels }}
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
secrets: |
|
||
composer_auth={"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}
|
||
|
||
manual_docker_build:
|
||
name: 🛠️ Manual Docker Build
|
||
if: github.event_name == 'workflow_dispatch'
|
||
needs:
|
||
- tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v6
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v4
|
||
|
||
- name: Log in to Docker Hub
|
||
uses: docker/login-action@v4
|
||
with:
|
||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||
|
||
- name: Build and push Docker image
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: .
|
||
file: docker/production/Dockerfile
|
||
platforms: linux/amd64,linux/arm64
|
||
push: true
|
||
tags: invoiceshelf/invoiceshelf:${{ github.event.inputs.tag }}
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
secrets: |
|
||
composer_auth={"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}
|