Simplify device settings help #108
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check and release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - dev | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: koreader-remote-${{ github.event_name == 'workflow_dispatch' && 'release' || github.ref }} | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} | |
| jobs: | |
| check-and-release: | |
| name: ${{ github.event_name == 'workflow_dispatch' && github.ref_name == 'main' && 'Validate and publish' || 'Validate' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Lua 5.1 | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install --yes --no-install-recommends -qq lua5.1 | |
| - name: Prepare build metadata | |
| shell: bash | |
| env: | |
| VERSION: ${{ github.workspace }}/VERSION | |
| run: | | |
| set -euo pipefail | |
| VERSION_VALUE="$(tr -d '[:space:]' < "${VERSION}")" | |
| if [[ "${GITHUB_REF_NAME}" == "dev" ]]; then | |
| CHANNEL="dev" | |
| SOURCE="dev" | |
| BUILD_ID="dev.${GITHUB_RUN_NUMBER}" | |
| RELEASE_VERSION="${VERSION_VALUE}-dev.${GITHUB_RUN_NUMBER}" | |
| else | |
| CHANNEL="stable" | |
| SOURCE="main" | |
| BUILD_ID="stable" | |
| RELEASE_VERSION="${VERSION_VALUE}" | |
| fi | |
| python3 - "${CHANNEL}" "${SOURCE}" "${VERSION_VALUE}" "${RELEASE_VERSION}" "${BUILD_ID}" "${GITHUB_SHA}" <<'PY' | |
| from pathlib import Path | |
| import sys | |
| channel, source, version, release_version, build_id, commit = sys.argv[1:] | |
| Path("koreaderremote.koplugin/build.lua").write_text( | |
| "return {\n" | |
| f' channel = "{channel}",\n' | |
| f' source = "{source}",\n' | |
| f' version = "{version}",\n' | |
| f' release_version = "{release_version}",\n' | |
| f' build_id = "{build_id}",\n' | |
| f' commit = "{commit}",\n' | |
| "}\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| - name: Validate repository | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="$(tr -d '[:space:]' < VERSION)" | |
| PLUGIN="koreaderremote.koplugin" | |
| MAIN="${PLUGIN}/main.lua" | |
| CONTROLS="${PLUGIN}/devicecontrols.lua" | |
| HTTP="${PLUGIN}/http.lua" | |
| INTERACTION="${PLUGIN}/interaction.lua" | |
| MENU="${PLUGIN}/menu.lua" | |
| NETWORK="${PLUGIN}/network.lua" | |
| UPDATER="${PLUGIN}/updater.lua" | |
| BUILD="${PLUGIN}/build.lua" | |
| INDEX="${PLUGIN}/web/index.html" | |
| LUAC="$(command -v luac5.1 || command -v luac)" | |
| test -n "${VERSION}" | |
| printf '%s\n' "${VERSION}" \ | |
| | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$' | |
| test -f LICENSE | |
| test -f README.md | |
| test -f CHANGELOG.md | |
| test -f VERSION | |
| test -f "${PLUGIN}/_meta.lua" | |
| test -f "${MAIN}" | |
| test -f "${CONTROLS}" | |
| test -f "${HTTP}" | |
| test -f "${INTERACTION}" | |
| test -f "${MENU}" | |
| test -f "${NETWORK}" | |
| test -f "${UPDATER}" | |
| test -f "${BUILD}" | |
| test -f "${INDEX}" | |
| test ! -e dev-updates | |
| python3 - "${VERSION}" "${GITHUB_REF_NAME}" <<'PY' | |
| from pathlib import Path | |
| import re | |
| import sys | |
| version, branch = sys.argv[1:] | |
| data = Path("koreaderremote.koplugin/build.lua").read_text() | |
| fields = dict(re.findall(r'^\s*(\w+) = "([^"]+)"', data, re.M)) | |
| expected_channel = "dev" if branch == "dev" else "stable" | |
| expected_source = "dev" if branch == "dev" else "main" | |
| if fields.get("channel") != expected_channel: | |
| raise SystemExit("Build channel does not match the branch.") | |
| if fields.get("source") != expected_source: | |
| raise SystemExit("Build source does not match the branch.") | |
| if fields.get("version") != version: | |
| raise SystemExit("Build version does not match VERSION.") | |
| if fields.get("release_version", "").split("-", 1)[0] != version: | |
| raise SystemExit("Build release version does not match VERSION.") | |
| if not fields.get("commit"): | |
| raise SystemExit("Build commit identity is missing.") | |
| PY | |
| test ! -e RELEASE_NOTES.md | |
| test ! -e scripts | |
| test ! -e docs | |
| "${LUAC}" -p "${PLUGIN}/_meta.lua" | |
| "${LUAC}" -p "${MAIN}" | |
| "${LUAC}" -p "${CONTROLS}" | |
| "${LUAC}" -p "${HTTP}" | |
| "${LUAC}" -p "${INTERACTION}" | |
| "${LUAC}" -p "${MENU}" | |
| "${LUAC}" -p "${NETWORK}" | |
| "${LUAC}" -p "${UPDATER}" | |
| "${LUAC}" -p "${BUILD}" | |
| grep -Fq "local VERSION = \"${VERSION}\"" "${MAIN}" | |
| grep -Fq 'repository = "helitra/koreader-remote"' "${MAIN}" | |
| grep -Fq "## v${VERSION}" CHANGELOG.md | |
| grep -Fq 'sorting_hint = "tools"' "${MENU}" | |
| grep -Fq '/api/ping' "${HTTP}" | |
| grep -Fq '/api/next' "${HTTP}" | |
| grep -Fq '/api/previous' "${HTTP}" | |
| grep -Fq '/api/v1/capabilities' "${HTTP}" | |
| grep -Fq '/api/v1/device-state' "${HTTP}" | |
| grep -Fq '/api/v1/idle-stop' "${HTTP}" | |
| grep -Fq '/api/v1/brightness' "${HTTP}" | |
| grep -Fq '/api/v1/warmth' "${HTTP}" | |
| grep -Fq '/api/v1/night-mode' "${HTTP}" | |
| grep -Fq '/api/v1/full-refresh' "${HTTP}" | |
| grep -Fq '/api/v1/note-session' "${HTTP}" | |
| grep -Fq '/api/v1/note-session/push' "${HTTP}" | |
| grep -Fq '/api/v1/note-session/save' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks/open' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks/return' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks/edit-note' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks/delete-note' "${HTTP}" | |
| grep -Fq '/api/v1/bookmarks/delete' "${HTTP}" | |
| grep -Fq '/api/v1/footnote/open' "${HTTP}" | |
| grep -Fq '<html lang="en">' "${INDEX}" | |
| grep -Fq '<details id="device-controls" hidden>' "${INDEX}" | |
| grep -Fq '<summary>Device settings</summary>' "${INDEX}" | |
| grep -Fq 'id="idle-stop-select"' "${INDEX}" | |
| grep -Fq '/api/v1/idle-stop?minutes=' "${INDEX}" | |
| grep -Fq 'aria-label="Previous page"' "${INDEX}" | |
| grep -Fq 'aria-label="Next page"' "${INDEX}" | |
| grep -Fq 'data-control="brightness" data-value="1"' "${INDEX}" | |
| grep -Fq 'data-control="brightness" data-value="100"' "${INDEX}" | |
| grep -Fq 'data-control="warmth" data-value="0"' "${INDEX}" | |
| grep -Fq 'data-control="warmth" data-value="100"' "${INDEX}" | |
| grep -Fq 'const HOLD_DELAY_MS = 500;' "${INDEX}" | |
| grep -Fq '{ until: Infinity, interval: 230 }' "${INDEX}" | |
| grep -Fq 'function controlIsBusy(name)' "${INDEX}" | |
| grep -Fq 'applyDeviceState(payload.state, name)' "${INDEX}" | |
| grep -Fq 'self.requested_brightness = percent' "${CONTROLS}" | |
| grep -Fq 'state.brightness_effective = effective_brightness' "${CONTROLS}" | |
| grep -Fq 'local Updater = dofile(PLUGIN_DIR .. "/updater.lua")' "${MAIN}" | |
| grep -Fq 'text = _("Check for updates")' "${MENU}" | |
| grep -Fq 'getInstalledBuildLabel' "${MENU}" | |
| grep -Fq 'Update channel: %s' "${MENU}" | |
| grep -Fq 'loadfile(PLUGIN_DIR .. "/build.lua")' "${MAIN}" | |
| grep -Fq 'api.github.com/repos/helitra/koreader-remote/releases/latest' "${UPDATER}" | |
| grep -Fq 'api.github.com/repos/helitra/koreader-remote/commits/dev' "${UPDATER}" | |
| grep -Fq 'codeload.github.com/helitra/koreader-remote/zip/' "${UPDATER}" | |
| grep -Fq 'The update build metadata does not match its release.' "${UPDATER}" | |
| grep -Fq 'candidate.commit = metadata.commit' "${UPDATER}" | |
| grep -Fq 'instance.backup_dir = instance.plugin_dir .. ".previous"' "${UPDATER}" | |
| grep -Fq 'local Archiver = require("ffi/archiver")' "${UPDATER}" | |
| grep -Fq 'local sha256 = require("ffi/sha2").sha256' "${UPDATER}" | |
| grep -Fq 'UIManager:askForRestart' "${UPDATER}" | |
| grep -Fq 'NetworkMgr:runWhenOnline' "${UPDATER}" | |
| grep -Fq 'function Updater:isRestartRequired()' "${UPDATER}" | |
| grep -Fq 'currentProcessId()' "${UPDATER}" | |
| grep -Fq 'runtime.update_restart_required' "${MAIN}" | |
| grep -Fq 'local Interaction = dofile(PLUGIN_DIR .. "/interaction.lua")' "${MAIN}" | |
| grep -Fq 'local Network = dofile(PLUGIN_DIR .. "/network.lua")' "${MAIN}" | |
| grep -Fq 'local HTTP = dofile(PLUGIN_DIR .. "/http.lua")' "${MAIN}" | |
| grep -Fq 'local Menu = dofile(PLUGIN_DIR .. "/menu.lua")' "${MAIN}" | |
| grep -Fq 'Write note on phone' "${INTERACTION}" | |
| grep -Fq 'Edit note on phone' "${INTERACTION}" | |
| grep -Fq 'function Interaction:getBookmarks()' "${INTERACTION}" | |
| grep -Fq 'function Interaction:openBookmark(id)' "${INTERACTION}" | |
| grep -Fq 'function Interaction:returnToReadingPosition()' "${INTERACTION}" | |
| grep -Fq 'function Interaction:editBookmarkNote(id)' "${INTERACTION}" | |
| grep -Fq 'function Interaction:deleteBookmarkNote(id)' "${INTERACTION}" | |
| grep -Fq 'function Interaction:deleteBookmark(id)' "${INTERACTION}" | |
| grep -Fq 'nb_highlights_added = 1' "${INTERACTION}" | |
| grep -Fq 'nb_notes_added = -1' "${INTERACTION}" | |
| grep -Fq 'instance.bookmark_return = nil' "${INTERACTION}" | |
| grep -Fq 'MAX_BOOKMARK_ITEMS = 300' "${INTERACTION}" | |
| grep -Fq 'ui.bookmark.gotoBookmark' "${INTERACTION}" | |
| grep -Fq 'ui.link.addCurrentLocationToStack' "${INTERACTION}" | |
| grep -Fq 'function Interaction:openNextFootnote()' "${INTERACTION}" | |
| grep -Fq 'showAsFootnotePopup' "${INTERACTION}" | |
| grep -Fq 'MAX_NOTE_BYTES = 12 * 1024' "${INTERACTION}" | |
| grep -Fq 'function Interaction:startNewNoteSession(highlight)' "${INTERACTION}" | |
| grep -Fq 'type(highlight.showHighlightPrompt) == "function"' "${INTERACTION}" | |
| grep -Fq 'highlight:saveHighlight(true)' "${INTERACTION}" | |
| grep -Fq 'xpcall(callback, debug.traceback)' "${INTERACTION}" | |
| grep -Fq 'local InputDialog = require("ui/widget/inputdialog")' "${INTERACTION}" | |
| grep -Fq 'function Interaction:openNoteDialog(session)' "${INTERACTION}" | |
| grep -Fq 'dialog.setInputText' "${INTERACTION}" | |
| grep -Fq 'function Interaction:saveNoteSession(' "${INTERACTION}" | |
| grep -Fq 'if encoded == "" then' "${INTERACTION}" | |
| grep -Fq 'padding ~= "=" and padding ~= "=="' "${INTERACTION}" | |
| grep -Fq 'local expected_bytes = (#encoded / 4) * 3 - padding_count' "${INTERACTION}" | |
| if grep -Fq '={0,2}' "${INTERACTION}"; then | |
| echo "Unsupported regex-style Base64 quantifier found." | |
| exit 1 | |
| fi | |
| grep -Fq 'id="bookmark-button"' "${INDEX}" | |
| grep -Fq 'id="bookmark-view"' "${INDEX}" | |
| grep -Fq 'data-bookmark-filter="note"' "${INDEX}" | |
| grep -Fq 'id="bookmark-return"' "${INDEX}" | |
| grep -Fq 'id="bookmark-search"' "${INDEX}" | |
| grep -Fq 'id="bookmark-sort"' "${INDEX}" | |
| grep -Fq '/api/v1/bookmarks/open?id=' "${INDEX}" | |
| grep -Fq '/api/v1/bookmarks/return' "${INDEX}" | |
| grep -Fq '/api/v1/bookmarks/edit-note?id=' "${INDEX}" | |
| grep -Fq '/api/v1/bookmarks/delete-note?id=' "${INDEX}" | |
| grep -Fq '/api/v1/bookmarks/delete?id=' "${INDEX}" | |
| grep -Fq '.bookmark-excerpt {' "${INDEX}" | |
| grep -Fq 'font-style: italic;' "${INDEX}" | |
| grep -Fq '.bookmark-note {' "${INDEX}" | |
| grep -Fq 'font-style: normal;' "${INDEX}" | |
| grep -Fq 'className = "bookmark-menu"' "${INDEX}" | |
| grep -Fq 'className = "bookmark-menu-panel"' "${INDEX}" | |
| grep -Fq 'className = "bookmark-menu-dot"' "${INDEX}" | |
| grep -Fq 'index < 3' "${INDEX}" | |
| grep -Fq '#device-controls > summary {' "${INDEX}" | |
| grep -Fq '.bookmark-menu > summary::before' "${INDEX}" | |
| grep -Fq 'content: none !important' "${INDEX}" | |
| if grep -Fq 'content: "⚙"' "${INDEX}"; then | |
| echo "Gear icon must not be present in the web UI." | |
| exit 1 | |
| fi | |
| if grep -Eq '^[[:space:]]*summary(::|[[:space:]]*\{)' "${INDEX}"; then | |
| echo "Unscoped summary CSS can leak into the annotation menu." | |
| exit 1 | |
| fi | |
| grep -Fq 'aria-expanded' "${INDEX}" | |
| grep -Fq 'background: transparent' "${INDEX}" | |
| grep -Fq 'border: 0' "${INDEX}" | |
| grep -Fq '"Edit Note"' "${INDEX}" | |
| grep -Fq '"Delete Note"' "${INDEX}" | |
| grep -Fq '"Delete Bookmark"' "${INDEX}" | |
| grep -Fq 'The bookmark and highlighted text will remain.' "${INDEX}" | |
| grep -Fq 'position: static' "${INDEX}" | |
| grep -Fq 'closeBookmarkMenus' "${INDEX}" | |
| if grep -Fq 'summary.textContent' "${INDEX}"; then | |
| echo "The overflow trigger must use CSS dots, not text." | |
| exit 1 | |
| fi | |
| if grep -Fq 'summary.textContent = "⋯"' "${INDEX}"; then | |
| echo "Emoji-like ellipsis glyph must not be used." | |
| exit 1 | |
| fi | |
| grep -Fq 'Open on Kindle' "${INDEX}" | |
| if grep -Fq 'bookmark-card-actions' "${INDEX}"; then | |
| echo "Large bookmark action rows must not be present." | |
| exit 1 | |
| fi | |
| grep -Fq 'id="note-button"' "${INDEX}" | |
| grep -Fq 'id="footnote-button"' "${INDEX}" | |
| grep -Fq 'id="oled-button"' "${INDEX}" | |
| grep -Fq 'id="note-view"' "${INDEX}" | |
| grep -Fq 'Pull from KOReader' "${INDEX}" | |
| grep -Fq 'Push to KOReader' "${INDEX}" | |
| grep -Fq 'Save note' "${INDEX}" | |
| grep -Fq '/api/v1/note-session/save' "${INDEX}" | |
| grep -Fq ':root[data-oled="true"]' "${INDEX}" | |
| if grep -Fq 'const OLED_IDLE_MS = 30000;' "${INDEX}"; then | |
| echo "OLED idle dimming must remain removed." | |
| exit 1 | |
| fi | |
| grep -Fq 'X-KOReader-Note-Base64' "${INDEX}" | |
| if grep -Fq 'type="range"' "${INDEX}"; then | |
| echo "The website must not contain sliders." | |
| exit 1 | |
| fi | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| import re | |
| files = [ | |
| Path("koreaderremote.koplugin/main.lua"), | |
| Path("koreaderremote.koplugin/build.lua"), | |
| Path("koreaderremote.koplugin/devicecontrols.lua"), | |
| Path("koreaderremote.koplugin/http.lua"), | |
| Path("koreaderremote.koplugin/interaction.lua"), | |
| Path("koreaderremote.koplugin/menu.lua"), | |
| Path("koreaderremote.koplugin/network.lua"), | |
| Path("koreaderremote.koplugin/updater.lua"), | |
| Path("koreaderremote.koplugin/web/index.html"), | |
| Path("README.md"), | |
| Path("CHANGELOG.md"), | |
| ] | |
| for path in files: | |
| data = path.read_bytes() | |
| if b"\r\n" in data: | |
| raise SystemExit(f"Windows line endings found in {path}") | |
| if b"\x00" in data: | |
| raise SystemExit(f"NUL byte found in {path}") | |
| index = Path("koreaderremote.koplugin/web/index.html").read_text( | |
| encoding="utf-8" | |
| ) | |
| forbidden = [ | |
| "Gerätesteuerung", | |
| "Vorherige Seite", | |
| "Nächste Seite", | |
| "Frontlicht", | |
| "Helligkeit", | |
| "Wärme", | |
| "Nachtmodus", | |
| "Vollständige Bildschirmaktualisierung", | |
| "Verbinde", | |
| "Verbunden", | |
| "Buch auf dem Reader öffnen", | |
| ] | |
| found = [text for text in forbidden if text in index] | |
| if found: | |
| raise SystemExit( | |
| "German website text found: " + ", ".join(found) | |
| ) | |
| settings_position = index.find('id="device-controls"') | |
| remote_position = index.find('id="remote"') | |
| if settings_position < 0 or remote_position < 0: | |
| raise SystemExit("Remote layout elements are missing.") | |
| if settings_position > remote_position: | |
| raise SystemExit("Device settings must appear above page controls.") | |
| required_presets = { | |
| "brightness": ["1", "10", "25", "50", "75", "100"], | |
| "warmth": ["0", "25", "50", "75", "100"], | |
| } | |
| for control, values in required_presets.items(): | |
| for value in values: | |
| marker = ( | |
| f'data-control="{control}" data-value="{value}"' | |
| ) | |
| if marker not in index: | |
| raise SystemExit( | |
| f"Missing {control} preset {value}%." | |
| ) | |
| if 'type="range"' in index: | |
| raise SystemExit("Sliders are not allowed.") | |
| if '<details id="device-controls" open' in index: | |
| raise SystemExit( | |
| "Device settings must start collapsed." | |
| ) | |
| if "const HOLD_DELAY_MS = 500;" not in index: | |
| raise SystemExit("The hold delay must remain 500 ms.") | |
| if "{ until: Infinity, interval: 230 }" not in index: | |
| raise SystemExit( | |
| "The capped human repeat interval is missing." | |
| ) | |
| if "function controlIsBusy(name)" not in index: | |
| raise SystemExit( | |
| "Control request race protection is missing." | |
| ) | |
| if "applyDeviceState(payload.state, name)" not in index: | |
| raise SystemExit( | |
| "Confirmed control responses are not handled." | |
| ) | |
| required_v080 = [ | |
| 'id="bookmark-button"', | |
| 'id="bookmark-view"', | |
| 'data-bookmark-filter="note"', | |
| 'id="bookmark-return"', | |
| 'id="bookmark-search"', | |
| 'id="bookmark-sort"', | |
| '/api/v1/bookmarks/open?id=', | |
| '/api/v1/bookmarks/return', | |
| '/api/v1/bookmarks/edit-note?id=', | |
| '/api/v1/bookmarks/delete-note?id=', | |
| '/api/v1/bookmarks/delete?id=', | |
| '.bookmark-excerpt {', | |
| 'font-style: italic;', | |
| '.bookmark-note {', | |
| 'font-style: normal;', | |
| 'className = "bookmark-menu"', | |
| 'className = "bookmark-menu-panel"', | |
| 'className = "bookmark-menu-dot"', | |
| 'index < 3', | |
| '#device-controls > summary {', | |
| '.bookmark-menu > summary::before', | |
| 'content: none !important', | |
| 'aria-expanded', | |
| 'background: transparent', | |
| 'border: 0', | |
| '"Edit Note"', | |
| '"Delete Note"', | |
| '"Delete Bookmark"', | |
| 'The bookmark and highlighted text will remain.', | |
| 'closeBookmarkMenus', | |
| 'Open on Kindle', | |
| 'id="note-button"', | |
| 'id="footnote-button"', | |
| 'id="oled-button"', | |
| 'id="note-view"', | |
| 'Pull from KOReader', | |
| 'Push to KOReader', | |
| 'Save note', | |
| '/api/v1/note-session/save', | |
| ':root[data-oled="true"]', | |
| 'X-KOReader-Note-Base64', | |
| ] | |
| missing_v080 = [ | |
| marker for marker in required_v080 if marker not in index | |
| ] | |
| if missing_v080: | |
| raise SystemExit( | |
| "Missing v0.8.0 UI markers: " | |
| + ", ".join(missing_v080) | |
| ) | |
| excerpt_match = re.search( | |
| r"\.bookmark-excerpt\s*\{(.*?)\n\s*\}", | |
| index, | |
| re.DOTALL, | |
| ) | |
| if not excerpt_match: | |
| raise SystemExit("Bookmark excerpt CSS block is missing.") | |
| if "font-style: italic;" not in excerpt_match.group(1): | |
| raise SystemExit( | |
| "Highlighted bookmark excerpts must be italic." | |
| ) | |
| note_match = re.search( | |
| r"\.bookmark-note\s*\{(.*?)\n\s*\}", | |
| index, | |
| re.DOTALL, | |
| ) | |
| if not note_match: | |
| raise SystemExit("Bookmark note CSS block is missing.") | |
| if "font-style: normal;" not in note_match.group(1): | |
| raise SystemExit( | |
| "Attached note text must remain upright." | |
| ) | |
| if "bookmark-card-actions" in index: | |
| raise SystemExit( | |
| "Large bookmark action rows are not allowed." | |
| ) | |
| if 'content: "⚙"' in index: | |
| raise SystemExit( | |
| "The gear icon must not be present in the web UI." | |
| ) | |
| if re.search(r"^\s*summary(?:::|\s*\{)", index, re.MULTILINE): | |
| raise SystemExit( | |
| "Unscoped summary CSS can leak into annotation menus." | |
| ) | |
| if "summary.textContent" in index: | |
| raise SystemExit( | |
| "The menu trigger must use three CSS dots, not text." | |
| ) | |
| if 'summary.textContent = "⋯"' in index: | |
| raise SystemExit( | |
| "The menu must not use an ellipsis glyph." | |
| ) | |
| PY | |
| - name: Build release files and notes | |
| if: github.event_name == 'workflow_dispatch' && github.ref_name == 'main' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_REF_NAME}" != "main" && "${GITHUB_REF_NAME}" != "dev" ]]; then | |
| echo "Run the release workflow from main or dev." | |
| exit 1 | |
| fi | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| import hashlib | |
| import zipfile | |
| version = Path("VERSION").read_text(encoding="utf-8").strip() | |
| plugin = Path("koreaderremote.koplugin") | |
| dist = Path("dist") | |
| dist.mkdir(exist_ok=True) | |
| metadata = {} | |
| for line in Path("koreaderremote.koplugin/build.lua").read_text().splitlines(): | |
| if " = \"" in line: | |
| key, value = line.strip().split(" = ", 1) | |
| metadata[key] = value.strip().strip(",").strip('"') | |
| release_version = metadata["release_version"] | |
| archive = dist / f"koreaderremote-v{release_version}.zip" | |
| with zipfile.ZipFile( | |
| archive, | |
| "w", | |
| compression=zipfile.ZIP_DEFLATED, | |
| compresslevel=9, | |
| ) as output: | |
| for path in sorted(plugin.rglob("*")): | |
| if path.is_file(): | |
| output.write(path, path.as_posix()) | |
| with zipfile.ZipFile(archive, "r") as packaged: | |
| names = packaged.namelist() | |
| if not names: | |
| raise SystemExit("Release archive is empty.") | |
| invalid = [ | |
| name for name in names | |
| if not name.startswith("koreaderremote.koplugin/") | |
| ] | |
| if invalid: | |
| raise SystemExit( | |
| "Release archive contains files outside the plugin folder: " | |
| + ", ".join(invalid) | |
| ) | |
| digest = hashlib.sha256(archive.read_bytes()).hexdigest() | |
| checksum = dist / f"{archive.name}.sha256" | |
| checksum.write_text( | |
| f"{digest} {archive.name}\n", | |
| encoding="utf-8", | |
| ) | |
| changelog = Path("CHANGELOG.md").read_text(encoding="utf-8") | |
| heading = f"## v{version}" | |
| start = changelog.find(heading) | |
| if start < 0: | |
| raise SystemExit(f"Missing {heading} in CHANGELOG.md") | |
| next_heading = changelog.find("\n## v", start + len(heading)) | |
| notes = changelog[start:next_heading if next_heading >= 0 else None] | |
| notes = notes.strip() + "\n" | |
| notes += ( | |
| "\nBuild channel: " + metadata["channel"] + "\n" | |
| "Build ID: " + metadata["build_id"] + "\n" | |
| "Commit: " + metadata["commit"] + "\n" | |
| ) | |
| (dist / "release-notes.md").write_text(notes, encoding="utf-8") | |
| PY | |
| - name: Create tag and GitHub release | |
| if: github.event_name == 'workflow_dispatch' && github.ref_name == 'main' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="$(tr -d '[:space:]' < VERSION)" | |
| RELEASE_VERSION="$(sed -n 's/.*release_version = "\([^"]*\)".*/\1/p' koreaderremote.koplugin/build.lua)" | |
| TAG="v${RELEASE_VERSION}" | |
| ARCHIVE="dist/koreaderremote-v${RELEASE_VERSION}.zip" | |
| CHECKSUM="${ARCHIVE}.sha256" | |
| NOTES="dist/release-notes.md" | |
| git fetch --tags --force | |
| EXISTING_SHA="$(git rev-list -n 1 "${TAG}" 2>/dev/null || true)" | |
| if [[ -n "${EXISTING_SHA}" && "${EXISTING_SHA}" != "${GITHUB_SHA}" ]]; then | |
| echo "Tag ${TAG} already points to ${EXISTING_SHA}, not ${GITHUB_SHA}." | |
| exit 1 | |
| fi | |
| if [[ -z "${EXISTING_SHA}" ]]; then | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "${TAG}" "${GITHUB_SHA}" -m "KOReader Remote ${TAG}" | |
| git push origin "${TAG}" | |
| fi | |
| if gh release view "${TAG}" >/dev/null 2>&1; then | |
| gh release upload "${TAG}" \ | |
| "${ARCHIVE}" \ | |
| "${CHECKSUM}" \ | |
| --clobber | |
| gh release edit "${TAG}" \ | |
| --title "${TAG}" \ | |
| --notes-file "${NOTES}" \ | |
| --latest | |
| else | |
| gh release create "${TAG}" \ | |
| "${ARCHIVE}" \ | |
| "${CHECKSUM}" \ | |
| --verify-tag \ | |
| --title "${TAG}" \ | |
| --notes-file "${NOTES}" \ | |
| --latest | |
| fi | |
| gh release view "${TAG}" |