feat: add containerized deployment workflow
This commit is contained in:
+6
-7
@@ -25,7 +25,6 @@
|
|||||||
**/compose.*.y*ml
|
**/compose.*.y*ml
|
||||||
!**/composer.json
|
!**/composer.json
|
||||||
!**/composer.lock
|
!**/composer.lock
|
||||||
**/Dockerfile*
|
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/npm-debug.log
|
**/npm-debug.log
|
||||||
**/obj
|
**/obj
|
||||||
@@ -41,9 +40,9 @@ vendor
|
|||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
|
|
||||||
# Ignore development-related files and directories
|
# Runtime-generated data must not become part of an image layer.
|
||||||
# For development environment, we will use mount bind to mount the source code to the container
|
storage/framework/cache/data/*
|
||||||
# So we don't need to copy the source code to the container for development
|
storage/framework/sessions/*
|
||||||
tests/
|
storage/framework/testing/*
|
||||||
phpunit.xml
|
storage/framework/views/*
|
||||||
phpunit.xml.dist
|
storage/logs/*
|
||||||
|
|||||||
+17
-1
@@ -10,7 +10,7 @@ APP_LOCALE=en
|
|||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=en_US
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
LOG_STACK=daily
|
LOG_STACK=stderr
|
||||||
|
|
||||||
# Optional Discord error logging (append `,discord_errors` to LOG_STACK to enable it)
|
# Optional Discord error logging (append `,discord_errors` to LOG_STACK to enable it)
|
||||||
LOG_DISCORD_WEBHOOK_URL=
|
LOG_DISCORD_WEBHOOK_URL=
|
||||||
@@ -38,6 +38,22 @@ MAIL_ENCRYPTION=tls
|
|||||||
MAIL_FROM_ADDRESS=no-reply@example.com
|
MAIL_FROM_ADDRESS=no-reply@example.com
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
|
||||||
FILESYSTEM_DISK=public
|
FILESYSTEM_DISK=public
|
||||||
|
AVATAR_FILESYSTEM_DISK=public
|
||||||
|
AVATAR_URLS_TEMPORARY=false
|
||||||
|
AVATAR_URL_EXPIRATION_MINUTES=15
|
||||||
|
|
||||||
|
# Production: set AVATAR_FILESYSTEM_DISK=s3 and inject these as runtime secrets.
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=eu-central-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_URL=
|
||||||
|
AWS_ENDPOINT=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: test_and_check_app
|
||||||
|
description: Build the test image and run all application checks
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Test and check app
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--target test \
|
||||||
|
--tag "local/kanbangit-test:${GITHUB_SHA}" \
|
||||||
|
--progress plain \
|
||||||
|
--load \
|
||||||
|
.
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
php-version: ['8.4']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php-version }}
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: xdebug
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- name: Install Node Dependencies
|
|
||||||
run: npm i --from-lock-file
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
|
||||||
|
|
||||||
- name: Copy Environment File
|
|
||||||
run: cp .env.example .env
|
|
||||||
|
|
||||||
- name: Generate Application Key
|
|
||||||
run: php artisan key:generate
|
|
||||||
|
|
||||||
- name: Build Assets
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
run: composer ci:check
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.twyco.de
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test_and_check_app:
|
||||||
|
name: test_and_check_app
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|
||||||
|
- name: Run required application checks
|
||||||
|
uses: ./.github/tests/test_and_check_app
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish immutable image
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'push' &&
|
||||||
|
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||||
|
needs:
|
||||||
|
- test_and_check_app
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Validate ref and calculate image name
|
||||||
|
id: image
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
image_path="$(printf '%s' "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
image="${REGISTRY}/${image_path}"
|
||||||
|
|
||||||
|
case "${GITHUB_REF}" in
|
||||||
|
refs/heads/main)
|
||||||
|
printf -v run_attempt '%03d' "${GITHUB_RUN_ATTEMPT}"
|
||||||
|
tag="main-${GITHUB_RUN_NUMBER}-${run_attempt}-${GITHUB_SHA:0:12}"
|
||||||
|
environment="staging"
|
||||||
|
;;
|
||||||
|
refs/tags/*)
|
||||||
|
tag="${GITHUB_REF#refs/tags/}"
|
||||||
|
|
||||||
|
if [[ ! "${tag}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||||
|
echo "Production tags must match vMAJOR.MINOR.PATCH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git fetch --no-tags origin main:refs/remotes/origin/main
|
||||||
|
|
||||||
|
if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
|
||||||
|
echo "Production tag must point to a commit contained in main." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
environment="production"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported ref: ${GITHUB_REF}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "image=${image}"
|
||||||
|
echo "tag=${tag}"
|
||||||
|
echo "environment=${environment}"
|
||||||
|
} >> "${GITEA_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Log in to private registry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${REGISTRY_USERNAME}" || -z "${REGISTRY_TOKEN}" ]]; then
|
||||||
|
echo "Registry credentials are missing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "${REGISTRY_TOKEN}" |
|
||||||
|
docker login \
|
||||||
|
--username "${REGISTRY_USERNAME}" \
|
||||||
|
--password-stdin \
|
||||||
|
"${REGISTRY}"
|
||||||
|
|
||||||
|
- name: Build and publish runtime image
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IMAGE: ${{ steps.image.outputs.image }}
|
||||||
|
IMAGE_TAG: ${{ steps.image.outputs.tag }}
|
||||||
|
DEPLOYMENT_ENVIRONMENT: ${{ steps.image.outputs.environment }}
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if [[ "${DEPLOYMENT_ENVIRONMENT}" == "production" ]] &&
|
||||||
|
docker buildx imagetools inspect "${IMAGE}:${IMAGE_TAG}" >/dev/null 2>&1; then
|
||||||
|
echo "Production image ${IMAGE}:${IMAGE_TAG} already exists and will not be overwritten." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--target runtime \
|
||||||
|
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
|
||||||
|
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
|
||||||
|
--label "de.twyco.environment=${DEPLOYMENT_ENVIRONMENT}" \
|
||||||
|
--tag "${IMAGE}:${IMAGE_TAG}" \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Remove registry credentials
|
||||||
|
if: ${{ always() }}
|
||||||
|
shell: bash
|
||||||
|
run: docker logout "${REGISTRY}" >/dev/null 2>&1 || true
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
ARG COMPOSER_IMAGE=composer:2.9.5
|
||||||
|
ARG NODE_IMAGE=node:22-bookworm-slim
|
||||||
|
ARG PHP_IMAGE=php:8.4-fpm-bookworm
|
||||||
|
|
||||||
|
FROM ${COMPOSER_IMAGE} AS composer_binary
|
||||||
|
FROM ${NODE_IMAGE} AS node_binary
|
||||||
|
|
||||||
|
FROM ${PHP_IMAGE} AS php_base
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gettext-base \
|
||||||
|
git \
|
||||||
|
libfcgi-bin \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libicu-dev \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
libmagickwand-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libzip-dev \
|
||||||
|
nginx \
|
||||||
|
procps \
|
||||||
|
unzip \
|
||||||
|
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||||
|
&& docker-php-ext-install -j"$(nproc)" \
|
||||||
|
bcmath \
|
||||||
|
gd \
|
||||||
|
intl \
|
||||||
|
opcache \
|
||||||
|
pcntl \
|
||||||
|
pdo_mysql \
|
||||||
|
pdo_pgsql \
|
||||||
|
pdo_sqlite \
|
||||||
|
pgsql \
|
||||||
|
soap \
|
||||||
|
zip \
|
||||||
|
&& pecl install imagick \
|
||||||
|
&& docker-php-ext-enable imagick \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
COPY --from=composer_binary /usr/bin/composer /usr/local/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
FROM php_base AS build
|
||||||
|
|
||||||
|
COPY --from=node_binary /usr/local/ /usr/local/
|
||||||
|
COPY . /var/www
|
||||||
|
|
||||||
|
RUN composer install --prefer-dist --no-interaction --no-progress \
|
||||||
|
&& npm ci \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
FROM build AS test
|
||||||
|
|
||||||
|
RUN touch .env \
|
||||||
|
&& composer audit --locked --no-interaction \
|
||||||
|
&& composer ci:check
|
||||||
|
|
||||||
|
FROM build AS production_builder
|
||||||
|
|
||||||
|
RUN composer install \
|
||||||
|
--classmap-authoritative \
|
||||||
|
--no-dev \
|
||||||
|
--no-interaction \
|
||||||
|
--no-progress \
|
||||||
|
--prefer-dist \
|
||||||
|
&& rm -rf node_modules tests phpunit.xml
|
||||||
|
|
||||||
|
FROM php_base AS runtime
|
||||||
|
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/kanbangit-entrypoint
|
||||||
|
COPY docker/nginx/nginx.conf.template /etc/nginx/nginx.conf.template
|
||||||
|
COPY docker/php-fpm-healthcheck.sh /usr/local/bin/php-fpm-healthcheck
|
||||||
|
COPY --from=production_builder --chown=www-data:www-data /var/www /var/www
|
||||||
|
|
||||||
|
RUN chmod +x /usr/local/bin/kanbangit-entrypoint /usr/local/bin/php-fpm-healthcheck \
|
||||||
|
&& mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
|
||||||
|
&& printf '%s\n' \
|
||||||
|
'[global]' \
|
||||||
|
'daemonize = no' \
|
||||||
|
'' \
|
||||||
|
'[www]' \
|
||||||
|
'pm.status_path = /status' \
|
||||||
|
> /usr/local/etc/php-fpm.d/zz-kanbangit.conf \
|
||||||
|
&& mkdir -p \
|
||||||
|
/var/www/bootstrap/cache \
|
||||||
|
/var/www/storage/framework/cache \
|
||||||
|
/var/www/storage/framework/sessions \
|
||||||
|
/var/www/storage/framework/testing \
|
||||||
|
/var/www/storage/framework/views \
|
||||||
|
/var/www/storage/logs \
|
||||||
|
&& chown -R www-data:www-data /var/www/bootstrap/cache /var/www/storage
|
||||||
|
|
||||||
|
USER www-data
|
||||||
|
|
||||||
|
EXPOSE 8080 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["kanbangit-entrypoint"]
|
||||||
|
CMD ["php-fpm"]
|
||||||
@@ -7,9 +7,9 @@ use App\Http\Resources\TicketPriorityResource;
|
|||||||
use App\Http\Resources\TicketReferenceResource;
|
use App\Http\Resources\TicketReferenceResource;
|
||||||
use App\Http\Resources\TicketStatusResource;
|
use App\Http\Resources\TicketStatusResource;
|
||||||
use App\Http\Resources\TicketTypeResource;
|
use App\Http\Resources\TicketTypeResource;
|
||||||
use App\Models\Ticket;
|
|
||||||
use App\Http\Resources\UserResource;
|
use App\Http\Resources\UserResource;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
class BuildProjectTicketOptionsPayload
|
class BuildProjectTicketOptionsPayload
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ class UpdateUserAvatar
|
|||||||
$color = $this->colorFor($user);
|
$color = $this->colorFor($user);
|
||||||
$path = sprintf('avatars/generated/%d-%s.svg', $user->id, (string) Str::uuid());
|
$path = sprintf('avatars/generated/%d-%s.svg', $user->id, (string) Str::uuid());
|
||||||
|
|
||||||
$stored = Storage::disk('public')->put(
|
$stored = Storage::disk($this->avatarDisk())->put(
|
||||||
$path,
|
$path,
|
||||||
$this->svg($this->initials($user->name), $color),
|
$this->svg($this->initials($user->name), $color),
|
||||||
|
['ContentType' => 'image/svg+xml'],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $stored) {
|
if (! $stored) {
|
||||||
@@ -53,7 +54,7 @@ class UpdateUserAvatar
|
|||||||
public function upload(User $user, UploadedFile $avatar): User
|
public function upload(User $user, UploadedFile $avatar): User
|
||||||
{
|
{
|
||||||
$oldPath = $user->avatar_path;
|
$oldPath = $user->avatar_path;
|
||||||
$path = $avatar->store('avatars/uploads', 'public');
|
$path = $avatar->store('avatars/uploads', $this->avatarDisk());
|
||||||
|
|
||||||
if (! is_string($path)) {
|
if (! is_string($path)) {
|
||||||
throw new RuntimeException('Uploaded avatar could not be stored.');
|
throw new RuntimeException('Uploaded avatar could not be stored.');
|
||||||
@@ -90,7 +91,14 @@ class UpdateUserAvatar
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage::disk('public')->delete($path);
|
Storage::disk($this->avatarDisk())->delete($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function avatarDisk(): string
|
||||||
|
{
|
||||||
|
$disk = config('filesystems.avatar_disk', 'public');
|
||||||
|
|
||||||
|
return is_string($disk) && $disk !== '' ? $disk : 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initials(string $name): string
|
private function initials(string $name): string
|
||||||
|
|||||||
+19
-3
@@ -77,12 +77,28 @@ class User extends Authenticatable
|
|||||||
protected function avatar(): Attribute
|
protected function avatar(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::get(
|
return Attribute::get(
|
||||||
fn (): ?string => filled($this->avatar_path)
|
fn (): ?string => $this->avatarUrl(),
|
||||||
? Storage::disk('public')->url($this->avatar_path)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function avatarUrl(): ?string
|
||||||
|
{
|
||||||
|
if (blank($this->avatar_path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diskName = config('filesystems.avatar_disk', 'public');
|
||||||
|
$disk = Storage::disk(is_string($diskName) && $diskName !== '' ? $diskName : 'public');
|
||||||
|
|
||||||
|
if ((bool) config('filesystems.avatar_urls_temporary', false)) {
|
||||||
|
$expirationMinutes = max(1, (int) config('filesystems.avatar_url_expiration_minutes', 15));
|
||||||
|
|
||||||
|
return $disk->temporaryUrl($this->avatar_path, now()->addMinutes($expirationMinutes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $disk->url($this->avatar_path);
|
||||||
|
}
|
||||||
|
|
||||||
public function permissionAssignments(): MorphMany
|
public function permissionAssignments(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(PermissionAssignment::class, 'assignee');
|
return $this->morphMany(PermissionAssignment::class, 'assignee');
|
||||||
|
|||||||
+2
-1
@@ -16,6 +16,7 @@
|
|||||||
"laravel/mcp": "^0.6",
|
"laravel/mcp": "^0.6",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"laravel/wayfinder": "^0.1.14",
|
"laravel/wayfinder": "^0.1.14",
|
||||||
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"scalar/laravel": "^0.2.1"
|
"scalar/laravel": "^0.2.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
@@ -66,8 +67,8 @@
|
|||||||
],
|
],
|
||||||
"ci:check": [
|
"ci:check": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npm audit --audit-level=high",
|
||||||
"npm run lint:check",
|
"npm run lint:check",
|
||||||
"npm run format:check",
|
|
||||||
"npm run types:check",
|
"npm run types:check",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+354
-10
@@ -4,8 +4,159 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "eff91ae30c1ac1ced1e64c7c3e8df7cc",
|
"content-hash": "7d965721310f81816807d6b78c5f9654",
|
||||||
"packages": [
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "aws/aws-crt-php",
|
||||||
|
"version": "v1.2.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/awslabs/aws-crt-php.git",
|
||||||
|
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||||
|
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.5"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"classmap": [
|
||||||
|
"src/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"Apache-2.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "AWS SDK Common Runtime Team",
|
||||||
|
"email": "aws-sdk-common-runtime@amazon.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "AWS Common Runtime for PHP",
|
||||||
|
"homepage": "https://github.com/awslabs/aws-crt-php",
|
||||||
|
"keywords": [
|
||||||
|
"amazon",
|
||||||
|
"aws",
|
||||||
|
"crt",
|
||||||
|
"sdk"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/awslabs/aws-crt-php/issues",
|
||||||
|
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
|
||||||
|
},
|
||||||
|
"time": "2024-10-18T22:15:13+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aws/aws-sdk-php",
|
||||||
|
"version": "3.393.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||||
|
"reference": "a5880510e500aa0fe13a6410183cd222b3263890"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5880510e500aa0fe13a6410183cd222b3263890",
|
||||||
|
"reference": "a5880510e500aa0fe13a6410183cd222b3263890",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"aws/aws-crt-php": "^1.2.3",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-pcre": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
|
||||||
|
"guzzlehttp/promises": "^2.0.3 || ^3.0",
|
||||||
|
"guzzlehttp/psr7": "^2.6.3 || ^3.0",
|
||||||
|
"mtdowling/jmespath.php": "^2.9.1",
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0",
|
||||||
|
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"andrewsville/php-token-reflection": "^1.4",
|
||||||
|
"aws/aws-php-sns-message-validator": "~1.0",
|
||||||
|
"behat/behat": "~3.0",
|
||||||
|
"composer/composer": "^2.7.8",
|
||||||
|
"dms/phpunit-arraysubset-asserts": "^v0.5.0",
|
||||||
|
"doctrine/cache": "~1.4",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"ext-sockets": "*",
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"psr/cache": "^2.0 || ^3.0",
|
||||||
|
"psr/simple-cache": "^2.0 || ^3.0",
|
||||||
|
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
|
||||||
|
"yoast/phpunit-polyfills": "^2.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||||
|
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||||
|
"ext-curl": "To send requests using cURL",
|
||||||
|
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
|
||||||
|
"ext-pcntl": "To use client-side monitoring",
|
||||||
|
"ext-sockets": "To use client-side monitoring"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Aws\\": "src/"
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"src/data/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"Apache-2.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Amazon Web Services",
|
||||||
|
"homepage": "https://aws.amazon.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||||
|
"homepage": "https://aws.amazon.com/sdk-for-php",
|
||||||
|
"keywords": [
|
||||||
|
"amazon",
|
||||||
|
"aws",
|
||||||
|
"cloud",
|
||||||
|
"dynamodb",
|
||||||
|
"ec2",
|
||||||
|
"glacier",
|
||||||
|
"s3",
|
||||||
|
"sdk"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||||
|
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||||
|
"source": "https://github.com/aws/aws-sdk-php/tree/3.393.4"
|
||||||
|
},
|
||||||
|
"time": "2026-08-21T18:24:57+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
"version": "v3.1.1",
|
"version": "v3.1.1",
|
||||||
@@ -2122,6 +2273,61 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-07-06T14:42:07+00:00"
|
"time": "2026-07-06T14:42:07+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "league/flysystem-aws-s3-v3",
|
||||||
|
"version": "3.35.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
|
||||||
|
"reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/8475ef9adfc6498b85469e2abec6fe3118cd08c4",
|
||||||
|
"reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"aws/aws-sdk-php": "^3.371.5",
|
||||||
|
"league/flysystem": "^3.10.0",
|
||||||
|
"league/mime-type-detection": "^1.0.0",
|
||||||
|
"php": "^8.0.2"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"guzzlehttp/guzzle": "<7.0",
|
||||||
|
"guzzlehttp/ringphp": "<1.1.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Flysystem\\AwsS3V3\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Frank de Jonge",
|
||||||
|
"email": "info@frankdejonge.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "AWS S3 filesystem adapter for Flysystem.",
|
||||||
|
"keywords": [
|
||||||
|
"Flysystem",
|
||||||
|
"aws",
|
||||||
|
"file",
|
||||||
|
"files",
|
||||||
|
"filesystem",
|
||||||
|
"s3",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.35.2"
|
||||||
|
},
|
||||||
|
"time": "2026-07-01T23:25:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "league/flysystem-local",
|
"name": "league/flysystem-local",
|
||||||
"version": "3.31.0",
|
"version": "3.31.0",
|
||||||
@@ -2512,6 +2718,72 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-01-02T08:56:05+00:00"
|
"time": "2026-01-02T08:56:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mtdowling/jmespath.php",
|
||||||
|
"version": "2.9.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/jmespath/jmespath.php.git",
|
||||||
|
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||||
|
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2.5 || ^8.0",
|
||||||
|
"symfony/polyfill-mbstring": "^1.17"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"composer/xdebug-handler": "^3.0.3",
|
||||||
|
"phpunit/phpunit": "^8.5.52"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/jp.php"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.9-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/JmesPath.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"JmesPath\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Graham Campbell",
|
||||||
|
"email": "hello@gjcampbell.co.uk",
|
||||||
|
"homepage": "https://github.com/GrahamCampbell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Michael Dowling",
|
||||||
|
"email": "mtdowling@gmail.com",
|
||||||
|
"homepage": "https://github.com/mtdowling"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Declaratively specify how to extract elements from a JSON document",
|
||||||
|
"keywords": [
|
||||||
|
"json",
|
||||||
|
"jsonpath"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/jmespath/jmespath.php/issues",
|
||||||
|
"source": "https://github.com/jmespath/jmespath.php/tree/2.9.2"
|
||||||
|
},
|
||||||
|
"time": "2026-07-06T18:56:19+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
"version": "3.13.2",
|
"version": "3.13.2",
|
||||||
@@ -4546,6 +4818,77 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-06-05T06:23:12+00:00"
|
"time": "2026-06-05T06:23:12+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/filesystem",
|
||||||
|
"version": "v8.1.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
|
"reference": "17856b7a222664a26a5ea1cb06ee0721c2438217"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/17856b7a222664a26a5ea1cb06ee0721c2438217",
|
||||||
|
"reference": "17856b7a222664a26a5ea1cb06ee0721c2438217",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4.1",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/polyfill-ctype": "~1.8",
|
||||||
|
"symfony/polyfill-mbstring": "~1.8"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/process": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Filesystem\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides basic utilities for the filesystem",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/filesystem/tree/v8.1.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-07-22T15:42:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v8.1.1",
|
"version": "v8.1.1",
|
||||||
@@ -8943,27 +9286,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v8.0.8",
|
"version": "v8.1.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/yaml.git",
|
"url": "https://github.com/symfony/yaml.git",
|
||||||
"reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1"
|
"reference": "faabdbe998e8c5c599dceffa27aa265b185c0736"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/54174ab48c0c0f9e21512b304be17f8150ccf8f1",
|
"url": "https://api.github.com/repos/symfony/yaml/zipball/faabdbe998e8c5c599dceffa27aa265b185c0736",
|
||||||
"reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1",
|
"reference": "faabdbe998e8c5c599dceffa27aa265b185c0736",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.4",
|
"php": ">=8.4.1",
|
||||||
"symfony/polyfill-ctype": "^1.8"
|
"symfony/polyfill-ctype": "^1.8"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"symfony/console": "<7.4"
|
"symfony/console": "<7.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/console": "^7.4|^8.0"
|
"symfony/console": "^7.4|^8.0",
|
||||||
|
"yaml/yaml-test-suite": "*"
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
"Resources/bin/yaml-lint"
|
"Resources/bin/yaml-lint"
|
||||||
@@ -8994,7 +9338,7 @@
|
|||||||
"description": "Loads and dumps YAML files",
|
"description": "Loads and dumps YAML files",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/yaml/tree/v8.0.8"
|
"source": "https://github.com/symfony/yaml/tree/v8.1.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -9014,7 +9358,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-03-30T15:14:47+00:00"
|
"time": "2026-07-22T15:42:13+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "theseer/tokenizer",
|
"name": "theseer/tokenizer",
|
||||||
@@ -9076,5 +9420,5 @@
|
|||||||
"php": "^8.4"
|
"php": "^8.4"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ return [
|
|||||||
|
|
||||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
'avatar_disk' => env('AVATAR_FILESYSTEM_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||||
|
|
||||||
|
'avatar_urls_temporary' => env('AVATAR_URLS_TEMPORARY', false),
|
||||||
|
|
||||||
|
'avatar_url_expiration_minutes' => env('AVATAR_URL_EXPIRATION_MINUTES', 15),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Filesystem Disks
|
| Filesystem Disks
|
||||||
|
|||||||
+32
-12
@@ -1,33 +1,38 @@
|
|||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
container_name: kanbangit-web
|
container_name: kanbangit-web
|
||||||
|
image: kanbangit-app:local
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./docker/nginx/Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
target: runtime
|
||||||
|
command: nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:8080"
|
||||||
volumes:
|
|
||||||
- kanbangit-storage:/var/www/storage:ro
|
|
||||||
- kanbangit-public-assets:/var/www/public/build:ro
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env.production
|
- .env.production
|
||||||
|
environment:
|
||||||
|
PHP_FPM_UPSTREAM: php-fpm:9000
|
||||||
networks:
|
networks:
|
||||||
- kanbangit-web
|
- kanbangit-web
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/up"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
depends_on:
|
depends_on:
|
||||||
php-fpm:
|
php-fpm:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
php-fpm:
|
php-fpm:
|
||||||
container_name: kanbangit-php-fpm
|
container_name: kanbangit-php-fpm
|
||||||
|
image: kanbangit-app:local
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./docker/php-fpm/Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: production
|
target: runtime
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
|
||||||
- kanbangit-storage:/var/www/storage
|
|
||||||
- kanbangit-public-assets:/var/www/public/build
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env.production
|
- .env.production
|
||||||
networks:
|
networks:
|
||||||
@@ -42,6 +47,23 @@ services:
|
|||||||
mariadb:
|
mariadb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
|
worker:
|
||||||
|
container_name: kanbangit-worker
|
||||||
|
image: kanbangit-app:local
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: runtime
|
||||||
|
command: php artisan queue:work --sleep=3 --tries=3 --timeout=60 --max-time=3600
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
networks:
|
||||||
|
- kanbangit-db
|
||||||
|
depends_on:
|
||||||
|
mariadb:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
container_name: kanbangit-db
|
container_name: kanbangit-db
|
||||||
image: mariadb:11.4
|
image: mariadb:11.4
|
||||||
@@ -65,5 +87,3 @@ networks:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb-data-production:
|
mariadb-data-production:
|
||||||
kanbangit-storage:
|
|
||||||
kanbangit-public-assets:
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "${1:-}" = "nginx" ]; then
|
||||||
|
: "${PHP_FPM_UPSTREAM:=127.0.0.1:9000}"
|
||||||
|
export PHP_FPM_UPSTREAM
|
||||||
|
|
||||||
|
envsubst '${PHP_FPM_UPSTREAM}' \
|
||||||
|
< /etc/nginx/nginx.conf.template \
|
||||||
|
> /tmp/nginx.conf
|
||||||
|
|
||||||
|
exec nginx -c /tmp/nginx.conf -g 'daemon off;'
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p \
|
||||||
|
bootstrap/cache \
|
||||||
|
storage/framework/cache \
|
||||||
|
storage/framework/sessions \
|
||||||
|
storage/framework/testing \
|
||||||
|
storage/framework/views \
|
||||||
|
storage/logs
|
||||||
|
|
||||||
|
php artisan config:cache --no-ansi
|
||||||
|
php artisan route:cache --no-ansi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
FROM php:8.4-cli-bookworm AS builder
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
curl ca-certificates git unzip \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- \
|
|
||||||
--install-dir=/usr/local/bin --filename=composer
|
|
||||||
WORKDIR /var/www
|
|
||||||
COPY . /var/www
|
|
||||||
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --optimize-autoloader
|
|
||||||
RUN npm ci && npm run build
|
|
||||||
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
|
||||||
COPY --from=builder /var/www/public /var/www/public
|
|
||||||
WORKDIR /var/www/public
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
client_max_body_size 100m;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /var/www/public;
|
|
||||||
index index.php index.html;
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
|
||||||
}
|
|
||||||
location ^~ /storage/ {
|
|
||||||
alias /var/www/storage/app/public/;
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
expires 7d;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
location ~ \.php$ {
|
|
||||||
fastcgi_pass php-fpm:9000;
|
|
||||||
fastcgi_index index.php;
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
||||||
include fastcgi_params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
pid /tmp/nginx.pid;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
access_log /dev/stdout;
|
||||||
|
client_body_temp_path /tmp/client_body;
|
||||||
|
proxy_temp_path /tmp/proxy;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi;
|
||||||
|
scgi_temp_path /tmp/scgi;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
client_max_body_size 3m;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location = /nginx-health {
|
||||||
|
access_log off;
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.php {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param HTTP_PROXY '';
|
||||||
|
fastcgi_pass ${PHP_FPM_UPSTREAM};
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_NAME=/status \
|
||||||
|
SCRIPT_FILENAME=/status \
|
||||||
|
REQUEST_METHOD=GET \
|
||||||
|
cgi-fcgi -bind -connect 127.0.0.1:9000 2>/dev/null | grep -q '^pool:'
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
FROM php:8.4-fpm AS builder
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
curl \
|
|
||||||
unzip \
|
|
||||||
libpq-dev \
|
|
||||||
libonig-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
libcurl4-openssl-dev \
|
|
||||||
libicu-dev \
|
|
||||||
libzip-dev \
|
|
||||||
libmagickwand-dev \
|
|
||||||
libpng-dev \
|
|
||||||
libjpeg-dev \
|
|
||||||
libfreetype6-dev \
|
|
||||||
&& docker-php-ext-install -j$(nproc) \
|
|
||||||
pdo_mysql \
|
|
||||||
pdo_pgsql \
|
|
||||||
pgsql \
|
|
||||||
opcache \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
bcmath \
|
|
||||||
soap \
|
|
||||||
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
|
|
||||||
&& docker-php-ext-install -j$(nproc) gd \
|
|
||||||
&& pecl install imagick \
|
|
||||||
&& docker-php-ext-enable imagick \
|
|
||||||
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
WORKDIR /var/www
|
|
||||||
COPY . /var/www
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
|
||||||
&& composer install --no-dev --optimize-autoloader --no-interaction --no-progress --prefer-dist
|
|
||||||
FROM php:8.4-fpm AS production
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libpq-dev \
|
|
||||||
libicu-dev \
|
|
||||||
libzip-dev \
|
|
||||||
libfcgi-bin \
|
|
||||||
procps \
|
|
||||||
libmagickwand-dev \
|
|
||||||
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
RUN curl -o /usr/local/bin/php-fpm-healthcheck \
|
|
||||||
https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \
|
|
||||||
&& chmod +x /usr/local/bin/php-fpm-healthcheck
|
|
||||||
COPY ./docker/php-fpm/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
||||||
COPY ./storage /var/www/storage-init
|
|
||||||
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
|
|
||||||
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
|
|
||||||
COPY --from=builder /usr/local/bin/docker-php-ext-* /usr/local/bin/
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'[global]' \
|
|
||||||
'daemonize = no' \
|
|
||||||
'' \
|
|
||||||
'[www]' \
|
|
||||||
'pm.status_path = /status' \
|
|
||||||
> /usr/local/etc/php-fpm.d/zz-docker.conf
|
|
||||||
COPY --from=builder /var/www /var/www
|
|
||||||
WORKDIR /var/www
|
|
||||||
RUN chown -R www-data:www-data /var/www
|
|
||||||
USER www-data
|
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
||||||
EXPOSE 9000
|
|
||||||
CMD ["php-fpm"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
if [ ! "$(ls -A /var/www/storage)" ]; then
|
|
||||||
echo "Initializing storage directory..."
|
|
||||||
cp -R /var/www/storage-init/. /var/www/storage
|
|
||||||
chown -R www-data:www-data /var/www/storage
|
|
||||||
fi
|
|
||||||
rm -rf /var/www/storage-init
|
|
||||||
php artisan config:cache
|
|
||||||
php artisan route:cache
|
|
||||||
exec "$@"
|
|
||||||
@@ -37,7 +37,10 @@ const eslintConfig = [
|
|||||||
'bootstrap/cache/**',
|
'bootstrap/cache/**',
|
||||||
'public/build/**',
|
'public/build/**',
|
||||||
'public/hot',
|
'public/hot',
|
||||||
|
'resources/js/actions/**',
|
||||||
'resources/js/components/ui/**',
|
'resources/js/components/ui/**',
|
||||||
|
'resources/js/routes/**',
|
||||||
|
'resources/js/wayfinder/**',
|
||||||
'resources/views/mail/**',
|
'resources/views/mail/**',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+497
-198
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,10 @@
|
|||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_KEY" value="base64:11ujGYe5vzbwqcdFDVQC6PuDgd7c8PKD9kWt9HH9Bvs="/>
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="AVATAR_FILESYSTEM_DISK" value="public"/>
|
||||||
|
<env name="AVATAR_URLS_TEMPORARY" value="false"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ namespace Tests\Feature\Models;
|
|||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Database\Seeders\UserSeeder;
|
use Database\Seeders\UserSeeder;
|
||||||
|
use DateTimeInterface;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Filesystem\FilesystemAdapter;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class UserTest extends TestCase
|
class UserTest extends TestCase
|
||||||
@@ -32,9 +35,13 @@ class UserTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_user_avatar_attribute_returns_public_storage_url(): void
|
public function test_user_avatar_attribute_returns_configured_storage_url(): void
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
config([
|
||||||
|
'filesystems.avatar_disk' => 'avatar-test',
|
||||||
|
'filesystems.avatar_urls_temporary' => false,
|
||||||
|
]);
|
||||||
|
Storage::fake('avatar-test');
|
||||||
|
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'avatar_path' => 'avatars/generated/example.svg',
|
'avatar_path' => 'avatars/generated/example.svg',
|
||||||
@@ -43,12 +50,38 @@ class UserTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
Storage::disk('public')->url('avatars/generated/example.svg'),
|
Storage::disk('avatar-test')->url('avatars/generated/example.svg'),
|
||||||
$user->avatar,
|
$user->avatar,
|
||||||
);
|
);
|
||||||
$this->assertArrayNotHasKey('avatar_path', $user->toArray());
|
$this->assertArrayNotHasKey('avatar_path', $user->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_user_avatar_attribute_returns_temporary_url_for_private_storage(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'filesystems.avatar_disk' => 's3',
|
||||||
|
'filesystems.avatar_urls_temporary' => true,
|
||||||
|
'filesystems.avatar_url_expiration_minutes' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$disk = Mockery::mock(FilesystemAdapter::class);
|
||||||
|
$disk->shouldReceive('temporaryUrl')
|
||||||
|
->once()
|
||||||
|
->with(
|
||||||
|
'avatars/uploads/private.png',
|
||||||
|
Mockery::on(fn (mixed $expiration): bool => $expiration instanceof DateTimeInterface),
|
||||||
|
)
|
||||||
|
->andReturn('https://object-storage.test/signed-avatar');
|
||||||
|
Storage::shouldReceive('disk')->once()->with('s3')->andReturn($disk);
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'avatar_path' => 'avatars/uploads/private.png',
|
||||||
|
'avatar_is_generated' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('https://object-storage.test/signed-avatar', $user->avatar);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_user_can_identify_uploaded_avatars(): void
|
public function test_user_can_identify_uploaded_avatars(): void
|
||||||
{
|
{
|
||||||
$generated = User::factory()->make([
|
$generated = User::factory()->make([
|
||||||
|
|||||||
@@ -72,14 +72,15 @@ class ProfileUpdateTest extends TestCase
|
|||||||
|
|
||||||
public function test_user_can_upload_a_profile_picture()
|
public function test_user_can_upload_a_profile_picture()
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
config(['filesystems.avatar_disk' => 'avatar-test']);
|
||||||
|
Storage::fake('avatar-test');
|
||||||
|
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'avatar_path' => 'avatars/generated/old.svg',
|
'avatar_path' => 'avatars/generated/old.svg',
|
||||||
'avatar_color' => '#F97316',
|
'avatar_color' => '#F97316',
|
||||||
'avatar_is_generated' => true,
|
'avatar_is_generated' => true,
|
||||||
]);
|
]);
|
||||||
Storage::disk('public')->put($user->avatar_path, '<svg>OU</svg>');
|
Storage::disk('avatar-test')->put($user->avatar_path, '<svg>OU</svg>');
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
@@ -100,8 +101,8 @@ class ProfileUpdateTest extends TestCase
|
|||||||
$this->assertSame('#F97316', $user->avatar_color);
|
$this->assertSame('#F97316', $user->avatar_color);
|
||||||
$this->assertNotSame('avatars/generated/old.svg', $user->avatar_path);
|
$this->assertNotSame('avatars/generated/old.svg', $user->avatar_path);
|
||||||
$this->assertStringStartsWith('avatars/uploads/', $user->avatar_path);
|
$this->assertStringStartsWith('avatars/uploads/', $user->avatar_path);
|
||||||
Storage::disk('public')->assertMissing('avatars/generated/old.svg');
|
Storage::disk('avatar-test')->assertMissing('avatars/generated/old.svg');
|
||||||
Storage::disk('public')->assertExists($user->avatar_path);
|
Storage::disk('avatar-test')->assertExists($user->avatar_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_user_can_delete_their_uploaded_profile_picture()
|
public function test_user_can_delete_their_uploaded_profile_picture()
|
||||||
|
|||||||
Reference in New Issue
Block a user