# Vanilla upstream Cronicle, packaged as a container image.
#
# Why this exists: the "official" cronicle/cronicle image is published by the
# cronicle-edge project and has not been rebuilt since 2025-02-24 (:latest ==
# :0.9.74) while upstream Cronicle ships roughly monthly. See README.md.
#
# Deliberately reproduces the layout of cronicle/cronicle:0.9.74 (recovered via
# `docker history`) so this is a drop-in replacement. Deviations are called out
# in comments below.

ARG ALPINE_VERSION=3.22
FROM alpine:${ALPINE_VERSION}

# Cronicle release to build, WITHOUT the leading "v" (e.g. 0.9.126).
# CI resolves this from upstream's latest GitHub release.
ARG CRONICLE_VERSION

# Upstream's exact package list, intentionally unmodified. This image is the
# runtime for every Shell Script job, so the package set is effectively the job
# environment -- trimming it to save a few MB risks breaking job scripts.
# Notably:
#   procps    - Cronicle shells out to a real `ps` for job CPU/memory
#               monitoring; busybox ps is not sufficient.
#   tini      - reaps zombies left by job child processes (PID 1 duties).
#   jq        - used directly by at least one job script.
#   util-linux, coreutils, bash - job scripts expect GNU/util-linux behaviour
#               rather than busybox applets.
RUN apk add --no-cache \
      acl \
      bash \
      coreutils \
      curl \
      git \
      jq \
      nodejs \
      npm \
      openssl \
      procps \
      tar \
      tini \
      util-linux

# Matches upstream's image env.
#
# CRONICLE_foreground / CRONICLE_echo are pixl-config env overrides (the
# CRONICLE_ prefix maps to config keys); together they keep the process in the
# foreground and stream the log to stdout, which is what makes `docker logs`
# useful.
#
# /usr/local/bin is on PATH because deployments commonly bind-mount a docker
# client there for jobs that drive sibling containers.
#
# DEVIATION: upstream also sets TZ=America/New_York. Omitted on purpose -- this
# image defaults to UTC, and the deployment sets TZ explicitly.
ENV CRONICLE_foreground=1 \
    CRONICLE_echo=1 \
    EDITOR=vi \
    PATH=/opt/cronicle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

RUN addgroup cronicle --gid 1099 \
 && adduser -D -h /opt/cronicle -u 1000 -G cronicle cronicle

WORKDIR /opt/cronicle

RUN test -n "$CRONICLE_VERSION" || { \
      echo "ERROR: --build-arg CRONICLE_VERSION=<x.y.z> is required" >&2; \
      exit 1; \
    }

# Built from the GitHub release tarball rather than npm: upstream marked the
# package "private": true as of v0.9.126, so it is not published to the
# registry. -f makes a bad/missing tag fail loudly instead of piping an HTML
# error page into tar.
#
# `busybox tar`, not GNU tar, and deliberately so. Alpine 3.22's GNU tar
# restores directory modes via the fchmodat2() syscall. Docker's default seccomp
# profile denies syscalls it does not know about by returning EPERM, and
# releases before ~25.0 predate fchmodat2 -- so on an older daemon GNU tar
# reports "Cannot change mode ...: Operation not permitted" for every directory
# and then exits 2, failing the build. The files do all extract, but the exit
# code is fatal and the directory modes are left unset. Verified: identical
# extraction under `--security-opt seccomp=unconfined` produces zero errors.
# Busybox tar uses plain fchmodat(), is unaffected, exits 0, and reproduces the
# archive's modes exactly. GNU tar is still installed above for runtime parity.
RUN curl -fsSL "https://github.com/jhuckaby/Cronicle/archive/refs/tags/v${CRONICLE_VERSION}.tar.gz" -o /tmp/cronicle.tar.gz \
 && busybox tar xzf /tmp/cronicle.tar.gz --strip-components 1 -C /opt/cronicle \
 && rm /tmp/cronicle.tar.gz

# `npm ci`, not upstream's `npm install`, so the build is reproducible from the
# lockfile committed in the release tarball.
#
# --ignore-scripts skips the root package's "postinstall": "pixl-boot install",
# which registers Cronicle as a systemd/init service -- meaningless in a
# container. No *dependency* declares an install script (verified against the
# lockfile), so nothing else is being skipped, and no build toolchain is needed.
#
# --omit=dev drops pixl-unit, the only devDependency, which is otherwise dead
# weight in the runtime image. bin/build.js needs only async, pixl-tools and
# uglify-js, all of which are regular dependencies.
RUN npm ci --ignore-scripts --omit=dev

# Bundles and minifies the front end into htdocs/js/_combo.js. Required; the UI
# 404s on its assets without it.
RUN node bin/build dist

# Regression gate -- see test/newline-regression.js for the full explanation.
# Lives inside /opt/cronicle so node resolves pixl-json-stream from the real
# installed tree rather than a copy.
COPY test/newline-regression.js /opt/cronicle/newline-regression.js
RUN node /opt/cronicle/newline-regression.js \
 && rm /opt/cronicle/newline-regression.js

# Bind mounts normally land on these; create them with tight modes so a missing
# mount does not silently expose job data or secrets.
RUN mkdir -p /opt/cronicle/data /opt/cronicle/conf \
 && chmod 0700 /opt/cronicle/data /opt/cronicle/conf

LABEL org.opencontainers.image.title="Cronicle" \
      org.opencontainers.image.description="Vanilla upstream Cronicle, built from the GitHub release tarball" \
      org.opencontainers.image.version="${CRONICLE_VERSION}" \
      org.opencontainers.image.url="https://github.com/jhuckaby/Cronicle" \
      org.opencontainers.image.source="https://git.lerch.org/lobo/cronicle-docker" \
      org.opencontainers.image.licenses="MIT"

# DEVIATION: upstream also COPYs bin/manager and bin/worker into the image.
# Those are cronicle-edge helper entrypoints; vanilla Cronicle is started with
# `control.sh start`, so they are omitted. No CMD is set, matching upstream --
# the caller supplies it.
ENTRYPOINT ["/sbin/tini", "--"]
