diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 00000000..059fe4c1 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,97 @@ +name: Create Release (Deterministic) + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (for example: 4.0.0)" + required: true + type: string + make_latest: + description: "Mark this release as latest" + required: true + type: boolean + default: true + prerelease: + description: "Mark as pre-release" + required: true + type: boolean + default: false + +permissions: + contents: write + +jobs: + create-release: + name: Bump version, tag, and create release + runs-on: ubuntu-latest + + steps: + - name: Check out default branch + uses: actions/checkout@v4.2.2 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version format: $VERSION" + echo "Use semver like 4.0.0 or 4.0.0-rc.1" + exit 1 + fi + + - name: Ensure release tag does not already exist + run: | + TAG="v${{ inputs.version }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" + exit 1 + fi + + - name: Bump package version + run: | + VERSION="${{ inputs.version }}" + CURRENT_VERSION="$(node -p \"require('./package.json').version\")" + + if [ "$CURRENT_VERSION" != "$VERSION" ]; then + npm version "$VERSION" --no-git-tag-version --allow-same-version + else + echo "package.json already at version $VERSION" + fi + + - name: Commit version bump + run: | + VERSION="${{ inputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add package.json + if [ -f package-lock.json ]; then + git add package-lock.json + fi + + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "chore(release): bump version to $VERSION" + fi + + - name: Create and push git tag + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + + git tag -a "$TAG" -m "Release $TAG" + git push origin "HEAD:${{ github.event.repository.default_branch }}" + git push origin "$TAG" + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ inputs.version }} + target_commitish: ${{ github.event.repository.default_branch }} + generate_release_notes: true + make_latest: ${{ inputs.make_latest && 'true' || 'false' }} + prerelease: ${{ inputs.prerelease }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..cd29d2d7 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,143 @@ +name: Publish Release Docker Images + +on: + release: + types: + - published + workflow_dispatch: + +env: + DOCKERHUB_REGISTRY: docker.io + GITHUB_REGISTRY: ghcr.io + DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} + GITHUB_IMAGE_NAME: ${{ github.repository }} + +concurrency: + group: release-docker-${{ github.event.release.tag_name || github.ref }} + cancel-in-progress: true + +jobs: + build-and-push-release: + name: Build and push release Docker images + strategy: + matrix: + variant: [debian, alpine] + base_path: ["", "/status"] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Check out release tag + uses: actions/checkout@v4.2.2 + with: + ref: refs/tags/${{ github.event.release.tag_name || github.ref_name }} + fetch-depth: 0 + + - name: Validate package version matches release tag + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + EXPECTED_VERSION="${TAG#v}" + PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" + + if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then + echo "package.json version mismatch" + echo "release tag: $TAG" + echo "expected package.json version: $EXPECTED_VERSION" + echo "actual package.json version: $PACKAGE_VERSION" + exit 1 + fi + + - name: Install cosign + uses: sigstore/cosign-installer@v3.8.0 + with: + cosign-release: v2.2.4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.8.0 + + - name: Log in to Docker Hub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3.3.0 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute release tags + id: vars + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + NORM_TAG="${TAG#v}" + + if [ "${{ matrix.base_path }}" = "/status" ]; then + BASE_SUFFIX="-status" + else + BASE_SUFFIX="" + fi + + if [ "${{ matrix.variant }}" = "alpine" ]; then + VARIANT_SUFFIX="-alpine" + else + VARIANT_SUFFIX="" + fi + + FULL_SUFFIX="${BASE_SUFFIX}${VARIANT_SUFFIX}" + + echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + + if [ "${{ matrix.variant }}" = "debian" ]; then + echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + echo "release_norm_tag_debian_alias=${NORM_TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + echo "latest_tag_debian_alias=latest${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + fi + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.6.1 + with: + images: | + ${{ env.DOCKERHUB_IMAGE_NAME }} + ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.vars.outputs.latest_tag }} + type=raw,value=${{ steps.vars.outputs.release_tag }} + type=raw,value=${{ steps.vars.outputs.release_norm_tag }},enable=${{ steps.vars.outputs.release_norm_tag != steps.vars.outputs.release_tag }} + type=raw,value=${{ steps.vars.outputs.latest_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }} + type=raw,value=${{ steps.vars.outputs.release_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }} + type=raw,value=${{ steps.vars.outputs.release_norm_tag_debian_alias }},enable=${{ matrix.variant == 'debian' && steps.vars.outputs.release_norm_tag_debian_alias != steps.vars.outputs.release_tag_debian_alias }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.3.0 + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6.13.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VARIANT=${{ matrix.variant }} + WITH_DOCS=true + KENER_BASE_PATH=${{ matrix.base_path }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign the published Docker images + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: | + echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/Dockerfile b/Dockerfile index 46b02e06..3f4b9d0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ ARG NODE_VERSION=24 ARG VARIANT=alpine ARG WITH_DOCS=false +ARG KENER_BASE_PATH= # ============================================================================= # STAGE 1 — BUILDER (installs deps, compiles native modules, builds app) @@ -52,6 +53,9 @@ ENV NPM_CONFIG_LOGLEVEL=error WORKDIR /app +ARG KENER_BASE_PATH +ENV KENER_BASE_PATH=${KENER_BASE_PATH} + # 1. Copy package manifests first (maximises layer cache hits) COPY package*.json ./ @@ -126,9 +130,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ FROM final-${VARIANT} AS final ARG PORT=3000 +ARG KENER_BASE_PATH= ENV NODE_ENV=production \ PORT=${PORT} \ + KENER_BASE_PATH=${KENER_BASE_PATH} \ TZ=UTC \ # Required so Node can import .ts migration/seed files at runtime NODE_OPTIONS="--experimental-strip-types" diff --git a/README.md b/README.md index b57345a3..b908271b 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,13 @@ You can use either image: - `docker.io/rajnandan1/kener:latest` - `ghcr.io/rajnandan1/kener:latest` +For subpath deployments (`/status`), use: + +- `docker.io/rajnandan1/kener:latest-status` +- `docker.io/rajnandan1/kener:latest-status-alpine` +- `ghcr.io/rajnandan1/kener:latest-status` +- `ghcr.io/rajnandan1/kener:latest-status-alpine` + ```bash mkdir -p database docker run -d \ @@ -118,6 +125,24 @@ docker run -d \ docker.io/rajnandan1/kener:latest ``` +### Run pre-built subpath image (`/status`) + +```bash +mkdir -p database +docker run -d \ + --name kener-status \ + -p 3000:3000 \ + -v "$(pwd)/database:/app/database" \ + -e "KENER_SECRET_KEY=replace_with_a_random_string" \ + -e "ORIGIN=http://localhost:3000" \ + -e "KENER_BASE_PATH=/status" \ + -e "REDIS_URL=redis://host.docker.internal:6379" \ + docker.io/rajnandan1/kener:latest-status +``` + +> [!NOTE] +> For subpath mode, keep `ORIGIN` as the site origin (`http://localhost:3000`), not `http://localhost:3000/status`. + ### Run without Docker Requirements: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 18016ce6..130630a2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -37,6 +37,7 @@ services: VARIANT: alpine # or "debian" NODE_VERSION: 24 WITH_DOCS: "true" + KENER_BASE_PATH: "" container_name: kener-dev environment: KENER_SECRET_KEY: dev-secret-key-for-local-testing-only diff --git a/docker-compose.status.yml b/docker-compose.status.yml new file mode 100644 index 00000000..32488568 --- /dev/null +++ b/docker-compose.status.yml @@ -0,0 +1,47 @@ +# ============================================================================= +# Kener v4 — Production Docker Compose for /status base path +# +# Usage: +# docker compose -f docker-compose.status.yml up -d +# ============================================================================= + +services: + redis: + image: redis:7-alpine + container_name: kener-redis-status + restart: unless-stopped + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + kener: + image: rajnandan1/kener:latest-status + # For Alpine variant use: rajnandan1/kener:latest-status-alpine + container_name: kener-status + environment: + KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32 + ORIGIN: http://localhost:3000/status + REDIS_URL: redis://redis:6379 + KENER_BASE_PATH: /status + + # DATABASE_URL: sqlite://./database/kener.sqlite.db + # DATABASE_URL: postgresql://user:password@postgres:5432/kener + # DATABASE_URL: mysql://user:password@mysql:3306/kener + ports: + - "3000:3000" + volumes: + - data:/app/database + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + +volumes: + data: + name: kener_db_status + redis_data: + name: kener_redis_status diff --git a/src/lib/components/ThemePlus.svelte b/src/lib/components/ThemePlus.svelte index 845bb8ea..eb286d55 100644 --- a/src/lib/components/ThemePlus.svelte +++ b/src/lib/components/ThemePlus.svelte @@ -78,7 +78,7 @@ }); -