Skip to content

Simplify device settings help #108

Simplify device settings help

Simplify device settings help #108

Workflow file for this run

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}"