Continuous integration¶
Three things connect Midair to a build system: a command-line client that runs inside a job, a Jenkins plugin around that client, and an incoming webhook that starts a template from outside.
Jenkins¶
The Jenkins plugin holds the servers, resolves the credential for the job that
asks, and puts both into the environment of a block. The work itself is done by
midair-cli inside the job, on the machine that already has the checkout.
See Command-line client.
withMidair('corp') {
sh 'midair analyze --run https://midair.example.com/analysis/edit?id=12 -- make -j8'
sh 'midair push'
}
Inside the block:
Variable |
Source |
|---|---|
|
The URL of the installation. |
|
A username and password credential. |
|
A secret text credential. |
The client exchanges the username and the password for a session at the start of each run, and it keeps the session nowhere. A build agent is shared, and a credential left in its keyring outlives the job that needed it.
Jenkins masks the secrets in the console and marks them sensitive in the environment view of the build.
A secret text credential that holds a Midair API token becomes
MIDAIR_TOKEN, and that is what a build agent should hold. A token expires
on its own schedule, is revoked on its own, and may do only what its scopes
name. A password can do everything that the account can do, for as long as it
lasts. See API tokens and the HTTP API.
The plugin checks the shape of the credential when the block opens. A token
reads as mid_<id>_<secret>, so a secret text credential that holds
something else is refused there, with the reason.
Configure the servers under Manage Jenkins → System → Midair: one entry per
server, with a name, a URL and a credential. The name is what
withMidair('name') asks for.
Test connection asks the server for its state. That address takes no credential. The answer says whether the host is a Midair server, whether the controller reaches it, and whether it is busy.
The same configuration in Configuration as Code:
unclassified:
midair:
installations:
- name: "corp"
serverUrl: "https://midair.example.com"
credentialsId: "midair-corp"
The plugin is early. withMidair works with either kind of credential, and
the configuration form checks the server. The client still has to be on the
PATH of the agent. Nothing in the plugin waits for a run or reads its
result; the exit code of the client is the verdict.
Incoming webhooks¶
A webhook is an address that Midair publishes, and a token that opens it. A call to it starts one thing: a test template, or an analysis. That is the whole configuration surface. The case that this serves is “CI finished a build, run the nightly suite”.
Configure the list under Settings, then Integrations, then Webhooks. Each entry names what it starts:
[
{"name": "CI nightly", "enabled": true, "action": "run_test",
"target": 12, "token": "…"}
]
The token is the credential. Midair compares it in constant time. An entry with no token is refused, and not treated as open.
A webhook starts a run in the same way as the button on the screen: the same fork, the same validation, the same failure handling.
Midair writes the hook definitions into a Horizon installation on the same server, and asks Horizon to reload them. Horizon verifies the caller, runs the command, and keeps the logs and the run history. Midair finds the Horizon installation by reading its settings file, so nothing about Horizon is configured twice.
The only setting that Midair needs for this is its own Public URL. Horizon calls back to it, and the hook addresses are built from it.
Instance state¶
GET /system/state says what the instance is doing. It takes no credential.
An orchestrator holds no account here. A probe that must be provisioned first
is a probe that is absent when somebody wants it.
The answer carries counts and nothing else. It names no run, no workspace and no device.
{
"succeeded": true,
"checked_at": 1789045678,
"busy": true,
"busy_domains": ["testing"],
"driven_here": 1,
"testing": { "busy": true, "unsettled": 2, "driven_here": 1 },
"analysis": { "busy": false, "unsettled": 0, "driven_here": 0 },
"runtime": { "busy": false, "moving": 0, "up": 2 }
}
The answer reports two readings of “busy”, because they answer two questions.
busyThe cautious reading. A record says that it has not finished, or this process is working on it.
driven_hereThe narrow reading. How much of that work this process drives, which is what a restart would interrupt.
The two differ after a restart. A run that was in progress when the server
stopped still says running, and nothing drives it.
A run is work while its state is queued, running or analyzing. A
template is never work. A device is work while it is building, starting
or stopping; a device that is merely active is not.
Each call scans the three collections. Poll it on the order of seconds, and not of milliseconds.
Badges¶
A workspace publishes three status badges for a README, a wiki page or a dashboard. See Badges.