Skip to content

Release Pipeline

Release Pipeline #20

Workflow file for this run

name: Release Pipeline
on:
workflow_dispatch:
inputs:
release_type:
description: "Release type"
required: true
default: "stable"
type: choice
options:
- stable
- preview
version_bump:
description: "Version bump (required for stable, optional for preview)"
required: false
default: "none"
type: choice
options:
- none
- major
- minor
- patch
preview_label:
description: "Preview label (only for preview releases)"
required: false
default: "preview"
type: string
dry_run:
description: "Dry run (skip publishing)"
required: false
default: false
type: boolean
publish_shared:
description: "Publish Spiderly.Shared"
required: false
default: true
type: boolean
publish_sourcegenerators:
description: "Publish Spiderly.SourceGenerators"
required: false
default: true
type: boolean
publish_security:
description: "Publish Spiderly.Security"
required: false
default: true
type: boolean
publish_infrastructure:
description: "Publish Spiderly.Infrastructure"
required: false
default: true
type: boolean
publish_cli:
description: "Publish Spiderly.CLI"
required: false
default: true
type: boolean
publish_angular:
description: "Publish spiderly (Angular/npm)"
required: false
default: true
type: boolean
env:
DOTNET_VERSION: "9.0.x"
NODE_VERSION: "20"
jobs:
validate:
name: Validate Inputs
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.version.outputs.NEW_VERSION }}
is_prerelease: ${{ steps.version.outputs.IS_PRERELEASE }}
dotnet_packages: ${{ steps.packages.outputs.dotnet_packages }}
publish_angular: ${{ steps.packages.outputs.publish_angular }}
has_dotnet_packages: ${{ steps.packages.outputs.has_dotnet_packages }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get current version
id: current
run: |
current_version=$(grep -oPm1 "(?<=<Version>)[^<]+" Spiderly.Shared/Spiderly.Shared.csproj)
echo "Current version: $current_version"
base_version="$current_version"
is_preview="false"
if [[ "$current_version" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then
base_version="${BASH_REMATCH[1]}"
if [ "$current_version" != "$base_version" ]; then
is_preview="true"
fi
fi
echo "CURRENT_VERSION=$current_version" >> $GITHUB_OUTPUT
echo "BASE_VERSION=$base_version" >> $GITHUB_OUTPUT
echo "IS_PREVIEW=$is_preview" >> $GITHUB_OUTPUT
- name: Validate inputs
run: |
if [ "${{ inputs.release_type }}" == "stable" ] && [ "${{ inputs.version_bump }}" == "none" ] && [ "${{ steps.current.outputs.IS_PREVIEW }}" == "false" ]; then
echo "::error::Version bump is required for stable releases from a stable version. Use 'none' only when promoting a preview to stable."
exit 1
fi
if [ "${{ inputs.publish_shared }}" != "true" ] && \
[ "${{ inputs.publish_sourcegenerators }}" != "true" ] && \
[ "${{ inputs.publish_security }}" != "true" ] && \
[ "${{ inputs.publish_infrastructure }}" != "true" ] && \
[ "${{ inputs.publish_cli }}" != "true" ] && \
[ "${{ inputs.publish_angular }}" != "true" ]; then
echo "::error::At least one package must be selected for publishing"
exit 1
fi
- name: Build package selection
id: packages
run: |
packages=()
[ "${{ inputs.publish_shared }}" == "true" ] && packages+=("Spiderly.Shared")
[ "${{ inputs.publish_sourcegenerators }}" == "true" ] && packages+=("Spiderly.SourceGenerators")
[ "${{ inputs.publish_security }}" == "true" ] && packages+=("Spiderly.Security")
[ "${{ inputs.publish_infrastructure }}" == "true" ] && packages+=("Spiderly.Infrastructure")
[ "${{ inputs.publish_cli }}" == "true" ] && packages+=("Spiderly.CLI")
if [ ${#packages[@]} -eq 0 ]; then
packages_json="[]"
has_packages="false"
else
packages_json=$(printf '%s\n' "${packages[@]}" | jq -R . | jq -sc .)
has_packages="true"
fi
echo "dotnet_packages=$packages_json" >> $GITHUB_OUTPUT
echo "publish_angular=${{ inputs.publish_angular }}" >> $GITHUB_OUTPUT
echo "has_dotnet_packages=$has_packages" >> $GITHUB_OUTPUT
echo "Selected .NET packages: ${packages[*]}"
echo "Publish Angular: ${{ inputs.publish_angular }}"
- name: Calculate new version
id: version
run: |
current_version="${{ steps.current.outputs.CURRENT_VERSION }}"
base_version="${{ steps.current.outputs.BASE_VERSION }}"
IFS='.' read -ra parts <<< "$base_version"
version_bump="${{ inputs.version_bump }}"
if [ "${{ inputs.release_type }}" == "stable" ]; then
case "$version_bump" in
none) new_version="$base_version" ;;
major) new_version="$((parts[0] + 1)).0.0" ;;
minor) new_version="${parts[0]}.$(( parts[1] + 1 )).0" ;;
patch) new_version="${parts[0]}.${parts[1]}.$(( parts[2] + 1 ))" ;;
esac
is_prerelease="false"
echo "Stable release: $new_version"
else
preview_label="${{ inputs.preview_label }}"
if [ "$version_bump" != "none" ]; then
case "$version_bump" in
major) base_version="$((parts[0] + 1)).0.0" ;;
minor) base_version="${parts[0]}.$(( parts[1] + 1 )).0" ;;
patch) base_version="${parts[0]}.${parts[1]}.$(( parts[2] + 1 ))" ;;
esac
new_version="${base_version}-${preview_label}.0"
echo "Bumped version to $base_version and started preview series"
else
if [[ "$current_version" =~ -${preview_label}\.([0-9]+)$ ]]; then
existing_number="${BASH_REMATCH[1]}"
new_number=$(( existing_number + 1 ))
new_version="${base_version}-${preview_label}.${new_number}"
echo "Incremented preview: ${existing_number} → ${new_number}"
else
echo "::error::Cannot create preview with 'none' bump from stable version $current_version"
exit 1
fi
fi
is_prerelease="true"
fi
echo "NEW_VERSION=$new_version" >> $GITHUB_OUTPUT
echo "IS_PRERELEASE=$is_prerelease" >> $GITHUB_OUTPUT
echo "New version: $new_version"
build-dotnet:
name: Build .NET Packages
needs: validate
if: needs.validate.outputs.has_dotnet_packages == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: ${{ fromJson(needs.validate.outputs.dotnet_packages) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Update version
run: |
project="${{ matrix.package }}/${{ matrix.package }}.csproj"
new_version="${{ needs.validate.outputs.new_version }}"
sed -i "s|<Version>.*</Version>|<Version>$new_version</Version>|" "$project"
echo "Updated $project to $new_version"
- name: Restore dependencies
run: dotnet restore "${{ matrix.package }}/${{ matrix.package }}.csproj"
- name: Build
run: dotnet build -c Release "${{ matrix.package }}/${{ matrix.package }}.csproj"
- name: Pack
run: |
dotnet pack -c Release \
-o ./artifacts \
/p:PackageVersion=${{ needs.validate.outputs.new_version }} \
"${{ matrix.package }}/${{ matrix.package }}.csproj"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: nupkg-${{ matrix.package }}
path: ./artifacts/*.nupkg
retention-days: 1
- name: Upload version file
uses: actions/upload-artifact@v4
with:
name: version-${{ matrix.package }}
path: ${{ matrix.package }}/${{ matrix.package }}.csproj
retention-days: 1
test-dotnet:
name: Test .NET
needs: [validate, build-dotnet]
if: needs.validate.outputs.has_dotnet_packages == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build -c Release
- name: Run tests
run: dotnet test -c Release --no-build --verbosity normal
build-angular:
name: Build Angular Package
needs: validate
if: needs.validate.outputs.publish_angular == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
cache-dependency-path: Angular/package-lock.json
- name: Update version
run: |
cd Angular/projects/spiderly
npm version ${{ needs.validate.outputs.new_version }} --no-git-tag-version
echo "Updated Angular package to ${{ needs.validate.outputs.new_version }}"
- name: Install dependencies
run: |
cd Angular
npm ci
- name: Build
run: |
cd Angular
npx ng build spiderly --configuration production
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: npm-package
path: Angular/dist/spiderly/
retention-days: 1
- name: Upload version file
uses: actions/upload-artifact@v4
with:
name: version-angular
path: Angular/projects/spiderly/package.json
retention-days: 1
publish-nuget:
name: Publish to NuGet
needs: [validate, build-dotnet, test-dotnet]
if: inputs.dry_run != true && needs.validate.outputs.has_dotnet_packages == 'true'
runs-on: ubuntu-latest
outputs:
published_packages: ${{ steps.publish.outputs.published_packages }}
steps:
- name: Download all NuGet packages
uses: actions/download-artifact@v4
with:
pattern: nupkg-*
path: ./nupkg
merge-multiple: true
- name: Publish to NuGet
id: publish
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
published=()
failed=()
for package in nupkg/*.nupkg; do
if [ ! -f "$package" ]; then
echo "No packages found to publish"
continue
fi
echo "Publishing $package..."
if dotnet nuget push "$package" \
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate; then
package_name=$(basename "$package" .nupkg | sed 's/\.[0-9].*//')
published+=("$package_name")
echo "Successfully published $package_name"
else
failed+=("$(basename "$package")")
echo "::warning::Failed to publish $package"
fi
done
if [ ${#failed[@]} -gt 0 ]; then
echo "::error::Failed to publish: ${failed[*]}"
exit 1
fi
published_json=$(printf '%s\n' "${published[@]}" | jq -R . | jq -sc . 2>/dev/null || echo "[]")
echo "published_packages=$published_json" >> $GITHUB_OUTPUT
echo "Published packages: ${published[*]}"
publish-npm:
name: Publish to npm
needs: [validate, build-angular]
if: inputs.dry_run != true && needs.validate.outputs.publish_angular == 'true'
runs-on: ubuntu-latest
outputs:
published: ${{ steps.publish.outputs.success }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: "https://registry.npmjs.org"
- name: Download npm package
uses: actions/download-artifact@v4
with:
name: npm-package
path: ./dist
- name: Publish to npm
id: publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd dist
if [ "${{ needs.validate.outputs.is_prerelease }}" = "true" ]; then
npm publish --access public --tag ${{ inputs.preview_label }}
echo "Published as preview with tag: ${{ inputs.preview_label }}"
else
npm publish --access public
echo "Published as latest stable"
fi
echo "success=true" >> $GITHUB_OUTPUT
create-release:
name: Create GitHub Release
needs: [validate, publish-nuget, publish-npm]
if: |
always() &&
inputs.dry_run != true &&
needs.validate.outputs.is_prerelease == 'false' &&
(needs.publish-nuget.result == 'success' || needs.publish-npm.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all NuGet packages
if: needs.validate.outputs.has_dotnet_packages == 'true'
uses: actions/download-artifact@v4
with:
pattern: nupkg-*
path: ./nupkg
merge-multiple: true
- name: Create Git Tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
tag="v${{ needs.validate.outputs.new_version }}"
git tag "$tag"
git push origin "$tag"
- name: Generate Release Notes
id: notes
run: |
new_version="${{ needs.validate.outputs.new_version }}"
previous_tag=$(git tag -l "v[0-9]*.[0-9]*.[0-9]" --sort=-v:refname | grep -v -- "-" | head -n 1)
{
echo "## What's Changed"
echo ""
if [ -n "$previous_tag" ]; then
git log "$previous_tag..HEAD" --pretty=format:"- %s (%h)" --no-merges
else
git log --pretty=format:"- %s (%h)" --no-merges -n 20
fi
echo ""
echo ""
echo "## Packages Published"
echo ""
} > release_notes.md
dotnet_packages='${{ needs.validate.outputs.dotnet_packages }}'
if [ "$dotnet_packages" != "[]" ] && [ -n "$dotnet_packages" ]; then
echo "### NuGet Packages" >> release_notes.md
echo "$dotnet_packages" | jq -r '.[]' | while read package; do
echo "- [$package $new_version](https://www.nuget.org/packages/$package/$new_version)" >> release_notes.md
done
echo "" >> release_notes.md
fi
if [ "${{ needs.validate.outputs.publish_angular }}" == "true" ]; then
echo "### npm Package" >> release_notes.md
echo "- [spiderly $new_version](https://www.npmjs.com/package/spiderly/v/$new_version)" >> release_notes.md
echo "" >> release_notes.md
fi
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/$previous_tag...v$new_version" >> release_notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.validate.outputs.new_version }}
name: Release v${{ needs.validate.outputs.new_version }}
body_path: release_notes.md
draft: false
prerelease: ${{ needs.validate.outputs.is_prerelease }}
files: nupkg/*.nupkg
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize:
name: Finalize Release
needs: [validate, create-release, publish-nuget, publish-npm]
if: |
always() &&
inputs.dry_run != true &&
needs.create-release.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
- name: Download version files
uses: actions/download-artifact@v4
with:
pattern: version-*
path: ./versions
- name: Commit version updates
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
dotnet_packages='${{ needs.validate.outputs.dotnet_packages }}'
if [ "$dotnet_packages" != "[]" ] && [ -n "$dotnet_packages" ]; then
echo "$dotnet_packages" | jq -r '.[]' | while read package; do
if [ -f "versions/version-$package/$package.csproj" ]; then
cp "versions/version-$package/$package.csproj" "$package/$package.csproj"
git add "$package/$package.csproj"
fi
done
fi
if [ "${{ needs.validate.outputs.publish_angular }}" == "true" ] && [ -f "versions/version-angular/package.json" ]; then
cp "versions/version-angular/package.json" "Angular/projects/spiderly/package.json"
git add "Angular/projects/spiderly/package.json"
fi
if ! git diff --staged --quiet; then
git commit -m "chore: bump version to ${{ needs.validate.outputs.new_version }}"
git push
else
echo "No version changes to commit"
fi
- name: Merge to main
if: needs.validate.outputs.is_prerelease == 'false' && github.ref != 'refs/heads/main'
run: |
current_branch=$(git branch --show-current)
echo "Merging stable release from $current_branch to main..."
git fetch origin main
git checkout main
git merge --no-ff "origin/$current_branch" -m "chore: merge stable release v${{ needs.validate.outputs.new_version }} to main"
git push origin main
echo "Successfully merged to main"
dry-run-summary:
name: Dry Run Summary
needs: [validate, build-dotnet, build-angular, test-dotnet]
if: |
always() &&
inputs.dry_run == true &&
needs.validate.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Generate summary
run: |
{
echo "## Dry Run Complete"
echo ""
echo "Version would be bumped to: **${{ needs.validate.outputs.new_version }}**"
echo ""
echo "### Build Status"
echo "- .NET Build: ${{ needs.build-dotnet.result || 'skipped' }}"
echo "- .NET Tests: ${{ needs.test-dotnet.result || 'skipped' }}"
echo "- Angular Build: ${{ needs.build-angular.result || 'skipped' }}"
echo ""
echo "Packages built successfully but NOT published (dry run mode)"
} >> $GITHUB_STEP_SUMMARY