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
25 changes: 16 additions & 9 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,25 @@ against the base branch to calculate the minimum impact and reject in-place
changes to published artifacts. Configure branch protection to require
`release-governance / semver-and-immutability`.

Manual release runs provide a previous Git ref, previous package version, next
package version, and declared impact. The pack workflows reject a version
transition that does not exactly match that declaration.
`publish-packages.yml` has no version inputs. It starts automatically on every
push to `main`; manual dispatch remains available only to retry an interrupted
release. It finds the latest reachable `vX.Y.Z` tag, calculates the required
impact from the repository diff, derives the next version, and validates that
transition. The first release uses the normalized version from `version.json`;
rerunning an already tagged commit reuses its version safely.

Release runs are serialized with `cancel-in-progress: false`, so a later commit
cannot cancel or race an in-flight package publication.

## Package artifacts

`pack-promptkit.yml` and `pack-prompts.yml` can only execute their packaging jobs
after the reusable prompt contract gate succeeds, including all four test
projects. A successful run uploads the generated `.nupkg` as a workflow artifact
tied to the commit SHA and publishes that same file to the repository owner's
GitHub Packages NuGet registry. A failed test or contract check therefore creates
neither a releasable artifact nor a registry version.
`publish-packages.yml` calls `pack-promptkit.yml` and `pack-prompts.yml` as internal
reusable workflows only after the prompt contract gate succeeds, including all
four test projects. PromptKit is published first because Prompts depends on the
same package version. A successful run uploads both `.nupkg` files as workflow
artifacts, publishes those files to the repository owner's GitHub Packages NuGet
registry, and creates the corresponding annotated tag. A failed test or contract
check therefore creates neither a releasable artifact nor a registry version.

Registry publication runs through the `production` environment with the
workflow-scoped `GITHUB_TOKEN` and the least-privilege `packages: write`
Expand Down
28 changes: 15 additions & 13 deletions .github/workflows/pack-promptkit.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
name: pack-promptkit

on:
workflow_dispatch:
workflow_call:
inputs:
base-ref:
description: Previous release tag or Git ref
required: true
type: string
previous-version:
description: Previously published package version (X.Y.Z)
required: true
required: false
type: string
package-version:
description: Package version to create (X.Y.Z)
required: true
type: string
semver-impact:
description: Declared release impact
required: true
type: choice
options: [patch, minor, major]
type: string

permissions:
contents: read
packages: write

jobs:
contract-gate:
name: validate prompt contract
uses: ./.github/workflows/validate-prompts.yml

pack:
name: pack PromptKit
needs: contract-gate
runs-on: ubuntu-latest
environment: production
timeout-minutes: 15
Expand All @@ -56,6 +49,7 @@ jobs:
run: dotnet restore tools/DevComputaria.Prompts.Cli/DevComputaria.Prompts.Cli.csproj

- name: Validate release version and impact
if: inputs['previous-version'] != ''
run: >-
dotnet run --project tools/DevComputaria.Prompts.Cli
--configuration Release --no-restore --
Expand All @@ -64,6 +58,14 @@ jobs:
--previous-version "${{ inputs['previous-version'] }}"
--next-version "${{ inputs['package-version'] }}"

- name: Validate initial release impact
if: inputs['previous-version'] == ''
run: >-
dotnet run --project tools/DevComputaria.Prompts.Cli
--configuration Release --no-restore --
release-check --base "${{ inputs['base-ref'] }}"
--impact "${{ inputs['semver-impact'] }}"

- name: Pack validated runtime
run: >-
dotnet pack src/DevComputaria.PromptKit/DevComputaria.PromptKit.csproj
Expand Down
43 changes: 29 additions & 14 deletions .github/workflows/pack-prompts.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
name: pack-prompts

on:
workflow_dispatch:
workflow_call:
inputs:
base-ref:
description: Previous release tag or Git ref
required: true
type: string
previous-version:
description: Previously published package version (X.Y.Z)
required: true
required: false
type: string
package-version:
description: Package version to create (X.Y.Z)
required: true
type: string
semver-impact:
description: Declared release impact
required: true
type: choice
options: [patch, minor, major]
type: string
create-tag:
required: true
type: boolean

permissions:
contents: read
contents: write
packages: write

jobs:
contract-gate:
name: validate prompt contract
uses: ./.github/workflows/validate-prompts.yml

pack:
name: pack prompts
needs: contract-gate
runs-on: ubuntu-latest
environment: production
timeout-minutes: 15
Expand All @@ -56,6 +52,7 @@ jobs:
run: dotnet restore tools/DevComputaria.Prompts.Cli/DevComputaria.Prompts.Cli.csproj

- name: Validate release version and impact
if: inputs['previous-version'] != ''
run: >-
dotnet run --project tools/DevComputaria.Prompts.Cli
--configuration Release --no-restore --
Expand All @@ -64,6 +61,14 @@ jobs:
--previous-version "${{ inputs['previous-version'] }}"
--next-version "${{ inputs['package-version'] }}"

- name: Validate initial release impact
if: inputs['previous-version'] == ''
run: >-
dotnet run --project tools/DevComputaria.Prompts.Cli
--configuration Release --no-restore --
release-check --base "${{ inputs['base-ref'] }}"
--impact "${{ inputs['semver-impact'] }}"

- name: Pack validated catalog
run: >-
dotnet pack src/DevComputaria.Prompts/DevComputaria.Prompts.csproj
Expand All @@ -90,3 +95,13 @@ jobs:
--source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
--api-key "$NUGET_AUTH_TOKEN"
--skip-duplicate

- name: Tag published commit
if: inputs['create-tag']
env:
PACKAGE_VERSION: ${{ inputs['package-version'] }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag --annotate "v$PACKAGE_VERSION" --message "Release v$PACKAGE_VERSION"
git push origin "v$PACKAGE_VERSION"
164 changes: 164 additions & 0 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: publish-packages

on:
push:
branches:
- main
workflow_dispatch:

concurrency:
group: publish-packages
cancel-in-progress: false

permissions:
contents: write
packages: write

jobs:
guard:
name: require main branch
runs-on: ubuntu-latest
steps:
- name: Reject non-main releases
env:
RELEASE_REF: ${{ github.ref }}
run: |
if [[ "$RELEASE_REF" != "refs/heads/main" ]]; then
echo "Packages can only be published from main; received '$RELEASE_REF'." >&2
exit 1
fi

contract-gate:
name: validate tests and prompt contract
needs: guard
uses: ./.github/workflows/validate-prompts.yml

release-plan:
name: calculate release version
needs: contract-gate
runs-on: ubuntu-latest
outputs:
base-ref: ${{ steps.plan.outputs.base-ref }}
previous-version: ${{ steps.plan.outputs.previous-version }}
package-version: ${{ steps.plan.outputs.package-version }}
semver-impact: ${{ steps.plan.outputs.semver-impact }}
create-tag: ${{ steps.plan.outputs.create-tag }}
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
steps:
- name: Checkout repository history
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
cache: true
cache-dependency-path: |
Directory.Packages.props
tools/DevComputaria.Prompts.Cli/DevComputaria.Prompts.Cli.csproj

- name: Restore release tooling
run: dotnet restore tools/DevComputaria.Prompts.Cli/DevComputaria.Prompts.Cli.csproj

- name: Calculate version from repository history
id: plan
shell: bash
run: |
set -euo pipefail

current_tag="$(git tag --points-at HEAD --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1)"
latest_tag="$(git tag --merged HEAD^ --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1)"

if [[ -n "$latest_tag" ]]; then
base_ref="$latest_tag"
previous_version="${latest_tag#v}"
else
base_ref="$(git rev-list --max-parents=0 HEAD | tail -n 1)"
previous_version=""
fi

governance_output="$(dotnet run --project tools/DevComputaria.Prompts.Cli \
--configuration Release --no-restore -- \
release-check --base "$base_ref" --impact major)"
printf '%s\n' "$governance_output"

impact="$(printf '%s\n' "$governance_output" \
| sed -n 's/^Required SemVer impact: //p' \
| tr '[:upper:]' '[:lower:]')"
if [[ ! "$impact" =~ ^(patch|minor|major)$ ]]; then
echo "Could not calculate a SemVer impact from release-check." >&2
exit 1
fi

if [[ -n "$current_tag" ]]; then
package_version="${current_tag#v}"
create_tag=false
elif [[ -z "$previous_version" ]]; then
package_version="$(jq -r '.version' version.json)"
if [[ "$package_version" =~ ^[0-9]+\.[0-9]+$ ]]; then
package_version="${package_version}.0"
fi
create_tag=true
else
IFS=. read -r major minor patch <<< "$previous_version"
case "$impact" in
patch) package_version="$major.$minor.$((patch + 1))" ;;
minor) package_version="$major.$((minor + 1)).0" ;;
major) package_version="$((major + 1)).0.0" ;;
esac
create_tag=true
fi

if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Calculated package version '$package_version' is not X.Y.Z." >&2
exit 1
fi

if [[ -n "$previous_version" ]]; then
dotnet run --project tools/DevComputaria.Prompts.Cli \
--configuration Release --no-restore -- \
release-check --base "$base_ref" --impact "$impact" \
--previous-version "$previous_version" \
--next-version "$package_version"
else
dotnet run --project tools/DevComputaria.Prompts.Cli \
--configuration Release --no-restore -- \
release-check --base "$base_ref" --impact "$impact"
fi

{
echo "base-ref=$base_ref"
echo "previous-version=$previous_version"
echo "package-version=$package_version"
echo "semver-impact=$impact"
echo "create-tag=$create_tag"
} >> "$GITHUB_OUTPUT"

echo "Release plan: ${current_tag:-new tag v$package_version}, base $base_ref, impact $impact."

publish-promptkit:
name: publish PromptKit
needs: release-plan
uses: ./.github/workflows/pack-promptkit.yml
with:
base-ref: ${{ needs.release-plan.outputs.base-ref }}
previous-version: ${{ needs.release-plan.outputs.previous-version }}
package-version: ${{ needs.release-plan.outputs.package-version }}
semver-impact: ${{ needs.release-plan.outputs.semver-impact }}

publish-prompts:
name: publish Prompts and finalize release
needs:
- release-plan
- publish-promptkit
uses: ./.github/workflows/pack-prompts.yml
with:
base-ref: ${{ needs.release-plan.outputs.base-ref }}
previous-version: ${{ needs.release-plan.outputs.previous-version }}
package-version: ${{ needs.release-plan.outputs.package-version }}
semver-impact: ${{ needs.release-plan.outputs.semver-impact }}
create-tag: ${{ needs.release-plan.outputs.create-tag == 'true' }}
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ The format is inspired by Keep a Changelog and adapted to the execution model of
- Public `PromptLogRedactor` with conservative sensitive-variable masking and DI registration.
- Security and release governance policy covering roles, access, logging, compliance, and rollback.
- CLI `release-check` command for diff-based SemVer classification and artifact immutability enforcement.
- Pull-request release-governance workflow and governed package-version inputs for pack workflows.
- Pull-request release-governance workflow and automatic tag-based package version planning.
- Security reporting policy and pull-request security/SemVer checklist.
- Unit-test and contract validation on pull requests and pushes to `main`, reused as a mandatory gate before package creation.
- Governed publication of validated PromptKit and Prompts packages to the GitHub Packages NuGet registry.
- Input-free `publish-packages` workflow that runs automatically on `main`, derives SemVer from the latest reviewed commit and release tag, and serializes publications.

## [2026-08-30]

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ DevComputaria.Prompting/
└── workflows/
├── validate-prompts.yml
├── release-governance.yml
├── publish-packages.yml
├── pack-promptkit.yml
└── pack-prompts.yml
```
Expand Down
Loading
Loading