docmost-cli 0.4.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/.claude/skills/docmost/SKILL.md +18 -4
- docmost_cli-0.7.0/.github/workflows/ci.yml +170 -0
- docmost_cli-0.7.0/.github/workflows/release.yml +231 -0
- docmost_cli-0.7.0/CHANGELOG.md +371 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/CLAUDE.md +56 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/PKG-INFO +45 -14
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/README.md +44 -12
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/SPECIFICATION.md +209 -51
- docmost_cli-0.7.0/man/man1/docmost-cli-attachment.1 +157 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-comment.1 +37 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-config.1 +32 -2
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-page.1 +165 -15
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-search.1 +30 -5
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-space.1 +37 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-sync.1 +22 -11
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-user.1 +4 -1
- docmost_cli-0.7.0/man/man1/docmost-cli-workspace.1 +105 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/man/man1/docmost-cli.1 +57 -5
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/pyproject.toml +6 -2
- docmost_cli-0.7.0/release-notes.md +62 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/__init__.py +1 -1
- docmost_cli-0.7.0/src/docmost_cli/api/attachments.py +97 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/auth.py +22 -4
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/client.py +40 -25
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/comments.py +13 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/pages.py +279 -61
- docmost_cli-0.7.0/src/docmost_cli/api/pagination.py +298 -0
- docmost_cli-0.7.0/src/docmost_cli/api/position.py +287 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/spaces.py +23 -7
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/users.py +4 -4
- docmost_cli-0.7.0/src/docmost_cli/cli/_list_opts.py +248 -0
- docmost_cli-0.7.0/src/docmost_cli/cli/attachment.py +98 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/comment.py +32 -8
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/config_cmd.py +83 -31
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/main.py +70 -32
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/page.py +216 -47
- docmost_cli-0.7.0/src/docmost_cli/cli/search.py +56 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/space.py +30 -7
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/sync_cmd.py +33 -27
- docmost_cli-0.7.0/src/docmost_cli/cli/user.py +30 -0
- docmost_cli-0.7.0/src/docmost_cli/cli/workspace.py +66 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/config/settings.py +3 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/config/store.py +17 -12
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/convert/prosemirror_to_md.py +10 -2
- docmost_cli-0.7.0/src/docmost_cli/models/__init__.py +5 -0
- docmost_cli-0.7.0/src/docmost_cli/models/common.py +47 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/output/__init__.py +10 -0
- docmost_cli-0.7.0/src/docmost_cli/output/formatter.py +166 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/output/tree.py +2 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/__init__.py +4 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/diff.py +42 -3
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/manifest.py +30 -8
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/pull.py +17 -19
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/push.py +142 -94
- docmost_cli-0.7.0/tests/test_api/test_attachments.py +122 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_auth.py +54 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_client.py +74 -0
- docmost_cli-0.7.0/tests/test_api/test_pages.py +504 -0
- docmost_cli-0.7.0/tests/test_api/test_pagination.py +249 -0
- docmost_cli-0.7.0/tests/test_api/test_position.py +127 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_spaces.py +67 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_attachment_commands.py +48 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_comment_commands.py +42 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_config_commands.py +31 -0
- docmost_cli-0.7.0/tests/test_cli/test_list_fields.py +139 -0
- docmost_cli-0.7.0/tests/test_cli/test_main.py +60 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_page_commands.py +351 -16
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_sync_commands.py +69 -0
- docmost_cli-0.7.0/tests/test_cli/test_user_commands.py +55 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_workspace_commands.py +50 -10
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_config/test_store.py +35 -0
- docmost_cli-0.7.0/tests/test_docs/test_man_pages.py +88 -0
- docmost_cli-0.7.0/tests/test_output/test_formatter.py +148 -0
- docmost_cli-0.7.0/tests/test_sync/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_sync/test_diff.py +90 -1
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_sync/test_push.py +126 -92
- docmost_cli-0.4.0/CHANGELOG.md +0 -102
- docmost_cli-0.4.0/man/man1/docmost-cli-attachment.1 +0 -57
- docmost_cli-0.4.0/man/man1/docmost-cli-workspace.1 +0 -68
- docmost_cli-0.4.0/src/docmost_cli/api/attachments.py +0 -30
- docmost_cli-0.4.0/src/docmost_cli/api/pagination.py +0 -94
- docmost_cli-0.4.0/src/docmost_cli/cli/attachment.py +0 -30
- docmost_cli-0.4.0/src/docmost_cli/cli/search.py +0 -33
- docmost_cli-0.4.0/src/docmost_cli/cli/user.py +0 -25
- docmost_cli-0.4.0/src/docmost_cli/cli/workspace.py +0 -40
- docmost_cli-0.4.0/src/docmost_cli/models/__init__.py +0 -3
- docmost_cli-0.4.0/src/docmost_cli/models/common.py +0 -3
- docmost_cli-0.4.0/src/docmost_cli/output/formatter.py +0 -85
- docmost_cli-0.4.0/tests/test_api/test_attachments.py +0 -45
- docmost_cli-0.4.0/tests/test_api/test_pages.py +0 -289
- docmost_cli-0.4.0/tests/test_api/test_pagination.py +0 -62
- docmost_cli-0.4.0/tests/test_cli/test_user_commands.py +0 -28
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/.claude/skills/docmost/examples.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/.gitignore +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/LICENSE +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/__main__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/search.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/api/workspace.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/config/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/convert/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/py.typed +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/frontmatter.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/conftest.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/code_block.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/code_block.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/empty_doc.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/empty_doc.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/headings_and_text.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/headings_and_text.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/lists.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/lists.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/marks.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/marks.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/simple_paragraph.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/simple_paragraph.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/table.json +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/fixtures/table.md +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_comments.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_search.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_users.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_api/test_workspace.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_search_commands.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_cli/test_space_commands.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_config/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_config/test_settings.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_convert/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_convert/test_prosemirror_to_md.py +0 -0
- {docmost_cli-0.4.0/tests/test_output → docmost_cli-0.7.0/tests/test_docs}/__init__.py +0 -0
- {docmost_cli-0.4.0/tests/test_sync → docmost_cli-0.7.0/tests/test_output}/__init__.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_output/test_tree.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_sync/test_frontmatter.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_sync/test_manifest.py +0 -0
- {docmost_cli-0.4.0 → docmost_cli-0.7.0}/tests/test_sync/test_pull.py +0 -0
|
@@ -96,19 +96,33 @@ docmost-cli page export <page-id> --format md
|
|
|
96
96
|
|
|
97
97
|
1. Get current content: `docmost-cli page get <page-id>`
|
|
98
98
|
2. Modify the Markdown content as needed
|
|
99
|
-
3. Update: `docmost-cli page update <page-id> --
|
|
100
|
-
4.
|
|
99
|
+
3. Update: `docmost-cli page update <page-id> --file updated.md`
|
|
100
|
+
4. Content updates are applied in place — the page keeps its ID, slug, history
|
|
101
|
+
and inbound links. Works on Community and Enterprise alike, from Docmost
|
|
102
|
+
v0.71. Use `--append` / `--prepend` to add without replacing.
|
|
101
103
|
|
|
102
104
|
## Output Format
|
|
103
105
|
|
|
104
106
|
- **Content commands** (`page get`): Raw Markdown to stdout — directly usable
|
|
105
|
-
- **List commands** (`--json`): JSON array
|
|
107
|
+
- **List commands** (`--json`): JSON array of **complete server objects** — every
|
|
108
|
+
field the API returned, not a subset. For pages that means `id`, `slugId`,
|
|
109
|
+
`title`, `icon`, `position`, `parentPageId`, `spaceId`, `creatorId`,
|
|
110
|
+
`lastUpdatedById`, `isLocked`, `createdAt`, `updatedAt` and more. Page
|
|
111
|
+
*content* is not included; fetch it with `page get <id>`.
|
|
112
|
+
Pagination is followed automatically, so the array is complete; add
|
|
113
|
+
`--envelope` if you need `hasNextPage` / `nextCursor`
|
|
114
|
+
- **Narrow the output** with `--fields id,title,updatedAt` when you only need a
|
|
115
|
+
few keys — worth doing to keep context small, since `--json` is now much
|
|
116
|
+
larger. It works in JSON and table mode, and an unknown name errors with the
|
|
117
|
+
list of fields the server actually returned
|
|
118
|
+
- **Single-item commands** (`user me`, `workspace info`): add `--json` for the
|
|
119
|
+
complete object
|
|
106
120
|
- **Write commands** (`page create`, `delete`): Page ID to stdout, confirmation to stderr
|
|
107
121
|
- **Always use `--json`** for list/search commands when you need to parse the output programmatically
|
|
108
122
|
|
|
109
123
|
## Tips
|
|
110
124
|
|
|
111
|
-
- Use `--json`
|
|
125
|
+
- Use `--json` on list commands to get parseable output, plus `--fields` to keep it small
|
|
112
126
|
- Page IDs are UUIDs like `019a2a69-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
|
113
127
|
- Space slugs are lowercase alphanumeric (e.g., `engineering`, `devtest`)
|
|
114
128
|
- Use `-y` flag to skip confirmation prompts for automated workflows
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Uses only first-party actions, and installs with pip rather than uv. The
|
|
4
|
+
# project prefers uv locally (see CLAUDE.md), but this runner could not resolve
|
|
5
|
+
# `astral-sh/setup-uv@v9` at all, and pip is the documented fallback — a CI file
|
|
6
|
+
# that reliably runs beats a marginally faster one.
|
|
7
|
+
#
|
|
8
|
+
# Pins are the current majors, which target Node 24. Node 20 is deprecated and
|
|
9
|
+
# is being force-migrated on the runners, so checkout@v4 / setup-python@v5 would
|
|
10
|
+
# have had a shelf life.
|
|
11
|
+
#
|
|
12
|
+
# actionlint is downloaded and checksummed rather than run as an action, for the
|
|
13
|
+
# same reason: one less third-party resolution in the path.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
branches: [main]
|
|
18
|
+
pull_request:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
25
|
+
cancel-in-progress: true
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
test:
|
|
29
|
+
name: test (py${{ matrix.python-version }})
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
strategy:
|
|
32
|
+
# Report every interpreter, rather than hiding 3.13 behind a 3.11 failure.
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v7
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-python@v6
|
|
40
|
+
with:
|
|
41
|
+
python-version: ${{ matrix.python-version }}
|
|
42
|
+
cache: pip
|
|
43
|
+
cache-dependency-path: pyproject.toml
|
|
44
|
+
|
|
45
|
+
- name: Install
|
|
46
|
+
run: |
|
|
47
|
+
python -m pip install --upgrade pip
|
|
48
|
+
python -m pip install -e ".[dev]"
|
|
49
|
+
|
|
50
|
+
- name: Verify the interpreter matches the matrix
|
|
51
|
+
# Without this, a misconfigured setup step would silently run every
|
|
52
|
+
# matrix entry on the runner's default Python and report three passes
|
|
53
|
+
# for one version.
|
|
54
|
+
run: |
|
|
55
|
+
python - <<'PY'
|
|
56
|
+
import sys
|
|
57
|
+
actual = "{}.{}".format(*sys.version_info[:2])
|
|
58
|
+
expected = "${{ matrix.python-version }}"
|
|
59
|
+
assert actual == expected, f"expected Python {expected}, got {actual}"
|
|
60
|
+
print("Python", sys.version)
|
|
61
|
+
PY
|
|
62
|
+
|
|
63
|
+
- name: Test
|
|
64
|
+
run: python -m pytest tests/ -q
|
|
65
|
+
|
|
66
|
+
quality:
|
|
67
|
+
name: lint and type-check
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v7
|
|
71
|
+
|
|
72
|
+
- uses: actions/setup-python@v6
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.11"
|
|
75
|
+
cache: pip
|
|
76
|
+
cache-dependency-path: pyproject.toml
|
|
77
|
+
|
|
78
|
+
- name: Install
|
|
79
|
+
run: |
|
|
80
|
+
python -m pip install --upgrade pip
|
|
81
|
+
python -m pip install -e ".[dev]"
|
|
82
|
+
|
|
83
|
+
- name: Ruff lint
|
|
84
|
+
run: python -m ruff check src/ tests/
|
|
85
|
+
|
|
86
|
+
- name: Ruff format
|
|
87
|
+
run: python -m ruff format --check src/ tests/
|
|
88
|
+
|
|
89
|
+
# Runs once, not per matrix entry: pyproject pins mypy's
|
|
90
|
+
# python_version = "3.11", so the result is interpreter-independent.
|
|
91
|
+
- name: Mypy (strict)
|
|
92
|
+
run: python -m mypy src/
|
|
93
|
+
|
|
94
|
+
workflows:
|
|
95
|
+
name: lint workflows
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
# Bump both together: the checksum is for the linux_amd64 tarball of this
|
|
98
|
+
# exact version, published on the actionlint releases page.
|
|
99
|
+
env:
|
|
100
|
+
ACTIONLINT_VERSION: 1.7.12
|
|
101
|
+
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v7
|
|
104
|
+
|
|
105
|
+
- name: Install actionlint
|
|
106
|
+
run: |
|
|
107
|
+
set -euo pipefail
|
|
108
|
+
curl -fsSL -o actionlint.tar.gz \
|
|
109
|
+
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
|
110
|
+
echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum --check --strict
|
|
111
|
+
tar xzf actionlint.tar.gz actionlint
|
|
112
|
+
|
|
113
|
+
- name: Lint the workflows
|
|
114
|
+
# actionlint only shell-lints `run:` blocks when shellcheck is on PATH,
|
|
115
|
+
# and quietly skips them when it is not. Assert it, rather than trust the
|
|
116
|
+
# runner image to keep shipping it and lose half the coverage in silence.
|
|
117
|
+
run: |
|
|
118
|
+
set -euo pipefail
|
|
119
|
+
command -v shellcheck
|
|
120
|
+
./actionlint -color
|
|
121
|
+
|
|
122
|
+
build:
|
|
123
|
+
name: build
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v7
|
|
127
|
+
|
|
128
|
+
- uses: actions/setup-python@v6
|
|
129
|
+
with:
|
|
130
|
+
python-version: "3.11"
|
|
131
|
+
|
|
132
|
+
- name: Build sdist and wheel
|
|
133
|
+
run: |
|
|
134
|
+
python -m pip install --upgrade pip build
|
|
135
|
+
python -m build
|
|
136
|
+
|
|
137
|
+
- name: Verify the built version matches __version__
|
|
138
|
+
# The version is dynamic: hatch reads it out of src/docmost_cli/__init__.py.
|
|
139
|
+
# If that regex ever stops matching, the build silently produces the wrong
|
|
140
|
+
# version rather than failing.
|
|
141
|
+
run: |
|
|
142
|
+
python - <<'PY'
|
|
143
|
+
import pathlib, re
|
|
144
|
+
|
|
145
|
+
source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
|
|
146
|
+
match = re.search(r'__version__ = "([^"]+)"', source)
|
|
147
|
+
assert match, "no __version__ in src/docmost_cli/__init__.py"
|
|
148
|
+
expected = match.group(1)
|
|
149
|
+
|
|
150
|
+
built = sorted(p.name for p in pathlib.Path("dist").glob("*.whl"))
|
|
151
|
+
assert built, "no wheel produced"
|
|
152
|
+
assert any(f"-{expected}-" in name for name in built), (
|
|
153
|
+
f"__version__ is {expected} but built {built}"
|
|
154
|
+
)
|
|
155
|
+
print(f"version {expected} OK: {built}")
|
|
156
|
+
PY
|
|
157
|
+
|
|
158
|
+
- name: Verify the man pages ship in the wheel
|
|
159
|
+
# They are installed via [tool.hatch.build.targets.wheel.shared-data],
|
|
160
|
+
# which breaks quietly if the path ever moves.
|
|
161
|
+
run: |
|
|
162
|
+
python - <<'PY'
|
|
163
|
+
import pathlib, zipfile
|
|
164
|
+
|
|
165
|
+
wheel = next(iter(pathlib.Path("dist").glob("*.whl")))
|
|
166
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
167
|
+
pages = [n for n in names if n.endswith(".1") and "share/man/man1" in n]
|
|
168
|
+
assert len(pages) == 10, f"expected 10 man pages in the wheel, found {len(pages)}"
|
|
169
|
+
print(f"{len(pages)} man pages OK")
|
|
170
|
+
PY
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI and cuts a GitHub Release when a v* tag is pushed.
|
|
4
|
+
#
|
|
5
|
+
# Authentication is PyPI Trusted Publishing (OIDC) — no API token is stored in
|
|
6
|
+
# this repository. One-time setup on pypi.org, under the project's
|
|
7
|
+
# "Publishing" settings, add a trusted publisher:
|
|
8
|
+
#
|
|
9
|
+
# Owner: glinhard
|
|
10
|
+
# Repository: docmost-cli
|
|
11
|
+
# Workflow name: release.yml
|
|
12
|
+
# Environment: pypi
|
|
13
|
+
#
|
|
14
|
+
# Dry runs: trigger this workflow manually (Actions -> Release -> Run workflow)
|
|
15
|
+
# to exercise the whole chain — version guard, build, artifact checks, and
|
|
16
|
+
# action resolution — without uploading anything anywhere.
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
tags: ["v*"]
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
inputs:
|
|
23
|
+
dry-run:
|
|
24
|
+
description: "Build and verify only; do not publish"
|
|
25
|
+
type: boolean
|
|
26
|
+
default: true
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
verify:
|
|
33
|
+
name: verify and build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
|
|
38
|
+
- uses: actions/setup-python@v6
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.11"
|
|
41
|
+
cache: pip
|
|
42
|
+
cache-dependency-path: pyproject.toml
|
|
43
|
+
|
|
44
|
+
- name: Install
|
|
45
|
+
run: |
|
|
46
|
+
python -m pip install --upgrade pip build twine
|
|
47
|
+
python -m pip install -e ".[dev]"
|
|
48
|
+
|
|
49
|
+
- name: Full test suite
|
|
50
|
+
# The tag is the last chance to catch a regression before it is
|
|
51
|
+
# published immutably: PyPI does not allow re-uploading a version.
|
|
52
|
+
run: python -m pytest tests/ -q
|
|
53
|
+
|
|
54
|
+
- name: Verify the tag matches __version__
|
|
55
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
56
|
+
run: |
|
|
57
|
+
python - <<'PY'
|
|
58
|
+
import os, pathlib, re
|
|
59
|
+
|
|
60
|
+
source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
|
|
61
|
+
version = re.search(r'__version__ = "([^"]+)"', source).group(1)
|
|
62
|
+
tag = os.environ["GITHUB_REF_NAME"]
|
|
63
|
+
assert tag == f"v{version}", f"tag {tag} does not match __version__ {version}"
|
|
64
|
+
print(f"tag {tag} matches __version__ {version}")
|
|
65
|
+
PY
|
|
66
|
+
|
|
67
|
+
- name: Verify this version is not already on PyPI
|
|
68
|
+
# PyPI rejects a duplicate upload anyway; failing here gives a clearer
|
|
69
|
+
# message than a 400 from the publish step.
|
|
70
|
+
run: |
|
|
71
|
+
python - <<'PY'
|
|
72
|
+
import json, pathlib, re, urllib.request
|
|
73
|
+
|
|
74
|
+
source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
|
|
75
|
+
version = re.search(r'__version__ = "([^"]+)"', source).group(1)
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(
|
|
78
|
+
"https://pypi.org/pypi/docmost-cli/json", timeout=30
|
|
79
|
+
) as response:
|
|
80
|
+
released = set(json.load(response)["releases"])
|
|
81
|
+
except Exception as exc: # noqa: BLE001 - never block a release on this
|
|
82
|
+
print(f"could not reach PyPI ({exc}); skipping the duplicate check")
|
|
83
|
+
else:
|
|
84
|
+
assert version not in released, (
|
|
85
|
+
f"{version} is already on PyPI; bump __version__ before tagging"
|
|
86
|
+
)
|
|
87
|
+
print(f"{version} is not yet on PyPI (has {sorted(released)})")
|
|
88
|
+
PY
|
|
89
|
+
|
|
90
|
+
- name: Build sdist and wheel
|
|
91
|
+
run: python -m build
|
|
92
|
+
|
|
93
|
+
- name: Check the distributions
|
|
94
|
+
run: python -m twine check dist/*
|
|
95
|
+
|
|
96
|
+
- name: Verify the wheel contents
|
|
97
|
+
run: |
|
|
98
|
+
python - <<'PY'
|
|
99
|
+
import pathlib, re, zipfile
|
|
100
|
+
|
|
101
|
+
source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
|
|
102
|
+
version = re.search(r'__version__ = "([^"]+)"', source).group(1)
|
|
103
|
+
|
|
104
|
+
wheel = next(iter(pathlib.Path("dist").glob("*.whl")))
|
|
105
|
+
assert f"-{version}-" in wheel.name, f"built {wheel.name}, expected {version}"
|
|
106
|
+
|
|
107
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
108
|
+
pages = [n for n in names if n.endswith(".1") and "share/man/man1" in n]
|
|
109
|
+
assert len(pages) == 10, f"expected 10 man pages, found {len(pages)}"
|
|
110
|
+
|
|
111
|
+
metadata = next(n for n in names if n.endswith("METADATA"))
|
|
112
|
+
text = zipfile.ZipFile(wheel).read(metadata).decode()
|
|
113
|
+
assert "License-Expression: AGPL-3.0-or-later" in text, "missing licence expression"
|
|
114
|
+
assert "Classifier: License ::" not in text, (
|
|
115
|
+
"deprecated License :: classifier is back; PEP 639 replaces it "
|
|
116
|
+
"with the license expression"
|
|
117
|
+
)
|
|
118
|
+
print(f"{wheel.name}: {len(pages)} man pages, licence metadata OK")
|
|
119
|
+
PY
|
|
120
|
+
|
|
121
|
+
# upload@v7 and download@v8 are a matched pair and should move together:
|
|
122
|
+
# v7 added the option to upload files as-is, and v8 is the major that
|
|
123
|
+
# stopped assuming every download is a zip.
|
|
124
|
+
#
|
|
125
|
+
# Keep `archive` at its default of true. Uploading as-is takes only a
|
|
126
|
+
# single file, and this step ships two — the wheel and the sdist.
|
|
127
|
+
- uses: actions/upload-artifact@v7
|
|
128
|
+
with:
|
|
129
|
+
name: dist
|
|
130
|
+
path: dist/
|
|
131
|
+
|
|
132
|
+
publish:
|
|
133
|
+
name: publish to PyPI
|
|
134
|
+
needs: verify
|
|
135
|
+
runs-on: ubuntu-latest
|
|
136
|
+
# The job itself also runs on a manual dispatch, so a dry run resolves and
|
|
137
|
+
# downloads every action here — including the third-party publish action.
|
|
138
|
+
# That matters: `astral-sh/setup-uv` turned out not to resolve on these
|
|
139
|
+
# runners at all, and a release is the worst moment to discover that.
|
|
140
|
+
environment:
|
|
141
|
+
name: pypi
|
|
142
|
+
url: https://pypi.org/project/docmost-cli/
|
|
143
|
+
permissions:
|
|
144
|
+
id-token: write # required for Trusted Publishing
|
|
145
|
+
steps:
|
|
146
|
+
- uses: actions/download-artifact@v8
|
|
147
|
+
with:
|
|
148
|
+
name: dist
|
|
149
|
+
path: dist/
|
|
150
|
+
|
|
151
|
+
- name: Publish
|
|
152
|
+
# Uploading is gated on the ref being a tag, not on the dry-run input.
|
|
153
|
+
# A dispatch always runs against a branch, so no manual invocation can
|
|
154
|
+
# publish, whatever inputs it is given.
|
|
155
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
156
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
157
|
+
|
|
158
|
+
- name: Dry run summary
|
|
159
|
+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
160
|
+
run: |
|
|
161
|
+
echo "Dry run: actions resolved and artifacts built, nothing published."
|
|
162
|
+
echo "Push a v* tag to publish for real."
|
|
163
|
+
ls -l dist/
|
|
164
|
+
|
|
165
|
+
github-release:
|
|
166
|
+
name: GitHub Release
|
|
167
|
+
needs: publish
|
|
168
|
+
runs-on: ubuntu-latest
|
|
169
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
170
|
+
permissions:
|
|
171
|
+
contents: write # required to create the release
|
|
172
|
+
steps:
|
|
173
|
+
- uses: actions/checkout@v7
|
|
174
|
+
|
|
175
|
+
- uses: actions/download-artifact@v8
|
|
176
|
+
with:
|
|
177
|
+
name: dist
|
|
178
|
+
path: dist/
|
|
179
|
+
|
|
180
|
+
- name: Extract this version's CHANGELOG section
|
|
181
|
+
run: |
|
|
182
|
+
python - <<'PY'
|
|
183
|
+
import os, pathlib, re
|
|
184
|
+
|
|
185
|
+
version = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
186
|
+
changelog = pathlib.Path("CHANGELOG.md").read_text()
|
|
187
|
+
# Everything from this version's heading up to the next "## " heading.
|
|
188
|
+
match = re.search(
|
|
189
|
+
rf"^## {re.escape(version)}.*?$(.*?)(?=^## )",
|
|
190
|
+
changelog,
|
|
191
|
+
re.MULTILINE | re.DOTALL,
|
|
192
|
+
)
|
|
193
|
+
body = match.group(1).strip() if match else f"See CHANGELOG.md for {version}."
|
|
194
|
+
pathlib.Path("release-notes.md").write_text(body + "\n")
|
|
195
|
+
print(body[:500])
|
|
196
|
+
PY
|
|
197
|
+
|
|
198
|
+
- name: Create or update the release
|
|
199
|
+
# The tag can arrive two ways: `git push origin v0.6.0`, or the GitHub
|
|
200
|
+
# web UI's "Draft a new release" flow, which creates the tag *and* the
|
|
201
|
+
# release before this job runs. Handle both, so neither route ends in a
|
|
202
|
+
# red job.
|
|
203
|
+
env:
|
|
204
|
+
GH_TOKEN: ${{ github.token }}
|
|
205
|
+
run: |
|
|
206
|
+
set -euo pipefail
|
|
207
|
+
tag="$GITHUB_REF_NAME"
|
|
208
|
+
|
|
209
|
+
if gh release view "$tag" >/dev/null 2>&1; then
|
|
210
|
+
echo "Release $tag already exists; attaching artifacts."
|
|
211
|
+
gh release upload "$tag" dist/* --clobber
|
|
212
|
+
|
|
213
|
+
# Only fill in notes if whoever created it left them empty —
|
|
214
|
+
# never overwrite notes a human wrote by hand.
|
|
215
|
+
#
|
|
216
|
+
# `body` is a nullable string, and jq renders a null as the four
|
|
217
|
+
# characters "null" — non-empty, so without the `// ""` this test
|
|
218
|
+
# reads an empty release as one a human had already written.
|
|
219
|
+
if [ -z "$(gh release view "$tag" --json body --jq '.body // ""' | tr -d '[:space:]')" ]; then
|
|
220
|
+
echo "Release body is empty; filling it from CHANGELOG.md."
|
|
221
|
+
gh release edit "$tag" --notes-file release-notes.md
|
|
222
|
+
else
|
|
223
|
+
echo "Release already has notes; leaving them alone."
|
|
224
|
+
fi
|
|
225
|
+
else
|
|
226
|
+
echo "Creating release $tag."
|
|
227
|
+
gh release create "$tag" \
|
|
228
|
+
--title "$tag" \
|
|
229
|
+
--notes-file release-notes.md \
|
|
230
|
+
dist/*
|
|
231
|
+
fi
|