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
227 changes: 213 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,195 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- ready_for_review

permissions:
contents: read

env:
PROJECT_PATH: 'ZWebAPI/ZWebAPI.csproj'
PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}/output
NUGET_PUSH_URL: ${{ secrets.NUGET_PUSH_URL }}
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
DOTNET_VERSION: 9.0.x

# Cancel superseded PR runs, but never cancel a release in progress;
# queueing runs on main also prevents two merges from racing the same version.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# Computes the next version with the same action and plugins as the publish
# job, in dry-run mode. Its outputs are the single source of truth for the
# version used by every job downstream.
version:
name: Semantic release version
runs-on: ubuntu-latest
# Even in dry-run mode, semantic-release verifies push access with
# "git push --dry-run" before analyzing anything, so the token must be
# allowed to push. Nothing is ever pushed by this job.
permissions:
contents: write
outputs:
new-release-published: ${{ steps.semantic.outputs.new_release_published }}
new-release-version: ${{ steps.semantic.outputs.new_release_version }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

# PRs from forks always get a read-only token, which would fail the push
# access check; skip the dry run there and let the build job fall back
# to the 0.0.0-validation version.
- name: Semantic release (dry run)
id: semantic
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
uses: cycjimmy/semantic-release-action@v5
with:
dry_run: true
ci: false
extra_plugins: |
@semantic-release/commit-analyzer
@semantic-release/release-notes-generator
@semantic-release/github
@semantic-release/git
env:
SEMANTIC_RELEASE_VALIDATION: 'true'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Builds, tests, packs with the predicted version, and validates the package
# against NuGet.org publishing rules. The produced package is uploaded as an
# artifact and is the exact file the publish job pushes.
build:
name: Build, test & pack
runs-on: ubuntu-latest
needs: version
env:
PACKAGE_VERSION: ${{ needs.version.outputs.new-release-version || '0.0.0-validation' }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore
run: dotnet restore

- name: Build
run: >-
dotnet build
--configuration Release
--no-restore
-p:Version=${{ env.PACKAGE_VERSION }}
-p:ContinuousIntegrationBuild=true

- name: Test
run: dotnet test --configuration Release --no-build

- name: Pack
run: >-
dotnet pack ${{ env.PROJECT_PATH }}
--configuration Release
--no-build
--include-symbols
-p:SymbolPackageFormat=snupkg
-p:Version=${{ env.PACKAGE_VERSION }}
-p:ContinuousIntegrationBuild=true
--output ${{ env.PACKAGE_OUTPUT_DIRECTORY }}

- name: Validate NuGet package
run: |
dotnet tool update --global Meziantou.Framework.NuGetPackageValidation.Tool
meziantou.validate-nuget-package ${{ env.PACKAGE_OUTPUT_DIRECTORY }}/*.nupkg --excluded-rules IconMustBeSet

- name: Upload package artifact
uses: actions/upload-artifact@v7
with:
name: nuget-package
path: ${{ env.PACKAGE_OUTPUT_DIRECTORY }}/*
retention-days: 7
overwrite: true

# Proves the Trusted Publishing handshake works (policy active, NUGET_USER
# correct, push URL reachable) without publishing anything. The minted API
# key is never used and expires after 1 hour. Deliberately has no checkout,
# so no repository code runs in the job that holds the key.
nuget-auth:
name: NuGet authentication
runs-on: ubuntu-latest
needs: build
# PRs from forks receive neither OIDC tokens nor secrets; skip the check for them.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
permissions:
id-token: write
steps:
- name: NuGet login (Trusted Publishing)
uses: NuGet/login@v1
with:
user: ${{ vars.NUGET_USER }}

- name: Validate push endpoint
run: curl --fail --silent --show-error --location --output /dev/null "${{ vars.NUGET_PUSH_URL }}"

# Aggregated status for branch protection: the "Build validation" required
# check on main maps to this job, so the jobs above can be renamed or
# extended without touching the protection rule. Runs with always() because
# a skipped required check would otherwise count as passing.
build-validation:
name: Build validation
runs-on: ubuntu-latest
needs:
- version
- build
- nuget-auth
if: ${{ always() }}
steps:
- name: Evaluate results
run: |
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
echo "::error::One or more validation jobs did not succeed."
exit 1
fi
echo "All validation jobs succeeded."

# Creates the git tag and GitHub release, then publishes the package built
# and validated by the build job. Re-running just this job after a failed
# push is safe: the version comes from the version job, the package from the
# build artifact, and --skip-duplicate makes the push idempotent.
release:
name: Release
name: Publish to NuGet.org
runs-on: ubuntu-latest
needs:
- version
- build
- nuget-auth
if: ${{ github.event_name != 'pull_request' && needs.version.outputs.new-release-published == 'true' }}
permissions:
contents: write
id-token: write
issues: write
pull-requests: write
env:
PACKAGE_VERSION: ${{ needs.version.outputs.new-release-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET Core SDK ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
uses: actions/checkout@v6
with:
dotnet-version: 9.0.x
fetch-depth: 0

- name: Semantic Release
id: release
uses: cycjimmy/semantic-release-action@v4
uses: cycjimmy/semantic-release-action@v5
with:
extra_plugins: |
@semantic-release/commit-analyzer
Expand All @@ -32,10 +201,40 @@ jobs:
@semantic-release/git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: 'Pack & Publish project'
if: ${{ steps.release.outputs.new_release_version }}

- name: Download package artifact
uses: actions/download-artifact@v8
with:
name: nuget-package
path: ${{ env.PACKAGE_OUTPUT_DIRECTORY }}

# Guards the artifact promotion: the version semantic-release just
# released (or already released in a previous attempt of this run) must
# match the version the package was built with.
- name: Verify package matches the released version
run: |
dotnet clean
dotnet pack ${{ env.PROJECT_PATH }} --configuration Release --include-symbols --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} -p:PackageVersion=${{ steps.release.outputs.new_release_version }}
dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s ${{ env.NUGET_PUSH_URL }} --skip-duplicate
if [ "${{ steps.release.outputs.new_release_published }}" = "true" ] && [ "${{ steps.release.outputs.new_release_version }}" != "${{ env.PACKAGE_VERSION }}" ]; then
echo "::error::Semantic release published version ${{ steps.release.outputs.new_release_version }}, but the validated package is ${{ env.PACKAGE_VERSION }}. Not publishing."
exit 1
fi
if [ "${{ steps.release.outputs.new_release_published }}" != "true" ] && ! git rev-parse -q --verify "refs/tags/v${{ env.PACKAGE_VERSION }}" > /dev/null; then
echo "::error::No release was created in this attempt and tag v${{ env.PACKAGE_VERSION }} does not exist."
exit 1
fi
if [ ! -f "${{ env.PACKAGE_OUTPUT_DIRECTORY }}/ZWebAPI.${{ env.PACKAGE_VERSION }}.nupkg" ]; then
echo "::error::Package ZWebAPI.${{ env.PACKAGE_VERSION }}.nupkg not found in the build artifact."
exit 1
fi

- name: NuGet login (Trusted Publishing)
id: login
uses: NuGet/login@v1
with:
user: ${{ vars.NUGET_USER }}

- name: Publish package
run: >-
dotnet nuget push "${{ env.PACKAGE_OUTPUT_DIRECTORY }}/ZWebAPI.${{ env.PACKAGE_VERSION }}.nupkg"
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source "${{ vars.NUGET_PUSH_URL }}"
--skip-duplicate
41 changes: 0 additions & 41 deletions .releaserc

This file was deleted.

27 changes: 27 additions & 0 deletions ZWebAPI/Models/BatchUpdateModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace ZWebAPI.Models
{
/// <summary>
/// Batch update model for entities.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
public class BatchUpdateModel<TEntity, TKey>
where TEntity : class
{
/// <summary>
/// Gets or sets the entities to delete.
/// </summary>
/// <value>
/// The entities to delete.
/// </value>
public TKey[]? EntitiesToDelete { get; set; }

/// <summary>
/// Gets or sets the entities to add or update.
/// </summary>
/// <value>
/// The entities to add or update.
/// </value>
public TEntity[]? EntitiesToInsertOrUpdate { get; set; }
}
}
25 changes: 25 additions & 0 deletions ZWebAPI/Models/RelationshipUpdateModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace ZWebAPI.Models
{
/// <summary>
/// Update model for entity relationships.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
public class RelationshipUpdateModel<TKey>
{
/// <summary>
/// Gets or sets the ids to add.
/// </summary>
/// <value>
/// The ids to add.
/// </value>
public IEnumerable<TKey> IDsToAdd { get; set; } = Enumerable.Empty<TKey>();

/// <summary>
/// Gets or sets the ids to remove.
/// </summary>
/// <value>
/// The ids to remove.
/// </value>
public IEnumerable<TKey> IDsToRemove { get; set; } = Enumerable.Empty<TKey>();
}
}
11 changes: 7 additions & 4 deletions ZWebAPI/ZWebAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
<Authors>Ricardo Zambon</Authors>
<Company>ZambonSoft</Company>
<Version>1.0.0</Version>
<RepositoryUrl>https://github.com/RicardoZambon/ZSecurity</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<PackageTags>webapi;aspnetcore;rest;crud</PackageTags>
<RepositoryUrl>https://github.com/RicardoZambon/ZWebAPI</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Description>ZWebAPI is a library that brings additional functionalities for web API projects.</Description>
<Copyright>ZambonSoft</Copyright>
<PackageProjectUrl>https://github.com/RicardoZambon/ZSecurity</PackageProjectUrl>
<PackageProjectUrl>https://github.com/RicardoZambon/ZWebAPI</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
Expand All @@ -27,8 +30,8 @@
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="ClosedXML" Version="0.104.2" />
<PackageReference Include="QuestPDF" Version="2024.12.3" />
<PackageReference Include="ZDatabase" Version="1.4.0" />
<PackageReference Include="ZSecurity" Version="1.3.0" />
<PackageReference Include="ZDatabase" Version="1.5.1" />
<PackageReference Include="ZSecurity" Version="1.3.1" />
</ItemGroup>

</Project>
Loading