From f76b9d831ded53daf030d3fbfbcc9879a2dd6c22 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Mon, 27 Jul 2026 13:29:50 -0400 Subject: [PATCH 1/3] Refactor Docker image build workflows into ci/prerelease/release tiers Replace the duplicated per-variant job bodies in dev.yml/release.yml with a single reusable _build-variant.yml called from a 4-way matrix, and split event handling into three purpose-built workflows: - ci.yml: push/PR, generates all variants and validates with hadolint + `docker buildx build --check` only, no real image build - prerelease.yml (new): v*.*.*-pre.* tags, adds a real multi-platform build (no Docker Hub push) and a GitHub prerelease with notes extracted from docs/Changelog.md - release.yml: v*.*.* tags (excluding -pre.*), adds a Docker Hub push and a full GitHub release Adopts a `v`-prefixed tag convention (v1.1.2, v1.1.2a, v1.1.2a-pre.1) and reuses Ceedling's extract_changelog.sh for changelog-driven release notes. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/_build-variant.yml | 161 +++++++++ .github/workflows/ci.yml | 66 ++++ .github/workflows/dev.yml | 348 -------------------- .github/workflows/extract_changelog.sh | 98 ++++++ .github/workflows/prerelease.yml | 139 ++++++++ .github/workflows/release.md | 39 --- .github/workflows/release.yml | 433 +++++++------------------ README.md | 14 +- 8 files changed, 591 insertions(+), 707 deletions(-) create mode 100644 .github/workflows/_build-variant.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dev.yml create mode 100755 .github/workflows/extract_changelog.sh create mode 100644 .github/workflows/prerelease.yml delete mode 100644 .github/workflows/release.md diff --git a/.github/workflows/_build-variant.yml b/.github/workflows/_build-variant.yml new file mode 100644 index 0000000..0a6791b --- /dev/null +++ b/.github/workflows/_build-variant.yml @@ -0,0 +1,161 @@ +# ========================================================================= +# MadScienceLab Docker Images +# ThrowTheSwitch.org +# SPDX-License-Identifier: MIT +# ========================================================================= + +# ----------------------------------------------------------------------- +# Internal reusable workflow — not intended to be triggered directly. +# Called once per image variant (matrix job) by ci.yml, prerelease.yml, +# and release.yml. +# +# Purpose: +# Generate the Dockerfile + welcome file for one image variant, lint and +# structurally validate the generated Dockerfile, then — depending on +# the calling workflow's mode — optionally perform a real multi-platform +# Docker image build, and optionally push that build to Docker Hub. +# +# Mode matrix (controlled by the `build` / `push` inputs): +# ci.yml build=false push=false → generate + lint + check only +# prerelease.yml build=true push=false → also a real multi-platform +# build, image kept local +# release.yml build=true push=true → also pushed to Docker Hub +# +# Validation vs. build: +# Lint (hadolint) and structural validation (`docker buildx build +# --check`) never produce an image and never need QEMU — they run on +# every call, including plain push/PR builds, without the cost of a +# real multi-platform build. QEMU is only set up when `build: true`. +# ----------------------------------------------------------------------- + +--- +name: "Build Docker Image Variant" + +on: + workflow_call: + inputs: + image_name: + type: string + required: true + description: 'Full image name, e.g. madsciencelab-plugins' + dir_args: + type: string + required: true + description: 'Space-separated --dir arguments for build.sh, e.g. "--dir build/standard --dir build/plugins"' + image_dir: + type: string + required: true + description: 'Directory holding the generated docker/ and assets/ output for this variant, e.g. build/plugins' + version: + type: string + required: true + description: 'Value passed to build.sh --version and baked in as the CONTAINER_VERSION build-arg' + build: + type: boolean + required: false + default: false + description: 'Perform a real multi-platform docker buildx build' + push: + type: boolean + required: false + default: false + description: 'Push the build to Docker Hub (requires build: true)' + secrets: + DOCKERHUB_USERNAME: + required: false + DOCKERHUB_TOKEN: + required: false + +env: + IMAGE_URL: throwtheswitch/${{ inputs.image_name }} + +jobs: + build-variant: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Set up Ruby for generation tool' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: false + + # Workaround: fix Ruby toolcache gem dir permissions (Bundler exit 38) + # Issue: https://github.com/rubygems/rubygems/issues/7983 + - name: 'Apply Workaround for Ruby toolcache Gem Directory Permissions' + run: | + sudo chmod -R go-w /opt/hostedtoolcache/Ruby/**/**/lib/ruby/gems/**/gems || true + + # --prefer-local: reuse gems already installed with Ruby rather than + # always fetching/rebuilding the newest version (same rationale as + # the --prefer-local treatment in build/base/Dockerfile-build) + - name: 'Install file generation Ruby dependencies' + run: bundle install --prefer-local + + - name: 'Run file generation' + id: file-gen + run: bash build.sh ${{ inputs.dir_args }} --version ${{ inputs.version }} + + - name: 'Set up Docker Buildx' + uses: docker/setup-buildx-action@v3 + + # Fast, no Docker daemon involved — pure static analysis of the + # generated Dockerfile text. + - name: 'Lint generated Dockerfile with hadolint' + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ${{ inputs.image_dir }}/docker/Dockerfile + + # Structural/syntax validation via BuildKit's check frontend — parses + # and validates the Dockerfile without executing any RUN steps or + # producing image layers. No QEMU required. + - name: 'Validate generated Dockerfile with docker build --check' + run: docker buildx build --check -f ${{ inputs.image_dir }}/docker/Dockerfile . + + - name: 'Set up QEMU' + if: ${{ inputs.build }} + uses: docker/setup-qemu-action@v3 + + - name: 'Login to Docker Hub' + if: ${{ inputs.push }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: 'Build Docker image ${{ inputs.image_name }}' + if: ${{ inputs.build }} + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + context: . + file: ${{ inputs.image_dir }}/docker/Dockerfile + build-args: | + CONTAINER_VERSION=${{ inputs.version }} + IMAGE_NAME=${{ inputs.image_name }} + push: ${{ inputs.push }} + tags: | + ${{ env.IMAGE_URL }}:${{ inputs.version }} + ${{ env.IMAGE_URL }}:latest + # Connect Docker driver to GitHub Action cache service + cache-from: type=gha + cache-to: type=gha,mode=max + + # Zip generated files as a single artifact — always produced, even + # for validate-only (ci.yml) runs + - name: 'Archive ${{ inputs.image_name }} generated files as a single artifact' + run: zip -j ${{ inputs.image_name }}.zip ${{ inputs.image_dir }}/docker/Dockerfile ${{ inputs.image_dir }}/assets/shell/welcome + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.image_name }} + path: ${{ inputs.image_name }}.zip + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..12f2378 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +# ========================================================================= +# MadScienceLab Docker Images +# ThrowTheSwitch.org +# SPDX-License-Identifier: MIT +# ========================================================================= + +# ----------------------------------------------------------------------- +# CI Workflow +# +# Purpose: +# Generate all four Dockerfile/asset variants and validate them — +# hadolint (static lint) then `docker buildx build --check` (structural +# validation) — without performing a real Docker image build. No QEMU, +# no Docker Hub interaction, no GitHub release. Generated artifacts are +# attached to the workflow run for inspection. +# +# Trigger: +# Any push to any branch, any pull request targeting main, or a manual +# workflow_dispatch. +# +# Related workflows: +# prerelease.yml — triggered by prerelease tags (v*.*.*-pre.*, etc.); +# also performs a real multi-platform build (no push) +# release.yml — triggered by release tags (v*.*.*, no suffix); +# also performs a real multi-platform build and pushes +# to Docker Hub +# ----------------------------------------------------------------------- + +name: CI + +on: + workflow_dispatch: + push: + branches: + - "**" + pull_request: + branches: + - "main" + +jobs: + build: + name: "Build ${{ matrix.image_name }}" + strategy: + fail-fast: false + matrix: + include: + - image_name: madsciencelab + image_dir: build/standard + dir_args: '--dir build/standard' + - image_name: madsciencelab-plugins + image_dir: build/plugins + dir_args: '--dir build/standard --dir build/plugins' + - image_name: madsciencelab-arm-none-eabi + image_dir: build/arm-none-eabi + dir_args: '--dir build/arm-none-eabi' + - image_name: madsciencelab-arm-none-eabi-plugins + image_dir: build/arm-none-eabi-plugins + dir_args: '--dir build/arm-none-eabi --dir build/plugins --dir build/arm-none-eabi-plugins' + uses: ./.github/workflows/_build-variant.yml + with: + image_name: ${{ matrix.image_name }} + image_dir: ${{ matrix.image_dir }} + dir_args: ${{ matrix.dir_args }} + version: dev + build: false + push: false diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 7a2b84c..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,348 +0,0 @@ -name: Docker images dev build - -on: - workflow_dispatch: - push: - branches: - - "**" - pull_request: - branches: - - "main" - -env: - IMAGE_ORG: 'throwtheswitch' - IMAGE_BASE_NAME: 'madsciencelab' - - -jobs: - # Jobs organized for concurrent Docker image builds - # Jobs only build :latest images without pushing to Docker Hub - - # This is a workaround to the limitation that an `env:` map cannot reference the `env:` map to create additional environment variables - image-details: - runs-on: ubuntu-latest - steps: - # A step must be present in order to make use of `outputs:` - - run: echo "Setting Docker image details..." - outputs: - base-name: 'madsciencelab' - base-url: 'throwtheswitch/madsciencelab' - - madsciencelab: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }} - IMAGE_URL: ${{ needs.image-details.outputs.base-url }} - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/standard - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - id: file-gen - run: bash build.sh --dir ${{ env.IMAGE_DIR }} --version dev - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab - # Note: standard/ directory maps to madsciencelab image (no variants) - - name: 'Build Docker image ${{ env.IMAGE_NAME }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - tags: ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-plugins: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-plugins - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-plugins - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/plugins - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir build/standard --dir ${{ env.IMAGE_DIR }} --version dev - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-plugins - - name: 'Build Docker image ${{ env.IMAGE_NAME }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - tags: ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-arm-none-eabi: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-arm-none-eabi - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-arm-none-eabi - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/arm-none-eabi - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir ${{ env.IMAGE_DIR }} --version dev - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-arm-none-eabi - - name: 'Build Docker image ${{ env.IMAGE_NAME }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - tags: ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-arm-none-eabi-plugins: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-arm-none-eabi-plugins - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-arm-none-eabi-plugins - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/arm-none-eabi-plugins - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir build/arm-none-eabi --dir build/plugins --dir ${{ env.IMAGE_DIR }} --version dev - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-arm-none-eabi-plugins - - name: 'Build Docker image ${{ env.IMAGE_NAME }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - tags: ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - # After all Docker image builds, collect results and publish a pre-release - artifacts-dev-build: - runs-on: ubuntu-latest - needs: - - madsciencelab - - madsciencelab-plugins - - madsciencelab-arm-none-eabi - - madsciencelab-arm-none-eabi-plugins - permissions: - contents: write - - steps: - # Get the repo so we have info for generating release details - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - # Download all artifacts from the 4 Docker image builds - - uses: actions/download-artifact@v4 - with: - # `pattern:` is a workaround to an artifact upload incompatibility with docker/build-push-action@v6 - # Otherwise, apart from this bug requiring `pattern:` the default of all artifacts would occur without any intervention - # https://github.com/docker/build-push-action/issues/1167 - pattern: "madsciencelab*" - path: artifacts - - # Capture the SHA string - - name: 'Git commit short SHA as environment variable' - shell: bash - run: | - echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - - uses: ncipollo/release-action@v1 - with: - prerelease: true - allowUpdates: true - bodyFile: .github/workflows/release.md - commit: ${{ github.sha }} - tag: ${{ env.SHA_SHORT }} - name: Dev Build ${{ env.SHA_SHORT }} - artifacts: "artifacts/*/*.zip" diff --git a/.github/workflows/extract_changelog.sh b/.github/workflows/extract_changelog.sh new file mode 100755 index 0000000..3d86e32 --- /dev/null +++ b/.github/workflows/extract_changelog.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# ========================================================================= +# MadScienceLab Docker Images +# ThrowTheSwitch.org +# SPDX-License-Identifier: MIT +# +# Adapted from Ceedling's .github/workflows/extract_changelog.sh +# ========================================================================= + +# Extract a version section from a Keep a Changelog formatted file. +# +# Usage: extract_changelog.sh +# +# Semver core string (e.g. 1.1.0 or 1.2.3) +# Path to the Changelog.md file +# Path to write the extracted section into +# +# Exits 0 and writes content to if the section is found. +# Exits 1 if the changelog file is missing or the version section is absent. +# +# Version section headers are matched by the pattern "^# [VERSION]". +# Any text following the closing "]" on the header line is ignored +# (dates, labels, "Prerelease", em-dashes, hyphens, etc. are all fine). +# The extracted section does NOT include the version header line itself — +# only the body content beneath it is written to . +# +# Trailing blank lines and trailing separator tokens (e.g. "---", "
") +# left over at the end of a section (from the horizontal rule that +# separates sections in the changelog) are trimmed from the tail of the +# extracted content. Such tokens are left untouched if they appear earlier +# in the body, followed by more content. +# +# Local testing examples: +# bash extract_changelog.sh 1.0.1 ../../docs/Changelog.md /tmp/out.md && cat /tmp/out.md +# bash extract_changelog.sh 9.9.9 ../../docs/Changelog.md /tmp/out.md; echo "exit: $?" + +set -euo pipefail + +SEMVER="${1:?version argument required}" +CHANGELOG="${2:?changelog path argument required}" +OUTPUT_FILE="${3:?output file path argument required}" + +if [ ! -f "$CHANGELOG" ]; then + echo "Changelog not found at '${CHANGELOG}'; release notes will use GitHub default." + exit 1 +fi + +# Extract from the matching "# [VERSION]" header to just before the next "# [" header. +# +# Dots in the version string are escaped in the awk BEGIN block so that e.g. +# 1.1.0 matches literally rather than treating "." as a regex wildcard. +awk -v ver="${SEMVER}" ' + BEGIN { gsub(/\./, "\\.", ver); pat = "^# \\[" ver "\\]" } + $0 ~ pat { found=1; next } + found && /^# \[/ { exit } + found { print } +' "$CHANGELOG" > "$OUTPUT_FILE" + +# Phase 2: Trim trailing blank lines and separator tokens (---,
, etc.) +# left over at the tail of the extracted section, e.g. the blank-line + +# "---" + blank-line horizontal rule that precedes the next version header +# in the changelog. Separators that appear mid-section (followed by more +# content) are left untouched. +# +# Add new trailing tokens to this pattern as needed. +TRAILING_TOKEN_PATTERN='^[[:space:]]*(---|
)[[:space:]]*$' + +lines=() +while IFS= read -r line || [ -n "$line" ]; do + lines+=("$line") +done < "$OUTPUT_FILE" + +last=$(( ${#lines[@]} - 1 )) +while [ "$last" -ge 0 ]; do + line="${lines[$last]}" + blank="${line//[[:space:]]/}" + if [ -z "$blank" ] || [[ "$line" =~ $TRAILING_TOKEN_PATTERN ]]; then + last=$(( last - 1 )) + else + break + fi +done + +if [ "$last" -ge 0 ]; then + printf '%s\n' "${lines[@]:0:$((last + 1))}" > "$OUTPUT_FILE" +else + : > "$OUTPUT_FILE" +fi + +# -s: file exists AND is non-empty +# (handles the edge case where the version header is present but has no content beneath it) +if [ -s "$OUTPUT_FILE" ]; then + echo "Changelog section found for version '${SEMVER}'." + exit 0 +else + echo "Changelog section not found for version '${SEMVER}'." + exit 1 +fi diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..0cfd603 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,139 @@ +# ========================================================================= +# MadScienceLab Docker Images +# ThrowTheSwitch.org +# SPDX-License-Identifier: MIT +# ========================================================================= + +# ----------------------------------------------------------------------- +# Pre-Release Workflow +# +# Purpose: +# Generate, lint, and validate all four Docker image variants (as in +# ci.yml), additionally performing a real multi-platform docker buildx +# build for each (no push to Docker Hub), then publish a GitHub +# pre-release with release notes extracted from docs/Changelog.md and +# the generated Dockerfile/asset artifacts attached. +# +# Trigger: +# Push of a tag matching: v*.*.*-pre.* (e.g. v1.1.2-pre.1, v1.1.2a-pre.1) +# +# Tag convention: +# v1.1.2 full release, no image-only revision +# v1.1.2a full release, lowercase letter = image-only revision +# v1.1.2a-pre.1 pre-release of the above; -pre.N always comes last +# +# Changelog lookup: +# The docs/Changelog.md section header must match the tag's version +# string verbatim (after stripping the leading 'v' and any trailing +# '-pre.N'), letter suffix included, e.g. tag v1.1.2a-pre.1 looks up +# header "# [1.1.2a]". If no matching section is found, GitHub's +# auto-generated release notes are used instead. +# +# Related workflows: +# ci.yml — runs on every push/PR; validation only, no build +# release.yml — triggered by full release tags (v*.*.*, no suffix); +# also pushes to Docker Hub +# ----------------------------------------------------------------------- + +name: Pre-Release + +on: + push: + tags: + - 'v*.*.*-pre.*' + +permissions: + contents: write + +jobs: + build: + name: "Build ${{ matrix.image_name }}" + strategy: + fail-fast: false + matrix: + include: + - image_name: madsciencelab + image_dir: build/standard + dir_args: '--dir build/standard' + - image_name: madsciencelab-plugins + image_dir: build/plugins + dir_args: '--dir build/standard --dir build/plugins' + - image_name: madsciencelab-arm-none-eabi + image_dir: build/arm-none-eabi + dir_args: '--dir build/arm-none-eabi' + - image_name: madsciencelab-arm-none-eabi-plugins + image_dir: build/arm-none-eabi-plugins + dir_args: '--dir build/arm-none-eabi --dir build/plugins --dir build/arm-none-eabi-plugins' + uses: ./.github/workflows/_build-variant.yml + with: + image_name: ${{ matrix.image_name }} + image_dir: ${{ matrix.image_dir }} + dir_args: ${{ matrix.dir_args }} + version: ${{ github.ref_name }} + build: true + push: false + secrets: inherit + + publish: + name: "Publish Pre-Release" + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Download all variant artifacts' + uses: actions/download-artifact@v4 + with: + # `pattern:` is a workaround to an artifact upload incompatibility with docker/build-push-action@v6 + # https://github.com/docker/build-push-action/issues/1167 + pattern: "madsciencelab*" + path: artifacts + + # Strip the leading 'v' then take only the version core (including + # any letter suffix), discarding the '-pre.N' marker, e.g. + # v1.1.2a-pre.1 → 1.1.2a. Changelog entries are keyed on this string. + - name: 'Extract Changelog Section for Release Notes' + id: changelog + shell: bash + run: | + SEMVER="${GITHUB_REF_NAME#v}" + SEMVER="${SEMVER%%-*}" + BODY_FILE="${RUNNER_TEMP}/release_body.md" + + echo "found=false" >> "$GITHUB_OUTPUT" + + if .github/workflows/extract_changelog.sh "${SEMVER}" "docs/Changelog.md" "${BODY_FILE}"; then + EOF_MARKER="RELEASE_BODY_EOF_$(date +%s)" + echo "body<<${EOF_MARKER}" >> "$GITHUB_OUTPUT" + cat "${BODY_FILE}" >> "$GITHUB_OUTPUT" + echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT" + echo "found=true" >> "$GITHUB_OUTPUT" + fi + + - name: 'Publish GitHub Pre-Release (changelog body)' + if: steps.changelog.outputs.found == 'true' + uses: ncipollo/release-action@v1 + with: + prerelease: true + allowUpdates: true + tag: ${{ github.ref_name }} + commit: ${{ github.sha }} + name: ${{ github.ref_name }} + body: ${{ steps.changelog.outputs.body }} + artifacts: "artifacts/*/*.zip" + + - name: 'Publish GitHub Pre-Release (auto-generated notes)' + if: steps.changelog.outputs.found != 'true' + uses: ncipollo/release-action@v1 + with: + prerelease: true + allowUpdates: true + tag: ${{ github.ref_name }} + commit: ${{ github.sha }} + name: ${{ github.ref_name }} + generateReleaseNotes: true + artifacts: "artifacts/*/*.zip" diff --git a/.github/workflows/release.md b/.github/workflows/release.md deleted file mode 100644 index 962fdf5..0000000 --- a/.github/workflows/release.md +++ /dev/null @@ -1,39 +0,0 @@ -## MadScienceLab Docker Images Builds - -Each build produces multiple variants of the `throwtheswitch/madsciencelab` Docker images containing [Ceedling](https://github.com/ThrowTheSwitch/Ceedling) and its supporting frameworks as well as various utilities and compilation toolchains. Each image built from this repository targets multiple runtime host platforms. - -Build types (via Github Actions): - -1. A dev build of this repository generates files and validates the Docker image build. -1. A release build adds to (1) by also pushing the resulting Docker images to Docker Hub. - -See the [Docker Hub repository](https://hub.docker.com/r/throwtheswitch) for official releases of the resulting Docker images and their documentation. - -Versioning of this repository and the resulting tags in Docker Hub tracks Ceedling’s version. Docker image changes are maintained with a lowercase letter suffix appended to the version of Ceedling contained in each image itself. - -## Build Artifacts - -* A zip archive for each Docker image containing the generated Dockerfile and any other generated file artifacts used to build the image in Docker Hub. -* A zip archive of the entire project including the static assets used to build the Docker images. - -See this reository’s documentation for instructions on how to use the tools of this repository and how to manually build the Docker images this repository maintains. - -## Changelog - -### Added - -* ... - -### Fixed - -* ... - -### Changed - -* ... - -### Removed - -* ... - - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e87bf3..2ac9539 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,349 +1,144 @@ -name: Docker images release build + Docker Hub push +# ========================================================================= +# MadScienceLab Docker Images +# ThrowTheSwitch.org +# SPDX-License-Identifier: MIT +# ========================================================================= + +# ----------------------------------------------------------------------- +# Release Workflow +# +# Purpose: +# Generate, lint, and validate all four Docker image variants (as in +# ci.yml), additionally performing a real multi-platform docker buildx +# build for each and pushing it to Docker Hub, then publish a full +# GitHub release with release notes extracted from docs/Changelog.md +# and the generated Dockerfile/asset artifacts attached. +# +# Trigger: +# Push of a tag matching: v*.*.* (e.g. v1.1.2, v1.1.2a) +# +# Important: the v*.*.* glob also matches pre-release tags +# (e.g. v1.1.2a-pre.1). The 'if' guard on the publish job provides a +# second line of defense: any tag containing a hyphen is rejected and +# the publish job is skipped, so only clean version tags (no -pre.N +# suffix) produce a full release. +# +# Tag convention: +# v1.1.2 full release, no image-only revision +# v1.1.2a full release, lowercase letter = image-only revision +# v1.1.2a-pre.1 pre-release of the above (handled by prerelease.yml) +# +# Changelog lookup: +# The docs/Changelog.md section header must match the tag's version +# string verbatim (after stripping the leading 'v'), letter suffix +# included, e.g. tag v1.1.2a looks up header "# [1.1.2a]". If no +# matching section is found, GitHub's auto-generated release notes are +# used instead. +# +# Related workflows: +# ci.yml — runs on every push/PR; validation only, no build +# prerelease.yml — triggered by pre-release tags (v*.*.*-pre.*); also +# performs a real multi-platform build, but no push +# ----------------------------------------------------------------------- + +name: Release on: - workflow_dispatch: push: tags: - - '*' - -env: - IMAGE_ORG: 'throwtheswitch' - IMAGE_BASE_NAME: 'madsciencelab' + - 'v*.*.*' + - '!v*.*.*-*' # Exclude pre-release tags (e.g. v1.1.2-pre.1, v1.1.2a-pre.1) +permissions: + contents: write jobs: - # Jobs organized for concurrent Docker image builds - # Jobs build tagged and :latest images and push to Docker Hub - - # This is a workaround to the limitation that an `env:` map cannot reference the `env:` map to create additional environment variables - image-details: - runs-on: ubuntu-latest - steps: - # A step must be present in order to make use of `outputs:` - - run: echo "Setting Docker image details..." - outputs: - base-name: 'madsciencelab' - base-url: 'throwtheswitch/madsciencelab' - - madsciencelab: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }} - IMAGE_URL: ${{ needs.image-details.outputs.base-url }} - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/standard - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - id: file-gen - run: bash build.sh --dir ${{ env.IMAGE_DIR }} --version ${{ github.ref_name }} - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab - # Note: standard/ directory maps to madsciencelab image (no variants) - - name: 'Build and push Docker image ${{ env.IMAGE_NAME }}:${{ github.ref_name }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ env.IMAGE_URL }}:${{ github.ref_name }}, ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-plugins: + build: + name: "Build ${{ matrix.image_name }}" + strategy: + fail-fast: false + matrix: + include: + - image_name: madsciencelab + image_dir: build/standard + dir_args: '--dir build/standard' + - image_name: madsciencelab-plugins + image_dir: build/plugins + dir_args: '--dir build/standard --dir build/plugins' + - image_name: madsciencelab-arm-none-eabi + image_dir: build/arm-none-eabi + dir_args: '--dir build/arm-none-eabi' + - image_name: madsciencelab-arm-none-eabi-plugins + image_dir: build/arm-none-eabi-plugins + dir_args: '--dir build/arm-none-eabi --dir build/plugins --dir build/arm-none-eabi-plugins' + uses: ./.github/workflows/_build-variant.yml + with: + image_name: ${{ matrix.image_name }} + image_dir: ${{ matrix.image_dir }} + dir_args: ${{ matrix.dir_args }} + version: ${{ github.ref_name }} + build: true + push: true + secrets: inherit + + publish: + name: "Publish Release" + needs: build + # Rejects tags that contain a hyphen (e.g. v1.1.2a-pre.1), which would + # otherwise match the v*.*.* trigger glob and create a spurious full + # release instead of leaving it to prerelease.yml + if: ${{ !contains(github.ref_name, '-') }} runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-plugins - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-plugins - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/plugins - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir build/standard --dir ${{ env.IMAGE_DIR }} --version ${{ github.ref_name }} - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-plugins - - name: 'Build and push Docker image ${{ env.IMAGE_NAME }}:${{ github.ref_name }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ env.IMAGE_URL }}:${{ github.ref_name }}, ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-arm-none-eabi: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-arm-none-eabi - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-arm-none-eabi - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/arm-none-eabi - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir ${{ env.IMAGE_DIR }} --version ${{ github.ref_name }} - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-arm-none-eabi - - name: 'Build and push Docker image ${{ env.IMAGE_NAME }}:${{ github.ref_name }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ env.IMAGE_URL }}:${{ github.ref_name }}, ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - madsciencelab-arm-none-eabi-plugins: - runs-on: ubuntu-latest - needs: [image-details] - permissions: - contents: read - packages: write - attestations: write - id-token: write - env: - IMAGE_NAME: ${{ needs.image-details.outputs.base-name }}-arm-none-eabi-plugins - IMAGE_URL: ${{ needs.image-details.outputs.base-url }}-arm-none-eabi-plugins - # Image variant name is drawn from final `--dir` entry in file generation command line - IMAGE_DIR: build/arm-none-eabi-plugins - - steps: - - name: 'Set up Ruby for generation tool' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Install file generation Ruby dependencies' - run: bundle install - - - name: 'Run file generation' - run: bash build.sh --dir build/arm-none-eabi --dir build/plugins --dir ${{ env.IMAGE_DIR }} --version ${{ github.ref_name }} - - - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v3 - - - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v3 - - - name: 'Login to Docker Hub' - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Docker image: madsciencelab-arm-none-eabi-plugins - - name: 'Build and push Docker image ${{ env.IMAGE_NAME }}:${{ github.ref_name }}' - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: ${{ env.IMAGE_DIR }}/docker/Dockerfile - build-args: | - CONTAINER_VERSION=${{ github.ref_name }} - IMAGE_NAME=${{ env.IMAGE_NAME }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ env.IMAGE_URL }}:${{ github.ref_name }}, ${{ env.IMAGE_URL }}:latest - # Connect Docker driver to GitHub Action cache service - cache-from: type=gha - cache-to: type=gha,mode=max - - # Zip generated files as a single artifact - - name: 'Archive ${{ env.IMAGE_NAME }} generated files as a single artifact' - run: zip -j ${{ env.IMAGE_NAME }}.zip ${{ env.IMAGE_DIR }}/docker/Dockerfile ${{ env.IMAGE_DIR }}/assets/shell/welcome - - # Upload the zip artifact - - uses: actions/upload-artifact@v4 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.IMAGE_NAME }}.zip - if-no-files-found: error - - artifacts-release: - runs-on: ubuntu-latest - needs: - - madsciencelab - - madsciencelab-plugins - - madsciencelab-arm-none-eabi - - madsciencelab-arm-none-eabi-plugins permissions: contents: write steps: - # Get the repo so we have info for generating release details - name: 'Checkout GitHub Action' uses: actions/checkout@v4 - # Download all artifacts from the 4 Docker image builds - - uses: actions/download-artifact@v4 + - name: 'Download all variant artifacts' + uses: actions/download-artifact@v4 with: # `pattern:` is a workaround to an artifact upload incompatibility with docker/build-push-action@v6 - # Otherwise, apart from this bug requiring `pattern:` the default of all artifacts would occur without any intervention # https://github.com/docker/build-push-action/issues/1167 pattern: "madsciencelab*" path: artifacts - # Capture the SHA string - - name: 'Git commit short SHA as environment variable' + # Strip the leading 'v'; the tag itself contains no -pre.N suffix + # here (guaranteed by the job-level 'if' guard above). + - name: 'Extract Changelog Section for Release Notes' + id: changelog shell: bash run: | - echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + SEMVER="${GITHUB_REF_NAME#v}" + BODY_FILE="${RUNNER_TEMP}/release_body.md" + + echo "found=false" >> "$GITHUB_OUTPUT" + + if .github/workflows/extract_changelog.sh "${SEMVER}" "docs/Changelog.md" "${BODY_FILE}"; then + EOF_MARKER="RELEASE_BODY_EOF_$(date +%s)" + echo "body<<${EOF_MARKER}" >> "$GITHUB_OUTPUT" + cat "${BODY_FILE}" >> "$GITHUB_OUTPUT" + echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT" + echo "found=true" >> "$GITHUB_OUTPUT" + fi - - uses: ncipollo/release-action@v1 + - name: 'Publish GitHub Release (changelog body)' + if: steps.changelog.outputs.found == 'true' + uses: ncipollo/release-action@v1 with: - # Defaults to using commit tag for `tag:`. - # This workflow is only triggered by tags. prerelease: false allowUpdates: true - bodyFile: .github/workflows/release.md name: ${{ github.ref_name }} + body: ${{ steps.changelog.outputs.body }} artifacts: "artifacts/*/*.zip" + - name: 'Publish GitHub Release (auto-generated notes)' + if: steps.changelog.outputs.found != 'true' + uses: ncipollo/release-action@v1 + with: + prerelease: false + allowUpdates: true + name: ${{ github.ref_name }} + generateReleaseNotes: true + artifacts: "artifacts/*/*.zip" diff --git a/README.md b/README.md index 1b65039..690c944 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,11 @@ Examples: * `./build.sh --dir build/arm-none-eabi --version 1.0.0` * `./build.sh --dir build/arm-none-eabi --dir build/plugins --dir build/arm-none-eabi-plugins --version ` -The Github Action maintained in this repository uses the same approach as the preceding to generate the Dockerfiles attached to releases. The Github Action goes on to build images from those Dockerfiles and pushes them to Docker Hub. +The Github Actions maintained in this repository use the same approach as the preceding to generate the Dockerfiles attached to workflow runs and releases: + +* Any push or pull request generates the four Dockerfile/asset variants and validates each generated Dockerfile with [hadolint](https://github.com/hadolint/hadolint) (static lint) and `docker buildx build --check` (structural validation) — no Docker image is actually built, and the generated artifacts are attached to the workflow run. +* Pushing a pre-release tag (see Versioning, below) does all of the above, plus a real multi-platform Docker image build for each variant (not pushed to Docker Hub), plus a GitHub pre-release with the generated artifacts attached and release notes drawn from [`docs/Changelog.md`](docs/Changelog.md). +* Pushing a release tag does all of the above, plus pushes the built images to Docker Hub, plus a full GitHub release. ## Ruby tool & shell script @@ -63,6 +67,14 @@ The shell script that calls the Ruby tool includes many options useful to a deve Versioning of this repository and the resulting tags in Docker Hub track [Ceedling]’s version. Docker image changes are maintained with a lowercase letter suffix appended to the version of Ceedling contained in each _MadScienceLab_ image itself. +Git tags drive the Github Actions release workflows and follow this pattern: + +* `v1.1.2` — a full release tracking Ceedling 1.1.2 with no image-only changes. +* `v1.1.2a` — a full release with an image-only revision (lowercase letter suffix) on top of Ceedling 1.1.2. +* `v1.1.2a-pre.1` — a pre-release of the above; the `-pre.N` suffix always comes last. Pushing a tag like this builds real multi-platform images (without pushing to Docker Hub) and publishes a GitHub pre-release for testing before the corresponding non-suffixed tag is pushed. + +Tags without a `-pre.N` suffix trigger a full release, including a push to Docker Hub. Only tags matching this `v`-prefixed pattern trigger release/pre-release Github Actions; all other pushes and pull requests only generate and validate the Dockerfiles (see Usage, above). + # 🔒 Security Security documentation is maintained in the [Docker Hub image repositories][hub-images]. From 5d4e74b069b5264458fc63a3e8f0f43ec3ecaa7b Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Mon, 27 Jul 2026 13:47:01 -0400 Subject: [PATCH 2/3] Add .hadolint.yaml to ignore DL3028 for generated Dockerfiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated gem install RUN commands target pre-built .gem files whose versions are pinned via Gemfile.lock at image-build time, not a live rubygems.org fetch — hadolint can't see through the glob to know that, so DL3028 fires as a false positive on every variant. Co-Authored-By: Claude Sonnet 5 --- .hadolint.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..872c0a6 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,7 @@ +ignored: + # `gem install --force --local --no-document /assets/base/gems/*.gem` installs + # pre-built .gem files whose versions are already pinned via Gemfile.lock at + # image-build time (see build/base/Dockerfile-build) — hadolint can't see + # through the glob to know that, so it flags every RUN gem install in the + # generated Dockerfiles as unpinned. Not fixable at the Dockerfile level. + - DL3028 From b17e617ab6035ef4f5e0e35a4a3e73bc63a547a7 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Mon, 27 Jul 2026 15:23:12 -0400 Subject: [PATCH 3/3] Grant packages/attestations/id-token permissions to prerelease/release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _build-variant.yml's job requests attestations:write, id-token:write, and packages:write (for the real multi-platform docker build). A reusable workflow's job can't request more permissions than its caller grants, and declaring any permissions: block at all caps everything unlisted at none — so prerelease.yml's contents-only permissions block was silently capping these to none and causing a startup_failure with zero jobs (discovered by actually pushing a v0.0.1-pre.1 tag). ci.yml never hit this because it has no permissions: block and inherits the default token scope. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/prerelease.yml | 9 +++++++++ .github/workflows/release.yml | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0cfd603..19214b0 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -42,8 +42,17 @@ on: tags: - 'v*.*.*-pre.*' +# contents: write is required to create and upload GitHub releases. +# packages/attestations/id-token: write are required because the called +# _build-variant.yml reusable workflow's job requests them (for the real +# multi-platform build here) — a reusable workflow's job can't request +# more permissions than its caller grants, so these must be listed here +# even though this workflow's own `publish` job only needs contents:write. permissions: contents: write + packages: write + attestations: write + id-token: write jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ac9539..427070a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,8 +49,18 @@ on: - 'v*.*.*' - '!v*.*.*-*' # Exclude pre-release tags (e.g. v1.1.2-pre.1, v1.1.2a-pre.1) +# contents: write is required to create and upload GitHub releases. +# packages/attestations/id-token: write are required because the called +# _build-variant.yml reusable workflow's job requests them (for the real +# multi-platform build + push here) — a reusable workflow's job can't +# request more permissions than its caller grants, so these must be listed +# here even though this workflow's own `publish` job only needs +# contents:write. permissions: contents: write + packages: write + attestations: write + id-token: write jobs: build: