diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml new file mode 100644 index 000000000..f437b6ff9 --- /dev/null +++ b/.github/workflows/uffizzi-build.yml @@ -0,0 +1,92 @@ +name: Build PR Image +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +jobs: + build-application: + name: Build and Push `Forem` + 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: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Generate UUID image name + id: uuid + run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV + - name: Docker metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: registry.uffizzi.com/${{ env.UUID_TAG_APP }} + tags: type=raw,value=60d + - uses: actions/checkout@master + - name: Create the .env file + run: | + cp .env.test .env + - 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 + cache-from: type=gha + cache-to: type=gha,mode=max + + render-compose-file: + name: Render Docker Compose File + # Pass output of this workflow to another triggered by `workflow_run` event. + runs-on: ubuntu-latest + needs: + - build-application + outputs: + compose-file-cache-key: ${{ steps.hash.outputs.hash }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Render Compose File + run: | + APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }}) + export APP_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..ce8a9c46f --- /dev/null +++ b/.github/workflows/uffizzi-preview.yml @@ -0,0 +1,84 @@ +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: ${{ env.COMPOSE_FILE_HASH }} + pr-number: ${{ env.PR_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 "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV + - name: Cache Rendered Compose File + if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + uses: actions/cache@v3 + with: + path: docker-compose.rendered.yml + key: ${{ env.COMPOSE_FILE_HASH }} + + - name: Read PR Number From Event Object + id: pr + run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV + + - name: DEBUG - Print Job Outputs + if: ${{ runner.debug }} + run: | + echo "PR number: ${{ env.PR_NUMBER }}" + echo "Compose file hash: ${{ env.COMPOSE_FILE_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.6.1 + 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 \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index a5179585b..bd61957b3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -87,6 +87,9 @@ Rails.application.configure do if (gitpod_workspace_url = ENV.fetch("GITPOD_WORKSPACE_URL", nil)) config.hosts << /.*#{URI.parse(gitpod_workspace_url).host}/ end + if (uffizzi_url = ENV.fetch("UFFIZZI_URL", nil)) + config.hosts << /.*#{URI.parse(uffizzi_url).host}/ + end config.app_domain = ENV.fetch("APP_DOMAIN", "localhost:3000") config.action_mailer.delivery_method = :smtp diff --git a/uffizzi/Dockerfile b/uffizzi/Dockerfile new file mode 100644 index 000000000..bb7f29df4 --- /dev/null +++ b/uffizzi/Dockerfile @@ -0,0 +1,83 @@ +FROM quay.io/forem/ruby:3.0.2 as base + +FROM base as builder + +USER root + +RUN curl -sL https://dl.yarnpkg.com/rpm/yarn.repo -o /etc/yum.repos.d/yarn.repo && \ + dnf install --setopt install_weak_deps=false -y \ + ImageMagick iproute jemalloc less libcurl libcurl-devel \ + libffi-devel libxml2-devel libxslt-devel nodejs pcre-devel \ + postgresql postgresql-devel tzdata yarn && \ + dnf -y clean all && \ + rm -rf /var/cache/yum + +ENV BUNDLER_VERSION=2.2.22 BUNDLE_SILENCE_ROOT_WARNING=true BUNDLE_SILENCE_DEPRECATIONS=true +RUN gem install -N bundler:"${BUNDLER_VERSION}" + +ENV APP_USER=forem APP_UID=1000 APP_GID=1000 APP_HOME=/opt/apps/forem \ + LD_PRELOAD=/usr/lib64/libjemalloc.so.2 +RUN mkdir -p ${APP_HOME} && chown "${APP_UID}":"${APP_GID}" "${APP_HOME}" && \ + groupadd -g "${APP_GID}" "${APP_USER}" && \ + adduser -u "${APP_UID}" -g "${APP_GID}" -d "${APP_HOME}" "${APP_USER}" + +ENV DOCKERIZE_VERSION=v0.6.1 +RUN wget https://github.com/jwilder/dockerize/releases/download/"${DOCKERIZE_VERSION}"/dockerize-linux-amd64-"${DOCKERIZE_VERSION}".tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-"${DOCKERIZE_VERSION}".tar.gz \ + && rm dockerize-linux-amd64-"${DOCKERIZE_VERSION}".tar.gz \ + && chown root:root /usr/local/bin/dockerize + +WORKDIR "${APP_HOME}" + +COPY ./.ruby-version "${APP_HOME}"/ +COPY ./Gemfile ./Gemfile.lock "${APP_HOME}"/ +COPY ./vendor/cache "${APP_HOME}"/vendor/cache + +RUN bundle config --local build.sassc --disable-march-tune-native && \ + BUNDLE_WITHOUT="development:test" bundle install --deployment --jobs 4 --retry 5 && \ + find "${APP_HOME}"/vendor/bundle -name "*.c" -delete && \ + find "${APP_HOME}"/vendor/bundle -name "*.o" -delete + +COPY . "${APP_HOME}" + +RUN mkdir -p "${APP_HOME}"/public/{assets,images,packs,podcasts,uploads} + +RUN NODE_ENV=production yarn install + +RUN RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile + +RUN echo $(date -u +'%Y-%m-%dT%H:%M:%SZ') >> "${APP_HOME}"/FOREM_BUILD_DATE && \ + echo $(git rev-parse --short HEAD) >> "${APP_HOME}"/FOREM_BUILD_SHA && \ + rm -rf "${APP_HOME}"/.git/ + +RUN rm -rf node_modules vendor/assets spec + +## Development +FROM builder AS development + +COPY --chown="${APP_USER}":"${APP_USER}" ./spec "${APP_HOME}"/spec +COPY --from=builder /usr/local/bin/dockerize /usr/local/bin/dockerize + +RUN chown "${APP_USER}":"${APP_USER}" -R "${APP_HOME}" + +USER "${APP_USER}" + +RUN bundle config --local build.sassc --disable-march-tune-native && \ + bundle config --delete without && \ + bundle install --deployment --jobs 4 --retry 5 && \ + find "${APP_HOME}"/vendor/bundle -name "*.c" -delete && \ + find "${APP_HOME}"/vendor/bundle -name "*.o" -delete + +# Replcament for volume +COPY --from=builder --chown="${APP_USER}":"${APP_USER}" ${APP_HOME} ${APP_HOME} +# husky install / fatal: not a git repository (or any of the parent directories): .git +COPY --chown="${APP_USER}":"${APP_USER}" .git/ "${APP_HOME}/.git/" +## Bund install +RUN ./scripts/bundle.sh +## Yarn install +RUN bash -c yarn install --dev + + +ENTRYPOINT ["./scripts/entrypoint.sh"] + +CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "3000"] diff --git a/uffizzi/db_seed.sh b/uffizzi/db_seed.sh new file mode 100755 index 000000000..ac9823159 --- /dev/null +++ b/uffizzi/db_seed.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +bundle exec rake db:seed + +tail -f /dev/null \ No newline at end of file diff --git a/uffizzi/docker-compose.uffizzi.yml b/uffizzi/docker-compose.uffizzi.yml new file mode 100644 index 000000000..f90f8f5ca --- /dev/null +++ b/uffizzi/docker-compose.uffizzi.yml @@ -0,0 +1,89 @@ +version: "3" + +x-uffizzi: + ingress: + service: rails + port: 3000 + +services: + rails: + image: "${APP_IMAGE}" + container_name: forem_rails + ports: + - "3000:3000" + depends_on: + - db + - redis + environment: + RAILS_ENV: development + DATABASE_URL: postgresql://forem:forem@localhost:5432/PracticalDeveloper_development + REDIS_SESSIONS_URL: redis://localhost:6379 + REDIS_SIDEKIQ_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379 + RACK_TIMEOUT_WAIT_TIMEOUT: 10000 + RACK_TIMEOUT_SERVICE_TIMEOUT: 10000 + STATEMENT_TIMEOUT: 10000 + APP_DOMAIN: localhost:3000 + APP_PROTOCOL: https:// + entrypoint: ["dockerize", "-wait", "tcp://localhost:5432", "-wait", "tcp://localhost:6379", "-wait", "file:///opt/apps/forem/vendor/bundle/.bundle_finished", "-timeout", "5400s"] + command: [ "bash", "-c", "./scripts/entrypoint.sh bootstrap && bundle exec rails server -b 0.0.0.0 -p 3000"] + deploy: + resources: + limits: + memory: 4000M + + seed: + image: "${APP_IMAGE}" + container_name: forem_seed + depends_on: + - rails + - redis + - db + environment: + RAILS_ENV: development + REDIS_SESSIONS_URL: redis://localhost:6379 + REDIS_SIDEKIQ_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379 + DATABASE_URL: postgresql://forem:forem@localhost:5432/PracticalDeveloper_development + entrypoint: ["dockerize", "-wait", "tcp://localhost:5432", "-wait", "tcp://localhost:6379", "-wait", "http://localhost:3000", "-timeout", "5400s"] + command: ["./uffizzi/db_seed.sh"] + deploy: + resources: + limits: + memory: 500M + + sidekiq: + image: "${APP_IMAGE}" + container_name: forem_sidekiq + depends_on: + - rails + - redis + - db + environment: + RAILS_ENV: development + REDIS_SESSIONS_URL: redis://localhost:6379 + REDIS_SIDEKIQ_URL: redis://localhost:6379 + REDIS_URL: redis://localhost:6379 + DATABASE_URL: postgresql://forem:forem@localhost:5432/PracticalDeveloper_development + entrypoint: ["dockerize", "-wait", "tcp://localhost:5432", "-wait", "tcp://localhost:6379", "-wait", "http://localhost:3000", "-timeout", "5400s"] + command: ["bundle", "exec", "sidekiq","-c","2"] + deploy: + resources: + limits: + memory: 500M + + db: + image: postgres:11-alpine + container_name: forem_postgresql + environment: + POSTGRES_USER: forem + POSTGRES_PASSWORD: forem + POSTGRES_DB: PracticalDeveloper_development + ports: + - "5432:5432" + + redis: + image: redis:6.0.9-alpine + container_name: forem_redis + ports: + - "6379:6379"