From 182a572ec20e43b0d527c2c4debca7f67b76b495 Mon Sep 17 00:00:00 2001 From: Auto Contents Date: Sat, 25 Jul 2026 22:29:52 +0000 Subject: [PATCH] Updating standard content files for repository --- .github/actions/build/action.yml | 57 +++++ .../actions/collect_publishables/action.yml | 101 +++++++++ .github/actions/initialize_ci/action.yml | 113 ++++++++++ .github/actions/publish_docker/action.yml | 99 +++++++++ .../actions/publish_github_release/action.yml | 52 +++++ .github/actions/publish_nuget/action.yml | 60 +++++ .github/readme.md | 14 ++ .github/workflows/ci.yml | 205 ++++++++++++++++++ .github/workflows/deploy.yml | 36 +++ .github/workflows/dotnet.yml | 55 ----- .github/workflows/nuget.yml | 36 --- .standard_content.json | 22 +- Directory.Build.props | 4 +- 13 files changed, 754 insertions(+), 100 deletions(-) create mode 100644 .github/actions/build/action.yml create mode 100644 .github/actions/collect_publishables/action.yml create mode 100644 .github/actions/initialize_ci/action.yml create mode 100644 .github/actions/publish_docker/action.yml create mode 100644 .github/actions/publish_github_release/action.yml create mode 100644 .github/actions/publish_nuget/action.yml create mode 100644 .github/readme.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/dotnet.yml delete mode 100644 .github/workflows/nuget.yml diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..75c748f --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,57 @@ +# This file is centrally managed; local edits may be lost. + +name: Standard .NET build +description: Build, test, and package a conventional .NET repository without publishing + +# Contract: +# - CI owns the fixed .artifacts layout; this action deliberately has no path inputs. +# - The repository must be buildable from its root with the dotnet CLI. +# - .ci-skip-tests remains a temporary escape hatch for repositories without tests. +# - Effective IsPackable projects write packages to the fixed NuGet directory. +# - This action never receives credentials and never publishes anything. + +runs: + using: composite + steps: + - name: Set up .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: 10.0.x + + - name: Restore + shell: bash + run: dotnet restore + + - name: Build + shell: bash + run: | + set -euo pipefail + version_args=() + if [ -n "${CI_VERSION:-}" ]; then + version_args+=("-p:Version=$CI_VERSION") + fi + + dotnet build --configuration Release --no-restore "${version_args[@]}" + + - name: Test + if: hashFiles('.ci-skip-tests') == '' + shell: bash + run: dotnet test --configuration Release --no-build --verbosity normal + + - name: Pack effective IsPackable projects + if: env.CI_PUBLISH_TARGET != 'none' + shell: bash + run: | + set -euo pipefail + mkdir -p .artifacts/nuget + + version_args=() + if [ -n "${CI_VERSION:-}" ]; then + version_args+=("-p:Version=$CI_VERSION") + fi + + dotnet pack \ + --configuration Release \ + --no-build \ + --output .artifacts/nuget \ + "${version_args[@]}" diff --git a/.github/actions/collect_publishables/action.yml b/.github/actions/collect_publishables/action.yml new file mode 100644 index 0000000..65a9f1b --- /dev/null +++ b/.github/actions/collect_publishables/action.yml @@ -0,0 +1,101 @@ +# This file is centrally managed; local edits may be lost. + +name: Collect publishable outputs +description: Detect package and Docker outputs without publishing + +# Contract: +# - Package presence, not project inspection, selects package publication. +# - Dockerfile presence, not repository configuration, selects Docker publication. +# - This action never builds Docker images and never receives credentials. + +outputs: + has-packages: + value: ${{ steps.packages.outputs.has-packages }} + has-docker: + value: ${{ steps.docker.outputs.has-docker }} + +runs: + using: composite + steps: + - name: Detect package artifacts + id: packages + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + packages=(.artifacts/nuget/*.nupkg) + has_packages=false + if [ "${#packages[@]}" -gt 0 ]; then + has_packages=true + fi + + echo "has-packages=$has_packages" >> "$GITHUB_OUTPUT" + + - name: Discover Dockerfiles + id: docker + shell: bash + run: | + set -euo pipefail + + metadata=.artifacts/docker-images.json + printf '[]\n' > "$metadata" + + mapfile -d '' dockerfiles < <( + find . -type f \( -name Dockerfile -o -name 'Dockerfile.*' \) \ + -not -path './.git/*' \ + -not -path './.artifacts/*' \ + -not -path '*/node_modules/*' \ + -not -path '*/bin/*' \ + -not -path '*/obj/*' \ + -print0 \ + | sort -z + ) + + if [ "${#dockerfiles[@]}" -eq 0 ]; then + echo 'has-docker=false' >> "$GITHUB_OUTPUT" + exit 0 + fi + + configured_dockerfile='' + configured_image='' + configured_platforms='' + if [ -f .env ]; then + configured_dockerfile="$(sed -nE 's/^DOCKER_DOCKERFILE=(.+)\r?$/\1/p' .env | tail -n1)" + configured_image="$(sed -nE 's/^DOCKER_TAG=(.+)\r?$/\1/p' .env | tail -n1)" + configured_platforms="$(sed -nE 's/^DOCKER_PLATFORMS=(.+)\r?$/\1/p' .env | tail -n1)" + fi + + repository_image="ghcr.io/${GITHUB_REPOSITORY,,}" + for dockerfile_path in "${dockerfiles[@]}"; do + dockerfile="${dockerfile_path#./}" + if [ -n "$configured_dockerfile" ] && [ -n "$configured_image" ] && [ "$dockerfile" = "$configured_dockerfile" ]; then + image="$configured_image" + platforms="${configured_platforms:-linux/amd64}" + else + filename="$(basename "$dockerfile")" + suffix='' + if [[ "$filename" == Dockerfile.* ]]; then + suffix="-${filename#Dockerfile.}" + elif [ "${#dockerfiles[@]}" -gt 1 ]; then + directory="$(dirname "$dockerfile")" + if [ "$directory" != . ]; then + suffix="-$(printf '%s' "$directory" | tr '/_' '--')" + fi + fi + + image="${repository_image}${suffix,,}" + platforms=linux/amd64 + fi + + jq \ + --arg dockerfile "$dockerfile" \ + --arg image "$image" \ + --arg platforms "$platforms" \ + '. + [{dockerfile:$dockerfile,context:".",image:$image,platforms:$platforms}]' \ + "$metadata" > "$metadata.next" + mv "$metadata.next" "$metadata" + done + + echo 'has-docker=true' >> "$GITHUB_OUTPUT" + jq -r '.[] | "Docker: \(.dockerfile) -> \(.image) [\(.platforms)]"' "$metadata" diff --git a/.github/actions/initialize_ci/action.yml b/.github/actions/initialize_ci/action.yml new file mode 100644 index 0000000..e084711 --- /dev/null +++ b/.github/actions/initialize_ci/action.yml @@ -0,0 +1,113 @@ +# This file is centrally managed; local edits may be lost. + +name: Initialize CI +description: Resolve the trusted ref context and prepare the fixed CI artifact layout + +# Contract: +# - This is the sole interpreter of branch and version semantics. +# - It exports CI_* variables for later steps and matching action outputs for jobs. +# - It owns the fixed .artifacts layout and provenance schema. + +outputs: + publish-target: + value: ${{ steps.context.outputs.publish-target }} + release-tag: + value: ${{ steps.context.outputs.release-tag }} + version: + value: ${{ steps.context.outputs.version }} + is-release: + value: ${{ steps.context.outputs.is-release }} + is-prerelease: + value: ${{ steps.context.outputs.is-prerelease }} + is-plain-version: + value: ${{ steps.context.outputs.is-plain-version }} + +runs: + using: composite + steps: + - name: Resolve ref and version + id: context + shell: bash + env: + CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + CI_EVENT_NAME: ${{ github.event_name }} + CI_REPOSITORY: ${{ github.repository }} + CI_RUN_ID: ${{ github.run_id }} + CI_RUN_ATTEMPT: ${{ github.run_attempt }} + run: | + set -euo pipefail + + is_release=false + is_prerelease=false + is_plain_version=false + release_tag='' + version='' + if [[ "$GITHUB_REF" == refs/tags/* ]] && printf '%s' "$GITHUB_REF_NAME" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then + is_release=true + release_tag="$GITHUB_REF_NAME" + version="${GITHUB_REF_NAME#v}" + if [[ "$version" == *-* ]]; then + is_prerelease=true + fi + if printf '%s' "$release_tag" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + is_plain_version=true + fi + fi + + publish_target=none + if [ "$CI_EVENT_NAME" = push ] && [ "$GITHUB_REF" = "refs/heads/$CI_DEFAULT_BRANCH" ]; then + publish_target=development + elif [ "$CI_EVENT_NAME" = push ] && [ "$is_release" = true ]; then + publish_target=release + fi + + mkdir -p .artifacts/nuget .artifacts/release .artifacts/deploy + + jq -n \ + --arg repository "$CI_REPOSITORY" \ + --arg event "$CI_EVENT_NAME" \ + --arg sha "$GITHUB_SHA" \ + --arg ref "$GITHUB_REF" \ + --arg ref_name "$GITHUB_REF_NAME" \ + --arg default_branch "$CI_DEFAULT_BRANCH" \ + --arg release_tag "$release_tag" \ + --arg version "$version" \ + --argjson run_id "$CI_RUN_ID" \ + --argjson run_attempt "$CI_RUN_ATTEMPT" \ + --argjson is_release "$is_release" \ + --argjson is_prerelease "$is_prerelease" \ + --argjson is_plain_version "$is_plain_version" \ + '{ + schemaVersion: 1, + repository: $repository, + event: $event, + sha: $sha, + ref: $ref, + refName: $ref_name, + defaultBranch: $default_branch, + runId: $run_id, + runAttempt: $run_attempt, + releaseTag: $release_tag, + version: $version, + isRelease: $is_release, + isPrerelease: $is_prerelease, + isPlainVersion: $is_plain_version + }' > .artifacts/ci-metadata.json + + { + echo "CI_PUBLISH_TARGET=$publish_target" + echo "CI_RELEASE_TAG=$release_tag" + echo "CI_VERSION=$version" + echo "CI_IS_RELEASE=$is_release" + echo "CI_IS_PRERELEASE=$is_prerelease" + echo "CI_IS_PLAIN_VERSION=$is_plain_version" + } >> "$GITHUB_ENV" + + { + echo "publish-target=$publish_target" + echo "release-tag=$release_tag" + echo "version=$version" + echo "is-release=$is_release" + echo "is-prerelease=$is_prerelease" + echo "is-plain-version=$is_plain_version" + } >> "$GITHUB_OUTPUT" diff --git a/.github/actions/publish_docker/action.yml b/.github/actions/publish_docker/action.yml new file mode 100644 index 0000000..adcc1c3 --- /dev/null +++ b/.github/actions/publish_docker/action.yml @@ -0,0 +1,99 @@ +# This file is centrally managed; local edits may be lost. + +name: Publish Docker images +description: Build and publish every Docker image described by CI metadata + +# Contract: +# - Image definitions are read only from .artifacts/docker-images.json. +# - Each image is built exactly once in this publishing job. +# - Development refs publish :dev; releases publish : and stable :latest. +# - Every build receives BUILD_VERSION=dev or the normalized release version. +# - Registry credentials are supplied only by the invoking publishing job. + +inputs: + publish-target: + description: CI publish target (development or release) + required: true + version: + description: Normalized release version, empty for development + required: false + default: '' + is-prerelease: + description: Whether the release version is a prerelease + required: true + +runs: + using: composite + steps: + - name: Detect required registries + id: registries + shell: bash + run: | + set -euo pipefail + + has_ghcr="$(jq 'any(.[]; .image | startswith("ghcr.io/"))' .artifacts/docker-images.json)" + has_docker_hub="$(jq 'any(.[]; (.image | startswith("ghcr.io/") | not))' .artifacts/docker-images.json)" + { + echo "has-ghcr=$has_ghcr" + echo "has-docker-hub=$has_docker_hub" + } >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: steps.registries.outputs.has-ghcr == 'true' + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ env.DOCKER_GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: steps.registries.outputs.has-docker-hub == 'true' + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_KEY }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + + - name: Build and publish Docker images + shell: bash + env: + PUBLISH_TARGET: ${{ inputs.publish-target }} + VERSION: ${{ inputs.version }} + IS_PRERELEASE: ${{ inputs.is-prerelease }} + run: | + set -euo pipefail + + while IFS= read -r image_json; do + dockerfile="$(jq -r '.dockerfile' <<< "$image_json")" + context="$(jq -r '.context' <<< "$image_json")" + image="$(jq -r '.image' <<< "$image_json")" + platforms="$(jq -r '.platforms' <<< "$image_json")" + cache_scope="$(printf '%s' "$image" | tr '/:.' '---')" + + tag_args=() + if [ "$PUBLISH_TARGET" = development ]; then + build_version=dev + tag_args+=(--tag "$image:dev") + else + build_version="$VERSION" + tag_args+=(--tag "$image:$VERSION") + if [ "$IS_PRERELEASE" = false ]; then + tag_args+=(--tag "$image:latest") + fi + fi + + docker buildx build \ + --file "$dockerfile" \ + --platform "$platforms" \ + "${tag_args[@]}" \ + --build-arg "BUILD_VERSION=$build_version" \ + --cache-from "type=gha,scope=$cache_scope" \ + --cache-to "type=gha,mode=max,scope=$cache_scope" \ + --push \ + "$context" + done < <(jq -c '.[]' .artifacts/docker-images.json) diff --git a/.github/actions/publish_github_release/action.yml b/.github/actions/publish_github_release/action.yml new file mode 100644 index 0000000..edb133d --- /dev/null +++ b/.github/actions/publish_github_release/action.yml @@ -0,0 +1,52 @@ +# This file is centrally managed; local edits may be lost. + +name: Publish GitHub Release +description: Idempotently create a GitHub Release and upload collected attachments + +# Contract: +# - CI invokes this only for a validated stable or prerelease SemVer tag. +# - Attachments are read only from .artifacts/release. +# - The GitHub token is supplied only by the invoking release job. + +inputs: + release-tag: + description: Original release tag, including any v prefix + required: true + is-prerelease: + description: Whether GitHub should mark the release as a prerelease + required: true + +runs: + using: composite + steps: + - name: Create or update GitHub Release + shell: bash + env: + GH_TOKEN: ${{ env.GITHUB_RELEASE_TOKEN }} + RELEASE_TAG: ${{ inputs.release-tag }} + IS_PRERELEASE: ${{ inputs.is-prerelease }} + run: | + set -euo pipefail + + if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + create_args=( + "$RELEASE_TAG" + --verify-tag + --generate-notes + --title "$RELEASE_TAG" + ) + if [ "$IS_PRERELEASE" = true ]; then + create_args+=(--prerelease) + fi + + gh release create "${create_args[@]}" + else + echo "GitHub Release $RELEASE_TAG already exists; refreshing attachments." + fi + + mapfile -d '' release_files < <(find .artifacts/release -type f -print0 | sort -z) + if [ "${#release_files[@]}" -gt 0 ]; then + gh release upload "$RELEASE_TAG" "${release_files[@]}" --clobber + else + echo 'No GitHub Release attachments were produced.' + fi diff --git a/.github/actions/publish_nuget/action.yml b/.github/actions/publish_nuget/action.yml new file mode 100644 index 0000000..b6dd35b --- /dev/null +++ b/.github/actions/publish_nuget/action.yml @@ -0,0 +1,60 @@ +# This file is centrally managed; local edits may be lost. + +name: Publish NuGet packages +description: Publish collected NuGet packages to the registries selected by CI + +# Contract: +# - Packages are read only from .artifacts/nuget. +# - All publishable refs go to GitHub Packages. +# - Release refs additionally go to NuGet.org. +# - Registry credentials are supplied only by the invoking publishing job. + +inputs: + publish-target: + description: CI publish target (development or release) + required: true + +runs: + using: composite + steps: + - name: Set up .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: 10.0.x + + - name: Publish NuGet packages to GitHub Packages + shell: bash + env: + PACKAGE_TOKEN: ${{ env.NUGET_GITHUB_TOKEN }} + run: | + set -euo pipefail + shopt -s nullglob + + for package in .artifacts/nuget/*.nupkg; do + dotnet nuget push "$package" \ + --no-symbols \ + --skip-duplicate \ + --api-key "$PACKAGE_TOKEN" \ + --source "https://nuget.pkg.github.com/${GITHUB_REPOSITORY_OWNER}/index.json" + done + + - name: Publish release packages to NuGet.org + if: inputs.publish-target == 'release' + shell: bash + env: + NUGET_KEY: ${{ env.NUGET_ORG_KEY }} + run: | + set -euo pipefail + shopt -s nullglob + + if [ -z "$NUGET_KEY" ]; then + echo '::error::NUGET_KEY is required for tagged package publication.' + exit 1 + fi + + for package in .artifacts/nuget/*.nupkg; do + dotnet nuget push "$package" \ + --skip-duplicate \ + --api-key "$NUGET_KEY" \ + --source https://api.nuget.org/v3/index.json + done diff --git a/.github/readme.md b/.github/readme.md new file mode 100644 index 0000000..5235f32 --- /dev/null +++ b/.github/readme.md @@ -0,0 +1,14 @@ +# Continuous integration + +The CI files in this directory are centrally managed from `MBW.Tool.GithubConfig`. Changes made directly to managed files may be overwritten the next time standard content is applied. Repository-specific behavior is kept in local composite actions so the repository can still describe how it builds and publishes without adding more event-triggered workflows. + +The build action may be centrally managed for repositories using a standard build, or maintained by the repository when custom tooling is required. The deploy workflow may likewise be a centrally managed no-op or a repository-specific post-release implementation. + +- `workflows/ci.yml` — orchestrates build, test, packaging, publishing, and release jobs in one workflow run. +- `workflows/deploy.yml` — performs optional repository-specific work after a successful SemVer release. +- `actions/initialize_ci/action.yml` — resolves the ref, version, and eligible publishing target once. +- `actions/build/action.yml` — builds, tests, and produces artifacts; this file may be centrally managed. +- `actions/collect_publishables/action.yml` — detects packages, Dockerfiles, and release assets. +- `actions/publish_nuget/action.yml` — publishes collected NuGet packages. +- `actions/publish_docker/action.yml` — builds and publishes discovered Docker images. +- `actions/publish_github_release/action.yml` — creates or updates a GitHub Release and uploads its assets. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..13074c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,205 @@ +# This file is centrally managed; local edits may be lost. + +name: CI + +# Contract: +# - This is the only event-triggered managed workflow. +# - Every active run builds and tests exactly once through the repository action. +# - Feature branches, pull requests, and manual runs allocate no publishing jobs. +# - Publishing jobs use trusted GitHub ref guards and receive only their own secrets. +# - Implementation lives in explicit local actions and the reusable deploy workflow. +# - This file describes orchestration and contains no repository-specific behavior. + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + +jobs: + build: + name: Build and test + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + outputs: + has-packages: ${{ steps.publishables.outputs.has-packages }} + has-docker: ${{ steps.publishables.outputs.has-docker }} + publish-target: ${{ steps.context.outputs.publish-target }} + is-prerelease: ${{ steps.context.outputs.is-prerelease }} + is-plain-version: ${{ steps.context.outputs.is-plain-version }} + release-tag: ${{ steps.context.outputs.release-tag }} + version: ${{ steps.context.outputs.version }} + + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Initialize CI context + id: context + uses: ./.github/actions/initialize_ci + + - name: Run repository build + uses: ./.github/actions/build + env: + CI_PUBLISH_TARGET: ${{ steps.context.outputs.publish-target }} + CI_VERSION: ${{ steps.context.outputs.version }} + + - name: Collect publishable outputs + id: publishables + if: steps.context.outputs.publish-target != 'none' + uses: ./.github/actions/collect_publishables + + - name: Upload CI artifacts + if: >- + steps.context.outputs.publish-target == 'release' || + steps.publishables.outputs.has-packages == 'true' || + steps.publishables.outputs.has-docker == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ci-artifacts + path: .artifacts + include-hidden-files: true + if-no-files-found: error + + publish_nuget: + name: Publish NuGet packages + needs: build + if: >- + needs.build.result == 'success' && + github.event_name == 'push' && + (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/')) && + needs.build.outputs.has-packages == 'true' && + needs.build.outputs.publish-target != 'none' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Download CI artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ci-artifacts + path: .artifacts + + - name: Publish collected NuGet packages + uses: ./.github/actions/publish_nuget + with: + publish-target: ${{ needs.build.outputs.publish-target }} + env: + NUGET_GITHUB_TOKEN: ${{ github.token }} + NUGET_ORG_KEY: ${{ secrets.NUGET_KEY }} + + publish_docker: + name: Publish Docker images + needs: build + if: >- + needs.build.result == 'success' && + github.event_name == 'push' && + (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/')) && + needs.build.outputs.has-docker == 'true' && + needs.build.outputs.publish-target != 'none' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Download CI artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ci-artifacts + path: .artifacts + + - name: Publish discovered Docker images + uses: ./.github/actions/publish_docker + with: + publish-target: ${{ needs.build.outputs.publish-target }} + version: ${{ needs.build.outputs.version }} + is-prerelease: ${{ needs.build.outputs.is-prerelease }} + env: + DOCKER_GITHUB_TOKEN: ${{ github.token }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_KEY: ${{ secrets.DOCKER_KEY }} + + publish_github_release: + name: Publish GitHub Release + needs: + - build + - publish_nuget + - publish_docker + if: >- + always() && + needs.build.result == 'success' && + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/') && + needs.build.outputs.publish-target == 'release' && + (needs.publish_nuget.result == 'success' || needs.publish_nuget.result == 'skipped') && + (needs.publish_docker.result == 'success' || needs.publish_docker.result == 'skipped') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Download CI artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ci-artifacts + path: .artifacts + + - name: Publish GitHub Release + uses: ./.github/actions/publish_github_release + with: + release-tag: ${{ needs.build.outputs.release-tag }} + is-prerelease: ${{ needs.build.outputs.is-prerelease }} + env: + GITHUB_RELEASE_TOKEN: ${{ github.token }} + + deploy: + name: Deploy + needs: + - build + - publish_github_release + if: >- + always() && + needs.build.result == 'success' && + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/') && + needs.build.outputs.publish-target == 'release' && + needs.publish_github_release.result == 'success' + uses: ./.github/workflows/deploy.yml + with: + release-tag: ${{ needs.build.outputs.release-tag }} + version: ${{ needs.build.outputs.version }} + is-prerelease: ${{ needs.build.outputs.is-prerelease }} + is-plain-version: ${{ needs.build.outputs.is-plain-version }} + secrets: inherit + permissions: + contents: read diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3c61b02 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,36 @@ +# This file is centrally managed; local edits may be lost. + +name: No deployment + +# Contract: +# - CI calls this after all applicable SemVer-tag publishing succeeds. +# - Repositories without post-release work use this specialization. +# - Its skipped job allocates no runner and keeps the parent CI successful. + +on: + workflow_call: + inputs: + release-tag: + description: Original release tag, including any v prefix + required: true + type: string + version: + description: Normalized SemVer version + required: true + type: string + is-prerelease: + description: Whether the version is a prerelease + required: true + type: string + is-plain-version: + description: Whether the tag is an exact stable numeric version + required: true + type: string + +jobs: + no_deployment: + name: No deployment configured + if: ${{ false }} + runs-on: ubuntu-latest + steps: + - run: ':' diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index 6e1c328..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Generic Build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -env: - DOTNET_CLI_TELEMETRY_OPTOUT: true - DOTNET_NOLOGO: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 50 - - run: git fetch --tags - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 10.0.x - - - name: Restore dependencies - run: dotnet restore - - - name: Build - run: dotnet build --no-restore - - - name: Test - run: "[ -f .ci-skip-tests ] && echo 'Skipping tests' || dotnet test --no-build --verbosity normal" - - - name: Pack - if: github.ref == 'refs/heads/master' - run: dotnet pack -c Debug -o Build - - - name: "Github packages: Nuget push" - if: github.ref == 'refs/heads/master' - run: dotnet nuget push --no-symbols --skip-duplicate -k ${{ secrets.GITHUB_TOKEN }} -s "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" Build/*.nupkg - - - name: "Github packages: Cleanup" - if: github.ref == 'refs/heads/master' - uses: stripethree/gpr-janitor@dist - with: - dry-run: false - keep-versions: 25 - min-age-days: 14 - versions-to-fetch: 20 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml deleted file mode 100644 index 7299996..0000000 --- a/.github/workflows/nuget.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Nuget push (tag) - -on: - push: - tags: - - 'v*' - - '!v*-*' - -env: - DOTNET_CLI_TELEMETRY_OPTOUT: true - DOTNET_NOLOGO: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - # We do not need to fetch tags, as we're already at a tagged build - it should be available automatically - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 10.0.x - - - name: Pack - run: dotnet pack -c Release -o Build - - - name: ls - run: ls -alh Build/ - - - name: Nuget push (github) - run: dotnet nuget push --no-symbols --skip-duplicate -k ${{ secrets.GITHUB_TOKEN }} -s "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" Build/*.nupkg - - - name: Nuget push - run: dotnet nuget push --skip-duplicate -k ${{secrets.NUGET_KEY}} -s https://api.nuget.org/v3/index.json Build/*.nupkg diff --git a/.standard_content.json b/.standard_content.json index 68212ee..373baae 100644 --- a/.standard_content.json +++ b/.standard_content.json @@ -1,20 +1,28 @@ { - "$schema": "https://github.com/LordMike/MBW.Tools.GhStandardContent/spec/StandardContent.json", + "$schema": "https://raw.githubusercontent.com/LordMike/MBW.Tools.GhStandardContent/master/spec/StandardContent.json", "meta": { "repo": "LordMike/MBW.Libraries.DeviceIOControlLib", "profiles": [ + "standardCI", "standardContent", - "standardDotnet", - "standardNugetPush" + "standardDotnetBuild", + "standardNoDeploy" ], "files": [ ".editorconfig", ".gitattributes", - ".github/workflows/dotnet.yml", - ".github/workflows/nuget.yml", + ".github/actions/build/action.yml", + ".github/actions/collect_publishables/action.yml", + ".github/actions/initialize_ci/action.yml", + ".github/actions/publish_docker/action.yml", + ".github/actions/publish_github_release/action.yml", + ".github/actions/publish_nuget/action.yml", + ".github/readme.md", + ".github/workflows/ci.yml", + ".github/workflows/deploy.yml", ".gitignore", "Directory.Build.props" ], - "last_updated": "2026-05-06T11:07:59.4827292+00:00" + "last_updated": "2026-07-25T22:29:47.6919040\u002B00:00" } -} \ No newline at end of file +} diff --git a/Directory.Build.props b/Directory.Build.props index 232822d..a4a3934 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ true snupkg false - + v @@ -32,7 +32,7 @@ - 1 + 0 500