diff --git a/.devenv-version b/.devenv-version index a45be4627..1866a362b 100644 --- a/.devenv-version +++ b/.devenv-version @@ -1 +1 @@ -0.2.8 +0.2.9 diff --git a/.github/actions/devenv-lint-tools/action.yml b/.github/actions/devenv-lint-tools/action.yml new file mode 100644 index 000000000..8e199a2e2 --- /dev/null +++ b/.github/actions/devenv-lint-tools/action.yml @@ -0,0 +1,27 @@ +# Managed by devenv (copy/ci). Edit it there, not in a child repo. +name: devenv lint tools +description: >- + Install the lint tools devenv's bin/ scripts run with Homebrew and put them + on PATH for later steps. The latest of each, as on the dev machines. Works + on ubuntu runners, where Homebrew is installed but not on PATH, and on macOS. +inputs: + tools: + description: Space-separated Homebrew formulae to install. + default: shellcheck shellharden shfmt hadolint dockerfmt actionlint +runs: + using: composite + steps: + - name: Install lint tools with Homebrew + shell: bash + env: + TOOLS: ${{ inputs.tools }} + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + HOMEBREW_NO_ANALYTICS: "1" + run: | + if [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + fi + read -ra formulae <<<"$TOOLS" + brew install "${formulae[@]}" + echo "$(brew --prefix)/bin" >>"$GITHUB_PATH" diff --git a/.github/workflows/devenv-check.yml b/.github/workflows/devenv-check.yml index 1bad0e684..42e2c0c1b 100644 --- a/.github/workflows/devenv-check.yml +++ b/.github/workflows/devenv-check.yml @@ -2,9 +2,10 @@ # # Gate, one CI image build, a fail-fast lint/test/build matrix that pulls that # image, and the required-check aggregator. With container: false the matrix -# runs natively on inputs.runner instead (for macOS-only projects) and no -# image is built. Call it from a job in ci.yml; later jobs read its outputs -# instead of repeating any trigger logic: +# runs natively on inputs.runner instead (for macOS-only projects), no image +# is built and the lint combo installs the lint tools with brew. Call it from +# a job in ci.yml; later jobs read its outputs instead of repeating any +# trigger logic: # # ci: # uses: ./.github/workflows/devenv-check.yml @@ -36,7 +37,8 @@ on: description: >- true runs every combo in the CI image built from the Dockerfile. false runs it natively on the runner after setup-uv, setup-bun and - make install, for projects that cannot run in a Linux container. + make install, for projects that cannot run in a Linux container; a + combo that runs lint installs the lint tools with brew first. type: boolean default: true runner: @@ -296,6 +298,12 @@ jobs: if: ${{ !inputs.container }} uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + # Only the combo that lints pays for the install. In container mode the + # CI stage of the project's Dockerfile has the tools. + - name: Install lint tools + if: ${{ !inputs.container && contains(matrix.make, 'lint') }} + uses: ./.github/actions/devenv-lint-tools + - name: Install if: ${{ !inputs.container }} run: | diff --git a/Dockerfile b/Dockerfile index 9a1005a2d..b135496db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,17 +40,18 @@ RUN pip3 install --no-cache --upgrade pip FROM oven/bun:latest AS bun-source FROM builder-base AS codex-ci -# hadolint ignore=DL3008 -RUN apt-get clean \ - && apt-get update \ - && apt-get install --no-install-recommends -y \ - shellcheck \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun COPY --from=bun-source /usr/local/bin/bunx /usr/local/bin/bunx +# Lint tools devenv's bin/ scripts run, unpinned on purpose. +COPY --from=koalaman/shellcheck:stable /bin/shellcheck /usr/local/bin/ +COPY --from=mvdan/shfmt:latest /bin/shfmt /usr/local/bin/ +COPY --from=hadolint/hadolint:latest /bin/hadolint /usr/local/bin/ +COPY --from=rhysd/actionlint:latest /usr/local/bin/actionlint /usr/local/bin/ +COPY --from=ghcr.io/reteps/dockerfmt:latest /ko-app/dockerfmt /usr/local/bin/ +ADD https://github.com/anordal/shellharden/releases/latest/download/shellharden-x86_64-unknown-linux-musl.tar.gz /tmp/shellharden.tar.gz +RUN tar -xzf /tmp/shellharden.tar.gz -C /usr/local/bin shellharden && rm /tmp/shellharden.tar.gz + WORKDIR /app # Python deps (cacheable when lockfiles unchanged) @@ -143,4 +144,4 @@ RUN python -B -c "import django, cryptography, granian, rapidfuzz" \ VOLUME /comics VOLUME /config EXPOSE 9810 -CMD ["/opt/codex/bin/codex"] +CMD ["/opt/codex/bin/codex"] \ No newline at end of file diff --git a/bin/_lib.sh b/bin/_lib.sh index e281ddd6c..098d86697 100644 --- a/bin/_lib.sh +++ b/bin/_lib.sh @@ -17,11 +17,16 @@ is_darwin() { } # Succeed when the tool is on PATH. Otherwise say so and how to install it, and -# fail, so a caller can skip that tool visibly instead of silently. +# fail, so a caller can skip that tool visibly instead of silently. In CI (CI +# set, as GitHub sets it) a missing tool is an error instead, so a green CI +# run means every tool ran. need() { if command -v "$1" >/dev/null 2>&1; then return 0 fi + if [[ -n ${CI:-} ]]; then + die "$1 not installed. CI must install it: brew install $1, or add it to the Dockerfile's CI stage" + fi echo "skipped: $1 not installed (brew install $1)" >&2 return 1 } diff --git a/codex/migrations/0001_init.py b/codex/migrations/0001_init.py index 979c1fb59..51eb61ad1 100644 --- a/codex/migrations/0001_init.py +++ b/codex/migrations/0001_init.py @@ -1,6 +1,6 @@ """Generated by Django 3.1 on 2020-08-21 18:59.""" -import datetime +import datetime as dt from decimal import Decimal import django.db.models.deletion @@ -88,7 +88,7 @@ class Migration(migrations.Migration): ("enable_scan_cron", models.BooleanField(db_index=True, default=True)), ( "scan_frequency", - models.DurationField(default=datetime.timedelta(seconds=43200)), + models.DurationField(default=dt.timedelta(seconds=43200)), ), ("last_scan", models.DateTimeField(null=True)), ("scan_in_progress", models.BooleanField(default=False)), diff --git a/codex/migrations/0007_auto_20211210_1710.py b/codex/migrations/0007_auto_20211210_1710.py index 2a637e2c4..108057dd9 100644 --- a/codex/migrations/0007_auto_20211210_1710.py +++ b/codex/migrations/0007_auto_20211210_1710.py @@ -1,6 +1,6 @@ """Generated by Django 3.2.9 on 2021-12-11 01:10.""" -import datetime +import datetime as dt import django.db.models.deletion from django.db import migrations, models @@ -111,7 +111,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="library", name="poll_every", - field=models.DurationField(default=datetime.timedelta(seconds=3600)), + field=models.DurationField(default=dt.timedelta(seconds=3600)), ), migrations.AlterField( model_name="comic", diff --git a/codex/user_data/restore.py b/codex/user_data/restore.py index 6d99b1755..afb3caec6 100644 --- a/codex/user_data/restore.py +++ b/codex/user_data/restore.py @@ -270,7 +270,7 @@ def _restore_libraries( store: SidecarStore, report: RestoreReport, *, dry_run: bool ) -> None: """Restore Library rows.""" - import datetime + import datetime as dt from codex.models.library import Library @@ -285,9 +285,7 @@ def _restore_libraries( } if row["poll_every"]: # Stored as total-seconds float by ``serialize_library``. - defaults["poll_every"] = datetime.timedelta( - seconds=float(row["poll_every"]) - ) + defaults["poll_every"] = dt.timedelta(seconds=float(row["poll_every"])) Library.objects.update_or_create(path=row["path"], defaults=defaults) report.note_written("libraries") diff --git a/compose.yaml b/compose.yaml index bfdb90b7c..552166153 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,7 @@ services: - infinity environment: - PYTHONUNBUFFERED=1 + - CI image: ${CI_IMAGE} volumes: - ./test-results:/app/test-results diff --git a/frontend/src/api/v4/browser.js b/frontend/src/api/v4/browser.js index ad36f07aa..96995b5af 100644 --- a/frontend/src/api/v4/browser.js +++ b/frontend/src/api/v4/browser.js @@ -55,13 +55,12 @@ export const getBrowserHref = ({ collection, pks, query }) => { export const getCoverSrc = ({ coverPk, coverCustomPk }, ts) => { const query = ts ? `?ts=${ts}` : ""; - if (coverCustomPk) { - return `${V4_BASE}covers/custom/${coverCustomPk}${query}`; - } // ``coverPk`` is the representative comic pk pre-annotated by the // browser response. v4 keeps the per-comic source so the URL is // cacheable across cards that share a representative. - return `${V4_BASE}covers/comic/${coverPk}${query}`; + return coverCustomPk + ? `${V4_BASE}covers/custom/${coverCustomPk}${query}` + : `${V4_BASE}covers/comic/${coverPk}${query}`; }; const PLACEHOLDER_BY_GROUP = Object.freeze({ diff --git a/frontend/src/stores/admin.js b/frontend/src/stores/admin.js index f790ba68e..5f98ce15f 100644 --- a/frontend/src/stores/admin.js +++ b/frontend/src/stores/admin.js @@ -170,7 +170,6 @@ export const useAdminStore = defineStore("admin", { } }, async loadTables(tables, options) { - if (this._requireAdmin()) return false; /* * ``Promise.all`` so every fetch runs concurrently and the * returned promise resolves only once they've all settled. @@ -179,9 +178,11 @@ export const useAdminStore = defineStore("admin", { * could observe an admin tab's state mid-load (some tables * populated, some still empty), which presented as flicker. */ - return await Promise.all( - tables.map((table) => this.loadTable(table, options)), - ); + return this._requireAdmin() + ? false + : await Promise.all( + tables.map((table) => this.loadTable(table, options)), + ); }, async loadFolders(path, showHidden) { if (this._requireAdmin()) return false; diff --git a/frontend/src/stores/reader.js b/frontend/src/stores/reader.js index 272242ba3..75ba1d68c 100644 --- a/frontend/src/stores/reader.js +++ b/frontend/src/stores/reader.js @@ -371,8 +371,7 @@ export const useReaderStore = defineStore("reader", { }, fitToClass(bookSettings) { const classes = {}; - let fitTo; - fitTo = + const fitTo = this.clientSettings.scale > SCALE_DEFAULT ? FIT_TO_CLASSES.O : FIT_TO_CLASSES[bookSettings.fitTo]; diff --git a/pyproject.toml b/pyproject.toml index 5fbc1d14e..daf1baa33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,6 +161,9 @@ source-include = [ "docs/**", ] source-exclude = [ + # Symlinks to directories, which the sdist builder reads as files. + "docs/NEWS/codex", + "docs/img", "**/*\\~", "**/.DS_Store", "codex/static_build", diff --git a/tests/test_models.py b/tests/test_models.py index d2f47b763..d2f7019dd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,6 @@ """Test models.""" -import datetime +import datetime as dt import shutil from typing import override @@ -40,7 +40,7 @@ class ComicTestCase(TestCase): YEAR = 1975 MONTH = 4 DAY = 9 - DATE = datetime.date(YEAR, MONTH, DAY) + DATE = dt.date(YEAR, MONTH, DAY) @override def setUp(self): diff --git a/uv.lock b/uv.lock index b893ac9f3..4a9f31255 100644 --- a/uv.lock +++ b/uv.lock @@ -2908,11 +2908,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.13.4" +version = "4.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/89/dfd269de936e76319cf27787d9eaaca513491f635d49b0fbe7cdb6d3bf6f/platformdirs-4.13.4.tar.gz", hash = "sha256:dd96496961684a7f42041923f842ec7494e09f5e87aded99117e1109cd40570e", size = 62364, upload-time = "2026-10-17T19:31:53.69Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/51/5614d0537dd830ca64d95a2f254bd66bdf3ac61d41cffcccaec62121fcfd/platformdirs-4.13.4-py3-none-any.whl", hash = "sha256:502ce08e9d36cf90579ee44e1e764838842fa658e4975ef47344fd053011d2c8", size = 32782, upload-time = "2026-10-17T19:31:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, ] [[package]]