Home A-CI-stant
Hi!
For years I lurked in the Home Assistant channel of a community Discord, but never had much interest in it. Earlier this month I had some discount coupons in hand and decided that it was time to give it a go. I got Home Assistant, the ZBT-1 and one IKEA Inspelning and managed to make it control my balcony lights. In my opinion, the sun
“device” in Home Assistant might be the most underrated out-of-the-box integration!
In any case, I live in an apartment, so after that I was mostly done. I don’t have much else to automate, and I do really value physical buttons, so it all sat there for a while. This weekend, while doing some maintenance on my NAS, it got me thinking: what if I use Home Assistant to manage a Continuous Integration server?
I run my own Forgejo instance, and I’ve been meaning to use the Actions feature for a while, but never got around it in my NAS because I don’t want to deal with VMs nor Docker-in-Docker. I also have a fairly decent Optiplex laying around, and while I already had it running a Forgejo Actions runner before, I simply have way to little usage of it to justify have it running 24x7.
So now it finally clicked. The Optiplex has a feature to ATX-power-on the motherboard once it is energized, and combining that with the smart plug, I can power it on just when there is demand. With that, I made a daemon that will wait for Forgejo’s webhooks, and turn on the CI if needed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
m.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
// ...
if payload.Repository.FullName != "canastra.online/monorepo" {
w.WriteHeader(http.StatusNoContent)
return
}
version, ok := strings.CutPrefix(payload.Ref, "refs/tags/")
if !ok {
w.WriteHeader(http.StatusNoContent)
return
}
isOn, err := hassIsOn(hassEntityId)
// ..
if isOn {
w.WriteHeader(http.StatusNoContent)
return
}
err = hassTurnOn(hassEntityId)
// ...
log.Printf("Waking up CI server for %s:%s\n", payload.Repository.FullName, version)
w.WriteHeader(http.StatusNoContent)
})
However, that only addresses half of the issue. Once the server is on, it must be powered off in case it’s not needed anymore. A systemd timer can take care of this, powering off the server 30 minutes after boot. We can abuse the runner’s shutdown_timeout to make systemd wait for the running jobs to complete.
1
2
3
4
5
6
7
8
9
10
11
12
# forgejo-runner.service
[Service]
TimeoutStopSec=30m # same as your runner's shutdown_timeout
# hass-ci.service
[Service]
ExecStart=systemctl stop forgejo-runner.service && systemctl poweroff
TimeoutStopSec=30m
# hass-ci.timer
[Timer]
OnBootSec=30m
Check out the complete code, MIT licensed.
The last piece of the puzzle is a Home Assistant automation that will turn off the plug once the server is completely off.
And that’s about it. Granted, it’s totally unnecessary, I could’ve just used Wake-Up on LAN, but it was the perfect project for me to dig into Home Assistant and tell myself I need just one more smart plug.
Thank you!