diff --git a/.github/workflows/ci-and-cd-for-review.yml b/.github/workflows/ci-and-cd-for-review.yml index 2e1c8a8..9184b22 100644 --- a/.github/workflows/ci-and-cd-for-review.yml +++ b/.github/workflows/ci-and-cd-for-review.yml @@ -6,7 +6,30 @@ on: branches: - main +# Generate a GitHub token that can be exchanged with Google Cloud - see +# https://github.com/google-github-actions/auth/tree/v0.6.0#setting-up-workload-identity-federation +permissions: + contents: read + id-token: write + deployments: write + jobs: test-and-build: name: Integration uses: ./.github/workflows/test-and-build.yml + secrets: inherit + + release-to-review: + if: ${{ github.actor != 'dependabot[bot]' }} + name: Delivery + uses: ./.github/workflows/deploy-to-cloud-run.yml + needs: + - test-and-build + with: + environment: ${{ github.head_ref }} + deploy-name: pr${{ github.event.pull_request.number }} + cloud-sql-instance: sepomex-365521:us-central1:sepomex + cloud-sql-username-secret: database-username:latest + cloud-sql-password-secret: database-password:latest + cloud-run-service-suffix: ${{ needs.test-and-build.outputs.git-commit-short-sha }} + cloud-run-container-image: us-central1-docker.pkg.dev/sepomex-365521/icalialabs-sepomex/sepomex@${{ needs.test-and-build.outputs.container-image-digest }} diff --git a/.github/workflows/ci-and-cd.yml b/.github/workflows/ci-and-cd.yml index e73ff75..c547c04 100644 --- a/.github/workflows/ci-and-cd.yml +++ b/.github/workflows/ci-and-cd.yml @@ -6,7 +6,29 @@ on: branches: - main +# Generate a GitHub token that can be exchanged with Google Cloud - see +# https://github.com/google-github-actions/auth/tree/v0.6.0#setting-up-workload-identity-federation +permissions: + contents: read + id-token: write + deployments: write + jobs: test-and-build: name: Integration uses: ./.github/workflows/test-and-build.yml + + deploy-to-staging: + name: Staging + uses: ./.github/workflows/deploy-to-cloud-run.yml + needs: + - test-and-build + with: + environment: staging + deploy-name: staging + cloud-sql-instance: sepomex-365521:us-central1:sepomex + cloud-sql-username-secret: database-username:latest + cloud-sql-password-secret: database-password:latest + cloud-run-service-suffix: ${{ needs.test-and-build.outputs.git-commit-short-sha }} + cloud-run-container-image: us-central1-docker.pkg.dev/sepomex-365521/sepomex/sepomex@${{ needs.test-and-build.outputs.container-image-digest }} + secrets: inherit diff --git a/.github/workflows/deploy-to-cloud-run.yml b/.github/workflows/deploy-to-cloud-run.yml new file mode 100644 index 0000000..532da83 --- /dev/null +++ b/.github/workflows/deploy-to-cloud-run.yml @@ -0,0 +1,101 @@ +on: + workflow_call: + inputs: + environment: + required: true + type: string + deploy-name: + required: true + type: string + cloud-sql-instance: + required: true + type: string + cloud-sql-username-secret: + required: false + type: string + default: projects/582875546495/secrets/database-username:latest + cloud-sql-password-secret: + required: false + type: string + default: projects/582875546495/secrets/database-password:latest + cloud-run-container-image: + required: true + type: string + cloud-run-service-suffix: + required: false + type: string + cloud-run-minimum-instances: + required: false + type: number + default: 0 + +jobs: + service-deploy: + name: Service Deploy + runs-on: ubuntu-latest + env: + DATABASE_NAME: sepomex_${{ inputs.deploy-name }} + steps: + # actions/checkout MUST come before auth + - name: Checkout the code + uses: actions/checkout@v3.1.0 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.8.0 + with: + service_account: github-actions@sepomex-365521.iam.gserviceaccount.com + workload_identity_provider: projects/582875546495/locations/global/workloadIdentityPools/github-pool/providers/github-provider + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v0.6.0 + + - name: URLEncode Cloud SQL Instance string + id: url-encode-cloud-sql-instance + run: |- + ruby -e 'require "erb"; puts "encoded-value=#{ERB::Util.url_encode("${{ inputs.cloud-sql-instance }}")}"' >> $GITHUB_OUTPUT + + - name: Register Deploy Start on Github + uses: bobheadxi/deployments@v1.3.0 + id: deploy-start + with: + step: start + ref: ${{ github.head_ref }} + env: ${{ inputs.environment }} + token: ${{ github.token }} + + - name: Set deploy timestamp + id: set-deploy-timestamp + run: echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT + + - name: Deploy to Cloud Run + id: deploy + uses: google-github-actions/deploy-cloudrun@v0.9.0 + with: + region: us-central1 + service: sepomex-${{ inputs.deploy-name }} + suffix: ${{ inputs.cloud-run-service-suffix }}-${{ steps.set-deploy-timestamp.outputs.timestamp }} + image: ${{ inputs.cloud-run-container-image }} + secrets: | + DATABASE_USERNAME=${{ inputs.cloud-sql-username-secret }} + DATABASE_PASSWORD=${{ inputs.cloud-sql-password-secret }} + env_vars: | + DATABASE_URL=postgres://%2Fcloudsql%2F${{ steps.url-encode-cloud-sql-instance.outputs.encoded-value }}/${{ env.DATABASE_NAME }} + GOOGLE_CLOUD_PROJECT=sepomex-365521 + flags: |- + --allow-unauthenticated + --add-cloudsql-instances ${{ inputs.cloud-sql-instance }} + --min-instances=${{ inputs.cloud-run-minimum-instances }} + --service-account github-actions@sepomex-365521.iam.gserviceaccount.com + + - name: Finalize the deployment state on Github + uses: bobheadxi/deployments@v1.3.0 + if: always() + with: + step: finish + override: true + auto_inactive: true + status: ${{ job.status }} + token: ${{ github.token }} + env_url: ${{ steps.deploy.outputs.url }} + env: ${{ steps.deploy-start.outputs.env }} + deployment_id: ${{ steps.deploy-start.outputs.deployment_id }} diff --git a/.github/workflows/review-env-setup.yml b/.github/workflows/review-env-setup.yml new file mode 100644 index 0000000..99c2b84 --- /dev/null +++ b/.github/workflows/review-env-setup.yml @@ -0,0 +1,48 @@ +name: Review Environment Setup + +on: + workflow_dispatch: + pull_request: + branches: + - main + types: + - opened + - reopened + +jobs: + create_deployment: + if: ${{ github.actor != 'dependabot[bot]' }} + name: Create deployment + runs-on: ubuntu-latest + env: + DB_NAME: "sepomex-pr${{ github.event.pull_request.number }}" + GOOGLE_CLOUD_PROJECT: sepomex-365521 + + # Generate a GitHub token that can be exchanged with Google Cloud - see + # https://github.com/google-github-actions/auth/tree/v0.6.0#setting-up-workload-identity-federation + permissions: + contents: read + id-token: write + + steps: + # actions/checkout MUST come before auth + - name: Checkout the code + uses: actions/checkout@v3.1.0 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.6.0 + with: + service_account: github-actions@sepomex-365521.iam.gserviceaccount.com + workload_identity_provider: projects/582875546495/locations/global/workloadIdentityPools/github-pool/providers/github-provider + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v0.6.0 + + - name: Ensure a review database exists + run: |- + gcloud sql databases describe ${{ env.DB_NAME }} \ + --instance=sepomex \ + --format="value(name)" \ + && echo "Database ${{ env.DB_NAME }} already exists" \ + || gcloud sql databases create ${{ env.DB_NAME }} \ + --instance=sepomex diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index dee8735..d8bbca2 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -47,7 +47,9 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.2.1 + with: + version: v0.9.1 - name: Build Test Image id: build-test-image @@ -81,11 +83,23 @@ jobs: path: | tmp/capybara/screenshots + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0.8.0 + with: + service_account: github-actions@sepomex-365521.iam.gserviceaccount.com + workload_identity_provider: projects/582875546495/locations/global/workloadIdentityPools/github-pool/providers/github-provider + + - name: Set up Google Cloud SDK + uses: google-github-actions/setup-gcloud@v0.6.0 + + - name: Authorize push to Google Cloud Artifact Registry + run: gcloud auth configure-docker us-central1-docker.pkg.dev + - name: Build & Push Release Image id: build-and-push-release-image uses: docker/build-push-action@v3.1.1 with: - push: false + push: true target: release platforms: linux/amd64 builder: ${{ steps.buildx.outputs.name }} @@ -93,8 +107,8 @@ jobs: DEVELOPER_UID=${{ steps.variables.outputs.runner-uid }} DEVELOPER_USERNAME=${{ steps.variables.outputs.runner-user }} tags: | - us-central1-docker.pkg.dev/icalia-labs-sepomex/sepomex/sepomex-web:${{ steps.variables.outputs.git-commit-short-sha }} - us-central1-docker.pkg.dev/icalia-labs-sepomex/sepomex/sepomex-web:${{ steps.variables.outputs.git-dasherized-branch }} - us-central1-docker.pkg.dev/icalia-labs-sepomex/sepomex/sepomex-web:latest + us-central1-docker.pkg.dev/sepomex-365521/icalialabs-sepomex/sepomex:${{ steps.variables.outputs.git-commit-short-sha }} + us-central1-docker.pkg.dev/sepomex-365521/icalialabs-sepomex/sepomex:${{ steps.variables.outputs.git-dasherized-branch }} + us-central1-docker.pkg.dev/sepomex-365521/icalialabs-sepomex/sepomex:latest cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 7d8ca56..f865de6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,9 @@ /db/development.sqlite3 /log/development.log -/tmp -tmp/cache/bootsnap-load-path-cache log/test.log docker-compose.override.yml -tmp/cache # Ignore history files **/.*_hist* @@ -15,3 +12,13 @@ tmp/cache # Ignore database dumps db/dumps/* !db/dumps/.keep + +# Ignore all logfiles and tempfiles. +/tmp/* +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep +!/tmp/pids/server.pid diff --git a/Gemfile b/Gemfile index dbbdc96..108634c 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ ruby '2.7.5' gem 'rails', '~> 6.0', '>= 6.0.3.2' # Use pg as the database for Active Record -gem 'pg' +gem 'pg', '~> 1.1' # Use Puma as the app server gem 'puma', '~> 4.3' @@ -31,6 +31,9 @@ gem 'pagy', '~> 3.8', '>= 3.8.2' gem 'rack-cors', '~> 1.1', '>= 1.1.1' # Testing +# Read secrets from Google Cloud Secret Manager +gem 'google-cloud-secret_manager', '~> 1.1', '>= 1.1.3' + group :development, :test do gem 'listen', '>= 3.0.5', '< 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 3b26030..638a11a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,6 +61,8 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) backport (1.2.0) benchmark (0.2.0) @@ -85,13 +87,67 @@ GEM factory_bot_rails (6.1.0) factory_bot (~> 6.1.0) railties (>= 5.0.0) + faraday (2.7.10) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + faraday-retry (2.2.0) + faraday (~> 2.0) ffi (1.13.1) + gapic-common (0.19.1) + faraday (>= 1.9, < 3.a) + faraday-retry (>= 1.0, < 3.a) + google-protobuf (~> 3.14) + googleapis-common-protos (>= 1.3.12, < 2.a) + googleapis-common-protos-types (>= 1.3.1, < 2.a) + googleauth (~> 1.0) + grpc (~> 1.36) globalid (0.4.2) activesupport (>= 4.2.0) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-secret_manager (1.2.0) + google-cloud-core (~> 1.6) + google-cloud-secret_manager-v1 (>= 0.1, < 2.a) + google-cloud-secret_manager-v1beta1 (>= 0.3, < 2.a) + google-cloud-secret_manager-v1 (0.17.2) + gapic-common (>= 0.19.1, < 2.a) + google-cloud-errors (~> 1.0) + grpc-google-iam-v1 (~> 1.1) + google-cloud-secret_manager-v1beta1 (0.12.2) + gapic-common (>= 0.19.1, < 2.a) + google-cloud-errors (~> 1.0) + grpc-google-iam-v1 (~> 1.1) + google-protobuf (3.24.1) + googleapis-common-protos (1.4.0) + google-protobuf (~> 3.14) + googleapis-common-protos-types (~> 1.2) + grpc (~> 1.27) + googleapis-common-protos-types (1.8.0) + google-protobuf (~> 3.18) + googleauth (1.7.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + grpc (1.57.0) + google-protobuf (~> 3.23) + googleapis-common-protos-types (~> 1.0) + grpc-google-iam-v1 (1.3.0) + google-protobuf (~> 3.18) + googleapis-common-protos (~> 1.4) + grpc (~> 1.41) i18n (1.8.5) concurrent-ruby (~> 1.0) jaro_winkler (1.5.4) jsonapi-renderer (0.2.2) + jwt (2.7.1) kramdown (2.3.1) rexml kramdown-parser-gfm (1.1.0) @@ -108,6 +164,7 @@ GEM mini_mime (>= 0.1.1) marcel (0.3.3) mimemagic (~> 0.3.2) + memoist (0.16.2) method_source (1.0.0) mimemagic (0.3.10) nokogiri (~> 1) @@ -116,18 +173,21 @@ GEM mini_portile2 (2.8.0) minitest (5.14.1) msgpack (1.3.3) + multi_json (1.15.0) nio4r (2.5.2) nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) on_container (0.0.16) activesupport (>= 4) + os (1.1.4) pager_api (0.3.2) pagy (3.8.3) parallel (1.21.0) parser (3.1.0.0) ast (~> 2.4.1) - pg (1.2.3) + pg (1.5.3) + public_suffix (5.0.3) puma (4.3.12) nio4r (~> 2.0) racc (1.6.0) @@ -215,9 +275,15 @@ GEM ruby-debug-ide (0.7.3) rake (>= 0.8.1) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) ruby_dep (1.5.0) shoulda-matchers (4.3.0) activesupport (>= 4.2.0) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) solargraph (0.44.3) backport (~> 1.2) benchmark @@ -269,11 +335,12 @@ DEPENDENCIES csv (~> 3.1, >= 3.1.5) debase (~> 0.2.4.1) factory_bot_rails (~> 6.1) + google-cloud-secret_manager (~> 1.1, >= 1.1.3) listen (>= 3.0.5, < 3.2) on_container (~> 0.0.16) pager_api (~> 0.3.2) pagy (~> 3.8, >= 3.8.2) - pg + pg (~> 1.1) puma (~> 4.3) rack-cors (~> 1.1, >= 1.1.1) rails (~> 6.0, >= 6.0.3.2) diff --git a/bin/entrypoint b/bin/entrypoint new file mode 100755 index 0000000..116b146 --- /dev/null +++ b/bin/entrypoint @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +# Entrypoint Script +# +# This scipt is executed by Docker on container start on release images (i.e. +# live environments) +puts 'Starting Entrypoint Script' + +def set_given_or_default_command + ARGV.concat %w[puma] if ARGV.empty? +end + +def execute_given_or_default_command + exec(*ARGV) +end + +def run_migrations + raise('Migrations failed') unless system('rails db:migrate') +end + +def run_database_seeds + raise('Seeds failed') unless system('rails db:seed') +end + +set_given_or_default_command +run_migrations +run_database_seeds +execute_given_or_default_command diff --git a/config/database.yml b/config/database.yml index 1b7ffff..9f8bffb 100644 --- a/config/database.yml +++ b/config/database.yml @@ -15,6 +15,7 @@ # gem 'pg' # default: &default + adapter: postgresql encoding: unicode # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling @@ -49,3 +50,5 @@ test: production: <<: *default + password: <%= ENV['DATABASE_PASSWORD'] %> + username: <%= ENV['DATABASE_USERNAME'] %> diff --git a/tmp/pids/server.pid b/tmp/pids/server.pid new file mode 100755 index 0000000..ea066eb --- /dev/null +++ b/tmp/pids/server.pid @@ -0,0 +1 @@ +11733 \ No newline at end of file