CI/CD for HA with Docker, Gitea and Drone

Hi all,

the past 3 days I spent some time to automate my deployment as I now maintain 4 installations of HA (parents, brother and some friends).

Every time I change something on the configs I would like to know if my config works correctly without check out from my local repository hosted on Gitea using Docker.

My CI/CD setup works find even with some custom components from different contributors hosted on GitHub.

My HA installation is also located on my QNAP in a Docker container using network_mode: host.

Assumed knowledge
- git
- docker

The following lines will describe a complete how-to. My setup was made on a QNAP using Container Station.

Docker-Compose
First of all you need to install some containers using the following docker-compose.yml file:

version: '3'

services:
  gitea:
    container_name: gitea
    image: gitea/gitea:latest
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - RUN_MODE=prod
    ports:
      - "3000:3000"
      - "222:22"
    volumes:
      - /<some-folder>/gitea:/data

  drone:
    container_name: drone
    image: drone/drone:latest
    restart: unless-stopped
    environment:
      - DRONE_GITEA_SERVER=http://<your-ip-address>:3000/
      - DRONE_GIT_ALWAYS_AUTH=false
      - DRONE_RUNNER_CAPACITY=2
      - DRONE_SERVER_PROTO=http
      - DRONE_SERVER_HOST=<your-ip-address>:3001
      - DRONE_TLS_AUTOCERT=false
      - DRONE_NETWORK=cicd_default
      - DRONE_RUNNER_NETWORKS=cicd_default
    ports:
      - "3001:80"
      - "9000:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /<some-folder>/drone:/data

Note:
- Change with the path of your desired location
- Change with the ip of your container (in my case it’s the IP address of my QNAP)

Gitea Setup

  • Open Gitea using http://:3000 and setup.
  • Create a username and password for the administrator (bottom of the page)
  • Add a new repository.
  • Initialize it using the commands shown on the page.

Drone Setup

  • Open Drone using http://:3001 and use the same administrator credentials you’ve setup on Gitea.
  • Click on sync and wait for it to fetch the Gitea repository.
  • Click on “Activate” and activate it on the next page.
  • Go to “Settings” and add two new credentials:
secret name: ssh_username
secret value: <yourusername>
secret name: ssh_password
secret value: <yoursupersecretpassword>

Note
The above used credentials should be the admin user on your QNAP (for ssh and write permissions to folders).

Commit to Gitea

  • Now that you’ve setup everything commit and push your code to your newly installed Gitea repository.
  • Add a new file named .drone.yml with the following content to your repository and - change with the IP of your QNAP.:
kind: pipeline
name: default

steps:
  - name: test
    image: homeassistant/amd64-homeassistant
    commands:
      - echo "Downloading here_travel_time component"
      - wget -O /tmp/here.zip https://github.com/eifinger/here_travel_time/archive/master.zip
      - unzip /tmp/here.zip 'here_travel_time-master/custom_components/here_travel_time/*' -d /tmp/
      - cp -R /tmp/here_travel_time-master/custom_components/here_travel_time ./custom_components/
      - hass -c . --script check_config
      - echo "Cleaning up custom components"
      - rm -rf ./custom_components/here_travel_time
    
  - name: deploy
    image: appleboy/drone-scp
    settings:
      host: <your-ip-address>
      username:
        from_secret: ssh_username
      password:
        from_secret: ssh_password
      port: 22
      target: /share/VM/homeautomation/home-assistant
      source:
         - .
    
  - name: update
    image: appleboy/drone-ssh
    settings:
      host: <your-ip-address>
      username:
        from_secret: ssh_username
      password:
        from_secret: ssh_password
      port: 22
      script:
        - /share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker restart homeassistant

Note
- If you use custom components installed using hats or downloading them manually using GitHub, you need to add the also to your test environment.
- In this example I’ve added here_travel_time to show how it works.
- Don’t forget to clean up with rm -rf ... after your test is completed .

steps:
  - name: test
    image: homeassistant/amd64-homeassistant
    commands:
      - echo "Downloading here_travel_time component"
      - wget -O /tmp/here.zip https://github.com/eifinger/here_travel_time/archive/master.zip
      - unzip /tmp/here.zip 'here_travel_time-master/custom_components/here_travel_time/*' -d /tmp/
      - cp -R /tmp/here_travel_time-master/custom_components/here_travel_time ./custom_components/
      - hass -c . --script check_config
      - echo "Cleaning up custom components"
      - rm -rf ./custom_components/here_travel_time

Thats it. If you have any troubles setting up I would love to help.

Here some screenshots of my setup:

Gitea

Drone

2 Likes

I have the exact same thing but go even further. Using the hass-cli, I run some smoke-tests before deploying. These tests include counting the amount of sensors, making sure their values are within certain ranges and making sure that all my light switches are working.

I even thought about changing the color of a RGB lamp if tests are red and only start the actual deploy after I press a physical light switch.

1 Like

Sounds great. It’s some kind of a first step in that direction. Would you mind sharing your setup?
I would love to see the smoke tests.

I use shunit2.

Here are my tests (still basic as I do not have a lot of trouble upgrading). The idea would be that everytime something breaks after an upgrade, I add an extra test.

#! /bin/sh

testCountNbrOfSwitches() {
  nbrOfSwitches="$(hass-cli --no-headers state list 'switch.*' | wc -l)"
  assertEquals ${nbrOfSwitches} 70
}

testAllSwitchesAvailable() {
  statesOfSwitches="($(hass-cli --no-headers --columns=state state list 'switch.*'))"
  assertNotContains "${statesOfSwitches}" "unavailable"
}

testStateSwitch() {
  bergingLichtState="$(hass-cli --no-headers --columns=state state get switch.berging_licht)"
  assertEquals ${bergingLichtState} "off" 
}

testEspSensor() {
  livingRoomTemperature="$(hass-cli --no-headers --columns=state state get sensor.living_room_temperature)"
  actual=`echo "$livingRoomTemperature >= 15" | bc`
  assertTrue "[ $actual -eq 1 ]"
}

testNoDotZeroVersions() {
  hassVersion="$(hass-cli --no-headers --columns=version info)"
  assertNotContains ${hassVersion} ".0"
}

# Load shUnit2.
. ./shunit2.sh
1 Like

I’m honored to see my custom component :blush:

Very cool project, my parents are getting more and more interested in my Smart Home. Maybe I have to manage more than one system too sometime in the future :smiley:

@rdehuyss When in the CI/CD process do you use your script. I assume hass has to be running. If it is running in a test environment how can you make sure it finds all your entities?