# Publish from CI

Publish from CI when your OpenAPI file is generated or updated alongside your
application. Each successful run sends the raw schema to the REST publishing
endpoint. Identical content is deduplicated, so rerunning a job is safe.

## Prepare the destination and token [#prepare-the-destination-and-token]

1. [Create a Skmtc account](https://skmtc.dev/signup), then choose the account
   handle, API slug, and visibility. For an organization API, the token's owner
   must be a member of that organization.
2. In [Settings → Access tokens](https://skmtc.dev/settings/tokens), generate
   a personal token with only `publish:schemas` and an expiration. Copy it
   immediately.
3. Save it as `SKMTC_TOKEN` in your CI secret store. Limit who can edit the
   publishing workflow and who can trigger deployments. Rotate the token from
   Settings and update the secret before it expires.

Choose `private` or `public` explicitly in the workflow. Many application
schemas should be private. The publishing endpoint has no visibility default.

## GitHub Actions example [#github-actions-example]

This workflow publishes `openapi.yaml` when it changes on `main`. Save it at
`.github/workflows/publish-openapi.yml`, and create a GitHub Actions repository
secret named `SKMTC_TOKEN`. Replace `ada` and `billing` with your destination.
If your schema is generated, add its generation and validation steps before
the publish step and point `--data-binary` at the generated file.

```yaml
name: Publish OpenAPI to Skmtc

on:
  push:
    branches: [main]
    paths:
      - openapi.yaml
      - .github/workflows/publish-openapi.yml
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: skmtc-billing-publish
  cancel-in-progress: false

jobs:
  publish:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Publish schema
        env:
          SKMTC_TOKEN: ${{ secrets.SKMTC_TOKEN }}
        run: |
          test -n "$SKMTC_TOKEN"
          test -s openapi.yaml
          curl --fail-with-body --show-error --silent \
            --retry 3 \
            --request POST \
            --header "Authorization: Bearer $SKMTC_TOKEN" \
            --header 'Content-Type: application/octet-stream' \
            --data-binary @openapi.yaml \
            'https://api.skmtc.dev/v1/apis/publish?owner=ada&slug=billing&visibility=private&filename=openapi.yaml'
```

Review the destination in the final URL before merging the workflow:

```text
Account:    ada
API slug:   billing
Visibility: private
```

The job fails on an HTTP error, including a missing token, invalid schema,
account permission error, slug conflict, or visibility mismatch. Keep the
token out of logs. If a new revision succeeds, the API page is
`https://skmtc.dev/ada/apis/billing`; its Markdown twin is
`https://skmtc.dev/ada/apis/billing.md`. Access to a private API still requires
authorization.

## Other CI systems [#other-ci-systems]

Use the same `curl` command in any CI runner after producing the OpenAPI file.
Inject `SKMTC_TOKEN` from the runner's secret store, make the request only
from a trusted branch or deployment job, and set `owner`, `slug`, and
`visibility` explicitly. Use `--data-binary @file` so the file is sent as raw
bytes. For a local or agent-driven publish, see [Publish an API](/docs/getting-started/publish-api).
