# What is StarSling? (/) ## Introduction Developers typically spend 20-30% of their time on annoying eng tasks outside of their code editor: * Fixing exceptions * Patching failed builds and flaky tests * Investigating and remediating incidents * Optimizing database and application performance These tasks are spread out across many different developer tools and workflows. StarSling is unifying these tools and bringing AI agents to the rest of the software development lifecycle (SDLC), starting with continuous integration (CI). Learn more at [starsling.dev](https://starsling.dev). ## StarSling Runners: self-driving CI StarSling Runners are an AI-native drop-in replacement for `ubuntu-latest` and `ubuntu-24.04` that make your GitHub Actions up to 6x faster. StarSling agents do continuous deep scans of your CI setup and ship optimizations to keep speeding up your builds and save you minutes. ## Quick Start 2,000 free minutes for your first month — no credit card required. ### Step 1: Install the StarSling GitHub App [Install StarSling GitHub App](https://github.com/apps/starslingdev) StarSling Runners are not available for personal repositories, only GitHub organizations. If you install the GitHub App in a personal repo, StarSling Runners will not pick up the jobs. [Learn why →](https://docs.starsling.dev/troubleshooting/common-issues#personal-repositories-not-supported) ### Step 2: Update Your Workflow(s) Paste this prompt into any AI coding agent (Claude Code, Cursor, Codex, etc.):
````markdown title="AI prompt" # Migrate GitHub Actions to StarSling Runners Migrate the user's workflows from GitHub-hosted runners to StarSling Runners. **Prerequisites:** `gh` CLI authenticated, [StarSling GitHub App](https://github.com/apps/starslingdev) installed on the repo's org. ## Configuration **Target:** `starsling-ubuntu-24.04` | **Branch:** `migrate-starsling-ubuntu-2404` **Source runners to replace:** `ubuntu-latest`, `ubuntu-24.04` Replace all UPPERCASE placeholders (`OWNER`, `REPO`, `BRANCH_NAME`, `HEAD_OID`, `BASE64_CONTENT`, `FILE_NAME`, `N`, `DEFAULT_BRANCH`) with actual values from previous steps. ## Procedure ### Step 1: Confirm GitHub App Installation Ask the user: "Have you installed the [StarSling GitHub App](https://github.com/apps/starslingdev) on your org? It's required for runners to pick up jobs after merge. If not, please install it first and let me know when you're ready." **Do not run any commands or proceed to Step 2 until the user explicitly confirms the app is installed.** ### Step 2: Verify CLI Auth Verify `gh auth status` succeeds. If not, direct the user to install from https://cli.github.com/ and run `gh auth login`. ### Step 3: Get Repository Ask the user for the repository (`owner/repo`). Then run `gh api repos/OWNER/REPO --jq '.owner.type'`. If the result is `User` (not `Organization`), stop and explain: "StarSling Runners only work with GitHub organization repositories. You can create a free organization at https://github.com/account/organizations/new." ### Step 4: Discover Workflows Fetch all workflow files in one API call: ```bash cat <<'QUERY' | gh api graphql --input - { "query": "query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id nameWithOwner defaultBranchRef { name target { oid } } object(expression: \"HEAD:.github/workflows\") { ... on Tree { entries { name object { ... on Blob { text } } } } } } }", "variables": { "owner": "OWNER", "repo": "REPO" } } QUERY ``` Note: `HEAD` in the GraphQL expression is a Git ref, not a placeholder — do not replace it. If the response is truncated or errors due to size, fall back to the REST API: fetch the default branch and HEAD SHA via `gh api repos/OWNER/REPO --jq '.default_branch'` and `gh api repos/OWNER/REPO/git/ref/heads/DEFAULT_BRANCH --jq '.object.sha'`, then fetch workflow file names via `gh api repos/OWNER/REPO/contents/.github/workflows` and each file individually via `gh api repos/OWNER/REPO/contents/.github/workflows/FILE_NAME`. The response `content` field is base64-encoded — decode it before scanning for `runs-on:` values. Save `defaultBranchRef.name` (or REST default branch), `.target.oid` / HEAD SHA, and workflow `entries[]`. Scan each `.yml`/`.yaml` file for: - direct `runs-on:` string values matching supported runners, and - matrix-driven patterns (e.g., `runs-on: ${{ matrix.os }}`) where the matrix values include supported runner labels. Show the user a summary including: - Workflows that will be migrated and which runners are being replaced - Any Ubuntu runners NOT in the supported list (e.g., `ubuntu-22.04`, `ubuntu-20.04`, larger runners like `ubuntu-latest-16-cores`) listed as "Not migrated — unsupported runner label" If no direct or matrix-backed supported runners match, stop. ### Step 5: Preview Changes **Never create a PR without the user confirming changes first.** Show what will change per workflow: - Replace supported runners in `runs-on:` string values with `starsling-ubuntu-24.04` (preserve quotes/comments) - Replace matching `runner:`/`os:` values in `matrix.include` sections - Skip commented lines **Complex `runs-on` patterns:** - **Array syntax** (e.g., `runs-on: [self-hosted, linux, ubuntu-latest]`): Only replace the matching label within the array; do not collapse to a single string - **Group/labels syntax** (e.g., `runs-on: { group: ..., labels: [...] }`): Skip and flag for manual review - **Expressions** (`${{ matrix.os }}`, ternary/conditional): If `runs-on` uses `${{ matrix.os }}` and the matrix values are hardcoded runner labels, replace the labels in the matrix definition and flag the workflow for manual verification. If the matrix values come from other expressions, skip entirely and flag for manual review **YAML fidelity:** Change ONLY the `runs-on` and matrix values. Preserve the original file exactly: same indentation, key ordering, comments, blank lines, and trailing newline. The PR diff should show only the runner label changes. ### Step 6: Create PR **Branch:** Before creating, check for existing branches: `gh api repos/OWNER/REPO/git/matching-refs/heads/migrate-starsling-ubuntu-2404 --jq '.[].ref'`. If any exist, find the highest numeric suffix and increment by 1 (if none have a suffix, use `-2`). Then create: `gh api repos/OWNER/REPO/git/refs -f ref=refs/heads/BRANCH_NAME -f sha=HEAD_OID`. **Atomic commit** — all files in one commit, contents base64-encoded: ```bash cat <<'MUTATION' | gh api graphql --input - { "query": "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }", "variables": { "input": { "branch": { "repositoryNameWithOwner": "OWNER/REPO", "branchName": "BRANCH_NAME" }, "message": { "headline": "Migrate N CI workflows to StarSling Runners", "body": "Replaced runners per file:\n- file1.yml: ubuntu-latest → starsling-ubuntu-24.04\n- file2.yml: ubuntu-24.04 → starsling-ubuntu-24.04" }, "expectedHeadOid": "HEAD_OID", "fileChanges": { "additions": [ { "path": ".github/workflows/FILE_NAME", "contents": "BASE64_CONTENT" } ] } } } } MUTATION ``` Verify the response contains a valid `commit.oid`. If the mutation returned an `expectedHeadOid` mismatch, re-fetch the HEAD SHA (Step 4) and retry the commit once. For any other errors, report and stop — do not create a PR against a failed commit. **Commit message:** The headline should read `Migrate N CI workflows to StarSling Runners` where N is the count of modified workflow files (`.yml` + `.yaml`). List each file and the runner label(s) replaced in the body, as shown in the example above. **PR** via `gh pr create --repo OWNER/REPO --head BRANCH_NAME --base DEFAULT_BRANCH --title "Migrate N CI workflows to StarSling Runners" --body "..."` with this body structure: ``` ## Summary Migrates CI workflows from GitHub-hosted runners to [StarSling Runners](https://docs.starsling.dev) for faster builds and AI-powered optimizations. ## Changes - `file1.yml`: `ubuntu-latest` → `starsling-ubuntu-24.04` - `file2.yml`: `ubuntu-24.04` → `starsling-ubuntu-24.04` ## Not Migrated - `file3.yml`: Uses `${{ matrix.os }}` — requires manual review (or "All workflows migrated successfully.") ## After Merging Workflows will automatically run on StarSling Runners. Ensure the [StarSling GitHub App](https://github.com/apps/starslingdev) is installed with access to this repo. ``` ### Step 7: Monitor (Optional) Ask the user if they'd like help monitoring after merge. If yes, explain they can return after merging and you'll check with: ```bash gh run list --repo OWNER/REPO --branch DEFAULT_BRANCH --limit 5 --json status,conclusion,name,createdAt ``` Look for runs created after the merge. If any show `queued` for more than 2 minutes, suggest checking the GitHub App installation and repo access at https://github.com/apps/starslingdev. ## Error Handling | Error | Solution | |-------|----------| | `gh` not found or not logged in | Install from https://cli.github.com/, run `gh auth login` | | 403 / insufficient permissions | User needs write access to the repository — check collaborator status or org role | | App not installed | Install from https://github.com/apps/starslingdev | | Personal repo (owner type `User`) | StarSling requires an org repo — create one at https://github.com/account/organizations/new | | Repository not found | Check repo name and permissions | | No workflows found | Ensure `.github/workflows/` exists | | Runner not available after merge | Verify app has repo access at https://github.com/apps/starslingdev | | Rate limit exceeded | Wait a few minutes and retry | | 422 on branch creation | Branch exists — append a number suffix | | `expectedHeadOid` mismatch | Re-fetch HEAD SHA and retry the commit | | `${{ matrix.os }}` / complex `runs-on` | Require manual review — determined at runtime | | GraphQL response truncated | Fall back to REST API for individual file fetches | ````
See the [full quickstart](https://docs.starsling.dev/getting-started/quickstart) for a manual `runs-on` label swap and other migration options. ## What You Get ### Up to 6x Faster CI Runs * **[30% faster hardware](https://docs.starsling.dev/runners/compute-sizing)** * **Unlimited concurrency** to minimize queue times ### AI-Powered Optimizations * **[Optimizations](https://docs.starsling.dev/ai-agents/optimizations)** — AI agents scan your workflows to continuously improve CI speed ([see real examples](https://starsling.dev/#ai-prs)). For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default. * **[ci-speedup](https://docs.starsling.dev/skills/ci-speedup)** — Run a free, open-source audit on demand with your own coding agent to find the GitHub Actions check gating your PRs. * **[ci-score](https://docs.starsling.dev/skills/ci-score)** — Grade your GitHub Actions configuration against CI best practices with the same free, open-source skill set, and get a ranked fix for every gap. * **[ci-secure](https://docs.starsling.dev/skills/ci-secure)** — Scan your workflows for the ten critical CI/CD attack vectors, each reported with the attack it enables and a fix you can apply. Teams running StarSling see this compound in production: ### Up to 33% Lower Cost Every StarSling runner is cheaper than GitHub's equivalent larger runner — up to 33%. | Runner | vCPU | Memory | GitHub Price | StarSling Price | Savings | | --------------------------- | ---- | ------ | ------------ | --------------- | ------- | | `starsling-ubuntu-24.04-2` | 2 | 8 GB | $0.006/min | $0.004/min | **33%** | | `starsling-ubuntu-24.04` | 4 | 16 GB | $0.012/min | **$0.008/min** | **33%** | | `starsling-ubuntu-24.04-8` | 8 | 32 GB | $0.022/min | $0.016/min | **27%** | | `starsling-ubuntu-24.04-16` | 16 | 64 GB | $0.042/min | $0.032/min | **24%** | | `starsling-ubuntu-24.04-32` | 32 | 128 GB | $0.082/min | $0.064/min | **22%** | | `starsling-ubuntu-24.04-64` | 64 | 256 GB | $0.162/min | $0.128/min | **21%** | *GitHub prices reference [GitHub Actions runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing) for Linux x64 larger runners (as of 2026-05-06).* **2,000 free minutes for your first month** — no credit card required to start. ## Comparison to GitHub | Feature | GitHub-hosted | StarSling | | ------------------------- | ------------- | ------------------- | | Queue time | 30-60s | **Under 30s (P50)** | | Build speed | Baseline | **Up to 6x faster** | | Optimization improvements | Manual | **AI-powered** | ## Get Started # Quickstart (/getting-started/quickstart) 2,000 free minutes for your first month — no credit card required. ## Step 1: Install the StarSling GitHub App Install the StarSling GitHub App in your organization: [Install StarSling GitHub App](https://github.com/apps/starslingdev) Grant access to the org repositories where you want to use StarSling Runners. StarSling Runners are not available for personal repositories, only GitHub organizations. If you install the GitHub App in a personal repo, StarSling Runners will not pick up the jobs. [Learn why →](https://docs.starsling.dev/troubleshooting/common-issues#personal-repositories-not-supported) ## Step 2: Update Your Workflow(s) ### Option A: Use an AI Prompt Paste this prompt into any AI coding agent (Claude Code, Cursor, Codex, etc.):
````markdown title="AI prompt" # Migrate GitHub Actions to StarSling Runners Migrate the user's workflows from GitHub-hosted runners to StarSling Runners. **Prerequisites:** `gh` CLI authenticated, [StarSling GitHub App](https://github.com/apps/starslingdev) installed on the repo's org. ## Configuration **Target:** `starsling-ubuntu-24.04` | **Branch:** `migrate-starsling-ubuntu-2404` **Source runners to replace:** `ubuntu-latest`, `ubuntu-24.04` Replace all UPPERCASE placeholders (`OWNER`, `REPO`, `BRANCH_NAME`, `HEAD_OID`, `BASE64_CONTENT`, `FILE_NAME`, `N`, `DEFAULT_BRANCH`) with actual values from previous steps. ## Procedure ### Step 1: Confirm GitHub App Installation Ask the user: "Have you installed the [StarSling GitHub App](https://github.com/apps/starslingdev) on your org? It's required for runners to pick up jobs after merge. If not, please install it first and let me know when you're ready." **Do not run any commands or proceed to Step 2 until the user explicitly confirms the app is installed.** ### Step 2: Verify CLI Auth Verify `gh auth status` succeeds. If not, direct the user to install from https://cli.github.com/ and run `gh auth login`. ### Step 3: Get Repository Ask the user for the repository (`owner/repo`). Then run `gh api repos/OWNER/REPO --jq '.owner.type'`. If the result is `User` (not `Organization`), stop and explain: "StarSling Runners only work with GitHub organization repositories. You can create a free organization at https://github.com/account/organizations/new." ### Step 4: Discover Workflows Fetch all workflow files in one API call: ```bash cat <<'QUERY' | gh api graphql --input - { "query": "query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id nameWithOwner defaultBranchRef { name target { oid } } object(expression: \"HEAD:.github/workflows\") { ... on Tree { entries { name object { ... on Blob { text } } } } } } }", "variables": { "owner": "OWNER", "repo": "REPO" } } QUERY ``` Note: `HEAD` in the GraphQL expression is a Git ref, not a placeholder — do not replace it. If the response is truncated or errors due to size, fall back to the REST API: fetch the default branch and HEAD SHA via `gh api repos/OWNER/REPO --jq '.default_branch'` and `gh api repos/OWNER/REPO/git/ref/heads/DEFAULT_BRANCH --jq '.object.sha'`, then fetch workflow file names via `gh api repos/OWNER/REPO/contents/.github/workflows` and each file individually via `gh api repos/OWNER/REPO/contents/.github/workflows/FILE_NAME`. The response `content` field is base64-encoded — decode it before scanning for `runs-on:` values. Save `defaultBranchRef.name` (or REST default branch), `.target.oid` / HEAD SHA, and workflow `entries[]`. Scan each `.yml`/`.yaml` file for: - direct `runs-on:` string values matching supported runners, and - matrix-driven patterns (e.g., `runs-on: ${{ matrix.os }}`) where the matrix values include supported runner labels. Show the user a summary including: - Workflows that will be migrated and which runners are being replaced - Any Ubuntu runners NOT in the supported list (e.g., `ubuntu-22.04`, `ubuntu-20.04`, larger runners like `ubuntu-latest-16-cores`) listed as "Not migrated — unsupported runner label" If no direct or matrix-backed supported runners match, stop. ### Step 5: Preview Changes **Never create a PR without the user confirming changes first.** Show what will change per workflow: - Replace supported runners in `runs-on:` string values with `starsling-ubuntu-24.04` (preserve quotes/comments) - Replace matching `runner:`/`os:` values in `matrix.include` sections - Skip commented lines **Complex `runs-on` patterns:** - **Array syntax** (e.g., `runs-on: [self-hosted, linux, ubuntu-latest]`): Only replace the matching label within the array; do not collapse to a single string - **Group/labels syntax** (e.g., `runs-on: { group: ..., labels: [...] }`): Skip and flag for manual review - **Expressions** (`${{ matrix.os }}`, ternary/conditional): If `runs-on` uses `${{ matrix.os }}` and the matrix values are hardcoded runner labels, replace the labels in the matrix definition and flag the workflow for manual verification. If the matrix values come from other expressions, skip entirely and flag for manual review **YAML fidelity:** Change ONLY the `runs-on` and matrix values. Preserve the original file exactly: same indentation, key ordering, comments, blank lines, and trailing newline. The PR diff should show only the runner label changes. ### Step 6: Create PR **Branch:** Before creating, check for existing branches: `gh api repos/OWNER/REPO/git/matching-refs/heads/migrate-starsling-ubuntu-2404 --jq '.[].ref'`. If any exist, find the highest numeric suffix and increment by 1 (if none have a suffix, use `-2`). Then create: `gh api repos/OWNER/REPO/git/refs -f ref=refs/heads/BRANCH_NAME -f sha=HEAD_OID`. **Atomic commit** — all files in one commit, contents base64-encoded: ```bash cat <<'MUTATION' | gh api graphql --input - { "query": "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }", "variables": { "input": { "branch": { "repositoryNameWithOwner": "OWNER/REPO", "branchName": "BRANCH_NAME" }, "message": { "headline": "Migrate N CI workflows to StarSling Runners", "body": "Replaced runners per file:\n- file1.yml: ubuntu-latest → starsling-ubuntu-24.04\n- file2.yml: ubuntu-24.04 → starsling-ubuntu-24.04" }, "expectedHeadOid": "HEAD_OID", "fileChanges": { "additions": [ { "path": ".github/workflows/FILE_NAME", "contents": "BASE64_CONTENT" } ] } } } } MUTATION ``` Verify the response contains a valid `commit.oid`. If the mutation returned an `expectedHeadOid` mismatch, re-fetch the HEAD SHA (Step 4) and retry the commit once. For any other errors, report and stop — do not create a PR against a failed commit. **Commit message:** The headline should read `Migrate N CI workflows to StarSling Runners` where N is the count of modified workflow files (`.yml` + `.yaml`). List each file and the runner label(s) replaced in the body, as shown in the example above. **PR** via `gh pr create --repo OWNER/REPO --head BRANCH_NAME --base DEFAULT_BRANCH --title "Migrate N CI workflows to StarSling Runners" --body "..."` with this body structure: ``` ## Summary Migrates CI workflows from GitHub-hosted runners to [StarSling Runners](https://docs.starsling.dev) for faster builds and AI-powered optimizations. ## Changes - `file1.yml`: `ubuntu-latest` → `starsling-ubuntu-24.04` - `file2.yml`: `ubuntu-24.04` → `starsling-ubuntu-24.04` ## Not Migrated - `file3.yml`: Uses `${{ matrix.os }}` — requires manual review (or "All workflows migrated successfully.") ## After Merging Workflows will automatically run on StarSling Runners. Ensure the [StarSling GitHub App](https://github.com/apps/starslingdev) is installed with access to this repo. ``` ### Step 7: Monitor (Optional) Ask the user if they'd like help monitoring after merge. If yes, explain they can return after merging and you'll check with: ```bash gh run list --repo OWNER/REPO --branch DEFAULT_BRANCH --limit 5 --json status,conclusion,name,createdAt ``` Look for runs created after the merge. If any show `queued` for more than 2 minutes, suggest checking the GitHub App installation and repo access at https://github.com/apps/starslingdev. ## Error Handling | Error | Solution | |-------|----------| | `gh` not found or not logged in | Install from https://cli.github.com/, run `gh auth login` | | 403 / insufficient permissions | User needs write access to the repository — check collaborator status or org role | | App not installed | Install from https://github.com/apps/starslingdev | | Personal repo (owner type `User`) | StarSling requires an org repo — create one at https://github.com/account/organizations/new | | Repository not found | Check repo name and permissions | | No workflows found | Ensure `.github/workflows/` exists | | Runner not available after merge | Verify app has repo access at https://github.com/apps/starslingdev | | Rate limit exceeded | Wait a few minutes and retry | | 422 on branch creation | Branch exists — append a number suffix | | `expectedHeadOid` mismatch | Re-fetch HEAD SHA and retry the commit | | `${{ matrix.os }}` / complex `runs-on` | Require manual review — determined at runtime | | GraphQL response truncated | Fall back to REST API for individual file fetches | ````
### Option B: Manual Update Change your `runs-on` label from GitHub-hosted to StarSling: ``` starsling-ubuntu-24.04 ``` ### Before ```yaml title=".github/workflows/ci.yml" jobs: build: runs-on: ubuntu-latest # [!code highlight] steps: - uses: actions/checkout@v4 # ... your build steps ``` ### After ```yaml title=".github/workflows/ci.yml" jobs: build: runs-on: starsling-ubuntu-24.04 # [!code highlight] steps: - uses: actions/checkout@v4 # ... your build steps ``` That's it. Push your changes and watch your builds fly. ## Label Mapping Use `starsling-ubuntu-24.04` (4 vCPU / 16 GB) to replace these GitHub-hosted runner labels: | GitHub-hosted Label | StarSling Label | | ------------------- | ------------------------ | | `ubuntu-latest` | `starsling-ubuntu-24.04` | | `ubuntu-24.04` | `starsling-ubuntu-24.04` | StarSling also offers additional runner sizes (2, 8, 16, 32, and 64 vCPU). See [Instance Types](https://docs.starsling.dev/runners/instance-types) for the full label reference. ## What's Next? # Instance Types (/runners/instance-types) StarSling Runners are built on Linux Ubuntu with pre-installed tools for common CI/CD workflows. ## Runner Labels Choose a runner label based on the compute resources your job requires. For non-default sizes, the numeric suffix on each label indicates the vCPU count (e.g. `-8` = 8 vCPU). The label without a suffix is the 4 vCPU default. | Label | OS | vCPU | Memory | Price per minute | | --------------------------- | ------------ | ---- | ------ | ---------------- | | `starsling-ubuntu-24.04-2` | Ubuntu 24.04 | 2 | 8 GB | $0.004 | | `starsling-ubuntu-24.04` | Ubuntu 24.04 | 4 | 16 GB | $0.008 | | `starsling-ubuntu-24.04-8` | Ubuntu 24.04 | 8 | 32 GB | $0.016 | | `starsling-ubuntu-24.04-16` | Ubuntu 24.04 | 16 | 64 GB | $0.032 | | `starsling-ubuntu-24.04-32` | Ubuntu 24.04 | 32 | 128 GB | $0.064 | | `starsling-ubuntu-24.04-64` | Ubuntu 24.04 | 64 | 256 GB | $0.128 | The `starsling-ubuntu-24.04` label (4 vCPU / 16 GB) is the default and replaces the following GitHub-hosted runner labels: * `ubuntu-latest` * `ubuntu-24.04` ## Example Usage ```yaml title=".github/workflows/build.yml" jobs: build: runs-on: starsling-ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install and Build run: | npm ci npm run build ``` # Compute Specifications (/runners/compute-sizing) StarSling offers two runner families: CPU runners on 5th Gen AMD EPYC processors, and GPU runners backed by NVIDIA cards. Choose a runner based on the compute resources your job requires. ## CPU Specifications | Label | vCPU | Memory | Price per minute | | --------------------------- | ---- | ------ | ---------------- | | `starsling-ubuntu-24.04-2` | 2 | 8 GB | $0.004 | | `starsling-ubuntu-24.04` | 4 | 16 GB | $0.008 | | `starsling-ubuntu-24.04-8` | 8 | 32 GB | $0.016 | | `starsling-ubuntu-24.04-16` | 16 | 64 GB | $0.032 | | `starsling-ubuntu-24.04-32` | 32 | 128 GB | $0.064 | | `starsling-ubuntu-24.04-64` | 64 | 256 GB | $0.128 | The numeric suffix on each label indicates the vCPU count (e.g. `-8` = 8 vCPU). The label without a suffix is the 4 vCPU default and replaces `ubuntu-latest` and `ubuntu-24.04`. All sizes run Ubuntu 24.04 on 5th Gen AMD EPYC. ## GPU Specifications GPU runners are in **private beta**. Contact [founders@starsling.dev](mailto:founders@starsling.dev) to request access. | Label | GPU | vCPU | Memory | Disk | Price per minute | | -------------------------------------------- | ------------ | ---- | ------ | ------ | ---------------- | | `starsling-ubuntu-24.04-gpu` | RTX PRO 6000 | 4 | 16 GB | 100 GB | $0.05922 | | `starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1` | RTX 5090 | 4 | 16 GB | 100 GB | $0.03022 | | `starsling-ubuntu-24.04-gpu/gpus=rtx-4090:1` | RTX 4090 | 4 | 16 GB | 100 GB | $0.02522 | All cards are NVIDIA. Prices are all-in: each rate covers the GPU and the host it runs on, billed per minute like any other runner. ### Selecting a card The `gpus=` selector takes a card SKU and a card count, `gpus=:`. While GPU runners are in beta they are single-card, so the count is always `1`. `starsling-ubuntu-24.04-gpu` is shorthand for the default card — it is equivalent to `starsling-ubuntu-24.04-gpu/gpus=rtx-pro-6000:1`. See the [Label Reference](https://docs.starsling.dev/configuration/label-reference#gpu-labels) for the full label grammar and the list of card SKUs. ## Performance Characteristics StarSling runners deliver excellent performance for most CI workloads: * **Web Applications (React, Next.js, Vue)**: Fast installs and builds * **Backend Services (Go, Rust, Java)**: Efficient parallel compilation * **Test Suites**: Parallel test execution Larger sizes (8 vCPU and above) are well-suited for monorepo builds, large test matrices, and compilation-heavy workloads. ## Example Usage ```yaml runs-on: starsling-ubuntu-24.04 ``` For a larger size: ```yaml runs-on: starsling-ubuntu-24.04-8 ``` On a GPU runner: ```yaml runs-on: starsling-ubuntu-24.04-gpu ``` To pick a specific card: ```yaml runs-on: starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1 ``` ## Optimization Tips 1. **Cache dependencies**: Use `actions/cache` to avoid repeated downloads 2. **Use matrix builds**: Split tests across multiple runners for parallelism 3. **Right-size your runner**: Larger isn't always better — measure before scaling up # Benchmarks (/performance/benchmarks) StarSling runners can deliver up to 6x faster build times compared to GitHub-hosted runners, depending on workload characteristics. The best way to see the difference is to benchmark your own workflows — the guide below shows how. ## Real-World Results What StarSling delivered for real teams after they switched `runs-on` — faster runners plus the optimizations StarSling agents shipped and the team merged: | Repo | Metric | GitHub-hosted | StarSling | Improvement | | ---------------------------------------------------------- | -------------------- | ------------- | --------- | ---------------- | | [Mastra](https://starsling.dev/customers/mastra) | Slowest test suite | \~30m | \~5m | **\~6x faster** | | [Mastra](https://starsling.dev/customers/mastra) | Job queue under load | \~15m | under 2m | **\~8x shorter** | | [Better Auth](https://starsling.dev/customers/better-auth) | E2E runtime per job | 2m 22s | 1m 04s | **\~2x faster** | Across all its CI and E2E jobs, Better Auth saves roughly **20,000 CI minutes every month**. These are live customer results — each links to its case study with the full methodology and the agent-authored PRs behind the numbers. ## Why StarSling is Faster ### 1. 30% Faster Hardware | Spec | GitHub-hosted | StarSling | | ------ | ----------------------------- | ---------------- | | CPU | 4th Gen AMD EPYC\* | 5th Gen AMD EPYC | | Cores | 4 vCPU | 4 vCPU | | Memory | 16 GB | 16 GB | 5th Gen AMD EPYC processors deliver \~30% better single-threaded performance over 4th Gen, directly improving build and test execution times. ** Not publicly disclosed by GitHub in [their docs](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) but inferred based on publicly available information.* ### 2. Unlimited Concurrency No concurrency limits means every job gets a runner quicker, even during peak CI loads. ## Run Your Own Benchmark Compare your actual workflows side by side — run the same steps on a GitHub-hosted runner and a StarSling runner, then compare the timings: ```yaml title=".github/workflows/benchmark.yml" jobs: github-hosted: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: time npm ci && time npm run build starsling: runs-on: starsling-ubuntu-24.04 steps: - uses: actions/checkout@v4 - run: time npm ci && time npm run build ``` # Optimizations (/ai-agents/optimizations) StarSling's AI agents continuously perform deep scans of your workflows and open PRs to optimize your CI speed. See [real examples of AI-authored PRs](https://starsling.dev/#ai-prs) or browse the [customer case studies](https://starsling.dev/customers). This page covers StarSling's paid, hosted, continuous optimization service. To run a free, local audit on demand with your own coding agent, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup), or grade your workflow configuration against CI best practices with [ci-score](https://docs.starsling.dev/skills/ci-score). For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default. ## How It Works StarSling monitors your CI workflows and identifies opportunities to improve performance, reliability, and efficiency. When an optimization is found, StarSling opens a pull request with the suggested changes and an explanation. ## What Gets Optimized ### Caching Detect missing or misconfigured caches and add them automatically. * [Better Auth](https://starsling.dev/customers/better-auth): fixed a Turborepo cache that wasn't hitting between runs and cached Playwright browser installs. * [Partcl](https://starsling.dev/customers/partcl): cached build artifacts across shards. ### Dependency installation Suggest faster install strategies (e.g., frozen lockfiles, parallel installs). ### Build steps Identify redundant or slow steps that can be parallelized or removed. * [Mastra](https://starsling.dev/customers/mastra): sharded the E2E kitchen-sink suite across 3 parallel jobs. * [Partcl](https://starsling.dev/customers/partcl): parallelized a test suite the team had never had time to split. ### Test reliability Replace fixed `sleep` calls with real readiness checks and add service healthchecks. * [Mastra](https://starsling.dev/customers/mastra): swapped blind sleeps for polling across MongoDB and Chroma and added Docker healthchecks. * [Better Auth](https://starsling.dev/customers/better-auth): added Docker Compose healthchecks. ### Workflow structure Recommend job splitting, matrix strategies, and dependency ordering. ### Runner configuration Flag suboptimal runner labels or resource usage. * [Partcl](https://starsling.dev/customers/partcl): right-sized the heaviest jobs from 64-core down to 8-core machines. ## How Optimizations Are Delivered 1. StarSling analyzes your workflow runs and configuration 2. When an optimization is identified, a PR is opened with the changes 3. Each PR includes an explanation of what changed and why 4. You review and merge at your own pace > "Within a day of migrating to StarSling Runners, their agents opened up a PR that literally made our Rust CI tests 2x faster!" > > — Vamshi Balanaga, Co-founder & CTO, [Partcl](https://starsling.dev/customers/partcl) ## See It In Action Real CI optimizations StarSling agents have shipped, with measured before/after numbers: See all [StarSling case studies →](https://starsling.dev/customers). # ci-speedup (/skills/ci-speedup) [ci-speedup](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, analyzes GitHub Actions, and gives your coding agent evidence to draft a fix. It works independently of StarSling Runners and does not require the StarSling GitHub App. [See the ci-speedup overview](https://starsling.dev/ci-speedup) for a quick introduction before following the operational guide below. ci-speedup is a local, on-demand audit that you invoke with your own coding agent. To grade your workflow configuration against CI best practices instead, use [ci-score](https://docs.starsling.dev/skills/ci-score); to scan it for attack vectors, use [ci-secure](https://docs.starsling.dev/skills/ci-secure). [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service that continuously analyzes your CI and opens reviewable PRs once enabled. ## Before you start You need: * The [GitHub CLI](https://cli.github.com/) authenticated to GitHub. Run `gh auth status` to check. * Read access to the target repository's GitHub Actions runs, jobs, and logs. * Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/). * Node.js and npm to install or invoke the skill with `npx`. ## Install and run Install the skill with Vercel's Skills CLI: ```bash npx skills add starslingdev/skills ``` The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode. In a repository, invoke the installed skill from your coding agent: ```text /ci-speedup ``` The skill confirms the repository before it starts reading run data. To use it without installing, ask your coding agent to run this command and follow the generated instructions: ```bash npx skills use "https://github.com/starslingdev/skills" --skill "ci-speedup" ``` ## On demand versus continuous | | ci-speedup | StarSling optimization PRs | | ------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | | **Delivery** | Agent skill you invoke | Hosted optimization service | | **Execution environment** | Your machine, coding agent, and authenticated `gh` CLI | StarSling's hosted agents and GitHub App | | **Cost and availability** | Free, open source, and independent of StarSling Runners | Available on paid plans once enabled; not enabled by default for new accounts | | **Trigger** | On demand, when you run it | Continuous analysis as workflows change | | **Output** | Diagnosis and evidence-backed prompt for your coding agent | Reviewable optimization PRs opened by StarSling | ## Resources # ci-score (/skills/ci-score) [ci-score](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, grades a repository's GitHub Actions configuration against CI best practices, and hands your coding agent a concrete fix for every gap. It works independently of StarSling Runners and does not require the StarSling GitHub App. [See the ci-score overview](https://starsling.dev/ci-score) for a quick introduction before following the operational guide below. ci-score grades configuration adherence, not speed — a fast repo can hold a low score. For a measured audit of what actually makes your CI slow, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup); for the ten critical attack vectors, use [ci-secure](https://docs.starsling.dev/skills/ci-secure). All three are local, on-demand skills; [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service. ## Before you start You need: * A full local checkout of the repository you want to score. The score reads workflow YAML, local composite actions, and repo-root build-tool configs, so a partial view understates your setup. * Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/). * Node.js and npm to install or invoke the skill with `npx`. No network access is used during scoring — everything reads the local tree, and nothing is sent to StarSling. ## Install and run Install the skill with Vercel's Skills CLI: ```bash npx skills add starslingdev/skills ``` The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode. In a repository, invoke the installed skill from your coding agent: ```text /ci-score ``` The skill confirms the repository before it starts scoring. To use it without installing, ask your coding agent to run this command and follow the generated instructions: ```bash npx skills use "https://github.com/starslingdev/skills" --skill "ci-score" ``` ## What the score measures The CI Score is a pass/fail rubric of eleven configuration facts, each verifiable in your own workflow files in under a minute: * **Dependency caching** — a cache action or a `setup-*` cache input is configured * **Build caching** — a build-tool cache (Turbo, Nx, Gradle, sccache, Bazel) is configured * **Shallow checkout** — no PR-gating checkout pulls full history with `fetch-depth: 0` * **Test sharding / matrix** — a test job runs a matrix or a shard-like axis * **Change-scoped builds** — CI scopes work to what changed * **Concurrency groups** — a PR-triggered workflow declares a concurrency group * **Superseded runs cancelled** — that concurrency group sets `cancel-in-progress` * **Path filters** — a PR-triggered workflow scopes itself with `paths` / `paths-ignore` * **Job timeouts** — jobs set `timeout-minutes` instead of GitHub's 360-minute default * **Scoped OIDC id-token** — `id-token: write` is granted per job, never workflow-wide * **Pinned action SHAs** — at least 95% of remote action references are pinned to a commit SHA A check whose subject doesn't exist in your repo is not applicable and leaves the denominator. Your score is checks passed over checks applicable. The report ranks one fix per failed check by impact × risk, each with a fix recipe and a paste-able agent prompt. Two caveats the report repeats beside the score: it measures **adherence, not speed**, and it is **not a security audit** — exactly two of the eleven checks (action pinning and OIDC token scoping) happen to be security-related. ## ci-score versus ci-speedup | | ci-score | ci-speedup | | ------------ | ----------------------------------------------- | ---------------------------------------------------------- | | **Question** | Does my CI config follow best practices? | What actually makes my CI slow? | | **Input** | Your local checkout's workflow YAML and configs | Real run history sampled over the `gh` API | | **Output** | A score plus one ranked fix per failed check | A measured root-cause report of the merge-gating long pole | | **Requires** | A full checkout, Python, and PyYAML | The above plus an authenticated `gh` CLI | | **Runtime** | Seconds, fully offline | Minutes, scaling with repo size | Both are free, open source, run on your machine, and send nothing to StarSling. ## Resources # ci-secure (/skills/ci-secure) [ci-secure](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, scans a repository's GitHub Actions workflows for the ten critical CI/CD attack vectors, and offers your coding agent a fix for every finding. It works independently of StarSling Runners and does not require the StarSling GitHub App. [See the ci-secure overview](https://starsling.dev/ci-secure) for a quick introduction before following the operational guide below. ci-secure is deliberately **not** a comprehensive security audit. It checks critical exploit-chain vectors only — complete outsider-to-compromise paths with real incidents behind them. A clean ci-secure report means those ten paths are closed, not that your CI is secure. The [selection criteria and the rejection record](https://github.com/starslingdev/skills/blob/main/skills/ci-secure/references/why-these-ten.md) document what was left out and why. ci-secure scans for attack vectors. To grade your workflow configuration against CI best practices, use [ci-score](https://docs.starsling.dev/skills/ci-score); to find what actually makes your CI slow, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup). All three are local, on-demand skills; [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service. ## Before you start You need: * A full local checkout of the repository you want to scan, with a `.github/workflows/` directory. The skill stops early if the repo has no GitHub Actions workflows. * Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/) — the scanner's only third-party dependency. * Node.js and npm to install or invoke the skill with `npx`. The [GitHub CLI](https://cli.github.com/) is **optional but recommended**. Four checks need it: the impostor-SHA vector, which cannot be answered from YAML alone, the dormancy note on findings, and two repository settings read over the API. Without `gh`, the scan still runs — the impostor-SHA check reports as skipped, and the two API-gated settings report as unmeasured coverage gaps rather than as passes. ## Install and run Install the skill with Vercel's Skills CLI: ```bash npx skills add starslingdev/skills ``` The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode. In a repository, invoke the installed skill from your coding agent: ```text /ci-secure ``` To use it without installing, ask your coding agent to run this command and follow the generated instructions: ```bash npx skills use "https://github.com/starslingdev/skills" --skill "ci-secure" ``` ## What it scans for Each of the ten vectors is a complete path from outside your organization to compromise, and each has a documented real-world incident behind it. Every finding renders with a "what an attacker could do" scenario rather than a rule name, so you can judge the risk without reading the detector. * **Template injection in `run:` blocks** — attacker-controlled text is interpolated into a shell script before the shell ever sees it, so a crafted branch name or issue title executes as code on the runner. * **Fork code executed with privileges** — a workflow on an untrusted trigger checks out and runs the attacker's head ref while holding secrets and a write token, the classic "pwn request". * **Cache poisoning from `pull_request_target`** — a job with fork code writes the shared cache, and the poisoned entry is then restored by trusted builds on your default branch. * **Impostor or unreachable action SHAs** — an action is pinned to a commit that is not reachable from the action's own repository, which looks like the safest possible pin while resolving to code its maintainers never published. * **Whole-context secret dumps** — `toJSON(secrets)`, `toJSON(github)`, or `toJSON(env)` written into logs or an artifact, exposing every secret at once. * **`$GITHUB_ENV` and `$GITHUB_PATH` hijack** — attacker-influenced text is appended to those files, letting an attacker set environment variables or prepend a directory to `PATH` for every later step. * **Write tokens on untrusted triggers** — `pull-requests: write` and similar grants handed to a workflow anyone can trigger from a fork. * **Credentials in caches and artifacts** — a cache or artifact `path:` that sweeps in known credential files such as `.npmrc`, `.git/config`, or a cloud credentials directory. * **Unverified remote code execution** — `curl | bash`, and fetch-and-run against a mutable ref, where whoever controls the remote host or branch controls what runs in your job. * **Dependency install scripts in privileged jobs** — lifecycle scripts from the dependency tree executing in a job that holds secrets, so one compromised transitive package reaches them. ## Configuration hygiene checks Alongside the vector scan, ci-secure reports eight pass/fail configuration facts. These are hygiene, not exploit chains — a failure is a weakened defense rather than a demonstrated path in: * **`permissions:` declared** — every workflow declares `permissions`, instead of inheriting a repository default that may be read-write everything. * **Write grants scoped to jobs** — no workflow-level `write` for any scope except `id-token`, which is [ci-score](https://docs.starsling.dev/skills/ci-score)'s check rather than this one, and no `permissions: write-all`. Write grants belong on the jobs that need them. * **CODEOWNERS covers workflows** — a CODEOWNERS entry covers `.github/workflows/`, so workflow changes need a specific approval rather than any approval. * **No fork code checked out on untrusted triggers** — bare untrusted triggers pass; checking out the attacker's ref is what fails. * **No blanket `secrets: inherit`** — reusable workflows are passed secrets by name, so a called workflow's blast radius is visible in the caller. * **Checkout credentials not persisted** — untrusted-trigger workflows set `persist-credentials: false`, keeping the token out of `.git/config` where a later step could read it. * **Required checks cannot be skipped** — every required status check is produced by a job that always runs. GitHub counts a skipped required check as a pass, so a check only a conditional job reports can be satisfied by never running it. * **Fork-PR approval is effective** — the repository's approval gate covers more than accounts brand new to GitHub, a setting that otherwise gates nobody real. Two of these read repository settings over the API, so they report as unmeasured rather than as passes when `gh` is unavailable. An unmeasured check is a coverage gap, not a clean bill. ## How the three skills differ | | ci-secure | ci-score | ci-speedup | | ----------------------- | ------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- | | **Question** | Can someone outside my org compromise my CI? | Does my CI config follow best practices? | What actually makes my CI slow? | | **Input** | Workflow YAML, plus four checks over the `gh` API | Your local checkout's workflow YAML and configs | Real run history sampled over the `gh` API | | **Output** | Findings with attacker scenarios, and a fix offer per finding | A score plus one ranked fix per failed check | A measured root-cause report of the merge-gating long pole | | **Requires** | A checkout, Python, and PyYAML; `gh` recommended | A full checkout, Python, and PyYAML | The above plus an authenticated `gh` CLI | | **Zero findings means** | Those ten paths are closed, not that CI is secure | Nothing — the score is the output | No single dominant long pole was found | All three are free, open source, run on your machine, and send nothing to StarSling. ## What it will and will not do The skill asks which findings you want fixed and dispatches one subagent per finding group. It **never commits, pushes, or opens a pull request unasked** — by default you review the working-tree diff yourself. Zero findings is a first-class result, reported as such rather than padded with lower-severity noise. ## Resources # Overview (/sling-cli) `sling` is the primary interface to the StarSling platform for agents and humans. The questions you ask most often are each a single command: [why](https://docs.starsling.dev/sling-cli/commands/analyze#sling-why) a job failed, where the [time](https://docs.starsling.dev/sling-cli/commands/analyze#sling-time) went, and what sits at the [top](https://docs.starsling.dev/sling-cli/commands/analyze#sling-top) of your spend. ```console copy="sling time" $ sling time 93165914090 Job Name: Lint, typecheck, test & spell · Wall-Clock: 58s · Queue-Wait: 2s PHASES TIME %-SHARE TIMELINE provision 4.0s 7.14% ██░░░░░░░░░░░░░░░░░░░░░░ checkout+patch 2.0s 3.57% ░░█░░░░░░░░░░░░░░░░░░░░░ steps 46.0s 82.14% ░░░███████████████████░░ Set up Bun 1.0s ░░░█░░░░░░░░░░░░░░░░░░░░ Set up mise tools 2.0s ░░░█░░░░░░░░░░░░░░░░░░░░ Install dependencies 1.0s ░░░░█░░░░░░░░░░░░░░░░░░░ Lint (Biome) 2.0s ░░░░█░░░░░░░░░░░░░░░░░░░ Lint shell (shellcheck) 14.0s ░░░░░██████░░░░░░░░░░░░░ Lint Dockerfiles (hadolint) 0.0s ░░░░░░░░░░░█░░░░░░░░░░░░ ▸ Typecheck 18.0s ░░░░░░░░░░░████████░░░░░ Test 7.0s ░░░░░░░░░░░░░░░░░░░███░░ Catalog drift 1.0s ░░░░░░░░░░░░░░░░░░░░░░█░ Spell (typos) 0.0s ░░░░░░░░░░░░░░░░░░░░░░█░ teardown 4.0s 7.14% ░░░░░░░░░░░░░░░░░░░░░░██ ``` ## Install On Apple Silicon macOS and x64 glibc Linux: ```bash curl -fsSL https://runners.starsling.dev/cli/install.sh | sh ``` Then sign in and confirm the install: ```bash sling login sling --version ``` `sling login` runs the GitHub device-code flow and saves your session **per user, not per shell** — so an agent running on the same machine inherits it with no further setup. For `PATH` setup, upgrades, digest verification, and `sling doctor`, see [Installation](https://docs.starsling.dev/sling-cli/installation). ## What you can do with it * [`sling why`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-why) classifies why a job failed, with evidence and a fix to run. * [`sling logs`](https://docs.starsling.dev/sling-cli/commands/inspect#sling-logs) filters server-side, returning only the logs that matter. * [`sling time`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-time) decomposes where the time went, phase by phase. * [`sling top`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-top) ranks what sits at the top of your spend. * [`sling usage`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-usage) attributes runner minutes and cost, grouped how you ask. * [`sling bill`](https://docs.starsling.dev/sling-cli/commands/billing#sling-bill) is a read-only budget check for the open period. ## Why it exists AI agents working inside CI jobs on StarSling runners, and the humans supervising them, have no first-class way to answer the questions CI work actually consists of: why did this job fail, where does the wall-clock go, what is burning our runner minutes, show me the logs that matter. Those answers otherwise live behind the GitHub UI (hostile to agents), raw APIs (no diagnosis, no aggregation), and dashboards (exports, not interfaces). Agents end up screen-scraping logs into their context windows, guessing at failure causes, and unable to branch on outcomes without parsing prose. Humans get no leverage from the telemetry StarSling already collects. ## Learn more # Installation (/sling-cli/installation) ## MacOS and Linux ```bash curl -fsSL https://runners.starsling.dev/cli/install.sh | sh ``` The installer detects your platform, downloads the matching build, verifies its SHA-256 digest, and activates it. If `~/.local/bin` is not on your `PATH`, the installer prints the `export` line to add. It supports Apple Silicon macOS and x64 glibc Linux. Check the install: ```bash sling --version ``` To upgrade, rerun the same command. ## Get Started ### Authenticate ```bash sling login ``` This runs the GitHub device-code flow: it opens an approval page, waits for you to confirm the device code, saves the session token to `~/.config/sling/credentials`, and links your default organization. The credential is stored **per user, not per shell**, with owner-only permissions. So an AI agent running on the same machine as you inherits it: once you have signed in, the agent can run any `sling` command with no further setup. See [Configuration](https://docs.starsling.dev/sling-cli/configuration#how-agents-authenticate) for how that differs in CI. ### Check your identity ```bash sling whoami ``` Reports your identity, org, plan, token scopes, and token expiry, so you can see exactly what the credential you just saved can reach. ### Verify ```bash sling doctor ``` `doctor` checks your token, the control plane's reachability, clock skew, git remote, patch tooling, version, org resolution, and whether the [sling agent skill](https://github.com/starslingdev/skills/tree/main/skills/sling) is installed for a coding agent on the machine. Exit `0` means healthy; exit `10` means a real check failed. A row marked `!` is an advisory — a newer CLI, or a coding agent without the skill — and still counts as healthy; `○` marks a check that could not run. ```console $ sling doctor ✓ token valid session (expires 4 Aug 2026, 17:45) ✓ control_plane reachable (https://runners.starsling.dev) ✓ clock_skew 0s vs server ✓ git_remote origin → https://github.com/acme/api.git ✓ patch_tooling git found (/usr/bin/git) ✓ version up to date (0.1.8) ✓ org starslingdev (paid) ✓ agent_skill sling skill installed for Claude Code Environment healthy! ``` ## Next steps # Authentication and setup (/sling-cli/commands/auth) Five commands cover getting `sling` working and confirming it still is. ## `sling login` ```text sling login [--force] [--clear] ``` Signs in with the **GitHub device-code flow**: requests a code, opens the approval page, waits for you to approve in the browser, saves the credential to `~/.config/sling/credentials`, then makes a best-effort attempt to set your default org. If you are already signed in it short-circuits — verifying the token still works rather than re-running the flow. The device-code flow is deliberate: it needs no localhost callback, so you can sign in from any terminal, including over SSH. | Flag | Meaning | | ----------------------- | --------------------------------------------------------------------- | | `--force` | Re-authenticate even when a valid session exists. | | `--clear` | Clear the stored credential; an alias for `logout --yes`. | | `--agent`, `--no-input` | Refuse: the device-code flow cannot run non-interactively (exit `2`). | ```console $ sling login Opened https://runners.starsling.dev/cli-login?user_code=AB58-LRFS in your browser. Confirm code AB58-LRFS to continue. Waiting for you to approve… Sling CLI v0.1.0 ✓ Successfully authenticated! ✓ Token saved to ~/.config/sling/credentials ✓ You're now linked to acme organization! ``` An agent on this machine inherits the credential this writes, so signing in once covers both of you. A fresh CI container has no session and no browser: see [how agents authenticate](https://docs.starsling.dev/sling-cli/configuration#how-agents-authenticate). ## `sling logout` ```text sling logout [--yes] ``` Clears the stored credential **and the persisted default org**, so a stale org cannot leak into the next login under a different account. On a terminal it asks first. | Flag | Meaning | | ------------ | ------------------------------------------------------------------------------------ | | `--yes` | Skip the confirmation — explicit consent to clear. | | `--agent` | Machine-mode consent; bundles `--yes`. | | `--no-input` | **Not** consent. Fails with exit `2` rather than prompting when a credential exists. | ```console $ sling logout Are you sure you want to sign out? (y/N) y ✓ Successfully signed out! ✓ Token cleared from ~/.config/sling/credentials ``` ## `sling whoami` ```text sling whoami [--json] ``` Your identity, org, plan, token scopes, and token expiry — so you can debug access issues without guessing. Session callers see the default org enriched with its plan; API-key callers also see the key id and its scopes. | Flag | Meaning | | ------------------- | ----------------------------------------------------------------------- | | `--json`, `--agent` | Machine output — the `/api/whoami` payload on stdout, errors on stderr. | ```console $ sling whoami Identity Name Lionel Messi GitHub leo_messi Email leomessi@example.com User ID leo_messi_10 Organization Default arg Plan enterprise Credential Type session Expires 1 Aug 2026, 00:00 ``` The human table renders on stderr, so stdout stays pipe-clean. Calls [`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami). ## `sling doctor` ```text sling doctor [--json] ``` Diagnoses a broken setup in one command: **binary version, token validity and scopes, org resolution, git remote detection, control-plane reachability, clock skew, patch-tooling presence**, and whether the [sling agent skill](https://github.com/starslingdev/skills/tree/main/skills/sling) is installed for a coding agent. Exit `0` when healthy, **`10`** when a real check fails — so an agent harness can preflight the environment before starting work. Each row carries a mark: `✓` passed, `○` **skipped** — the check could not run, like the version comparison when the control plane is unreachable, or `agent_skill` when no coding agent is installed — and `!` **advisory**, a suggestion worth acting on. Only `✗` fails the run. | Flag | Meaning | | ------------------- | ---------------------------------------------------------------------------------------------- | | `--json`, `--agent` | Machine output — `{ checks: [{ key, ok, detail, skipped?, warn?, fix_command? }] }` on stdout. | ```console $ sling doctor --json { "checks": [ { "key": "token", "ok": true, "detail": "valid session (expires 2026-08-04T17:45:00.000Z)" }, { "key": "control_plane", "ok": true, "detail": "reachable (https://runners.starsling.dev)" }, { "key": "clock_skew", "ok": true, "detail": "0s vs server" }, { "key": "git_remote", "ok": true, "detail": "origin → https://github.com/acme/api.git" }, { "key": "patch_tooling", "ok": true, "detail": "git found (/usr/bin/git)" }, { "key": "version", "ok": true, "detail": "up to date (0.1.8)" }, { "key": "org", "ok": true, "detail": "starslingdev (paid)" }, { "key": "agent_skill", "ok": true, "detail": "sling skill installed for Claude Code" } ] } ``` A real failing check carries a `fix_command` and flips the exit code to `10`. A skipped check — `ok: false` with `skipped: true` — is informational and keeps the run healthy. An **advisory** check is the third state: `ok: true` with `warn: true`, the remedy in `fix_command`, and the exit code still `0`. An outdated binary suggests the installer one-liner; `agent_skill` suggests the skill install when a coding agent is present but the skill is not. With no coding agent on the machine, `agent_skill` is skipped rather than nagging. ```json title="advisory and skipped rows" { "key": "version", "ok": true, "warn": true, "detail": "version 0.1.8\n0.1.9 is available, to update:", "fix_command": "curl -fsSL https://runners.starsling.dev/cli/install.sh | sh" } { "key": "agent_skill", "ok": true, "warn": true, "detail": "not installed — the sling skill lets your coding agent run sling for you, to install:", "fix_command": "npx skills add starslingdev/skills --skill sling" } { "key": "agent_skill", "ok": false, "skipped": true, "detail": "no supported coding agent detected — sling is usable directly" } ``` ## `sling org switch` ```text sling org switch [slug] ``` Sets your **default org** so later commands resolve it without a flag. Pass a slug to persist it directly, or omit it on a terminal for an arrow-key picker. An unknown slug exits `2` and lists your valid orgs. | Flag | Meaning | | ------------------- | ------------------------------------------------------------------------------- | | `slug` | The org to make default. Omit for the interactive picker. | | `--json`, `--agent` | Machine JSON — `{"defaultOrg":"…"}` on stdout. A missing slug is a usage error. | | `--no-input` | Refuse the picker when no slug is given (exit `2`). | ```console $ sling org switch beta You're now linked to beta organization! # no slug on a TTY → picker $ sling org switch Select your default org (↑/↓, Enter; Esc to cancel): ❯ acme beta ``` Your org is auto-resolved when it is unambiguous, so this is only needed on multi-org accounts — the point is that they do not pay a flag tax on every call. Either way it is a CLI convenience: the HTTP API never infers an org, and [`org` is a required parameter](https://docs.starsling.dev/api/credentials#org-scoping) on every org-scoped endpoint. Use [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) to list the slugs you can use. # Inspecting CI (/sling-cli/commands/inspect) Four commands for navigating what actually ran. They share one primitive — polymorphic id resolution — so all of them accept a prefixed id, a bare number, or a pasted GitHub Actions URL. ## Identifiers | Form | Meaning | | ------------------ | -------------------------------------------------------------------------------- | | `run_` | One workflow run. | | `job_` | One job within a run. | | `att_.` | One execution of a job. Retries create new attempts; earlier ones are preserved. | | `runner_` | The GitHub Actions runner a job ran on. | Bare GitHub ids and **GitHub Actions UI URLs are accepted anywhere** a run, job, or attempt id is — so you can paste a URL straight from the browser without translating identifiers. Resolution is polymorphic: an id resolves downward to the most specific sensible target. Where several candidates match, the command lists them and exits `2` — it never picks silently, so you are never handed the wrong target. ## `sling resolve` ```text sling resolve [--target run|job|attempt] ``` Resolves a polymorphic id to its concrete target. This is the primitive the id-taking commands share, exposed on its own so you can see what an id maps to. | Flag | Meaning | | ---------------------------- | ----------------------------------------------------------------------- | | `--target run\|job\|attempt` | Bias resolution to one level when the id is ambiguous. | | `--json`, `--agent` | Machine output — the resolved target, or `{ candidates }` on ambiguity. | ```console $ sling resolve run_2990884649 run_2990884649 (run) acme/api # ambiguous → candidates (exit 2) $ sling resolve 88886665361 Ambiguous — 2 candidates; pass one: run_88886665361 (acme/api) job_88886665361 typecheck (acme/api) ``` **Exit codes:** `0` resolved · `2` usage or ambiguous · `3` not found · `4` not signed in · `5` control-plane failure. Calls [`POST /api/resolve`](https://docs.starsling.dev/api/resolve/resolve-target). ## `sling runs` Inspect workflow runs. `list` (alias `ls`) pages newest-first with server-side filters; `show` (alias `get`) renders one run and can block until it concludes. ### `sling runs list` ```text sling runs list [filters] [--limit n] [--cursor c] ``` | Flag | Meaning | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `--branch`, `--status`, `--conclusion`, `--trigger`, `--workflow-path`, `--label` | Server-side filters; combine freely. | | `--window`, `--month`, `--from` / `--to` | Time window. Mutually exclusive; passing two is a usage error. | | `--limit`, `--cursor` | Page size, and the cursor to page with. `has_more` prints the next `--cursor`. | | `--json`, `--agent` | Machine mode — `{ runs, has_more, next_cursor }` on stdout. | ```console $ sling runs list --status in_progress --limit 3 RUN WORKFLOW BRANCH EVENT STATUS DUR JOBS CREATED 2990884649 Test main push in_progress — 4 2026-07-23 14:02 2990884101 Prebuild main push queued — 1 2026-07-23 13:58 ``` Calls [`GET /api/runs`](https://docs.starsling.dev/api/runs/list-runs). ### `sling runs show` ```text sling runs show [--wait [--fail-fast] [--wait-timeout 30m]] ``` | Flag | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | | `--wait` | Poll until the run reaches a terminal state, then exit `0` on success or **`10`** on any non-success conclusion. | | `--fail-fast` | With `--wait`, return as soon as a job fails instead of waiting for the rest. | | `--wait-timeout ` | Cap the wait, e.g. `30m`. | | `--json`, `--agent` | Machine mode — the run detail on stdout. | ```console $ sling runs show run_2990884649 --wait; echo $? …run concludes… 10 # a non-success conclusion under --wait ``` `--wait` is what lets a script or agent gate on a run. Exit `10` is not an error — the command worked and the *run* failed. See [exit codes](https://docs.starsling.dev/sling-cli/configuration#exit-codes). **Exit codes:** `0` ok · `2` usage · `3` not found · `4` not signed in · `5` control-plane failure · `10` run concluded non-success under `--wait`. Calls [`GET /api/runs/{id}`](https://docs.starsling.dev/api/runs/get-run). ## `sling jobs` Inspect workflow jobs. `list` is scoped to **one run** (`--run`) or **one repo** (`--repo` plus a window) — exactly one is required. `show` renders one job; passing a *run* id is a usage error that redirects you to `runs show`. ### `sling jobs list` ```text sling jobs list --run | --repo [--conclusion c] [window] ``` | Flag | Meaning | | ---------------------------------------- | ----------------------------------------------------------- | | `--run ` \| `--repo ` | The scope. Mutually exclusive; one is required. | | `--conclusion` | Filter by conclusion — `failure`, `success`, and so on. | | `--window`, `--month`, `--from` / `--to` | Time window, with `--repo`. | | `--limit`, `--cursor` | Pagination. | | `--json`, `--agent` | Machine mode — `{ jobs, has_more, next_cursor }` on stdout. | ```console $ sling jobs list --run 2990884649 --conclusion failure JOB NAME STATUS LABEL ATTEMPTS DURATION RUN CREATED 88886665361 typecheck failure starsling-ubuntu-24.04 1 2.4m 2990884649 2026-07-23 14:02 $ sling jobs show run_2990884649 That id is a run, not a job — try `sling runs show`. # exit 2 ``` A terminal job shows its **conclusion**; an in-flight one shows its **status**. Calls [`GET /api/jobs`](https://docs.starsling.dev/api/jobs/list-jobs) and [`GET /api/jobs/{id}`](https://docs.starsling.dev/api/jobs/get-job). ## `sling logs` ```text sling logs [--job ] [--grep ] [--since ] [--timestamps] [--limit ] [--cursor ] [--output-file ] ``` Reads a run, job, or attempt's logs **filtered server-side** — so you fetch the failing lines, not the whole transcript. | Flag | Meaning | | ---------------------- | ----------------------------------------------------------------------------- | | `--job ` | For a run target, restrict to one job leg. | | `--grep ` | Server-side regex filter, e.g. `--grep '##\[error\]'`. | | `--since ` | Only lines newer than a trailing duration. | | `--timestamps` | Prefix each line with its timestamp. | | `--limit ` | Maximum lines per page. | | `--cursor ` | Resume from a `next_cursor`. | | `--output-file ` | Write to a file instead of stdout. | | `--json`, `--agent` | Emit a JSON page — `{ lines, has_more, next_cursor }` — instead of raw lines. | ```console $ sling logs job_88886665361 --grep '##\[error\]' ##[error]Process completed with exit code 101. ``` Unusually for `sling`, this command writes to **stdout in both modes**: the human path streams raw lines and auto-follows pages to the end. That makes `| head` and `| less` work naturally — closing the pipe early exits cleanly. **Exit codes:** `0` ok · `2` usage · `3` not found · `4` not signed in · `5` control-plane failure. Calls [`GET /api/logs/{id}`](https://docs.starsling.dev/api/logs/get-logs). # Analyzing CI (/sling-cli/commands/analyze) Five commands that answer analytical questions rather than listing records. Each is a first-class product surface, not a dashboard export. ## `sling usage` ```text sling usage [--org | --repo ] [--group-by ] [--order-by ] [--asc | --desc] [window] [--json | --agent] ``` Attributes StarSling runner minutes and dollar cost over a time window. | Flag | Meaning | | ------------------- | ------------------------------------------------------------------------------------------------- | | `--org`, `--repo` | Scope. `--repo owner/name` narrows to one repo; mutually exclusive. Defaults to your default org. | | `--group-by` | `label` (default) · `workflow` · `job` · `repo` · `day` | | `--order-by` | `cost` (default) · `minutes` · `jobs` · `key` | | `--asc` / `--desc` | Sort direction, default `--desc`. Naming both is a usage error. Not valid with `--group-by day`. | | `--window d` | Trailing window, e.g. `30d`. Defaults to 30 days. | | `--month ` | A calendar month. | | `--from` / `--to` | An explicit half-open range `[from, to)`. Both required together. | | `--json`, `--agent` | Machine mode. | ```console $ sling usage --org mastra-ai --window 7d --group-by repo StarSling usage — by repo 2026-07-07 → 2026-07-14 KEY RUNNER MIN JOBS COST $ % TOTAL mastra-ai/mastra 25459.0 10784 $202.85 89.3% mastra-ai/docs 3040.2 1120 $24.22 10.7% ``` The window flags are **mutually exclusive** — passing two is a usage error, not a silent precedence rule. A single-row result omits `% TOTAL`, since a lone row's share is always 100%. `--group-by day` renders a burn-down chart instead of a table. Cost is `minutes × vCPU × $0.002`, where minutes are **billed** minutes — `max(1, ceil(duration / 60s))` per job. A five-second job still costs a minute. ### The `plan` block Every response carries a top-level `plan` describing the caller's billing state, which shapes both the window and the human chrome on any `--group-by` axis: | Field | Meaning | | ------------------------------------------ | ------------------------------------------------------------------------------- | | `status` | `paid` · `free` · `blocked`. Paid orgs get no bar or banner. | | `period_source` | `stripe` · `install_cycle` · `calendar_month` · `explicit` — labels the window. | | `free_minutes_limit` / `free_minutes_used` | Free orgs only — the remaining lifetime grant, drawn as a bar. | | `blocked_reason` | Blocked orgs only — why jobs will not dispatch, shown as a red banner. | Fields that do not apply are **omitted, not `null`**. Machine mode passes the whole block through verbatim. **Exit codes:** `0` ok · `2` usage error · `4` not signed in · `5` control-plane failure. Calls [`GET /api/usage`](https://docs.starsling.dev/api/usage/get-usage). ## `sling top` ```text sling top [--by ] [--metric ] [--asc | --desc] [--org | --repo] [-n ] [window] [--json | --agent] ``` Ranks **what burns the most runner time and money** — the hotspots worth optimizing. Groups the same facts as `usage`, but sorts by a chosen metric and adds a period-over-period trend so a regression stands out. | Flag | Meaning | | ------------------- | ----------------------------------------------------------------------------------- | | `--by` | `workflow` (default) · `job` · `label` · `repo` · `branch` | | `--metric` | `runner-minutes` · `cost` · `jobs` · `p95-duration` · `p99-duration` · `queue-wait` | | `--asc` / `--desc` | Sort direction, default `--desc`. | | `--org`, `--repo` | Scope; mutually exclusive. | | `-n`, `--limit` | Row count. | | `--json`, `--agent` | Machine mode — ranked rows plus a `local` block. | ```console $ sling top --by workflow --metric cost --window 30d sling top — by workflow · cost · last 30d KEY · WORKFLOW REPO COST TREND Prebuild acme/api $528.35 ↑55.2% Test (multiple) $203.11 ↓12.3% Lint acme/web $44.90 — ``` `(multiple)` marks a key spanning several repos. **TREND** compares against the prior equal-length window, or shows `—` when there is no prior data. Calls [`GET /api/top`](https://docs.starsling.dev/api/top/get-top). ## `sling time` ```text sling time sling time --repo [window] ``` Decomposes CI **wall-clock into phases** — so you can see whether time goes to infrastructure or to your workload. Polymorphic, with a different analysis at each level: * A **job or attempt** splits wall-clock into the phase enum, with a per-step breakdown inside `steps`. * A **run** adds DAG analysis: the **critical path**, **parallelism efficiency** (busy-runner-seconds ÷ wall-clock × width), and the **blocking job** — the single job whose speedup most reduces run wall-clock, so optimization effort lands where it pays. * **`--repo`** aggregates p50/p95 per phase, per runner label, over a window — answering whether a repo's time goes to queue, provisioning, cache, or actual work. | Flag | Meaning | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `` | A run, job, or attempt id, or a pasted GitHub Actions URL. | | `--repo ` | The repo aggregate instead of a single target. | | `--window`, `--month`, `--from` / `--to` | **Only with `--repo`.** Passing one alongside an id is a usage error — a single target already names its window. | | `--json`, `--agent` | Machine mode. | ```console copy="sling time" $ sling time 93165914090 Job Name: Lint, typecheck, test & spell · Wall-Clock: 58s · Queue-Wait: 2s PHASES TIME %-SHARE TIMELINE provision 4.0s 7.14% ██░░░░░░░░░░░░░░░░░░░░░░ checkout+patch 2.0s 3.57% ░░█░░░░░░░░░░░░░░░░░░░░░ steps 46.0s 82.14% ░░░███████████████████░░ Set up Bun 1.0s ░░░█░░░░░░░░░░░░░░░░░░░░ Set up mise tools 2.0s ░░░█░░░░░░░░░░░░░░░░░░░░ Install dependencies 1.0s ░░░░█░░░░░░░░░░░░░░░░░░░ Lint (Biome) 2.0s ░░░░█░░░░░░░░░░░░░░░░░░░ Lint shell (shellcheck) 14.0s ░░░░░██████░░░░░░░░░░░░░ Lint Dockerfiles (hadolint) 0.0s ░░░░░░░░░░░█░░░░░░░░░░░░ ▸ Typecheck 18.0s ░░░░░░░░░░░████████░░░░░ Test 7.0s ░░░░░░░░░░░░░░░░░░░███░░ Catalog drift 1.0s ░░░░░░░░░░░░░░░░░░░░░░█░ Spell (typos) 0.0s ░░░░░░░░░░░░░░░░░░░░░░█░ teardown 4.0s 7.14% ░░░░░░░░░░░░░░░░░░░░░░██ Top Steps STEP TIME Typecheck 18.0s Lint shell (shellcheck) 14.0s Test 7.0s Set up mise tools 2.0s Lint (Biome) 2.0s ``` The **TIMELINE** column places each phase and step on the job's wall-clock, so you can see what ran when and what overlapped — not just what was slow. `▸` marks the single largest contributor. **Top Steps** then ranks the steps outright, which is the list to optimize from: here `Typecheck` and `Lint shell` are 32 of the 46 seconds inside `steps`. Phases come from a frozen vocabulary shared by the runner hooks and the facts store: `queue_wait`, `provision`, `image_pull`, `cache_restore`, `checkout+patch`, `steps`, `cache_save`, `teardown`. Only the phases with time recorded against them are shown. Percentages are share of phase time, so they sum across the phase rows — the nested steps are a breakdown of `steps`, not extra rows in that sum. **Exit codes:** as elsewhere, plus `6` for partial — telemetry was incomplete, and the body is still emitted. Calls [`GET /api/time`](https://docs.starsling.dev/api/time/get-time). ## `sling why` ```text sling why [--json | --agent] ``` A **server-authored diagnosis** — why a CI job failed, so an agent can fix the failure instead of re-deriving it from logs. It is read straight from the facts store: the last failing step, a bounded `##[error]` window, and run context. **No LLM sits in the request path**, so the same job always classifies the same way. A run id diagnoses that run's most-diagnosable job. Every diagnosis carries a classification, evidence references, suggested actions with ready-to-run commands, and an agent-ready remediation `prompt`. Classifications: `step_failure` · `hang` · `oom` · `timeout` · `cancelled` · `terminated` · `infra` · `network_egress` · `unknown`. On a **cancelled** target, `why` identifies the canceller — a user, a concurrency group, or a merge-queue flush — so an external cancellation is not misdiagnosed as a code failure. ```console $ sling why job_88886665361 JobID: 88886665361 · JobName: test (3) Failure Reason: The step 8 "Test" step failed (exit code 101). Evidence Classification: step_failure Phase: step 8 "Test" Logs: ##[error]Process completed with exit code 101 Suggestions: 1. Verify the logs sling logs 88886665361 --grep '##\[error\]' ``` Every diagnosis carries evidence, a suggested next step, and a ready-to-run `prompt` for a downstream agent. Calls [`GET /api/why`](https://docs.starsling.dev/api/why). ## `sling labels list` ```text sling labels list [--json | --agent] ``` The catalog of available runner labels and their specs, so `runs-on` mapping and cost hints reference concrete options instead of hardcoded names. The catalog is **static and global** — identical for every caller — so the usual `--org`, `--repo`, and window flags are accepted but inert. `list` is the only subcommand. ```console $ sling labels list StarSling runner labels LABEL CPU MEM (GiB) ARCH $/MIN starsling-ubuntu-24.04 4 16 x64 $0.008 starsling-ubuntu-24.04-2 2 8 x64 $0.004 starsling-ubuntu-24.04-8 8 32 x64 $0.016 starsling-ubuntu-24.04-16 16 64 x64 $0.032 starsling-ubuntu-24.04-32 32 128 x64 $0.064 starsling-ubuntu-24.04-64 64 256 x64 $0.128 ``` | Field | Meaning | | ------------------- | -------------------------------------------------------------------------- | | `label` | The `runs-on` runner label. | | `cpu` | vCPU count. | | `memory_gb` | RAM in GiB — a strict 4 GiB per vCPU. | | `arch` | CPU architecture; `x64` today. | | `price_per_min_usd` | Per-minute cost, `cpu × $0.002` — the same rate `usage` and `bill` charge. | See [Instance types](https://docs.starsling.dev/runners/instance-types) for the same catalog in context. Calls [`GET /api/labels`](https://docs.starsling.dev/api/labels/get-labels). # Billing (/sling-cli/commands/billing) Two read-only commands. Plan and payment changes stay in the dashboard; `sling` never mutates billing state. ## `sling bill` ```text sling bill [--org ] [--month ] [--json | --agent] ``` A budget snapshot for the current open billing period: runner minutes, cost, credits, amount due, per-runner-label line items, and a run-rate month-end projection. | Flag | Meaning | | ------------------- | --------------------------------------------------------------------------------------- | | `--org` | Scope. Defaults to your default org. | | `--month ` | A **past** month, resolving to its finalized invoice. Omit for the current open period. | | `--json`, `--agent` | Machine mode — the `/api/bill` envelope on stdout, wrapped with a `local` block. | ```console $ sling bill --org partcleda StarSling bill — current period Billing period · 2026-07-02 → 2026-08-02 Invoice: pending (issued when the period closes) Runner minutes 45390.0 Amount $396.91 Projected month-end $848.43 RUNNER LABEL TIME COST starsling-ubuntu-24.04 41166.0 min $329.33 starsling-ubuntu-24.04-8 4224.0 min $67.58 Plan & payment changes live in the dashboard — this command is read-only. ``` Other window flags — `--window`, `--from`, `--to` — are **rejected** here. A bill covers a fixed billing period, so an arbitrary range is meaningless. Use [`sling usage`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-usage) for arbitrary ranges. ### A closed month Passing `--month` for a closed period resolves to its finalized invoice: a real invoice id, a status, and `Credit` / `Amount due` in place of the run-rate projection. ```console $ sling bill --month 2026-06 --org partcleda StarSling bill — invoice Billing period · 2026-06-02 → 2026-07-02 Invoice: in_1ToapkFSjWlUgKNBIKavWGIJ Runner minutes 120978.0 Amount $1055.56 Credit −$527.78 Amount due $0.00 Status paid ``` ### Response fields | Field | Meaning | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `invoice_id` | Omitted for the open period; a Stripe invoice id for a finalized month. | | `status` | `open` for the current period; the Stripe status (`paid`, `open`, …) for a finalized month. | | `amount_usd` | Gross billed — the Stripe subtotal for an invoice, in-house run-rate cost for the open period. | | `credits_usd` | All reductions off the gross: a partner discount and/or the free grant. | | `amount_due_usd` | Still outstanding. `$0` once an invoice is paid. | | `free_credit_total_usd` / `free_credit_remaining_usd` | Free orgs only — the one-time lifetime grant. Omitted otherwise. | | `line_items` | Per-runner-label minutes and cost. | | `projected_month_end_usd` | Run-rate projection for the open period; the finalized net billed for an invoice. | A field the org does not have — a paid org's grant fields — is omitted rather than erroring. Calls [`GET /api/bill`](https://docs.starsling.dev/api/bill/get-bill). ## `sling bill history` ```text sling bill history [--org ] [-n ] [--after ] [--json | --agent] ``` Prior **finalized** invoices, newest first. Each carries Stripe's authoritative id, status, and amount, joined with per-label usage. | Flag | Meaning | | ---------------------- | --------------------------------------------------------------------- | | `-n`, `--limit` | How many invoices — 1 to 100, default 6. | | `--after ` | Cursor. Pages to invoices older than the given id; follow `has_more`. | | `--json`, `--agent` | Machine mode — `{ invoices, has_more }` on stdout. | ```console $ sling bill history --org partcleda StarSling bill — prior invoices (newest first) INVOICE PERIOD STATUS RUNNER MIN AMOUNT CREDIT DUE in_1ToapkFSjWlUgKNBIKavWGIJ 2026-06-02 → 2026-07-02 paid 120978.0 $1055.56 −$527.78 $0.00 RUNNER LABEL MINUTES COST starsling-ubuntu-24.04 112049.0 $896.39 starsling-ubuntu-24.04-8 8775.0 $140.40 AMOUNT is Stripe's gross (before −CREDIT); DUE is what's still outstanding ($0 once paid). ``` Note that `--after` takes an **invoice id**, not the opaque cursor the listing endpoints use. Calls [`GET /api/bill/history`](https://docs.starsling.dev/api/bill/get-bill-history). ## Exit codes Both commands: `0` ok · `2` usage error · `4` not signed in · `5` control-plane failure. The human view is chrome and goes to stderr, so `sling bill --json | jq` is safe to pipe unconditionally. See [Pricing](https://docs.starsling.dev/pricing) for how the rates are set. # Configuration (/sling-cli/configuration) ## Environment variables `sling` is configured entirely through the environment — there are no config files beyond the credential it writes at `~/.config/sling/credentials`. | Variable | Meaning | | ------------ | ----------------------- | | `SLING_HOST` | Control-plane base URL. | ### `SLING_HOST` Env-only by design — there is no `--host` flag. It must be `https://` (or `http://` on a loopback development host) so the token never travels in cleartext. ## How agents authenticate **On your own machine, an agent needs nothing extra.** [`sling login`](https://docs.starsling.dev/sling-cli/commands/auth#sling-login) writes the session to `~/.config/sling/credentials` for your user, so any agent running as you picks it up. Sign in once and the agent is authenticated. See [API credentials](https://docs.starsling.dev/api/credentials) for the credentials the control plane accepts and the scopes each endpoint needs. ## Context resolution `sling` resolves the org and repo it is acting on so that in-repo invocations need zero addressing: * **Org** is auto-resolved when unambiguous, overridable with `--org `, and persisted as a default by [`sling org switch`](https://docs.starsling.dev/sling-cli/commands/auth#sling-org-switch). Multi-org accounts do not pay a flag tax on every call. * **Repo** is detected from the git remote of the current directory, overridable with `--repo `. This is a CLI convenience. The HTTP API never infers either — [`org` is a required parameter](https://docs.starsling.dev/api/credentials#org-scoping) on every org-scoped endpoint. ## Global flags Available on the root command and every subcommand. | Flag | Meaning | | --------------------- | -------------------------------------------------------------------------------------- | | `--org ` | Org context. Auto-resolved when unambiguous; persisted default via `sling org switch`. | | `--repo ` | Repo context. Defaults to the git remote detected in the current directory. | | `--version`, `-v` | Print the version and exit. | | `--help`, `-h` | Show help. An unknown command still fails, so a typo does not read as help. | Machine mode is one switch with individually available parts: | Flag | Meaning | | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | `--agent` | Machine mode. Exactly equivalent to `--json --compact --no-input --no-color --yes`. This is the flag machine callers should pass. | | `--json` | JSON output on stdout. | | `--yes` | Assume "yes" for confirmations. | `sling --help` currently lists only `--json`, `--help`, and `--version` in its flags section — `--agent`, `--compact`, `--no-input`, `--no-color`, and `--yes` all work (confirmed by running each against `sling whoami`) but are not shown. This page is the only place `--agent` is documented today; the CLI owner should add these to `sling --help`. ## Output streams **stdout carries data only.** Human chrome — summary rows, spinners, prompts — goes to **stderr**. This happens unconditionally: the CLI does not check whether stderr is a TTY, so the same spinner and cursor-control sequences land on stderr even when it is redirected to a file or piped into another process. Don't rely on TTY detection to decide whether chrome is present — pipe stderr to `/dev/null` if you need to suppress it silently. That still leaves the more important case: on a prompt-bearing command like [`sling logout`](https://docs.starsling.dev/sling-cli/commands/auth#sling-logout), `--json` by itself only changes what stdout serializes as — it does **not** imply `--yes`/`--no-input`, so a script passing `--json` alone still blocks on the confirmation prompt. `--agent` is the flag that guarantees non-interactive behavior, since it bundles `--yes` and `--no-input`; reach for `--agent`, not bare `--json`, in anything unattended. A failed command usually writes nothing to stdout: a bad id makes `sling runs show --agent` exit `3` with zero bytes on stdout. `sling logs` is the exception — asked for a real job that stores no logs it exits `3` *and* emits its envelope (`{ "lines": [], "has_more": false, "local": { ... } }`). Branch on `$?`, not on whether stdout is empty. That is what makes `sling usage --json | jq` safe to pipe unconditionally: on failure `jq` receives empty input rather than half a table. **JSON casing is set per endpoint, not by whether the field crossed the network.** Nearly every control-plane response `sling` passes through — `top`, `labels`, `usage`, `bill`, `runs`, `jobs`, `resolve`, `why`, `time` — is `snake_case` and frozen, matching its API schema. [`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami) is the confirmed exception: it is a real control-plane call like any other (a bad `SLING_HOST` makes `sling whoami --agent` fail with exit `5`, the same as any other command that reaches the API), but its schema is `camelCase` end-to-end (`userId`, `githubLogin`, `expiresAt`). The `local` envelope described below is composed by the CLI rather than passed through, but every key observed inside it so far is a single lowercase word (`org`, `kind`, `slug`, `plan`), so its casing convention for a multi-word key is not yet established by any shipped response. Don't infer a command's casing from "was this local" — check that command's reference page, or default to expecting `snake_case` and treat `whoami` as the documented exception. The control-plane response, when there is one, is passed through **verbatim at the top level** of the JSON, with a sibling `local` key recording what the CLI resolved (org, repo, etc.) — not nested inside a wrapper. `local` is not on every response: `sling top`, `sling usage`, `sling runs list` and `sling whoami` all carry it, while `sling labels list` returns `{ "labels": [ ... ] }` with no `local` key at all, so treat it as optional when parsing. For example (values redacted): ```json title="sling top --agent" { "by": "workflow", "metric": "runner-minutes", "window": { "from": "2026-07-25T22:47:14.510Z", "to": "2026-08-24T22:47:14.510Z" }, "rows": [ { "key": "ci", "repo": "example-repo", "runner_minutes": 69559, "cost_usd": 556.47, "p50_ms": 69000, "p95_ms": 180000, "p99_ms": 358540, "queue_wait_ms": 14000, "trend_pct": 170.7 } ], "local": { "org": "example-org" } } ``` ```json title="sling whoami --agent" { "identity": { "userId": "example-user-id", "name": "Example User", "email": "user@example.com", "githubLogin": "example-login" }, "credential": { "type": "session", "expiresAt": "2026-08-31T20:38:25.617Z" }, "local": { "org": { "kind": "set", "slug": "example-org", "plan": "paid" } } } ``` Note the casing split across these two commands: `runner_minutes`, `cost_usd`, and `p50_ms` (the `sling top` control-plane fields) are `snake_case`, while `userId`, `githubLogin`, and `expiresAt` (the `sling whoami` control-plane fields) are `camelCase` — `whoami` calls the control plane the same as `top` does, its response schema is just `camelCase`. In both examples, the `local` block is `camelCase`-shaped regardless of the surrounding response's casing. [`sling logs`](https://docs.starsling.dev/sling-cli/commands/inspect#sling-logs) differs by mode, and the difference runs the opposite way to the rest of this page. In **human** mode it streams raw log lines to stdout, so `| head` and `| less` behave as you would expect, exiting cleanly when the pipe closes early. Under `--agent` or `--json` it returns a structured envelope like every other command — `{ "lines": [ ... ], "has_more": ..., "local": { ... } }`, one object per line — so a machine caller can hand `logs` to the same JSON parser it uses everywhere else rather than special-casing it. ## Exit codes Every command exits from one table, so a script or agent can branch on `$?` without parsing output. | Code | Meaning | | ---- | --------------------------------------------------------------------------------------------------- | | `0` | Success. | | `1` | Unexpected CLI or internal error — reserved for a crash, never a mapped API outcome. | | `2` | Usage — bad flags, a prompt refused under `--no-input` or `--agent`, or org ambiguity. | | `3` | Not found — a resolved id has no such run, job, or attempt in this org, or it stores no logs. | | `4` | Auth — missing, expired, or under-scoped credential. The message includes `sling login`. | | `5` | Control-plane or API error — a `5xx`, or a transport failure. | | `6` | Partial — telemetry incomplete; the result is still emitted. Used by `sling time` and `sling why`. | | `7` | Rate limited — the control plane returned HTTP `429`. | | `10` | Remote outcome failed — `sling doctor` unhealthy, or `sling runs show --wait` on a non-success run. | Codes `3`, `4`, `5`, and `7` are mapped by a shared error handler, so any command can surface them. See [API errors](https://docs.starsling.dev/api/errors) for the HTTP responses behind each. The binary's own `sling exit-codes` help text (as of v0.1.2) documents only codes `0`–`5` — it omits `6`, `7`, and `10`, which are the codes this page documents for `sling time`/`sling why` (partial), rate limiting, and `sling doctor`/`sling runs show --wait` (remote outcome failed). This repo's git history for this page has no earlier version to check when `6`, `7`, and `10` were introduced, so it's unclear whether they're newer than the installed binary's help text or the help text is simply stale. This table is kept as-is (all nine codes) rather than trimmed to match the binary — a healthy local environment couldn't be made to exercise `10` to confirm it directly, but the table's own example below documents it as the unhealthy-`sling doctor` code, and neither this page nor `sling exit-codes` should be trusted blind until the CLI owner reconciles the two. Exit `10` is not an error. It means the command succeeded and the *answer* was a failure — which is exactly what lets `sling runs show --wait` gate a pipeline on a run's conclusion. Read a command's exit code with `$?` immediately after it, since the next command overwrites it: ```console $ sling doctor; echo $? 10 # 0 = healthy, 10 = unhealthy ``` # Overview (/api) The StarSling control plane exposes an HTTP API for everything the platform knows about your CI: which runs and jobs executed, why one failed, where the wall-clock went, and what it all cost. Prefer a terminal to an HTTP client? The `sling` CLI wraps these same endpoints, adding argument validation, readable tables, and a stable exit-code contract. ## Base URL All endpoints are served from a single origin: Every path in this reference is relative to it, and every path is prefixed with `/api`. ## Conventions Two things hold across every endpoint: * **Org scoping.** Results are scoped to the orgs you belong to. Org-scoped endpoints take `org` as a **required** parameter (and often an optional `repo`); a resource in an org you cannot see returns `404`, not `403`, so the API never confirms that an id exists elsewhere. * **Money and time.** Costs are US dollars in `*_usd` fields. Durations are seconds unless the field name says otherwise, and runner minutes are *billed* minutes, `max(1, ceil(duration / 60s))` per job, so a five-second job still costs a minute. ## Endpoint groups ## Before you start Two pages cover the behaviour shared by every endpoint. Reading them first saves working it out one `400` at a time: # Credentials (/api/credentials) Every endpoint except [`GET /api/info`](https://docs.starsling.dev/api/info/get-control-plane-info) requires a credential. The control plane accepts a bearer token, and rejects anything else with [`401 UNAUTHENTICATED`](https://docs.starsling.dev/api/errors). ## Bearer token What [`sling login`](https://docs.starsling.dev/sling-cli/commands/auth#sling-login) stores at `~/.config/sling/credentials`, sent in the `Authorization` header: Anyone holding your token can read everything it has access to. Keep it in a secret store or CI secret. Never commit it, and never put it in a URL, where it leaks into logs and browser history. ## Confirming your credential [`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami) reports the caller's identity, org, plan, scopes, and token expiry. It is the fastest way to tell an expired token from a scope problem: ## Scopes Three endpoint groups require a named scope. A credential that lacks it is rejected with [`403 INSUFFICIENT_SCOPE`](https://docs.starsling.dev/api/errors), not `401`: the token was valid, it just could not reach that resource. | Scope | Required by | | ------------ | -------------------------------------------------------------------------------------------- | | `orgs:read` | [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) | | `usage:read` | [`GET /api/usage`](https://docs.starsling.dev/api/usage/get-usage), [`GET /api/top`](https://docs.starsling.dev/api/top/get-top) | | `bill:read` | [`GET /api/bill`](https://docs.starsling.dev/api/bill/get-bill), [`GET /api/bill/history`](https://docs.starsling.dev/api/bill/get-bill-history) | Endpoints not listed here need a valid credential but no particular scope. Grant the narrowest set that does the job: a credential that only powers a cost dashboard needs `usage:read` and nothing more. ## Org scoping Authentication establishes *who* you are; org membership establishes *what* you can see. Results are always scoped to the orgs the caller belongs to. Every org-scoped endpoint takes `org` as a **required** query parameter. The API never infers it, even when you belong to exactly one org. (The CLI is what makes it feel optional: it fills in the default org you set with `sling org switch`.) Call [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) to list the slugs you can use. A run, job, or attempt belonging to an org you cannot reach returns [`404 NOT_FOUND`](https://docs.starsling.dev/api/errors) rather than `403`. This is deliberate: a `403` would confirm the id exists somewhere, which is itself a disclosure. Do not read a `404` as proof that an id is invalid. # Errors (/api/errors) Every failure returns a JSON body with the same shape, so a client can branch on one field rather than parsing prose. ## Error body ```json { "code": "INSUFFICIENT_SCOPE", "message": "This credential is missing the bill:read scope." } ``` | Field | Type | Notes | | --------- | ------ | --------------------------------------------------------------------------------------------------------------- | | `code` | string | A stable machine-readable identifier. Branch on this, never on `message`. | | `message` | string | Human-readable explanation. Wording may change at any time. | | `details` | array | Present on validation failures only. Each entry is `{ "path": …, "error": … }`, naming the offending parameter. | `code` is drawn from a closed set: | `code` | Meaning | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `VALIDATION` | A parameter was present but unacceptable — an unknown enum value, an out-of-range `limit`, two mutually exclusive window parameters. | | `MALFORMED_REQUEST` | The request could not be parsed — bad JSON in a `POST` body, or a query string the server cannot read. | | `UNAUTHENTICATED` | No credential, or one that is expired or invalid. | | `INSUFFICIENT_SCOPE` | The credential is valid but lacks a required [scope](https://docs.starsling.dev/api/credentials#scopes). | | `NOT_FOUND` | No such resource *within the orgs you can reach*. | ## Status codes | Status | Typical `code` | What to do | | ------ | -------------------- | ------------------------------------------------------------------------------------------------- | | `400` | `MALFORMED_REQUEST` | Fix the request itself. Retrying unchanged will not help. | | `401` | `UNAUTHENTICATED` | Supply a credential, or refresh an expired session. See [Authentication](https://docs.starsling.dev/api/credentials). | | `403` | `INSUFFICIENT_SCOPE` | Reissue the credential with the scope the endpoint names. | | `404` | `NOT_FOUND` | Check the id — but note it may exist in an org you cannot reach. | | `422` | `VALIDATION` | Read `details` for the offending parameter and correct it. | | `500` | — | A control-plane fault. Safe to retry with backoff; every endpoint in this reference is read-only. | A `404` is deliberately indistinguishable from "exists, but not yours". The API will not confirm that an id exists in an org you cannot see, so treat `404` as "not visible to this credential" rather than "does not exist". ## Retrying Every endpoint documented here is a read, so retries are safe and cannot double-charge or mutate state. Retry `500` and `429` with exponential backoff; do not retry `400`, `401`, `403`, `404`, or `422` without changing the request, since the outcome is deterministic. If you are rate limited the control plane returns `429`. Back off before retrying. ## Exit codes in the CLI The `sling` CLI maps these responses onto a fixed exit-code table, so a script or agent can branch on `$?` without parsing output: | Exit | Meaning | From | | ---- | ----------------------------------------------------------- | --------------------- | | `0` | Success | `2xx` | | `1` | Unexpected CLI crash | never an API outcome | | `2` | Usage error — bad flags, conflicting parameters | client-side, or `422` | | `3` | Not found | `404` | | `4` | Auth — missing, expired, or under-scoped credential | `401`, `403` | | `5` | Control-plane or transport failure | `5xx` | | `6` | Partial — telemetry incomplete, result still returned | `200` with gaps | | `7` | Rate limited | `429` | | `10` | Remote outcome failed — e.g. a run concluded unsuccessfully | `200` | Note that `10` is not an error: the request succeeded and the *result* was a failure. It is what lets `sling runs show --wait` gate a pipeline on a run's conclusion. # List workflow runs (/api/runs/list-runs) `GET /api/runs` Filtered workflow-run listing (sling runs list): branch / status / conclusion / trigger / workflow_path / runner-label / time-window filters over the CI facts store, keyset-paginated via an opaque cursor. Authenticated; scoped to the caller's orgs by membership. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Required — a run listing is always org-scoped. e.g. `acme` | | `repo` | query | string | no | Bare repository name, without the org prefix. Narrows to one repo. e.g. `api` | | `branch` | query | string | no | Only runs on this branch. e.g. `main` | | `status` | query | array | no | Lifecycle state. Repeatable: ?status=queued&status=completed. | | `conclusion` | query | array | no | Terminal outcome. Repeatable, and absent until a run completes. | | `trigger` | query | string | no | The GitHub event that started the run. e.g. `push` | | `workflow_path` | query | string | no | Workflow file path, as committed in the repository. e.g. `.github/workflows/ci.yml` | | `label` | query | string | no | Runner label a job in the run requested. e.g. `self-hosted` | | `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with from/to and month. e.g. `30d` | | `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` | | `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` | | `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` | | `limit` | query | string \| integer | no | Rows per page. Page with `cursor`. 1–100; e.g. `30` | | `cursor` | query | string | no | Opaque keyset cursor from a prior page's `next_cursor`. | ## Responses | Status | Description | | --- | --- | | `200` | One page of workflow runs, newest first. Page with `next_cursor`. | | `400` | The window flags conflict or don't parse, or the cursor is stale. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - `runs` — array, required - array of object - `run_id` — string, required - `run_url` — string, required - `workflow_path` — string, required - `branch` — string - `trigger` — string, required - `status` — string, required - `conclusion` — string - `created_at` — string, required - `duration_ms` — string | integer - `jobs_total` — string | integer, required - `jobs_failed` — string | integer, required - `has_more` — boolean, required - `next_cursor` — string ### Example ```json { "runs": [ { "run_id": "28961231706", "run_url": "https://github.com/acme/api/actions/runs/28961231706", "workflow_path": ".github/workflows/ci.yml", "branch": "main", "trigger": "push", "status": "completed", "conclusion": "failure", "created_at": "2026-07-22T09:14:03.000Z", "duration_ms": 412000, "jobs_total": 12, "jobs_failed": 1 } ], "has_more": false } ``` # Get a run's jobs (/api/runs/get-run) `GET /api/runs/{id}` The run → jobs → attempts hierarchy for a single run (sling runs show): the run header plus each logical job (stable by name) with its attempts oldest → newest. Authenticated; the run id is resolved against the CI facts store within the caller's org. A run in another org 404s (opaque). Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `id` | path | string | yes | GitHub run id. A non-numeric id resolves to 404, not 422. e.g. `28961231706` | | `org` | query | string | yes | GitHub organization login. Scopes the request to your membership. e.g. `acme` | ## Responses | Status | Description | | --- | --- | | `200` | One run, with each of its jobs and that job's attempts. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `404` | No such run in that org. A run in an org you can't see answers the same way — existence is never confirmed across a tenant boundary. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - `run` — object, required - `run_id` — string, required - `run_url` — string, required - `workflow_path` — string, required - `branch` — string - `trigger` — string, required - `status` — string, required - `conclusion` — string - `created_at` — string, required - `duration_ms` — string | integer - **Variant 1** — string - **Variant 2** — integer - `jobs` — array, required - array of object - `job_name` — string, required - `attempts` — array, required ### Example ```json { "run": { "run_id": "28961231706", "run_url": "https://github.com/acme/api/actions/runs/28961231706", "workflow_path": ".github/workflows/ci.yml", "branch": "main", "trigger": "push", "status": "completed", "conclusion": "failure", "created_at": "2026-07-22T09:14:03.000Z", "duration_ms": 412000 }, "jobs": [ { "job_name": "typecheck", "attempts": [ { "attempt_id": "att_88392691097.1", "attempt": 1, "status": "completed", "conclusion": "failure", "runner_id": "611593", "label": "starsling-ubuntu-24.04-8", "duration_ms": 89000 } ] } ] } ``` # List workflow jobs (/api/jobs/list-jobs) `GET /api/jobs` Workflow-job listing (sling jobs list): jobs in one run (run_id), a repo, and/or a time window — at least one of the three is required — filtered by conclusion, keyset-paginated via an opaque cursor. One row per job attempt. Authenticated; scoped to the caller's orgs by membership. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Required — a job listing is always org-scoped. e.g. `acme` | | `run_id` | query | string | no | Every job in one run. Alternative to scoping by repo and/or window. e.g. `28961231706` | | `repo` | query | string | no | Bare repository name, without the org prefix. e.g. `api` | | `conclusion` | query | array | no | Terminal outcome. Repeatable: ?conclusion=failure&conclusion=timed_out. | | `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with from/to and month. e.g. `30d` | | `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` | | `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` | | `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` | | `limit` | query | string \| integer | no | Rows per page. Page with `cursor`. 1–100; e.g. `30` | | `cursor` | query | string | no | Opaque keyset cursor from a prior page's `next_cursor`. | ## Responses | Status | Description | | --- | --- | | `200` | One page of workflow jobs, newest first. Page with `next_cursor`. | | `400` | No scope was given (`run_id`, `repo`, or a window), the window flags conflict, or the cursor is stale. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - `jobs` — array, required - array of object - `job_id` — string, required - `run_id` — string, required - `name` — string, required - `status` — string, required - `conclusion` — string - `label` — string - `runner_id` — string - `attempt` — string | integer, required - `duration_ms` — string | integer - `created_at` — string, required - `has_more` — boolean, required - `next_cursor` — string ### Example ```json { "jobs": [ { "job_id": "85933007091", "run_id": "28961231706", "name": "typecheck", "status": "completed", "conclusion": "failure", "label": "starsling-ubuntu-24.04-8", "runner_id": "611593", "attempt": 1, "duration_ms": 89000, "created_at": "2026-07-22T09:14:11.000Z" } ], "has_more": false } ``` # Get a job's steps (/api/jobs/get-job) `GET /api/jobs/{id}` One workflow job's attempts, each with its step list (sling jobs show). A job id is one attempt; this resolves it to the logical job (run_id, job_name) and returns every attempt with its steps. Authenticated; a job in another org 404s (opaque). Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `id` | path | string | yes | GitHub job id. A non-numeric id resolves to 404, not 422. e.g. `85933007091` | | `org` | query | string | yes | GitHub organization login. Scopes the request to your membership. e.g. `acme` | ## Responses | Status | Description | | --- | --- | | `200` | One job, with each attempt's step list. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `404` | No such job in that org. A job in an org you can't see answers the same way — existence is never confirmed across a tenant boundary. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - `run_id` — string, required - `run_url` — string, required - `job_name` — string, required - `attempts` — array, required - array of object - `attempt_id` — string, required. e.g. `att_85933007091.1` - `attempt` — string | integer, required - `status` — string, required - `conclusion` — string - `runner_id` — string - `label` — string - `duration_ms` — string | integer - `steps` — array, required ### Example ```json { "run_id": "28961231706", "run_url": "https://github.com/acme/api/actions/runs/28961231706", "job_name": "typecheck", "attempts": [ { "attempt_id": "att_85933007091.1", "attempt": 1, "status": "completed", "conclusion": "failure", "runner_id": "611593", "label": "starsling-ubuntu-24.04-8", "duration_ms": 89000, "steps": [ { "name": "Set up job", "number": 1, "status": "completed", "conclusion": "success", "duration_ms": 2000 }, { "name": "Run bun run typecheck", "number": 4, "status": "completed", "conclusion": "failure", "duration_ms": 61000 } ] } ] } ``` # Read job log lines (/api/logs/get-logs) `GET /api/logs/{id}` Ingested job-log lines for a completed attempt (sling logs), filtered SERVER-SIDE so an agent reads only what matters: grep (RE2), since (the log tail), job (narrow a run to one job). A run id reads all its jobs; a job/attempt id reads one attempt. Keyset-paginated via an opaque cursor. Authenticated; a run/job in another org 404s (opaque). Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Prefixed log target: run_, job_, or att_.. A malformed shape is a 400. e.g. `run_1234567890` | | `org` | query | string | yes | GitHub organization login. Scopes the request to your membership. e.g. `acme` | | `job` | query | string | no | Narrow a run to one job, by job name. e.g. `check` | | `grep` | query | string | no | RE2 regular expression. Only matching lines are returned. e.g. `(?i)error` | | `since` | query | string | no | Only the last N of the log, e.g. 5m or 2h. e.g. `5m` | | `timestamps` | query | boolean | no | Include a per-line timestamp. Off by default. | | `cursor` | query | string | no | Opaque keyset cursor from a prior page's `next_cursor`. | | `limit` | query | string \| integer | no | Lines per page. Page with `cursor`. 1–5000; e.g. `1000` | ## Responses | Status | Description | | --- | --- | | `200` | One page of log lines, in order. `next_cursor` is present exactly when there is another page. | | `400` | The target id isn't a `run_`/`job_`/`att_` shape, `since` isn't a duration, the cursor is stale, or `grep` isn't a compilable RE2 expression. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `404` | No such run or job in that org. A target in an org you can't see answers the same way — existence is never confirmed across a tenant boundary. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The control plane failed, or the log store is not configured for this deployment. | ### `200` body - `lines` — array, required - array of object - `job_id` — string, required - `job_name` — string, required - `line_number` — string | integer, required - `timestamp` — string - `log_data` — string, required - `has_more` — boolean, required - `next_cursor` — string ### Example ```json { "lines": [ { "job_id": "85933007091", "job_name": "typecheck", "line_number": 812, "timestamp": "2026-07-22T09:15:40.118Z", "log_data": "src/index.ts(42,7): error TS2322: Type 'string' is not assignable to type 'number'." }, { "job_id": "85933007091", "job_name": "typecheck", "line_number": 813, "timestamp": "2026-07-22T09:15:40.119Z", "log_data": "##[error]Process completed with exit code 2." } ], "has_more": true, "next_cursor": "eyJqb2JfaWQiOiI4NTkzMzAwNzA5MSIsImxpbmUiOjgxM30" } ``` # Diagnose a failed job or run (/api/why) `GET /api/why` Classifies why a CI job failed (sling why) — step_failure / timeout / cancelled / terminated / oom / infra / network_egress / hang / unknown — with evidence, suggested actions, and a downstream-agent prompt, read deterministically from the CI facts store. No LLM in the request path. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Scopes the request to your membership. e.g. `acme` | | `job_id` | query | string | no | Diagnose this job. Give either job_id or run_id, not both. e.g. `88886665361` | | `run_id` | query | string | no | Diagnose this run — the most diagnosable failed job in it is chosen. e.g. `2990884649` | ## Responses | Status | Description | | --- | --- | | `200` | A deterministic read of why the job failed: a classification, the evidence behind it, and what to try next. | | `400` | Neither `job_id` nor `run_id` was given, or both were — a diagnosis targets exactly one. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `404` | No such job or run in that org, or the run has no diagnosable failed job. A target in an org you can't see answers the same way. | | `422` | `job_id` or `run_id` is not a positive decimal id. `details` names the offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - `job_id` — string, required - `job_name` — string, required - `conclusion` — string, required - `classification` — string, required. `step_failure`, `hang`, `oom`, `timeout`, `cancelled`, `infra`, `terminated`, `network_egress`, `unknown` - `summary` — string, required - `evidence` — array, required - array of object - `kind` — string, required - `ref` — string, required - `logs` — string, required - `log_window` — string, required - `suggested_actions` — array, required - array of object - `title` — string, required - `command` — string, required - `prompt` — string, required - `meta` — object, required - `truncated` — boolean, required ### Example ```json { "job_id": "88886665361", "job_name": "typecheck", "conclusion": "failure", "classification": "step_failure", "summary": "Step `Run bun run typecheck` failed with exit code 2.", "evidence": [ { "kind": "step", "ref": "Run bun run typecheck" }, { "kind": "log_line", "ref": "812" } ], "logs": "##[error]Process completed with exit code 2.", "log_window": "src/index.ts(42,7): error TS2322: Type 'string' is not assignable to type 'number'.", "suggested_actions": [ { "title": "Read the failing step's log", "command": "sling logs att_88886665361.1" } ], "prompt": "The job `typecheck` failed at step `Run bun run typecheck` with a TS2322 type error in src/index.ts:42.", "meta": { "truncated": false } } ``` # Break down CI wall-clock time (/api/time/get-time) `GET /api/time` Phase decomposition: job/attempt level splits wall-clock into the frozen phase enum with a per-step breakdown; run level adds a timing-inferred critical path, parallelism efficiency, and the blocking job; repo level aggregates p50/p95 per phase per runner label. Scoped to the caller's orgs by membership. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Required — timings are always org-scoped. e.g. `acme` | | `level` | query | string | yes | Granularity of the breakdown — it selects which id you pass. `job`, `attempt`, `run`, `repo`; e.g. `attempt` | | `job_id` | query | string | no | Target job, for level=job or level=attempt. e.g. `88886665361` | | `run_id` | query | string | no | Target run, for level=run. e.g. `2990884649` | | `attempt` | query | string \| integer | no | Pin a run to one attempt. Defaults to the latest. 1–4294967295; e.g. `2` | | `repo` | query | string | no | Target repository, for level=repo, as owner/name. e.g. `acme/api` | | `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with month and from/to. e.g. `30d` | | `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` | | `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` | | `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` | ## Responses | Status | Description | | --- | --- | | `200` | Wall-clock split into runner-lifecycle phases. The shape follows `level`; `meta.truncated` names anything the facts store could not account for. | | `400` | The id the `level` requires is missing (`job_id` for job/attempt, `run_id` for run, `repo` for repo), or the window flags conflict. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`. | | `404` | No such job, run, or repo in that org. A target in an org you can't see answers the same way — existence is never confirmed across a tenant boundary. | | `422` | A query parameter failed schema validation — most often an unrecognised `level`. `details` names the offending field. | | `500` | The control plane failed, or the CI facts store is not configured for this deployment. | ### `200` body - **Variant 1** — object - `level` — string, required. e.g. `attempt` - **Variant 1** — string - **Variant 2** — string - `id` — string, required. e.g. `att_88886665361.1` - `run_id` — string, required. e.g. `2990884649` - `job_id` — string, required. e.g. `88886665361` - `attempt` — number, required. e.g. `1` - `job_name` — string, required. e.g. `typecheck` - `wall_clock_ms` — number, required. e.g. `89000` - `phases` — array, required - array of object - `steps` — array, required - array of object - `meta` — object, required - `source` — string, required. `logs`, `steps`; e.g. `steps` - `truncated` — array, required - **Variant 2** — object - `level` — string, required. `run` - `run_id` — string, required. e.g. `2990884649` - `wall_clock_ms` — number, required. e.g. `420000` - `wait_time_ms` — number, required. e.g. `18000` - `critical_path` — array, required - array of object - `parallelism_efficiency` — number, required. e.g. `0.62` - `blocking_job` — object | null, required. e.g. `null` - `job_id` — string, required. e.g. `88886665361` - `job_name` — string, required. e.g. `typecheck` - `ms` — number, required. e.g. `89000` - `meta` — object, required - `source` — string, required. `logs`, `steps`; e.g. `steps` - `truncated` — array, required - **Variant 3** — object - `level` — string, required. `repo` - `repo` — string, required. e.g. `acme/api` - `window` — object, required - `from` — string, required - `to` — string, required - `phases` — array, required - array of object - `meta` — object, required - `source` — string, required. `logs`, `steps`; e.g. `steps` - `truncated` — array, required ### Example ```json { "level": "attempt", "id": "att_88886665361.1", "run_id": "2990884649", "job_id": "88886665361", "attempt": 1, "job_name": "typecheck", "wall_clock_ms": 89000, "phases": [ { "key": "queue_wait", "ms": 16000, "pct": 18 }, { "key": "provision", "ms": 9000, "pct": 10.1, "detail": { "instance_type": "c7a.2xlarge" } }, { "key": "steps", "ms": 61000, "pct": 68.5 }, { "key": "teardown", "ms": 3000, "pct": 3.4 } ], "steps": [ { "key": "Set up job", "ms": 2000, "conclusion": "success" }, { "key": "Run e2e tests", "ms": 59000, "conclusion": "failure" } ], "meta": { "source": "steps", "truncated": [ { "key": "image_pull", "reason": "bundled into provision — no v1 facts source" } ] } } ``` # Rank the biggest CI consumers (/api/top/get-top) `GET /api/top` Aggregate CI leaderboard: rank StarSling-billed jobs by runner-minutes, cost, jobs, p95 duration, p99 duration, or queue wait, grouped by workflow/job/label/repo/branch, over a time window, with each row's trend vs. the prior equal-length window. Scoped to the caller's orgs. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Required — a leaderboard is always org-scoped. e.g. `acme` | | `repo` | query | string | no | Narrow to one repository, as owner/name. Both segments are required. e.g. `acme/api` | | `by` | query | string | no | Ranking axis — what each row is a total for. `workflow`, `job`, `label`, `repo`, `branch`; e.g. `workflow` | | `metric` | query | string | no | Metric the leaderboard ranks on. `runner-minutes`, `cost`, `jobs`, `p95-duration`, `p99-duration`, `queue-wait`; e.g. `runner-minutes` | | `order` | query | string | no | Sort direction — desc puts the biggest burner first. `asc`, `desc`; e.g. `desc` | | `n` | query | string \| integer | no | How many rows to rank. Defaults to 20. 1–100; e.g. `20` | | `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with month and from/to. e.g. `30d` | | `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` | | `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` | | `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` | ## Responses | Status | Description | | --- | --- | | `200` | The ranked leaderboard for the resolved window, with each row's trend against the window before it. | | `400` | The window flags conflict or don't parse. Omit them all for the default window, or give exactly one of `window`, `month`, or `from`+`to`. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`, or the API key lacks `usage:read`. | | `422` | A query parameter failed schema validation — an unrecognised `by`, `metric`, or `order`, or an `n` outside 1-100. `details` names the offending field. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `by` — string, required. Ranking axis — what each row is a total for. `workflow`, `job`, `label`, `repo`, `branch`; e.g. `workflow` - `metric` — string, required. Metric the leaderboard ranks on. `runner-minutes`, `cost`, `jobs`, `p95-duration`, `p99-duration`, `queue-wait`; e.g. `runner-minutes` - `window` — object, required - `from` — string, required. e.g. `2026-06-22T00:00:00.000Z` - `to` — string, required. e.g. `2026-07-22T00:00:00.000Z` - `rows` — array, required - array of object - `key` — string, required. e.g. `Prebuild` - `repo` — string, required. e.g. `hpc-sandbox-benchmarks` - `runner_minutes` — number, required. e.g. `66043.9` - `cost_usd` — number, required. e.g. `528.35` - `runs` — number, required. e.g. `37` - `jobs` — number, required. e.g. `1460` - `p50_ms` — number, required. e.g. `92000` - `p95_ms` — number, required. e.g. `405000` - `p99_ms` — number, required. e.g. `1200000` - `queue_wait_ms` — number, required. e.g. `16000` - `trend_pct` — number | null, required. e.g. `55.2` ### Example ```json { "by": "workflow", "metric": "runner-minutes", "window": { "from": "2026-06-22T00:00:00.000Z", "to": "2026-07-22T00:00:00.000Z" }, "rows": [ { "key": "Prebuild", "repo": "hpc-sandbox-benchmarks", "runner_minutes": 66043.9, "cost_usd": 528.35, "runs": 37, "jobs": 1460, "p50_ms": 92000, "p95_ms": 405000, "p99_ms": 1200000, "queue_wait_ms": 16000, "trend_pct": 55.2 }, { "key": "CI", "repo": "api", "runner_minutes": 1240.5, "cost_usd": 9.92, "runs": 318, "jobs": 3816, "p50_ms": 41000, "p95_ms": 118000, "p99_ms": 240000, "queue_wait_ms": 4000, "trend_pct": null } ] } ``` # Runner minutes and cost by group (/api/usage/get-usage) `GET /api/usage` Runner-minute and cost attribution over a time window, grouped by label/workflow/job/repo/day. Scoped to the caller's orgs. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login. Required — usage is always org-scoped. e.g. `acme` | | `repo` | query | string | no | Narrow to one repository, as owner/name. e.g. `acme/api` | | `group_by` | query | string | no | Attribution axis — what each row is a total for. Defaults to repo. `label`, `workflow`, `job`, `repo`, `day`; e.g. `repo` | | `order_by` | query | string | no | Column the rows are sorted by. Defaults to cost. `cost`, `minutes`, `jobs`, `key`; e.g. `cost` | | `order` | query | string | no | Sort direction. Defaults to desc — the biggest spender first. `asc`, `desc`; e.g. `desc` | | `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with month and from/to. e.g. `30d` | | `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` | | `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` | | `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` | ## Responses | Status | Description | | --- | --- | | `200` | Attributed runner minutes and cost for the resolved window, plus the plan the window came from. | | `400` | The window flags conflict or don't parse. Omit them all for the default window, or give exactly one of `window`, `month`, or `from`+`to`. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`, or the API key lacks `usage:read`. | | `422` | A query parameter failed schema validation — most often an unrecognised `group_by`, `order_by`, or `order` value. `details` names the offending field. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `group_by` — string, required. Attribution axis — what each row is a total for. Defaults to repo. `label`, `workflow`, `job`, `repo`, `day`; e.g. `repo` - `window` — object, required - `from` — string, required. e.g. `2026-06-01T00:00:00.000Z` - `to` — string, required. e.g. `2026-07-01T00:00:00.000Z` - `plan` — object, required - `status` — string, required. `paid`, `free`, `blocked` - `free_minutes_limit` — number. e.g. `2000` - `free_minutes_used` — number. e.g. `1840` - `blocked_reason` — string. e.g. `…free allowance is used up…` - `period_source` — string, required. `stripe`, `install_cycle`, `calendar_month`, `explicit` - `rows` — array, required - array of object - `key` — string, required. e.g. `acme/api` - `runner_minutes` — number, required. e.g. `1240.5` - `jobs` — number, required. e.g. `318` - `cost_usd` — number, required. e.g. `9.92` - `pct_of_total` — number, required. e.g. `42.7` ### Example ```json { "group_by": "repo", "window": { "from": "2026-07-12T00:00:00.000Z", "to": "2026-08-12T00:00:00.000Z" }, "plan": { "status": "free", "free_minutes_limit": 2000, "free_minutes_used": 1840, "period_source": "install_cycle" }, "rows": [ { "key": "acme/api", "runner_minutes": 1240.5, "jobs": 318, "cost_usd": 9.92, "pct_of_total": 71.3 }, { "key": "acme/web", "runner_minutes": 499.5, "jobs": 96, "cost_usd": 3.99, "pct_of_total": 28.7 } ] } ``` # Billing snapshot for a period (/api/bill/get-bill) `GET /api/bill` Read-only budget check. With no month, it returns the current open billing period: runner minutes, cost, credits, amount due, per-label line items, and a projected month-end total. Pass month=YYYY-MM to check a past period — a closed month returns its finalized Stripe invoice, or an in-house estimate when no invoice covers it. Scoped to the caller's orgs. Plan/payment changes stay in the dashboard. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login the bill belongs to. e.g. `acme` | | `month` | query | string | no | A finalized month, YYYY-MM. Omit for the current open period. e.g. `2026-06` | ## Responses | Status | Description | | --- | --- | | `200` | The billing period's spend, credits, and line items. Open unless a month was named. | | `400` | `month` isn't a YYYY-MM calendar month. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`, or the API key lacks `bill:read`. | | `404` | The org has no billing record — it has never started a subscription. Distinct from a month with no usage, which is a 200 with zeroes. | | `422` | A path or query parameter failed schema validation. `details` names each offending field. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `invoice_id` — string. e.g. `in_1ToVfTFSjWlUgKNB` - `period` — object, required - `from` — string, required. e.g. `2026-07-01T00:00:00.000Z` - `to` — string, required. e.g. `2026-08-01T00:00:00.000Z` - `status` — string, required. e.g. `open` - `period_source` — string, required. `stripe`, `install_cycle`, `calendar_month`, `explicit` - `runner_minutes` — number, required. e.g. `5820` - `amount_usd` — number, required. e.g. `46.56` - `credits_usd` — number, required. e.g. `8` - `amount_due_usd` — number, required. e.g. `38.56` - `free_credit_total_usd` — number. e.g. `8` - `free_credit_remaining_usd` — number. e.g. `7.95` - `line_items` — array, required - array of object - `label` — string, required. e.g. `starsling-ubuntu-24.04-8` - `minutes` — number, required. e.g. `1240.5` - `usd` — number, required. e.g. `19.85` - `projected_month_end_usd` — number, required. e.g. `92.4` ### Example ```json { "period": { "from": "2026-07-12T00:00:00.000Z", "to": "2026-08-12T00:00:00.000Z" }, "status": "open", "period_source": "install_cycle", "runner_minutes": 2910, "amount_usd": 46.56, "credits_usd": 8, "amount_due_usd": 38.56, "free_credit_total_usd": 8, "free_credit_remaining_usd": 0, "line_items": [ { "label": "starsling-ubuntu-24.04-8", "minutes": 2910, "usd": 46.56 } ], "projected_month_end_usd": 92.4 } ``` # List prior invoices (/api/bill/get-bill-history) `GET /api/bill/history` Read-only list of the caller's prior finalized invoices (newest first), same per-item shape as /api/bill, paginated by count. Scoped to the caller's orgs. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Parameters | Name | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `org` | query | string | yes | GitHub organization login whose invoices to list. e.g. `acme` | | `limit` | query | string \| integer | no | Invoices per page, newest first. 1–100; e.g. `6` | | `after` | query | string | no | Invoice id from a prior page — returns the ones after it. e.g. `in_1ToVfTFSjWlUgKNB` | ## Responses | Status | Description | | --- | --- | | `200` | Prior finalized invoices, newest first. Page with `after`. | | `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. | | `403` | You are not a member of the org named in `?org=`, or the API key lacks `bill:read`. | | `404` | The org has no billing record — it has never started a subscription. An org that simply has no finalized invoices yet is a 200 with an empty list. | | `422` | A query parameter failed schema validation — most often a `limit` outside 1-100. `details` names the offending field. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `invoices` — array, required - array of object - `invoice_id` — string. e.g. `in_1ToVfTFSjWlUgKNB` - `period` — object, required - `status` — string, required. e.g. `open` - `period_source` — string, required. `stripe`, `install_cycle`, `calendar_month`, `explicit` - `runner_minutes` — number, required. e.g. `5820` - `amount_usd` — number, required. e.g. `46.56` - `credits_usd` — number, required. e.g. `8` - `amount_due_usd` — number, required. e.g. `38.56` - `free_credit_total_usd` — number. e.g. `8` - `free_credit_remaining_usd` — number. e.g. `7.95` - `line_items` — array, required - `projected_month_end_usd` — number, required. e.g. `92.4` - `has_more` — boolean, required ### Example ```json { "invoices": [ { "invoice_id": "in_1ToVfTFSjWlUgKNB", "period": { "from": "2026-06-12T00:00:00.000Z", "to": "2026-07-12T00:00:00.000Z" }, "status": "paid", "period_source": "stripe", "runner_minutes": 5820, "amount_usd": 93.12, "credits_usd": 0, "amount_due_usd": 93.12, "line_items": [ { "label": "starsling-ubuntu-24.04-8", "minutes": 5820, "usd": 93.12 } ], "projected_month_end_usd": 93.12 } ], "has_more": false } ``` # Resolve an id or URL to a target (/api/resolve/resolve-target) `POST /api/resolve` Polymorphic ID resolution: given a parsed descriptor (run/job/attempt id, a runner id, a bare id, or a GitHub Actions URL), resolve it against the CI facts store to the most specific target, or return a candidate list on ambiguity. A runner id resolves to the job/attempt target(s) that ran on that runner — or, with target=run, to the distinct run(s) it touched; several candidates when a runner is reused. Authenticated; scoped to the caller's orgs by membership. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Request body Required, sent as `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`. - `input` — object, required - **Variant 1** — object - `kind` — string, required. `run` - `github_run_id` — string, required. e.g. `1234567890` - **Variant 2** — object - `kind` — string, required. `job` - `github_job_id` — string, required. e.g. `9876543210` - **Variant 3** — object - `kind` — string, required. `attempt` - `github_job_id` — string, required - `attempt` — string | integer, required. 1–4294967295 - **Variant 4** — object - `kind` — string, required. `run_attempt` - `github_run_id` — string, required - `attempt` — string | integer, required. 1–4294967295 - **Variant 5** — object - `kind` — string, required. `bare` - `github_id` — string, required - **Variant 6** — object - `kind` — string, required. `runner` - `github_runner_id` — string, required. e.g. `611593` - `target` — string. `run`, `job`, `attempt` ## Responses | Status | Description | | --- | --- | | `200` | Either the one target the id resolves to, or the candidates it could mean. Ambiguity is a 200, not an error. | | `401` | No credential was sent, or the one sent is expired or revoked. Sign in with `sling login`. | | `404` | No such id in any org you belong to. Deliberately the same answer as an id that exists elsewhere — this never confirms another org's runs. | | `422` | The request body failed schema validation — `input` is missing or not a recognised descriptor. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - **Variant 1** — object - `resolved` — object, required - **Variant 1** — object - **Variant 2** — object - **Variant 3** — object - **Variant 2** — object - `candidates` — array, required - array of object ### Example ```json { "resolved": { "id": "att_9876543210.2", "kind": "attempt", "run_id": "1234567890", "org": "acme", "repo": "acme/api", "job_id": "9876543210", "attempt": 2, "job_name": "typecheck", "runner_id": "611593", "runner_name": "starsling-n-4f2a1c" } } ``` # List runner labels (/api/labels/get-labels) `GET /api/labels` The catalog of available runner labels with their specs (cpu, memory, arch, price/min) so runs-on mapping and cost-savings hints reference concrete options. Static and identical for every caller — authenticated, but requires no resource scope. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Responses | Status | Description | | --- | --- | | `200` | Every runner label `runs-on` accepts, with its specs and price. | | `401` | No credential was sent, or the one sent is expired or revoked. Sign in with `sling login`. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `labels` — array, required - array of object - `label` — string, required. e.g. `starsling-ubuntu-24.04-8` - `cpu` — number, required. e.g. `8` - `memory_gb` — number, required. e.g. `32` - `arch` — string, required. e.g. `x64` - `price_per_min_usd` — number, required. e.g. `0.016` ### Example ```json { "labels": [ { "label": "starsling-ubuntu-24.04-8", "cpu": 8, "memory_gb": 32, "arch": "x64", "price_per_min_usd": 0.016 } ] } ``` # Your user id (/api/identity/get-current-user) `GET /api/me` The authenticated user's id, and nothing else — the cheapest way to confirm a token works. Use /api/whoami for the full identity and credential details. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Responses | Status | Description | | --- | --- | | `200` | The authenticated user's id. Use /api/whoami for the full identity. | | `401` | No credential was sent, or the one sent is expired or revoked. Sign in with `sling login`. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `userId` — string, required. e.g. `u_abc123` ### Example ```json { "userId": "u_abc123" } ``` # Your identity and credential (/api/identity/get-whoami) `GET /api/whoami` Who you are and which credential you presented (sling whoami). credential.type discriminates a session — a browser cookie or a device-flow bearer token — from an api-key caller, which also carries its key id and scopes. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Responses | Status | Description | | --- | --- | | `200` | Who you are and which credential you presented. `credential.type` discriminates a session from an api-key caller. | | `401` | No credential was sent, or the one sent is expired or revoked. Sign in with `sling login`. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `identity` — object, required - `userId` — string, required. e.g. `u_abc123` - `name` — string, required. e.g. `Ada Lovelace` - `email` — string, required. e.g. `ada@example.com` - `githubLogin` — string | null, required. e.g. `ada` - `credential` — object, required - **Variant 1** — object - `type` — string, required. `session` - `expiresAt` — string | null, required - **Variant 2** — object - `type` — string, required. `apiKey` - `expiresAt` — string | null, required - `keyId` — string, required - `scopes` — object ### Example ```json { "identity": { "userId": "u_abc123", "name": "Ada Lovelace", "email": "ada@example.com", "githubLogin": "ada" }, "credential": { "type": "session", "expiresAt": "2026-08-19T09:41:00.000Z" } } ``` # List your organizations (/api/identity/list-orgs) `GET /api/orgs` Every org you belong to that the control plane has claimed, with the billing gate's verdict for each. Base URL: `https://runners.starsling.dev` ## Authentication Requires a bearer token (`Authorization: Bearer`). ## Responses | Status | Description | | --- | --- | | `200` | Every org you belong to that the control plane has claimed, each with the billing gate's verdict. | | `401` | No credential, or a GitHub grant that predates `read:org` — re-run `sling login`. | | `403` | An API key without the `orgs:read` scope. Session callers are unscoped and never see this. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `orgs` — array, required - array of object - `slug` — string, required. e.g. `acme` - `name` — string, required. e.g. `Acme, Inc.` - `role` — string. e.g. `admin` - `plan` — string, required. `paid`, `free`, `blocked` - `blocked_because` — string. `lapsed`, `grant_exhausted` ### Example ```json { "orgs": [ { "slug": "acme", "name": "Acme, Inc.", "plan": "free" } ] } ``` # Control-plane version (/api/info/get-control-plane-info) `GET /api/info` The running control plane and the commit it was built from. Needs no credential, so it answers whether the service is reachable before you start debugging one. Base URL: `https://runners.starsling.dev` ## Authentication No credential required. ## Responses | Status | Description | | --- | --- | | `200` | The running control plane and the commit it was built from. | | `500` | The request was valid; the control plane or a dependency it calls failed. | ### `200` body - `service` — string, required. `blazar-control-plane` - `version` — string, required. e.g. `dev` ### Example ```json { "service": "blazar-control-plane", "version": "a1b2c3d" } ``` # Start the device flow (/api/authentication/request-device-code) `POST /api/auth/device/code` Request a device and user code Follow [rfc8628#section-3.2](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2) Base URL: `https://runners.starsling.dev` ## Authentication No credential required. ## Request body Required, sent as `application/json`. - `client_id` — string, required. The client ID of the application - `user_id` — string. The user ID to which the device code should be pre-bound. - `scope` — string. Space-separated list of scopes ## Responses | Status | Description | | --- | --- | | `200` | Success | | `400` | Error response | | `401` | Unauthorized. Due to missing or invalid authentication. | | `403` | Forbidden. You do not have permission to access this resource or to perform this action. | | `404` | Not Found. The requested resource was not found. | | `429` | Too Many Requests. You have exceeded the rate limit. Try again later. | | `500` | Internal Server Error. This is a problem with the server that you cannot fix. | ### `200` body - `device_code` — string. The device verification code - `user_code` — string. The user code to display - `verification_uri` — string. The URL for user verification. Defaults to /device if not configured. - `verification_uri_complete` — string. The complete URL with user code as query parameter. - `expires_in` — number. Lifetime in seconds of the device code - `interval` — number. Minimum polling interval in seconds ### Example ```json { "device_code": "5f1c9d0e8b3a4c27", "user_code": "8226-T2GH", "verification_uri": "https://runners.starsling.dev/cli-login", "verification_uri_complete": "https://runners.starsling.dev/cli-login?user_code=8226-T2GH", "expires_in": 1800, "interval": 5 } ``` # Exchange a device code for a token (/api/authentication/exchange-device-code) `POST /api/auth/device/token` Exchange device code for access token Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#section-3.4) Base URL: `https://runners.starsling.dev` ## Authentication No credential required. ## Request body Required, sent as `application/json`. - `grant_type` — string, required. The grant type for device flow `urn:ietf:params:oauth:grant-type:device_code` - `device_code` — string, required. The device verification code - `client_id` — string, required. The client ID of the application ## Responses | Status | Description | | --- | --- | | `200` | Success | | `400` | Error response | | `401` | Unauthorized. Due to missing or invalid authentication. | | `403` | Forbidden. You do not have permission to access this resource or to perform this action. | | `404` | Not Found. The requested resource was not found. | | `429` | Too Many Requests. You have exceeded the rate limit. Try again later. | | `500` | Internal Server Error. This is a problem with the server that you cannot fix. | ### `200` body - `access_token` — string, required. The bearer token. Send it as `Authorization: Bearer `. - `token_type` — string, required. `Bearer` - `expires_in` — number, required. Seconds until the token expires. - `scope` — string. Space-separated granted scopes. Empty when none were requested. ### Example ```json { "access_token": "sess_9f2c1b7a4d3e", "token_type": "Bearer", "expires_in": 604800, "scope": "" } ``` # Tailscale Integration (/integrations/tailscale) [Tailscale](https://tailscale.com/) is a zero-config VPN that connects your devices, services, and cloud networks using encrypted [WireGuard](https://www.wireguard.com/) tunnels. By connecting StarSling Runners to your Tailscale network, your CI jobs get secure access to private services — databases, NFS volumes, internal APIs — without exposing them to the public internet or maintaining static IP allow lists. ## Prerequisites Before you start, make sure you have: * A [Tailscale account](https://login.tailscale.com) with admin access to the [ACL editor](https://login.tailscale.com/admin/acls/file) * A GitHub repository with Actions enabled * Tailscale 1.90.1+ on your tailnet for OIDC (any version for OAuth fallback) By the end of this guide, you'll have configured: * A Tailscale tag for your runners * An OAuth client with OIDC federation * A GitHub Actions workflow step that connects to your tailnet ## Connecting StarSling Runners to your tailnet ### Create a tag in your Tailnet ACLs [Tailscale tags](https://tailscale.com/kb/1068/tags) group non-user devices and let you manage access control policies based on device role. Create a tag for your StarSling Runners in the [admin console ACL editor](https://login.tailscale.com/admin/acls/file) by adding it under [`tagOwners`](https://tailscale.com/kb/1337/acl-syntax#tag-owners): ```json { "tagOwners": { "tag:starslingdev": ["autogroup:admin"] } } ``` This tag will be assigned to every StarSling Runner that joins your tailnet. You'll use it in [ACL rules](#granting-access-to-private-services) to control what runners can access. ### Create a trust credential Go to the [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials) page in the Tailscale admin console and click **Credential**. 1. Select **OpenID Connect** 2. Set **Issuer** to **GitHub** — Tailscale auto-fills the issuer URL 3. Set **Subject** to match your repository: `repo:/:*` 4. Click **Continue** to reach the **Scopes** page 5. Expand the **Keys** section, enable **Auth Keys** (Read & Write), and add the tag `tag:starslingdev` 6. Click **Generate credential** Save the **Client ID** and **Audience** values shown after generation — you'll need them in the next step. For the OAuth fallback path, create an **OAuth** credential instead of OpenID Connect. See the **OAuth Client Secret** tab in the next step. ### Add the Tailscale step to your workflow OIDC uses short-lived tokens issued by GitHub — no long-lived secrets are stored. Requires Tailscale 1.90.1+. Add the following secrets to your GitHub repository (**Settings** > **Secrets and variables** > **Actions** > **Secrets**): | Secret | Value | | -------------------- | ------------------------------------- | | `TS_OAUTH_CLIENT_ID` | Client ID from the previous step | | `TS_AUDIENCE` | Audience value from the previous step | Then add the Tailscale connection step to your workflow: ```yaml title=".github/workflows/ci.yml" jobs: build: runs-on: starsling-ubuntu-24.04 # [!code highlight] permissions: id-token: write # Required for OIDC contents: read steps: - uses: actions/checkout@v6 - name: Connect to Tailscale uses: tailscale/github-action@v4 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} audience: ${{ secrets.TS_AUDIENCE }} tags: tag:starslingdev # Your runner is now on the tailnet. # Access any private service allowed by your ACLs. - name: Run tests run: npm test ``` If your tailnet doesn't support OIDC, create an **OAuth** credential instead. Go to [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials), click **Credential**, select the **OAuth** tab, and configure with `auth_keys` (Read & Write) scope and `tag:starslingdev`. Save the **Client ID** and **Client Secret**. Add the following secrets to your GitHub repository (**Settings** > **Secrets and variables** > **Actions** > **Secrets**): | Secret | Value | | -------------------- | ------------------- | | `TS_OAUTH_CLIENT_ID` | OAuth Client ID | | `TS_OAUTH_SECRET` | OAuth Client Secret | Then add the Tailscale connection step to your workflow: ```yaml title=".github/workflows/ci.yml" jobs: build: runs-on: starsling-ubuntu-24.04 # [!code highlight] permissions: contents: read steps: - uses: actions/checkout@v6 - name: Connect to Tailscale uses: tailscale/github-action@v4 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} tags: tag:starslingdev # Your runner is now on the tailnet. # Access any private service allowed by your ACLs. - name: Run tests run: npm test ``` The OAuth client secret is a long-lived credential stored in GitHub. Rotate it periodically — you can regenerate it in the Tailscale admin console and update the GitHub secret without downtime. Your StarSling Runner is now connected to your tailnet as an [ephemeral node](https://tailscale.com/kb/1111/ephemeral-nodes). When the job completes, the node is automatically removed. **Verify it works:** Add a step to your workflow to confirm connectivity: ```yaml - name: Verify Tailscale connection run: | tailscale status tailscale ping ``` ## Granting access to private services With runners connected, use [Tailscale ACLs](https://tailscale.com/kb/1337/acl-syntax) to control what they can reach. Runners are tagged with `tag:starslingdev`, which you reference in ACL rules. ### Access a specific service Grant runners access to a private database by hostname and port: ```json { "acls": [ { "action": "accept", "src": ["tag:starslingdev"], "dst": ["database-hostname:5432"] } ] } ``` ### Access a subnet Using [Tailscale subnet routers](https://tailscale.com/kb/1019/subnets), grant runners access to a VPC or on-premises network: ```json { "acls": [ { "action": "accept", "src": ["tag:starslingdev"], "dst": ["192.0.2.0/24:5432"] } ] } ``` Always restrict to specific ports (e.g. `:5432`). Avoid `:*` (all ports) in production — overly broad rules are the most common ACL mistake. ## Security and compliance StarSling's Tailscale integration is designed for zero-trust CI environments: * **No stored secrets (OIDC)** — each job receives a short-lived GitHub OIDC token that Tailscale validates directly. No long-lived credentials are stored in GitHub. * **Ephemeral nodes** — runners are automatically removed from your tailnet when the job completes, leaving no persistent footprint. * **Audit visibility** — connected runners appear as tagged nodes in the [Tailscale admin console](https://login.tailscale.com/admin/machines) and are recorded in your [tailnet audit logs](https://tailscale.com/kb/1011/log-streaming). * **Scoped access** — tag-based ACLs enforce least-privilege per job. Runners can only reach services explicitly permitted by your ACL rules. ## Troubleshooting ### Runner can't reach private services 1. **Check the ACL.** Confirm your ACL rules allow `tag:starslingdev` to reach the target service and port. Verify the ACL has been applied in the [admin console](https://login.tailscale.com/admin/acls/file). 2. **Verify the host.** If using an IP, confirm it hasn't changed. If using a MagicDNS hostname, check for stale nodes — run `tailscale status` from the runner to see what's reachable. 3. **Look for stale nodes.** If `tailscale status` shows your target hostname as "offline" alongside a `-2` variant, delete the stale entries from the [Tailscale Admin Console](https://login.tailscale.com/admin/machines). # Label Reference (/configuration/label-reference) StarSling uses structured labels to specify runner configuration. A label is a base runner name, optionally followed by `/` and a configuration selector: ``` starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1 └────── base label ──────┘ └─ selector ──┘ ``` The numeric suffix on a base label indicates the vCPU count (e.g. `-8` = 8 vCPU). The label without a suffix is the 4 vCPU default. Selectors are used by GPU runners to choose a card; CPU runners take no selector. ## StarSling Labels | Label | OS | vCPU | Memory | Price per minute | | --------------------------- | ------------ | ---- | ------ | ---------------- | | `starsling-ubuntu-24.04-2` | Ubuntu 24.04 | 2 | 8 GB | $0.004 | | `starsling-ubuntu-24.04` | Ubuntu 24.04 | 4 | 16 GB | $0.008 | | `starsling-ubuntu-24.04-8` | Ubuntu 24.04 | 8 | 32 GB | $0.016 | | `starsling-ubuntu-24.04-16` | Ubuntu 24.04 | 16 | 64 GB | $0.032 | | `starsling-ubuntu-24.04-32` | Ubuntu 24.04 | 32 | 128 GB | $0.064 | | `starsling-ubuntu-24.04-64` | Ubuntu 24.04 | 64 | 256 GB | $0.128 | ## GPU Labels GPU runners are in **private beta**. Contact [founders@starsling.dev](mailto:founders@starsling.dev) to request access. GPU runners use the `starsling-ubuntu-24.04-gpu` base label and run Ubuntu 24.04 like every other size. | Label | GPU | vCPU | Memory | Disk | Price per minute | | -------------------------------------------- | ------------ | ---- | ------ | ------ | ---------------- | | `starsling-ubuntu-24.04-gpu` | RTX PRO 6000 | 4 | 16 GB | 100 GB | $0.05922 | | `starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1` | RTX 5090 | 4 | 16 GB | 100 GB | $0.03022 | | `starsling-ubuntu-24.04-gpu/gpus=rtx-4090:1` | RTX 4090 | 4 | 16 GB | 100 GB | $0.02522 | ### The gpus selector `gpus=:` chooses the card and how many of them. The SKU is one of: | SKU | GPU | | -------------- | ------------------- | | `rtx-pro-6000` | NVIDIA RTX PRO 6000 | | `rtx-5090` | NVIDIA RTX 5090 | | `rtx-4090` | NVIDIA RTX 4090 | While GPU runners are in beta they are single-card, so `` is always `1`. The bare `starsling-ubuntu-24.04-gpu` label carries no selector and is equivalent to `starsling-ubuntu-24.04-gpu/gpus=rtx-pro-6000:1`. See [Compute Specifications](https://docs.starsling.dev/runners/compute-sizing#gpu-specifications) for pricing details. ## GitHub Label Mapping The default `starsling-ubuntu-24.04` (4 vCPU / 16 GB) replaces these GitHub-hosted runner labels: | GitHub-hosted Label | StarSling Label | | ------------------- | ------------------------ | | `ubuntu-latest` | `starsling-ubuntu-24.04` | | `ubuntu-24.04` | `starsling-ubuntu-24.04` | For larger workloads, opt into a bigger size by appending the vCPU suffix (e.g. `starsling-ubuntu-24.04-8`). For cost-sensitive or lightweight jobs, use the 2 vCPU `starsling-ubuntu-24.04-2` label. ## Examples ### Basic Usage ```yaml jobs: build: runs-on: starsling-ubuntu-24.04 ``` ### Drop-in Replacement ```yaml jobs: build: # Before: runs-on: ubuntu-latest runs-on: starsling-ubuntu-24.04 ``` ### Larger Size ```yaml jobs: build: runs-on: starsling-ubuntu-24.04-8 ``` ### GPU Runner ```yaml jobs: train: runs-on: starsling-ubuntu-24.04-gpu ``` ### Specific GPU Card ```yaml jobs: train: runs-on: starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1 ``` ## Invalid Labels These will fail to match a runner: | Invalid Label | Reason | | -------------------------------------------- | ------------------------------------------------------------------ | | `starsling-ubuntu-22.04` | Only Ubuntu 24.04 is supported | | `starsling/ubuntu-24.04` | Use hyphens in the base label — `/` only introduces a selector | | `starsling-ubuntu-latest` | Use `starsling-ubuntu-24.04` | | `starsling-ubuntu-24.04-gpu/gpus=rtx-3090:1` | Only the SKUs listed under [GPU Labels](#gpu-labels) are available | # Migration Guide (/configuration/migration-guide) Migrate your workflows from GitHub-hosted runners to StarSling in minutes. ## Step 1: Install the StarSling GitHub App Install the StarSling GitHub App in your organization: [Install StarSling GitHub App](https://github.com/apps/starslingdev) Grant access to the org repositories where you want to use StarSling Runners. StarSling Runners are not available for personal repositories, only GitHub organizations. If you install the GitHub App in a personal repo, StarSling Runners will not pick up the jobs. [Learn why →](https://docs.starsling.dev/troubleshooting/common-issues#personal-repositories-not-supported) ## Step 2: Update Your Workflow(s) ### Option A: Use an AI Prompt Paste this prompt into any AI coding agent (Claude Code, Cursor, Codex, etc.):
````markdown title="AI prompt" # Migrate GitHub Actions to StarSling Runners Migrate the user's workflows from GitHub-hosted runners to StarSling Runners. **Prerequisites:** `gh` CLI authenticated, [StarSling GitHub App](https://github.com/apps/starslingdev) installed on the repo's org. ## Configuration **Target:** `starsling-ubuntu-24.04` | **Branch:** `migrate-starsling-ubuntu-2404` **Source runners to replace:** `ubuntu-latest`, `ubuntu-24.04` Replace all UPPERCASE placeholders (`OWNER`, `REPO`, `BRANCH_NAME`, `HEAD_OID`, `BASE64_CONTENT`, `FILE_NAME`, `N`, `DEFAULT_BRANCH`) with actual values from previous steps. ## Procedure ### Step 1: Confirm GitHub App Installation Ask the user: "Have you installed the [StarSling GitHub App](https://github.com/apps/starslingdev) on your org? It's required for runners to pick up jobs after merge. If not, please install it first and let me know when you're ready." **Do not run any commands or proceed to Step 2 until the user explicitly confirms the app is installed.** ### Step 2: Verify CLI Auth Verify `gh auth status` succeeds. If not, direct the user to install from https://cli.github.com/ and run `gh auth login`. ### Step 3: Get Repository Ask the user for the repository (`owner/repo`). Then run `gh api repos/OWNER/REPO --jq '.owner.type'`. If the result is `User` (not `Organization`), stop and explain: "StarSling Runners only work with GitHub organization repositories. You can create a free organization at https://github.com/account/organizations/new." ### Step 4: Discover Workflows Fetch all workflow files in one API call: ```bash cat <<'QUERY' | gh api graphql --input - { "query": "query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id nameWithOwner defaultBranchRef { name target { oid } } object(expression: \"HEAD:.github/workflows\") { ... on Tree { entries { name object { ... on Blob { text } } } } } } }", "variables": { "owner": "OWNER", "repo": "REPO" } } QUERY ``` Note: `HEAD` in the GraphQL expression is a Git ref, not a placeholder — do not replace it. If the response is truncated or errors due to size, fall back to the REST API: fetch the default branch and HEAD SHA via `gh api repos/OWNER/REPO --jq '.default_branch'` and `gh api repos/OWNER/REPO/git/ref/heads/DEFAULT_BRANCH --jq '.object.sha'`, then fetch workflow file names via `gh api repos/OWNER/REPO/contents/.github/workflows` and each file individually via `gh api repos/OWNER/REPO/contents/.github/workflows/FILE_NAME`. The response `content` field is base64-encoded — decode it before scanning for `runs-on:` values. Save `defaultBranchRef.name` (or REST default branch), `.target.oid` / HEAD SHA, and workflow `entries[]`. Scan each `.yml`/`.yaml` file for: - direct `runs-on:` string values matching supported runners, and - matrix-driven patterns (e.g., `runs-on: ${{ matrix.os }}`) where the matrix values include supported runner labels. Show the user a summary including: - Workflows that will be migrated and which runners are being replaced - Any Ubuntu runners NOT in the supported list (e.g., `ubuntu-22.04`, `ubuntu-20.04`, larger runners like `ubuntu-latest-16-cores`) listed as "Not migrated — unsupported runner label" If no direct or matrix-backed supported runners match, stop. ### Step 5: Preview Changes **Never create a PR without the user confirming changes first.** Show what will change per workflow: - Replace supported runners in `runs-on:` string values with `starsling-ubuntu-24.04` (preserve quotes/comments) - Replace matching `runner:`/`os:` values in `matrix.include` sections - Skip commented lines **Complex `runs-on` patterns:** - **Array syntax** (e.g., `runs-on: [self-hosted, linux, ubuntu-latest]`): Only replace the matching label within the array; do not collapse to a single string - **Group/labels syntax** (e.g., `runs-on: { group: ..., labels: [...] }`): Skip and flag for manual review - **Expressions** (`${{ matrix.os }}`, ternary/conditional): If `runs-on` uses `${{ matrix.os }}` and the matrix values are hardcoded runner labels, replace the labels in the matrix definition and flag the workflow for manual verification. If the matrix values come from other expressions, skip entirely and flag for manual review **YAML fidelity:** Change ONLY the `runs-on` and matrix values. Preserve the original file exactly: same indentation, key ordering, comments, blank lines, and trailing newline. The PR diff should show only the runner label changes. ### Step 6: Create PR **Branch:** Before creating, check for existing branches: `gh api repos/OWNER/REPO/git/matching-refs/heads/migrate-starsling-ubuntu-2404 --jq '.[].ref'`. If any exist, find the highest numeric suffix and increment by 1 (if none have a suffix, use `-2`). Then create: `gh api repos/OWNER/REPO/git/refs -f ref=refs/heads/BRANCH_NAME -f sha=HEAD_OID`. **Atomic commit** — all files in one commit, contents base64-encoded: ```bash cat <<'MUTATION' | gh api graphql --input - { "query": "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }", "variables": { "input": { "branch": { "repositoryNameWithOwner": "OWNER/REPO", "branchName": "BRANCH_NAME" }, "message": { "headline": "Migrate N CI workflows to StarSling Runners", "body": "Replaced runners per file:\n- file1.yml: ubuntu-latest → starsling-ubuntu-24.04\n- file2.yml: ubuntu-24.04 → starsling-ubuntu-24.04" }, "expectedHeadOid": "HEAD_OID", "fileChanges": { "additions": [ { "path": ".github/workflows/FILE_NAME", "contents": "BASE64_CONTENT" } ] } } } } MUTATION ``` Verify the response contains a valid `commit.oid`. If the mutation returned an `expectedHeadOid` mismatch, re-fetch the HEAD SHA (Step 4) and retry the commit once. For any other errors, report and stop — do not create a PR against a failed commit. **Commit message:** The headline should read `Migrate N CI workflows to StarSling Runners` where N is the count of modified workflow files (`.yml` + `.yaml`). List each file and the runner label(s) replaced in the body, as shown in the example above. **PR** via `gh pr create --repo OWNER/REPO --head BRANCH_NAME --base DEFAULT_BRANCH --title "Migrate N CI workflows to StarSling Runners" --body "..."` with this body structure: ``` ## Summary Migrates CI workflows from GitHub-hosted runners to [StarSling Runners](https://docs.starsling.dev) for faster builds and AI-powered optimizations. ## Changes - `file1.yml`: `ubuntu-latest` → `starsling-ubuntu-24.04` - `file2.yml`: `ubuntu-24.04` → `starsling-ubuntu-24.04` ## Not Migrated - `file3.yml`: Uses `${{ matrix.os }}` — requires manual review (or "All workflows migrated successfully.") ## After Merging Workflows will automatically run on StarSling Runners. Ensure the [StarSling GitHub App](https://github.com/apps/starslingdev) is installed with access to this repo. ``` ### Step 7: Monitor (Optional) Ask the user if they'd like help monitoring after merge. If yes, explain they can return after merging and you'll check with: ```bash gh run list --repo OWNER/REPO --branch DEFAULT_BRANCH --limit 5 --json status,conclusion,name,createdAt ``` Look for runs created after the merge. If any show `queued` for more than 2 minutes, suggest checking the GitHub App installation and repo access at https://github.com/apps/starslingdev. ## Error Handling | Error | Solution | |-------|----------| | `gh` not found or not logged in | Install from https://cli.github.com/, run `gh auth login` | | 403 / insufficient permissions | User needs write access to the repository — check collaborator status or org role | | App not installed | Install from https://github.com/apps/starslingdev | | Personal repo (owner type `User`) | StarSling requires an org repo — create one at https://github.com/account/organizations/new | | Repository not found | Check repo name and permissions | | No workflows found | Ensure `.github/workflows/` exists | | Runner not available after merge | Verify app has repo access at https://github.com/apps/starslingdev | | Rate limit exceeded | Wait a few minutes and retry | | 422 on branch creation | Branch exists — append a number suffix | | `expectedHeadOid` mismatch | Re-fetch HEAD SHA and retry the commit | | `${{ matrix.os }}` / complex `runs-on` | Require manual review — determined at runtime | | GraphQL response truncated | Fall back to REST API for individual file fetches | ````
### Option B: Manual Update Change your `runs-on` label from GitHub-hosted to StarSling: ``` starsling-ubuntu-24.04 ``` ```yaml title="Before" jobs: build: runs-on: ubuntu-latest # [!code highlight] steps: - uses: actions/checkout@v4 # ... your build steps ``` ```yaml title="After" jobs: build: runs-on: starsling-ubuntu-24.04 # [!code highlight] steps: - uses: actions/checkout@v4 # ... your build steps ``` ### Label Mapping Use `starsling-ubuntu-24.04` to replace any of these GitHub-hosted runner labels: | GitHub-hosted Label | StarSling Label | | ------------------- | ------------------------ | | `ubuntu-latest` | `starsling-ubuntu-24.04` | | `ubuntu-24.04` | `starsling-ubuntu-24.04` | ## Compatibility Notes ### Fully Compatible * All `actions/*` official actions * Matrix strategies * Caching with `actions/cache` * Artifacts with `actions/upload-artifact` * Secrets and environment variables ### Minor Differences | Feature | GitHub-hosted | StarSling | | -------------- | ---------------------- | ---------------------- | | Default shell | bash | bash | | Home directory | `/home/runner` | `/home/runner` | | Work directory | `/home/runner/work` | `/home/runner/work` | | Tool cache | `/opt/hostedtoolcache` | `/opt/hostedtoolcache` | ### Private Beta * GPU runners — available on request. See [GPU Specifications](https://docs.starsling.dev/runners/compute-sizing#gpu-specifications) for labels and pricing. ### Not Supported * macOS runners (coming soon) * Windows runners (coming soon) ## Rollback If you need to rollback, simply change the label back: ```yaml # Rollback to GitHub-hosted runs-on: ubuntu-latest ``` No other changes required. ## Troubleshooting Migration ### Runner Not Starting 1. Verify the GitHub App is installed 2. Check the label format is correct 3. Ensure your account has available runner capacity ### Slower Than Expected 1. Verify caching is working 2. Check if dependencies are being re-downloaded 3. Use matrix builds to parallelize tests ### Action Compatibility Issues Most actions work unchanged. If an action fails: 1. Check the action's documentation for self-hosted runner support 2. Ensure required tools are available 3. [Contact support](https://docs.starsling.dev/troubleshooting/debug-access) if issues persist # GitHub App Permissions (/configuration/github-app-permissions) The StarSling GitHub App requests specific permissions to provide runner functionality and AI-powered optimizations. ## Required Permissions ### Repository Permissions | Permission | Access | Purpose | | ------------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------- | | **Actions** | Read & Write | Register runners and receive workflow job events | | **Checks** | Read & Write | Report runner assignment and job status | | **Contents** | Read & Write | Read workflow and source files for AI optimization scanning, and commit proposed changes to pull request branches | | **Pull requests** | Read & Write | Open AI optimization pull requests | | **Workflows** | Read & Write | Propose changes to `.github/workflows` files via pull requests | | **Code scanning alerts** | Read | Repository context for AI optimization analysis | | **Deployments** | Read | Repository context for AI optimization analysis | | **Discussions** | Read | Repository context for AI optimization analysis | | **Issues** | Read | Repository context for AI optimization analysis | | **Metadata** | Read | Basic repository information (required by all GitHub Apps) | | **Pages** | Read | Repository context for AI optimization analysis | | **Webhooks** | Read | Receive webhook deliveries and view hook metadata | ### Organization Permissions | Permission | Access | Purpose | | ----------------------- | ------------ | ----------------------------------------------------- | | **Self-hosted runners** | Read & Write | Register and manage runners at the organization level | | **Members** | Read | Organization membership for access management | | **Webhooks** | Read | Receive webhook deliveries and view hook metadata | ### Account Permissions Granted when you sign in to StarSling with GitHub, not when the app is installed on a repository. | Permission | Access | Purpose | | ------------------- | ------ | -------------------------------------------------------------------------------- | | **Email addresses** | Read | Create your StarSling account and send account email such as the welcome message | ## Permission Details ### Actions (Read & Write) **Why:** StarSling needs to register self-hosted runners with your repository and receive webhook events when workflows start. **What we do:** * Register ephemeral runners * Remove runners after job completion * Receive `workflow_job` webhooks **What we don't do:** * Modify your workflow files outside of a pull request you review (see [Workflows](#workflows-read--write)) * Access workflow run logs (except for optimization scanning) ### Checks (Read & Write) **Why:** To report runner assignment status and provide visibility into job execution. **What we do:** * Update check status when runner is assigned * Report runner health information ### Contents (Read & Write) **Why:** To read workflow definitions and repository source code for AI optimization scanning, and to commit proposed changes to pull request branches. **What we do:** * Read workflow definitions for optimization scanning * Read source code files in your repository to provide context for AI optimization suggestions * Process file contents in memory during analysis * Commit optimization changes to a new branch and open a pull request **What we don't do:** * Persist the checkout beyond the temporary 24-hour AI analysis window (see [Data Handling](https://docs.starsling.dev/security/data-handling)). Source code a workflow prints to the console — a diff, a file dump, a stack trace — is part of the retained job log and follows that log's retention instead * Push to your default branch or existing branches — changes are isolated to pull request branches for your review ### Pull Requests (Read & Write) **Why:** To open AI optimization pull requests against your repository. **What we do:** * Open pull requests with optimization changes and explanations **What we don't do:** * Approve or merge pull requests * Close or modify PR metadata ### Workflows (Read & Write) **Why:** To propose optimizations to your GitHub Actions workflow files. **What we do:** * Include updated `.github/workflows/*.yml` files in optimization pull requests **What we don't do:** * Change workflow files outside of a pull request you review and merge ### Email Addresses (Read) **Why:** GitHub sign-in does not return your email address unless the app asks for it, and StarSling identifies your account by email. **What we do:** * Create your StarSling user account when you first sign in * Send account email, such as the welcome message * Match your GitHub commit author email to your Slack user if you enable pull request notifications **What we don't do:** * Read the mailbox itself — GitHub exposes only the addresses on your account * Sell, share, or use your address for marketing ### Read-Only Permissions The remaining read scopes — deployments, discussions, issues, pages, code scanning alerts, repository and organization webhooks, and organization members — give StarSling's agents repository and organization context for optimization analysis and let the app receive the webhook events that trigger runner provisioning. None of these are used to modify your repositories or organization. ## Data Access Summary | Data Type | Accessed | Stored | Retained | | ------------------------- | -------- | ----------- | ----------------------------- | | Workflow events | Yes | No | No | | Workflow definitions | Yes | No | No | | Source code (AI analysis) | Yes | Temporarily | 24 hours | | Workflow job logs | Yes | Yes | No longer than 12 months | | Secrets | No | No | No | | Environment variables | No | No | No | | Account email | Yes | Yes | Until you delete your account | For a full breakdown of access, storage, and retention, see [Data Handling](https://docs.starsling.dev/security/data-handling). ## Security Practices ### Secrets Passthrough Your GitHub secrets are passed directly to the runner by GitHub. StarSling's control plane never receives, stores, or logs secret values — there is no code path that reads them. The one place a secret value can reach what we retain is the job's console output, and only if the workflow prints it. We fetch that log from GitHub's own Actions log archive, so values registered as Actions secrets arrive already masked by GitHub; anything else a job echoes is in the output, exactly as it is in GitHub's copy. See [Workflow Job Logs](https://docs.starsling.dev/security/data-handling#workflow-job-logs). ### Ephemeral Runners Each job runs on its own dedicated, single-use machine that's destroyed when the run finishes — there's nothing for a later job or a fork pull request to persist on or reach, and the boundary between two customers is a machine boundary. See [Isolation](https://docs.starsling.dev/security/data-handling#isolation) for details. ### Encrypted Transit All communication uses TLS 1.3. Webhooks are verified using GitHub's signature. ## Revoking Access To remove StarSling: 1. Go to your repository or organization settings 2. Navigate to **Integrations** → **GitHub Apps** 3. Find StarSling and click **Configure** 4. Click **Uninstall** After uninstalling: * All runners are immediately deregistered * No further webhooks are received * Retained job logs age out on the retention schedule in the [Data Access Summary](https://docs.starsling.dev/security/data-handling#data-access-summary). Uninstalling does not itself delete them — to have them removed, request deletion via [Data Deletion](https://docs.starsling.dev/security/data-handling#data-deletion) # Pricing (/pricing) StarSling Runners are: * **Up to 6x Faster** — faster builds mean fewer minutes billed * **Up to 33% Cheaper** — save on compute vs GitHub-hosted runners * **AI-Powered** — continuous AI optimizations of your CI workflows, included at no extra cost on paid plans. For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default. *** ## Pricing **2,000 free minutes for your first month** to get started. After that, billing is per minute and depends on runner size — from **$0.004/min** (2 vCPU) to **$0.128/min** (64 vCPU). The 4 vCPU default is **$0.008/min**. See the [Compute](#compute) table below for all sizes. AI-powered optimizations are included with every paid minute — no limits and no add-ons. For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default. | | Free Trial | Usage-Based | Enterprise | | ----------------------- | --------------------- | ------------------------------------------------- | ------------------------------------------------- | | **Price** | Free | $0.004–$0.128/min by runner size | Custom | | **Minutes** | 2,000 for first month | Pay as you go | Custom | | **AI Optimization PRs** | — | Included; not enabled by default for new accounts | Included; not enabled by default for new accounts | | **Support** | Community | Email | Dedicated | | **SLA** | — | 99.99% runner availability | 99.99% runner availability | No credit card required to start. *** ## Enterprise For large organizations needing advanced features and dedicated support. **Included:** * Custom minute allocation and volume discounts * SSO/SAML integration * Dedicated account manager * Custom SLAs with contractual guarantees * Priority incident response (\< 1 hour) * Quarterly business reviews **Ideal for:** * Organizations with 50+ developers * Monthly CI usage exceeding 50,000 minutes * Teams requiring compliance certifications (SOC 2 Type II in progress) * Custom security or data residency requirements [Book a Demo →](https://cal.com/team/starsling/starsling-founders-chat) · [Contact Sales →](mailto:founders@starsling.dev) *** ## Compute | Runner | vCPU | Memory | Price per Minute | | --------------------------- | ---- | ------ | ---------------- | | `starsling-ubuntu-24.04-2` | 2 | 8 GB | $0.004 | | `starsling-ubuntu-24.04` | 4 | 16 GB | **$0.008** | | `starsling-ubuntu-24.04-8` | 8 | 32 GB | $0.016 | | `starsling-ubuntu-24.04-16` | 16 | 64 GB | $0.032 | | `starsling-ubuntu-24.04-32` | 32 | 128 GB | $0.064 | | `starsling-ubuntu-24.04-64` | 64 | 256 GB | $0.128 | The 4 vCPU `starsling-ubuntu-24.04` label is the default and replaces `ubuntu-latest` and `ubuntu-24.04`. See [Label Reference](https://docs.starsling.dev/configuration/label-reference) for the full label reference. ### Comparison to GitHub-hosted Runners (GitHub Actions) | Runner Size | GitHub Price | StarSling Price | Savings | | ------------- | ------------ | --------------- | ------- | | 2 vCPU Linux | $0.006/min | $0.004/min | **33%** | | 4 vCPU Linux | $0.012/min | **$0.008/min** | **33%** | | 8 vCPU Linux | $0.022/min | $0.016/min | **27%** | | 16 vCPU Linux | $0.042/min | $0.032/min | **24%** | | 32 vCPU Linux | $0.082/min | $0.064/min | **22%** | | 64 vCPU Linux | $0.162/min | $0.128/min | **21%** | Comparison is against GitHub's Linux x64 larger runners. GitHub prices are sourced from the [GitHub Actions runner pricing reference](https://docs.github.com/en/billing/reference/actions-runner-pricing) (as of 2026-05-06). StarSling is **up to 33% cheaper** than GitHub-hosted runners — while also being up to 6x faster. ### How Minutes Are Calculated * Billed from job start to finish * Rounded up to the nearest minute * Queue time is **not** billed *** ## AI-Powered Optimizations AI-powered optimizations are **included with every paid minute** at no additional cost — no per-optimization charges, no monthly caps, and no add-ons to buy. For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default. This entitlement covers StarSling's hosted, continuous optimization PR service. The separate [ci-speedup](https://docs.starsling.dev/skills/ci-speedup), [ci-score](https://docs.starsling.dev/skills/ci-score), and [ci-secure](https://docs.starsling.dev/skills/ci-secure) skills are free, open source, and run on demand with your own coding agent. StarSling agents continuously: * **Scan your GitHub Actions workflows** — Looking for caching, parallelization, and configuration improvements * **Identify wasted minutes** — Spotting redundant or slow steps * **Open pull requests with optimizations** — Each PR includes the change and an explanation See the cost and speed wins these optimizations produced in real repos: [StarSling case studies →](https://starsling.dev/customers). *** ## Billing FAQ ### Do I pay for failed jobs? Yes, but failed jobs typically exit quickly, minimizing costs. ### Do unused free minutes roll over? No. The 2,000 free minutes are a one-time introductory offer for your first month. ### How does billing work after the free minutes? After your first month (or once you exceed 2,000 minutes), you're billed per minute based on runner size — from $0.004/min for the 2 vCPU runner up to $0.128/min for the 64 vCPU runner (see the [Compute](#compute) table above for all sizes). AI-powered optimizations remain included. *** ## Get Started 1. **Sign up** at [starsling.dev](https://starsling.dev) 2. **Add StarSling** to your GitHub workflow with a single line change 3. **Start building** faster with AI-powered optimizations # Security (/security) StarSling is built with security as a core principle. Your code and secrets are protected at every layer. ## Security Highlights * **Secrets never touch StarSling** - Passed directly from GitHub to runner * **One single-use machine per job** - Every job gets its own dedicated VM, destroyed when the run finishes, with a virtual-machine boundary between customers * **Encrypted everywhere** - TLS 1.3 for all communications * **Your data works only for you** - Job logs are retained to optimize your own CI, never pooled across customers and never used to train models # Security FAQ (/security/faq) The questions below come up in nearly every enterprise security review. Each answer links to the section that covers it in full. ## Can any StarSling employee access our source code, API keys, credentials, or environment variables? **Secrets, API keys, environment variables, and build artifacts: no — by architecture, not by policy.** GitHub injects secrets and environment variables directly into the runner at job time, and artifacts upload directly to GitHub's storage. None of it reaches StarSling's control plane or database, so there is no stored copy for an employee to read and no access control that could be misconfigured to expose one. Two qualifications, stated plainly rather than left for you to find: * **The runner itself is our machine.** Secrets are in memory and on disk on the VM while your job runs. What protects them there is that the machine is single-use and destroyed with the job, not that the values never arrived. The architectural claim above is about our control plane, not about the runner. * **Printed values are a different question from injected ones.** Anything your workflow echoes to the console is part of the job log we retain — see [the console-log note below](#is-source-code-or-are-files-secrets-and-credentials-retained-after-the-job-finishes). **Source code and job data: only through a named, audited support path.** Platform administrators can open a support session against your account to answer a support request. That session is a real session with your account's permissions — it is granted so an engineer can reproduce what you are reporting, not restricted to reading — so it reaches what you reach: job history, runner assignment, usage, and the retained console output of your jobs, including anything your workflow printed. Source code held for AI analysis is not exposed through this path. The controls on it are that the session expires (one hour by default), can be revoked sooner, and writes an audit record under **both** identities for every request made inside it. Administrator rights are a per-person grant on the user record — no environment variable, job title, or GitHub organization role confers them. Separately, on-call engineers hold direct access to production infrastructure for incident response. That path is governed by MFA and the on-call rotation rather than by the support-session audit trail; see [Access Control](https://docs.starsling.dev/security/compliance#access-control). See [Operator Access](https://docs.starsling.dev/security/data-handling#operator-access) for what that path can and cannot reach. ## If operator or support access is required, is it fully audit logged? Yes for the support path itself. Every request to the StarSling API writes an audit record identifying the person or API key that made it, the operation, the result, and the time. When an administrator is acting on behalf of a user, the record names **both** identities — the account being viewed and the administrator viewing it — so support activity is never indistinguishable from the customer's own. To be precise about the boundary: this trail covers access through the application. Direct infrastructure access — on-call engineers reaching production systems during an incident — is governed separately by MFA and the on-call rotation described under [Access Control](https://docs.starsling.dev/security/compliance#access-control), and is not captured as API audit records. The application only ever inserts into this trail — no code path updates or deletes a record — and records are retained independently of the accounts they reference, so history survives a user being removed and is not erased by a request to delete your job data. That insert-only behavior is an application guarantee rather than a database or write-once-storage constraint. See [Audit Logging](https://docs.starsling.dev/security/data-handling#audit-logging). ## Are our jobs isolated from other customers? Yes. Every job runs on its own dedicated, single-use virtual machine, and isolation between customers is enforced at the virtual-machine boundary rather than by containers sharing a host. A runner also cannot receive another customer's work even by accident. Each one registers with GitHub using a single-use, just-in-time token minted for **one organization's** app installation, so the set of jobs it can ever be assigned is scoped to your organization. Worth knowing before you ask: *within* a single organization, GitHub — not StarSling — decides which of your queued jobs a given runner picks up. That assignment never crosses an organization boundary, so it is not a cross-customer concern, but it does mean the isolation unit is the GitHub organization. If your company spans several organizations, or shares one with contractors, that is the line to reason about. See [Isolation](https://docs.starsling.dev/security/data-handling#isolation). ## Is the machine destroyed after each job? Yes. A machine is provisioned when your job is queued and destroyed when the job finishes. There is no reuse across jobs, no warm pool of shared runners, and nothing — filesystem, image cache, process, or credential — carries from one job to the next. The machine is never handed to another job in any case: its registration credential is single-use, so even an instance that outlived a failed teardown could not be assigned further work. ## Is source code, or are files, secrets, and credentials retained after the job finishes? * **Secrets, credentials, and environment variables** — never received, so never retained as values. What a workflow *prints* is a separate matter: see the note under the console log below. * **Your job's filesystem, including the checkout** — destroyed with the machine. Nothing is copied off it. * **Build artifacts** — uploaded to GitHub's storage, never to ours. * **Source code read for AI optimization analysis** — held only for the duration of the analysis and deleted within 24 hours. * **Your job's console log** — retained, and used to produce optimization analysis for your organization. See the retention table below. The console log is the one thing we keep. It is the log GitHub itself shows under the Actions tab, retrieved after the job completes, so GitHub's masking of registered Actions secrets has already been applied. Anything else a workflow prints — an unmasked variable, a credential fetched at runtime, a debug dump — is in that output, exactly as it is in GitHub's own log for the run. See the [Data Access Summary](https://docs.starsling.dev/security/data-handling#data-access-summary) for the full table. ## How long are logs and cached artifacts retained? | Data | Retention | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Workflow job logs | Retained no longer than 12 months | | Source code held for AI analysis | Deleted within 24 hours | | Repository metadata | Kept for the life of the account, deleted within 7 days of uninstalling | | GitHub's own copy of the logs, plus artifacts and Actions caches | Held by GitHub under your repository's own retention settings — StarSling does not store or control them | Your `actions/upload-artifact` uploads and `actions/cache` entries live in GitHub and are governed by your own repository and organization settings. StarSling operates no separate cache or artifact store. We do retain the console output of jobs that run on StarSling runners, because it is what our optimization agents read to find where your CI time goes. That data is used **only to produce analysis for your own organization** — never pooled with other customers' data, never used to train models, and never sold or shared. See [How Your Data Is Used](https://docs.starsling.dev/security/data-handling#how-your-data-is-used). See [Data Deletion](https://docs.starsling.dev/security/data-handling#data-deletion). ## What further documentation can you provide? Published: [Data Handling](https://docs.starsling.dev/security/data-handling), [Compliance](https://docs.starsling.dev/security/compliance), and [GitHub App Permissions](https://docs.starsling.dev/configuration/github-app-permissions). Available on request for enterprise security reviews: * CAIQ and SIG questionnaire responses * Custom questionnaire responses * A security review call with our engineering team Contact [founders@starsling.dev](mailto:founders@starsling.dev) to request them. To report a vulnerability, email [security@starsling.dev](mailto:security@starsling.dev) — see [Responsible Disclosure](https://docs.starsling.dev/security/compliance#responsible-disclosure). ## Can Enterprise customers configure custom retention and export audit logs? Both are available to Enterprise customers as part of an agreement with us, arranged and delivered by our team rather than configured self-serve in the product. Custom data retention policies and audit log export are listed under [Enterprise Security Features](https://docs.starsling.dev/security/compliance#enterprise-security-features), alongside SAML single sign-on and a dedicated support channel. Contact [founders@starsling.dev](mailto:founders@starsling.dev) to scope the retention window and export format your compliance program requires. # Data Handling (/security/data-handling) Transparency about data handling is essential. Here's exactly what the hosted StarSling product and GitHub App access and how we handle it. ## ci-speedup, ci-score, and ci-secure The [ci-speedup skill](https://docs.starsling.dev/skills/ci-speedup) runs locally through your coding agent and authenticated `gh` CLI. The [ci-score skill](https://docs.starsling.dev/skills/ci-score) runs locally against your checkout; once installed, scoring uses no network access at all. The [ci-secure skill](https://docs.starsling.dev/skills/ci-secure) runs locally against your workflow files; it calls the GitHub API through your own authenticated `gh` CLI for four checks, and runs without them if `gh` is unavailable. None of the three sends your code or CI data to StarSling, and none requires the StarSling GitHub App. Your coding-agent provider's own data policy applies separately. ## Data Access Summary | Data Type | Accessed | Stored | Retention | | ----------------------------- | -------- | ----------- | ----------------------------- | | Repository metadata | Yes | Yes | Account lifetime | | Account email | Yes | Yes | Until you delete your account | | Workflow definitions | Yes | No | - | | Source code | Yes | Temporarily | 24 hours | | Workflow job logs | Yes | Yes | No longer than 12 months | | Repository activity & context | Yes | No | - | | Secrets | No | No | - | | Environment variables | No | No | - | | Build artifacts | No | No | - | ## Detailed Breakdown ### Repository Metadata **What:** Repository name, owner, installation ID **Why:** To route webhooks and manage runner registration **Stored:** Yes, in our database **Retention:** Until you uninstall the GitHub App ### Account Email **What:** The email addresses on your GitHub account **When:** Read when you sign in to StarSling with GitHub **Why:** To create your StarSling account, send account email such as the welcome message, and match your GitHub commit author email to your Slack user if you enable pull request notifications **Stored:** Yes, in our database **Retention:** Until you delete your account ### Workflow Events **What:** Webhook payloads for `workflow_job` events **Why:** To provision runners when jobs start **Stored:** No (processed in memory) **Retention:** None ### Workflow Definitions (Optimizations) **What:** Your `.github/workflows/*.yml` files **When:** Periodically scanned by StarSling's AI agents to identify optimization opportunities **Why:** To suggest caching improvements, faster install strategies, build step optimizations, and workflow restructuring **Stored:** No (processed in memory during analysis) **Retention:** None ### Source Code (AI Analysis) **What:** Source code files in your repository **When:** Accessed for AI-powered optimizations **Why:** To provide context for optimization suggestions **Stored:** Yes, temporarily during analysis **Retention:** Deleted within 24 hours ### Workflow Job Logs **What:** The console output of workflow jobs that run on StarSling runners — the same log GitHub shows you under the Actions tab **When:** Retrieved after a job completes **Why:** Log output is where CI time is actually spent. Our optimization agents read it to find stalls, redundant work, and cache misses that job timings alone do not reveal, and to measure whether a proposed change worked **Stored:** Yes **Retention:** We retain job logs for no longer than 12 months, so analysis can compare a job against your own history. Deleted sooner on request — see [Data Deletion](#data-deletion) Job logs are the one thing StarSling retains on your behalf rather than processes and discards. If your workflows print sensitive values to the console, they are in that output — the same as they are in GitHub's own log for the run. ### Repository Activity & Context **What:** Read-only repository signals such as deployments, issues, discussions, pages, and code scanning alerts **When:** Accessed alongside workflow and source analysis **Why:** To give optimization suggestions broader context about your repository **Stored:** No (processed in memory during analysis) **Retention:** None ## How Your Data Is Used Your job logs and CI history are used for one purpose: producing optimization analysis **for your own organization**. * **Never pooled across customers.** Analysis for your organization runs against your organization's data only, and no other customer's analysis draws on yours. The single exception is aggregate usage counting for billing integrity, which sums job counts across the fleet and reads no log content. * **Never used to train models.** StarSling does not train, fine-tune, or otherwise build models on customer data. * **Never sold or shared.** Your data is not sold, licensed, or shared with third parties for their own purposes. ### Language model providers Our optimization agents are built on commercial language models, so analysis sends the specific log windows and timings a given analysis needs to that provider's API. Those providers are sub-processors, not recipients of your data for their own use, and we send only what the analysis requires — never a bulk export of your logs. If your security review needs the current provider list and their contractual terms, request it at [founders@starsling.dev](mailto:founders@starsling.dev). ## What We Never Access ### Secrets GitHub secrets are passed directly from GitHub to the runner. StarSling's infrastructure never sees secret values. ### Environment Variables Custom environment variables are injected by GitHub, not StarSling. ### Build Artifacts Artifacts uploaded via `actions/upload-artifact` go directly to GitHub's artifact storage. ## Changes We Make StarSling's AI agents propose optimizations by opening pull requests. This requires write access to **Contents** and **Workflows**, scoped to changes the GitHub App commits on new pull request branches. **What we do:** * Create a new branch and commit proposed changes * Open a pull request with an explanation for your review **What we don't do:** * Push to your default branch or any existing branch * Merge or approve pull requests See [GitHub App Permissions](https://docs.starsling.dev/configuration/github-app-permissions) for the full list of permissions and why each is requested. ## Isolation Every job runs on its own dedicated, single-use virtual machine. No machine is ever shared between jobs, and no machine is ever shared between customers. **One machine per job.** A machine is dedicated to your job when it is queued and destroyed when the job finishes. No machine is ever reused across jobs, so no job inherits a filesystem, an image cache, a process, or a credential from an earlier one — including from a fork pull request. (We keep a pool of pre-built, stopped machines so a job does not wait on a cold boot; an instance is claimed from it for exactly one job and destroyed afterwards, never returned to the pool.) **Single-use, organization-scoped credentials.** Each runner registers with GitHub using a just-in-time token minted for one organization's app installation. The token is single-use, and the runner deregisters itself when it exits. A runner can therefore only ever be assigned work from the organization it was minted for. **A machine boundary between customers.** No two customers' jobs ever share a host. On our sandbox providers a CPU job runs in a microVM; on our EC2 fleet a job gets a dedicated instance that terminates when the job ends. In both cases the boundary between two customers is a whole machine, not a container on a shared host. Being precise about what that does and does not mean: on the EC2 path your job runs in a privileged container *on its own dedicated instance*, so it shares a kernel with nothing but itself — the container is a packaging boundary, and the instance is the security boundary. Our GPU tiers are container-class on a dedicated machine on the same basis. The claim we make is the machine boundary between customers; we do not claim a microVM boundary on every path. Within a single organization, GitHub — not StarSling — decides which queued job a runner picks up: a runner registers with labels, and GitHub hands it the oldest queued job in your organization matching them. That assignment never crosses an organization boundary. ## Operator Access No StarSling employee can reach your account data **through the application** without opening a session that expires and is recorded. Direct access to production infrastructure is a separate, narrower path, described at the end of this section. Two categories are unreachable regardless of role: * **Secrets, credentials, and environment variables** — never received by our control plane, so there is no stored copy to grant access to. * **Build artifacts** — uploaded by GitHub directly to GitHub's storage. For everything else, support access is a named, deliberate path rather than a default: * **Administrator rights are granted individually.** They are a per-person grant held on the user record. No environment variable, job title, or organization role confers them, and holding an admin role inside your GitHub organization confers nothing on StarSling's side. To be exact about the shape of this: the administrator role itself is standing, held by a small number of people, and an existing administrator can grant it to another employee. What is not standing is reaching your data — that requires opening a support session, and that session is what expires and is audited. * **Support sessions are time-boxed.** When an administrator opens a session against an account to resolve a support request, it expires automatically (one hour by default) and can be revoked before then. * **A support session is a real session, not a read-only view.** It is granted so an administrator can reproduce and resolve what you are reporting, and it carries the permissions of the account it was opened against. Every request made within it is audited under both identities, which is the control — not a technical restriction to reading. * **Every action is attributable.** The session records the administrator behind it, so support activity is never indistinguishable from your own. * **Production infrastructure access requires MFA** and is limited to on-call engineers. This is a separate path from the support session above: it is direct access to the systems that store the data, it is not captured as API audit records, and it is governed by the on-call rotation and access review described under [Access Control](https://docs.starsling.dev/security/compliance#access-control). ## Audit Logging Authenticated requests to the StarSling API write an audit record capturing: | Field | What it records | | -------------------- | --------------------------------------------------------------------------- | | Identity | The user or API key that made the request | | Acting administrator | The administrator behind the request, when one is acting on a user's behalf | | Operation | The specific API operation invoked | | Outcome | HTTP status and result code | | Timestamp | When the request was handled | Records are stored independently of the accounts they reference, so removing a user does not erase the history of what was done with that account. Support access is covered by the same trail as ordinary traffic, with both identities — the account viewed and the administrator viewing it — on the record. On immutability, precisely: the application only ever inserts into this trail — no code path updates or deletes an audit record. That is an application-level guarantee, not a storage-level one; the table does not currently sit behind a database constraint or write-once storage that would stop someone holding production database credentials from altering it. Treat it as a complete record of application activity, not as tamper-proof evidence against an insider with infrastructure access. The scope of this trail, stated exactly: it covers authenticated requests to the application API. Requests rejected before an identity is established are not recorded as audit rows, and direct infrastructure access by on-call engineers is a separate path governed by [Access Control](https://docs.starsling.dev/security/compliance#access-control) rather than by this trail. Audit log export is available to Enterprise customers as part of an agreement with us; see [Enterprise Security Features](https://docs.starsling.dev/security/compliance#enterprise-security-features). ## Data Location | Data | Location | | ------------- | ----------------------------- | | Control plane | US East | | Runners | US East (more regions coming) | | Logs | US East | ## Encryption ### In Transit * All communications use TLS 1.3 * Webhook payloads verified with GitHub signatures * Runner-to-GitHub communication encrypted ### At Rest * Databases encrypted at rest with managed keys * Logs encrypted at rest ## Data Deletion ### On Uninstall When you uninstall the StarSling GitHub App: 1. All runners immediately deregistered 2. Account metadata deleted (within 7 days) 3. No backups retained **Uninstalling does not delete your retained job logs.** They stay under the 12-month window above unless you ask for them. If your intent in uninstalling is to have your data removed, say so — see below. ### On Request Contact [support@starsling.dev](mailto:support@starsling.dev) to request immediate deletion of your job logs and account data. The audit trail is the one exception, and deliberately so: the records described under [Audit Logging](#audit-logging) are stored independently of the accounts they reference and are not removed by a deletion request. That is what makes the trail evidence — a log the party it describes can delete cannot answer the question a security review is asking. # Compliance (/security/compliance) StarSling is committed to meeting enterprise security requirements. ## Current Status ### SOC 2 Type II **Status:** In progress We are actively working toward SOC 2 Type II certification. Expected completion: Q1 2027. ### ISO 27001 **Status:** In progress We are actively working toward ISO 27001 certification in parallel with SOC 2 Type II. Expected completion: Q1 2027. ## Security Practices ### Access Control * All employee access requires MFA * Production infrastructure access limited to on-call engineers * Access logged and audited quarterly * No employee reaches customer data through the application without opening a support session that expires and is audited under both identities — see [Operator Access](https://docs.starsling.dev/security/data-handling#operator-access) and [Audit Logging](https://docs.starsling.dev/security/data-handling#audit-logging) ### Infrastructure Security * Cloud infrastructure with security best practices * Regular security patches applied * Network segmentation between environments ### Incident Response * 24/7 on-call rotation * Documented incident response procedures * Customer notification within 24 hours for security incidents ### Vulnerability Management * Regular dependency updates * Automated security scanning in CI * Responsible disclosure program ## Vendor Security ### GitHub We integrate with GitHub's APIs, which maintain: * SOC 1, 2 * ISO 27001 * FedRAMP ## Security Questionnaire For enterprise security reviews, we provide: * CAIQ (Consensus Assessment Initiative Questionnaire) * SIG (Standard Information Gathering) * Custom questionnaire responses Contact [founders@starsling.dev](mailto:founders@starsling.dev) to request security documentation. ## Responsible Disclosure If you discover a security vulnerability, please report it to: **[security@starsling.dev](mailto:security@starsling.dev)** We commit to: * Acknowledging receipt within 24 hours * Providing status updates every 72 hours * Not pursuing legal action for good-faith research ## Enterprise Security Features Available for Enterprise: * Single Sign-On (SSO) via SAML * Audit log export * Custom data retention policies * Dedicated support channel * Security review calls # Common Issues (/troubleshooting/common-issues) Solutions to the most common issues with StarSling Runners. ## Runner Issues ### Personal Repositories Not Supported **Symptom:** You installed the StarSling GitHub App on a personal repository, but jobs remain queued and are never picked up by StarSling Runners. **Cause:** StarSling Runners only work with GitHub organizations, not personal repositories. GitHub's self-hosted runner architecture requires organization-level permissions for secure job routing and runner management. When a workflow runs, GitHub needs to authenticate and route the job to the correct runner pool. This authentication flow relies on organization-level API endpoints and webhook events that aren't available for personal repositories. Additionally, organization-level runner groups provide security boundaries that allow StarSling to safely manage runners across multiple repositories while maintaining isolation between different customers. **Solution:** Move your repository to a GitHub organization. You can [create a free organization](https://github.com/account/organizations/new) and transfer your repository to it. ### Runner Not Starting **Symptom:** Job stays in "Queued" state indefinitely. **Possible causes:** 1. **GitHub App not installed** * Verify the StarSling app is installed on your repository * Check Settings → Integrations → GitHub Apps 2. **Invalid label format** * Use the correct label format * Example: `starsling-ubuntu-24.04` 3. **Quota exceeded** * Check if you've hit your account limits * Contact support to increase limits **Solution:** ```yaml # Correct format runs-on: starsling-ubuntu-24.04 # Wrong formats runs-on: starsling-ubuntu-22.04 # ❌ Only Ubuntu 24.04 is supported runs-on: starsling/ubuntu-24.04 # ❌ Use hyphens, not slashes runs-on: starsling-ubuntu-latest # ❌ Use starsling-ubuntu-24.04 ``` ### Job Fails Immediately **Symptom:** Job starts but fails within seconds. **Possible causes:** 1. **Action compatibility issue** * Some actions have self-hosted runner restrictions * Check action documentation 2. **Missing dependencies** * Tool may not be pre-installed * Add setup step to install **Solution:** ```yaml steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 # Ensure Node.js is configured with: node-version: '20' ``` ### Slow Performance **Symptom:** Builds are slower than expected. **Possible causes:** 1. **Cache misses** * Check cache hit rate in logs * Verify cache key is correct 2. **Network downloads** * Dependencies being downloaded each run * Add caching for node\_modules, pip, etc. 3. **Sequential tests** * Use matrix builds to parallelize across multiple runners ## Cache Issues ### Cache Not Restoring **Symptom:** Cache action shows "Cache not found". **Possible causes:** 1. **Key mismatch** * Lockfile changed, generating new key * This is expected behavior 2. **Cache expired** * Unused caches expire after 7 days * Run a fresh build to repopulate 3. **Cross-branch caching** * Default branch caches aren't shared to feature branches initially **Solution:** ```yaml - uses: actions/cache@v4 with: path: node_modules key: deps-${{ hashFiles('package-lock.json') }} restore-keys: | deps- # Fallback to partial match ``` ### Cache Upload Slow **Symptom:** Cache upload takes several minutes. **Possible causes:** 1. **Large cache size** * Check what's being cached * Exclude unnecessary files 2. **Network issues** * Temporary connectivity problems * Usually resolves on retry ## Workflow Issues ### Environment Variables Missing **Symptom:** Expected env vars are undefined. **Solution:** Ensure you're setting them correctly: ```yaml jobs: build: runs-on: starsling-ubuntu-24.04 env: NODE_ENV: production steps: - run: echo $NODE_ENV ``` # Debug Access (/troubleshooting/debug-access) When troubleshooting isn't enough, here's how to get help. ## Viewing Logs ### GitHub Actions Logs Standard workflow logs are available in GitHub: 1. Go to your repository 2. Click **Actions** tab 3. Select the workflow run 4. Click on the failed job 5. Expand step logs ### Runner Assignment Logs To see StarSling-specific logs: 1. Look for the "Set up job" step 2. Expand to see runner assignment details 3. Note the runner ID for support requests ## Contacting Support ### Email Support **[support@starsling.dev](mailto:support@starsling.dev)** Include in your request: * Repository name (org/repo) * Workflow run URL * Runner ID (if available) * Description of the issue **Response time:** Within 24 hours (business days) ### Priority Support (Enterprise) Enterprise customers have access to: * Dedicated Slack channel * 4-hour response SLA * Direct engineering escalation ## Community ### Discord Join our Discord community for: * Real-time help from other users * Feature discussions * Release announcements [Join Discord](https://discord.gg/starsling) ### GitHub Discussions For longer-form questions and feature requests: [GitHub Discussions](https://github.com/starsling/community/discussions) ## Debug Checklist Before contacting support, verify: * [ ] GitHub App is installed and has repository access * [ ] Label format is correct (`starsling-ubuntu-24.04`) * [ ] No typos in the workflow file * [ ] Repository is not archived * [ ] The issue reproduces consistently ## Useful Information for Support When reporting an issue, include: ```markdown **Repository:** org/repo **Workflow:** ci.yml **Run URL:** https://github.com/org/repo/actions/runs/12345 **Runner ID:** sr-abc123xyz (from "Set up job" step) **Issue:** Description of what's happening vs. what you expected. **Steps to reproduce:** 1. Push to branch X 2. Workflow triggers 3. Job fails with error Y **What I've tried:** - Checked label format - Verified GitHub App installation - etc. ``` ## Status Page Check current system status: [status.starsling.dev](https://status.starsling.dev) Subscribe for incident notifications via email or RSS. ## Feature Requests Have an idea for StarSling? We'd love to hear it: * **Discord:** #feature-requests channel * **GitHub:** Open a discussion * **Email:** [founders@starsling.dev](mailto:founders@starsling.dev) Popular requests are prioritized in our roadmap.