Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm

RUN rm -f /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
sqlite3 \
git \
curl \
&& rm -rf /var/lib/apt/lists/*
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "2.1.0",
"resolved": "ghcr.io/devcontainers/features/node@sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857",
"integrity": "sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857"
}
}
}
29 changes: 29 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "PocketKid Development",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "lts"
}
},
"workspaceFolder": "/workspace/pocketkid",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace/pocketkid,type=bind",
"postCreateCommand": "pip install -r requirements.txt",
"forwardPorts": [
8000
],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
}
}
},
"remoteUser": "vscode"
}
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ VAPID_PRIVATE_KEY_B64=

# Optional
VAPID_SUBJECT=mailto:pocketkid@example.com

# Docker Compose image tag (for example 0.1.0); defaults to latest
POCKETKID_IMAGE_TAG=latest
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
pull_request:
branches: [master]
push:
branches: [master]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
VAPID_PUBLIC_KEY: test-public-key
VAPID_PRIVATE_KEY: test-private-key

steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements.txt

- name: Install dependencies
run: python -m pip install -r requirements.txt

- name: Run tests
run: python -m unittest discover -s tests -v

container-smoke-test:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Build container image
run: docker build --tag pocketkid:ci .

- name: Start and verify container
shell: bash
run: |
docker run --detach --name pocketkid-smoke \
--publish 127.0.0.1:8000:8000 \
--env VAPID_PUBLIC_KEY=test-public-key \
--env VAPID_PRIVATE_KEY=test-private-key \
pocketkid:ci

for attempt in {1..30}; do
status="$(curl --silent --output /dev/null --write-out "%{http_code}" http://127.0.0.1:8000/ || true)"
if [ "$status" = "302" ]; then
exit 0
fi
sleep 1
done

docker logs pocketkid-smoke
exit 1

- name: Stop smoke-test container
if: always()
run: docker rm --force pocketkid-smoke || true
193 changes: 168 additions & 25 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -1,52 +1,195 @@
name: Build and Publish Docker image
name: Publish release image

on:
push:
branches: [ "main" ]
tags: [ "v*" ]
workflow_dispatch:
release:
types: [published]

permissions:
contents: read
packages: write
attestations: write
id-token: write

concurrency:
group: release-${{ github.event.release.tag_name }}
cancel-in-progress: false

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # owner/repo
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
publish:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
VAPID_PUBLIC_KEY: test-public-key
VAPID_PRIVATE_KEY: test-private-key

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout release tag
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.event.release.tag_name }}
fetch-depth: 0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Validate release tag and version
id: version
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
if [ "$RELEASE_PRERELEASE" != "false" ]; then
echo "Pre-releases are not supported by this stable release workflow" >&2
exit 1
fi

version="$(tr -d '\r\n' < VERSION)"

if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "VERSION must contain a stable semantic version, got: $version" >&2
exit 1
fi

expected_tag="fork-v${version}"
if [ "$RELEASE_TAG" != "$expected_tag" ]; then
echo "Release tag $RELEASE_TAG does not match VERSION ($expected_tag)" >&2
exit 1
fi

if ! git merge-base --is-ancestor HEAD origin/master; then
echo "Release commit must be contained in master" >&2
exit 1
fi

major="${version%%.*}"
major_minor="${version%.*}"
publish_major=true
if [ "$major" = "0" ]; then
publish_major=false
fi

- name: Log in to GHCR
uses: docker/login-action@v3
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "major=$major" >> "$GITHUB_OUTPUT"
echo "major_minor=$major_minor" >> "$GITHUB_OUTPUT"
echo "publish_major=$publish_major" >> "$GITHUB_OUTPUT"
echo "commit=$GITHUB_SHA" >> "$GITHUB_OUTPUT"

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
python-version: "3.12"
cache: pip
cache-dependency-path: requirements.txt

- name: Install dependencies
run: python -m pip install -r requirements.txt

- name: Run tests
run: python -m unittest discover -s tests -v

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Extract Docker metadata
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=${{ steps.version.outputs.major_minor }}
type=raw,value=${{ steps.version.outputs.major }},enable=${{ steps.version.outputs.publish_major }}
type=raw,value=latest
type=raw,value=sha-${{ steps.version.outputs.commit }}
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.revision=${{ steps.version.outputs.commit }}

- name: Build and push
uses: docker/build-push-action@v6
- name: Build release image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: true
load: true
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Verify image version
env:
EXPECTED_VERSION: ${{ steps.version.outputs.version }}
RELEASE_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
run: |
docker run --rm \
--env EXPECTED_VERSION \
--env VAPID_PUBLIC_KEY \
--env VAPID_PRIVATE_KEY \
"$RELEASE_IMAGE" \
python -c 'import os; from pocketkid.config import APP_VERSION; assert APP_VERSION == os.environ["EXPECTED_VERSION"]'

- name: Start and verify release container
shell: bash
env:
RELEASE_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
run: |
docker run --detach --name pocketkid-release-smoke \
--publish 127.0.0.1:8000:8000 \
--env VAPID_PUBLIC_KEY \
--env VAPID_PRIVATE_KEY \
"$RELEASE_IMAGE"

for attempt in {1..30}; do
status="$(curl --silent --output /dev/null --write-out "%{http_code}" http://127.0.0.1:8000/ || true)"
if [ "$status" = "302" ]; then
exit 0
fi
sleep 1
done

docker logs pocketkid-release-smoke
exit 1

- name: Stop release smoke-test container
if: always()
run: docker rm --force pocketkid-release-smoke || true

- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Publish tested image tags
id: publish
shell: bash
env:
DOCKER_TAGS: ${{ steps.meta.outputs.tags }}
RELEASE_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
run: |
push_output="$(docker push "$RELEASE_IMAGE")"
printf '%s\n' "$push_output"

digest="$(printf '%s\n' "$push_output" | sed -n 's/.*digest: \(sha256:[0-9a-f]\{64\}\).*/\1/p' | tail -n 1)"
if [ -z "$digest" ]; then
echo "Could not determine published image digest" >&2
exit 1
fi

while IFS= read -r tag; do
if [ -n "$tag" ] && [ "$tag" != "$RELEASE_IMAGE" ]; then
docker push "$tag"
fi
done <<< "$DOCKER_TAGS"

echo "digest=$digest" >> "$GITHUB_OUTPUT"

- name: Attest image provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.publish.outputs.digest }}
push-to-registry: true
Loading