Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
.pytest_cache/
110 changes: 110 additions & 0 deletions extensions/avneeshjadhav04/superdocs-docs-pr-action/PROGRESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# PROGRESS.md — assumptions and decisions log

Assumptions made while building, logged per the task brief. Each entry: the
assumption, the reasoning, and where it lives in the code.

## Assumptions

1. **The doc target is a changelog (`CHANGELOG.md` by default).** A changelog
is the canonical "documentation update on merge" artifact: every merge
either deserves an entry or provably does not, which makes the skip logic
honest and testable. Configurable via `doc_path` for repos that maintain a
different doc. — `action.yml`, `decisions.py`

2. **REST API over MCP.** The card allows either surface. A CI runner has no
MCP server dependency, and the four-call contract (upload, chat, approve,
export) is the same on both surfaces. The MCP tool mapping is documented in
the README. — `superdocs_client.py`

3. **The machine approves the proposed change; the human reviews the PR.**
The card's "Review" surface is satisfied by `approval_mode:
ask_every_time` + the approve call (the gate is an operation the flow
exposes, exactly as behavior 4 of the shared task requires), and the human
review is the pull request itself — the durable artifact the card asks for.
The API auto-denies unattended reviews after ~1h, so a PR-gated flow is the
only shape that survives CI latency. — `main.py`

4. **The merged PR's title/body are untrusted data.** They are quoted into the
instruction as data, never executed as instructions, and the edit is scoped
to one changelog entry. Tested: a title that tries to give orders produces
a data-only entry. — `decisions.py::build_instruction`,
`tests::InstructionBuilderTests`

5. **"Nothing needs saying" has five independent triggers.** Docs-only PR,
skip marker, no change from SuperDocs (byte-identical export), entry
already present, docs PR already open. Each is a separate early exit so a
skip is provable, not assumed. — `decisions.py`, `main.py`

6. **The `pull_request.closed` event does not carry the file list.** The
action derives changed files from the merge commit (`git diff sha^1 sha`),
which is why checkout uses `fetch-depth: 0`. — `main.py`

7. **Proposed-change content can arrive as a JSON-encoded string.** The task
doc warns this is the most common integration trap; the client parses both
shapes. — `superdocs_client.py::_parse_changes`, tested.

8. **`awaiting_approval` has two flavours.** `continue_prompt` (large edit
paused) is resumed via `/continue`, never `/approve`; the client branches on
`metadata.awaiting_kind` exactly as the docs require. — tested.

9. **A docs PR for a merge that already exists is a duplicate.** Idempotency
is checked by searching open PRs with the same title before doing any work.
— `main.py::_existing_docs_pr`, tested.

## Cuts (defended)

- **No per-change human approval inside the action.** The card's review is the
PR; a second approval round inside CI would block on a human who is not
watching CI. The PR diff is the review surface.
- **No MCP mode.** REST covers the contract; MCP would add a server dependency
to the runner for no behavioral gain. Documented as a future option.
- **No multi-doc support.** The action maintains one doc file per repo; a
multi-file mode would multiply operations per merge for little value.
- **No PyGithub.** The first version used PyGithub (LGPL-3.0, weak copyleft).
Swapped for a ~120-line `github_client.py` that calls the GitHub REST API
directly with `requests`, so the dependency tree is permissively licensed
only (MIT/Apache/BSD). Cost: a few more lines and manual pagination if the
open-PR list ever grows past one page (unlikely for a docs-PR queue).

## Known limitations

- The changelog entry is only as accurate as SuperDocs' summary of the merged
PR; the human review is the safety net.
- `git diff` fallback for changed files requires `fetch-depth: 0`; without it,
a docs-only PR may not be detected and the action may run when it should
skip (it will still skip if SuperDocs produces no change).
- The action does not auto-merge; a human must merge the docs PR.

## Live-test findings (demo repo: avneeshjadhav04/docs-pr-demo)

Verified end to end on a real repository with a real SuperDocs API key:

- **Feature merge -> docs PR opens.** PR #8 merged, action drafted the entry
("- feat: slugify names (#8)"), opened PR #9 with a one-entry diff. The
diff touched only CHANGELOG.md.
- **Docs-only PR -> skip.** PR #10 (README change) merged; action logged
"changed only documentation" and exited 0.
- **Skip marker -> skip.** PR #11 with `[skip-docs]` in the title merged;
action logged "matches a skip pattern" and exited 0.

Three real bugs found and fixed during the live run:

1. **`${{ secrets.X }}` is invalid inside action.yml.** The `secrets` context
exists only in workflow files, not action manifests; the runner rejected
the manifest at load time. Fixed by removing the expression from the input
description. (Found via run log, not docs.)
2. **Session-level `Content-Type: application/json` broke multipart uploads.**
`requests` will not override an existing Content-Type header when sending
`files=`, so the upload body went out as JSON and the server returned 422
"Field required: file". Fixed by setting only the Authorization header on
the session; `json=` sets Content-Type per request.
3. **A completed job can carry `metadata.pending_changes: None`.** The AI
applied the edit directly (approval_mode was honored, but the job completed
with no pending-changes array), and `_parse_changes` crashed on None.
Fixed by treating None/missing as an empty list and logging the AI's
response text for diagnostics.

Also observed: GitHub blocks Actions-created PRs unless the repository setting
"Allow GitHub Actions to create and approve pull requests" is enabled
(403 "GitHub Actions is not permitted to create or approve pull requests").
Documented in the README as a setup requirement.
126 changes: 126 additions & 0 deletions extensions/avneeshjadhav04/superdocs-docs-pr-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# SuperDocs Docs PR Action

A GitHub Action that drafts a documentation update with SuperDocs when a pull
request is merged, and opens a pull request with the diff for human review.
When nothing needs saying, it says nothing: the action skips entirely.

Built for the SuperDocs task (round 2, engineer track).

## What it does

On `pull_request` closed + merged:

1. Decides whether the merge needs a documentation update at all.
2. If yes, asks SuperDocs to add one changelog entry for the merged PR.
3. Approves the proposed change (the machine drives the review gate) and
exports the finished file.
4. Opens a pull request containing only that diff, for a human to review.

## Screenshots

The docs PR the action opened automatically after merging PR #14 on the demo
repository, and the changelog diff it drafted via SuperDocs — one entry,
nothing else touched:

![Docs PR opened by the action](screenshot-pr.png)

![Changelog diff drafted by SuperDocs](screenshot-diff.png)

## When it skips

The action exits silently (success) when nothing needs saying:

- the merged PR changed only documentation (`.md`, `.rst`, `.txt`, `.adoc`,
or anything under `docs/`, `documentation/`, `doc/`),
- the merged PR changed only the doc file itself,
- the PR title or body matches a skip pattern (`[skip-docs]`, `no-docs`, ...),
- SuperDocs produced no change (export byte-identical to the current file),
- the doc already mentions the merged PR number,
- a docs PR for this merge is already open (idempotency).

## How to use it

> **Setup requirement:** enable **Settings -> Actions -> General -> Workflow
> permissions -> "Allow GitHub Actions to create and approve pull requests"**.
> Without it, GitHub rejects the docs PR with 403.

```yaml
name: Draft documentation PR on merge

on:
pull_request:
types: [closed]

permissions:
contents: write
pull-requests: write

jobs:
docs-pr:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Draft documentation PR
uses: avneeshjadhav04/superdocs-docs-pr-action@v1
with:
superdocs_api_key: ${{ secrets.SUPERDOCS_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
doc_path: CHANGELOG.md
```

### Inputs

| Input | Required | Default | Description |
| --- | --- | --- | --- |
| `superdocs_api_key` | yes | — | SuperDocs API key (`sk_...`). Store as a repository secret. |
| `github_token` | no | `github.token` | Token used to open the docs PR. Use a PAT if the PR must trigger other workflows. |
| `doc_path` | no | `CHANGELOG.md` | Documentation file the action maintains. |
| `base_branch` | no | merged PR's base | Branch the docs PR targets. |
| `skip_patterns` | no | `skip-docs,no-docs` | Comma-separated regexes; a match on the merged PR title/body skips the run. |
| `dry_run` | no | `false` | Print every decision without calling the API, writing files, or opening a PR. |

## How it works

- **SuperDocs surface:** REST API, the four-call contract — upload
(`POST /v1/documents/upload`), edit instruction (`POST /v1/chat/async` with
`approval_mode: ask_every_time`), approve (`POST /v1/chat/{session}/approve`),
export (`POST /v1/documents/export`). The same four operations exist as MCP
tools (`upload_document`, `chat_async`, `approve_changes`, `export_document`);
the action uses REST because a CI runner has no MCP server dependency.
- **Review:** the machine approves the proposed change (the gate is an
operation the flow exposes), and the human review is the pull request itself.
- **Prompt safety:** the merged PR's title and body are untrusted data. They
are quoted into the instruction as data to summarize, never as instructions
to follow, and the edit is scoped to a single changelog entry.
- **Cost:** one run is 1–2 operations (chat + export; exports are free).
`dry_run: true` previews a run without spending anything.

## Development

```bash
pip install -r requirements.txt
python -m pytest tests/ -q
```

All tests run without a live API key: the SuperDocs client and the GitHub
client are mocked, and the decision logic is tested directly.

## Dependencies and licensing

The only runtime dependency is `requests` (Apache-2.0). GitHub operations
(branch, commit, PR) go through the GitHub REST API directly via
`src/github_client.py` — no PyGithub, no copyleft dependencies. The project
is MIT-licensed.

## What it does not do

- It does not edit code, open issues, or touch anything outside the doc file.
- It does not auto-merge the docs PR; a human reviews and merges it.
- It does not run on PRs that are closed without merging.
- It does not guarantee the changelog entry is accurate — SuperDocs summarizes
the merged PR, and the human review is the safety net.

## Credits

Built by Avneesh Jadhav for the SuperDocs hiring task (round 2). Uses the
SuperDocs REST API (docs.superdocs.app).
81 changes: 81 additions & 0 deletions extensions/avneeshjadhav04/superdocs-docs-pr-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: "SuperDocs Docs PR"
description: >-
On merge, drafts a documentation update with SuperDocs and opens a pull
request with the diff for human review. Skips entirely when nothing needs
saying.
author: avneeshjadhav04
branding:
icon: "file-text"
color: "blue"

inputs:
superdocs_api_key:
description: >-
SuperDocs API key (sk_...). Create one at use.superdocs.app under
Settings -> API Keys. Store it as a repository secret named
SUPERDOCS_API_KEY and pass it in your workflow.
required: true
github_token:
description: >-
Token used to open the documentation pull request. Defaults to the
built-in GITHUB_TOKEN; use a PAT if you need the PR to trigger other
workflows.
required: false
default: ${{ github.token }}
doc_path:
description: >-
Path to the documentation file the action maintains, relative to the
repository root. Defaults to CHANGELOG.md.
required: false
default: "CHANGELOG.md"
base_branch:
description: >-
Branch the documentation pull request targets. Defaults to the branch
the merged pull request was merged into.
required: false
default: ""
skip_patterns:
description: >-
Comma-separated list of regex patterns. If the merged pull request's
title or body matches any of them, the action skips. Defaults to
skip-docs,no-docs.
required: false
default: "skip-docs,no-docs"
dry_run:
description: >-
When true, prints every decision the action would make and never calls
the SuperDocs API, never writes files, and never opens a pull request.
Use this to preview a run without spending operations.
required: false
default: "false"

runs:
using: "composite"
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
shell: bash
run: |
python -m pip install --quiet --upgrade pip
python -m pip install --quiet requests

- name: Run SuperDocs Docs PR
shell: bash
env:
SUPERDOCS_API_KEY: ${{ inputs.superdocs_api_key }}
GITHUB_TOKEN: ${{ inputs.github_token }}
DOC_PATH: ${{ inputs.doc_path }}
BASE_BRANCH: ${{ inputs.base_branch }}
SKIP_PATTERNS: ${{ inputs.skip_patterns }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
python "${{ github.action_path }}/src/main.py"
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Draft documentation PR on merge

on:
pull_request:
types: [closed]

permissions:
contents: write
pull-requests: write

jobs:
docs-pr:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Draft documentation PR
uses: avneeshjadhav04/superdocs-docs-pr-action@v1
with:
superdocs_api_key: ${{ secrets.SUPERDOCS_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
doc_path: CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests>=2.31
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading