Automated on-demand deployments with Semaphore CI Classic API (and Ruby)
Tags: ci, cd, semaphoreci, ruby
Published: 2020-05-11 18:02:27

Semaphore is a popular tool used to create CI/CD pipelines. It does a great job at quickly integrating with Git repositories and running build or deployment tasks. Additionally, it comes with two different flavours: the standard Semaphore Classic and, the much more sophisticated Semaphore 2.0.

While I'm not fully familiar with Semaphore 2.0 capabilities, one thing I can tell about Semaphore Classic is that it feels like one of the good, old Unix programs that was designed to do one simple thing, and do it well. Or, perhaps more accurately, it feels like a tool that was built with the Convention over Configuration rule in mind: it can definitely do a lot of things, but only in a specific way, and under specific assumptions.

In this post I'll explore the surprisingly underdocumented capabilities of Semaphore Classic HTTP API to create a custom workflow supporting on-demand deployments to different servers.

Now, imagine you've submitted a pull request and you'd like to test it somehow, e.g. on a staging environment. At this point you've probably already defined a new staging server that Semaphore could deploy to. So, all you have to do is to select a branch, pick a build and click `Deploy manually` and select your non-production server. Pretty easy, eh? Well, what if you misclicked and accidentally deployed to production? Of course you could count on your reflexes and instantly stop the deploy, but we all know that it's better to automate such risky processes. Human error is inevitable. So where do we start?

Semaphore CI operates on a small number of easy-to-comprehend concepts: Branches, Builds, Servers and Deploys. As we saw in the previous paragraph, a build is triggered by some kind of an external event (e.g. by a new brach being created or a new pull request being submitted). Then, you'd usually want CI to deploy this build to a specific server. These concepts map to entities in the HTTP API:
- [`Branch and Build`](https://semaphoreci.com/docs/branches-and-builds-api.html#branch_status)
- [`Server`](https://semaphoreci.com/docs/servers-and-deploys-api.html#server_status)
- [`Deploy`](https://semaphoreci.com/docs/servers-and-deploys-api.html#deploy_information)

With this knowledge in mind, let's try to automate something!

1. URL structure & basic Semaphore API client setup

    In this preliminary step, let's create a simple Ruby class that uses   the [`HTTParty`](https://github.com/jnunemaker/httparty) gem and lists required configuration variables that'll be passed in requests to Semaphore API.

    ```ruby
    class SemaphoreClient
      include HTTParty
  
      PROJECT_ID = ENV["SEMAPHORE_PROJECT_ID"]
      SERVER_ID = ENV["SEMAPHORE_SERVER_ID"]

      base_uri "https://semaphoreci.com/api/v1/projects/#{PROJECT_ID}"
  
      default_params auth_token: ENV["SEMAPHORE_API_AUTH_TOKEN"]
    end
    ```
  - `base_uri` method defines the root URL of Semaphore Classic API
  - `auth_token` param will be passed in every request. This is the User   Authentication Token found in [Account Settings](https://semaphoreci.com/users/edit)
  - `PROJECT_ID` is an ID of a Semaphore project, corresponding to a Git repository. This is a bit tricky to find: you can either query the [Projects API](https://semaphoreci.com/docs/projects-api.html#users-projects) to find your repo, or go to Project Badge Settings where you can copy-paste the ID from provided snippets. This should look something like this: `https://semaphoreci.com/api/v1/projects/<project_id>/badge.svg`.
  - `SERVER_ID` is an ID of a target deployment server, e.g. the one you'd like to use to deploy to your Staging/Preview environment. It seems that the only way to fetch internal IDs of servers is via the [Servers API](https://semaphoreci.com/docs/servers-and-deploys-api.html#project_servers)

2. Check branch build status

    First, we need to check if a Semaphore build completed succesfully on a branch. In other words, let's see if the PR is "green" (build passed), "yellow" (build pending) or "red" (build failed):

    ```ruby
    class SemaphoreClient

      ...

      class << self
        def pr_status(pr_number:)
          response = get(
            "/pull-request-#{pr_number}/status"
          )

          {
            build_number: response["build_number"],
            result: response["result"]
          }
        end
      end
    end
    ```

     As you can see, calling `SemaphoreClient.pr_status("1234")` should return a simple hash with `build_number` and `result`: we'll need these values for later.

3. Trigger deployment

    If `result` is `"passed"`, this means that the Semaphore build has passed and can be safely deployed. Of course, it's also possible to deploy a running build (if you're brave enough). Anyway, let's add another method which'll use the `build_number` that we received in previous call.

    ```ruby
    class SemaphoreClient

      ...

      class << self
        def trigger_deploy(pr_number:, build_number:)
          response = post(
            "/pull-request-#{pr_number}/builds/" \
            "#{build_number}/deploy/#{SERVER_ID}"
          )

          {
            number: response["number"],
            result: response["result"]
          }
        end
      end   
    end
    ```

    Ta-da! You've just triggered a deployment to a non-default server without having to click through the Semaphore user interface. Time to pop the champagne?

4. Check deployment status

    Finally, it'd be good to monitor whether a deployment completed successfully. Let's add one more method:

    ```ruby
    class SemaphoreClient

      ...

      class << self
        def deploy_status(deploy_number:)
          response = get(
            "/servers/#{SERVER_ID}/deploys/#{deploy_number}"
          )

          {
            number: response["number"],
            result: response["result"]
          }
        end
      end
    end
    ```

And that's it. It's a very simplistic solution, yet should be enough to serve as a proof of concept for futher integration with Github Actions, a Github/Gitlab/Bitbucket bot or any other tool automating your Semaphore CI workflows.

Finally, for the record, here's the entire `SemaphoreClient` class. Again, please treat it as a basic PoC: it definitely calls for some error handling and, depending on your needs, you might want to fetch more data from the API responses. Anyway, happy integrating with Semaphore!

```ruby
class SemaphoreClient
  include HTTParty

  PROJECT_ID = ENV["SEMAPHORE_PROJECT_ID"]
  SERVER_ID = ENV["SEMAPHORE_SERVER_ID"]

  base_uri "https://semaphoreci.com/api/v1/projects/#{PROJECT_ID}"

  default_params auth_token: ENV["SEMAPHORE_API_AUTH_TOKEN"]

  class << self
    def pr_status(pr_number:)
      response = get(
        "/pull-request-#{pr_number}/status"
      )

      {
        build_number: response["build_number"],
        result: response["result"]
      }
    end

    def trigger_deploy(pr_number:, build_number:)
      response = post(
        "/pull-request-#{pr_number}/builds/" \
        "#{build_number}/deploy/#{SERVER_ID}"
      )

      {
        number: response["number"],
        result: response["result"]
      }
    end

    def deploy_status(deploy_number:)
      response = get(
        "/servers/#{SERVER_ID}/deploys/#{deploy_number}"
      )

      {
        number: response["number"],
        result: response["result"]
      }
    end
  end
end
```