Skip to content

Feature list ​

Everything Walis does today, grouped by what you would go looking for. Each line is a feature that exists in the shipping build — not a roadmap.

Jobs and scripts ​

FeatureNotes
Two authoring modesScript mode is a shell buffer you edit directly. Flow mode builds the script from actions.
33 built-in actionsEight categories: general, files, Git, Node, .NET, containers, deployment, checks. See Actions.
Custom actionsA parameter declaration plus a shell template, stored as JSON. No code.
Compiled, not interpretedA flow compiles into Jobs.Command on save, so runners never need to know actions exist.
AI script reviewSends the script to your configured provider and reports back what looks wrong.
Import and exportJobs and custom actions as JSON, matched by name, with an overwrite switch. Identifiers and build history stay put.
Import from JenkinsPaste a Jenkinsfile (declarative or scripted) or a job's config.xml; Walis converts it to a draft with a warning list and opens it in the editor. Nothing is written until you save. See the mapping table.
Build parametersUp to 50 per job: string, text, choice, boolean, password. Manual runs prompt; schedules use the defaults. Unknown names are rejected, not ignored.
Server-side listsPaging, sorting, searching and filtering run in SQL, so the job list behaves the same at thirty jobs and at three thousand.

Switching from flow mode back to script mode keeps the compiled script; switching the other way restores the steps. Manual edits made in script mode are not preserved across a switch back to flow.

Builds and steps ​

FeatureNotes
Per-step statusEvery step keeps its own status, output, exit code and duration.
Independent shellsSteps share a working directory but not environment variables. Append KEY=VALUE to $WALIS_ENV_FILE to pass values on.
Step dependenciesA flow is a directed acyclic graph: a step declares what it depends on, and anything not waiting on anything else runs at the same time — up to 8 steps per build. List view or graph view.
Step outputsA step writes name=value to $WALIS_OUTPUT_FILE; later steps reference it as ${{ steps.ID.NAME }}. Values are injected as environment variables, never spliced into the script text. Recorded on the build and shown in the log.
Run conditions on dataAn expression over earlier steps' outputs and the environment decides whether a step runs. Two branches with opposite conditions off one parent is an if/else.
Run conditionsEach step runs only on success (default), always, or only after a failure — Jenkins' post {}. A failure no longer aborts the build: cleanup and notification steps still run, ordinary ones are marked Skipped, and the result is still Failed.
Per-step retries0–10 further attempts, each a fresh shell with the earlier output kept above it. Retries draw on the job timeout rather than extending it.
One build at a timeJenkins' disableConcurrentBuilds(). The gate lives in the same transaction as the Queued → Running move, so two nodes racing cannot both win; the loser re-queues instead of dropping the build.
Downstream jobsUp to 20 targets per job, on success / on failure / always, matched by name. The downstream build gets WALIS_UPSTREAM_JOB, _BUILD and _STATUS. Chains stop at ten deep, and the upstream job's run role gates the trigger.
Build matrix{"NODE":["20","22"],"OS":["mac"]} fans out into one build per combination, capped at 50.
Build cacheNamed paths are saved on success and restored before the next run — after the workspace is cleaned, not before.
ArtifactsCopied out on success, capped at 200 files / 64 MB, downloadable from the log window.
Live outputstdout and stderr interleave in real order; the log window re-fetches every two seconds and follows the tail until you scroll up.
TimeoutA whole-build budget. Each step receives whatever time is left.
RetentionBuilds, artifacts and audit entries share one retention setting, swept hourly.

continueOnError on an action lets the build carry on past a failure without the build being marked failed at all.

Triggers ​

  • Manual — a button on the job list and job detail.
  • Push — POST /api/webhooks/push accepts GitHub, GitLab and a plain {repository, branch} body, matched against jobs by branch and normalised repository address. Signatures verified via X-Hub-Signature-256, X-Gitlab-Token or X-Walis-Token once a webhook secret is set.
  • Cron — five-field expressions evaluated once a minute in the configured time zone.

These are mutually exclusive — a job has one trigger kind. Running a job by hand is separate and always available, so manual really means "no automatic trigger", not "the only way to start it".

Runners ​

FeatureNotes
Built-in local runnerRuns in the API process. Concurrency comes from the system setting.
Remote agentswalis-runner polls, claims work, and reports back. No runtime to install — it is a self-contained binary.
Shared step loopBuildStepSequence is the same code locally and remotely; only the sink differs (database vs HTTP).
Labels and slotsFree-form labels, plus a slot count per runner.
HeartbeatReported every minute; runners silent for five minutes are marked offline and can raise a notification.

Databases and high availability ​

ProviderMultiple nodesNotes
SQLiteNoThe default. One file, nothing to run.
PostgreSQLYes
MySQL / MariaDBYes
SQL ServerYesAzure SQL included.

Point several API nodes at one server database and they share the work:

  • Atomic build claiming — two nodes can race for the same queued build; exactly one wins the Queued → Running transition.
  • Lease-based leader election — cron evaluation, offline detection, retention cleanup and scheduled backups run on one node at a time. Leases expire, so a dead holder is replaced without a separate coordination service.
  • Cross-node polling — a build queued on one node is picked up by whichever node has capacity.
  • Per-node recovery — a restarting node recovers only its own interrupted builds.

GET /health reports the provider, the node name and whether the deployment is clustered. Migrating from SQLite is a backup and a restore, because the archive is engine-agnostic. See High availability.

Isolation and build safety ​

Builds run as the same user as the service, so these are the defences that matter.

On every platform:

  • Allowed working directories — when the list is non-empty, a build whose working directory is outside it fails immediately rather than running somewhere unexpected.
  • Environment allow-list — a build does not inherit the service's environment. It receives PATH, USER, LOGNAME, SHELL, locale, temp directories and the TLS certificate variables, and nothing else, so connection strings and service secrets are not there to be read. Names are matched case-sensitively on POSIX, because treating Path as PATH would let a differently-cased secret through.
  • Workspace cleaning — clears directory contents only, and refuses disk roots, home directories and suspiciously shallow paths.

On macOS:

  • Sandbox — with IsolateBuilds on (the default), each step is wrapped in sandbox-exec and cannot read or write the Walis data directory, the database directory, backup destinations, or any path you list.
  • Symlink resolution — sandbox paths are resolved segment by segment, so rules on /tmp and /var/folders actually bite.

Everywhere else, a build runs unisolated by default. sandbox-exec has no equivalent that Walis uses on Windows or Linux, and a remote runner applies no sandbox of its own, so there a script runs with the permissions of the account that runs it. That is the default so that a first build works on every platform. The build log says so in its first lines, and the settings page shows a warning when it applies to the built-in runner.

An administrator can require a boundary instead, with Refuse to run without an isolation boundary in Settings → Build. From then on:

  • The built-in runner on Windows or Linux refuses to start a build: "isolation is required but this runner has no sandbox to apply."
  • A remote runner is handed work only if it is isolated by something outside Walis (a container, a VM), declares so, and an administrator lists its name under Externally isolated runners. A runner claiming external isolation that is not on that list is refused — the header alone is not trusted.

Neither layer is real isolation — same user, same machine. They stop a build script from casually reading the database or credentials. For stronger boundaries, run builds on a dedicated runner under its own account, or in a container your script starts.

Access control ​

FeatureNotes
Optional sign-inOff by default. With it off, everyone is treated as an administrator.
Password storagePBKDF2-SHA256. Sessions live in the database.
One-time passwordsTOTP with backup codes; can be required for all accounts.
Rolesviewer, operator, administrator.
Per-job permissionsEach job has a view role and a run role.
Quiet 404sA job you cannot see returns 404, not 403 — the identifier does not confirm the job exists.
API keyswalis_<prefix>_<124-bit secret>, stored only as a domain-separated SHA-256 lookup hash and shown once. Scoped mcp (the safe default, /mcp only), api, or runner — a runner key is bound to one runner name and can only register, heartbeat, claim and report for it. No API key can touch sign-in, accounts or key lifecycle.

Plugins ​

Action plugins run in Jint, a pure-C# JavaScript interpreter, and return a shell snippet rather than behaviour — so you can read what a plugin will run before installing it.

  • Declared capabilities only — network access is limited to named domains; secrets can only be injected into headers via withSecret, never read by the plugin.
  • Resource ceilings — 10 s timeout, 8 MB memory, 500 000 statements, 64 recursion levels.
  • AI drafting — generated plugins have permissions cleared, type and version forced, and are never auto-installed.
  • Administrator only — installing a plugin means adding code that will run.

Notifications ​

Email, Slack (incoming webhook) and a custom JSON webhook, each independently switchable and testable. Five events: build failed, build recovered, every successful build, build timed out, runner offline. Quiet hours suppress everything except failures. Delivery history is kept.

AI assistance ​

Claude, Gemini and Codex. Claude uses the official Anthropic SDK; the other two use their REST endpoints. Used for script review, build failure analysis and plugin drafting. Every call is logged with provider, model, purpose, duration, token usage and result — prompt content is never stored.

Backup and restore ​

  • Scheduled by cron, encrypted with an authenticated format (PBKDF2-SHA256, AES-256-GCM chunks) — a modified or truncated archive is rejected before anything is restored.
  • Packs the database (VACUUM INTO), artifacts, extra paths you list, and optionally PostgreSQL via pg_dump.
  • Destinations: local path, S3-compatible (self-signed SigV4), Dropbox, Google Drive, OneDrive. None shell out to an external binary.
  • Restore is database-agnostic and can target a different database engine than the source.

A passphrase is mandatory once a remote destination exists, because the archive contains password hashes and credentials.

Observability ​

  • Dashboard — active jobs, 7-day success rate, running count, failure count, build trend, recent builds, runner load and per-job success rate.
  • Prometheus — GET /metrics, deliberately outside /api so a local agent scrapes a stable path, but still administrator-gated by the authentication middleware. The output is aggregate numbers plus job names.
  • Audit log — jobs, builds, runners, environment variables, settings, notifications and translations, sharing the retention setting.

Integrations ​

  • REST API for everything the UI does.
  • MCP served at /mcp by the API itself, so an agent needs only a URL and a key — nothing installed locally. Source/Walis.Mcp is a stdio bridge for clients that cannot speak HTTP MCP.
  • Portability — export and import jobs and custom actions as reviewable JSON.

Localization ​

Traditional Chinese is the source language; English and Japanese ship as dictionaries, overlaid at load time with any overrides you set. Translations are editable in the app, importable and exportable as JSON, with an auto-translate pass for missing entries.

Deployment ​

deploy/release.sh publishes self-contained binaries — the target machine needs no .NET — for Windows (x64, arm64), macOS (Intel, Apple Silicon) and Linux (x64, arm64, 32-bit arm for Raspberry Pi, and musl/Alpine for both architectures). Each target gets an archive containing the API with the admin UI built in, plus the runner, and the release ships a SHA256SUMS.txt.

On macOS, deploy/install.sh registers two LaunchDaemons: the service (running as an ordinary account, not root) and a restarter watching a trigger file. On other platforms, run the binary under whatever init system you already use. The built-in self-update job pulls, builds, swaps a symlink and touches that file — restarts happen outside the build's process tree, so the build survives long enough to record its own result.

The API serves the admin UI from wwwroot, so the interface and the API share an origin and a port.

Released under the MIT License.