Skip to content
jsnjackPublic

Repository files navigation

wakeci

What is it?

wakeci is an automation tool which helps to execute repetitive tasks

ScreenShot

Features

  • 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!

Job configuration example

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 ok

How to use it?

Usage 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.

Wakefile.yaml format

# 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.

Settings

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: null

Import a file on the same card, or on a stopped server, also before its first start:

wakeci --config Wakefile.yaml --import-settings settings.yaml

An 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.

Restarts and updates

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.

Single sign-on

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):

  1. Download the IdP metadata and point sso.idpmetadata at the file.
  2. Service provider details: ACS URL https://<hostname>/auth/sso/acs, Entity ID https://<hostname>, Name ID format EMAIL, Name ID Basic Information > Primary email.
  3. Attribute mapping: under "Group membership", pick the groups and set the app attribute to groups. Add the same group names in wakeci's settings.
  4. 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 (MCP)

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.

API keys

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:

  1. Suspend or delete their Google account, so they can't sign in again.
  2. 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.
  3. 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!

Audit log

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.

API documentation

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.

Development

Requires golang 1.18+

How to install 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 go

Golang downloads page https://golang.org/dl/

Install dependencies

sudo dnf install entr

# cd src/frontend
npm install

Start application

# frontend
make runf

# backend
make runb

End-to-end tests

The 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 report

The 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.

Update all outdated npm packages

npm outdated --json | jq -r 'keys[]' | xargs -I % npm i %@latest --save

Update all outdated go packages

GOWORK=off go-mod-upgrade

Releases

Packages

Used by

Contributors

Languages