Release #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| schedule: | |
| - cron: "0 4 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| catalog_version: | |
| description: "Catalog version to publish, for example 1.0.0, 1.2.3-preview.1, or 2026.3.15.0" | |
| required: false | |
| type: string | |
| force_release: | |
| description: "Force a release even when the latest catalog-v* release already covers the current main commit" | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| env: | |
| CATALOG_TAG_PREFIX: "catalog-v" | |
| DOTNET_SKILLS_SITE_URL: https://skills.managed-code.com/ | |
| DOTNET_VERSION: "10.0.x" | |
| SOLUTION_FILE: "dotnet-skills.slnx" | |
| TOOL_PROJECTS: "cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj cli/ManagedCode.Agents/ManagedCode.Agents.csproj" | |
| PACKAGE_SOURCE: "https://api.nuget.org/v3/index.json" | |
| PACKAGE_OUTPUT_GLOB: "artifacts/nuget/*.nupkg" | |
| concurrency: | |
| group: "release" | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| environment: release | |
| outputs: | |
| should_release: ${{ steps.changes.outputs.has_new_commits }} | |
| previous_tag: ${{ steps.changes.outputs.latest_tag }} | |
| catalog_version: ${{ steps.version.outputs.version }} | |
| catalog_tag: ${{ steps.version.outputs.tag }} | |
| package_version: ${{ steps.package_version.outputs.package_version }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for new commits on main since latest catalog release | |
| id: changes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| git fetch --force --tags origin main | |
| latest_tag=$(gh api "repos/${{ github.repository }}/releases?per_page=100" --jq 'map(select(.draft == false and (.tag_name | startswith("'"${{ env.CATALOG_TAG_PREFIX }}"'")))) | sort_by(.published_at) | reverse | (.[0].tag_name // "")') | |
| main_ref="origin/main" | |
| force_release='${{ inputs.force_release }}' | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && "$force_release" == "true" ]]; then | |
| echo "has_new_commits=true" >> "$GITHUB_OUTPUT" | |
| echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" | |
| echo "override=true" >> "$GITHUB_OUTPUT" | |
| echo "reason=manual_force_release" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ -z "$latest_tag" ]]; then | |
| if [[ "$(git rev-list --count "$main_ref")" -gt 0 ]]; then | |
| echo "has_new_commits=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_new_commits=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "latest_tag=" >> "$GITHUB_OUTPUT" | |
| echo "override=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=initial_catalog_release" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| commits_since_tag=$(git rev-list --count "${latest_tag}..${main_ref}") | |
| if [[ "$commits_since_tag" -gt 0 ]]; then | |
| echo "has_new_commits=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_new_commits=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" | |
| echo "override=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=$([ "$commits_since_tag" -gt 0 ] && echo commits_since_last_release || echo no_new_commits)" >> "$GITHUB_OUTPUT" | |
| - name: Skip release when no new commits exist | |
| if: steps.changes.outputs.has_new_commits != 'true' | |
| shell: bash | |
| run: | | |
| echo "No unreleased commits on main since latest non-draft catalog release '${{ steps.changes.outputs.latest_tag }}'. Skipping release." | |
| - name: Set up Python | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up .NET | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Validate catalog version | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| id: version | |
| shell: bash | |
| run: | | |
| version='${{ inputs.catalog_version }}' | |
| if [[ -z "$version" ]]; then | |
| year="$(date -u +%Y)" | |
| month=$((10#$(date -u +%m))) | |
| day=$((10#$(date -u +%d))) | |
| today_pattern="${{ env.CATALOG_TAG_PREFIX }}${year}.${month}.${day}.*" | |
| latest_today_tag=$(git tag --list "$today_pattern" --sort=-v:refname | head -n 1) | |
| if [[ -z "$latest_today_tag" ]]; then | |
| daily_build_index=0 | |
| else | |
| latest_today_index="${latest_today_tag##*.}" | |
| if [[ ! "$latest_today_index" =~ ^[0-9]+$ ]]; then | |
| echo "Latest tag for today has a non-numeric daily build index: $latest_today_tag" >&2 | |
| exit 1 | |
| fi | |
| daily_build_index=$((10#$latest_today_index + 1)) | |
| fi | |
| version="${year}.${month}.${day}.${daily_build_index}" | |
| fi | |
| if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then | |
| echo "Catalog version must be SemVer-compatible. Got: $version" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=${{ env.CATALOG_TAG_PREFIX }}$version" >> "$GITHUB_OUTPUT" | |
| - name: Generate catalog outputs in CI | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| run: python3 scripts/generate_catalog.py | |
| - name: Determine package version | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| id: package_version | |
| shell: bash | |
| run: | | |
| base_version="" | |
| for project in ${{ env.TOOL_PROJECTS }}; do | |
| project_version=$(sed -n 's:.*<VersionPrefix>\(.*\)</VersionPrefix>.*:\1:p' "$project" | head -n 1 | tr -d '[:space:]') | |
| if [[ -z "$project_version" ]]; then | |
| echo "VersionPrefix is required in $project." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$project_version" =~ ^[0-9]+\.[0-9]+$ ]]; then | |
| echo "VersionPrefix must use <major>.<minor>. Got: $project_version in $project" >&2 | |
| exit 1 | |
| fi | |
| if [[ -z "$base_version" ]]; then | |
| base_version="$project_version" | |
| continue | |
| fi | |
| if [[ "$project_version" != "$base_version" ]]; then | |
| echo "All tool projects must share the same VersionPrefix. Found $base_version and $project_version." >&2 | |
| exit 1 | |
| fi | |
| done | |
| version="${base_version}.${GITHUB_RUN_NUMBER}" | |
| echo "base_version=$base_version" >> "$GITHUB_OUTPUT" | |
| echo "package_version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Generate release notes | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| previous_tag='${{ steps.changes.outputs.latest_tag }}' | |
| previous_args=() | |
| if [[ -n "$previous_tag" ]]; then | |
| previous_args+=(--previous-tag "$previous_tag") | |
| fi | |
| python3 scripts/generate_release_notes.py \ | |
| --repo "${{ github.repository }}" \ | |
| --tag "${{ steps.version.outputs.tag }}" \ | |
| --catalog-version "${{ steps.version.outputs.version }}" \ | |
| --package-version "${{ steps.package_version.outputs.package_version }}" \ | |
| --target-commit "${{ github.sha }}" \ | |
| --output artifacts/release/release-notes.md \ | |
| "${previous_args[@]}" | |
| - name: Build dotnet tool | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| run: dotnet build "${{ env.SOLUTION_FILE }}" -c Release -p:ContinuousIntegrationBuild=true -p:Version="${{ steps.package_version.outputs.package_version }}" | |
| - name: Run dotnet tests | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| run: dotnet test "${{ env.SOLUTION_FILE }}" -c Release --no-build -p:ContinuousIntegrationBuild=true -p:Version="${{ steps.package_version.outputs.package_version }}" | |
| - name: Pack dotnet tool | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| run: dotnet pack "${{ env.SOLUTION_FILE }}" -c Release --no-build -p:ContinuousIntegrationBuild=true -p:Version="${{ steps.package_version.outputs.package_version }}" | |
| - name: Smoke test installable tool | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| run: bash scripts/smoke_test_tool.sh artifacts/nuget | |
| - name: Package catalog release assets | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| shell: bash | |
| run: | | |
| rm -rf artifacts/catalog-release artifacts/catalog-payload | |
| mkdir -p artifacts/catalog-release artifacts/catalog-payload | |
| python3 scripts/generate_catalog.py --manifest-output artifacts/catalog-release/dotnet-skills-manifest.json | |
| cp -R catalog artifacts/catalog-payload/catalog | |
| ( | |
| cd artifacts/catalog-payload | |
| zip -r ../catalog-release/dotnet-skills-catalog.zip catalog | |
| ) | |
| - name: Upload packaged catalog artifacts | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dotnet-skills-catalog-${{ steps.version.outputs.version }} | |
| path: artifacts/catalog-release/* | |
| if-no-files-found: error | |
| - name: Upload NuGet package artifact | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: managedcode-dotnet-tools-${{ steps.package_version.outputs.package_version }} | |
| path: ${{ env.PACKAGE_OUTPUT_GLOB }} | |
| if-no-files-found: error | |
| - name: Create or update catalog release | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| tag='${{ steps.version.outputs.tag }}' | |
| title="Catalog ${{ steps.version.outputs.version }}" | |
| if gh release view "$tag" >/dev/null 2>&1; then | |
| gh release edit "$tag" --title "$title" --notes-file artifacts/release/release-notes.md | |
| else | |
| gh release create "$tag" --title "$title" --notes-file artifacts/release/release-notes.md --latest | |
| fi | |
| gh release upload "$tag" artifacts/catalog-release/dotnet-skills-manifest.json artifacts/catalog-release/dotnet-skills-catalog.zip --clobber | |
| - name: Publish to NuGet | |
| if: steps.changes.outputs.has_new_commits == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${{ secrets.NUGET_API_KEY }}" ]]; then | |
| echo "NUGET_API_KEY is required for NuGet publish." >&2 | |
| exit 1 | |
| fi | |
| shopt -s nullglob | |
| packages=( ${{ env.PACKAGE_OUTPUT_GLOB }} ) | |
| if [[ ${#packages[@]} -eq 0 ]]; then | |
| echo "No NuGet packages found for publish." >&2 | |
| exit 1 | |
| fi | |
| for package in "${packages[@]}"; do | |
| exit_code=0 | |
| echo "Publishing $package..." | |
| result=$(dotnet nuget push "$package" \ | |
| --api-key "${{ secrets.NUGET_API_KEY }}" \ | |
| --source "${{ env.PACKAGE_SOURCE }}" \ | |
| --skip-duplicate 2>&1) || exit_code=$? | |
| echo "$result" | |
| if [[ "$exit_code" -eq 0 ]]; then | |
| continue | |
| fi | |
| if echo "$result" | grep -qi "already exists"; then | |
| echo "Package already exists, skipping." | |
| continue | |
| fi | |
| exit "$exit_code" | |
| done | |
| build-pages: | |
| if: needs.release.outputs.should_release == 'true' | |
| needs: release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Generate GitHub Pages payload | |
| run: python3 scripts/generate_catalog.py | |
| - name: Render GitHub Pages | |
| env: | |
| DOTNET_SKILLS_RELEASE_VERSION: ${{ needs.release.outputs.catalog_version }} | |
| DOTNET_SKILLS_RELEASE_TAG: ${{ needs.release.outputs.catalog_tag }} | |
| DOTNET_SKILLS_RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/${{ needs.release.outputs.catalog_tag }} | |
| run: python3 scripts/generate_pages.py | |
| - name: Setup Pages | |
| uses: actions/configure-pages@v6 | |
| - name: Upload GitHub Pages artifact | |
| uses: actions/upload-pages-artifact@v4 | |
| with: | |
| path: "artifacts/github-pages" | |
| deploy-pages: | |
| if: needs.release.outputs.should_release == 'true' | |
| needs: | |
| - release | |
| - build-pages | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 |