From 3802732f434be784e3d8373e9b30f02b7c44dbac Mon Sep 17 00:00:00 2001 From: Lovro Bikic Date: Mon, 3 Jan 2022 14:03:39 +0100 Subject: [PATCH] Add GitHub Actions workflows for build and deploy Remove bundler setup from bin/deploy Fix bin/build reference Add reusable workflows for build and deploy Add workflows for build and deploy to staging and production Copy build/deploy workflows to .github/workflows folder Add GA explanation to readme Turn off node by default Ask about manual deployers Move postgres image prefix to reusable workflow Update SSH key naming scheme Add commented-out automatic deploy to production Add interpolation marks Change Postgres image to 13.2 Add --frozen-lockfile flag to yarn install Remove cancel-in-progress for deploys Add optional input for GHA runner Revert "Update SSH key naming scheme" This reverts commit f1df594fb6bb6d1e9e72d6d5a808564a653766fb. Separate Mina commands Add RAILS_ENV=test Document workflow inputs Add bin/audit, force color output Add prepare_ci script Run CI steps in parallel Move workflows to .github/workflows folder Remove postgres user Use trust auth method Add -j4 flag Add rubocop cache step Give names to all steps Move rubocop cache step Rename job to build Use github format for rubocop Use both simple and github formats Fix workflow path Make the ci_steps input required Change location of rubocop cache Change flag -j4 to -j0 Add example for deployers input Create .node-version file Add info about frontend to readme --- .github/workflows/build.yml | 107 +++++++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 62 ++++++++++++++++++++ README.md | 16 ++++++ build.yml | 17 ++++++ deploy-production.yml | 19 +++++++ deploy-staging.yml | 19 +++++++ template.rb | 103 ++++++++++++++++++++------------- 7 files changed, 304 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 build.yml create mode 100644 deploy-production.yml create mode 100644 deploy-staging.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..63ad170 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,107 @@ +name: Build + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_call: + inputs: + # Selects the version of Postgres for running tests + # See: https://github.com/docker-library/docs/blob/master/postgres/README.md#supported-tags-and-respective-dockerfile-links + postgres_image: + required: true + type: string + + # Determines whether to install Node and run `yarn install` + use_node: + required: false + type: boolean + default: true + + # Sets BUNDLE_APP_CONFIG environment variable + # See: https://bundler.io/man/bundle-config.1.html + bundle_app_config: + required: false + type: string + default: .bundle/ci-build + + # Selects the runner on which the workflow will run + # See: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + runner: + required: false + type: string + default: ubuntu-20.04 + + # Defines which scripts will run on CI + # Format: space-delimited paths to scripts + # Example: 'bin/audit bin/lint bin/test' + ci_steps: + required: true + type: string + secrets: + VAULT_ADDR: + required: true + VAULT_AUTH_METHOD: + required: true + VAULT_AUTH_USER_ID: + required: true + VAULT_AUTH_APP_ID: + required: true + +jobs: + build: + name: 'Build' + runs-on: ${{ inputs.runner }} + env: + BUNDLE_APP_CONFIG: ${{ inputs.bundle_app_config }} + RUBOCOP_CACHE_ROOT: .rubocop-cache + services: + postgres: + image: postgres:${{ inputs.postgres_image }} + env: + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + options: --name=postgres + steps: + - name: Git checkout + uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Prepare RuboCop cache + uses: actions/cache@v2 + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: ${{ runner.os }}-rubocop-cache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-rubocop-cache- + - name: Set up Node + uses: actions/setup-node@v2 + if: ${{ inputs.use_node }} + with: + node-version-file: '.node-version' + - name: Prepare node_modules cache + uses: actions/cache@v2 + if: ${{ inputs.use_node }} + with: + path: node_modules + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-modules- + - name: Install JS packages + if: ${{ inputs.use_node }} + run: yarn install --frozen-lockfile + - name: Prepare CI + run: bin/prepare_ci + env: + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + VAULT_AUTH_METHOD: ${{ secrets.VAULT_AUTH_METHOD }} + VAULT_AUTH_USER_ID: ${{ secrets.VAULT_AUTH_USER_ID }} + VAULT_AUTH_APP_ID: ${{ secrets.VAULT_AUTH_APP_ID }} + - name: Wait for Postgres to be ready + run: until docker exec postgres pg_isready; do sleep 1; done + - name: CI steps + run: 'parallel --lb -k -j0 ::: ${{ inputs.ci_steps }}' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..8bbfa69 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Deploy + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +on: + workflow_call: + inputs: + # Sets the Mina environment (e.g. staging, production) + # A task by the same name must exist in config/deploy.rb + environment: + required: true + type: string + + # Sets the Git branch which will be checked out + branch: + required: true + type: string + + # Determines who can manually trigger the workflow + # Example: "@github_username1 @github_username2" + # See: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow + deployers: + required: false + type: string + default: '' + + # Sets BUNDLE_APP_CONFIG environment variable + # See: https://bundler.io/man/bundle-config.1.html + bundle_app_config: + required: false + type: string + default: .bundle/ci-deploy + + # Selects the runner on which the workflow will run + # See: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + runner: + required: false + type: string + default: ubuntu-20.04 + secrets: + SSH_PRIVATE_KEY: + required: true + +jobs: + deploy: + name: Deploy + runs-on: ${{ inputs.runner }} + env: + BUNDLE_APP_CONFIG: ${{ inputs.bundle_app_config }} + if: ${{ github.event_name == 'workflow_dispatch' && contains(inputs.deployers, format('@{0}', github.actor)) || github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ inputs.branch }} + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - run: bin/deploy ${{ inputs.environment }} diff --git a/README.md b/README.md index a0ebbe2..d878019 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ then run if needed: rbenv global #{latest_ruby} ``` +### GitHub Actions + +This template uses GitHub Actions for CI/CD. In order for workflows to work properly some [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) have to be set up. + +For build workflow to work, the following secrets must exist (usually set up by DevOps): +- `VAULT_ADDR` +- `VAULT_AUTH_METHOD` +- `VAULT_AUTH_USER_ID` +- `VAULT_AUTH_APP_ID` + +For deploy workflows, you need to generate private/public SSH key pairs for each environment. Public key should be added to the server to which you're deploying. Private key should be added as a secret to GitHub and named `SSH_PRIVATE_KEY_#{ENVIRONMENT}`, where `ENVIRONMENT` is replaced with an appropriate environment name (`STAGING`, `PRODUCTION`, etc.). + +### Frontend + +If your application will have a frontend (the template will ask you that), you must have Node installed on your machine. The template creates a `.node-version` file with the Node version set to the version you're currently running (check by executing `node -v`). Therefore, ensure that you have the latest [Active LTS](https://nodejs.org/en/about/releases/) version of Node running on your machine before using the template. + ## Usage ```shell diff --git a/build.yml b/build.yml new file mode 100644 index 0000000..7b65f1f --- /dev/null +++ b/build.yml @@ -0,0 +1,17 @@ +name: Build + +on: [push] + +jobs: + build: + name: Build + uses: infinum/default_rails_template/.github/workflows/build.yml@v1 + with: + postgres_image: '13.2' + use_node: false + ci_steps: 'bin/audit bin/lint bin/test' + secrets: + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + VAULT_AUTH_METHOD: ${{ secrets.VAULT_AUTH_METHOD }} + VAULT_AUTH_USER_ID: ${{ secrets.VAULT_AUTH_USER_ID }} + VAULT_AUTH_APP_ID: ${{ secrets.VAULT_AUTH_APP_ID }} diff --git a/deploy-production.yml b/deploy-production.yml new file mode 100644 index 0000000..02631e1 --- /dev/null +++ b/deploy-production.yml @@ -0,0 +1,19 @@ +name: Deploy production + +on: + workflow_dispatch: + # workflow_run: # UNCOMMENT THIS IF YOU WANT AUTOMATIC PRODUCTION DEPLOYS + # workflows: [Build] + # branches: [master] + # types: [completed] + +jobs: + deploy: + name: Deploy + uses: infinum/default_rails_template/.github/workflows/deploy.yml@v1 + with: + environment: production + branch: master + deployers: 'DEPLOY USERS GO HERE' # Example: '@github_username1 @github_username2' + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY_PRODUCTION }} diff --git a/deploy-staging.yml b/deploy-staging.yml new file mode 100644 index 0000000..c695b6a --- /dev/null +++ b/deploy-staging.yml @@ -0,0 +1,19 @@ +name: Deploy staging + +on: + workflow_dispatch: + workflow_run: + workflows: [Build] + branches: [staging] + types: [completed] + +jobs: + deploy: + name: Deploy + uses: infinum/default_rails_template/.github/workflows/deploy.yml@v1 + with: + environment: staging + branch: staging + deployers: 'DEPLOY USERS GO HERE' # Example: '@github_username1 @github_username2' + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY_STAGING }} diff --git a/template.rb b/template.rb index 85f2151..d4a3b74 100644 --- a/template.rb +++ b/template.rb @@ -135,55 +135,67 @@ HEREDOC create_file 'bin/update', BIN_UPDATE, force: true -BIN_BUILD = <<~HEREDOC.strip_heredoc +BIN_PREPARE_CI = <<~HEREDOC.strip_heredoc #!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset - echo "=========== setting env variables ===========" - export RAILS_ENV='test' - export BUNDLE_APP_CONFIG='.bundle/ci-build' - - echo "=========== install bundler ===========" - bundler_version=`tail -n 1 Gemfile.lock | xargs` - time gem install bundler:$bundler_version --no-document - - echo "=========== bundle install ===========" - time bundle install + echo "=========== pull secrets ===========" + bundle exec secrets pull -e development -y +HEREDOC +create_file 'bin/prepare_ci', BIN_PREPARE_CI, force: true +chmod 'bin/prepare_ci', 0755, verbose: false - echo "=========== secrets pull =============" - time bundle exec secrets pull -e development -y +BIN_AUDIT = <<~HEREDOC.strip_heredoc + #!/usr/bin/env bash - echo "=========== rails db:test:prepare ===========" - time bundle exec rails db:test:prepare + set -o errexit + set -o pipefail + set -o nounset echo "=========== bundle audit ===========" time bundle exec bundle-audit update --quiet time bundle exec bundle-audit check - ############################################# - # Uncomment this if you need yarn libraries # - # for running your tests # - ############################################# - # echo "=========== yarn install ===========" - # time yarn install + echo "=========== brakeman ===========" + time bundle exec brakeman -q --color +HEREDOC +create_file 'bin/audit', BIN_AUDIT, force: true +chmod 'bin/audit', 0755, verbose: false + +BIN_LINT = <<~HEREDOC.strip_heredoc + #!/usr/bin/env bash + + set -o errexit + set -o pipefail + set -o nounset echo "=========== zeitwerk check ===========" time bundle exec rails zeitwerk:check - echo "=========== brakeman ===========" - time bundle exec brakeman -q - echo "=========== rubocop ===========" - time bundle exec rubocop --format simple + time bundle exec rubocop --format simple --format github --color --parallel +HEREDOC +create_file 'bin/lint', BIN_LINT, force: true +chmod 'bin/lint', 0755, verbose: false + +BIN_TEST = <<~HEREDOC.strip_heredoc + #!/usr/bin/env bash + + set -o errexit + set -o pipefail + set -o nounset + + echo "=========== rails db:test:prepare ===========" + time RAILS_ENV=test bundle exec rails db:test:prepare echo "=========== rspec ===========" - time bundle exec rspec + time bundle exec rspec --force-color HEREDOC -create_file 'bin/build', BIN_BUILD, force: true -chmod 'bin/build', 0755, verbose: false +create_file 'bin/test', BIN_TEST, force: true +chmod 'bin/test', 0755, verbose: false BIN_DEPLOY = <<~HEREDOC.strip_heredoc #!/usr/bin/env bash @@ -194,18 +206,11 @@ echo "=========== setting env variables ===========" environment=$1 - export RAILS_ENV='test' - export BUNDLE_APP_CONFIG='.bundle/ci-deploy' - - echo "=========== install bundler ===========" - bundler_version=`tail -n 1 Gemfile.lock | xargs` - time gem install bundler:$bundler_version --no-document - - echo "=========== bundle install ===========" - time bundle install echo "=========== mina deploy ==============" - time bundle exec mina $environment ssh_keyscan_domain setup deploy + time bundle exec mina $environment ssh_keyscan_domain + time bundle exec mina $environment setup + time bundle exec mina $environment deploy ############################################# # Uncomment this if you need to publish dox # @@ -228,6 +233,10 @@ create_file 'bin/deploy', BIN_DEPLOY, force: true chmod 'bin/deploy', 0755, verbose: false +# get("#{BASE_URL}/build.yml", '.github/workflows/build.yml') +# get("#{BASE_URL}/deploy-staging.yml", '.github/workflows/deploy-staging.yml') +# get("#{BASE_URL}/deploy-production.yml", '.github/workflows/deploy-production.yml') + # bundler config BUNDLER_CI_BUILD_CONFIG = <<~HEREDOC.strip_heredoc --- @@ -578,6 +587,10 @@ def run get("#{BASE_URL}/.slim-lint.yml", '.slim-lint.yml') + node_version = `node -v`.chomp.sub('v', '') + + create_file '.node-version', node_version + PACKAGE_JSON_FILE = <<~HEREDOC { "name": "#{app_name}", @@ -592,6 +605,9 @@ def run }, "eslintConfig": { "extends": "@infinumrails/eslint-config-js" + }, + "engines": { + "node": "#{node_version}" } } HEREDOC @@ -634,7 +650,7 @@ def run create_file '.stylelintignore', STYLELINTIGNORE_FILE - append_to_file 'bin/build', after: "time bundle exec rubocop --format simple\n" do + append_to_file 'bin/lint' do <<~HEREDOC echo "=========== slim lint ===========" @@ -649,6 +665,8 @@ def run end run 'yarn add --dev @infinumrails/eslint-config-js @infinumrails/stylelint-config-scss eslint postcss stylelint' + + gsub_file('.github/workflows/build.yml', /^.*use_node: false.*\n/, '') end ## Ask about default PR reviewers @@ -659,6 +677,13 @@ def run HEREDOC end +## Users allowed to manually trigger deploys +staging_deployers = ask('Who can manually trigger a deploy to staging? (Example: @username1 @username2)', :green) +gsub_file('.github/workflows/deploy-staging.yml', 'DEPLOY USERS GO HERE', staging_deployers) + +production_deployers = ask('Who can manually trigger a deploy to production? (Example: @username1 @username2)', :green) +gsub_file('.github/workflows/deploy-production.yml', 'DEPLOY USERS GO HERE', production_deployers) + # add annotate task file and ignore its rubocop violations rails_command 'generate annotate:install' ANNOTATE_TASK_FILE = 'lib/tasks/auto_annotate_models.rake'