diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml new file mode 100644 index 000000000..4d3ab1948 --- /dev/null +++ b/.github/workflows/uffizzi-build.yml @@ -0,0 +1,146 @@ +name: Build PR Image +on: + pull_request: + types: [opened,synchronize,reopened,closed] + +jobs: + + build-appication: + name: Build and Push `Appication` + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} + outputs: + tags: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Generate UUID image name + id: uuid + run: echo "::set-output name=uuid::$(uuidgen)" + - name: Docker metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: registry.uffizzi.com/${{ steps.uuid.outputs.uuid }} + tags: type=raw,value=60d + - name: Build and Push Image to registry.uffizzi.com ephemeral registry + uses: docker/build-push-action@v2 + with: + push: true + context: ./ + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./uffizzi/Dockerfile + + build-nginx: + name: Build and Push `nginx` + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} + outputs: + tags: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Generate UUID image name + id: uuid + run: echo "::set-output name=uuid::$(uuidgen)" + - name: Docker metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: registry.uffizzi.com/${{ steps.uuid.outputs.uuid }} + tags: type=raw,value=60d + - name: Build and Push Image to Uffizzi ephemeral registry + uses: docker/build-push-action@v2 + with: + push: true + context: ./ + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./uffizzi/nginx/Dockerfile + + build-crond: + name: Build and Push `crond` + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} + outputs: + tags: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Generate UUID image name + id: uuid + run: echo "::set-output name=uuid::$(uuidgen)" + - name: Docker metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: registry.uffizzi.com/${{ steps.uuid.outputs.uuid }} + tags: type=raw,value=60d + - name: Build and Push Image to registry.uffizzi.com ephemeral registry + uses: docker/build-push-action@v2 + with: + push: true + context: ./ + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./uffizzi/crond/Dockerfile + + render-compose-file: + name: Render Docker Compose File + # Pass output of this workflow to another triggered by `workflow_run` event. + runs-on: ubuntu-latest + outputs: + compose-file-cache-key: ${{ steps.hash.outputs.hash }} + needs: + - build-appication + - build-nginx + - build-crond + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Render Compose File + run: | + APP_IMAGE=$(echo ${{ needs.build-appication.outputs.tags }}) + export APP_IMAGE + NGINX_IMAGE=$(echo ${{ needs.build-nginx.outputs.tags }}) + export NGINX_IMAGE + CROND_IMAGE=$(echo ${{ needs.build-crond.outputs.tags }}) + export CROND_IMAGE + # Render simple template from environment variables. + envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml + cat docker-compose.rendered.yml + - name: Upload Rendered Compose File as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: docker-compose.rendered.yml + retention-days: 2 + - name: Serialize PR Event to File + run: | + cat << EOF > event.json + ${{ toJSON(github.event) }} + + EOF + - name: Upload PR Event as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: event.json + retention-days: 2 + + delete-preview: + name: Call for Preview Deletion + runs-on: ubuntu-latest + if: ${{ github.event.action == 'closed' }} + steps: + # If this PR is closing, we will not render a compose file nor pass it to the next workflow. + - name: Serialize PR Event to File + run: echo '${{ toJSON(github.event) }}' > event.json + - name: Upload PR Event as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: event.json + retention-days: 2 + diff --git a/.github/workflows/uffizzi-preview.yml b/.github/workflows/uffizzi-preview.yml new file mode 100644 index 000000000..0dad49586 --- /dev/null +++ b/.github/workflows/uffizzi-preview.yml @@ -0,0 +1,86 @@ +name: Deploy Uffizzi Preview + +on: + workflow_run: + workflows: + - "Build PR Image" + types: + - completed + + +jobs: + cache-compose-file: + name: Cache Compose File + runs-on: ubuntu-latest + outputs: + compose-file-cache-key: ${{ steps.hash.outputs.hash }} + pr-number: ${{ steps.pr.outputs.number }} + steps: + - name: 'Download artifacts' + # Fetch output (zip archive) from the workflow run that triggered this workflow. + uses: actions/github-script@v6 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "preview-spec" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data)); + + - name: 'Unzip artifact' + run: unzip preview-spec.zip + - name: Read Event into ENV + run: | + echo 'EVENT_JSON<> $GITHUB_ENV + cat event.json >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + - name: Hash Rendered Compose File + id: hash + # If the previous workflow was triggered by a PR close event, we will not have a compose file artifact. + if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + run: echo "::set-output name=hash::$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" + - name: Cache Rendered Compose File + if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + uses: actions/cache@v3 + with: + path: docker-compose.rendered.yml + key: ${{ steps.hash.outputs.hash }} + + - name: Read PR Number From Event Object + id: pr + run: echo "::set-output name=number::${{ fromJSON(env.EVENT_JSON).number }}" + - name: DEBUG - Print Job Outputs + if: ${{ runner.debug }} + run: | + echo "PR number: ${{ steps.pr.outputs.number }}" + echo "Compose file hash: ${{ steps.hash.outputs.hash }}" + cat event.json + + deploy-uffizzi-preview: + name: Use Remote Workflow to Preview on Uffizzi + needs: + - cache-compose-file + uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2 + with: + # If this workflow was triggered by a PR close event, cache-key will be an empty string + # and this reusable workflow will delete the preview deployment. + compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }} + compose-file-cache-path: docker-compose.rendered.yml + server: https://app.uffizzi.com/ + pr-number: ${{ needs.cache-compose-file.outputs.pr-number }} + permissions: + contents: read + pull-requests: write + id-token: write diff --git a/uffizzi/.env.example b/uffizzi/.env.example new file mode 100644 index 000000000..3b52a3dba --- /dev/null +++ b/uffizzi/.env.example @@ -0,0 +1,40 @@ +APP_ENV=production +APP_KEY=base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0= +APP_DEBUG=true +APP_LOG_LEVEL=debug +APP_URL=http://crater.test + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=crater +DB_USERNAME=crater +DB_PASSWORD=crater + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_DRIVER=sync +SESSION_DRIVER=cookie +SESSION_LIFETIME=1440 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST= +MAIL_PORT= +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION= + +PUSHER_APP_ID= +PUSHER_KEY= +PUSHER_SECRET= + +SANCTUM_STATEFUL_DOMAINS=crater.test +SESSION_DOMAIN=crater.test + +TRUSTED_PROXIES="*" + +CRON_JOB_AUTH_TOKEN="" diff --git a/uffizzi/Dockerfile b/uffizzi/Dockerfile new file mode 100644 index 000000000..de0291625 --- /dev/null +++ b/uffizzi/Dockerfile @@ -0,0 +1,46 @@ +FROM php:7.4-fpm + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + zip \ + unzip \ + libzip-dev \ + libmagickwand-dev \ + mariadb-client + +# Clear cache +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN pecl install imagick \ + && docker-php-ext-enable imagick + +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd + +# Get latest Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Create system user to run Composer and Artisan Commands +RUN useradd -G www-data,root -u 1000 -d /home/crater-user crater-user +RUN mkdir -p /home/crater-user/.composer && \ + chown -R crater-user:crater-user /home/crater-user + +# Mounted volumes +COPY ./ /var/www +COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini +COPY ./uffizzi/.env.example /var/www/.env + +# Set working directory +WORKDIR /var/www + +RUN chown -R crater-user:crater-user ./ +RUN chmod -R 775 composer.json composer.lock \ + composer.lock storage/framework/ \ + storage/logs/ bootstrap/cache/ /home/crater-user/.composer + +USER crater-user diff --git a/uffizzi/crond/Dockerfile b/uffizzi/crond/Dockerfile new file mode 100644 index 000000000..a6d517dae --- /dev/null +++ b/uffizzi/crond/Dockerfile @@ -0,0 +1,66 @@ +FROM php:7.4-fpm as build + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + zip \ + unzip \ + libzip-dev \ + libmagickwand-dev \ + mariadb-client + +# Clear cache +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN pecl install imagick \ + && docker-php-ext-enable imagick + +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd + +# Get latest Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Create system user to run Composer and Artisan Commands +RUN useradd -G www-data,root -u 1000 -d /home/crater-user crater-user +RUN mkdir -p /home/crater-user/.composer && \ + chown -R crater-user:crater-user /home/crater-user + +# Mounted volumes +COPY ./ /var/www +COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini +COPY ./uffizzi/.env.example /var/www/.env + +# Set working directory +WORKDIR /var/www + +RUN chown -R crater-user:crater-user ./ +RUN chmod -R 775 composer.json composer.lock \ + composer.lock storage/framework/ \ + storage/logs/ bootstrap/cache/ /home/crater-user/.composer + +RUN composer config --no-plugins allow-plugins.pestphp/pest-plugin true && \ + composer install --no-interaction --prefer-dist --optimize-autoloader && \ + php artisan storage:link || true && \ + php artisan key:generate + +FROM php:8.0-fpm-alpine + +RUN apk add --no-cache \ + php8-bcmath + +RUN docker-php-ext-install pdo pdo_mysql bcmath + +COPY docker-compose/crontab /etc/crontabs/root + +# Mounted volumes +COPY --from=build /var/www /var/www + +RUN chown -R $(whoami):$(whoami) /var/www/ +RUN chmod -R 775 /var/www/ + +CMD ["crond", "-f"] diff --git a/uffizzi/docker-compose.uffizzi.yml b/uffizzi/docker-compose.uffizzi.yml new file mode 100644 index 000000000..f035ff6df --- /dev/null +++ b/uffizzi/docker-compose.uffizzi.yml @@ -0,0 +1,57 @@ +version: '3' + +x-uffizzi: + ingress: + service: nginx + port: 80 + +services: + app: + image: "${APP_IMAGE}" + restart: unless-stopped + working_dir: /var/www/ + command: ["-c"," + composer config --no-plugins allow-plugins.pestphp/pest-plugin true && + composer install --no-interaction --prefer-dist --optimize-autoloader && + php artisan storage:link || true && + php artisan key:generate --force && + php-fpm", + ] + entrypoint: /bin/sh + depends_on: + - db + deploy: + resources: + limits: + memory: 4000m + cpu: 4000m + + db: + image: mariadb + restart: always + environment: + MYSQL_USER: crater + MYSQL_PASSWORD: crater + MYSQL_DATABASE: crater + MYSQL_ROOT_PASSWORD: crater + ports: + - '33006:3306' + deploy: + resources: + limits: + memory: 2000m + cpu: 2000m + + nginx: + image: "${NGINX_IMAGE}" + restart: unless-stopped + ports: + - 80:80 + depends_on: + - app + + cron: + image: "${CROND_IMAGE}" + restart: always + + diff --git a/uffizzi/nginx/Dockerfile b/uffizzi/nginx/Dockerfile new file mode 100644 index 000000000..620de570f --- /dev/null +++ b/uffizzi/nginx/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:1.17-alpine + +RUN rm /etc/nginx/conf.d/default.conf + +COPY ./ /var/www +COPY ./uffizzi/nginx/nginx /etc/nginx/conf.d/ + diff --git a/uffizzi/nginx/nginx/nginx.conf b/uffizzi/nginx/nginx/nginx.conf new file mode 100644 index 000000000..e7650454a --- /dev/null +++ b/uffizzi/nginx/nginx/nginx.conf @@ -0,0 +1,22 @@ +server { + client_max_body_size 64M; + listen 80; + index index.php index.html; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /var/www/public; + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass localhost:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 300; + } + location / { + try_files $uri $uri/ /index.php?$query_string; + gzip_static on; + } +}