Skip to content

Commit 0c287b2

Browse files
committed
chore: release v3.1.9
1 parent 74046df commit 0c287b2

22 files changed

+147
-1887
lines changed

.github/workflows/release.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@ jobs:
1717
with:
1818
python-version: '3.x'
1919

20+
- name: Install dependencies
21+
run: pip install pyyaml
22+
2023
- name: Build XPI
21-
run: cd fulltext-attach-plugin && python3 build.py
24+
run: python3 build.py
2225

2326
- name: Get version
2427
id: ver
25-
run: |
26-
echo "version=$(cd fulltext-attach-plugin && python3 -c 'from version import VERSION; print(VERSION)')" >> "$GITHUB_OUTPUT"
28+
run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"
2729

2830
- name: Create release
2931
uses: softprops/action-gh-release@v2
3032
with:
31-
files: fulltext-attach-plugin/fulltext-attach-plugin-${{ steps.ver.outputs.version }}.xpi
33+
files: local-write-api-${{ steps.ver.outputs.version }}.xpi
3234
generate_release_notes: true

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.serena/
2-
**/__pycache__/
2+
__pycache__/
33
*.pyc
4+
*.xpi
5+
.DS_Store

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ See [issue #1](https://github.com/dzackgarza/zotero-attachment-plugin/issues/1)
2020

2121
## What It Ships
2222

23-
The add-on lives in [`fulltext-attach-plugin`](./fulltext-attach-plugin) and registers three endpoints on Zotero's local HTTP server:
23+
The add-on lives in [`local-write-api`](./local-write-api) and registers three endpoints on Zotero's local HTTP server:
2424

2525
| Endpoint | Method | Purpose |
2626
|---|---|---|
@@ -38,10 +38,12 @@ The version probe lets consumers require a minimum installed add-on version befo
3838
## Repo Layout
3939

4040
```text
41-
fulltext-attach-plugin/ Add-on source, build scripts, manifest, and update manifest
42-
examples/ Example clients and requests
43-
scripts/ Utility scripts
44-
tests/ Add-on-specific verification assets
41+
src/ Plugin source (bootstrap.js, icons, generated manifest.json)
42+
build.py Builds the XPI from src/ and writes updates.json
43+
config.yml All stable constants — addon ID, repo, Zotero compatibility, endpoints
44+
VERSION Current version number (plain text, bumped by justfile)
45+
updates.json Committed; fetched by Zotero at the update_url for auto-update
46+
justfile Release workflow
4547
```
4648

4749
## Build and Release
@@ -52,12 +54,12 @@ just release-minor # bump minor version
5254
just release-major # bump major version
5355
```
5456

55-
GitHub Actions picks up the tag and publishes the GitHub Release with the `.xpi` asset. Zotero polls the `update_url` in the installed manifest and offers the update automatically.
57+
`VERSION` and `config.yml` are the two sources of truth. `build.py` derives everything else — `updates.json`, the XPI, and the injected constants in `bootstrap.js`.
5658

57-
[`version.py`](./fulltext-attach-plugin/version.py) is the single source of truth for the version number. Everything else is derived from it.
59+
GitHub Actions picks up the tag and publishes the GitHub Release with the `.xpi` asset. Zotero polls `update_url` in the installed manifest and offers the update automatically.
5860

59-
Install the generated `.xpi` from Zotero's `Tools -> Add-ons` menu, then verify:
61+
Install the `.xpi` from Zotero's `Tools Add-ons → Install Add-on From File`, then verify:
6062

61-
- `GET http://127.0.0.1:23119/opencode-zotero-plugin-version`
62-
- `POST http://127.0.0.1:23119/fulltext-attach`
63-
- `POST http://127.0.0.1:23119/opencode-zotero-write`
63+
- `GET http://127.0.0.1:23119/version`
64+
- `POST http://127.0.0.1:23119/attach`
65+
- `POST http://127.0.0.1:23119/write`

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.1.9
Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,66 @@
11
#!/usr/bin/env python3
22
from __future__ import annotations
33

4-
"""Build release artifacts for the Zotero attachment plugin."""
4+
"""Build release artifacts for the Zotero Local Write API plugin."""
55

66
import hashlib
77
import json
88
import re
99
import zipfile
1010
from pathlib import Path
1111

12-
from version import (
13-
ADDON_AUTHOR,
14-
ADDON_DESCRIPTION,
15-
ADDON_ID,
16-
ADDON_NAME,
17-
FULLTEXT_ATTACH_PATH,
18-
LOCAL_WRITE_PATH,
19-
REPO_URL,
20-
STRICT_MIN_VERSION,
21-
STRICT_MAX_VERSION,
22-
TESTED_ZOTERO_VERSION,
23-
UPDATE_MANIFEST_FILENAME,
24-
UPDATE_MANIFEST_URL,
25-
VERSION,
26-
VERSION_PATH,
27-
XPI_FILENAME,
28-
XPI_URL,
29-
)
30-
12+
import yaml
3113

3214
ROOT = Path(__file__).resolve().parent
33-
BOOTSTRAP_PATH = ROOT / "bootstrap.js"
34-
MANIFEST_PATH = ROOT / "manifest.json"
35-
UPDATES_PATH = ROOT / UPDATE_MANIFEST_FILENAME
15+
SRC = ROOT / "src"
16+
BOOTSTRAP_PATH = SRC / "bootstrap.js"
17+
ICONS_DIR = SRC / "icons"
18+
UPDATES_PATH = ROOT / "updates.json"
19+
20+
cfg = yaml.safe_load((ROOT / "config.yml").read_text())
21+
VERSION = (ROOT / "VERSION").read_text().strip()
22+
23+
ADDON_ID = cfg["addon"]["id"]
24+
ADDON_SLUG = cfg["addon"]["slug"]
25+
ADDON_NAME = cfg["addon"]["name"]
26+
ADDON_AUTHOR = cfg["addon"]["author"]
27+
ADDON_DESCRIPTION = cfg["addon"]["description"]
28+
29+
REPO_OWNER = cfg["repo"]["owner"]
30+
REPO_NAME = cfg["repo"]["name"]
31+
REPO_BRANCH = cfg["repo"]["branch"]
32+
REPO_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}"
33+
34+
STRICT_MIN_VERSION = cfg["zotero"]["strict_min_version"]
35+
STRICT_MAX_VERSION = cfg["zotero"]["strict_max_version"]
36+
TESTED_ZOTERO_VERSION = cfg["zotero"]["tested_version"]
37+
38+
ATTACH_PATH = cfg["endpoints"]["attach"]
39+
WRITE_PATH = cfg["endpoints"]["write"]
40+
VERSION_PATH = cfg["endpoints"]["version"]
41+
42+
UPDATE_MANIFEST_URL = (
43+
f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{REPO_BRANCH}/updates.json"
44+
)
45+
XPI_FILENAME = f"{ADDON_SLUG}-{VERSION}.xpi"
46+
XPI_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/download/v{VERSION}/{XPI_FILENAME}"
47+
3648
BOOTSTRAP_VAR_PATTERNS = {
37-
"PLUGIN_VERSION": re.compile(r'var PLUGIN_VERSION = .*?;'),
38-
"FULLTEXT_ATTACH_PATH": re.compile(r'var FULLTEXT_ATTACH_PATH = .*?;'),
39-
"LOCAL_WRITE_PATH": re.compile(r'var LOCAL_WRITE_PATH = .*?;'),
40-
"VERSION_PATH": re.compile(r'var VERSION_PATH = .*?;'),
41-
"ADDON_ID": re.compile(r'var ADDON_ID = .*?;'),
42-
"HOMEPAGE_URL": re.compile(r'var HOMEPAGE_URL = .*?;'),
43-
"UPDATE_URL": re.compile(r'var UPDATE_URL = .*?;'),
44-
"STRICT_MIN_VERSION": re.compile(r'var STRICT_MIN_VERSION = .*?;'),
45-
"STRICT_MAX_VERSION": re.compile(r'var STRICT_MAX_VERSION = .*?;'),
46-
"TESTED_ZOTERO_VERSION": re.compile(r'var TESTED_ZOTERO_VERSION = .*?;'),
49+
"PLUGIN_VERSION": re.compile(r"var PLUGIN_VERSION = .*?;"),
50+
"FULLTEXT_ATTACH_PATH": re.compile(r"var FULLTEXT_ATTACH_PATH = .*?;"),
51+
"LOCAL_WRITE_PATH": re.compile(r"var LOCAL_WRITE_PATH = .*?;"),
52+
"VERSION_PATH": re.compile(r"var VERSION_PATH = .*?;"),
53+
"ADDON_ID": re.compile(r"var ADDON_ID = .*?;"),
54+
"HOMEPAGE_URL": re.compile(r"var HOMEPAGE_URL = .*?;"),
55+
"UPDATE_URL": re.compile(r"var UPDATE_URL = .*?;"),
56+
"STRICT_MIN_VERSION": re.compile(r"var STRICT_MIN_VERSION = .*?;"),
57+
"STRICT_MAX_VERSION": re.compile(r"var STRICT_MAX_VERSION = .*?;"),
58+
"TESTED_ZOTERO_VERSION": re.compile(r"var TESTED_ZOTERO_VERSION = .*?;"),
4759
}
4860
BOOTSTRAP_VAR_VALUES = {
4961
"PLUGIN_VERSION": VERSION,
50-
"FULLTEXT_ATTACH_PATH": FULLTEXT_ATTACH_PATH,
51-
"LOCAL_WRITE_PATH": LOCAL_WRITE_PATH,
62+
"FULLTEXT_ATTACH_PATH": ATTACH_PATH,
63+
"LOCAL_WRITE_PATH": WRITE_PATH,
5264
"VERSION_PATH": VERSION_PATH,
5365
"ADDON_ID": ADDON_ID,
5466
"HOMEPAGE_URL": REPO_URL,
@@ -59,6 +71,23 @@
5971
}
6072

6173

74+
def write_json(path: Path, payload: dict[str, object]) -> None:
75+
path.write_text(f"{json.dumps(payload, indent=2, sort_keys=True)}\n")
76+
77+
78+
def update_bootstrap_metadata() -> None:
79+
source = BOOTSTRAP_PATH.read_text()
80+
for var, pattern in BOOTSTRAP_VAR_PATTERNS.items():
81+
source, n = pattern.subn(
82+
f"var {var} = {json.dumps(BOOTSTRAP_VAR_VALUES[var])};",
83+
source,
84+
count=1,
85+
)
86+
if n != 1:
87+
raise RuntimeError(f"Could not update {var} in bootstrap.js")
88+
BOOTSTRAP_PATH.write_text(source)
89+
90+
6291
def build_manifest() -> dict[str, object]:
6392
return {
6493
"manifest_version": 2,
@@ -79,29 +108,6 @@ def build_manifest() -> dict[str, object]:
79108
}
80109

81110

82-
def write_json(path: Path, payload: dict[str, object]) -> None:
83-
path.write_text(f"{json.dumps(payload, indent=2, sort_keys=True)}\n")
84-
85-
86-
def update_bootstrap_metadata() -> None:
87-
bootstrap_source = BOOTSTRAP_PATH.read_text()
88-
updated_source = bootstrap_source
89-
for variable_name, pattern in BOOTSTRAP_VAR_PATTERNS.items():
90-
updated_source, replacements = pattern.subn(
91-
f"var {variable_name} = {json.dumps(BOOTSTRAP_VAR_VALUES[variable_name])};",
92-
updated_source,
93-
count=1,
94-
)
95-
if replacements != 1:
96-
raise RuntimeError(f"Could not update {variable_name} in bootstrap.js")
97-
BOOTSTRAP_PATH.write_text(updated_source)
98-
99-
100-
def remove_old_xpis() -> None:
101-
for old_xpi in ROOT.glob("*.xpi"):
102-
old_xpi.unlink()
103-
104-
105111
_EPOCH = (2020, 1, 1, 0, 0, 0) # fixed timestamp for deterministic builds
106112

107113

@@ -113,13 +119,13 @@ def _zip_entry(arcname: str) -> zipfile.ZipInfo:
113119

114120

115121
def build_xpi() -> Path:
122+
manifest_path = SRC / "manifest.json"
116123
xpi_path = ROOT / XPI_FILENAME
117-
icons_dir = ROOT / "icons"
118124
with zipfile.ZipFile(xpi_path, "w", zipfile.ZIP_DEFLATED) as xpi:
119-
for path, arcname in [(MANIFEST_PATH, MANIFEST_PATH.name), (BOOTSTRAP_PATH, BOOTSTRAP_PATH.name)]:
120-
xpi.writestr(_zip_entry(arcname), path.read_bytes())
121-
if icons_dir.is_dir():
122-
for icon in sorted(icons_dir.iterdir()):
125+
xpi.writestr(_zip_entry("manifest.json"), manifest_path.read_bytes())
126+
xpi.writestr(_zip_entry("bootstrap.js"), BOOTSTRAP_PATH.read_bytes())
127+
if ICONS_DIR.is_dir():
128+
for icon in sorted(ICONS_DIR.iterdir()):
123129
if icon.is_file():
124130
xpi.writestr(_zip_entry(f"icons/{icon.name}"), icon.read_bytes())
125131
return xpi_path
@@ -151,22 +157,27 @@ def build_updates_manifest(xpi_hash: str) -> dict[str, object]:
151157
}
152158

153159

160+
def remove_old_xpis() -> None:
161+
for old_xpi in ROOT.glob("*.xpi"):
162+
old_xpi.unlink()
163+
164+
154165
def build() -> Path:
155166
print(f"Building {ADDON_NAME} v{VERSION}")
156-
print(f"Zotero compatibility: {STRICT_MIN_VERSION} - {STRICT_MAX_VERSION}")
157-
print(f"Tested target for release gating: Zotero {TESTED_ZOTERO_VERSION}")
167+
print(f"Zotero compatibility: {STRICT_MIN_VERSION} {STRICT_MAX_VERSION}")
168+
print(f"Tested target: Zotero {TESTED_ZOTERO_VERSION}")
158169

159170
update_bootstrap_metadata()
160-
write_json(MANIFEST_PATH, build_manifest())
171+
manifest = build_manifest()
172+
write_json(SRC / "manifest.json", manifest)
161173
remove_old_xpis()
162174
xpi_path = build_xpi()
163175
write_json(UPDATES_PATH, build_updates_manifest(sha256sum(xpi_path)))
164176

165-
print(f"Wrote {MANIFEST_PATH.name}")
166-
print(f"Wrote {UPDATES_PATH.name}")
177+
print(f"Wrote updates.json")
167178
print(f"Built {xpi_path.name}")
168-
print(f"Published update manifest URL: {UPDATE_MANIFEST_URL}")
169-
print(f"Published XPI URL: {XPI_URL}")
179+
print(f"Update manifest URL: {UPDATE_MANIFEST_URL}")
180+
print(f"XPI URL: {XPI_URL}")
170181
return xpi_path
171182

172183

config.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
addon:
2+
id: local-write-api@dzackgarza.com
3+
slug: local-write-api
4+
name: Local Write API
5+
author: D. Zack Garza
6+
description: >-
7+
Exposes write endpoints on Zotero's local HTTP server, covering the gap
8+
left by the read-only built-in API.
9+
10+
repo:
11+
owner: dzackgarza
12+
name: zotero-local-write-api
13+
branch: main
14+
15+
zotero:
16+
strict_min_version: "7.0"
17+
strict_max_version: "*"
18+
tested_version: "8.0.1"
19+
20+
endpoints:
21+
attach: /attach
22+
write: /write
23+
version: /version

0 commit comments

Comments
 (0)