Skip to content
Merged
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
102 changes: 102 additions & 0 deletions .github/workflows/_mirror-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Reusable workflow: mirror a container image from an upstream registry into GHCR.
#
# This workflow is internal (note the leading underscore in the filename) and is
# not meant to be triggered directly. It is called by per-image "mirror-*"
# workflows via `uses:`.
#
# It performs an idempotent sync: on every run it compares the digest of the
# source tag with the digest already present in the destination, and only copies
# when they differ. crane copy preserves multi-architecture manifest lists.
name: _reusable / mirror-image

on:
workflow_call:
inputs:
source_image:
description: "Fully qualified source image without tag (e.g. docker.io/library/python)."
required: true
type: string
source_tag:
description: "Source image tag to mirror (e.g. 3.14-slim)."
required: true
type: string
dest_image:
description: "Fully qualified destination image without tag (e.g. ghcr.io/toddysm/base/python)."
required: true
type: string
dest_tag:
description: "Destination image tag (e.g. 3.14-slim)."
required: true
type: string
force:
description: "Copy even when the source and destination digests match."
required: false
default: false
type: boolean

jobs:
mirror:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Set up crane
uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4

- name: Log in to GHCR
run: |
echo "${{ github.token }}" | crane auth login ghcr.io \
--username "${{ github.actor }}" --password-stdin

- name: Compare digests and copy if changed
env:
SOURCE_REF: "${{ inputs.source_image }}:${{ inputs.source_tag }}"
DEST_REF: "${{ inputs.dest_image }}:${{ inputs.dest_tag }}"
FORCE: "${{ inputs.force }}"
run: |
set -euo pipefail

echo "Source: ${SOURCE_REF}"
echo "Destination: ${DEST_REF}"

source_digest="$(crane digest "${SOURCE_REF}")"
echo "Source digest: ${source_digest}"

if dest_digest="$(crane digest "${DEST_REF}" 2>/dev/null)"; then
echo "Destination digest: ${dest_digest}"
else
dest_digest=""
echo "Destination digest: <not present>"
fi

if [ "${FORCE}" != "true" ] && [ "${source_digest}" = "${dest_digest}" ]; then
echo "Image is already up to date; nothing to copy."
{
echo "### Mirror image: up to date :white_check_mark:"
echo ""
echo "- **Source:** \`${SOURCE_REF}\`"
echo "- **Destination:** \`${DEST_REF}\`"
echo "- **Digest:** \`${source_digest}\`"
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi

if [ "${FORCE}" = "true" ]; then
echo "Force enabled; copying regardless of digest match."
else
echo "Digests differ; copying updated image."
fi

crane copy "${SOURCE_REF}" "${DEST_REF}"

new_digest="$(crane digest "${DEST_REF}")"
echo "Copied. New destination digest: ${new_digest}"
{
echo "### Mirror image: copied :inbox_tray:"
echo ""
echo "- **Source:** \`${SOURCE_REF}\`"
echo "- **Destination:** \`${DEST_REF}\`"
echo "- **Previous digest:** \`${dest_digest:-<none>}\`"
echo "- **New digest:** \`${new_digest}\`"
} >> "${GITHUB_STEP_SUMMARY}"
41 changes: 41 additions & 0 deletions .github/workflows/mirror-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Mirror python:3.14-slim from Docker Hub into GHCR as base/python.
#
# This caller only defines triggers and the image-specific inputs; the actual
# digest-check-and-copy logic lives in the reusable _mirror-image.yml workflow.
#
# Naming convention: "mirror-<image>.yml" for image mirror workflows, kept
# separate from "build-<app>.yml" build workflows. See
# docs/contributing/workflow-naming.md.
name: mirror / base/python

on:
# Daily upstream check at 06:00 UTC.
schedule:
- cron: "0 6 * * *"
# Manual run, with an optional force copy.
workflow_dispatch:
inputs:
force:
description: "Copy even when the source and destination digests match."
required: false
default: false
type: boolean

# Avoid overlapping runs for this specific image mirror.
concurrency:
group: mirror-base-python
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
mirror-python:
uses: ./.github/workflows/_mirror-image.yml
with:
source_image: docker.io/library/python
source_tag: 3.14-slim
dest_image: ghcr.io/toddysm/base/python
dest_tag: 3.14-slim
force: ${{ github.event_name == 'workflow_dispatch' && inputs.force || false }}
18 changes: 18 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Documentation

Documentation for the `cssc-framework` repository. Content is organized by
topic so each area can grow independently.

## Structure

| Folder | Purpose |
| ------ | ------- |
| [`contributing/`](contributing/) | Guides and conventions for people contributing to this repository. |
| [`architecture/`](architecture/) | Design and architecture documentation. |
| [`guides/`](guides/) | How-to and operational guides. |
| [`reference/`](reference/) | Reference material and conventions. |

## Contributing docs

- [Workflow naming conventions](contributing/workflow-naming.md) — how GitHub
Actions workflows are named and organized in this repository.
5 changes: 5 additions & 0 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Architecture

Design and architecture documentation for the `cssc-framework` repository.

> Placeholder — add architecture docs here.
63 changes: 63 additions & 0 deletions docs/contributing/workflow-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Workflow naming conventions

This repository uses GitHub Actions for two distinct purposes that must never be
confused with each other: **mirroring** upstream base images into GitHub
Container Registry (GHCR), and **building** the demo applications on top of those
bases. The naming convention below keeps the two categories clearly separated,
both in the file system and in the Actions UI.

## Categories

| Category | Purpose | Workflow file | Display `name:` | Concurrency group |
| -------- | ------- | ------------- | --------------- | ----------------- |
| **Mirror** | Copy / refresh a base image from an upstream registry into GHCR | `mirror-<image>.yml` | `mirror / base/<image>` | `mirror-base-<image>` |
| **Build** | Build an application image on top of a mirrored base | `build-<app>.yml` | `build / <app>` | `build-<app>` |
| **Reusable** | Shared logic invoked by other workflows; never triggered directly | `_<purpose>.yml` (leading underscore) | `_reusable / <purpose>` | n/a |

### Rules

1. **Verb prefix.** Every workflow filename starts with a category verb:
`mirror-`, `build-`, or a leading underscore (`_`) for reusable workflows.
This groups related workflows together alphabetically and makes intent
obvious at a glance.
2. **Leading underscore = internal.** Reusable workflows (triggered by
`workflow_call`) are prefixed with `_` so they sort to the top of the list
and signal "do not run me directly."
3. **Display names use a `category / subject` format** (for example
`mirror / base/python`) so the Actions sidebar reads cleanly.
4. **Concurrency groups** mirror the filename so two runs of the same logical
job never overlap, while different images/apps run independently.
5. **GHCR destination repositories** for mirrored base images follow the
`base/<image>` scheme, e.g. `ghcr.io/<owner>/base/python`.

## Mirror workflows

Mirror workflows keep a copy of an upstream base image fresh in GHCR.

- **Structure.** Logic lives in a single reusable workflow,
[`_mirror-image.yml`](../../.github/workflows/_mirror-image.yml). Each image
has a thin caller, e.g.
[`mirror-python.yml`](../../.github/workflows/mirror-python.yml), that only
declares triggers and the image-specific inputs and calls the reusable
workflow via `uses:`.
- **Idempotent sync.** On every run the workflow compares the digest of the
source tag against the digest already in GHCR and only copies when they
differ. `crane copy` preserves multi-architecture manifest lists.
- **Triggers.** A daily `schedule` (06:00 UTC) plus `workflow_dispatch` with an
optional `force` input to copy even when digests match.
- **Auth.** GHCR is accessed with the built-in `GITHUB_TOKEN`
(`packages: write`). Public Docker Hub sources are pulled anonymously.

### Adding a new mirror workflow

1. Copy `mirror-python.yml` to `mirror-<image>.yml`.
2. Update the display `name:`, the `concurrency.group`, and the four inputs
(`source_image`, `source_tag`, `dest_image`, `dest_tag`).
3. No logic changes are needed — the reusable workflow does the work.

## Build workflows

Build workflows (added later) build the demo applications under `apps/` on top
of the mirrored base images. They use the `build-<app>.yml` filename and the
`build / <app>` display name so they remain clearly separate from mirror
workflows.
5 changes: 5 additions & 0 deletions docs/guides/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Guides

How-to and operational guides for the `cssc-framework` repository.

> Placeholder — add guides here.
5 changes: 5 additions & 0 deletions docs/reference/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Reference

Reference material and conventions for the `cssc-framework` repository.

> Placeholder — add reference docs here.