wakeci is an automation tool which helps to execute repetitive tasks
- simple job configuration using YAML files with Ansible inspired syntax
- easy to install - just download a binary file from Releases
- automatic Let's Encrypt SSL certificates
- parameterized builds, artifacts, intervals and timeouts - see job configuration example below
- single sign-on (SAML, e.g. Google Workspace) next to the password, with read, run and write permissions per group
- API keys limited to reading, running or editing jobs, and to the jobs you pick, with their use per day
- an MCP server, a guide and a Markdown API reference for AI agents
- an audit log of who signed in and who changed what
- updates itself from the settings page, waiting for running builds
- no plugins, no extensive configuration - focus on your project instead!
See full description here
desc: Build and release wake application
params:
- VERSION: master
tasks:
- name: Clone repository
run: git clone git@github.com:jsnjack/wakeci.git --recursive
- name: Checkout version
run: sh ${WAKE_CONFIG_DIR}utils/checkout.sh wakeci ${VERSION}
- name: Install npm dependencies
run: cd wakeci/src/frontend && npm install
- name: Build application
run: cd wakeci && make build
- name: Create a release on github
run: python ${WAKE_CONFIG_DIR}utils/release_on_github.py -f wakeci/bin/wakeci -r jsnjack/wakeci -t "v`cd wakeci && monova`"
timeout: 10m
on_failed:
- name: Send notification to Slack
run: >-
python ${WAKE_CONFIG_DIR}utils/notify_slack.py
-t "Job ${WAKE_JOB_NAME} has failed at '${WAKE_FAILED_TASK_NAME}' <${WAKE_URL}build/${WAKE_BUILD_ID}|#${WAKE_BUILD_ID}>"
-k error
on_finished:
- name: Send notification to Slack
run: >-
python ${WAKE_CONFIG_DIR}utils/notify_slack.py
-t "New wake version `cd wakeci && monova` <${WAKE_URL}build/${WAKE_BUILD_ID}|#${WAKE_BUILD_ID}>"
-k okUsage of ./bin/wakeci:
-V Print the version and exit (shorthand).
-c string
Configuration file location (shorthand) (default "Wakefile.yaml")
-compactdb
Reclaim space in the database which is no longer used
-config string
Configuration file location (default "Wakefile.yaml")
-d Debug-level logging on stderr (shorthand).
-debug
Debug-level logging on stderr.
-import-settings string
Import a settings file, as the settings page downloads it, into the database of a stopped server, then exit
-trace
Trace-level logs to /tmp/wakeci.log (truncated each run).
-version
Print the version and exit.
# Port to start the server on (default "8081")
port: 8081
# Hostname for autocert (active only when port is 443) and the default
# single sign-on URLs
hostname: ""
# Working directory (default ".wakeci/")
workdir: ./wakeci
# Configuration directory - all your job files (default "./"). Every .yaml file
# in it can be opened as a job, so keep this file and the secrets file outside it
jobdir: ./jobs
# Path to the file with secrets
secretsfile: ./secrets.yaml
# Scheduled jobs (via `interval` field) will use this timezone, if not specified
# in the job configuration
timezone: Europe/Amsterdam
# Optional single sign-on through a SAML identity provider such as Google
# Workspace, next to the password. See "Single sign-on" below.
sso:
# Metadata file downloaded from the identity provider. Keep it outside jobdir
idpmetadata: ./GoogleIDPMetadata.xml
# The groups that may sign in are added in the settings, not here
# Where the identity provider posts the response.
# Defaults to https://<hostname>/auth/sso/acs
acsurl: ""
# Identifies wakeci to the identity provider. Defaults to https://<hostname>
entityid: ""
# How long a stopping wakeci waits for running builds before it aborts them
# (default 30m). See "Restarts and updates" below.
draintimeout: 30m
Wakefile.yaml holds what wakeci needs to start; a change needs a restart. What you change while it runs is on the settings page instead (see Settings), and nothing is in both.
The settings page (the gear icon) holds what you change while wakeci runs: the password, how many builds run at once and how many are kept, the API key limits, and, with the admin password, the single sign-on groups. A change applies at once and is in the audit log.
The Server card at the bottom shows what Wakefile.yaml set, read-only: the file's path, port, hostname, directories, the secrets file with how many secrets it holds (never their values), time zone, drain timeout and single sign-on. It warns 30 days before the identity provider's certificate expires.
With the admin password, "Settings file" downloads the settings as YAML, for a backup, review in git or another server. The password and API keys are never in it:
concurrentBuilds: 2
buildHistorySize: 200
apiKeyRate: 10
apiKeyBurst: 120
ssoGroups:
- name: Developers
scopes: [read, run, write]
allJobs: true
- name: Service Team
scopes: [read]
jobs: [deploy]
patterns: [test_*]
ssoNoGroup: nullImport a file on the same card, or on a stopped server, also before its first start:
wakeci --config Wakefile.yaml --import-settings settings.yamlAn import sets what the file has and leaves the rest: a file with only
ssoGroups changes only the groups, and they replace all groups
(ssoGroups: [] removes them all; an empty ssoGroups: is refused);
ssoNoGroup: null turns off "People in no group". The whole file is checked
first, so a file with a mistake changes nothing.
When wakeci gets SIGTERM or SIGINT, it stops starting builds and waits for
the running ones to end. Builds still running after draintimeout are
aborted, so their on_aborted and finally tasks run, with up to two more
minutes for them. Queued builds, and builds started while wakeci waits, stay
in the queue and start after the restart. Their on_pending tasks don't run
again. A second signal aborts the running builds at once, and a third one
stops waiting for their on_aborted and finally tasks. Before it exits,
wakeci aborts any task that still runs and waits up to 10 seconds for it, so
no task outlives it; further signals don't cut this short.
Builds that were running when wakeci was killed are shown as aborted after the
restart. Sessions survive restarts, and open pages reconnect on their own; after
an update they reload to get the new version.
Under systemd, let wakeci send the signal to its builds itself and give it time to wait for them, as in config/wakeci.service:
KillMode=mixed
# Longer than draintimeout plus 3 minutes
TimeoutStopSec=35min
systemctl restart then blocks until wakeci stopped; add --no-block to
return at once.
The card at the top of the settings page shows the running version and asks
GitHub for the latest release, at most every 10 minutes. If it is newer,
"Update to " downloads it, checks its sha256 digest and that it runs,
replaces the binary and keeps the old one as wakeci.previous; if it can't
keep it, the update fails and nothing changes. Then wakeci stops as on
SIGTERM and starts the new binary in the same process. A SIGTERM or SIGINT
while it waits makes it stop instead, and the new binary runs when wakeci
starts again. If a task still runs that wakeci can't stop, for example one
started with sudo, it exits with status 1 instead of restarting, so systemd
ends the task and starts the new binary. To roll back, move wakeci.previous back and restart. wakeci can't update itself if
it can't write to the directory of its binary, or if the binary has file
capabilities from setcap, which a new file wouldn't have; use
AmbientCapabilities=CAP_NET_BIND_SERVICE in the systemd unit to listen on
port 443 instead.
To set it up with Google Workspace, add a custom SAML app in the Google Admin console (Apps, Web and mobile apps, Add app, Add custom SAML app):
- Download the IdP metadata and point
sso.idpmetadataat the file. - Service provider details: ACS URL
https://<hostname>/auth/sso/acs, Entity IDhttps://<hostname>, Name ID formatEMAIL, Name IDBasic Information > Primary email. - Attribute mapping: under "Group membership", pick the groups and set the app
attribute to
groups. Add the same group names in wakeci's settings. - User access: turn the app on for those groups.
Only accounts of your own Workspace can sign in to a custom SAML app, and of those only members of a group listed in wakeci's settings can log in, unless you allow people in no group (below); a rejected login logs the groups wakeci received. With the admin password, the settings page lists the groups and what their members may do: read, run or write, on all jobs or on selected jobs and name patterns, as for API keys. Someone in several groups gets the permissions of all of them, and a change applies to their next request. Until the admin adds a group, nobody can sign in with SSO, and wakeci says so in a warning at startup. Opening wakeci from the Google apps menu starts a normal login. SSO sessions last 12 hours, password sessions 5 days. API calls use an API key (see below) or Basic auth with the password.
"People in no group", at the end of the groups, is off by default. When you
allow it, anyone the identity provider lets in whose groups match none in
the settings, or who has no groups, signs in with the permissions you give
it. Members of a listed group get only their groups' permissions. It can
give write, but then everyone your Google app lets in can run commands on
the server.
Serve wakeci over HTTPS, or on localhost: Google posts the response from
another site, which browsers only allow with a Secure login cookie. When
Google renews its certificate, download the metadata again and restart
wakeci; the Server card in the settings warns 30 days before it expires.
Agents such as Claude Code, Codex or Copilot can use wakeci through MCP at
https://<hostname>/mcp, with an API key. The tools find builds, summarize
why one failed, read logs and text artifacts, wait for a build, and run or
abort jobs, within the key's scopes and jobs:
claude mcp add --transport http wakeci https://<hostname>/mcp \
--header "Authorization: Bearer wk_..."The Help page's MCP tab gives this command for Claude Code, Codex, Copilot CLI and VS Code (with an install link), filled in with a key you paste there, and lists the tools with their inputs and scopes. A tool call counts as one request of the key.
/llms.txt is a one-page guide to the API for agents that don't use MCP:
how to authenticate, find out why a build failed, wait for builds, follow
the feed and read logs. /llms-full.txt (also /docs/api.md) is every
route as Markdown, built from the API spec, for agents that can't run the
docs page's JavaScript. Neither needs a key.
Scripts and other tools can call the API with a key instead of the password. Create one on the API keys page (the key icon), choose what it can do and on which jobs, and send it as a Bearer token:
curl -X POST -H "Authorization: Bearer wk_..." https://<hostname>/api/job/deploy/run| Scope | Allows |
|---|---|
read |
the job list, job files, the feed, builds, task logs and artifacts |
run |
running jobs, and aborting, starting or flushing their builds |
write |
creating, editing, deleting, enabling and disabling jobs |
A key covers all jobs, including jobs added later, or only the jobs you pick
and the ones whose names match a pattern such as test_*. write always
covers all jobs: jobs run commands on the server, so a key that can edit one
can reach everything wakeci can. The job list and the feed only show the key's
jobs. Keys never open the settings, key management, login or the websocket.
Run parameters sent with a key, or by someone who signs in with SSO and
may not edit jobs (write), can't reference a secret ({{ secrets.NAME }})
or contain quotes or line breaks, and in the builds they start, build.env
values aren't turned into secrets. Whoever may run a job still chooses its
parameters, so treat them as untrusted input: quote them, and don't let one
pick where a job sends a secret, such as the URL a token is posted to.
A request with a missing, wrong or expired key, password or session gets
401; a request for something the key or the user may not do gets 403. A
key may send bursts of 120 requests and 10 a second after that, unless you
change it in the settings; beyond it it gets 429 with Retry-After. The
API keys page shows when each key was last used, how many requests it
made, and its requests per day over the last 30 days, so you can see
whether anything still uses a key before you revoke it.
The feed (GET /api/feed) returns 15 builds, newest first, or up to 200
with limit. A script that keeps up with new builds remembers the newest
build ID it has and asks for ?after=<id>&limit=200; before=<id> pages
back from the oldest ID it got. job and status keep only builds of those
jobs or with those statuses, by exact value, and q searches. An unchanged
page answers If-None-Match with 304. offset and filter still work but
are deprecated: page with before and search with q
(docs/deprecations.md). A build (GET /api/build/<id>)
names its tasks and gives the URL and size of each task's log, and the URL
of each artifact. GET /api/build/<id>/summary answers "why did it fail"
in one request: the status, the tasks, the artifacts and the last 100 lines
(tail) of each task that failed, timed out or was aborted.
GET /api/build/<id>/log/<task> returns a task's log, its last lines
(tail=200) or the lines that contain a text (grep=FAILED, with line
numbers). Both build routes take wait=<seconds> (at most 300): the request
answers as soon as the build ends, so a script that waits for a build
needs one request every 5 minutes instead of polling. GET /api/whoami
tells a key or a login what it may do, and which server answers, with the
range of build IDs it holds. Files of a build, such as task logs and
artifacts, answer a byte range: Range: bytes=-65536 gets the end of a log.
The key is shown once, when it is created; wakeci only stores a hash of it. Revoking a key stops it at once. Keys don't depend on the password, and survive restarts.
Each key records who created it. Logged in with the admin password, the API
keys page shows every key and who made it, and can revoke all keys of a
person, which also signs them out. People who sign in with SSO only see and
revoke the keys they created, and only the admin password can change the
password or the groups. A key made by someone who signs in with SSO can't do
more than their groups allow at the time of each request, so a key on all
jobs means all jobs their groups allow. Only give write to groups you
trust: editing a job runs commands on the server.
wakeci can't tell on its own that a Google account is gone, so when someone leaves:
- Suspend or delete their Google account, so they can't sign in again.
- On the API keys page, logged in with the admin password, find their section (keys are grouped by who made them, people signed in without keys are listed too, and a search appears when there are many) and click "Revoke access". That signs them out, closes their live updates and revokes their keys.
- If they knew the admin password, change it. That ends every other session logged in with the password; sessions survive restarts, so a restart doesn't. Then revoke the keys under "Made with the admin password", since those keys cannot be attributed to a person and survive password changes and restarts.
Default password is
admin. Don't forget to immediately change it!
wakeci writes one line for every sign-in and every change, saying who did
what, from where and whether it worked. The lines go to stderr with the other
logs, whatever --debug says, and start with msg=audit. Under systemd,
journalctl -u wakeci | grep msg=audit shows them; ship stderr to your log
system to read several servers together.
time=2026-11-05T11:02:03Z level=INFO msg=audit server=ci.example.com event=login.sso outcome=ok auth=sso user=alice@example.com groups="[\"Developers\"]" ip=198.51.100.4 logID=Xk2pQ
time=2026-11-05T11:02:41Z level=INFO msg=audit server=ci.example.com event=build.run outcome=ok auth=sso user=alice@example.com job=deploy build=1234 params="[\"TARGET\"]" status=200 method=POST path=/api/job/deploy/run ip=198.51.100.4 logID=b7RtM
time=2026-11-05T11:05:10Z level=INFO msg=audit server=ci.example.com event=login.sso outcome=rejected user=bob@example.com reason="in no allowed group" groups="[\"Marketing\"]" ip=203.0.113.8 logID=Qe9Zs
| Field | Meaning |
|---|---|
server |
hostname from the config, or the machine's name |
event |
what happened, see below |
outcome |
ok, failed, or rejected for a login refused by group or revoke; status is the HTTP status |
auth |
how the request signed in: password (session from the admin password), sso, basic (Basic auth with the password) or apikey |
user |
the SSO email; for an API key, the email of who created it. Empty for the admin password, which isn't a person |
apiKey |
the ID of the API key used |
ip |
the address the connection came from |
forwardedFor |
the X-Forwarded-For header, if the request had one. Anyone can send it: behind a proxy, ip is the proxy and this is what it passed on; without one, it can be made up. A long header keeps its start and its end, where a proxy adds the address it saw |
logID |
the same ID as the request's request completed line |
| Events | |
|---|---|
login.password, login.basic, login.sso, login.apikey |
sign-ins, including ones the server failed to check; failures other than a wrong password have a reason, SSO logins the groups the identity provider sent, and the failure that blocks an address for too many attempts has blocked=true |
logout |
|
build.run, build.abort, build.start, build.flush |
with job and build (an abort, start or flush only names the job for a request that signed in); a run lists the job parameters it set in params (names only) and how many other names it sent in ignoredParams, and a scheduled one has trigger=interval and no user |
job.create, job.edit, job.delete, job.set_active |
with job, and active (true, false or invalid) for job.set_active; a create that failed after writing the job file has fileCreated=true, since the file can still be added as a job |
settings.change |
the new numbers (invalid for anything else), including apiKeyRate and apiKeyBurst when sent, and passwordChange=true if the password was sent; a changed password has endedSessions, the other password sessions it signed out |
settings.import |
which settings the file set, in applied; from the command line also via=--import-settings and the file |
group.create, group.change, group.delete |
the group and, except for a delete, its scopes, allJobs, and otherwise its jobs and patterns |
group.nogroup.set, group.nogroup.delete |
"People in no group" allowed or changed, with its scopes and jobs as for a group, or turned off |
server.update |
the running version in from, and the release it installs in to |
key.create, key.revoke, access.revoke |
the key, its scopes, allJobs, and otherwise its jobs and patterns as JSON lists; revoking someone's access names the person and how many sessions and keys it ended. Sessions end first, so a revoke that failed may still have sessions |
Refused attempts are recorded too, e.g. an SSO user trying to change the
password, or an API key without the run scope trying to run a job. The log
never contains passwords, tokens, secrets or the values of run parameters,
nor parameter names the job doesn't have, which can hold anything (curl -d
with a JSON body sends the whole body as one name).
Values longer than 1024 bytes are cut and end in .... Lists of names
(groups, params, jobs, patterns) are JSON, so a name with a comma
keeps its boundaries; they stay valid within 1024 bytes by leaving names out,
and say how many in groupsOmitted, paramsOmitted and so on. A request
whose handler crashed is recorded as failed with status 500 and panic=true.
Reading isn't an audit event, but the request completed line of every API
call, task log and artifact names who made it the same way. The app's pages
themselves are public files without data, so their lines name no one; the
data a page shows comes from API calls. Requests from an address blocked for
too many failed attempts are only in those lines. Job files changed on the
server's disk, not through wakeci, aren't attributed to anyone.
The Help page's REST API tab, also at /docs/api/, documents every route
with its parameters, answers and API key scope, and can try requests. The
same is at /llms-full.txt as Markdown and at /docs/swagger.json as
OpenAPI 2.0; none of them needs a key.
Requires golang 1.18+
go get golang.org/dl/go1.18.2
/home/$USER/go/bin/go1.18.2 download
# Manage different versions with `alternatives`
sudo alternatives --install /usr/bin/go go /home/$USERNAME/go/bin/go1.18.2 10
# Switch between different go versions
sudo alternatives --config goGolang downloads page https://golang.org/dl/
sudo dnf install entr
# cd src/frontend
npm install# frontend
make runf
# backend
make runbThe Playwright suite runs against isolated production backends in parallel. It requires Google Chrome. Each worker gets a temporary database, job directory, test secrets, and an available port; it does not use your running server.
make testprod # build, then run the full E2E suite
make testprod E2E_ARGS="--workers=4" # choose the worker count
make testprod E2E_ARGS="e2e/feed.spec.js" # run one spec
make testdev # run with visible browsers
make check # full project validation, including E2E
make testreport # open the last HTML report from the repo root
# After building, rerun tests without rebuilding:
cd src/frontend
npm test
npm run test:open # interactive Playwright UI
npm run test:report # inspect the last HTML reportThe default worker count is half the available CPUs, capped at ten. On a Ryzen AI 9 HX PRO 370 with 24 CPU threads, ten workers had the fastest measured median (13.7 seconds over nine full-suite runs); twelve took 15.3 seconds and twenty-four took 17.9 seconds.
Tests retain screenshots, browser traces, and backend logs on failure in
src/frontend/test-results/. To use Playwright's Chromium instead of Chrome,
install it explicitly with npx playwright install chromium and run with
E2E_BROWSER_CHANNEL=chromium.
npm outdated --json | jq -r 'keys[]' | xargs -I % npm i %@latest --saveGOWORK=off go-mod-upgrade