Appcircle.io self-hosted scripts repository that has helper scripts for installation, upgrade, or runtime.
This script was made for downloading, validating and extracting the Appcircle runner VM image and Xcode image tar.gz files in the background.
The curl command under the Download MacOS VM can exit if user closes the SSH session.
The download and unzip takes so much time. So this script handles manual tasks for the user.
For detailed usage, check the docs.
By default the images are downloaded from https://storage.googleapis.com/appcircle-dev-common/self-hosted. To download them through a mirror of that location, such as an artifact proxy on a host without internet access, set AC_RUNNER_IMAGE_BASE_URL:
AC_RUNNER_IMAGE_BASE_URL="https://nexus.example.com/repository/appcircle-gcs" ./download-runner.sh 260914- The image version is required with a mirror, because the latest image is found through the Google Cloud Storage API.
- Credentials for the mirror are read from
~/.netrcwhen it has an entry for the mirror host. - Each image is verified with the MD5 checksum in the
<file>.md5file next to it. With the default location the script falls back to the MD5 checksum in the object'sETagwhile the.md5files are not published. With a mirror there is no fallback, because a mirror serves its ownETag.
You can use that script to download the latest Appcircle server package.
Save the script to a directory.
Save the cred.json file to the same directory.
Run the script with no argument.
./download-server.shThis will download the latest and licensed Appcircle server for your cred.json file.
Applies a security baseline to the physical macOS host that runs the Tart VMs,
based on a subset of the CIS macOS Level 1 benchmark. Audit-only and dry-run
modes are available. Published as harden-macos-host.sh (see Publishing).
For detailed usage, check the docs.
The ephemeral runner loop for a Tart macOS VM. Clones the base VM, runs the
clone until the build finishes, deletes the clone and repeats. Started either
manually or, preferably, by runner-service.sh.
Beyond the loop it prepares the host for an unattended start: waits for tart
and for outbound networking to become usable at boot, removes clones orphaned
by a crash or a hard stop, and unlocks the login keychain.
That last step is what lets a runner survive a host reboot with nobody logged
in. On macOS 15 and newer, tart cannot read the Virtualization.framework
HostKey unless the runner account's login keychain is unlocked, and fails with
VZErrorDomain Code=-9 otherwise. Put the keychain password in a mode-600 file
owned by the runner account and run.sh performs the unlock itself:
mkdir -p ~/.appcircle && chmod 700 ~/.appcircle
printf '%s' '<keychain-password>' > ~/.appcircle/runner-keychain.pw
chmod 600 ~/.appcircle/runner-keychain.pwOverride the path, never the secret, with KEYCHAIN_PW_FILE. Without the file
the unlock is skipped, which is correct on macOS 13 and when a desktop session
has already unlocked the keychain.
Installs run.sh as a per-runner launchd system daemon so the runners start at
boot and are unaffected by an SSH session being closed. The service label is
derived from the runner folder name, so $HOME/runner1 becomes
io.appcircle.runner1 and several runners on one host get independent
services.
sudo ./runner-service.sh install <vm-name>
sudo ./runner-service.sh stop [--now] [--disable]
./runner-service.sh status
./runner-service.sh logsRead-only readiness check for a runner host. Reports PASS, WARN or FAIL per item and exits non-zero on any failure. Covers sizing, MDM policy and password rotation, desktop session and keychain state, auto-login and FileVault, power settings, automatic updates, security posture, corporate root CA trust and TLS interception, egress reachability, Tart and VM images, and runner supervision.
Run it before opening a support ticket about a runner that will not start.
The scripts are served to customers from https://cdn.appcircle.io/self-hosted/
and the documentation links to that URL. Publishing is manual, so the mapping
below is the only record of which repository file becomes which public file.
Keep it accurate.
| Repository file | Published as |
|---|---|
download-runner.sh |
https://cdn.appcircle.io/self-hosted/download-runner.sh, and the same file as download-runner-beta.sh while older docs and customer automation still use that name |
harden-host.sh |
https://cdn.appcircle.io/self-hosted/harden-macos-host.sh |
run.sh |
https://cdn.appcircle.io/self-hosted/run-<VERSION>.sh |
runner-service.sh |
https://cdn.appcircle.io/self-hosted/runner-service.sh |
check-runner-host.sh |
https://cdn.appcircle.io/self-hosted/check-runner-host.sh |
download-server.sh |
https://cdn.appcircle.io/self-hosted/download-server.sh |
download-server.sh is the script the server installation and update docs run
with curl ... | bash. It must keep working with the stock tools of Linux and
macOS (no grep -P, no tac, bash 3.2 syntax); ./tests/download-server-tests.sh
checks that on both.
Two of these rows are renames rather than plain copies, and nothing enforces either one, so verify the published URL after every upload.
harden-host.sh is published under a different name than it has here. This was
recorded nowhere before, and cdn.appcircle.io/self-hosted/harden-host.sh
returns 404 while harden-macos-host.sh serves the file.
run.sh is published per version, not under a fixed name, because a given
run.sh is only guaranteed to work with the macOS VM image it shipped with.
Take VERSION from the top of the script, so 1.9.0 is published as
run-1.9.0.sh, and add a tab for it in the
macOS VM setup
page next to the image version it belongs to. A bare run.sh also exists on
the old storage bucket, pinned at 1.0.2 and reachable by hosts installed before
this convention; leave it in place, it is not the current download path.
The images are not built from this repository, but download-runner.sh depends on how they are published. Upload an MD5 checksum file next to every macOS_<version>.tar.gz and xcodes_<version>.tar.gz in gs://appcircle-dev-common/self-hosted/, in the format md5sum prints (<checksum> <file name>). The latest-image lookup (no image version given) matches only macOS_*.tar.gz and xcodes_*.tar.gz since 1.2.1; up to 1.2.0 it matched every name starting with macOS or xcodes and picked a .md5 file once they were published. Cloud Storage already keeps the MD5 of each object, so the file can be written from the object metadata without downloading the image:
B=gs://appcircle-dev-common/self-hosted
f=macOS_260914.tar.gz
printf '%s %s\n' "$(gcloud storage objects describe "$B/$f" --format='value(md5_hash)' | base64 -d | xxd -p)" "$f" > "$f.md5"
gcloud storage cp --no-clobber --content-type=text/plain "$f.md5" "$B/"Without these files the script cannot verify downloads made through a mirror (AC_RUNNER_IMAGE_BASE_URL), and it stops.
- The tests should run in a clean environment.
- You can test overall functionality with bash script tests.
- You can see all the tests inside the
testsfolder. - The ones with names ending with
*Test.shfiles are the test cases that are waiting to run. - For example, to run tests for the
download-runner.sh, you can simply call it:
./tests/download-runner-tests.sh
⚠️ Warning: The test scripts must be executed from the repo's root directory. For instance,./tests/download-runner-tests.sh. The command running inside thetestsfolder as./download-runner-tests.shwill not work.
- You should see an output like:
You can see the outputs in ./tests/reports/test-6514-23621 folder
testRunnerDownload
test-6514-23621 test starting
Tests finished.
Ran 1 test.
OK- If you face any errors while testing and want to see the outputs, please check the
./tests/reportsfolder. - Test outputs are written to that directory with the name of test id.
download-runner-mirror-tests.shrunsdownload-runner.shagainst a local mirror (tests/mirror-server.py) and needs no internet access, only macOS andpython3:- testMirrorDownloadWithChecksum: downloads, verifies with the
.md5files and extracts through the mirror; a trailing slash in the URL is accepted. - testMirrorRequiresVersion: fails before downloading when no version is given.
- testMirrorChecksumMismatch: fails when the checksum does not match.
- testMirrorMissingChecksumFile: fails when the mirror does not serve the
.md5file. - testMirrorWithNetrcCredentials: fails without credentials and succeeds with them in
~/.netrc. - testMirrorUrlHidesCredentials: credentials in the URL work and are not printed.
- testMirrorDownloadWithChecksum: downloads, verifies with the
download-runner-tests.sh- testRunnerDownload
- Valid remote MD5 for macOS images should be found.
- MD5 check for macOS image should be successful.
- Valid remote MD5 for Xcode images should be found.
- MD5 check for Xcodes image should be successful.
- MacOS image should be extracted successfully.
- MacOS image directory should be found in the "$HOME/.tart/vms" directory.
- Xcode images should be extracted successfully.
- Xcode images directory should be found in the "$HOME" directory.
- Xcode images directory should contain some dmg files.
- Script output should container success log.
- testRunnerDownloadWithNonExistingVersion
- Script output should contain 404 logs 6 times.
- Script output should contain fail log.
- testRunnerDownload