Run Surface Code Discovery in GitHub Actions
This guide adds Surface Code Discovery to one GitHub repository. Each push to main registers or updates the repository's APIs in APIsec. It also analyzes the code's dependencies, authentication, and committed secrets. With the settings in this workflow, your source code stays on the GitHub runner; only facts and observations about it reach APIsec.
Prerequisites
- A GitHub repository that contains your API's source code, in a supported framework. The workflow runs on pushes to
main; change the branch name if yours differs. - An APIsec Personal Access Token with the Create App and Get App scopes, created by a user whose platform role is
ADMINorUSER. Create it on the API Credentials page. - Your APIsec registration code, in the format
###-###, given to you by your APIsec Support resource. Each customer has their own code. If you don't have it, contact support. - Permission to add Actions secrets, variables, and workflows to the repository.
Step 1: Add the secrets
In the repository, open Settings › Secrets and variables › Actions and, on the Secrets tab, add:
| Name | Value |
|---|---|
APISEC_TOKEN | Your APIsec Personal Access Token |
APISEC_REGISTRATION_CODE | Your registration code. Each customer has their own; the CLI needs it on every run. |
That's all the workflow needs. To update an application that already exists in APIsec, such as one the Code Bolt action created, you also set the APISEC_APPLICATION_ID variable. See Step 3.
Step 2: Add the workflow
Create .github/workflows/apisec-surface.yml:
name: APIsec Surface
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
discover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Install the Surface CLI
run: |
pip install "apisec-surface==1.2.0"
apisec-surface telemetry off
- name: Pin the APIsec application
if: vars.APISEC_APPLICATION_ID != ''
env:
APISEC_APPLICATION_ID: ${{ vars.APISEC_APPLICATION_ID }}
run: |
mkdir -p apisec-code-bolt
printf 'applicationId: %s\n' "$APISEC_APPLICATION_ID" > apisec-code-bolt/state.yaml
- name: Discover and analyze APIs
env:
APISEC_PAT: ${{ secrets.APISEC_TOKEN }}
APISEC_REGISTRATION_CODE: ${{ secrets.APISEC_REGISTRATION_CODE }}
APISEC_SURFACE_LLM_EXTRACTION: "0"
run: apisec-surface analyze . --no-detect-monorepo --api-url https://api.apisecapps.com
Commit the file. The workflow runs on every push to main, and you can also start it from the Actions tab. It doesn't run on pull requests: a pull request's unmerged code would replace the application's inventory. Always pass --api-url: the CLI reads the APIsec URL only from this option, and the run fails without it. Use https://api.apisecapps.com unless APIsec gave you a different API URL.
What each environment variable does:
| Variable | Purpose |
|---|---|
APISEC_PAT | Authenticates the upload to APIsec. |
APISEC_REGISTRATION_CODE | Identifies your installation. Every fresh runner is a new installation, so the CLI needs the code on every run, even with --dry-run. If the secret is missing or empty, the log shows "Please enter code (###-###):" followed by "Aborted!", and the step fails with exit code 1. |
APISEC_SURFACE_LLM_EXTRACTION | "0" keeps all source code on the runner. The CLI sends APIsec only facts and observations about the code, such as routes, called functions, dependency versions, and masked secrets. Endpoints come from static analysis of the code. |
The CLI identifies the repository from the GITHUB_REPOSITORY variable that GitHub sets on every run, so each run updates the same application in APIsec. The Pin the APIsec application step is skipped unless you set APISEC_APPLICATION_ID in step 3, which points the workflow at an application that already exists. --no-detect-monorepo keeps the repository as one application. See Monorepos for the alternative.
apisec-surface telemetry off stops the CLI from sending anonymous usage data. The CLI does not commit files or open pull requests, so the workflow needs read access to the repository only. The workflow pins version 1.2.0; change the pin to upgrade. Versions before 1.2.0 can't identify the repository and register a new application on every run unless you set APISEC_APPLICATION_ID.
Step 3: Attach to an existing application (optional)
Skip this step for a new repository. The first run registers an application in APIsec, and every later run updates it.
Set APISEC_APPLICATION_ID when the workflow must update an application that already exists: one the Code Bolt action created, or one registered by an earlier run with a CLI older than 1.2.0. The pinned application takes precedence over the repository identity.
- Find the application's ID. In the APIsec console, open the application; the ID is in the page URL after
/application/. The View Results in APIsec link at the end of a workflow log contains it too. - In Settings › Secrets and variables › Actions, on the Variables tab, add
APISEC_APPLICATION_IDwith that ID. It's not secret, so a variable is fine. - Run the workflow. In Applications, that application updates and no new one appears.
Monorepos
For a repository with several services, choose one of these:
- Let the CLI split the repository. Remove
--no-detect-monorepofrom the analyze command. The CLI detects workspace boundaries, such as pnpm, npm, Maven, Gradle, .NET, Go, uv, and Poetry workspaces, and registers one application per sub-project, named after the repository and the sub-project's folder. Each run updates the same applications. The split ignoresAPISEC_APPLICATION_ID. - Run one job per service. Give each job the service's folder as the analyze path, keep
--no-detect-monorepo, and pin a different application in each job, for example with anAPISEC_APPLICATION_ID_ORDERSvariable and the pin step pointed at that folder (mkdir -p services/orders/apisec-code-bolt). The pin is required here: every job identifies the repository the same way, so without pins all the jobs update one application.
If your services import shared workspace packages, such as route factories or authentication middleware in packages/shared, a job that analyzes only one service's folder can't resolve those imports, and it can miss routes or their authentication. For these repositories, analyze from the repository root as one application, or compare a per-service run with a root run before you rely on it.
Step 4: Adjust the scan (optional)
Add any of these to the apisec-surface analyze command:
| Option | Effect |
|---|---|
A path instead of . | Scans only that folder, for example apisec-surface analyze ./backend. If you pin an application, write the pin to the same folder (./backend/apisec-code-bolt/state.yaml). |
--exclude "PATTERN" | Skips files that match a glob pattern. Repeat the option for more patterns. |
--timeout SECONDS | Sets how long the run waits for APIsec to finish verifying the analysis. The default is 30 minutes. A lower value saves runner minutes; the analysis continues in APIsec either way. |
--dry-run | Analyzes the code and prints a summary, but uploads nothing. APISEC_REGISTRATION_CODE is still required. |
--fail-on secrets | Fails the run with exit code 3, before anything is uploaded, when the scan finds a credential, such as an API key, committed to the repository. |
Verify
- Open the workflow run in the Actions tab. The Discover and analyze APIs step finishes green and the log lists the routes and frameworks it found, followed by a link to the results.
- In the APIsec console, open Applications. The repository (or, with one job per service, each service) appears as an application with its endpoints, frameworks, and libraries.
- In the application, dependency vulnerabilities that the analysis traced to an endpoint appear as predicted exploits. An APIsec scan of the running API later confirms or clears each one.
To test the setup without uploading, add --dry-run to the analyze command in the workflow and run it once.
For large applications, verification can take longer than the run waits. The run then logs "Stopped waiting for verification" and still finishes green. The analysis continues in APIsec, and the results appear at the link in the log.
Troubleshoot
Use the exit code and the error in the log to find the cause.
| Exit code or symptom | Cause | Fix |
|---|---|---|
1 with "Please enter code" and "Aborted!", or 4 with "No registration code found" | The APISEC_REGISTRATION_CODE secret is missing or empty | Add the repository secret and pass it in env, as in step 2. |
4, with "Cannot register application in CI" | The CLI couldn't identify the repository, because GITHUB_REPOSITORY didn't reach it | Run the CLI directly on the runner, as in step 2. If the step runs in a container, pass GITHUB_REPOSITORY into it. |
| Each run creates a new application | The workflow pins a CLI older than 1.2.0 and APISEC_APPLICATION_ID is not set | Change the pin to 1.2.0, or set APISEC_APPLICATION_ID as in step 3. With one job per service, every job needs its own pin. |
1, analysis error | The CLI couldn't analyze the code | Check the path you passed, and that the code uses a supported framework. |
2, with HTTP 401 in the log | The token is invalid or expired | Create a new token and update the APISEC_TOKEN secret. |
2, with HTTP 403 in the log | The token lacks a scope, or its creator lacks a role | Use a token with Create App and Get App, created by an ADMIN or USER. |
2, with SSL certificate verify failed | --api-url is wrong | Use https://api.apisecapps.com, or the API URL APIsec gave you. |
3, policy violation | --fail-on secrets found a committed credential | Remove the credential from the repository. |
4, configuration error | A required setting is missing or invalid | Read the error in the log. It names the missing setting. |
6, memory limit | The repository is too large for the runner | Scan a subfolder, or exclude generated and vendored code with --exclude. |
| Green run, no endpoints | No supported framework was found | Point the analyze command at the folder that contains the API code. |
Migrate from the Code Bolt GitHub Action
Code Bolt is now Surface Code Discovery. If a workflow uses apisec-inc/apisec-bolt-code-discovery-github-action, replace it with the workflow on this page. The old inputs map as follows: pass the folder as the analyze path instead of repo-path, and add --dry-run instead of dry-run: true. The CLI has no host-url option; set the API's base URL on the application in APIsec instead.
- Replace the
uses: apisec-inc/apisec-bolt-code-discovery-github-actionstep with theactions/setup-python, Install the Surface CLI, and Discover and analyze APIs steps from step 2. - Pass your token as
APISEC_PAT. You can keep the existing secret, for exampleAPISEC_PAT: ${{ secrets.API_DISCOVERY_TOKEN }}, if the token has the Create App and Get App scopes. - Add the
APISEC_REGISTRATION_CODEsecret. To keep the old action's application, add the Pin the APIsec application step and theAPISEC_APPLICATION_IDvariable. - Remove
contents: writeandpull-requests: writefrom the workflow's permissions. - Delete the
apisec-bolt-code-discovery/folder from the repository, and close any open pull requests that the old action created. The Surface CLI doesn't use that folder.
To keep the application the old action created, set APISEC_APPLICATION_ID to its ID before the first run. Otherwise the first run registers a new application. Copy any settings you need, such as the base URL and authentication, to it, then delete the old one. The published specification lives on the application in APIsec, not in the repository.
Related
- Surface Code Discovery: what it does, ways to run, and supported frameworks
- Surface Discovery for GitHub: discover APIs across a whole GitHub organization
- Run Surface Code Discovery in Azure Pipelines: the same setup for a repository in Azure DevOps
- Gate CI/CD builds on APIsec scan results: fail a build on scan findings