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.
- logseq_md-0.1.0/.biome.json +24 -0
- logseq_md-0.1.0/.checkmake.ini +4 -0
- logseq_md-0.1.0/.editorconfig +30 -0
- logseq_md-0.1.0/.gitattributes +46 -0
- logseq_md-0.1.0/.github/dependabot.yml +28 -0
- logseq_md-0.1.0/.github/workflows/governance.yml +59 -0
- logseq_md-0.1.0/.github/workflows/rehearsal.yml +124 -0
- logseq_md-0.1.0/.github/workflows/release.yml +288 -0
- logseq_md-0.1.0/.github/workflows/tests.yml +145 -0
- logseq_md-0.1.0/.gitignore +37 -0
- logseq_md-0.1.0/.linkspector.yml +24 -0
- logseq_md-0.1.0/.mailmap +11 -0
- logseq_md-0.1.0/.markdownlint-cli2.jsonc +11 -0
- logseq_md-0.1.0/.pre-commit-config.yaml +195 -0
- logseq_md-0.1.0/.pymarkdown.json +18 -0
- logseq_md-0.1.0/.ruff.toml +20 -0
- logseq_md-0.1.0/.scripts/check_markdown_style.py +78 -0
- logseq_md-0.1.0/.scripts/check_no_symlinks.py +61 -0
- logseq_md-0.1.0/.scripts/check_uvx_invocation.py +103 -0
- logseq_md-0.1.0/.scripts/verify-published.py +118 -0
- logseq_md-0.1.0/.scripts/version.py +166 -0
- logseq_md-0.1.0/.taplo.toml +11 -0
- logseq_md-0.1.0/.yamlfmt +14 -0
- logseq_md-0.1.0/CONTRIBUTING.md +41 -0
- logseq_md-0.1.0/DESIGN.md +882 -0
- logseq_md-0.1.0/IMPLEMENTATION.md +607 -0
- logseq_md-0.1.0/LICENSE +21 -0
- logseq_md-0.1.0/Makefile +35 -0
- logseq_md-0.1.0/PKG-INFO +85 -0
- logseq_md-0.1.0/README.md +60 -0
- logseq_md-0.1.0/pyproject.toml +60 -0
- logseq_md-0.1.0/src/logseq_md/__init__.py +21 -0
- logseq_md-0.1.0/src/logseq_md/__main__.py +8 -0
- logseq_md-0.1.0/src/logseq_md/cli.py +51 -0
- logseq_md-0.1.0/tests/test_cli.py +57 -0
- 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,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
|