# GitHub Action

Use [ContextOwl/contextowl-action](https://github.com/ContextOwl/contextowl-action) to publish Markdown docs, changelog entries, and an OpenAPI reference from a GitHub Actions workflow. The action calls the ContextOwl REST API with an agent key.

## Quick start

1. Create an agent key in **Admin > Settings > API**. Bind it to one workspace for the tightest scope.
2. Store the key in a repository secret named CONTEXTOWL_PAT.
3. Add .contextowl.yml to the repository root:

```yaml:.contextowl.yml
docs:
  dir: docs
changelog:
  file: CHANGELOG.md
openapi:
  spec: openapi.yaml
```

4. Add .github/workflows/contextowl.yml:

```yaml:contextowl.yml
name: Publish to ContextOwl

on:
  push:
    branches: [main]
    paths: ["docs/**", "CHANGELOG.md", "openapi.yaml", ".contextowl.yml"]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ContextOwl/contextowl-action@v1
        with:
          token: ${{ secrets.CONTEXTOWL_PAT }}
```

Push to main, and the action publishes the content that you configured. A workflow that passes a secret named CONTEXTOWL_TOKEN keeps working, because the action reads the key from the token input.

## Inputs

| Input | Required | Default | Description |
| --- | --- | --- | --- |
| token | yes | none | Agent key that starts with cowl_pat_. Always pass it from a secret. |
| server-url | no | https://contextowl.co | Base URL of the ContextOwl instance. The action appends /api/v1. |
| config | no | .contextowl.yml | Path to the config file, relative to the repository root. |
| workspace | no | none | Target workspace. It overrides the config file. Leave it out for a workspace-bound key. |
| prune | no | false | Remove content that is no longer in the repository. See Prune below. |
| dry-run | no | false | Print the plan and make no changes. |
| fail-on-error | no | true | Fail the job when an item fails to sync. Set it to false to report failed items as warnings. |
| allow-shrink | no | false | Accept a new article body that removes most of the current text. See Large removals below. |

The outputs created, updated, deleted, skipped, and failed hold the totals for all surfaces. A surface that stops before it finishes counts as 1 in failed. The action also writes a summary table to the workflow run.

## Configuration

Every surface is optional. Configure only the surfaces that you use. The file needs at least one.

```yaml:.contextowl.yml
workspace: prod # optional for workspace-bound keys
docs:
  dir: docs # Markdown directory
changelog:
  file: CHANGELOG.md # Keep a Changelog file
openapi:
  spec: openapi.yaml # OpenAPI 3.x JSON or YAML
prune: false # also settable with the prune input
```

## Docs

Each .md or .mdx file becomes one article. Front matter is optional:

```markdown:getting-started.md
---
title: Getting Started # defaults to the first H1, then the file name
section: Guides # defaults to the name of the parent directory
status: STABLE # DRAFT | IN REVIEW | BETA | STABLE | DEPRECATED
slug: getting-started # optional explicit URL slug
version: v2 # optional doc version label
---

# Getting Started
```

- The action matches an article by an explicit front-matter slug that exists, else by its title. The title match ignores case.
- The action also uses an explicit slug when it creates an article, so the URL stays the same when the title changes.
- The parent directory, or the section in the front matter, becomes the sidebar section. A new article goes into its section when the action creates it, and the action creates a section that doesn't exist yet.
- A status other than DRAFT needs article.publish. Without it, the article stays a draft and the action logs a warning.

## Changelog

A single [Keep a Changelog](https://keepachangelog.com) file becomes one entry for each version heading:

```markdown:CHANGELOG.md
## [1.4.0] - 2026-02-01

### Added

- Dark mode

### Fixed

- Export crash
```

- The version, such as 1.4.0, is the title and the identity of the entry.
- The date becomes the publish date. A future date schedules the entry.
- The action skips an Unreleased section.
- Publishing needs changelog.publish. Without it, new entries stay drafts.

Before the action sends an entry, it maps each ### subsection name to a ContextOwl tag. The match ignores case.

| Subsection | Tag |
| --- | --- |
| Added | new |
| Changed | improved |
| Deprecated | deprecated |
| Removed | deprecated |
| Fixed | fixed |
| Security | security |

The tag names and the server aliases also work as subsection names: New, Improved, Feature, Features, Improvement, Improvements, Fix, Fixes, Bugfix, and Deprecation. Any other subsection name gets no tag. The action logs a warning for that name and still syncs the entry.

## OpenAPI

The action uploads the spec to ContextOwl. ContextOwl generates the API reference pages again and removes the pages of endpoints that left the spec. An unchanged spec writes nothing. This needs openapi.attach.

## Sync behavior

- The action creates new content, updates changed content, and skips unchanged content, so revision history and the audit log stay clean.
- The changelog sync reads all remote entries, 100 in each request, so a file with many versions never creates duplicate entries.
- The action never changes encrypted articles or generated OpenAPI pages.
- When the server answers 429, the action waits for the time in Retry-After and sends the request again, up to 3 times. A read request does the same for 502, 503, and 504.

## Failures and warnings

The action writes the job summary first. Then it sets the job result:

- When an item fails to sync and fail-on-error is true, the job fails. An item is one article, one changelog entry, or the OpenAPI spec. With fail-on-error set to false, a failed item is a warning, and the failed output holds the count.
- When an error stops a whole surface, the job fails for every value of fail-on-error. A rejected key and a failed list request are examples. The other surfaces still run.
- When the key lacks article.publish, changelog.publish, or changelog.delete, the action logs a warning and continues without that step.
- When the key lacks changelog.update, the action sees only the published changelog entries. It doesn't create a draft or a scheduled entry, because the next run can't find that entry and would create it again. Each such version is a failed item. Add changelog.update to the key to sync these versions.
- When the server answers 402 or 403 to the OpenAPI upload, the action skips the OpenAPI step with a warning.

## Large removals

The server refuses a new article body that removes more than half of the current text and more than 2,000 characters. This guards a live article against a truncated or broken file. The article keeps its current body and counts as a failed item.

To accept such a change, set allow-shrink to true. The action then sends the body again with allow_shrink and logs a warning.

## Prune

Prune is off by default. When you turn it on with the input or with prune: true, the action removes content that is no longer in the repository:

- It sets docs to DEPRECATED. This needs article.publish.
- It deletes changelog entries. This needs changelog.delete.

> [!WARNING] Prune treats the repository as the source of truth for the whole workspace. It can deprecate or delete content that somebody created in the ContextOwl editor. Always do a dry run first.

## Recommended key permissions

| Surface | Minimum | Add for publish | Add for prune |
| --- | --- | --- | --- |
| Docs | article.read, article.create, article.update, section.create, article.place | article.publish | article.publish |
| Changelog | changelog.read, changelog.create, changelog.update | changelog.publish | changelog.delete |
| OpenAPI | openapi.read, openapi.attach, openapi.sync | none | none |

## Dry run

```yaml:dry-run.yml
- uses: ContextOwl/contextowl-action@v1
  with:
    token: ${{ secrets.CONTEXTOWL_PAT }}
    dry-run: true
```

The action prints what it would create, update, skip, or remove, and makes no changes.
