Compare commits
2 Commits
feat/testi
...
2a5c55d90f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a5c55d90f | |||
| 9c43ad1d65 |
@@ -2,7 +2,6 @@ APP_NAME=Amare
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
# Staging/production: set APP_URL to the public HTTPS origin (e.g. https://staging.example.com).
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=pt_BR
|
||||
@@ -39,10 +38,6 @@ SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
# Staging/production behind HTTPS (Dokploy Traefik): SESSION_SECURE_COOKIE=true
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_HTTP_ONLY=true
|
||||
SESSION_SAME_SITE=lax
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
82
.github/workflows/deploy-staging.yml
vendored
82
.github/workflows/deploy-staging.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: Deploy staging
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: publish-and-deploy-staging
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout deployed SHA
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Set image metadata
|
||||
id: meta
|
||||
run: |
|
||||
SHA="${{ github.event.workflow_run.head_sha }}"
|
||||
SHORT_SHA="${SHA:0:7}"
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push SHA + staging tags
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}
|
||||
${{ steps.meta.outputs.image }}:staging
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Deploy staging on Dokploy
|
||||
env:
|
||||
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_STAGING_COMPOSE_ID }}
|
||||
DEPLOY_TITLE: "staging ${{ steps.meta.outputs.short_sha }}"
|
||||
run: |
|
||||
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||
./scripts/deploy/dokploy-deploy.sh
|
||||
|
||||
- name: Smoke staging
|
||||
env:
|
||||
SMOKE_BASE_URL: ${{ secrets.STAGING_URL }}
|
||||
run: |
|
||||
chmod +x scripts/deploy/smoke.sh
|
||||
./scripts/deploy/smoke.sh
|
||||
84
.github/workflows/promote-production.yml
vendored
84
.github/workflows/promote-production.yml
vendored
@@ -1,84 +0,0 @@
|
||||
name: Promote production
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: Full git SHA already published to GHCR (same digest used by staging)
|
||||
required: true
|
||||
type: string
|
||||
confirm:
|
||||
description: Type PRODUCTION to confirm promotion of the given SHA
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
name: promote-production
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Guard confirmation
|
||||
run: |
|
||||
if [ "${{ inputs.confirm }}" != "PRODUCTION" ]; then
|
||||
echo "Confirmation must be exactly PRODUCTION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository scripts
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set image metadata
|
||||
id: meta
|
||||
run: |
|
||||
SHA="${{ inputs.sha }}"
|
||||
SHORT_SHA="${SHA:0:7}"
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Point :production at existing SHA digest (no rebuild)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag "${{ steps.meta.outputs.image }}:production" \
|
||||
"${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}"
|
||||
|
||||
- name: Deploy production on Dokploy
|
||||
env:
|
||||
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_PRODUCTION_COMPOSE_ID }}
|
||||
DEPLOY_TITLE: "production ${{ steps.meta.outputs.short_sha }}"
|
||||
run: |
|
||||
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||
./scripts/deploy/dokploy-deploy.sh
|
||||
|
||||
- name: Smoke production
|
||||
env:
|
||||
SMOKE_BASE_URL: ${{ secrets.PRODUCTION_URL }}
|
||||
run: |
|
||||
chmod +x scripts/deploy/smoke.sh
|
||||
./scripts/deploy/smoke.sh
|
||||
@@ -125,10 +125,3 @@ Após `php artisan db:seed`:
|
||||
- [SPEC.md](SPEC.md) — especificação do produto
|
||||
- [docs/adr/](docs/adr/) — ADRs aceitas
|
||||
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
|
||||
- [docs/deployment/dokploy.md](docs/deployment/dokploy.md) — deploy staging/produção no Dokploy + GHCR
|
||||
|
||||
## Deploy (Dokploy)
|
||||
|
||||
Staging publica automaticamente após CI verde em `main` (imagem GHCR por SHA + alias `:staging`). Produção promove a **mesma digest** com workflow manual `Promote production` (sem rebuild).
|
||||
|
||||
Ver runbook completo: [docs/deployment/dokploy.md](docs/deployment/dokploy.md).
|
||||
|
||||
@@ -26,7 +26,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureLivewireTemporaryUploads();
|
||||
$this->freezeClockWhenConfigured();
|
||||
|
||||
View::composer('layouts.public', function (ViewInstance $view): void {
|
||||
@@ -47,22 +46,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep Livewire/Filament temp uploads on the local disk.
|
||||
*
|
||||
* When FILESYSTEM_DISK=r2, Livewire would otherwise use the S3 driver and
|
||||
* browser-PUT straight to R2 (CORS). Final media still uses the r2 disk via
|
||||
* PublicImageUploadRules. Explicit LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK wins.
|
||||
*/
|
||||
private function configureLivewireTemporaryUploads(): void
|
||||
{
|
||||
if (filled(config('livewire.temporary_file_upload.disk'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
config(['livewire.temporary_file_upload.disk' => 'local']);
|
||||
}
|
||||
|
||||
private function freezeClockWhenConfigured(): void
|
||||
{
|
||||
if ($this->app->environment('production')) {
|
||||
|
||||
@@ -12,8 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
// Trust Traefik/Dokploy (and local reverse proxies) for X-Forwarded-* headers.
|
||||
$middleware->trustProxies(at: '*');
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
||||
use App\Models\PortfolioImage;
|
||||
use App\Models\Service;
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -22,7 +23,7 @@ class ContentSeeder extends Seeder
|
||||
$this->seedSiteSettings();
|
||||
$this->seedServices();
|
||||
$this->seedPortfolioCases();
|
||||
$this->call(TestimonialsSeeder::class);
|
||||
$this->seedTestimonials();
|
||||
}
|
||||
|
||||
private function seedSiteSettings(): void
|
||||
@@ -169,6 +170,67 @@ class ContentSeeder extends Seeder
|
||||
}
|
||||
}
|
||||
|
||||
private function seedTestimonials(): void
|
||||
{
|
||||
// Real couples from depoimentos.md. Production publication still requires
|
||||
// explicit couple authorization before setting published_at outside local/demo seeds.
|
||||
$testimonials = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
],
|
||||
];
|
||||
|
||||
$keepAuthors = array_column($testimonials, 'author_name');
|
||||
|
||||
Testimonial::query()
|
||||
->whereNotIn('author_name', $keepAuthors)
|
||||
->delete();
|
||||
|
||||
foreach ($testimonials as $testimonial) {
|
||||
Testimonial::query()->updateOrCreate(
|
||||
['author_name' => $testimonial['author_name']],
|
||||
[
|
||||
...$testimonial,
|
||||
'photo_path' => null,
|
||||
'photo_alt' => null,
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFixture(string $fixtureName, string $destination): string
|
||||
{
|
||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TestimonialsSeeder extends Seeder
|
||||
{
|
||||
private const PUBLISHED_AT = '2026-08-05 00:00:00';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$testimonials = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
],
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($testimonials): void {
|
||||
foreach ($testimonials as $testimonial) {
|
||||
Testimonial::query()->updateOrCreate(
|
||||
['author_name' => $testimonial['author_name']],
|
||||
[
|
||||
'quote' => $testimonial['quote'],
|
||||
'context' => $testimonial['context'],
|
||||
'sort_order' => $testimonial['sort_order'],
|
||||
'is_featured' => $testimonial['is_featured'],
|
||||
'published_at' => self::PUBLISHED_AT,
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
# Shared Compose for Dokploy staging and production.
|
||||
# Both stacks use the same file with different env:
|
||||
# APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||
# IMAGE_TAG=staging|production|<git-sha>
|
||||
# PostgreSQL is a separate Dokploy database service (not defined here).
|
||||
# Traefik/Dokploy domains should target service `web` port 8000.
|
||||
# App services join dokploy-network so they can resolve Dokploy-managed
|
||||
# Postgres internal hosts (e.g. amare-stg-pez43e).
|
||||
|
||||
services:
|
||||
migrate:
|
||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||
pull_policy: always
|
||||
restart: "no"
|
||||
env_file:
|
||||
- .env
|
||||
command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
web:
|
||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
expose:
|
||||
- "8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/up"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 40s
|
||||
retries: 3
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
queue:
|
||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
command: ["php", "artisan", "queue:work", "--sleep=2", "--tries=3", "--max-time=3600"]
|
||||
stop_grace_period: 60s
|
||||
stop_signal: SIGTERM
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
scheduler:
|
||||
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
command: ["php", "artisan", "schedule:work"]
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
@@ -7,8 +7,5 @@ Mesma imagem, comandos distintos:
|
||||
| web | `frankenphp run --config /etc/caddy/Caddyfile` |
|
||||
| queue | `php artisan queue:work --sleep=2 --tries=3` |
|
||||
| scheduler | `php artisan schedule:work` |
|
||||
| migrate | `php artisan migrate --force` (one-shot no Compose de deploy) |
|
||||
|
||||
FrankenPHP em **modo regular** (ADR-006). Worker mode proibido no MVP.
|
||||
|
||||
Deploy Dokploy (staging/produção): ver [`docker-compose.deploy.yml`](../docker-compose.deploy.yml) e [docs/deployment/dokploy.md](../docs/deployment/dokploy.md).
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
# Deploy Dokploy (staging → production)
|
||||
|
||||
Runbook for operating Amare on a VPS with Dokploy connected to GitHub, publishing immutable images to GHCR.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CI (main) → build FrankenPHP image → GHCR :<sha> + :staging
|
||||
→ Dokploy staging compose.deploy
|
||||
→ smoke /up / /admin/login
|
||||
|
||||
Promote (manual) → retag same digest as :production (no rebuild)
|
||||
→ Dokploy production compose.deploy
|
||||
→ smoke
|
||||
```
|
||||
|
||||
| Piece | Detail |
|
||||
|---|---|
|
||||
| Compose file | [`docker-compose.deploy.yml`](../../docker-compose.deploy.yml) |
|
||||
| Processes | `migrate` (one-shot) → `web` / `queue` / `scheduler` |
|
||||
| Image | `ghcr.io/<owner>/<repo>:<sha>` (+ aliases `:staging`, `:production`) |
|
||||
| Database | Dokploy PostgreSQL **per environment** (not in the app image) |
|
||||
| Media | Cloudflare R2 (`FILESYSTEM_DISK=r2`), separate buckets per environment |
|
||||
| Mail | Resend (`MAIL_MAILER=resend`) |
|
||||
| Proxy | Dokploy Traefik → service `web` port `8000` |
|
||||
|
||||
## Prerequisites (manual)
|
||||
|
||||
1. Dokploy installed on the VPS; GitHub provider connected.
|
||||
2. GHCR registry in Dokploy (`ghcr.io`) with a PAT that can **read** packages (`read:packages`). Prefer a dedicated bot/token; do not store write tokens on the VPS.
|
||||
3. Two PostgreSQL services in Dokploy (staging + production), private (no public port).
|
||||
4. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
|
||||
5. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
|
||||
|
||||
## Create Compose stacks
|
||||
|
||||
Create **two** Dokploy Compose services (same repo, same compose path):
|
||||
|
||||
| Stack | Compose path | `IMAGE_TAG` | Notes |
|
||||
|---|---|---|---|
|
||||
| staging | `docker-compose.deploy.yml` | `staging` | Auto-deployed after CI on `main` |
|
||||
| production | `docker-compose.deploy.yml` | `production` | Manual promotion only |
|
||||
|
||||
Dokploy Environment for each stack must set:
|
||||
|
||||
```bash
|
||||
APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||
IMAGE_TAG=staging # or production
|
||||
```
|
||||
|
||||
Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish PostgreSQL or host ports for app processes.
|
||||
|
||||
Compose services must join the external Docker network `dokploy-network` (declared in `docker-compose.deploy.yml`) so they can resolve the Dokploy-managed Postgres internal host (e.g. `amare-stg-pez43e`). Set `DB_HOST` to that **Internal Host** from the Dokploy database UI — not a public hostname.
|
||||
|
||||
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
|
||||
|
||||
## Required Laravel env (Dokploy only)
|
||||
|
||||
Set these in Dokploy Environment UI (written to `.env` next to the compose file). **Never** put them in GitHub Actions secrets or image layers.
|
||||
|
||||
```env
|
||||
APP_NAME=Amare
|
||||
APP_ENV=staging # or production
|
||||
APP_KEY=base64:... # unique per environment — generate with php artisan key:generate --show
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://staging.example.com
|
||||
|
||||
APP_LOCALE=pt_BR
|
||||
APP_FALLBACK_LOCALE=pt_BR
|
||||
APP_TIMEZONE=America/Fortaleza
|
||||
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=<dokploy-postgres-internal-host> # Internal Host from Dokploy UI (requires dokploy-network)
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=amare_staging
|
||||
DB_USERNAME=...
|
||||
DB_PASSWORD=...
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_SECURE_COOKIE=true
|
||||
SESSION_HTTP_ONLY=true
|
||||
SESSION_SAME_SITE=lax
|
||||
CACHE_STORE=database
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
FILESYSTEM_DISK=r2
|
||||
R2_ACCESS_KEY_ID=...
|
||||
R2_SECRET_ACCESS_KEY=...
|
||||
R2_BUCKET=...
|
||||
R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
|
||||
R2_URL=https://media-staging.example.com
|
||||
|
||||
MAIL_MAILER=resend
|
||||
RESEND_API_KEY=...
|
||||
MAIL_FROM_ADDRESS=noreply@example.com
|
||||
MAIL_FROM_NAME=Amare
|
||||
|
||||
LOG_LEVEL=warning
|
||||
```
|
||||
|
||||
Trusted proxies are configured in `bootstrap/app.php` so Traefik `X-Forwarded-*` headers work for HTTPS cookies and URLs.
|
||||
|
||||
Livewire temporary uploads default to the **local** disk in `AppServiceProvider` (even when `FILESYSTEM_DISK=r2`), so Filament does not browser-PUT to R2. Final media still lands on R2 via `PublicImageUploadRules`. Optional override: `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK` in Dokploy `.env` (`env_file` accepts any key — does not need a compose `environment:` entry).
|
||||
|
||||
### R2 CORS (public/media reads from JS)
|
||||
|
||||
Upload path does not need R2 CORS with the local temp-disk default. Still useful if the browser fetches R2 URLs cross-origin from JS. Origin must match `APP_URL` exactly; include `AllowedHeaders`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedOrigins": ["https://hellomanoel.com"],
|
||||
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"ExposeHeaders": ["ETag", "Content-Type"],
|
||||
"MaxAgeSeconds": 3600
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Also enable public access / custom domain for `R2_URL` so `<img>` URLs work after save.
|
||||
|
||||
## GitHub Actions secrets
|
||||
|
||||
Repository secrets used by workflows:
|
||||
|
||||
| Secret | Purpose |
|
||||
|---|---|
|
||||
| `DOKPLOY_URL` | Panel origin **without** `/api` (e.g. `https://panel.example.com`). Do not use the OpenAPI base URL that ends in `/api` — that yields `/api/api/...` and 404s. |
|
||||
| `DOKPLOY_API_KEY` | API key from Dokploy profile → API/CLI |
|
||||
| `DOKPLOY_STAGING_COMPOSE_ID` | Staging **Compose** service id (not an Application id) |
|
||||
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production **Compose** service id (not an Application id) |
|
||||
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
|
||||
| `PRODUCTION_URL` | Public origin for production smoke |
|
||||
|
||||
HTTP 404 from `compose.deploy` usually means the compose id is wrong (Application id instead of Compose) or `DOKPLOY_URL` still includes `/api`.
|
||||
|
||||
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Staging (automatic)
|
||||
|
||||
[`.github/workflows/deploy-staging.yml`](../../.github/workflows/deploy-staging.yml)
|
||||
|
||||
1. Waits for workflow `CI` success on push to `main`.
|
||||
2. Builds once; pushes `:<full-sha>` and `:staging`.
|
||||
3. Calls Dokploy `compose.deploy` and polls until done.
|
||||
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
|
||||
|
||||
### Production (manual)
|
||||
|
||||
[`.github/workflows/promote-production.yml`](../../.github/workflows/promote-production.yml)
|
||||
|
||||
1. Operator runs **Actions → Promote production**.
|
||||
2. Inputs: full `sha` already on GHCR; `confirm` must be exactly `PRODUCTION`.
|
||||
3. Retags the **same digest** as `:production` (no rebuild).
|
||||
4. Deploys production compose + smoke.
|
||||
|
||||
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||
|
||||
## First admin and authorized production seeding
|
||||
|
||||
Seed credentials are local-only. For staging/production:
|
||||
|
||||
FrankenPHP sets `XDG_CONFIG_HOME=/config` (Caddy). PsySH/tinker then tries `/config/psysh`, which `appuser` cannot write — you get `Writing to directory /config/psysh is not allowed.` Override that env for the one-shot command:
|
||||
|
||||
```bash
|
||||
# From Dokploy → staging/production → Open terminal on `web` (or one-off run)
|
||||
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||
\$user = \\App\\Models\\User::query()->updateOrCreate(
|
||||
['email' => 'admin@example.com'],
|
||||
[
|
||||
'name' => 'Admin',
|
||||
'password' => 'use-a-strong-password',
|
||||
'role' => 'admin',
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
\$user->forceFill(['email_verified_at' => now()])->save();
|
||||
echo \$user->email.PHP_EOL;
|
||||
"
|
||||
```
|
||||
|
||||
Plain password is enough: `User` casts `password` to `hashed` (and skips re-hash when value already hashed). `email_verified_at` is not mass-assignable — use `forceFill` as above.
|
||||
|
||||
Confirm:
|
||||
|
||||
```bash
|
||||
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::query()->where('email', 'admin@example.com')->exists() ? 'ok' : 'missing';"
|
||||
```
|
||||
|
||||
Never reuse `admin@amare.local` / `password`.
|
||||
|
||||
### Load authorized testimonials after migrations
|
||||
|
||||
After migrations, manually load the five authorized testimonials in **staging**, then repeat in **production**. From Dokploy, open a terminal on the environment's `web` service (or run an equivalent one-off process):
|
||||
|
||||
```bash
|
||||
php artisan db:seed --class='Database\Seeders\TestimonialsSeeder' --force --no-interaction
|
||||
```
|
||||
|
||||
This seeder is safe to rerun: it overwrites canonical source-owned fields, preserves curated photo fields, and leaves unrelated testimonials unchanged. The five records are published with the approved deterministic timestamp. Upsert keys on `author_name` (no unique DB constraint); keep one row per couple before/after running.
|
||||
|
||||
Optional verification:
|
||||
|
||||
```bash
|
||||
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||
\$expected = collect(['Jeniffer e Maick', 'Quesia e Jhonata', 'Milena e Weslley', 'Raquel e Pedro', 'Victoria e Pedro']);
|
||||
\$rows = \\App\\Models\\Testimonial::query()->whereIn('author_name', \$expected)->get(['author_name', 'published_at'])->groupBy('author_name');
|
||||
\$valid = \$expected->every(function (string \$author) use (\$rows): bool {
|
||||
\$matches = \$rows->get(\$author, collect());
|
||||
return \$matches->count() === 1
|
||||
&& \$matches->first()->published_at?->format('Y-m-d H:i:s') === '2026-08-05 00:00:00';
|
||||
});
|
||||
echo (\$valid ? 'ok' : 'invalid').PHP_EOL;
|
||||
"
|
||||
```
|
||||
|
||||
Expected output: `ok`.
|
||||
|
||||
Do not run `DatabaseSeeder` or `ContentSeeder` in staging or production: they include local credentials and/or broad demo-content effects. Deployment workflows intentionally remain migrate-only; loading these testimonials is a deliberate manual operation in each environment.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
||||
|
||||
### Configure (Dokploy)
|
||||
|
||||
1. Settings → Destinations: add S3-compatible destination (AWS S3, R2, etc.).
|
||||
2. Open each PostgreSQL service → Backup:
|
||||
- Destination: the S3 destination
|
||||
- Schedule: cron e.g. `0 3 * * *`
|
||||
- Prefix: `amare/staging` or `amare/production`
|
||||
- Enabled: on
|
||||
3. Click **Test** and verify the object appears in the bucket.
|
||||
4. Prefer Dokploy alerts/webhooks for backup failure if configured.
|
||||
|
||||
### Restore (staging rehearsal before first production promote)
|
||||
|
||||
1. Create a scratch database or restore into a disposable Postgres service.
|
||||
2. Database → Backup → **Restore**: pick destination + backup file + target database name.
|
||||
3. Point a temporary compose env at the restored DB and confirm `/up` + `/admin/login`.
|
||||
4. Document the timestamp of the successful rehearsal.
|
||||
|
||||
Do **not** promote to production until staging restore has been proven once.
|
||||
|
||||
## Rollback
|
||||
|
||||
No rebuild. Move the environment alias to a previous SHA digest and redeploy.
|
||||
|
||||
### Staging
|
||||
|
||||
```bash
|
||||
# Locally or in a one-off Actions shell with GHCR login
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/<owner>/<repo>:staging \
|
||||
ghcr.io/<owner>/<repo>:<previous-sha>
|
||||
|
||||
# Then trigger Dokploy deploy (UI Deploy, or):
|
||||
DOKPLOY_URL=... DOKPLOY_API_KEY=... DOKPLOY_COMPOSE_ID=... \
|
||||
./scripts/deploy/dokploy-deploy.sh
|
||||
|
||||
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
Same pattern with `:production` tag and production compose id / URL. Prefer re-running **Promote production** with the previous SHA and confirmation `PRODUCTION`.
|
||||
|
||||
If a migration is not backward-compatible, fix forward with a new SHA; keep migrations reversible when possible.
|
||||
|
||||
## Smoke checks
|
||||
|
||||
```bash
|
||||
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||
```
|
||||
|
||||
Expects HTTP 200 for `/up`, `/`, and `/admin/login`.
|
||||
|
||||
## Domain checklist before production promote
|
||||
|
||||
- [ ] Final hostname DNS → VPS
|
||||
- [ ] Dokploy TLS certificate issued
|
||||
- [ ] `APP_URL` matches public HTTPS origin
|
||||
- [ ] `SESSION_SECURE_COOKIE=true`
|
||||
- [ ] Staging smoke green on the SHA to promote
|
||||
- [ ] Staging backup + restore rehearsed
|
||||
- [ ] Production Postgres backup schedule enabled
|
||||
- [ ] Production R2 bucket + Resend domain ready
|
||||
- [ ] First admin created without seed
|
||||
|
||||
## Local validation of Compose
|
||||
|
||||
```bash
|
||||
APP_IMAGE=ghcr.io/<owner>/<repo> IMAGE_TAG=staging \
|
||||
docker compose -f docker-compose.deploy.yml config
|
||||
```
|
||||
|
||||
Requires a `.env` file present (Dokploy creates it from Environment UI). For local config checks, an empty `.env` is enough.
|
||||
@@ -15,17 +15,15 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
||||
|
||||
- Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
|
||||
- Deploy automático em `main` após CI: build → GHCR → Dokploy API → migrate → health → smoke.
|
||||
- Produção via promoção manual da mesma digest SHA (alias `:production`), sem rebuild.
|
||||
- Backup PostgreSQL diário (retenção ≥14 dias), restore documentado e rollback por SHA.
|
||||
- Compose local com app FrankenPHP + Postgres (pendente fora da fatia deploy).
|
||||
- PHP 8.4 canônico em docs/Docker/CI (pendente).
|
||||
- Gates com `npm audit` e cobertura Domain/Application ≥ 80% (pendente).
|
||||
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1) (pendente).
|
||||
- Strict types em PHP próprio faltante (pendente).
|
||||
- Compose local com app FrankenPHP + Postgres.
|
||||
- PHP 8.4 canônico em docs/Docker/CI.
|
||||
- Gates com `npm audit` e cobertura Domain/Application ≥ 80%.
|
||||
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1).
|
||||
- Strict types em PHP próprio faltante.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Deploy automático em produção; provisionamento de VPS para clientes.
|
||||
- Produção, promoção humana, provisionamento de VPS para clientes.
|
||||
- Fase 2 (briefing/CRM/E2E-01/02).
|
||||
- Redis, worker mode, CDN automation, signed private media.
|
||||
|
||||
@@ -33,13 +31,11 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
||||
|
||||
### D1 — Dokploy Compose na VPS, imagem imutável no GHCR
|
||||
|
||||
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` no Dokploy com `x-api-key`.
|
||||
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` (ou `compose.update` + `compose.deploy`) no Dokploy com `x-api-key`.
|
||||
|
||||
Compose compartilhado (`docker-compose.deploy.yml`) referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). Dokploy mantém **duas** stacks Compose (staging e production) com `IMAGE_TAG` distinto. PostgreSQL é serviço Dokploy separado por ambiente (não na imagem da app).
|
||||
Compose de staging referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). PostgreSQL é serviço Dokploy separado (não na imagem da app).
|
||||
|
||||
Produção: `workflow_dispatch` com SHA + confirmação `PRODUCTION` move alias `:production` para o **mesmo digest** já publicado e dispara `compose.deploy` na stack de produção. Repo privado no GitHub Free não tem required reviewers de Environment; aprovação humana = disparo manual explícito (GitHub Pro opcional depois).
|
||||
|
||||
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil); deploy automático direto em produção.
|
||||
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil).
|
||||
|
||||
### D2 — Processos e health
|
||||
|
||||
@@ -54,11 +50,7 @@ Healthcheck Docker/Dokploy e smoke pós-deploy usam `GET /up` (sem auth, sem sec
|
||||
|
||||
### D3 — Secrets e providers
|
||||
|
||||
GitHub Actions guarda só orquestração: `DOKPLOY_URL`, API key, compose IDs, URLs públicas de smoke. Env Laravel (`APP_KEY`, DB, Resend, R2) vive **somente** no Dokploy, isolado por ambiente. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
||||
|
||||
### D3b — Backup e restore
|
||||
|
||||
PostgreSQL staging/produção: backup diário via Dokploy → destino S3-compatible, retenção mínima 14 dias (SPEC §16.3). Restore documentado e testado em staging antes da primeira promoção a produção.
|
||||
Secrets em GitHub Actions + env Dokploy: `APP_KEY`, DB, `MAIL_MAILER=resend` / `RESEND_API_KEY`, `FILESYSTEM_DISK=r2` / `R2_*`, tokens Dokploy/GHCR. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
||||
|
||||
### D4 — Compose local com app
|
||||
|
||||
@@ -82,32 +74,26 @@ Alinhar README, CI (`setup-php` 8.4) e `Dockerfile` `ARG PHP_VERSION=8.4`. Mante
|
||||
|
||||
### D8 — Rollback
|
||||
|
||||
Rollback = mover alias do ambiente (`:staging` ou `:production`) para tag SHA anterior no GHCR e `compose.deploy`. Falha de healthcheck/smoke impede promoção. Sem rebuild.
|
||||
|
||||
### D9 — Trusted proxies atrás do Traefik
|
||||
|
||||
`bootstrap/app.php` confia em proxies (`trustProxies(at: '*')`) para honrar `X-Forwarded-*` do Traefik/Dokploy. Staging/produção usam `SESSION_SECURE_COOKIE=true` com HTTPS.
|
||||
Rollback = apontar Compose staging para tag SHA anterior no GHCR e `compose.deploy`/`redeploy`. Falha de healthcheck impede promoção. Sem rebuild.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[GHCR privado + pull na VPS]** → configurar registry no Dokploy com PAT `read:packages`; documentar checklist.
|
||||
- **[Migrate one-shot falha]** → web/queue/scheduler dependem de migrate exit 0; manter migrations backward-compatible.
|
||||
- **[GitHub Free sem required reviewers]** → promoção humana via `workflow_dispatch` + input `PRODUCTION`; Pro opcional.
|
||||
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain (pendente).
|
||||
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário (pendente).
|
||||
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado (pendente).
|
||||
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP (pendente).
|
||||
- **[Migrate one-shot falha]** → deploy não promove web; manter migrations backward-compatible.
|
||||
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain.
|
||||
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário; sem silenciar sem justificativa.
|
||||
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado.
|
||||
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Fatia deploy: Compose deploy, workflows, smoke, trusted proxies, docs Dokploy/backup/rollback; CI verde.
|
||||
2. Criar projeto Dokploy + Postgres (staging + produção) + Compose apps; registrar GHCR.
|
||||
3. Primeiro push de imagem SHA → staging; smoke `/up` + home + login; testar rollback e backup/restore.
|
||||
4. Promoção manual para produção após domínio/TLS/`APP_URL` confirmados.
|
||||
5. Fatias restantes da change (auth/coverage/npm/Compose local/PHP docs) em PRs seguintes.
|
||||
6. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
||||
1. Implementar auth/coverage/npm/strict_types/docs/Compose local; CI verde.
|
||||
2. Criar projeto Dokploy + Postgres + Compose app; registrar GHCR.
|
||||
3. Adicionar workflow deploy; primeiro push de imagem SHA; smoke `/up` + home + login.
|
||||
4. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
||||
5. Rollback: redeploy tag SHA anterior.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Domínio público exato do staging/produção (DNS) — preencher na implementação com valor do operador.
|
||||
- Se Dokploy Compose API exigir `compose.update` env para `IMAGE_TAG` a cada deploy: preferir aliases `:staging`/`:production` estáveis no Compose Dokploy para evitar rewrite de env.
|
||||
- Domínio público exato do staging (DNS) — preencher na implementação com valor do operador.
|
||||
- Se Dokploy Compose API exigir `compose.saveEnvironment` para `IMAGE_TAG` a cada deploy: confirmar payload na primeira fatia de integração.
|
||||
|
||||
@@ -6,20 +6,17 @@ Fases 0 e 1 estão implementadas e mescladas, mas o critério de saída da Fase
|
||||
|
||||
- Implantar staging na VPS própria via Dokploy (Docker Compose): imagem única por SHA no GHCR, serviços `web`/`queue`/`scheduler`/migrate, PostgreSQL gerenciado, healthcheck `/up`, smoke pós-deploy e rollback por tag SHA anterior.
|
||||
- Adicionar workflow GitHub Actions de deploy em `main` após CI verde (build → push GHCR → acionar API Dokploy).
|
||||
- Adicionar promoção manual de produção: mesma digest SHA já publicada, alias `:production`, sem rebuild (`workflow_dispatch` + confirmação explícita).
|
||||
- Documentar backup PostgreSQL diário (retenção ≥14d), restore e runbook operacional Dokploy/GHCR.
|
||||
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL) — **ainda pendente** nesta fatia de deploy.
|
||||
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação — **ainda pendente**.
|
||||
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9) — **ainda pendente**.
|
||||
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01) — **ainda pendente**.
|
||||
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões — **ainda pendente**.
|
||||
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL).
|
||||
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação.
|
||||
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9).
|
||||
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01).
|
||||
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Conforme [SPEC.md §4.2](../../SPEC.md):
|
||||
|
||||
- Deploy automático direto em produção (produção exige promoção humana da mesma imagem).
|
||||
- Portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
||||
- Produção com promoção humana, portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
||||
- Fase 2 (WEB-05 briefing, CRM, E2E-01/E2E-02) — change futura `build-leads-crm` após esta fechar.
|
||||
- Provisionamento genérico de VPS/Dokploy para clientes finais.
|
||||
- Templates de e-mail de lead, auditoria completa (ADM-02), documentos privados.
|
||||
@@ -28,20 +25,19 @@ Conforme [SPEC.md §4.2](../../SPEC.md):
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback; promoção manual da mesma digest para produção (SPEC §14.3, §15.2, §16.3, §18 Fase 0).
|
||||
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback (SPEC §14.3, §15.2, §18 Fase 0).
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging/produção.
|
||||
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging.
|
||||
- `quality-gates`: `npm audit` no gate; cobertura mínima 80% para Domain/Application; job de deploy staging após CI.
|
||||
- `health-check`: healthcheck e smoke pós-deploy de staging usam `/up` sem autenticação.
|
||||
- `internal-authentication`: e-mail verificado obrigatório para acesso ao painel; reset de senha seguro disponível (SPEC §12.1, ADM-01).
|
||||
|
||||
## Impact
|
||||
|
||||
- **Cria (fatia deploy)**: `docker-compose.deploy.yml`, workflows `deploy-staging.yml` / `promote-production.yml`, scripts smoke/Dokploy, docs operacionais Dokploy/GHCR/backup/rollback.
|
||||
- **Altera (fatia deploy)**: `bootstrap/app.php` (trusted proxies), `.env.example`, README.
|
||||
- **Ainda pendente nesta change**: Compose local FrankenPHP, PHP 8.4 docs, npm audit/coverage, auth verification/reset, strict_types.
|
||||
- **Infra (manual)**: projeto Dokploy na VPS (duas stacks), registry GHCR, secrets (`DOKPLOY_*`, DB, `APP_KEY`, Resend/R2), backup S3.
|
||||
- **Cria**: `docker-compose` de staging (ou extensão), workflow `.github/workflows/deploy-staging.yml`, docs operacionais de Dokploy/GHCR, testes de auth verification/reset e cobertura.
|
||||
- **Altera**: `docker-compose.yml`, `Dockerfile`/docs PHP, `composer.json`/`package.json` scripts, `.github/workflows/ci.yml`, `User`/`AdminPanelProvider`, README, `.env.example`.
|
||||
- **Infra (manual)**: projeto Dokploy na VPS, registry GHCR, secrets (`DOKPLOY_*`, `GHCR_*`, DB, `APP_KEY`, Resend/R2).
|
||||
- **Depende de**: specs já arquivadas (`container-runtime`, `quality-gates`, `health-check`, `internal-authentication`, `transactional-email`, `object-storage`).
|
||||
- **Risco**: secrets e registry privados; mitigações: tokens com escopo mínimo, imagem por SHA, healthcheck antes de promover, rollback por tag anterior.
|
||||
|
||||
@@ -55,23 +55,3 @@ Rollback SHALL redeploy a previously published SHA-tagged image without rebuildi
|
||||
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
||||
- **THEN** web, queue, and scheduler MUST run that previous image
|
||||
- **AND** no source rebuild MUST be required
|
||||
|
||||
### Requirement: Production promotion reuses the same immutable digest
|
||||
|
||||
Production SHALL be promoted from an already-published SHA-tagged image without rebuilding from source. Promotion MUST require explicit human action (SPEC §14.3).
|
||||
|
||||
#### Scenario: Operator promotes a staging-approved SHA to production
|
||||
|
||||
- **WHEN** the operator confirms promotion of commit SHA `abc123`
|
||||
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `ghcr.io/<owner>/<repo>:abc123`
|
||||
- **AND** MUST NOT rebuild from source for that promotion
|
||||
|
||||
### Requirement: Database backups exist before production cutover
|
||||
|
||||
Staging and production PostgreSQL services SHALL have automated daily backups with retention of at least 14 days, and a documented restore procedure MUST be verified on staging before the first production promotion (SPEC §16.3).
|
||||
|
||||
#### Scenario: Staging restore is proven before production promotion
|
||||
|
||||
- **WHEN** the operator prepares the first production promotion
|
||||
- **THEN** a restore from a staging backup MUST have been documented and successfully tested
|
||||
- **AND** production MUST have daily backup configured with retention of at least 14 days
|
||||
|
||||
@@ -19,25 +19,23 @@
|
||||
- [ ] 3.3 Add/adjust unit tests if current Domain/Application coverage is below threshold
|
||||
- [ ] 3.4 Verify CI `static` and `unit` fail appropriately on intentional audit/coverage breakage in a branch experiment or equivalent proof
|
||||
|
||||
## 4. Staging/production Compose and Dokploy prep
|
||||
## 4. Staging Compose and Dokploy prep
|
||||
|
||||
- [x] 4.1 Add versioned Compose template (`docker-compose.deploy.yml`: web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG` for staging and production stacks
|
||||
- [x] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres per environment, Compose import, required env vars (APP_KEY, DB, Resend, R2), trusted proxies/session cookies
|
||||
- [x] 4.3 Document rollback procedure: move environment alias to previous SHA and redeploy without rebuild
|
||||
- [x] 4.4 Document PostgreSQL daily backup (≥14d retention), restore procedure, and test restore on staging before first production promotion
|
||||
- [ ] 4.1 Add versioned staging Compose template (web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG`
|
||||
- [ ] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres service, Compose import, required env vars (APP_KEY, DB, Resend, R2)
|
||||
- [ ] 4.3 Document rollback procedure: redeploy previous SHA tag without rebuild
|
||||
|
||||
## 5. Deploy workflow and smoke
|
||||
|
||||
- [x] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
||||
- [x] 5.2 Create `.github/workflows/promote-production.yml` (`workflow_dispatch` + confirmation): retag same digest as `:production`, deploy production stack, smoke
|
||||
- [x] 5.3 Wire migrate-before-serve (Compose migrate service) and healthcheck on `/up`
|
||||
- [x] 5.4 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
||||
- [x] 5.5 Store orchestration secrets only in GitHub; Laravel/DB/R2/Resend only in Dokploy; ensure no secrets in image layers
|
||||
- [ ] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
||||
- [ ] 5.2 Wire migrate-before-serve (Compose migrate service or Dokploy deploy command) and healthcheck on `/up`
|
||||
- [ ] 5.3 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
||||
- [ ] 5.4 Store secrets only in GitHub/Dokploy; ensure no secrets in image layers (reuse container CI check)
|
||||
|
||||
## 6. Phase 0 exit evidence
|
||||
|
||||
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
||||
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
||||
- [ ] 6.2 Verify rollback to previous SHA works once
|
||||
- [ ] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any
|
||||
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
||||
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks (1–3) also complete
|
||||
- [ ] 6.5 Report in SPEC §24 format and mark this change ready to archive after merge
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Trigger a Dokploy Compose deploy and wait until it finishes.
|
||||
# Required env:
|
||||
# DOKPLOY_URL e.g. https://panel.example.com (panel origin, no /api)
|
||||
# DOKPLOY_API_KEY x-api-key value
|
||||
# DOKPLOY_COMPOSE_ID target compose id
|
||||
# Optional env:
|
||||
# DEPLOY_TITLE deployment title (default: GitHub deploy)
|
||||
# DEPLOY_TIMEOUT_SEC total wait seconds (default: 900)
|
||||
# DEPLOY_POLL_SEC poll interval (default: 10)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${DOKPLOY_URL:?DOKPLOY_URL is required}"
|
||||
: "${DOKPLOY_API_KEY:?DOKPLOY_API_KEY is required}"
|
||||
: "${DOKPLOY_COMPOSE_ID:?DOKPLOY_COMPOSE_ID is required}"
|
||||
|
||||
# Panel origin only: strip trailing slash and accidental OpenAPI /api suffix.
|
||||
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
||||
DOKPLOY_URL="${DOKPLOY_URL%/api}"
|
||||
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
||||
|
||||
DEPLOY_TITLE="${DEPLOY_TITLE:-GitHub deploy}"
|
||||
DEPLOY_TIMEOUT_SEC="${DEPLOY_TIMEOUT_SEC:-900}"
|
||||
DEPLOY_POLL_SEC="${DEPLOY_POLL_SEC:-10}"
|
||||
|
||||
echo "Dokploy API base: ${DOKPLOY_URL}/api"
|
||||
|
||||
api() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
shift 2
|
||||
local url="${DOKPLOY_URL}/api${path}"
|
||||
local response http_code body
|
||||
|
||||
# Body then HTTP status on the last line (no -f so 4xx/5xx still return a body).
|
||||
response="$(
|
||||
curl -sS -w $'\n%{http_code}' -X "$method" \
|
||||
-H "accept: application/json" \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-api-key: ${DOKPLOY_API_KEY}" \
|
||||
"$url" \
|
||||
"$@"
|
||||
)" || {
|
||||
echo "Dokploy API request failed (curl transport error)." >&2
|
||||
echo " method: ${method}" >&2
|
||||
echo " url: ${url}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
http_code="$(printf '%s' "$response" | tail -n1)"
|
||||
body="$(printf '%s' "$response" | sed '$d')"
|
||||
|
||||
if [[ ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "Dokploy API error." >&2
|
||||
echo " method: ${method}" >&2
|
||||
echo " url: ${url}" >&2
|
||||
echo " status: ${http_code}" >&2
|
||||
echo " body:" >&2
|
||||
printf '%s\n' "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$body"
|
||||
}
|
||||
|
||||
echo "Triggering Dokploy compose.deploy for ${DOKPLOY_COMPOSE_ID}..."
|
||||
api POST "/compose.deploy" \
|
||||
-d "$(jq -n \
|
||||
--arg composeId "$DOKPLOY_COMPOSE_ID" \
|
||||
--arg title "$DEPLOY_TITLE" \
|
||||
'{composeId: $composeId, title: $title}')" >/dev/null
|
||||
|
||||
echo "Waiting for deployment to finish (timeout ${DEPLOY_TIMEOUT_SEC}s)..."
|
||||
deadline=$((SECONDS + DEPLOY_TIMEOUT_SEC))
|
||||
last_status=""
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
payload="$(api GET "/deployment.allByCompose?composeId=${DOKPLOY_COMPOSE_ID}")"
|
||||
latest="$(echo "$payload" | jq -c 'if type=="array" then .[0] else . end')"
|
||||
|
||||
if [[ -z "$latest" || "$latest" == "null" ]]; then
|
||||
echo "No deployment records yet; retrying..."
|
||||
sleep "$DEPLOY_POLL_SEC"
|
||||
continue
|
||||
fi
|
||||
|
||||
status="$(echo "$latest" | jq -r '.status // .deploymentStatus // empty')"
|
||||
created_at="$(echo "$latest" | jq -r '.createdAt // .created_at // empty')"
|
||||
title="$(echo "$latest" | jq -r '.title // .titleLog // empty')"
|
||||
|
||||
if [[ "$status" != "$last_status" ]]; then
|
||||
echo "Deployment status=${status:-unknown} title=${title:-n/a} createdAt=${created_at:-n/a}"
|
||||
last_status="$status"
|
||||
fi
|
||||
|
||||
case "${status,,}" in
|
||||
done|success|successful|finished)
|
||||
echo "Dokploy deployment succeeded."
|
||||
exit 0
|
||||
;;
|
||||
error|failed|failure)
|
||||
echo "Dokploy deployment failed." >&2
|
||||
echo "$latest" | jq . >&2 || true
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sleep "$DEPLOY_POLL_SEC"
|
||||
done
|
||||
|
||||
echo "Timed out waiting for Dokploy deployment after ${DEPLOY_TIMEOUT_SEC}s." >&2
|
||||
api GET "/deployment.allByCompose?composeId=${DOKPLOY_COMPOSE_ID}" | jq . >&2 || true
|
||||
exit 1
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Post-deploy HTTP smoke checks for Amare.
|
||||
# Required env:
|
||||
# SMOKE_BASE_URL e.g. https://staging.example.com
|
||||
# Optional env:
|
||||
# SMOKE_RETRIES attempts per path (default: 30)
|
||||
# SMOKE_SLEEP_SEC sleep between attempts (default: 5)
|
||||
# SMOKE_PATHS space-separated paths (default: /up / /admin/login)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${SMOKE_BASE_URL:?SMOKE_BASE_URL is required}"
|
||||
|
||||
SMOKE_BASE_URL="${SMOKE_BASE_URL%/}"
|
||||
SMOKE_RETRIES="${SMOKE_RETRIES:-30}"
|
||||
SMOKE_SLEEP_SEC="${SMOKE_SLEEP_SEC:-5}"
|
||||
SMOKE_PATHS="${SMOKE_PATHS:-/up / /admin/login}"
|
||||
|
||||
check_path() {
|
||||
local path="$1"
|
||||
local url="${SMOKE_BASE_URL}${path}"
|
||||
local attempt
|
||||
local code
|
||||
|
||||
for ((attempt = 1; attempt <= SMOKE_RETRIES; attempt++)); do
|
||||
code="$(curl -sS -o /tmp/amare-smoke-body -w '%{http_code}' --max-time 20 "$url" 2>/dev/null || true)"
|
||||
if [[ "$code" == "200" ]]; then
|
||||
echo "OK ${path} (HTTP ${code}) attempt=${attempt}"
|
||||
return 0
|
||||
fi
|
||||
echo "WAIT ${path} (HTTP ${code:-000}) attempt=${attempt}/${SMOKE_RETRIES}"
|
||||
sleep "$SMOKE_SLEEP_SEC"
|
||||
done
|
||||
|
||||
echo "FAIL ${path} after ${SMOKE_RETRIES} attempts" >&2
|
||||
if [[ -f /tmp/amare-smoke-body ]]; then
|
||||
head -c 500 /tmp/amare-smoke-body >&2 || true
|
||||
echo >&2
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "Smoke against ${SMOKE_BASE_URL}"
|
||||
failed=0
|
||||
for path in $SMOKE_PATHS; do
|
||||
if ! check_path "$path"; then
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if (( failed != 0 )); then
|
||||
echo "Smoke checks failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Smoke checks passed."
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Livewire\Features\SupportFileUploads\FileUploadConfiguration;
|
||||
|
||||
it('keeps livewire temporary uploads on the local disk when the default filesystem is r2', function (): void {
|
||||
config(['filesystems.default' => 'r2']);
|
||||
|
||||
expect(config('livewire.temporary_file_upload.disk'))->toBe('local')
|
||||
->and(FileUploadConfiguration::disk())->toBe('local')
|
||||
->and(FileUploadConfiguration::isUsingS3())->toBeFalse();
|
||||
});
|
||||
@@ -9,7 +9,6 @@ use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\ContentSeeder;
|
||||
use Database\Seeders\TestimonialsSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
@@ -19,59 +18,6 @@ class TestimonialsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @var list<array{
|
||||
* quote: string,
|
||||
* author_name: string,
|
||||
* context: string,
|
||||
* sort_order: int,
|
||||
* is_featured: bool,
|
||||
* published_at: string
|
||||
* }>
|
||||
*/
|
||||
private const EXPECTED_TESTIMONIALS = [
|
||||
[
|
||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Casamento · 06/12/2025',
|
||||
'sort_order' => 1,
|
||||
'is_featured' => true,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||
'author_name' => 'Quesia e Jhonata',
|
||||
'context' => 'Casamento · 21/12/2025',
|
||||
'sort_order' => 2,
|
||||
'is_featured' => true,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||
'author_name' => 'Milena e Weslley',
|
||||
'context' => 'Casamento · 13/02/2026',
|
||||
'sort_order' => 3,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||
'author_name' => 'Raquel e Pedro',
|
||||
'context' => 'Casamento · 09/05/2026',
|
||||
'sort_order' => 4,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
[
|
||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||
'author_name' => 'Victoria e Pedro',
|
||||
'context' => 'Casamento · 24/06/2026',
|
||||
'sort_order' => 5,
|
||||
'is_featured' => false,
|
||||
'published_at' => '2026-08-05 00:00:00',
|
||||
],
|
||||
];
|
||||
|
||||
public function test_published_scope_excludes_unpublished_testimonials(): void
|
||||
{
|
||||
Testimonial::factory()->create(['published_at' => null]);
|
||||
@@ -102,27 +48,11 @@ class TestimonialsTest extends TestCase
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_maps_all_five_real_couples_from_depoimentos(): void
|
||||
{
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$testimonials = Testimonial::query()
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $this->sourceOwnedState($testimonial))
|
||||
->all();
|
||||
|
||||
$this->assertSame(self::EXPECTED_TESTIMONIALS, $testimonials);
|
||||
}
|
||||
|
||||
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$this->artisan('db:seed', [
|
||||
'--class' => ContentSeeder::class,
|
||||
'--no-interaction' => true,
|
||||
])->assertExitCode(0);
|
||||
$this->seed(ContentSeeder::class);
|
||||
|
||||
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
||||
|
||||
@@ -134,6 +64,11 @@ class TestimonialsTest extends TestCase
|
||||
'Victoria e Pedro',
|
||||
], $authors);
|
||||
|
||||
$this->assertNull(
|
||||
Testimonial::query()->where('author_name', 'Ana Souza')->first(),
|
||||
'Fictional demo authors must not remain after seeding real testimonials.',
|
||||
);
|
||||
|
||||
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
||||
|
||||
$this->assertNotNull($jeniffer);
|
||||
@@ -142,87 +77,6 @@ class TestimonialsTest extends TestCase
|
||||
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_authoritatively_overwrites_source_owned_fields(): void
|
||||
{
|
||||
$testimonial = Testimonial::factory()->create([
|
||||
'quote' => 'Depoimento desatualizado.',
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'context' => 'Contexto desatualizado',
|
||||
'sort_order' => 99,
|
||||
'is_featured' => false,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$this->assertSame(
|
||||
self::EXPECTED_TESTIMONIALS[0],
|
||||
$this->sourceOwnedState($testimonial->refresh()),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_preserves_curated_photo_fields_for_known_author(): void
|
||||
{
|
||||
$testimonial = Testimonial::factory()->create([
|
||||
'author_name' => 'Jeniffer e Maick',
|
||||
'photo_path' => 'testimonials/jeniffer-e-maick.jpg',
|
||||
'photo_alt' => 'Jeniffer e Maick durante o casamento',
|
||||
]);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$testimonial->refresh();
|
||||
|
||||
$this->assertSame('testimonials/jeniffer-e-maick.jpg', $testimonial->photo_path);
|
||||
$this->assertSame('Jeniffer e Maick durante o casamento', $testimonial->photo_alt);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_leaves_unrelated_testimonials_unchanged(): void
|
||||
{
|
||||
$unrelated = Testimonial::factory()->published()->featured()->create([
|
||||
'quote' => 'Depoimento cadastrado pela equipe.',
|
||||
'author_name' => 'Casal não presente na fonte',
|
||||
'context' => 'Bodas · 02/08/2026',
|
||||
'photo_path' => 'testimonials/casal-equipe.jpg',
|
||||
'photo_alt' => 'Casal cadastrado pela equipe',
|
||||
'sort_order' => 42,
|
||||
]);
|
||||
$originalState = $unrelated->getAttributes();
|
||||
ksort($originalState);
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$persisted = $unrelated->fresh();
|
||||
|
||||
$this->assertNotNull($persisted);
|
||||
$persistedState = $persisted->getAttributes();
|
||||
ksort($persistedState);
|
||||
|
||||
$this->assertSame($originalState, $persistedState);
|
||||
}
|
||||
|
||||
public function test_testimonials_seeder_is_idempotent_without_duplicate_authors(): void
|
||||
{
|
||||
$this->runTestimonialsSeeder();
|
||||
$firstRunState = Testimonial::query()
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||
->all();
|
||||
|
||||
$this->runTestimonialsSeeder();
|
||||
|
||||
$secondRunState = Testimonial::query()
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||
->all();
|
||||
|
||||
$this->assertCount(5, $secondRunState);
|
||||
$this->assertSame($firstRunState, $secondRunState);
|
||||
$this->assertSame(5, Testimonial::query()->distinct()->count('author_name'));
|
||||
}
|
||||
|
||||
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
@@ -243,34 +97,4 @@ class TestimonialsTest extends TestCase
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private function runTestimonialsSeeder(): void
|
||||
{
|
||||
$this->artisan('db:seed', [
|
||||
'--class' => TestimonialsSeeder::class,
|
||||
'--no-interaction' => true,
|
||||
])->assertExitCode(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* quote: string,
|
||||
* author_name: string,
|
||||
* context: string,
|
||||
* sort_order: int,
|
||||
* is_featured: bool,
|
||||
* published_at: string
|
||||
* }
|
||||
*/
|
||||
private function sourceOwnedState(Testimonial $testimonial): array
|
||||
{
|
||||
return [
|
||||
'quote' => (string) $testimonial->quote,
|
||||
'author_name' => (string) $testimonial->author_name,
|
||||
'context' => (string) $testimonial->context,
|
||||
'sort_order' => (int) $testimonial->sort_order,
|
||||
'is_featured' => (bool) $testimonial->is_featured,
|
||||
'published_at' => $testimonial->published_at?->format('Y-m-d H:i:s') ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class TrustedProxyTest extends TestCase
|
||||
{
|
||||
public function test_forwarded_proto_https_is_recognized_behind_proxy(): void
|
||||
{
|
||||
$response = $this->get('/up', [
|
||||
'HTTP_X_FORWARDED_PROTO' => 'https',
|
||||
'HTTP_X_FORWARDED_FOR' => '203.0.113.10',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertTrue(request()->secure());
|
||||
$this->assertSame('203.0.113.10', request()->ip());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user