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'