Skip to content

Instantly share code, notes, and snippets.

@umonaca
Last active July 8, 2020 09:22
Show Gist options
  • Save umonaca/e864a896800f9af56057e8eb44ec478a to your computer and use it in GitHub Desktop.
Save umonaca/e864a896800f9af56057e8eb44ec478a to your computer and use it in GitHub Desktop.
Yet another simple CI/CD pipeline for mastodon using CircleCI
version: 2

aliases:
  - &defaults
    docker:
      - image: circleci/ruby:2.7-buster-node
        environment: &ruby_environment
          BUNDLE_JOBS: 3
          BUNDLE_RETRY: 3
          BUNDLE_APP_CONFIG: ./.bundle/
          BUNDLE_PATH: ./vendor/bundle/
          DB_HOST: localhost
          DB_USER: root
          RAILS_ENV: test
          ALLOW_NOPAM: true
          CONTINUOUS_INTEGRATION: true
          DISABLE_SIMPLECOV: true
          PAM_ENABLED: true
          PAM_DEFAULT_SERVICE: pam_test
          PAM_CONTROLLED_SERVICE: pam_test_controlled
    working_directory: ~/projects/mastodon/

  - &attach_workspace
    attach_workspace:
      at: ~/projects/

  - &persist_to_workspace
    persist_to_workspace:
      root: ~/projects/
      paths:
        - ./mastodon/

  - &restore_ruby_dependencies
    restore_cache:
      keys:
        - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
        - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
        - v3-ruby-dependencies-

  - &install_steps
    steps:
      - checkout
      - *attach_workspace
      - restore_cache:
          keys:
            - v2-node-dependencies-{{ checksum "yarn.lock" }}
            - v2-node-dependencies-
      - run:
          name: Install yarn dependencies
          command: yarn install --frozen-lockfile
      - save_cache:
          key: v2-node-dependencies-{{ checksum "yarn.lock" }}
          paths:
            - ./node_modules/
      - *persist_to_workspace

  - &install_system_dependencies
      run:
        name: Install system dependencies
        command: |
          sudo apt-get update
          sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler

  - &install_ruby_dependencies
      steps:
        - *attach_workspace
        - *install_system_dependencies
        - run:
            name: Set Ruby version
            command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
        - *restore_ruby_dependencies
        - run:
            name: Set bundler settings
            command: |
              bundle config clean 'true'
              bundle config deployment 'true'
              bundle config with 'pam_authentication'
              bundle config without 'development production'
              bundle config frozen 'true'
        - run:
            name: Install bundler dependencies
            command: bundle check || (bundle install && bundle clean)
        - save_cache:
            key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
            paths:
              - ./.bundle/
              - ./vendor/bundle/
        - persist_to_workspace:
            root: ~/projects/
            paths:
                - ./mastodon/.bundle/
                - ./mastodon/vendor/bundle/

  - &test_steps
      parallelism: 4
      steps:
        - *attach_workspace
        - *install_system_dependencies
        - run:
            name: Install FFMPEG
            command: sudo apt-get install -y ffmpeg
        - run:
            name: Load database schema
            command: ./bin/rails db:create db:schema:load db:seed
        - run:
            name: Run rspec in parallel
            command: |
              bundle exec rspec --profile 10 \
                                --format RspecJunitFormatter \
                                --out test_results/rspec.xml \
                                --format progress \
                                $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
        - store_test_results:
            path: test_results
jobs:
  install:
    <<: *defaults
    <<: *install_steps

  install-ruby2.7:
    <<: *defaults
    <<: *install_ruby_dependencies

  install-ruby2.6:
    <<: *defaults
    docker:
      - image: circleci/ruby:2.6-buster-node
        environment: *ruby_environment
    <<: *install_ruby_dependencies

  build:
    <<: *defaults
    steps:
      - *attach_workspace
      - *install_system_dependencies
      - run:
          name: Precompile assets
          command: ./bin/rails assets:precompile
      - persist_to_workspace:
          root: ~/projects/
          paths:
              - ./mastodon/public/assets
              - ./mastodon/public/packs-test/

  test-migrations:
    <<: *defaults
    docker:
      - image: circleci/ruby:2.7-buster-node
        environment: *ruby_environment
      - image: circleci/postgres:12.2
        environment:
          POSTGRES_USER: root
          POSTGRES_HOST_AUTH_METHOD: trust
      - image: circleci/redis:5-alpine
    steps:
      - *attach_workspace
      - *install_system_dependencies
      - run:
          name: Create database
          command: ./bin/rails db:create
      - run:
          name: Run migrations
          command: ./bin/rails db:migrate

  test-ruby2.7:
    <<: *defaults
    docker:
      - image: circleci/ruby:2.7-buster-node
        environment: *ruby_environment
      - image: circleci/postgres:12.2
        environment:
          POSTGRES_USER: root
          POSTGRES_HOST_AUTH_METHOD: trust
      - image: circleci/redis:5-alpine
    <<: *test_steps

  test-ruby2.6:
    <<: *defaults
    docker:
      - image: circleci/ruby:2.6-buster-node
        environment: *ruby_environment
      - image: circleci/postgres:12.2
        environment:
          POSTGRES_USER: root
          POSTGRES_HOST_AUTH_METHOD: trust
      - image: circleci/redis:5-alpine
    <<: *test_steps

  test-webui:
    <<: *defaults
    docker:
      - image: circleci/node:12-buster
    steps:
      - *attach_workspace
      - run:
          name: Run jest
          command: yarn test:jest

  check-i18n:
    <<: *defaults
    steps:
      - *attach_workspace
      - *install_system_dependencies
      - run:
          name: Check locale file normalization
          command: bundle exec i18n-tasks check-normalized
      - run:
          name: Check for unused strings
          command: bundle exec i18n-tasks unused -l en
      - run:
          name: Check for wrong string interpolations
          command: bundle exec i18n-tasks check-consistent-interpolations
      - run:
          name: Check that all required locale files exist
          command: bundle exec rake repo:check_locales_files
  
  check-ruby-version:
    <<: *defaults
    steps:
      - checkout
      - run:
          name: Check if ruby version unchanged
          command: cat .ruby-version | egrep -q '2.6.6'

  check-node-version:
    <<: *defaults
    steps:
      - checkout
      - run:
          name: Check if node version unchanged
          command: cat .nvmrc | egrep -q '12'

  deploy:
    machine:
      enabled: true
    steps:
      - add_ssh_keys:
          fingerprints:
            - "SO:ME:FIN:G:ER:PR:IN:T"
      - run:
          name: Add known host
          command: mkdir -p ~/.ssh && ssh-keyscan YOUR_SERVER_IP >> ~/.ssh/known_hosts
      - run:
          name: Deploy source code over SSH
          command: ssh mastodon@YOUR_SERVER_IP "cd ~/live; git stash; git pull --no-edit origin master;"
      - run:
          name: Upgrade gems and js packages
          command: ssh mastodon@YOUR_SERVER_IP 'zsh -lc "set -e; cd ~/live;
            bundle install -j$(getconf _NPROCESSORS_ONLN); 
            yarn install --pure-lockfile"'
      - run:
          name: Database migration and precompile assets
          command: ssh mastodon@YOUR_SERVER_IP 'zsh -lc "set -e; cd ~/live; RAILS_ENV=production SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rails db:migrate; 
            RAILS_ENV=production bundle exec rails assets:precompile"'
      - run:
          name: Reload and restart
          command: ssh mastodon@YOUR_SERVER_IP 'zsh -lc "set -e; sudo systemctl reload mastodon-web && sudo systemctl restart mastodon-{sidekiq,streaming}"'
      - run:
          name: Clean up
          command: ssh mastodon@YOUR_SERVER_IP 'zsh -lc "set -e; cd ~/live; RAILS_ENV=production bin/tootctl cache clear; 
            RAILS_ENV=production bundle exec rails db:migrate"'

workflows:
  version: 2
  build-test-and-deploy:
    jobs:
      - install
      - install-ruby2.7:
          requires:
            - install
      - install-ruby2.6:
          requires:
            - install
            - install-ruby2.7
      - build:
          requires:
            - install-ruby2.7
      - test-migrations:
          requires:
            - install-ruby2.7
      - test-ruby2.7:
          requires:
            - install-ruby2.7
            - build
      - test-ruby2.6:
          requires:
            - install-ruby2.6
            - build
      - test-webui:
          requires:
            - install
      - check-i18n:
          requires:
            - install-ruby2.7
      - check-ruby-version
      - check-node-version
      - deploy:
          requires:
            - check-ruby-version
            - test-migrations
            - test-webui
            - test-ruby2.7
            - check-i18n
          filters:
            branches:
              only: master
@umonaca
Copy link
Author

umonaca commented Jul 1, 2020

  • Requires manual intervention when ruby version or node version changes
  • Stops the deploy pipeline when ruby version changes, but allows node version changes to pass with warnings
  • I'm using zsh as the default shell
  • I did not spend a lot of time to optimize the code. Feel free to write down your suggestions

@umonaca
Copy link
Author

umonaca commented Jul 1, 2020

Steps

  1. ssh-keygen -m PEM -t rsa -C "[email protected]" to generate a key pair.

  2. Connect your repository with CircleCI. Put the private key in CircleCI project settings. Put the public key in ~/.ssh/authorized_keys of the server.

  3. ssh-keygen -l -E md5 -f ~/.ssh/your_public_key to get the old type of md5 fingerprint, separated by colons. (Yes CircleCI still uses MD5 fingerprint.) Replace "SO:ME:FIN:G:ER:PR:IN:T" with the fingerprint that you get from the command line. Don't put "MD5" inside.

  4. Since I'm using zsh, according to the document here, I need to change .zshenv file. Append the following lines to that config file:

    export PATH="$HOME/.rbenv/bin:$PATH"
    eval "$(rbenv init -)"
    

    The basic idea is that a login shell is different from an interactive shell. The best way to set variables in a login shell is to put them inside .zshenv file.

  5. Set the sudo permission for the mastodon user with visudo. Restarting the services requires sudoer or root permission. If you are using a distribution without sudo, you are on your own.
    mastodon ALL=(root) NOPASSWD:/bin/systemctl reload mastodon-web,/bin/systemctl restart mastodon-sidekiq mastodon-streaming
    Make sure that this line follows the sudoer group config, so it overrides the sudoer group policy if you have added user mastodon to sudoer. (You probably shouldn't.)

  6. Change YOUR_SERVER_IP to your server ip. Replace the content of .circleci/config.yml with this file.

Now see if the pipelines are working in CircleCI.

@umonaca
Copy link
Author

umonaca commented Jul 1, 2020

Note on concurrency and race conditions:

Since Free version or CircleCI only runs 1 job at a time their are no race conditions for the deployment job. If you are using other non-free plans, you can use this to ensure that only one deploy job runs at a time.

@umonaca
Copy link
Author

umonaca commented Jul 3, 2020

Update: I have fixed a missing step "Upgrade gems and js packages".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment