Skip to content
Gauge

ax-check.com

Semaphore

semaphore.io

ax-check.com

Checked 22 October 2026

C

63/100

Agents need your login

Docs and pricing are clear, but the llms.txt index skips them

Pricing, quickstart and CLI install steps are all readable without login, but the llms.txt file lists only blog and marketing pages, omitting docs, API and MCP links.

5 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a text/markdown request; no Markdown representation offered.

Docs

Add links to your product documentation and quickstart in /llms.txt.

llms.txt exists but lists only blog, marketing and customer pages, no docs, API, CLI or quickstart links.

Docs

Tell agents where to start and organize the links in /llms.txt.

llms.txt groups pages by marketing type (Pages, Posts, Customers) with no starting guidance for developers.

Docs

List every offered API, MCP, and skills surface in /llms.txt.

llms.txt omits the offered MCP server, API reference and sem-ai CLI surfaces.

Recorded coding sessions

Same task, 3 models, no semaphore.io account. Recorded 22 October 2026 on Pi.

3 recorded · 4m 35s

4m 35s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Semaphore. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user is asking about Semaphore (semaphoreci.com), i.e., a CI/CD product. Let me first check the environment.

Important considerations: Semaphore is a hosted CI/CD product. "Using the hosted product via SDK or API" — Semaphore has an API. But API access requires an account/token, and I probably don't have one. To build an example, I need to create an organization, and this is probably a paid/authenticated thing.

Let me check the working directory and see if there are credentials.

Also, things to consider: pricing. Semaphore's pricing is free plan + paid tiers. I should tell the truth based on my own knowledge, and it might be fine to check whether network access is available.

"Whether it's easy for me to manage" — a question about agentic operation.

Let me quickly check the environment first.

I'll check the environment first, then look at what Semaphore actually offers.

pwd; ls -la; echo "---"; env | grep -iE "semaphore|SEMA|CI_" | head -50; echo "--- net check"; curl -s -m 8 -o /dev/null -w "%{http_code}\n" https://docs.semaphoreci.com/ 2>&1 | tail -2
/sandbox/repo
/sandbox/repo
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct 22 11:30 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
---
--- net check
301
ls -a ~ 2>/dev/null | head -30; echo "--- git creds"; cat ~/.netrc 2>/dev/null | head; echo "--- pip/npm"; which pip python3 node npm 2>/dev/null
.
.
..
.cache
.gauge
.pi
.tmp
repo
--- git creds
--- pip/npm
/usr/local/bin/pip
/usr/local/bin/python3
/usr/local/bin/node
/usr/local/bin/npm
Thinking…

The network is available (301 redirect). There's an empty git repository. No Semaphore credentials.

To check the state of the API and SDK, let's take a look at Semaphore's documentation.

Network works, repo is empty, no credentials. Let me check what Semaphore's API/SDK surface actually is.

cd /tmp && for u in https://docs.semaphoreci.com/api/ https://docs.semaphoreci.com/api/overview/ https://docs.semaphoreci.com/api/projects/; do echo "== $u"; curl -sL -m 10 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read()
t=re.sub(r'(?s)<(script|style|nav|svg).*?</\1>','',t)
t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:1500])
"; done
== https://docs.semaphoreci.com/api/
== https://docs.semaphoreci.com/api/
Semaphore
Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
== https://docs.semaphoreci.com/api/overview/
Semaphore
Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
== https://docs.semaphoreci.com/api/projects/
Semaphore
Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
cd /tmp && echo "== api.semaphoreci.com root"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" https://api.semaphoreci.com/ | head -20; echo "== pypi semaphore sdk"; curl -s -m 10 "https://pypi.org/pypi/semaphoreci/json" | head -c 300; echo; echo "== npm"; curl -s -m 10 "https://registry.npmjs.org/semaphoreci" | head -c 300
== api.semaphoreci.com root
== api.semaphoreci.com root

HTTP:301
== pypi semaphore sdk
{"info":{"author":"Ian Cordasco","author_email":"graffatcolmingov@gmail.com","bugtrack_url":null,"classifiers":["Development Status :: 4 - Beta","Intended Audience :: Developers","License :: OSI Approved","License :: OSI Approved :: Apache Software License","Programming Language :: Python","Programm
== npm
{"_id":"semaphoreci","_rev":"16-3944c9a131bb79fe87defe8f463534a3","name":"semaphoreci","description":"Unofficial SemaphoreCi node api","dist-tags":{"latest":"0.0.14"},"versions":{"0.0.1":{"name":"semaphoreci","version":"0.0.1","description":"Unofficial SemaphoreCi command line tool.","main":"index.j
Thinking…

The documentation structure has changed. Let me find the current API documentation URL and check the api.semaphoreci.com redirect.

Note: The "semaphoreci" package on PyPI is the old Python SDK for Semaphore 1.0/v1 API (made by Ian Cordasco), and it's probably outdated. The npm one is unofficial and old (0.0.14).

Let me look for the actual API documentation.

cd /tmp && curl -s -m 10 -w "\nHTTP:%{http_code}\n" -D- https://api.semaphoreci.com/ -o /dev/null | head -20; echo "== search docs sitemap"; curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -iE "api|reference" | head -40
HTTP/2 301
HTTP/2 301 
date: Thu, 22 Oct 2026 11:30:40 GMT
content-type: text/html; charset=UTF-8
location: https://semaphore.io/
cf-ray: a4e82bd95feb1555-IAD
cf-cache-status: HIT
age: 5
cache-control: public, max-age=0, s-maxage=900
expires: Thu, 22 Oct 2026 10:34:29 GMT
server: cloudflare
ki-cf-cache-status: HIT
x-content-type-options: nosniff
x-edge-location-klb: 1
content-security-policy: frame-ancestors 'self';
content-security-policy-report-only: default-src 'self'; script-src 'self' 'unsafe-inline' l.getsitecontrol.com www.googletagmanager.com region1.google-analytics.com www.google-analytics.com googleads.g.doubleclick.net static.ads-twitter.com cdn.cookiehub.eu eu-assets.i.posthog.com www.loom.com youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com *.semaphoreci.com *.semaphore.io; style-src 'self' 'unsafe-inline' fonts.googleapis.com cdn.cookiehub.eu; img-src 'self' data: secure.gravatar.com s.w.org www.google.com www.google.pt www.google-analytics.com www.googletagmanager.com googleads.g.doubleclick.net static.ads-twitter.com t.co analytics.twitter.com img.youtube.com i.ytimg.com *.cookiehub.eu *.posthog.com *.semaphoreci.com *.semaphore.io; font-src 'self' fonts.gstatic.com data:; frame-src youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com www.loom.com calendar.google.com www.googletagmanager.com; media-src audio.buzzsprout.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.google.com https://*.doubleclick.net https://*.getsitecontrol.com https://*.cookiehub.eu https://*.posthog.com https://*.twitter.com https://t.co; report-uri https://sentry.io/api/4509293704970240/security/?sentry_key=de4512f268813ed97e73abec15d22aab
ki-cache-tag: bae2d4ff-cd68-4ec8-950b-9d3747e2d7c5,ece8363b804a58a0da6158a5ee08041acede658be31c7616e20697e27dfb45a1
ki-cache-type: Edge
ki-edge: v=28.6.0;mv=99.9.9
ki-origin: g1p
x-frame-options: SAMEORIGIN
== search docs sitemap
https://docs.semaphore.io/CE-1.4/reference/agent-aws-stack
https://docs.semaphore.io/CE-1.4/reference/agent-yaml
https://docs.semaphore.io/CE-1.4/reference/api
https://docs.semaphore.io/CE-1.4/reference/audit-events
https://docs.semaphore.io/CE-1.4/reference/conditions-dsl
https://docs.semaphore.io/CE-1.4/reference/dashboard-yaml
https://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml
https://docs.semaphore.io/CE-1.4/reference/env-vars
https://docs.semaphore.io/CE-1.4/reference/jobs-yaml
https://docs.semaphore.io/CE-1.4/reference/machine-types
https://docs.semaphore.io/CE-1.4/reference/notifications-yaml
https://docs.semaphore.io/CE-1.4/reference/openid
https://docs.semaphore.io/CE-1.4/reference/os-apple
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image
https://docs.semaphore.io/CE-1.4/reference/pipeline-yaml
https://docs.semaphore.io/CE-1.4/reference/project-yaml
https://docs.semaphore.io/CE-1.4/reference/resources-yaml
https://docs.semaphore.io/CE-1.4/reference/secret-yaml
https://docs.semaphore.io/CE-1.4/reference/self-hosted-config
https://docs.semaphore.io/CE-1.4/reference/semaphore-cli
https://docs.semaphore.io/CE-1.4/reference/toolbox
https://docs.semaphore.io/CE/reference/agent-aws-stack
https://docs.semaphore.io/CE/reference/agent-yaml
https://docs.semaphore.io/CE/reference/api
https://docs.semaphore.io/CE/reference/audit-events
https://docs.semaphore.io/CE/reference/conditions-dsl
https://docs.semaphore.io/CE/reference/dashboard-yaml
https://docs.semaphore.io/CE/reference/deployment-target-yaml
https://docs.semaphore.io/CE/reference/env-vars
https://docs.semaphore.io/CE/reference/jobs-yaml
https://docs.semaphore.io/CE/reference/machine-types
https://docs.semaphore.io/CE/reference/notifications-yaml
https://docs.semaphore.io/CE/reference/openid
https://docs.semaphore.io/CE/reference/os-apple
https://docs.semaphore.io/CE/reference/os-ubuntu
https://docs.semaphore.io/CE/reference/os-ubuntu-images/ubuntu-2004-image
cd /tmp && curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | head -80
https://docs.semaphore.io/search
https://docs.semaphore.io/search
https://docs.semaphore.io/
https://docs.semaphore.io/CE-1.4/category/choose-your-platform
https://docs.semaphore.io/CE-1.4/getting-started/about-semaphore
https://docs.semaphore.io/CE-1.4/getting-started/changelog
https://docs.semaphore.io/CE-1.4/getting-started/experiments
https://docs.semaphore.io/CE-1.4/getting-started/faq
https://docs.semaphore.io/CE-1.4/getting-started/features
https://docs.semaphore.io/CE-1.4/getting-started/install
https://docs.semaphore.io/CE-1.4/getting-started/install-aws-ec2
https://docs.semaphore.io/CE-1.4/getting-started/install-eks
https://docs.semaphore.io/CE-1.4/getting-started/install-gcompute
https://docs.semaphore.io/CE-1.4/getting-started/install-gke
https://docs.semaphore.io/CE-1.4/getting-started/install-local
https://docs.semaphore.io/CE-1.4/getting-started/install-overview
https://docs.semaphore.io/CE-1.4/getting-started/install-ubuntu
https://docs.semaphore.io/CE-1.4/getting-started/migration-overview
https://docs.semaphore.io/CE-1.4/getting-started/migration/bitbucket
https://docs.semaphore.io/CE-1.4/getting-started/migration/circle
https://docs.semaphore.io/CE-1.4/getting-started/migration/github-actions
https://docs.semaphore.io/CE-1.4/getting-started/migration/jenkins
https://docs.semaphore.io/CE-1.4/getting-started/migration/travis
https://docs.semaphore.io/CE-1.4/getting-started/quickstart
https://docs.semaphore.io/CE-1.4/getting-started/tour/build-project
https://docs.semaphore.io/CE-1.4/getting-started/tour/deploy-project
https://docs.semaphore.io/CE-1.4/getting-started/tour/first-steps
https://docs.semaphore.io/CE-1.4/getting-started/tour/hello-world
https://docs.semaphore.io/CE-1.4/reference/agent-aws-stack
https://docs.semaphore.io/CE-1.4/reference/agent-yaml
https://docs.semaphore.io/CE-1.4/reference/api
https://docs.semaphore.io/CE-1.4/reference/audit-events
https://docs.semaphore.io/CE-1.4/reference/conditions-dsl
https://docs.semaphore.io/CE-1.4/reference/dashboard-yaml
https://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml
https://docs.semaphore.io/CE-1.4/reference/env-vars
https://docs.semaphore.io/CE-1.4/reference/jobs-yaml
https://docs.semaphore.io/CE-1.4/reference/machine-types
https://docs.semaphore.io/CE-1.4/reference/notifications-yaml
https://docs.semaphore.io/CE-1.4/reference/openid
https://docs.semaphore.io/CE-1.4/reference/os-apple
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image
https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image
https://docs.semaphore.io/CE-1.4/reference/pipeline-yaml
https://docs.semaphore.io/CE-1.4/reference/project-yaml
https://docs.semaphore.io/CE-1.4/reference/resources-yaml
https://docs.semaphore.io/CE-1.4/reference/secret-yaml
https://docs.semaphore.io/CE-1.4/reference/self-hosted-config
https://docs.semaphore.io/CE-1.4/reference/semaphore-cli
https://docs.semaphore.io/CE-1.4/reference/toolbox
https://docs.semaphore.io/CE-1.4/using-semaphore/artifacts
https://docs.semaphore.io/CE-1.4/using-semaphore/cache
https://docs.semaphore.io/CE-1.4/using-semaphore/connect-bitbucket
https://docs.semaphore.io/CE-1.4/using-semaphore/connect-git-server
https://docs.semaphore.io/CE-1.4/using-semaphore/connect-github
https://docs.semaphore.io/CE-1.4/using-semaphore/connect-github-oauth
https://docs.semaphore.io/CE-1.4/using-semaphore/connect-gitlab
https://docs.semaphore.io/CE-1.4/using-semaphore/containers/container-registry
https://docs.semaphore.io/CE-1.4/using-semaphore/containers/docker
https://docs.semaphore.io/CE-1.4/using-semaphore/github-sso
https://docs.semaphore.io/CE-1.4/using-semaphore/insights
https://docs.semaphore.io/CE-1.4/using-semaphore/jobs
https://docs.semaphore.io/CE-1.4/using-semaphore/monorepo
https://docs.semaphore.io/CE-1.4/using-semaphore/notifications
https://docs.semaphore.io/CE-1.4/using-semaphore/observability
https://docs.semaphore.io/CE-1.4/using-semaphore/okta
https://docs.semaphore.io/CE-1.4/using-semaphore/openid
https://docs.semaphore.io/CE-1.4/using-semaphore/org-health
https://docs.semaphore.io/CE-1.4/using-semaphore/org-preflight
https://docs.semaphore.io/CE-1.4/using-semaphore/organizations
https://docs.semaphore.io/CE-1.4/using-semaphore/pipelines
https://docs.semaphore.io/CE-1.4/using-semaphore/projects
https://docs.semaphore.io/CE-1.4/using-semaphore/promotions
https://docs.semaphore.io/CE-1.4/using-semaphore/rbac
https://docs.semaphore.io/CE-1.4/using-semaphore/recipes/infracost
https://docs.semaphore.io/CE-1.4/using-semaphore/recipes/trivy
https://docs.semaphore.io/CE-1.4/using-semaphore/secrets
https://docs.semaphore.io/CE-1.4/using-semaphore/self-hosted
Thinking…

Important finding: api.semaphoreci.com redirects to https://semaphore.io/ — this is noteworthy. Also, the current documentation is at docs.semaphore.io, and they're all in the CE (Community Edition, self-hosted) section. This suggests that Semaphore Cloud may have been sunset or migrated. Let me verify: Semaphore announced that they would end Semaphore Cloud and focus on the self-hosted CE. Let me check the site.

The user said: "Keep it light: use hosted products via SDK or API. Do not spin up local service stacks… if the quickstart requires either, state that clearly and stop."

If Semaphore is currently self-hosted-only (CE), the quickstart would require spinning up a local service stack — which is exactly the case where I should say so and stop. Let me verify carefully.

api.semaphoreci.com now redirects to semaphore.io, and all docs are under CE (Community Edition). Let me verify whether Semaphore Cloud still exists.

cd /tmp && curl -sL -m 12 https://docs.semaphore.io/ | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2000])"
Semaphore
Semaphore
Skip to main content Semaphore Docs Cloud (SaaS) Cloud (SaaS) Community Edition v1.5 (latest) Community Edition v1.4 Enterprise Edition v1.5 (latest) Enterprise Edition v1.4 Service Status Search Semaphore All-in-one delivery platform for AI-driven development Install Semaphore Read the docs Workflows Workflows can build, test, release and deploy your code. Learn how to create workflows using the visual editor. Learn more Jobs Jobs are the basic unit of work. Read about jobs, blocks and pipelines. Learn more Projects Use projects to connect your GitHub or BitBucket repositories to Semaphore. Learn more Migration Guides Check out the migration guides if you are using other CI provider like as Jenkins or CircleCI. Learn more Dashboards Create dashboards to track the performance and reliability of your CI. Learn more Test reports Configure test reports to view all the errors and flaky tests in a single place. Learn more Public API Make your custom integrations using the API. Manage Semaphore using the CLI. Learn more Organizations Invite your team, set up roles and groups, manage secrets, and create notifications. Learn more Language Guides Learn how to build and test your project in your language of choice. Learn more Self-hosted Agents Learn how to set up self-hosted agents to run jobs in your own infrastructure. Learn more Optimization Guide Learn how to speed up workflows using the cache and container registry. Learn more Monorepo Workflows Read how to use change detection to speed up monorepo workflows. Learn more Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
cd /tmp && curl -sL -m 12 https://semaphore.io/ | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2500])"
Continuous Integration & Delivery - Semaphore
Continuous Integration & Delivery - Semaphore
Semaphore
Product
Features
Continuous Integration & Pipelines
Deployments & Automation
MCP Server 🆕
Metrics & Observability
Security & Compliance
Developer Toolkit
Test reports
Monorepos
Self-hosted agents
Menu Item
Deployment options
Open source
Cloud
Hybrid
Enterprise
Explore
Customers
Security
new
Semaphore Product News
Read the update s
Solutions
Menu Item
Use Cases
iOS
Docker & Kubernetes
Android
Compare
Benchmark
vs Jenkins
vs GitHub Actions
vs Travis CI
vs Bitbucket
vs GitLab CI
vs CircleCI
vs Buildkite
WHITEPAPER
How does Semaphore Open Source work?
Download
Pricing
Resources
Menu Item
Explore
Cloud Docs
Community Edition Docs
Blog
Engineering Talks 🎙️
Resources
CI/CD Learning tool
Newsletter
Community
Youtube ↗
Discord ↗
GitHub ↗
Semaphore Blog
Explore
Semaphore on YouTube
Watch
Login
Let’s talk
Start free
Choose a language
EN
DE
PT
Semaphore
Product
Features
Continuous Integration & Pipelines
Deployments & Automation
MCP Server 🆕
Metrics & Observability
Security & Compliance
Developer Toolkit
Test reports
Monorepos
Self-hosted agents
Menu Item
Deployment options
Open source
Cloud
Hybrid
Enterprise
Explore
Customers
Security
Solutions
Menu Item
Use Cases
iOS
Docker & Kubernetes
Android
Compare
Benchmark
vs Jenkins
vs GitHub Actions
vs Travis CI
vs Bitbucket
vs GitLab CI
vs CircleCI
vs Buildkite
Pricing
Resources
Menu Item
Explore
Cloud Docs
Community Edition Docs
Blog
Engineering Talks 🎙️
Resources
CI/CD Learning tool
Newsletter
Community
Youtube ↗
Discord ↗
GitHub ↗
Get started
Login
Agent-native CI · open source
The CI built for coding agents: fast, affordable, and enterprise-ready.
Tell your agent what you want. It runs your CI.
Start free
You: set up CI for this repo, keep it green
1.
Your agent sets it up
2.
Checks before you push
3.
Review suggestions before merge
4.
Keeps CI fast as your code grows
You just ship.
STAGE BY STAGE
See it work
No setup. Just ask.
You don’t configure anything or learn a new tool. Tell your agent what you want, and it sets up your CI for you — and connects you to Semaphore along the way.
“set up CI for this repo” → done.
onboarding
testbox
Check it before you push.
Made a change? Your agent runs it first in a quick, production-identical sandbox — so you catch problems before they ever reach CI. Warm the box once, then run check after check in seconds.
same machine, image, secrets, cache · ~seconds
Reliable Execution
Stable, reproducible test runs that pass more often 
Thinking…

In other words, Semaphore Cloud (SaaS) does indeed exist. Interesting — they're marketing an "MCP Server" and "agent-native CI." Let's check the following:

  1. Pricing
  2. Cloud API documentation
  3. MCP server

The user was asking about the SDK or API. Semaphore Cloud has a public API. Let's take a look at the cloud documentation for API references.

Key blocker: Using the Cloud API requires an API token from a Semaphore organization (requires account creation + OAuth with GitHub/Bitbucket). Without credentials, you can't call the hosted API.

Things to check:

  • Pricing page
  • Cloud API documentation

Let's find the sitemap of the cloud documentation.

Semaphore Cloud (SaaS) does exist, plus an MCP server. Let me check pricing and the Cloud API surface.

cd /tmp && curl -sL -m 12 https://semaphore.io/pricing | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:4000])"
Semaphore CI/CD Platform Pricing - Semaphore
Semaphore CI/CD Platform Pricing - Semaphore
Semaphore
Product
Features
Continuous Integration & Pipelines
Deployments & Automation
MCP Server 🆕
Metrics & Observability
Security & Compliance
Developer Toolkit
Test reports
Monorepos
Self-hosted agents
Menu Item
Deployment options
Open source
Cloud
Hybrid
Enterprise
Explore
Customers
Security
new
Semaphore Product News
Read the update s
Solutions
Menu Item
Use Cases
iOS
Docker & Kubernetes
Android
Compare
Benchmark
vs Jenkins
vs GitHub Actions
vs Travis CI
vs Bitbucket
vs GitLab CI
vs CircleCI
vs Buildkite
WHITEPAPER
How does Semaphore Open Source work?
Download
Pricing
Resources
Menu Item
Explore
Cloud Docs
Community Edition Docs
Blog
Engineering Talks 🎙️
Resources
CI/CD Learning tool
Newsletter
Community
Youtube ↗
Discord ↗
GitHub ↗
Semaphore Blog
Explore
Semaphore on YouTube
Watch
Login
Let’s talk
Start free
Choose a language
EN
DE
PT
Semaphore
Product
Features
Continuous Integration & Pipelines
Deployments & Automation
MCP Server 🆕
Metrics & Observability
Security & Compliance
Developer Toolkit
Test reports
Monorepos
Self-hosted agents
Menu Item
Deployment options
Open source
Cloud
Hybrid
Enterprise
Explore
Customers
Security
Solutions
Menu Item
Use Cases
iOS
Docker & Kubernetes
Android
Compare
Benchmark
vs Jenkins
vs GitHub Actions
vs Travis CI
vs Bitbucket
vs GitLab CI
vs CircleCI
vs Buildkite
Pricing
Resources
Menu Item
Explore
Cloud Docs
Community Edition Docs
Blog
Engineering Talks 🎙️
Resources
CI/CD Learning tool
Newsletter
Community
Youtube ↗
Discord ↗
GitHub ↗
Get started
Login
Semaphore got cheaper
The Most Cost-Effective CI/CD Platform
Semaphore now separates compute pricing from support and success services. Pay only for what you use. Add services when you need them.
Ubuntu ARM
$0.003
/min 2vCPU
$0.006
/min 4vCPU
$0.012
/min 8vCPU
Ubuntu x64
$0.0075
/min 2vCPU
$0.015
/min 4vCPU
MacOS
$0.09
/min 4vCPU
Self-Hosted
$0.0025
/min any machine
Let’s talk
Start free
Semaphore CI/CD Competitive Benchmark
Avg build time and cost per job across leading CI/CD providers. Read proof
Average Build Time (minutes)
Semaphore
5m 01s Fastest
Buildkite
7m 15s
GitHub Actions
9m 44s
GitLab
11m 15s
CircleCI
13m 18s
Cost per Job (USD)
Semaphore
$0.04 Cheapest
GitHub Actions
$0.06
CircleCI
$0.08
Buildkite
$0.09
GitLab
$0.11
Concurrency
Includes 20 concurrent jobs by default . Higher limits are available on request based on usage.
Free Credits
Get $15 free credits per month (≈ 2,000 Ubuntu x64 2-vCPU minutes). Credits can be used across available services.
Artifacts
Free up to 20 GB/month egress and 100 GB/month storage . Additional usage: $0.35/GB egress and $0.07/GB storage.
Cache
Each project includes 9.6 GB of cache storage to speed up builds and dependency installs.
Dedicated Cache Server
Optional add-on: $400/month for 10 Gbps bandwidth and 400 GB storage.
Private Docker Registry
Optional add-on: $400/month with 10 Gbps bandwidth and 400 GB storage.
Annual Discounts
Discounted pricing is available for annual billing . Contact support for details.
Free Support
Includes help with account setup, projects, and Git integration . Advanced CI troubleshooting is available through paid support plans .
Add-ons
Semaphore’s support and success options are fully modular. Add them at any time, scale them up or down, or bundle them for complete coverage.
Support
Success
Engineering
Support
✅ 98% customer satisfaction (CSAT) in 2025 ✅ 4.7 / 5 average rating on G2
Basic
$50
/month
✔️ Email support ✔️ During business hours* ✔️ No guaranteed response times
Best for small teams that can rely on business-hours support.
Priority Response
$250
/month
✔️ Email + Slack ✔️ Best-effort accelerated responses 24/5
Best for teams that need fast responses during work week.
SLA Standard
$500
/month
✔️ Email + Slack + Customer Portal ✔️ Guaranteed fast responses 24/5 ✔️ SLA: P1 4h, P2 1 day, P3 3 days, P4 1 week
Best for production workloads needing predictable reliability
SLA Premium
$750
/month
✔️ Email + Slack + C
cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -viE "/CE|/EE" | head -60
https://docs.semaphore.io/search
https://docs.semaphore.io/search
https://docs.semaphore.io/
https://docs.semaphore.io/getting-started/about-semaphore
https://docs.semaphore.io/getting-started/changelog
https://docs.semaphore.io/getting-started/experiments
https://docs.semaphore.io/getting-started/faq
https://docs.semaphore.io/getting-started/features
https://docs.semaphore.io/getting-started/install
https://docs.semaphore.io/getting-started/migration-overview
https://docs.semaphore.io/getting-started/migration/bitbucket
https://docs.semaphore.io/getting-started/migration/circle
https://docs.semaphore.io/getting-started/migration/github-actions
https://docs.semaphore.io/getting-started/migration/jenkins
https://docs.semaphore.io/getting-started/migration/travis
https://docs.semaphore.io/getting-started/quickstart
https://docs.semaphore.io/getting-started/tour/build-project
https://docs.semaphore.io/getting-started/tour/deploy-project
https://docs.semaphore.io/getting-started/tour/hello-world
https://docs.semaphore.io/getting-started/tour/sign-up
https://docs.semaphore.io/reference/agent-aws-stack
https://docs.semaphore.io/reference/agent-yaml
https://docs.semaphore.io/reference/api
https://docs.semaphore.io/reference/audit-events
https://docs.semaphore.io/reference/conditions-dsl
https://docs.semaphore.io/reference/dashboard-yaml
https://docs.semaphore.io/reference/deployment-target-yaml
https://docs.semaphore.io/reference/env-vars
https://docs.semaphore.io/reference/jobs-yaml
https://docs.semaphore.io/reference/machine-types
https://docs.semaphore.io/reference/notifications-yaml
https://docs.semaphore.io/reference/openid
https://docs.semaphore.io/reference/os-apple
https://docs.semaphore.io/reference/os-ubuntu
https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2004-image
https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2204-image
https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-arm-image
https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-image
https://docs.semaphore.io/reference/pipeline-yaml
https://docs.semaphore.io/reference/project-yaml
https://docs.semaphore.io/reference/resources-yaml
https://docs.semaphore.io/reference/secret-yaml
https://docs.semaphore.io/reference/self-hosted-config
https://docs.semaphore.io/reference/sem-ai-cli
https://docs.semaphore.io/reference/semaphore-cli
https://docs.semaphore.io/reference/toolbox
https://docs.semaphore.io/using-semaphore/ai/copilot-agent-cloud
https://docs.semaphore.io/using-semaphore/ai/mcp-server
https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples
https://docs.semaphore.io/using-semaphore/ai/self-healing-ci
https://docs.semaphore.io/using-semaphore/ai/sem-ai
https://docs.semaphore.io/using-semaphore/artifacts
https://docs.semaphore.io/using-semaphore/billing
https://docs.semaphore.io/using-semaphore/cache
https://docs.semaphore.io/using-semaphore/connect-bitbucket
https://docs.semaphore.io/using-semaphore/connect-github
https://docs.semaphore.io/using-semaphore/connect-github-oauth
https://docs.semaphore.io/using-semaphore/connect-gitlab
https://docs.semaphore.io/using-semaphore/containers
https://docs.semaphore.io/using-semaphore/containers/container-registry
https://docs.semaphore.io/using-semaphore/containers/docker
Thinking…

Alright — there's an MCP server documentation page and a Cloud API reference. Let me read through them:

  1. https://docs.semaphore.io/reference/api
  2. https://docs.semaphore.io/using-semaphore/ai/mcp-server
  3. https://docs.semaphore.io/getting-started/quickstart

Let me fetch these.

Found Cloud API + MCP server docs. Let me read them.

cd /tmp && for u in https://docs.semaphore.io/reference/api https://docs.semaphore.io/using-semaphore/ai/mcp-server; do echo "######## $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:5000])"; done
######## https://docs.semaphore.io/reference/api
######## https://docs.semaphore.io/reference/api
Semaphore API | Semaphore
Skip to main content Version: Cloud (SaaS) On this page Semaphore API
This document describes all the resources that make up Semaphore API version v1alpha . If you have any problems or requests please contact support .
The root of the API can be found here: https://<organization-url>.semaphoreci.com/api/v1alpha .
Overview ​
Every API request and response must satisfy the following constraints:
All requests must use HTTPS.
All data is sent and received as JSON.
Blank fields are included as null instead of being omitted.
Timestamps are in different formats due to the historical circumstances of how these public APIs appeared. In the next release of the API, they will be standardized. Currently, there are the following formats:
Unixtime Epoch time: "create_time": "1571083003"
Unixtime Epoch time with nanoseconds: "created_at": {"seconds": 1571063401, "nanos": 559492000}
Custom format: YYYY-MM-DD HH:MM:SS.ffffffZ , e.g., "2019-10-14 12:11:47.824128Z"
All API requests must set the User-Agent to SemaphoreCI v2.0 Client .
Authentication ​
All API requests require authentication. To authenticate, you need an API Token. You can find your token by visiting your account settings .
Your API Token must be sent as an HTTP header in all requests, as shown below:
curl -H "Authorization: Token {api_token}" "https://<organization-url>.semaphoreci.com/api/v1alpha/{resource_name}"
Errors ​
You can receive several errors as a response to an API request
Error Message Meaning HTTP/1.1 401 Unauthorized Failure to authenticate HTTP/1.1 404 Not Found Resource doesn't exist or is not visible to user
Pagination ​
Every request that that returns more than 30 items will be paginated. To avoid this, you should form calls with link header values instead of constructing your own URLs.
A link header includes information about pagination, as shown below:
link: <http://<organization-url>.semaphoreci.com/api/v1alpha/?PAGE_PARAMS>; rel="first", <http://<organization-url>.semaphoreci.com/api/orgs?PAGE_PARAMS>; rel="next"
The possible rel values are:
next - The link for the next page of results.
prev - The link for the previous page of results.
first - The link for the first page of results.
Stability ​
There are two types of changes:
Compatible and emergency changes may be made with no advanced notice
Disruptive changes will not occur without advanced notice. In the event that a disruptive change is needed, a new major version will be developed
Emergency changes happen without notice. We will, however, try to notify users as best as possible
Compatible changes ​
Small in scope and unlikely to break or change semantics of existing methods.
Adding nested resources, methods, and attributes.
Change of documentation.
Change of undocumented behavior.
Disruptive changes ​
May have a larger impact and effort will be made to provide migration paths as needed.
Changing semantics of existing methods.
Removing resources, methods, and attributes.
Emergency change ​
May have larger impact, but is unavoidable due to legal compliance, security vulnerabilities, or violation of specification.
Organizations ​
Create an organization ​
Creates a new organization owned by the authenticated user.
Unlike every other endpoint, this one is account-scoped, not
organization-scoped : it is served on the me. host (there is no organization
subdomain yet) and is authenticated with your personal account API token. The
user resolved from the token becomes the owner of the new organization.
Request:
POST https://me.semaphoreci.com/api/v1alpha/organizations
Parameters:
username ( required ) - the organization's URL handle, used as its subdomain ( <username>.semaphoreci.com ). Must be unique and not already taken.
name ( optional ) - human-readable display name. Defaults to username when omitted.
Response:
HTTP status: 200 { "organization_id": "2b4e4c0f-c760-4f47-9dc4-b3e84f993f63", "name": "My Organization", "username": "my-org" }
A username that is missing or already taken returns a 4xx error describing
the problem.
Example:
curl -X POST --location "https://me.semaphoreci.com/api/v1alpha/organizations" \ -H "Authorization: Token {api_token}" \ -H "Content-Type: application/json" \ -d $'{ "username": "my-org", "name": "My Organization" }'
Workflows ​
Run workflow ​
Request:
POST <organization-url>.semaphoreci.com/api/v1alpha/plumber-workflows
Parameters:
project_id ( required ) - ID of a project.
reference ( required ) - git reference for the desired branch, tag, or pull request--e.g. refs/heads/master , refs/tags/v1.0 , or refs/pull/123 .
commit_sha ( optional ) - Commit sha of the desired commit.
pipeline_file ( optional ) - The path within the repository to the YAML file that contains the pipeline definition. The default value is .semaphore/semaphore.yml .
parameters : (key-values) specify parameter values that will be available in all jobs of the initial pipeline and can be used in the same way as the parameters from the parameterized promotions .
Res
######## https://docs.semaphore.io/using-semaphore/ai/mcp-server
MCP Server | Semaphore
Skip to main content Version: Cloud (SaaS) On this page MCP Server
This page explains how Semaphore's MCP Server works, how to configure your clients/agents, and what capabilities are available.
Request access to this feature If you want to try the MCP Server, contact support@semaphore.io so we can enable this feature in your organization at no cost.
Overview ​
MCP servers (Model Context Protocol servers) connect AI models to external data and tools through a standardized interface. They provide structured, real-time context-like data from APIs so models can retrieve accurate information and perform actions securely and consistently.
You can connect your favorite AI agent with Semaphore using the official MCP Server. This allows the AI agent or IDE to access the activity in your organization using a conversational interface.
With MCP, you can now ask your AI agent why your build failed and to provide a fix.
MCP Server Capabilities ​
Semaphore's MCP Server provides access to your Semaphore organization via the following tools and resources:
echo : Returns the provided message verbatim (handy for smoke tests)
organizations_list : Lists organizations that the user can access
projects_list : List projects that belong to a specific organization
projects_search : Search projects inside an organization by project name, repository URL, or description
workflows_search : Search recent workflows for a project (most recent first)
pipelines_list : List pipelines associated with a workflow (most recent first)
pipeline_jobs : List jobs belonging to a specific pipeline
jobs_describe : Describes a job, surfacing agent details, and lifecycle timestamps
jobs_logs : Fetches job logs. For cloud jobs, it streams loghub events. For self-hosted jobs, returns a URL where logs can be fetched
get_test_results : Returns a signed URL (gzip JSON) for JUnit-style test reports . Response includes URL, path, compression, and fetch instructions.
doc_tools : Allows AI to access a copy of the full Semaphore documentation site
The following tools require MCP Server write permissions in your organization. Contact support@semaphore.io to enable this feature.
workflow_run : Schedules a new workflow to be executed
workflow_rerun : Reruns an existing workflow with the original parameters
See example prompts to see a bit of what's possible.
Configure your AI Agent or IDE ​
Semaphore MCP supports two authentication methods:
OAuth 2.1 (recommended): clients that support MCP OAuth can sign in via browser and grant access without manually managing API tokens.
API token: personal or service account token via Authorization: Bearer <token> .
Both methods use the same endpoint: https://mcp.semaphoreci.com/mcp .
Connect with OAuth (recommended) ​
If your MCP client supports OAuth for remote MCP servers:
Add https://mcp.semaphoreci.com/mcp as the MCP server URL.
Start the connection flow in your client.
Complete browser sign-in and grant access to the MCP client.
OAuth-capable clients use discovery and dynamic client registration automatically.
Connect with an API token ​
If your client does not support OAuth yet, use an API token instead.
You can obtain an API token in two ways:
Personal API Token : if you don't know your personal API token, you can reset it and obtain a new one.
Service Account : create a service account with Member role, add it to projects that you want to access via the MCP server, and use its API token.
If you have problems connecting to the MCP Server, see troubleshooting .
API token setup examples ​
The client configurations below use API token authentication and continue to work for clients that do not support OAuth yet.
Claude Code ​
Export an environment variable with your API token
export SEMAPHORE_API_TOKEN=my-token
Add the MCP Server to Claude Code
claude mcp add semaphore https://mcp.semaphoreci.com/mcp \ --scope user --transport http \ --header "Authorization: Bearer $SEMAPHORE_API_TOKEN"
If you have a session open, restart Claude Code
Open a terminal and navigate to your repository
Start Claude Code and run the following command to update CLAUDE.md and add Semaphore project details to the repository. This step is optional but recommended to speed up AI tasks and reduce token usage
Initialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization
note If you get an error stating strategy requires thinking to be enabled . This error is caused by known bug in Claude Code. The workaround is to add ultrathink to the previous command: Initialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization ultrathink
OpenAI's Codex ​
Open $HOME/.codex/config.toml and add the following lines to the config file
Semaphore MCP Configuration for Codex [mcp_servers.semaphore] url = "https://mcp.semaphoreci.com/mcp" bearer_token_env_var = "SEMAPHORE_API_TOKEN" startup_timeout_sec = 30 tool_timeout_sec = 300
Environment variable nam
cd /tmp && for u in https://docs.semaphore.io/getting-started/quickstart https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples; do echo "######## $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:4500])"; done
######## https://docs.semaphore.io/getting-started/quickstart
######## https://docs.semaphore.io/getting-started/quickstart
Quickstart | Semaphore
Skip to main content Version: Cloud (SaaS) Quickstart
Welcome 👋 Thank you for trying out Semaphore.
This 20-minute tutorial will give you 80% of the time needed to use Semaphore effectively. No strings attached. No credit card is required. This is our promise.
What will you learn?
Sign up : sign up and create an organization
First project : a "Hello, World!" of CI, invite your teammates to your project
Build your project : a real example of CI with build and test steps
Deploy your project : almost everything you need to release or deploy your apps
Let's dive in!
######## https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples
MCP Usage Examples | Semaphore
Skip to main content Version: Cloud (SaaS) On this page MCP Usage Examples
Semaphore’s MCP Server unlocks practical, low-friction AI assistance for CI/CD. With an AI agent already connected, you can ask natural questions and get immediate insights from your Semaphore projects.
Below, we outline key use cases and examples for developers, each accompanied by example prompts, an explanation of what happens under the hood, and relevant technical details. These scenarios assume that you have MCP access enabled and are authenticated either with OAuth or an API token.
Organization and Project Context Discovery ​
Quickly identify which Semaphore organizations and projects your AI agent can access. This is useful for setting context before deeper queries. The agent can also cache organization and project IDs for later use, saving you time.
Example prompts:
List organizations you have access to
Find the current project’s organization ID and project ID and save them in AGENTS.md
Claude Users Claude users can automate project and organization ID discovery with the following command. See MCP Server configuration for more information: Initialize project's CLAUDE.md /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization
Under the hood, the MCP server will determine the user based on the provided API token and fetch organizations or projects that are accessible to that user. The response returns organization or project metadata (IDs, names, etc.). The AI agent then presents a list of organization or project names or IDs, and can optionally write these identifiers to a file for future reference.
Related API calls GET https://<your-org>.semaphoreci.com/api/v1alpha/projects Authorization: Bearer <YOUR_API_TOKEN>
For instance, the MCP response might return a JSON array of projects in your organization.
Example API response [ {"name": "hello-semaphore", "id": "proj-1234-..."}, {"name": "dockerizing-ruby", "id": "proj-5678-..."}, {"name": "golang-mathapp", "id": "proj-9012-..."} ]
The agent uses this data to confirm connectivity and context. With IDs known, subsequent commands (like triggering or inspecting pipelines) don’t require you to manually look up IDs, reducing friction.
Pipeline creation and edition ​
With the MCP Server, AI agents can create Semaphore pipelines from scratch using real project context and official documentation—without relying on guesswork or generic CI templates. This is especially useful when onboarding new projects, migrating CI systems, or iterating quickly on pipeline design.
In this scenario, the agent combines repository inspection with access to Semaphore’s documentation (via doc_tools``) to generate a valid semaphore.yml` tailored to the project.
Example prompts:
Create a Semaphore pipeline for this repository that runs linting and unit tests
Generate a semaphore.yml file for this project and cache dependencies
To create a pipeline, the agent typically performs the following steps:
Repository inspection: the agent analyzes the repository structure (language, package manager, test framework, etc.) to understand what the pipeline should do
Documentation lookup via doc_tools: using the doc_tools resource, the agent reads Semaphore’s official pipeline YAML documentation
Pipeline generation: based on the repository context and documentation guidance, the agent generates a complete semaphore.yml file aligned with Semaphore’s syntax and best practices
Validation through execution: after the YAML is committed and pushed, Semaphore validates and runs the pipeline. Because the agent relied on authoritative docs, the pipeline typically passes validation and executes successfully on the first run.
Pipeline Overview and Understanding ​
Get a high-level summary of what a given Semaphore pipeline does. This helps onboard to a new project or review pipeline structure without digging through YAML. The AI agent can describe the pipeline’s stages, jobs, and purpose in plain language.
The MCP server exposes pipeline queries that the agent uses to gather pipeline details. Typically, the agent will:
Use workflows_search to find the most recent workflow for the project (often filtered to the main branch or a specific workflow).
Call pipelines_list with that workflow’s ID to get the pipeline(s) in that run (usually one pipeline unless promotions are involved).
Invoke pipeline_jobs for the pipeline to list all jobs (and their statuses) defined in that pipeline.
Example prompts:
De
Thinking…

Excellent. In other words, Semaphore Cloud has the following:

  1. A public REST API (v1alpha). Requires an API token from account settings. Notably, there is an API endpoint to create organizations: POST https://me.semaphoreci.com/api/v1alpha/organizations — account-scoped, authenticated with a personal account API token.
  2. An MCP server at https://mcp.semaphoreci.com/mcp (requires requesting access via support@semaphore.io).

The blocker: To obtain a personal API token, you need a Semaphore Cloud account. To create an account, you need to sign up via the Web UI (GitHub/Bitbucket OAuth or email). I don't have credentials in this environment. Let me check whether the API is reachable without authentication, and whether there's a signup endpoint.

Things to test:

  • Accessing https://me.semaphoreci.com/api/v1alpha/organizations without a token → expect 401.
  • Check whether there's a signup API.

Also, let me check whether the MCP endpoint is reachable, and whether a token is required.

Let's run these checks.

Semaphore Cloud has a public REST API and an MCP server. Let me probe what's reachable without credentials.

cd /tmp && echo "== orgs create, no auth"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" -X POST "https://me.semaphoreci.com/api/v1alpha/organizations" -H "Authorization: Token nope" -H "Content-Type: application/json" -d '{"username":"probe-test"}' | head -5
echo "== MCP endpoint, no auth"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" -X POST "https://mcp.semaphoreci.com/mcp" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' | head -10
== orgs create, no auth
== orgs create, no auth

HTTP:401
== MCP endpoint, no auth
{"error":"invalid_token","error_description":"Missing bearer token"}
HTTP:401
cd /tmp && echo "== sem CLI availability"; curl -s -m 10 -o /dev/null -w "storage.googleapis.com/sem-cli-releases HTTP:%{http_code}\n" "https://storage.googleapis.com/sem-cli-releases/get.sh"; echo "== sem-ai docs"; curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2200])"
== sem CLI availability
== sem CLI availability
storage.googleapis.com/sem-cli-releases HTTP:200
== sem-ai docs
sem-ai Command Line | Semaphore
Skip to main content Version: Cloud (SaaS) On this page sem-ai Command Line
sem-ai is an agent-first CLI for Semaphore CI/CD. It's designed for AI agents and automation, with structured JSON output, self-discovery, and composable commands. It can also run as an MCP server for direct tool integration.
Overview ​
sem-ai provides full control over your Semaphore CI/CD from the terminal or from AI agents. Every command returns structured JSON by default. Key capabilities:
Diagnose CI failures with parsed test results
Pipeline topology analysis (critical path, blast radius)
Test intelligence (flaky detection, test summaries)
Testbox: run commands in real CI environments before pushing
MCP server mode for native AI agent integration
Installation ​
curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh
Installs the latest release for macOS / Linux on amd64 / arm64. The binary lands at $HOME/.local/bin/sem-ai (or $HOME/.semaphore-ai/bin/sem-ai if that's not on your $PATH ). Re-run the same command to upgrade.
From source ​
Requires Go 1.25+.
git clone https://github.com/semaphoreio/sem-ai.git cd sem-ai make install
Setup ​
sem-ai connect ​
Connect to your Semaphore organization. You need an API token .
sem-ai connect <organization>.semaphoreci.com <API_TOKEN>
For example:
sem-ai connect myorg.semaphoreci.com NeUFkim46BCdpqCAyWXN
The token is stored in ~/.sem.yaml , shared with the Semaphore CLI . If you already have sem configured, sem-ai uses the same credentials automatically.
sem-ai signin ​
Sign in without pasting a token. signin runs a device flow: it shows a one-time code and a verification URL, opening a browser when one is available.
sem-ai signin
Enter the code in the browser, sign in — or create an account there, if you do not have one — and approve. The terminal finishes on its own. signup and login are aliases for the same flow. The resulting token is stored in ~/.sem.yaml the same way connect stores one.
Defaults to Semaphore Cloud. For another deployment, pass the host, and --id-host when the CLI-auth endpoints live elsewhere:
sem-ai signin my-onprem.example.com --id-host id.my-onprem.example.co
Thinking…

In other words, the API and MCP both require tokens. The sem-ai CLI has a device flow sign-in (sem-ai signin), which displays a one-time code and a verification URL — you can also create an account on the browser. This requires a human with a browser. This is a human-in-the-loop step, not a local service stack or long-running command.

So this is actually a path I can act on: install sem-ai (curl | sh — quick install), then run sem-ai signin. This displays a code for the user to approve in the browser. This is short-running and interactive — not a long-running stack.

However, this needs the user to click. Let me decide: the user said "let me know if you get stuck." The real blocker is the credentials. But the device flow is a legitimate lightweight path: install the CLI, initiate signin, hand the code to the user.

Hmm — but is the signin command interactive/blocking? It displays the code and waits for approval. It'll probably block until approved. This is a "long-running command" in the sense that it waits. But I can run it in the background, capture the code, and then poll the status.

Let me install sem-ai and check. This is lightweight — it's not a service stack, just a single static binary.

First let me check the installation script (don't blindly pipe curl into sh).

Let me proceed pragmatically: download install.sh, verify its contents, then install.

Let's do that. Also, let me further check the sem-ai docs for things like "testbox".

First, let me fetch and verify install.sh.

Both API and MCP need a token, but sem-ai has a device-flow signin. Let me inspect the installer before running it.

cd /tmp && curl -fsSL -m 20 https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh -o sem-ai-install.sh && wc -l sem-ai-install.sh && head -60 sem-ai-install.sh
134 sem-ai-install.sh
134 sem-ai-install.sh
#!/bin/sh
set -eu

GITHUB_OWNER=semaphoreio
GITHUB_REPO=sem-ai
API_URL="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest"
DL_BASE="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download"

say() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }

TMP=$(mktemp -d 2>/dev/null || mktemp -d -t sem-ai-install)
trap 'rm -rf "$TMP"' EXIT INT TERM

# Step 1 — platform detect
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)

case "$os" in
  darwin|linux) ;;
  *)
    warn "unsupported platform: ${os}/${arch}"
    exit 1
    ;;
esac

case "$arch" in
  x86_64|amd64)  arch=amd64 ;;
  aarch64|arm64) arch=arm64 ;;
  *)
    warn "unsupported platform: ${os}/${arch}"
    exit 1
    ;;
esac

# Step 2 — resolve latest release tag
tag=$(curl -fsSL "$API_URL" \
  | grep '"tag_name"' \
  | sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/')

if [ -z "$tag" ]; then
  warn "failed to resolve latest release (network error or rate limit)"
  exit 1
fi

# GoReleaser strips the leading v from {{ .Version }}
ver=${tag#v}

# Step 3 — install destination detect
path_hint=0

case ":${PATH}:" in
  *":${HOME}/.local/bin:"*)
    dest_dir="${HOME}/.local/bin"
    ;;
  *)
    dest_dir="${HOME}/.semaphore-ai/bin"
    path_hint=1
    ;;
esac
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[2200:9000])"
m
m
Flag Description --headless or --device Never try to open a browser; print the code and URL --browser Open the verification page even when the terminal looks non-interactive --id-host Host serving the CLI-auth endpoints (defaults to <host> ) --org Also create a first organization with this name (new accounts only) --org-host Host for the --org organization's context
An account has a single API token. If yours already has one, approving the sign-in resets it , after an explicit confirmation in the browser. The previous token then stops working everywhere it is used — CI secrets, scripts, other machines. To authenticate with a token you already hold, use connect instead.
sem-ai context ​
Each organization you connect to is stored in ~/.sem.yaml as a named context .
List configured organizations:
sem-ai context list
Show the organization this invocation resolves to — the pin, if one is set, otherwise the active context:
sem-ai context show
Under a pin, show and list report different things by design: show names what this invocation will actually talk to, while list reports the file, marking the stored default active and the pinned entry pinned .
Change the default organization used by unpinned commands:
sem-ai context switch myorg_semaphoreci_com
context switch rewrites the shared active-context key in ~/.sem.yaml . Unpinned invocations that set no credential environment variables read that key — including invocations of the legacy sem CLI — so switching affects sessions other than your own. It is meant for a person changing their own default, not for scoping a command, a script, or an agent. To target an organization for one command without changing what anyone else sees, pin it instead.
Pinning an organization ​
--context selects a named context for one invocation, without writing to the config file. It takes a context name , not a host: connect names each context after its host with the dots replaced by underscores, so myorg.semaphoreci.com is stored as myorg_semaphoreci_com . Run sem-ai context list to see the exact names.
sem-ai --context myorg_semaphoreci_com project list
SEM_CONTEXT does the same for a whole shell session:
export SEM_CONTEXT=myorg_semaphoreci_com sem-ai project list
A name that is not in ~/.sem.yaml fails immediately and lists the contexts that are, instead of quietly falling back to another organization.
Pinning isolates the read path: a pinned invocation neither reads nor writes active-context , so pinned sessions cannot flip each other's organization. It does not make every operation concurrency-safe. connect , signin , and context switch still write ~/.sem.yaml ; each write replaces the whole file, and two overlapping writes are last-writer-wins, so a context one session adds can disappear when another lands. Pinning also covers sem-ai only — the legacy sem CLI reads neither --context nor SEM_CONTEXT and still follows active-context . When several agents run at once, sequence the commands that write, or give each agent its own HOME .
signin , context switch , and context list ignore the pin entirely, so a pin naming a context that does not exist yet cannot block the command that is about to create it. connect ignores it when resolving credentials, but refuses outright when the pin resolves to a different host than its <host> argument, since that names two organizations at once. connect names the context it creates after its host, so re-pin to that name once onboarding finishes.
Credential resolution order ​
sem-ai resolves a host and a token from these sources, in order:
Priority Source Scope Behavior 1 --context <name> A single invocation Replaces everything below 2 SEM_CONTEXT=<name> A shell session Replaces everything below 3 SEMAPHORE_HOST and/or SEMAPHORE_API_TOKEN The process environment Merged — each variable overrides only its own field 4 active-context in ~/.sem.yaml Shared by every session on the machine The starting point when nothing above applies
The two halves of that table behave differently, and the difference matters.
A context named by --context or SEM_CONTEXT supplies the host and the token together and fully replaces the sources below it, so its credentials are never mixed with the environment variables.
Without a selector, rows 3 and 4 merge field by field . SEMAPHORE_HOST and SEMAPHORE_API_TOKEN are independent, so setting only one leaves the other coming from the active context. Exporting SEMAPHORE_API_TOKEN on its own — which the hosted MCP server setup instructs you to do — means every later sem-ai command in that shell sends that token to whichever host active-context currently names. Set both variables together, or pin a context, whenever the host and the token have to belong to the same organization.
General syntax ​
sem-ai <command> [subcommand] [flags]
Global flags:
Flag Description --context Run against a named context from ~/.sem.yaml without changing the active one. See pinning an organization --format or -f Output format: json (default), table , yaml --verbose or -v Show HTTP requests for debugging --examples Show usage examples for any command --help or -h Help for any command
Self-discovery ​
sem-ai discover ​
Returns a structured map of every command, its flags, and examples. Designed for AI agents to self-orient without documentation.
sem-ai discover sem-ai discover --format table
sem-ai <command> --examples ​
Every command supports --examples to show usage examples:
sem-ai diagnose --examples sem-ai pipeline promote --examples
Working with projects ​
sem-ai project list ​
List all projects in the organization:
sem-ai project list
sem-ai project show ​
Show project details:
sem-ai project show <project-name>
sem-ai project create ​
Create a project from a git repository. With no flags it uses the origin remote of the current directory and derives the name from the repo URL, then bootstraps an initial .semaphore/semaphore.yml (unless --skip-yaml ). If a project with the same name already exists it returns the existing one, unless --fail-on-exists is set.
sem-ai project create sem-ai project create --repo-url git@github.com:org/repo.git sem-ai project create --name my-project --github-integration github_app
Flag Default Description --repo-url origin of cwd git repository URL --name derived from repo URL project name --github-integration github_token GitHub integration: github_token or github_app --remote origin git remote to detect when --repo-url is not set --skip-yaml false don't generate .semaphore/semaphore.yml in cwd --fail-on-exists false exit non-zero if a project with the same name already exists
sem-ai project update ​
Update project settings:
sem-ai project update <project-name> --visibility public sem-ai project update <project-name> --description "My app"
sem-ai project delete ​
Delete a project:
s
Thinking…

The installer is clean and simple. Let's install sem-ai. Then check sem-ai discover. This works without authentication (self-discovery). Next, try sem-ai signin --headless. This will print a code and URL — that will be a handoff to the user.

Let's install.

Note: signin with the --org flag creates an initial organization — perfect for a new account.

Let's install and run discover.

Note: sem-ai signin --headless blocks until approved. Let's run it in the background with nohup and poll the code.

Let's install first.

Installer is clean. Installing sem-ai:

cd /tmp && sh sem-ai-install.sh 2>&1 | tail -15; echo "---"; ls -la ~/.local/bin/sem-ai ~/.semaphore-ai/bin/sem-ai 2>/dev/null; export PATH="$HOME/.local/bin:$HOME/.semaphore-ai/bin:$PATH"; sem-ai --version 2>&1 | head -3
claude plugin marketplace add semaphoreio/sem-ai # first time only
  claude plugin marketplace add semaphoreio/sem-ai   # first time only
  claude plugin install sem-ai@semaphoreio           # first time
  claude plugin marketplace update semaphoreio       # already installed — refresh catalog

Codex CLI — install (or refresh) from the shell:
  codex plugin marketplace add semaphoreio/sem-ai    # first time only
  codex plugin add sem-ai@semaphoreio                # first time
  codex plugin marketplace upgrade semaphoreio       # already installed — refresh catalog

Then run /sem-ai:init in a repo to set up Semaphore CI/CD for it.

Prefer Claude Desktop? Install the one-click MCPB extension instead of the CLI —
download sem-ai_0.5.0_linux_amd64.mcpb from the release and open it:
  https://github.com/semaphoreio/sem-ai/releases/download/v0.5.0/sem-ai_0.5.0_linux_amd64.mcpb
It bundles this same server; enter your org host + API token in the install dialog.
---
-rwxr-xr-x 1 1001 1001 10240184 Oct  6 16:08 /sandbox/.semaphore-ai/bin/sem-ai
Error: unknown flag: --version
Thinking…

Already installed at /sandbox/.semaphore-ai/bin/sem-ai. Let's run sem-ai discover and check the functionality without authentication.

Installed. Let me self-discover its capabilities (no auth needed):

export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && sem-ai discover 2>&1 | python3 -c "
import sys,json
d=json.load(sys.stdin)
def walk(o,p=''):
    if isinstance(o,dict):
        for k,v in o.items():
            if k in ('summary','description','use'): print(p,k,'=',str(v)[:110])
            else: walk(v,p+'.'+k if p else k)
    elif isinstance(o,list):
        for i in o: walk(i,p)
print(json.dumps(list(d.keys())))
walk(d)" 2>&1 | head -70
["commands", "tips"]
["commands", "tips"]
commands description = Self-hosted agent management
commands description = Delete a self-hosted agent type
commands description = List agents for a given type
commands description = Show agent type details
commands description = List self-hosted agent types
commands description = Pipeline and workflow analytics over time
commands description = Deploy frequency and promotion stats
commands description = Pipeline duration trends (avg, p50, p95)
commands description = Block-level failure rates
commands description = Queue wait time analysis
commands description = All-in-one analytics overview for a project
commands description = Week-over-week trends for duration, pass rate, queue time, and failure reasons
commands description = Artifact operations: list and download build artifacts
commands description = Download an artifact via signed URL
commands description = List artifacts for a job, workflow, or project
commands description = Show which blocks were affected by failures in a pipeline
commands description = Generate the autocompletion script for the specified shell
commands description = Generate the autocompletion script for bash
commands description = Generate the autocompletion script for fish
commands description = Generate the autocompletion script for powershell
commands description = Generate the autocompletion script for zsh
commands description = Connect to a Semaphore organization
commands description = Manage Semaphore contexts (orgs/auth)
commands description = List all configured contexts
commands description = Show active context details
commands description = Switch active context
commands description = Show the longest dependency chain (bottleneck) in a pipeline
commands description = Deployment target operations: visibility and management
commands description = Activate a deployment target
commands description = Create a deployment target
commands description = Deactivate a deployment target
commands description = Delete a deployment target
commands description = Show deployment history for a target
commands description = Show deployment target details
commands description = List deployment targets for a project
commands description = Update a deployment target (PATCH)
commands description = Failure diagnosis: one command, full root cause
commands description = Introspect all registered commands: capability map for agents
commands description = History-backed flaky-test signals (Superjerry) for a project
commands description = List individual disruption occurrences for a flaky test
commands description = Show the real failure for a flaky test by fetching its disruption job log
commands description = List flaky tests for a project
commands description = Show details for a single flaky test (per-context pass rate, p95, disruptions)
commands description = Project-level flaky/disruption count time series (day, count)
commands description = Group management — organize members into teams
commands description = Create a group
commands description = Delete a group
commands description = List groups in the organization
commands description = Update a group — add or remove members
commands description = Project health summary: recent pass rate, failures, deployment status
commands description = Help about any command
commands description = Pipeline insights (Velocity): performance, reliability, frequency
commands description = Pipeline run frequency
commands description = Pipeline duration metrics (mean/median/p95 seconds)
commands description = Pipeline pass/fail counts
commands description = Job operations
commands description = List jobs by state
commands description = Fetch job logs
commands description = Show job details
commands description = Stop a running job
commands description = Log out of a Semaphore context, clearing its stored API token
commands description = Start MCP (Model Context Protocol) stdio server
commands description = Notification management: Slack, email, webhook
commands description = Create a notification rule from a YAML/JSON file
commands description = Delete a notification rule
commands description = List notification rules
commands description = Show notification rule details
commands description = Open workflow/pipeline in browser
commands description = Organization management — members, roles
Thinking…

This is a rich, agent-first CLI. Everything requires authentication. Next, let's attempt the device flow sign-in that outputs the code and URL — that's the handoff to the user.

Let's run sem-ai signin --headless in the background and grab the code.

Let's do it. It'll block while waiting for approval. I'll run it with nohup and poll.

Also, should I include --org for creating the initial organization? That requires a name. Should I hold off and ask the user — no, better to just grab the code first. The org can be created later, or via the API after obtaining the token.

Actually, --org only applies to new accounts, and creating the org right away saves a step. But we need an org name. Let's start without --org, grab the code, and hand it off to the user. After that, once the token is obtained, we can use the API endpoint to create the org.

Let's run signin --headless in the background.

This is a genuinely agent-first CLI. Everything needs a token. Let me start the device-flow signin to get a handoff code:

export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && rm -f signin.log; nohup sem-ai signin --headless > signin.log 2>&1 &
echo "pid=$!"; sleep 12; echo "--- log:"; cat signin.log
pid=4580
pid=4580
--- log:
Heads up: if your account already has an API token, completing this sign-in
lets you reset it (you confirm in the browser first). A reset token stops
working everywhere it is used: CI secrets, scripts, other machines.

First, copy your one-time code:

    RKRK-DJBP

Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
Thinking…

Got the code. Now I need the user to approve it in a browser. That's the handoff. The process is waiting in the background.

Note: The signin process is running in the background. Should leave it running so the user can approve. But watch the timeout — device flow codes usually expire in about 5–15 minutes. Should tell the user to act quickly.

In the meantime, could I prepare the sample repo content (.semaphore/semaphore.yml) so it's ready to use right away once authenticated? But without knowing the project, it'd be too generic. Actually, the repo is empty (only .gitignore). Let me check what the repo is — there's a .git directory. Let me check the remote and contents.

Actually, ls -la only showed .git and .gitignore. Let me check git log/remote.

While waiting, let me prep the example scaffolding. This is useful and lightweight work.

Let me check the git repo.

Got a device code. Let me check the repo state and prep the example scaffolding while it waits:

cd /sandbox/repo && git log --oneline -5 2>&1 | head; echo "--- remotes"; git remote -v; echo "--- branches"; git branch -a; echo "--- gitignore"; cat .gitignore
9ebf5fb Initial workspace
9ebf5fb Initial workspace
--- remotes
--- branches
* main
--- gitignore
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/
cd /tmp && for u in https://docs.semaphore.io/using-semaphore/ai/testbox https://docs.semaphore.io/reference/pipeline-yaml; do echo "#### $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:1800])"; done
#### https://docs.semaphore.io/using-semaphore/ai/testbox
#### https://docs.semaphore.io/using-semaphore/ai/testbox
Semaphore
Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken.
#### https://docs.semaphore.io/reference/pipeline-yaml
Pipeline YAML | Semaphore
Skip to main content Version: Cloud (SaaS) On this page Pipeline YAML
This page describes the formal pipeline YAML specification for Semaphore.
Overview ​
Semaphore uses YAML for the definition of pipeline . Every Semaphore project requires at least one pipeline to work. If you don't want to write pipelines by hand, you can use the visual workflow editor .
Execution order ​
You cannot assume that jobs in the same task run in any particular order. They run in parallel on a resource availability basis.
To force execution order, you must use block dependencies . Semaphore only starts a block when all their dependencies are completed.
Comments ​
Lines beginning with # are considered comments and are ignored by the YAML parser.
version ​
The version of the pipeline YAML specification to be used. The only value supported is v1.0
Example version : v1.0
name ​
A Unicode string for the pipeline name. It is strongly recommended that you give descriptive names to your Semaphore pipelines.
Example name : The name of the Semaphore pipeline
agent ​
Defines the global agent's machine type and os_image to run jobs . See agents to learn more.
The agent can contain the following properties:
[ machine ]: VM machine type to run the jobs
containers : optional Docker containers to run the jobs
Example agent : machine : type : e1 - standard - 2 os_image : ubuntu2404
note The default agent can be overridden inside tasks .
machine ​
Part of the agent definition. It defines the global VM machine type to run the jobs.
It requires two properties:
type
os_image
Example machine : type : e1 - standard - 2 os_image : ubuntu2404
type ​
Part of the agent definition. It selects the hardware or self-hosted agent type that runs the jobs.
The list of valid values for Semaphore Clou
Thinking…

The repo is essentially empty (a single "Initial workspace" commit, no remote). That means there's no GitHub repo to connect — sem-ai project create needs a git repo URL, and Semaphore requires either a GitHub or Bitbucket repository.

This is a second blocker: to create a project, you need a hosted git repo (GitHub/Bitbucket) and an integration connected to the Semaphore org. The empty local repo has no remote.

So the full blocker chain is:

  1. No Semaphore account/token → device flow requires a human browser operation (code RKRK-DJBP).
  2. No GitHub/Bitbucket repo → Semaphore projects must connect to a hosted git repo. The local repo has no remote, and I don't have GitHub credentials either.

Let me confirm I don't have GitHub credentials.

I'll check gh CLI / GITHUB_TOKEN.

Let me verify and also check whether the signin process is still alive.

env | grep -iE "github|gitlab|bitbucket|GH_" | sed 's/=.*/=<set>/'; echo "--- gh cli"; which gh 2>/dev/null || echo "no gh"; echo "--- signin still waiting?"; pgrep -f "sem-ai signin" >/dev/null && echo "alive (waiting for approval)" || echo "exited"; tail -3 /tmp/signin.log; echo "--- sem.yaml"; ls -la ~/.sem.yaml 2>/dev/null || echo "no ~/.sem.yaml yet"
--- gh cli
--- gh cli
no gh
--- signin still waiting?
alive (waiting for approval)

Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
--- sem.yaml
-rw------- 1 root root 0 Oct 22 11:31 /sandbox/.sem.yaml
Thinking…

Confirmed:

  • No GitHub/GitLab/Bitbucket credentials, no gh CLI.
  • Signin is still waiting (device flow) — needs human browser operation.
  • Repo has no remote.

So, we're blocked on two points, both of which require human response:

  1. Approving the device code (account creation) — browser work, human only.
  2. A hosted GitHub/Bitbucket repo to connect as a Semaphore project. Semaphore can't build a local-only repo in the cloud.

The user said "let me know if you get blocked." So I should report it. However, I've also done useful groundwork: installed sem-ai, obtained the device code, validated the API surface.

Should I stop here, or should I do prep that doesn't require auth? I can write an example .semaphore/semaphore.yml and a small app, so that the moment auth comes through, we're ready to go. That's lightweight and useful, and it's the "easy example" part that I can do. Let's do that — make a small Python project with tests and a valid semaphore.yml.

Actually, let me think carefully: the user asked "help me build an easy example using Semaphore." Given that we're blocked, the most useful deliverable is:

  • Example repo files (app + tests + .semaphore/semaphore.yml) — these work without auth
  • Clear report of blockers
  • Pricing info
  • Assessment of ease of management

That's a solid response. Let's build the example files. I'll validate the YAML against the spec.

Let me look a bit more at the pipeline YAML spec for blocks/tasks/prologue/epilogue and agent types, and machine types for pricing correlation.

Let me grab the machine-types reference to get the type names right.

Confirmed blocked on two human-only steps. Let me still build the auth-free part of the example. First, exact agent type names:

cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/machine-types | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2500])"
Machine Types | Semaphore
Machine Types | Semaphore
Skip to main content Version: Cloud (SaaS) On this page Machine Types
This page gives technical specifications for the machines available on Semaphore Cloud.
Overview ​
A machine type defines what virtualized hardware to use in your agents .
The machines described in this page only apply to Semaphore Cloud. You can add more types of machines using self-hosted agents .
Performance baseline and resource availability ​
Each Semaphore machine type is provisioned with a guaranteed baseline of virtual CPUs, memory, and disk as listed in the specifications below.
Depending on host utilization at any given moment, a machine may temporarily receive additional CPU time, disk throughput, network bandwidth, and memory speed beyond its baseline specification. This happens when the physical host running your VM has spare capacity available — for example, during off-peak hours when fewer jobs are running concurrently.
As a result, you may notice that identical jobs sometimes complete faster than previous runs, even when nothing in the job configuration has changed. This is not a sign of performance degradation — these faster runs are benefiting from additional resources temporarily available on the host, and performance may vary between runs.
Linux machines ​
Linux machines are provided in two generations. The following table shows what operating systems each generation supports and if they support Docker environments .
Generation Arch OS Supported Docker environments F1 Intel x86_64 Ubuntu 22.04 Ubuntu 24.04 Yes R1 ARM Ubuntu 24.04 ARM No
For legacy machine generations still supported for existing usage, see Legacy Linux generations .
F1 generation ​
This generation is intended for compute-intensive tasks. They provide the most performance amongst the machines offered in Semaphore Cloud.
Implementation details:
CPU : hyperthreaded on a 4.6GHz 12th generation Intel i5 125000
Memory : DDR4 RAM
Disk : NvME storage
The F1 generation is presented in the following types:
Type Virtual CPUs Memory (GB) Disk (GB) f1-standard-2 2 8 45 f1-standard-4 4 16 65
F1 machines can be paired with:
ubuntu2204
ubuntu2404
Docker-based environments .
R1 generation ​
These are ARM machines.
Implementation details:
CPU : emulated ARM on Ampere Altra Q80-30
Memory : DDR4 RAM
Disk : NvME storage
The r1 generation is presented in the following types:
Type Virtual CPUs Memory (GB) Disk (GB) r1-standard-2 2 4 50 r1-standard-4 4 8 65 r1-standard-8 8 16 75
R1 Generation machin
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/pipeline-yaml | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('blocks')
print(o[i:i+2600])"
blocks and jobs , where a limit longer than the pipeline's is rejected when the pipeline is compiled. Jobs in an after_pipeline task instead inherit the pipelin
blocks and jobs , where a limit longer than the pipeline's is rejected when the pipeline is compiled. Jobs in an after_pipeline task instead inherit the pipeline limit unless they set their own.
Example version : v1.0 name : Using execution_time_limit agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 execution_time_limit : hours : 3
fail_fast ​
This optional property defines what happens when a job fails. It accepts the following properties:
stop
'cancel'
If both are set, stop is evaluated first. If fail_fast is not defined, jobs continue running following declared dependencies when a job fails.
stop ​
The stop property causes all running jobs to stop as soon as one job fails. It requires a when property that defines a condition according to Conditions DSL .
In the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails and the workflow is initiated from a non-master branch all running jobs stop immediately.
Example version : v1.0 name : Setting fail fast stop policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : stop : when : "branch != 'master'" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ "Block B" ] task : jobs : - name : Job C commands : - sleep 60
cancel ​
The cancel property causes all non-started jobs to be canceled as soon as one job fails. Already running jobs are allowed to finish. This property requires a when property that defines a condition according to Conditions DSL .
In the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails in a workflow that was initiated from a non-master branch:
Block B is allowed to finish
Block C is canceled, i.e. it never starts
Example version : v1.0 name : Setting fail fast cancel policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : cancel : when : "branch != 'master'" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ "Block B" ] task : jobs : - name : Job C commands : - sleep 60
queue ​
The optional queue property enables you to assign pipelines to custom execution queues or to configure the way the pipelines are processed when queuing happens.
There
Thinking…

Now the spec is all lined up. Let's create a sample project. Write a small Python app and tests, plus a .semaphore/semaphore.yml that includes lint + test blocks and a promotion.

What to create:

  • greeter/__init__.py (small module)
  • tests/test_greeter.py
  • requirements-dev.txt (pytest, ruff)
  • .semaphore/semaphore.yml
  • README.md documenting what's remaining

After that, verify that the tests pass locally (fast, no service stack needed), and validate that the YAML parses.

Machine type: use f1-standard-2 and ubuntu2404. Note: the docs examples use e1-standard-2 (legacy). F1 is current gen x64. Going with f1-standard-2.

Let's write the files.

On pricing correlation:

  • Ubuntu x64 $0.0075/min 2vCPU → f1-standard-2
  • $15/month free credit ≈ 2000 minutes of x64 2vCPU
  • Concurrency of 20 included
  • Artifacts are free up to 20GB/month egress, 100GB storage; overages are $0.35/GB egress, $0.07/GB storage
  • Cache is 9.6GB per project
  • Dedicated cache server / private Docker registry add-on is $400/month each
  • Support add-ons: Basic $50, Priority $250, SLA Standard $500, SLA Premium $750
  • ARM Ubuntu $0.003/min 2vCPU, $0.006 4vCPU, $0.012 8vCPU
  • x64: $0.0075 2vCPU, $0.015 4vCPU
  • macOS $0.09/min 4vCPU
  • Self-hosted $0.0025/min any machine
  • MCP server: free if you contact support

Pay-as-you-go, credits-based. Good.

Alright, let's create the files.

The pipeline YAML will have two blocks: Lint and Test, with Test depending on Lint. Plus caching of pip deps. And add a promotion for deploy gated on master.

Going to keep it simple but realistic.

Let's write it.

For caching, Semaphore uses cache store / cache restore.

Let's write semaphore.yml:

version: v1.0
name: Greeter CI
agent:
  machine:
    type: f1-standard-2
    os_image: ubuntu2404

blocks:
  - name: Lint
    task:
      jobs:
        - name: ruff
          commands:
            - checkout
            - python3 -m venv .venv
            - source .venv/bin/activate
            - pip install -q -r requirements-dev.txt
            - ruff check .
  - name: Test
    dependencies: [Lint]
    task:
      jobs:
        - name: pytest
          commands:
            - checkout
            - cache restore pip-$(checksum requirements-dev.txt) || true
            - python3 -m venv .venv
            - source .venv/bin/activate
            - pip install -q -r requirements-dev.txt
            - cache store pip-$(checksum requirements-dev.txt) .venv
            - pytest -q --junitxml=report.xml

Hmm, caching .venv may be unreliable. But it's good enough as an example. Actually, more simply: caching the pip cache dir is better. Let's not over-engineer; cache with key by checksum and store .venv — Semaphore's docs do show caching of venv/dep dirs. Acceptable.

Add test report? Requires additional config. Skip to stay lightweight, but mention it.

Let's also add a promotion to demonstrate deploy gating. Keep it minimal — maybe skip promotion to avoid extra complexity. Actually, a small promotion is the characteristic Semaphore feature. Add one gated on master branch that just echoes (no real deploy). Hmm — this creates a .semaphore/deploy.yml. Let's include it; it's illustrative and cheap.

Let's write the files.

Then run pytest locally to verify the tests pass.

Alright, writing it all now.

Spec confirmed. Now building the auth-free part of the example:

File edits

1 file

Completed
Writtengreeter/__init__.pyView changes

/sandbox/repo/greeter/__init__.py

Written content · previous contents were not recorded

"""Tiny library used as a Semaphore CI example."""
from __future__ import annotations
VALID_LANGS = {"en", "es", "de"}
_GREETINGS = {
"en": "Hello",
"es": "Hola",
"de": "Hallo",
}
def greet(name: str, lang: str = "en") -> str:
"""Return a greeting for ``name`` in ``lang``.
Raises:
ValueError: if ``name`` is blank or ``lang`` is unsupported.
"""
if not isinstance(name, str) or not name.strip():
raise ValueError("name must be a non-empty string")
if lang not in VALID_LANGS:
raise ValueError(f"unsupported lang: {lang!r}")
return f"{_GREETINGS[lang]}, {name.strip()}!"

File edits

1 file

Completed
Writtentests/test_greeter.pyView changes

/sandbox/repo/tests/test_greeter.py

Written content · previous contents were not recorded

import pytest
from greeter import greet
@pytest.mark.parametrize(
("lang", "expected"),
[("en", "Hello, Ada!"), ("es", "Hola, Ada!"), ("de", "Hallo, Ada!")],
)
def test_greet_supported_langs(lang, expected):
assert greet("Ada", lang) == expected
def test_greet_strips_whitespace():
assert greet(" Ada ") == "Hello, Ada!"
@pytest.mark.parametrize("bad", ["", " ", None, 42])
def test_greet_rejects_blank_or_non_string(bad):
with pytest.raises(ValueError):
greet(bad)
def test_greet_rejects_unknown_lang():
with pytest.raises(ValueError, match="unsupported lang"):
greet("Ada", "fr")
Thinking…

Next up are requirements-dev.txt, .semaphore/semaphore.yml, and .semaphore/deploy.yml.

File edits

1 file

Completed
Writtenrequirements-dev.txtView changes

/sandbox/repo/requirements-dev.txt

Written content · previous contents were not recorded

pytest==8.3.4
ruff==0.8.4

File edits

1 file

Completed
Written.semaphore/semaphore.ymlView changes

/sandbox/repo/.semaphore/semaphore.yml

Written content · previous contents were not recorded

version: v1.0
name: Greeter CI
agent:
machine:
# Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.
type: f1-standard-2
os_image: ubuntu2404
execution_time_limit:
minutes: 15
blocks:
- name: Lint
task:
jobs:
- name: ruff
commands:
- checkout
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -q -r requirements-dev.txt
- ruff check .
- name: Test
dependencies: [Lint]
task:
jobs:
- name: pytest
commands:
- checkout
- cache restore pip-$SEMAPHORE_PROJECT_ID || true
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -q -r requirements-dev.txt
- cache store pip-$SEMAPHORE_PROJECT_ID .venv
- pytest -q --junitxml=report.xml
epilogue:
always:
commands:
- test-results publish --name pytest report.xml || true
promotions:
# Only offer the deploy path on the main branch, after everything passes.
- name: Deploy
pipeline_file: .semaphore/deploy.yml
auto_promote_on:
- result: passed
branch:
- main
Thinking…

Alright, next up is deploy.yml. Let's keep it to a minimum — just echoing a placeholder that shows where to incorporate the actual deploy.

File edits

1 file

Completed
Written.semaphore/deploy.ymlView changes

/sandbox/repo/.semaphore/deploy.yml

Written content · previous contents were not recorded

version: v1.0
name: Deploy
agent:
machine:
type: f1-standard-2
os_image: ubuntu2404
blocks:
- name: Ship
task:
jobs:
- name: publish
commands:
- checkout
# Placeholder: swap for your real release step, e.g.
# - docker build -t myapp:$SEMAPHORE_GIT_SHA .
# - docker push myregistry/myapp:$SEMAPHORE_GIT_SHA
- echo "Would deploy $SEMAPHORE_PROJECT_NAME @ $SEMAPHORE_GIT_SHA"
Thinking…

Alright, let's verify: run pytest locally, run ruff, and verify that the YAML can be parsed. These are fast and don't require a service stack.

Let's create a venv, install pytest+ruff, and run them.

It might take about 20 seconds, but that's fine — it's not a long-running service.

Alright, let's execute.

Verifying locally (fast, no services):

cd /sandbox/repo && python3 -m venv .venv && . .venv/bin/activate && pip install -q -r requirements-dev.txt 2>&1 | tail -3; echo "== ruff"; ruff check . && echo "ruff OK"; echo "== pytest"; pytest -q --junitxml=/tmp/report.xml 2>&1 | tail -8
== ruff
== ruff
All checks passed!
ruff OK
== pytest
tests/test_greeter.py:3: in <module>
    from greeter import greet
E   ModuleNotFoundError: No module named 'greeter'
--------------------- generated xml file: /tmp/report.xml ----------------------
=========================== short test summary info ============================
ERROR tests/test_greeter.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.09s
cd /sandbox/repo && python3 -c "
import yaml
for f in ['.semaphore/semaphore.yml','.semaphore/deploy.yml']:
    d=yaml.safe_load(open(f))
    print(f,'-> parsed OK; version',d['version'],'| agent',d['agent']['machine'],'| blocks',[b['name'] for b in d['blocks']])
" 2>&1 | tail -5
.semaphore/semaphore.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Lint', 'Test']
.semaphore/semaphore.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Lint', 'Test']
.semaphore/deploy.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Ship']
Thinking…

The reason pytest can't find greeter is that there's no packaging / rootdir configuration. This will also fail on CI. Need to fix it properly — either add a pyproject.toml containing pytest configuration (pythonpath), or make it an installable package. The cleanest and most minimal fix: add [tool.pytest.ini_options] pythonpath = ["."] to pyproject.toml. This works with pytest 8.

Let's add pyproject.toml. This will also be where we put the ruff configuration.

This is a bug I actually caught — good thing I ran it.

Alright, let's write pyproject.toml.

Real bug caught — pytest can't resolve the package. Adding proper config:

File edits

1 file

Completed
Writtenpyproject.tomlView changes

/sandbox/repo/pyproject.toml

Written content · previous contents were not recorded

[project]
name = "greeter"
version = "0.1.0"
description = "Tiny library used as a Semaphore CI example"
requires-python = ">=3.10"
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
addopts = "-q"
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
Thinking…

Note: The ruff "I" rule (isort) might warn about import ordering. Let's re-run both. Also, regarding the from __future__ import annotations and the use of pytest.mark.parametrize. Let me try running it.

cd /sandbox/repo && . .venv/bin/activate && echo "== ruff"; ruff check . && echo "ruff OK"; echo "== pytest"; pytest --junitxml=/tmp/report.xml 2>&1 | tail -6
== ruff
== ruff
All checks passed!
ruff OK
== pytest
.........                                                                [100%]
--------------------- generated xml file: /tmp/report.xml ----------------------
9 passed in 0.02s
Thinking…

All 9 tests pass, and ruff is clean. The YAML also parses correctly.

Next, let's check whether the signin device code is still valid / pending, and add a README documenting the handoff. Also, let's verify that the test-results command in the epilogue is correct — test-results publish. Let's quickly validate this in the toolbox docs. Actually, this is a documented Semaphore toolbox command. I'll keep the || true guard, but let's quickly check the exact syntax.

I'll do a quick check of the test-results publish syntax and check the signin status.

All 9 tests pass, ruff clean, YAML valid. Let me verify the toolbox command syntax and signin status:

cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/test-reports 2>/dev/null | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('test-results publish'); print(o[max(0,i-400):i+500] if i>0 else o[:600])"
Semaphore
Semaphore
Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken.
pgrep -f "sem-ai signin" >/dev/null && echo "signin: STILL WAITING for browser approval" || echo "signin: exited"; echo "--- log tail"; tail -4 /tmp/signin.log; echo "--- token stored?"; cat ~/.sem.yaml 2>/dev/null | head -5; echo "(empty = not yet authenticated)"
signin: STILL WAITING for browser approval
signin: STILL WAITING for browser approval
--- log tail
    RKRK-DJBP

Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
--- token stored?
(empty = not yet authenticated)
Thinking…

The URL for the test-reports documentation was wrong. Let's find the correct one from the sitemap. Earlier, I saw https://docs.semaphore.io/using-semaphore/... — let me grep the sitemap for test-report.

Let's check.

cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -viE "/CE|/EE" | grep -iE "test-report|toolbox|semaphore-cli"
https://docs.semaphore.io/reference/semaphore-cli
https://docs.semaphore.io/reference/semaphore-cli
https://docs.semaphore.io/reference/toolbox
https://docs.semaphore.io/using-semaphore/tests/test-reports
Thinking…

The correct URL is https://docs.semaphore.io/using-semaphore/tests/test-reports. Let me fetch this and check the test-results publish syntax.

cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('test-results publish')
print(o[max(0,i-600):i+700] if i>0 else o[:900])"
karma-junit-reporter JavaScript ESLint built-in JavaScript Jest jest-junit Ruby RSpec rspec_junit_formatter Ruby Cucumber built-in Elixir ExUnit junit-formatte
 karma-junit-reporter JavaScript ESLint built-in JavaScript Jest jest-junit Ruby RSpec rspec_junit_formatter Ruby Cucumber built-in Elixir ExUnit junit-formatter Go GoTestSum built-in PHP PHPUnit built-in Python PyTest built-in Bash Bats built-in Rust Cargo Test junit-report Java Maven maven-surefire
Step 2 - Publish ​
The publishing step uploads all report files to the artifact store . This is accomplished using the test-results tool, which is part of the Semaphore toolbox .
Assuming the file is called report.xml , you can publish the report with the following command.
[[ -f report.xml ]] && test-results publish report.xml
To simplify this step, you can add a pipeline epilogue to run the publish command after every job ends.
Editor YAML
Select the pipeline with the test jobs
Add the publish command in the "Execute always" box of the Epilogue
Open the pipeline file with the test jobs
Add a global_job_config key at the YAML root
Type the publish command under epilogue.always.commands
.semaphore/semaphore.yml # ... global_job_config : epilogue : always : commands : - '[[ -f report.xml ]] && test-results publish report.xml'
Step 3 - Merge ​
The final step is to merge and process all report files. This is achieved using an after-pipeline job .
Editor YAML
Press +Add After Jobs
Type a
Thinking…

The canonical syntax is [[ -f report.xml ]] && test-results publish report.xml — no --name flag is documented. Let's fix the epilogue to use the documented format, and move it into global_job_config as the docs recommend. That way is cleaner.

Let's update semaphore.yml: add global_job_config with the epilogue, and remove the per-job epilogue. Also, since a merge step for after_pipeline is needed for test reports to show up in the UI, let's add an after_pipeline block to properly merge the reports. The docs say "Add an after-pipeline job". Let's add that — this is the documented pattern and it makes the test reports actually work.

Let me check what the after_pipeline merge job looks like. Grab a bit more of that page.

Actually, let's be careful about scope creep. What the user asked for was a "simple example". Test report merging adds one more block. But this is the documented way to make test-results work, and I already introduced test-results publish. Options: (a) Remove test-results entirely → simpler, no risk of getting the syntax wrong. (b) Do it properly with global_job_config + after_pipeline merge.

I'll go with (b), but keep it compact, because this demonstrates a real Semaphore feature. Let me grab the exact after_pipeline merge syntax.

Let me fetch a bit more of the page.

The documented syntax differs from mine. Let me get the exact merge pattern:

cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('Step 3 - Merge')
print(o[i:i+1200])"
Step 3 - Merge ​
Step 3 - Merge ​
The final step is to merge and process all report files. This is achieved using an after-pipeline job .
Editor YAML
Press +Add After Jobs
Type a name for the job
Type the command test-results gen-pipeline-report
Open the pipeline with the test jobs
Add an after_pipeline key at the YAML root
Add the name under task.jobs
Type the merge command test-results gen-pipeline-report in the commands section
.semaphore/semaphore.yml # ... after_pipeline : task : jobs : - name : Merge reports commands : - test - results gen - pipeline - report
How to view test reports ​
To view test results, navigate to your pipeline and go to the Tests tab.
info The test tab is only populated for the pipelines where test reports are configured . If you don't see anything yet, try rerunning the pipeline.
The default Test view shows failed tests first.
Click on "Failed first" to change the sort order. You can sort by "Slowest first" or alphabetically.
Click on "View" to change display preferences. You can hide passed or skipped tests and change how the report looks.
See also ​
Semaphore toolbox reference
How to use the flaky tests dashboard
Plan job and block execution with pipelines
Overview H
Thinking…

Alright, let's modify semaphore.yml to use the documented pattern:

  • global_job_config.epilogue.always.commands: ['[[ -f report.xml ]] && test-results publish report.xml']
  • after_pipeline.task.jobs: [{name: Merge reports, commands: [test-results gen-pipeline-report]}]
  • Remove the per-job epilogue

Now let's make the edits.

Fixing to the documented pattern:

File edits

1 file

Completed
Modified.semaphore/semaphore.yml+16−5View changes

/sandbox/repo/.semaphore/semaphore.yml

Replacement excerpt

execution_time_limit:
minutes: 15
+# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.
+global_job_config:
+ epilogue:
+ always:
+ commands:
+ - "[[ -f report.xml ]] && test-results publish report.xml"
+

Replacement excerpt

- - pytest -q --junitxml=report.xml
- epilogue:
- always:
- commands:
- - test-results publish --name pytest report.xml || true
+ - pytest --junitxml=report.xml

Replacement excerpt

+# Merges the per-job reports so the Tests tab is populated in the UI.
+after_pipeline:
+ task:
+ jobs:
+ - name: Merge reports
+ commands:
+ - test-results gen-pipeline-report
+
promotions:
Thinking…

Now, let's validate the YAML once more and check the structure. Also, addopts in pyproject already has -q, so pytest --junitxml=report.xml is fine.

Let's do the validation.

cd /sandbox/repo && python3 -c "
import yaml
d=yaml.safe_load(open('.semaphore/semaphore.yml'))
print('keys:',list(d.keys()))
print('epilogue:',d['global_job_config']['epilogue']['always']['commands'])
print('after_pipeline:',d['after_pipeline']['task']['jobs'])
print('promotions:',d['promotions'])
" && echo "== re-run tests" && . .venv/bin/activate && pytest --junitxml=/tmp/report.xml 2>&1 | tail -3
keys: ['version', 'name', 'agent', 'execution_time_limit', 'global_job_config', 'blocks', 'after_pipeline', 'promotions']
keys: ['version', 'name', 'agent', 'execution_time_limit', 'global_job_config', 'blocks', 'after_pipeline', 'promotions']
epilogue: ['[[ -f report.xml ]] && test-results publish report.xml']
after_pipeline: [{'name': 'Merge reports', 'commands': ['test-results gen-pipeline-report']}]
promotions: [{'name': 'Deploy', 'pipeline_file': '.semaphore/deploy.yml', 'auto_promote_on': [{'result': 'passed', 'branch': ['main']}]}]
== re-run tests
.........                                                                [100%]
--------------------- generated xml file: /tmp/report.xml ----------------------
9 passed in 0.02s
Thinking…

All good, no problems. Now, let's create a README that records the handover content and the remaining steps. Also, make sure to add .venv to gitignore (already there? .venv/ is listed — yes).

Let's write a README.md that clearly documents the following:

  • Contents of the repository
  • How it corresponds to Semaphore
  • The two blocked steps and the exact commands to complete them

Then commit? The repository has a git history ("Initial workspace"). Committing is reasonable, but pushing is not possible (no remote). Let's commit to get things into a clean state. Actually — should I commit? It's useful. But the user might want to review first. To keep things tidy, let's commit and tell them so. Hmm, let's play it safe: stage + commit is reversible and useful. Let's do it.

First let's write the README.

Then re-check the sign-in process — it's probably expired by now (device codes usually last about 5–15 minutes). Let's check and restart if necessary to provide a valid code. Actually, let's check the status and restart to get a new code at the end. Because the old code might be expired.

Let's write the README.

Now a README documenting the handoff:

File edits

1 file

Completed
WrittenREADME.mdView changes

/sandbox/repo/README.md

Written content · previous contents were not recorded

# Semaphore CI example
A minimal Python project wired up for **Semaphore Cloud** (SaaS). It lints, tests,
publishes JUnit test reports, and auto-promotes to a deploy pipeline on `main`.
## Layout
```
greeter/__init__.py # the library under test
tests/test_greeter.py # 9 pytest cases
pyproject.toml # pytest + ruff config
requirements-dev.txt # pytest, ruff
.semaphore/semaphore.yml # main pipeline: Lint -> Test, then Merge reports
.semaphore/deploy.yml # promotion target (placeholder deploy step)
```
## Pipeline shape
| Stage | What runs | Depends on |
|---|---|---|
| `Lint` | `ruff check .` | — |
| `Test` | `pytest --junitxml=report.xml`, restores/stores a pip cache | `Lint` |
| `after_pipeline` | `test-results gen-pipeline-report` (merges reports) | all blocks |
| `Deploy` promotion | `.semaphore/deploy.yml` | passed + branch `main` |
Agent is `f1-standard-2` (Ubuntu 24.04 x64, 2 vCPU / 8 GB) at **$0.0075/min**.
A full run is roughly 2 jobs x ~1 min, well inside the $15/month free credit.
## Verify locally
```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
ruff check . && pytest # -> 9 passed
```
## Remaining setup (needs a human + a browser)
Semaphore Cloud builds code from a **hosted GitHub/Bitbucket repo**, so two steps
cannot be done headless:
1. **Create an account and get an API token.** Run `sem-ai signin --headless`,
then open the printed URL, enter the one-time code, sign up, and approve.
The token lands in `~/.sem.yaml`.
2. **Push this repo to GitHub (or Bitbucket)**, then create the project:
```bash
git remote add origin git@github.com:<you>/<repo>.git
git push -u origin main
sem-ai project create # uses the origin remote, bootstraps YAML
```
After that, `sem-ai workflow list` / `sem-ai diagnose` drive everything from the CLI.
Thinking…

Next, let's terminate the stale sign-in (the code has likely expired), start a new sign-in, and provide the user with a valid code. Let me check the elapsed time. Started around 11:31, and it's probably a few minutes later now. Device codes typically expire in 5-15 minutes. Let's terminate it and restart to get a new code.

Also, let's commit the work.

Let's do both.

Let me commit, and refresh the device code (the earlier one likely expired):

cd /sandbox/repo && git add -A && git -c user.email=agent@local -c user.name=agent commit -q -m "Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion" && git log --oneline -3 && echo "--- files" && git ls-files
a126a18 Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion
a126a18 Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion
9ebf5fb Initial workspace
--- files
.gitignore
.semaphore/deploy.yml
.semaphore/semaphore.yml
README.md
greeter/__init__.py
pyproject.toml
requirements-dev.txt
tests/test_greeter.py
pkill -f "sem-ai signin" 2>/dev/null; sleep 1; export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && rm -f signin2.log && nohup sem-ai signin --headless --org semaphore-demo > signin2.log 2>&1 & sleep 12; cat /tmp/signin2.log | tail -8
(no output)
(no output)
Thinking…

The second sign-in didn't produce any output. Let me check.

sleep 5; echo "--- log:"; cat /tmp/signin2.log; echo "--- proc:"; pgrep -f "sem-ai signin" >/dev/null && echo alive || echo "exited"
--- log:
--- log:
cat: /tmp/signin2.log: No such file or directory
--- proc:
alive
Thinking…

Weird — the log file doesn't exist, but the process is alive. Maybe pkill killed the shell, or the nohup redirection raced. Or possibly the background job inherited a different cwd. Let me check where it is. Actually, cd /tmp && rm -f signin2.log && nohup ... > signin2.log — rm was executed, then nohup created it. But the file doesn't exist? Maybe pkill killed the entire background subshell including the redirection before creation, and the surviving process is from the first invocation that got re-executed? Let me investigate more carefully.

Let me look for the actual process and its output.

ps aux | grep -i "sem-ai" | grep -v grep; echo "--- tmp files"; ls -la /tmp/*.log /tmp/nohup.out 2>/dev/null; echo "--- sem.yaml"; cat ~/.sem.yaml 2>/dev/null; echo "(end)"
--- tmp files
--- tmp files
-rw------- 1 root root 404 Oct 22 11:31 /tmp/signin.log
--- sem.yaml
(end)
Thinking…

The process doesn't actually exist (was the earlier "alive" a false positive from pgrep matching pgrep's own pipeline? No wait…). Anyway, currently no sem-ai process is running, and no new log has been created either. It seems pkill killed it, and either the restart silently failed, or the background job got killed at the point when the bash tool invocation ended (each bash invocation may terminate its child processes).

Ah — that's the key insight: background processes started in a single bash tool invocation are likely to be killed when that invocation returns. The reason the first one survived was… hmm, actually, the first one survived across invocations (I checked "STILL WAITING" in a later invocation). So background processes do survive.

But the second one didn't. It's possible that pkill -f "sem-ai signin" matched, and then the new process got killed at startup… or there's a chance the pattern sem-ai signin in pkill also matched the command string of the new process launched in the same command line and killed it. Actually, no, pkill runs before the startup.

Wait — nohup sem-ai signin --headless --org semaphore-demo > signin2.log — the file wasn't created. Which means the shell redirection didn't happen, i.e., the command wasn't executed. Perhaps pkill returned nonzero… no, we're not using set -e.

Hmm, could sleep 12; cat /tmp/signin2.log | tail -8 have produced no output because the file was empty at that point and tail output nothing? No — cat says "No such file".

Let me retry cleanly with setsid and absolute paths.

Note: --org semaphore-demo may conflict with the org name. Let me retry with a unique org name.

I'll do a clean retry.

export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo > /tmp/signin3.log 2>&1 < /dev/null & disown; sleep 15; echo "--- log:"; cat /tmp/signin3.log; echo "--- proc:"; ps aux | grep sem-ai | grep -v grep | head -3
--- log:
--- log:
{
  "code": "signin_error",
  "error": true,
  "message": "--org requires --org-host: the create API does not return the new org's host, so it must be given explicitly",
  "status": 1
}
--- proc:
Thinking…

--org requires --org-host. Let's provide --org-host semaphore-agent-demo.semaphoreci.com. Retrying.

Nice — structured JSON error, very agent-friendly. Worth mentioning.

Structured JSON error (nice). --org needs --org-host:

export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com > /tmp/signin4.log 2>&1 < /dev/null & disown; sleep 15; cat /tmp/signin4.log; echo "--- proc:"; ps aux|grep sem-ai|grep -v grep|head -2
Heads up: if your account already has an API token, completing this sign-in
Heads up: if your account already has an API token, completing this sign-in
lets you reset it (you confirm in the browser first). A reset token stops
working everywhere it is used: CI secrets, scripts, other machines.

First, copy your one-time code:

    TRHR-RLCG

Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
--- proc:
root        6742  0.2  0.1 1237512 15732 ?       Ssl  11:34   0:00 sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com
Thinking…

A new valid code is active: TRHR-RLCG, and the process is running.

Alright, let me write up the final response:

  1. What I built (samples, verified locally)
  2. Blockers (2 steps that only a human can perform)
  3. Pricing
  4. Ease-of-management assessment

Let me confirm that the final state of semaphore.yml displays cleanly for the response, and do a final sanity check.

Let me read the final file.

read /sandbox/repo/.semaphore/semaphore.yml{"content":[{"text":"version: v1.0\nname: Greeter CI\n\nagent:\n machine:\n # Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.\n type: f1-standa…ctrl+o
{"content":[{"text":"version: v1.0\nname: Greeter CI\n\nagent:\n  machine:\n    # Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.\n    type: f1-standard-2\n    os_image: ubuntu2404\n\nexecution_time_limit:\n  minutes: 15\n\n# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.\nglobal_job_config:\n  epilogue:\n    always:\n      commands:\n        - \"[[ -f report.xml ]] && test-results publish report.xml\"\n\nblocks:\n  - name: Lint\n    task:\n      jobs:\n        - name: ruff\n          commands:\n            - checkout\n            - python3 -m venv .venv\n            - source .venv/bin/activate\n            - pip install -q -r requirements-dev.txt\n            - ruff check .\n\n  - name: Test\n    dependencies: [Lint]\n    task:\n      jobs:\n        - name: pytest\n          commands:\n            - checkout\n            - cache restore pip-$SEMAPHORE_PROJECT_ID || true\n            - python3 -m venv .venv\n            - source .venv/bin/activate\n            - pip install -q -r requirements-dev.txt\n            - cache store pip-$SEMAPHORE_PROJECT_ID .venv\n            - pytest --junitxml=report.xml\n\n# Merges the per-job reports so the Tests tab is populated in the UI.\nafter_pipeline:\n  task:\n    jobs:\n      - name: Merge reports\n        commands:\n          - test-results gen-pipeline-report\n\npromotions:\n  # Only offer the deploy path on the main branch, after everything passes.\n  - name: Deploy\n    pipeline_file: .semaphore/deploy.yml\n    auto_promote_on:\n      - result: passed\n        branch:\n          - main\n","type":"text"}]}
Thinking…

Everything's going smoothly. Now, let me write the final answer.

To summarize:

What I built — committed to /sandbox/repo:

  • greeter library + 9 pytest tests
  • pyproject.toml (pytest/ruff config)
  • requirements-dev.txt
  • .semaphore/semaphore.yml (Lint -> Test, epilogue for test reports, merging in after_pipeline, deploy promotion on main)
  • .semaphore/deploy.yml
  • README.md
  • Verified: ruff clean, 9/9 tests pass, both YAMLs parse successfully

Found and fixed one real bug: without pyproject.toml, pytest couldn't resolve greeter — it would have failed in CI too.

Where I got stuck — two steps that only a human can do:

  1. No Semaphore account/token. The API (me.semaphoreci.com, <org>.semaphoreci.com/api/v1alpha) and MCP (mcp.semaphoreci.com/mcp) both return 401 without a token. There's no way to create an account headlessly. I kicked off a device flow:
  2. Semaphore Cloud builds from a hosted GitHub/Bitbucket repository. /sandbox/repo has no remote, and there are no GitHub credentials/gh CLI in this environment. So I can't create a project.

Note: the quickstart didn't require a local service stack, so that wasn't a reason to stop. I stayed within the hosted API/CLI. The blockers are auth + hosted git repo, both of which are human-handoff items, not infrastructure issues.

Worth noting: Semaphore also has a self-hosted Community Edition that requires a local stack — I didn't use that one.

Pricing — usage-based, compute separate from support:

  • $15/month free credit (about 2000 minutes of Ubuntu x64 2 vCPU). No credit card required.
  • Ubuntu x64: $0.0075/min (2 vCPU), $0.015 (4 vCPU)
  • Ubuntu ARM: $0.003/min (2 vCPU), $0.006 (4), $0.012 (8)
  • macOS: $0.09/min (4 vCPU)
  • Self-hosted agents: $0.0025/min regardless of machine
  • Concurrency: 20 jobs included, more on request
  • Artifacts: free up to 20 GB/month egress + 100 GB storage; then $0.35/GB egress, $0.07/GB storage
  • Cache: 9.6 GB per project included
  • Optional add-ons: dedicated cache server $400/month, private Docker registry $400/month
  • Support is separated out and modular: Basic $50/month, Priority $250, SLA Standard $500, SLA Premium $750
  • MCP server: free, but requires an email to support@semaphore.io to enable

For this example: about 2 jobs × ~1 min on f1-standard-2 ≈ $0.015 per run — easily fits within the free tier.

Is it easy for me to operate? — honest assessment: pretty easy, better than most CIs. Pros:

  • sem-ai is explicitly agent-first: sem-ai discover returns a machine-readable capability map, all commands emit structured JSON by default, and every command has --examples. This is a rare thing.
  • Errors are structured JSON with clear messages (I hit one: --org requires --org-host).
  • There's an MCP server, plus sem-ai mcp stdio mode.
  • sem-ai diagnose does root-cause analysis of failures; flaky test detection; analytics.
  • Context pinning via --context/SEM_CONTEXT is designed for concurrent agents — docs explicitly warn about shared ~/.sem.yaml last-writer-wins, which shows they've thought about multi-agent operation.
  • Pipelines are just YAML in the repo, so the normal read/edit/commit tools work.

Friction points:

  • The initial auth requires a browser (device flow) — not fully headless.
  • Requires a hosted GitHub/Bitbucket repo; can't build a local-only repo.
  • MCP server write tools (workflow_run, workflow_rerun) require an email to support to enable — a human gate.
  • Docs URLs have moved (docs.semaphoreci.com → docs.semaphore.io); several links 404, so I had to discover paths from the sitemap.

So: day-to-day operations = easy and pleasant. Onboarding = needs a human once.

Let me keep it reasonably concise and complete. I should present the code prominently.

Let me confirm the code is still valid — the process is running. Good.

I'll write the final answer.

Done with everything I can do headless. Here's where things stand.

What I built (committed in /sandbox/repo)

greeter/__init__.py        # small library under test
tests/test_greeter.py      # 9 pytest cases
pyproject.toml             # pytest + ruff config
requirements-dev.txt
.semaphore/semaphore.yml   # Lint -> Test, test reports, deploy promotion
.semaphore/deploy.yml      # promotion target
README.md

The pipeline: Lint (ruff) → Test (pytest + pip cache) → after_pipeline merges JUnit reports → Deploy promotion auto-fires on passed main. Agent is f1-standard-2 (Ubuntu 24.04 x64).

Verified locally: ruff clean, 9/9 tests pass, both YAML files parse. I caught one real bug doing this — pytest couldn't resolve the greeter module, which would have failed in CI too; fixed via pythonpath in pyproject.toml. I also corrected my test-results syntax against the docs (canonical form is [[ -f report.xml ]] && test-results publish report.xml).

Where I'm blocked — two human-only steps

The quickstart did not need a local service stack, so that wasn't a stop reason. I stayed on the hosted API/CLI. But:

  1. No account/token. Both the REST API (<org>.semaphoreci.com/api/v1alpha, me.semaphoreci.com) and the MCP server (mcp.semaphoreci.com/mcp) return 401. There's no headless signup. I started a device flow that's live and waiting:

    Code TRHR-RLCG → https://id.semaphoreci.com/device Approving also creates an org named semaphore-agent-demo.

  2. No hosted git repo. Semaphore Cloud builds from GitHub/Bitbucket. /sandbox/repo has no remote, and this environment has no GitHub credentials or gh CLI. Once you push, sem-ai project create finishes the wiring.

Pricing

Usage-based, with compute deliberately unbundled from support. $15 free credits/month (≈2,000 Ubuntu x64 2-vCPU minutes), no credit card.

Computeper minute
Ubuntu ARM$0.003 (2 vCPU) / $0.006 (4) / $0.012 (8)
Ubuntu x64$0.0075 (2 vCPU) / $0.015 (4)
macOS$0.09 (4 vCPU)
Self-hosted$0.0025, any machine

20 concurrent jobs included. Artifacts free to 20 GB/mo egress + 100 GB storage, then $0.35/GB and $0.07/GB. 9.6 GB cache per project. Optional add-ons: dedicated cache server $400/mo, private Docker registry $400/mo. Support is separate and modular — $50 / $250 / $500 / $750 per month. MCP server is free but must be enabled by emailing support.

This example costs roughly $0.015 per run — comfortably inside the free tier.

Will this be easy for me to manage?

Day-to-day, yes — unusually easy. Onboarding, no — it needs you once.

sem-ai is genuinely agent-first, which is rare: sem-ai discover returns a machine-readable capability map, every command defaults to structured JSON, and every command has --examples. Errors come back as structured JSON with actionable messages — I hit one (--org requires --org-host) and it told me exactly what to fix. It has diagnose for failure root-cause, flaky-test detection, and analytics. The docs even warn about ~/.sem.yaml last-writer-wins under concurrent agents and give --context pinning to isolate sessions — they've clearly thought about multi-agent use. Pipelines are plain YAML in-repo, so my normal read/edit/commit flow applies directly.

The friction is all at the edges: first auth needs a browser, a hosted git repo is mandatory, and the MCP write tools (workflow_run, workflow_rerun) require an email to support to unlock. Docs also moved from docs.semaphoreci.com to docs.semaphore.io with several 404s, so I had to recover paths from the sitemap.

One note: Semaphore also ships a self-hosted Community Edition whose quickstart does require standing up a local stack. I avoided that and used the hosted Cloud product, per your instruction.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge