logseq-md 0.1.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.
Files changed (36) hide show
  1. logseq_md-0.1.0/.biome.json +24 -0
  2. logseq_md-0.1.0/.checkmake.ini +4 -0
  3. logseq_md-0.1.0/.editorconfig +30 -0
  4. logseq_md-0.1.0/.gitattributes +46 -0
  5. logseq_md-0.1.0/.github/dependabot.yml +28 -0
  6. logseq_md-0.1.0/.github/workflows/governance.yml +59 -0
  7. logseq_md-0.1.0/.github/workflows/rehearsal.yml +124 -0
  8. logseq_md-0.1.0/.github/workflows/release.yml +288 -0
  9. logseq_md-0.1.0/.github/workflows/tests.yml +145 -0
  10. logseq_md-0.1.0/.gitignore +37 -0
  11. logseq_md-0.1.0/.linkspector.yml +24 -0
  12. logseq_md-0.1.0/.mailmap +11 -0
  13. logseq_md-0.1.0/.markdownlint-cli2.jsonc +11 -0
  14. logseq_md-0.1.0/.pre-commit-config.yaml +195 -0
  15. logseq_md-0.1.0/.pymarkdown.json +18 -0
  16. logseq_md-0.1.0/.ruff.toml +20 -0
  17. logseq_md-0.1.0/.scripts/check_markdown_style.py +78 -0
  18. logseq_md-0.1.0/.scripts/check_no_symlinks.py +61 -0
  19. logseq_md-0.1.0/.scripts/check_uvx_invocation.py +103 -0
  20. logseq_md-0.1.0/.scripts/verify-published.py +118 -0
  21. logseq_md-0.1.0/.scripts/version.py +166 -0
  22. logseq_md-0.1.0/.taplo.toml +11 -0
  23. logseq_md-0.1.0/.yamlfmt +14 -0
  24. logseq_md-0.1.0/CONTRIBUTING.md +41 -0
  25. logseq_md-0.1.0/DESIGN.md +882 -0
  26. logseq_md-0.1.0/IMPLEMENTATION.md +607 -0
  27. logseq_md-0.1.0/LICENSE +21 -0
  28. logseq_md-0.1.0/Makefile +35 -0
  29. logseq_md-0.1.0/PKG-INFO +85 -0
  30. logseq_md-0.1.0/README.md +60 -0
  31. logseq_md-0.1.0/pyproject.toml +60 -0
  32. logseq_md-0.1.0/src/logseq_md/__init__.py +21 -0
  33. logseq_md-0.1.0/src/logseq_md/__main__.py +8 -0
  34. logseq_md-0.1.0/src/logseq_md/cli.py +51 -0
  35. logseq_md-0.1.0/tests/test_cli.py +57 -0
  36. logseq_md-0.1.0/tests/test_release_scripts.py +218 -0
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "includes": ["**/*.json", "**/*.jsonc"]
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "space",
14
+ "indentWidth": 2,
15
+ "lineEnding": "lf",
16
+ "lineWidth": 100
17
+ },
18
+ "linter": {
19
+ "enabled": true,
20
+ "rules": {
21
+ "recommended": true
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ [minphony]
2
+ # This repo has no build/clean phase; the real entry points are test/check/
3
+ # bootstrap. Disable the default requirement for `all` and `clean` targets.
4
+ disabled = true
@@ -0,0 +1,30 @@
1
+ # EditorConfig - editor-layer half of LF enforcement (see .gitattributes and the
2
+ # mixed-line-ending pre-commit hook for the other two layers).
3
+ # https://editorconfig.org
4
+ root = true
5
+
6
+ [*]
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+ charset = utf-8
11
+ indent_style = space
12
+ indent_size = 2
13
+
14
+ # What ruff-format emits, written down so an editor does not fight it.
15
+ [*.py]
16
+ indent_size = 4
17
+
18
+ [*.{json,jsonc,yaml,yml,toml,md}]
19
+ indent_size = 2
20
+
21
+ [Makefile]
22
+ indent_style = tab
23
+
24
+ # Logseq's own files, for whenever fixtures land here. The format is tab
25
+ # indented and that is not a preference: a fixture reindented to spaces stops
26
+ # testing what it was written to test.
27
+ [tests/fixtures/**]
28
+ indent_style = tab
29
+ trim_trailing_whitespace = false
30
+ insert_final_newline = false
@@ -0,0 +1,46 @@
1
+ # Normalise all text to LF on commit, independent of contributor OS.
2
+ # Pairs with .editorconfig and the mixed-line-ending pre-commit hook
3
+ # (belt-and-braces LF enforcement).
4
+ * text=auto eol=lf
5
+
6
+ # Explicit text types (source, documentation, tooling)
7
+ *.py text eol=lf
8
+ *.pyi text eol=lf
9
+ *.md text eol=lf
10
+ *.json text eol=lf
11
+ *.jsonc text eol=lf
12
+ *.yaml text eol=lf
13
+ *.yml text eol=lf
14
+ *.toml text eol=lf
15
+ *.cfg text eol=lf
16
+ *.ini text eol=lf
17
+ *.sh text eol=lf
18
+ .gitignore text eol=lf
19
+ .gitattributes text eol=lf
20
+ .editorconfig text eol=lf
21
+
22
+ # Test fixtures are bytes under test, not text to normalise. A graph fixture
23
+ # exists to prove a write left every untouched byte alone, so a checkout that
24
+ # rewrites its line endings makes the test assert something else - and it passes
25
+ # on Linux and fails on Windows, which is the worst of the three outcomes.
26
+ tests/fixtures/** -text
27
+
28
+ # Generated or vendored content that should not count as authored code in the
29
+ # language statistics on the repository page.
30
+ tests/fixtures/** linguist-vendored
31
+
32
+ # Binary assets: if we ever add images or PDFs, they belong in Git LFS (keeps
33
+ # history lean). `-text` exempts them from the `* text eol=lf` rule above so LFS
34
+ # and line-ending normalisation do not collide.
35
+ #
36
+ # These rules are inert until a matching file is added: with none tracked, git
37
+ # never invokes the lfs filter, so git-lfs is NOT required to clone, commit or
38
+ # test. It becomes required only for whoever first adds such a file - run
39
+ # `git lfs install` in your clone before committing one.
40
+ *.png filter=lfs diff=lfs merge=lfs -text
41
+ *.jpg filter=lfs diff=lfs merge=lfs -text
42
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
43
+ *.gif filter=lfs diff=lfs merge=lfs -text
44
+ *.webp filter=lfs diff=lfs merge=lfs -text
45
+ *.svg filter=lfs diff=lfs merge=lfs -text
46
+ *.pdf filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,28 @@
1
+ ---
2
+ version: 2
3
+ updates:
4
+ # Bump pinned pre-commit hook SHAs (keeps the frozen revisions current).
5
+ - package-ecosystem: pre-commit
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ commit-message:
10
+ prefix: "chore(pre-commit)"
11
+
12
+ # Bump pinned GitHub Actions SHAs.
13
+ - package-ecosystem: github-actions
14
+ directory: /
15
+ schedule:
16
+ interval: weekly
17
+ commit-message:
18
+ prefix: "chore(actions)"
19
+
20
+ # Python dependencies. There are none yet beyond the dev group, and the entry
21
+ # is here so the first one added is watched from the day it lands rather than
22
+ # from the day somebody remembers this file.
23
+ - package-ecosystem: uv
24
+ directory: /
25
+ schedule:
26
+ interval: weekly
27
+ commit-message:
28
+ prefix: "chore(deps)"
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: Governance
3
+
4
+ permissions:
5
+ contents: read
6
+
7
+ # Pull requests only, and deliberately: GitHub builds a pull request's checks
8
+ # against the head merged into `main`, so what this runs on is already the merge
9
+ # result. `workflow_dispatch` remains for asking `main` the question by hand.
10
+ #
11
+ # What this gives up is the case where two pull requests pass separately and
12
+ # conflict in meaning once both are in. Closing that needs the branch to be up
13
+ # to date before merging, which is a repository setting rather than anything
14
+ # here.
15
+ on:
16
+ pull_request:
17
+ workflow_dispatch:
18
+
19
+ concurrency:
20
+ group: governance_${{ github.head_ref || github.ref_name }}
21
+ cancel-in-progress: true
22
+
23
+ jobs:
24
+ governance:
25
+ name: 🩺 (run governance checks)
26
+ runs-on: ubuntu-latest
27
+ timeout-minutes: 10
28
+ defaults:
29
+ run:
30
+ shell: bash -elo pipefail {0}
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+
35
+ - name: Setup node
36
+ # The link checker drives a headless browser, which is a node toolchain.
37
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
38
+ with:
39
+ node-version: "24"
40
+
41
+ - name: Setup uv
42
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
43
+ with:
44
+ cache-dependency-glob: |
45
+ pyproject.toml
46
+ .scripts/*.py
47
+
48
+ # The suite includes ruff over every Python file and `ty` over `src/` and
49
+ # `tests/`, so the type check is one hook rather than a second job: a
50
+ # contributor who runs `make check` gets the same answer CI does, which is
51
+ # the property worth having.
52
+ #
53
+ # It also covers every link in the tree. `linkspector-changed` is handed the
54
+ # changed files on the commit path and every file here, which is the whole
55
+ # sweep already: a separate `linkspector-all` step would drive the same
56
+ # browser over the same URLs a second time, double the runtime and double
57
+ # the chance of a throttled link failing the job.
58
+ - name: Run pre-commit hooks
59
+ run: uvx prek run --show-diff-on-failure --color=always --all-files
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: Rehearsal
3
+
4
+ # Publish every pull request to TestPyPI under a throwaway version, then install
5
+ # it from there and run it.
6
+ #
7
+ # What this catches and nothing else does is an index saying no. A classifier
8
+ # warehouse dislikes, a licence expression it will not parse, a name that
9
+ # normalises onto an existing project, a metadata version the upload path does
10
+ # not know. Those are answered by an upload and by nothing before it, and the
11
+ # first release of federated-agent-kits, where this workflow comes from, was
12
+ # stopped by the last of them, on `main`, after a tag had been pushed.
13
+ #
14
+ # It is a separate workflow from `release.yml` rather than a job inside it,
15
+ # because a trusted publisher is matched on the workflow's filename. One file
16
+ # can reach TestPyPI and one can reach PyPI, and the one that can reach PyPI
17
+ # runs only on a push to `main`. A rehearsal that could authenticate as the
18
+ # release would not be a rehearsal.
19
+
20
+ on:
21
+ pull_request:
22
+ branches: [main]
23
+ workflow_dispatch:
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ concurrency:
29
+ group: rehearsal_${{ github.head_ref || github.ref_name }}
30
+ cancel-in-progress: true
31
+
32
+ jobs:
33
+ rehearsal:
34
+ name: 🧪 (publish a rehearsal and install it)
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 10
37
+ environment: testpypi
38
+ permissions:
39
+ contents: read
40
+ id-token: write
41
+ steps:
42
+
43
+ - name: Checkout
44
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
45
+ with:
46
+ fetch-depth: 0
47
+
48
+ - name: Setup uv
49
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
50
+ with:
51
+ cache-dependency-glob: |
52
+ pyproject.toml
53
+ .scripts/*.py
54
+
55
+ - name: Work out a version nobody will want
56
+ id: version
57
+ # The version this pull request would release if it merged next, with a
58
+ # dev segment behind it, so what gets uploaded is shaped like what would
59
+ # be. The level comes from the bump label, which the release workflow
60
+ # already insists on exactly one of; without one, `patch` stands in,
61
+ # because the point here is the upload path rather than the number.
62
+ #
63
+ # The serial is the run id. Not a commit sha, which is the obvious choice
64
+ # and is not available: PEP 440 wants a number in a dev segment, and the
65
+ # other spelling, `0.2.1+g6ec3cb1`, is refused by both indexes because
66
+ # warehouse forbids local versions. A run id is also the better answer
67
+ # anyway, because an index accepts a version exactly once and never again,
68
+ # so re-running this workflow on the same commit has to produce a new
69
+ # string.
70
+ env:
71
+ LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
72
+ SERIAL: ${{ github.run_id }}
73
+ run: |
74
+ set -euo pipefail
75
+ level="patch"
76
+ for label in ${LABELS}; do
77
+ case "${label}" in
78
+ major|minor|patch) level="${label}" ;;
79
+ esac
80
+ done
81
+ rehearsal=$(uv run .scripts/version.py dev "${level}" "${SERIAL}")
82
+ echo "rehearsal=${rehearsal}" >> "${GITHUB_OUTPUT}"
83
+ echo "Rehearsing ${level} as ${rehearsal}."
84
+
85
+ - name: Write it into the working tree
86
+ # Written, never committed. The release workflow refuses a pull request
87
+ # that edits the version line, and this does not: nothing here is pushed.
88
+ env:
89
+ REHEARSAL: ${{ steps.version.outputs.rehearsal }}
90
+ run: uv run .scripts/version.py write "${REHEARSAL}"
91
+
92
+ - name: Build the distribution
93
+ run: uv build --out-dir dist
94
+
95
+ - name: Check the distribution
96
+ # The same `--strict` gate the release runs, here so that the cheap
97
+ # failure is reported before the slow one.
98
+ run: uvx twine check --strict dist/*
99
+
100
+ - name: Publish to TestPyPI
101
+ # Skipped for a pull request from a fork, which cannot be given
102
+ # `id-token: write` and therefore cannot authenticate. The job still
103
+ # reports, having built and checked, so this can be a required check
104
+ # without a fork's pull request being unmergeable.
105
+ if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
106
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
107
+ with:
108
+ repository-url: https://test.pypi.org/legacy/
109
+
110
+ - name: Install it from TestPyPI and run it
111
+ # A green publish means an index accepted some bytes. It does not mean
112
+ # anybody can install them, and it does not mean what they install works.
113
+ if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
114
+ env:
115
+ REHEARSAL: ${{ steps.version.outputs.rehearsal }}
116
+ run: uv run .scripts/verify-published.py "${REHEARSAL}" https://test.pypi.org/simple/
117
+
118
+ - name: Say why nothing was uploaded
119
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
120
+ # Out loud, because a silently skipped step in a green job is how a gate
121
+ # stops being a gate without anybody noticing.
122
+ run: |-
123
+ echo "::warning::This pull request is from a fork, which cannot be given an OIDC token."
124
+ echo "::warning::The distribution was built and checked; it was not uploaded or installed."
@@ -0,0 +1,288 @@
1
+ ---
2
+ name: Release
3
+
4
+ # Every merge to main ships. The version lives in `pyproject.toml`, is written
5
+ # here and never in a pull request. Two reasons, and the second is the one that
6
+ # decided it: a hand-bumped version disagrees with its tag the first time
7
+ # somebody forgets, and every concurrent pull request touches the same line and
8
+ # conflicts on merge. What a pull request carries instead is a label saying how
9
+ # big it is, which is a thing a reviewer already decides.
10
+ #
11
+ # An unpublished package is an untested release pipeline, which is why the first
12
+ # version ships with this pipeline (IMPLEMENTATION.md, S0) rather than with the
13
+ # first verb: the first real publish is the one that finds the misconfigured
14
+ # name, the missing classifier and the trusted-publisher mismatch.
15
+ #
16
+ # The order of the steps below is the lesson from that first publish, and it is
17
+ # not the one first drawn from it. The upload is the only step that cannot be
18
+ # undone, so it goes last. Everything before it is revocable: a version written
19
+ # into a working tree, a commit, a tag. What stopped the sibling's first release was a
20
+ # malformed distribution, and `twine check --strict` now catches that before
21
+ # anything is written at all.
22
+
23
+ on:
24
+ pull_request:
25
+ types: [opened, reopened, labeled, unlabeled, synchronize]
26
+ push:
27
+ branches: [main]
28
+
29
+ permissions:
30
+ contents: read
31
+
32
+ concurrency:
33
+ # Never cancelled in flight: a release half-done is a tag with no version
34
+ # behind it, or the reverse.
35
+ group: release_${{ github.head_ref || github.ref_name }}
36
+ cancel-in-progress: false
37
+
38
+ jobs:
39
+ bump_label:
40
+ name: 🏷 (the pull request says how big it is)
41
+ if: github.event_name == 'pull_request'
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 5
44
+ steps:
45
+
46
+ - name: Checkout
47
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
48
+ with:
49
+ fetch-depth: 0
50
+
51
+ - name: Setup uv
52
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
53
+ with:
54
+ cache-dependency-glob: |
55
+ pyproject.toml
56
+ .scripts/*.py
57
+
58
+ - name: Refuse a pull request without exactly one bump label
59
+ # Exactly one, not at least one: two labels is not a bigger bump, it is a
60
+ # disagreement, and picking the larger silently would release a major
61
+ # because somebody left `minor` on from an earlier round.
62
+ env:
63
+ LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
64
+ run: |
65
+ set -euo pipefail
66
+ chosen=""
67
+ for label in ${LABELS}; do
68
+ case "${label}" in
69
+ major|minor|patch|no-release)
70
+ if [ -n "${chosen}" ]; then
71
+ echo "::error::Two bump labels (${chosen}, ${label}). One says what this change is."
72
+ exit 1
73
+ fi
74
+ chosen="${label}"
75
+ ;;
76
+ esac
77
+ done
78
+ if [ -z "${chosen}" ]; then
79
+ echo "::error::Label this pull request major, minor, patch or no-release."
80
+ echo "major: a setup that works stops working. minor: something new. patch: a fix."
81
+ echo "no-release: nothing that ships - CI, docs about the repo itself."
82
+ exit 1
83
+ fi
84
+ echo "This pull request is a ${chosen}."
85
+
86
+ - name: Refuse a hand-edited version
87
+ # The release job owns that one line. A pull request that also writes it
88
+ # gets overwritten on merge at best, and at worst ships a version the tag
89
+ # does not agree with, which is unfixable afterwards because the package is
90
+ # already on PyPI. The rest of `pyproject.toml` is free: a classifier or a
91
+ # dependency is an ordinary change, so this reads the diff rather than the
92
+ # file list.
93
+ run: uv run .scripts/version.py check-untouched "origin/${{ github.event.pull_request.base.ref }}"
94
+
95
+ release:
96
+ name: 🏷 (tag the merge)
97
+ if: github.event_name == 'push'
98
+ runs-on: ubuntu-latest
99
+ timeout-minutes: 10
100
+ environment: pypi
101
+ permissions:
102
+ contents: write
103
+ # Trusted publishing: PyPI is told to trust this workflow in this
104
+ # repository, so there is no API token to leak, rotate or forget.
105
+ id-token: write
106
+ steps:
107
+
108
+ - name: Mint a token for the release bot
109
+ id: token
110
+ # The release commit has to pass the ruleset on main, and the token a job
111
+ # is handed by default cannot: it is deliberately not allowed to bypass
112
+ # rules, which is the right default everywhere except here. An app
113
+ # installed on this repository can be. It holds one permission
114
+ # (contents: write), the token it mints expires within the hour, and it is
115
+ # not a person - so revoking it touches nobody's own access and the release
116
+ # commits read as a bot's rather than as a maintainer's.
117
+ #
118
+ # Configured by hand, once, by somebody with admin: the app, its
119
+ # installation here, the two secrets below, and an entry for the app in
120
+ # the bypass list of the ruleset protecting main. The last of those is not
121
+ # a permission and is the one that gets forgotten; without it this pushes
122
+ # and is declined. IMPLEMENTATION.md, S0, lists the steps.
123
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
124
+ with:
125
+ # The client id rather than the numeric app id, which the action now
126
+ # deprecates. Both name the same app; the client id is the one GitHub
127
+ # has standardised on, and it is on the app's own settings page beside
128
+ # the id it replaces.
129
+ client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
130
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
131
+
132
+ - name: Checkout
133
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
134
+ with:
135
+ fetch-depth: 0
136
+ token: ${{ steps.token.outputs.token }}
137
+
138
+ - name: Setup uv
139
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
140
+ with:
141
+ cache-dependency-glob: |
142
+ pyproject.toml
143
+ .scripts/*.py
144
+
145
+ - name: Find the bump this merge asked for
146
+ id: bump
147
+ # From the pull request the commit came in on, because that is where a
148
+ # person judged the change as a whole. A push straight to main has no such
149
+ # judgement and releases nothing: silence is the safe reading, since the
150
+ # alternative is guessing a severity for a change nobody reviewed.
151
+ env:
152
+ GH_TOKEN: ${{ github.token }}
153
+ run: |
154
+ set -euo pipefail
155
+ labels=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
156
+ --jq '.[0].labels[].name' || true)
157
+ level=""
158
+ for label in ${labels}; do
159
+ case "${label}" in
160
+ major|minor|patch) level="${label}" ;;
161
+ no-release) level="none" ;;
162
+ esac
163
+ done
164
+ if [ -z "${level}" ]; then
165
+ echo "No bump label on the pull request this commit arrived in; nothing to release."
166
+ level="none"
167
+ fi
168
+ echo "level=${level}" >> "${GITHUB_OUTPUT}"
169
+
170
+ - name: Work out the next version
171
+ id: version
172
+ if: steps.bump.outputs.level != 'none'
173
+ env:
174
+ LEVEL: ${{ steps.bump.outputs.level }}
175
+ run: |
176
+ set -euo pipefail
177
+ current=$(uv run .scripts/version.py current)
178
+ next=$(uv run .scripts/version.py next "${LEVEL}")
179
+ echo "current=${current}" >> "${GITHUB_OUTPUT}"
180
+ echo "next=${next}" >> "${GITHUB_OUTPUT}"
181
+ echo "Releasing ${current} -> ${next}."
182
+
183
+ - name: Work out who the bot commits as
184
+ id: identity
185
+ if: steps.bump.outputs.level != 'none'
186
+ # A bot's noreply address has to carry its numeric user id, and a commit
187
+ # whose address does not match is attributed to nobody - it renders as a
188
+ # grey avatar and links nowhere, which is how a history stops answering
189
+ # "who did this". The id is asked for rather than pasted in because the app
190
+ # it belongs to is created by hand, and a number hardcoded here is one
191
+ # nobody will think to change.
192
+ env:
193
+ GH_TOKEN: ${{ steps.token.outputs.token }}
194
+ SLUG: ${{ steps.token.outputs.app-slug }}
195
+ run: |
196
+ set -euo pipefail
197
+ id=$(gh api "/users/${SLUG}[bot]" --jq .id)
198
+ echo "name=${SLUG}[bot]" >> "${GITHUB_OUTPUT}"
199
+ echo "email=${id}+${SLUG}[bot]@users.noreply.github.com" >> "${GITHUB_OUTPUT}"
200
+
201
+ - name: Write the version into the working tree
202
+ if: steps.bump.outputs.level != 'none'
203
+ env:
204
+ NEXT: ${{ steps.version.outputs.next }}
205
+ run: uv run .scripts/version.py write "${NEXT}"
206
+
207
+ - name: Build the distribution
208
+ if: steps.bump.outputs.level != 'none'
209
+ # Both artefacts: the wheel is what uvx resolves and runs, and the sdist is
210
+ # what anybody packaging this downstream needs.
211
+ run: uv build --out-dir dist
212
+
213
+ - name: Check the distribution before anyone sees it
214
+ if: steps.bump.outputs.level != 'none'
215
+ # `--strict`, so a warning is a failure. This is the half of the upload's
216
+ # validation that can be run without an index, and the half that stopped
217
+ # the sibling's first release: a metadata version the pinned twine did not know.
218
+ # Running it here rather than inside the publish step means it fails
219
+ # before anything has been written anywhere.
220
+ #
221
+ # The other half, what an index will actually accept, is answered on every
222
+ # pull request by the rehearsal workflow.
223
+ run: uvx twine check --strict dist/*
224
+
225
+ - name: Commit the version and tag it
226
+ if: steps.bump.outputs.level != 'none'
227
+ # Before the upload, and this is deliberate: the upload is the only step
228
+ # here that cannot be undone, so it goes last, once everything that can be
229
+ # has already succeeded.
230
+ #
231
+ # If this step fails, nothing has been published and the version number is
232
+ # still free. If it ran after the upload instead, a push declined by the
233
+ # ruleset - the exact failure the bypass list exists to prevent - would
234
+ # leave PyPI holding a version this repository never recorded, and PyPI
235
+ # has no delete-and-reuse, only yank.
236
+ #
237
+ # One commit carrying the version, and the tag on that commit rather than
238
+ # on the merge: a tag whose tree holds the previous version is a release
239
+ # that installs as its predecessor, and nothing downstream can tell.
240
+ #
241
+ # **One push, carrying both refs.** As two pushes, federated-agent-kits shipped
242
+ # 0.13.0's commit and then had its tag rejected with "missing necessary objects":
243
+ # the second pack holds the tag alone, because the commit it points at
244
+ # went up a moment earlier, and a receiving replica that has not seen that
245
+ # commit yet reads the pack as incomplete. One push puts both in one pack
246
+ # and closes the window. `--atomic` then makes the pair all-or-nothing,
247
+ # which is what the concurrency group at the top of this file is for: a
248
+ # half-done release is a tag with no version behind it, or the reverse.
249
+ env:
250
+ NEXT: ${{ steps.version.outputs.next }}
251
+ BOT_NAME: ${{ steps.identity.outputs.name }}
252
+ BOT_EMAIL: ${{ steps.identity.outputs.email }}
253
+ run: |
254
+ set -euo pipefail
255
+ git config user.name "${BOT_NAME}"
256
+ git config user.email "${BOT_EMAIL}"
257
+ git add pyproject.toml
258
+ git commit -m "Release ${NEXT}"
259
+ git tag -a "v${NEXT}" -m "Release ${NEXT}"
260
+ git push --atomic origin HEAD:main "refs/tags/v${NEXT}"
261
+
262
+ - name: Publish to PyPI
263
+ if: steps.bump.outputs.level != 'none'
264
+ # Last, because it is the one thing that cannot be taken back. A failure
265
+ # here leaves a tag naming a version that is not on the index, which is a
266
+ # tag to delete and a number still free to reuse. That is the cheap half
267
+ # of the only trade available: there is no transaction across an index and
268
+ # a git remote, so something has to be the orphan, and a deletable tag is
269
+ # a better orphan than an unpublishable version.
270
+ #
271
+ # One index. A rehearsal that is allowed to fail protects nothing, and one
272
+ # that is not blocks releases on an index that expires its projects. The
273
+ # rehearsal belongs on pull requests, where it is free, and that is where
274
+ # it is.
275
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
276
+
277
+ - name: Announce it
278
+ if: steps.bump.outputs.level != 'none'
279
+ env:
280
+ GH_TOKEN: ${{ github.token }}
281
+ NEXT: ${{ steps.version.outputs.next }}
282
+ run: |-
283
+ set -euo pipefail
284
+ gh release create "v${NEXT}" \
285
+ --title "v${NEXT}" \
286
+ --notes "\`uvx logseq-md@${NEXT} --help\`" \
287
+ --generate-notes \
288
+ --verify-tag