diff --git a/.github/workflows/_mirror-image.yml b/.github/workflows/_mirror-image.yml new file mode 100644 index 0000000..2404741 --- /dev/null +++ b/.github/workflows/_mirror-image.yml @@ -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: " + 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:-}\`" + echo "- **New digest:** \`${new_digest}\`" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/mirror-python.yml b/.github/workflows/mirror-python.yml new file mode 100644 index 0000000..616f664 --- /dev/null +++ b/.github/workflows/mirror-python.yml @@ -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-.yml" for image mirror workflows, kept +# separate from "build-.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 }} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..24a8386 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..500dd44 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,5 @@ +# Architecture + +Design and architecture documentation for the `cssc-framework` repository. + +> Placeholder — add architecture docs here. diff --git a/docs/contributing/workflow-naming.md b/docs/contributing/workflow-naming.md new file mode 100644 index 0000000..24ba6b4 --- /dev/null +++ b/docs/contributing/workflow-naming.md @@ -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-.yml` | `mirror / base/` | `mirror-base-` | +| **Build** | Build an application image on top of a mirrored base | `build-.yml` | `build / ` | `build-` | +| **Reusable** | Shared logic invoked by other workflows; never triggered directly | `_.yml` (leading underscore) | `_reusable / ` | 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/` scheme, e.g. `ghcr.io//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-.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-.yml` filename and the +`build / ` display name so they remain clearly separate from mirror +workflows. diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..1b87987 --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,5 @@ +# Guides + +How-to and operational guides for the `cssc-framework` repository. + +> Placeholder — add guides here. diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..8d26bb0 --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1,5 @@ +# Reference + +Reference material and conventions for the `cssc-framework` repository. + +> Placeholder — add reference docs here.