diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 97f5177..fee26d9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
@@ -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
diff --git a/.releaserc b/.releaserc
deleted file mode 100644
index 23614ae..0000000
--- a/.releaserc
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "branches": [
- "v+([0-9])?(.{+([0-9]),x}).x",
- "main",
- {
- "name": "beta",
- "prerelease": true
- },
- {
- "name": "alpha",
- "prerelease": true
- }
- ],
- "plugins": [
- [
- "@semantic-release/commit-analyzer",
- {
- "preset": "angular",
- "releaseRules": [
- { "type": "bug", "release": "patch" },
- { "type": "fix", "release": "patch" },
- { "type": "refactor", "release": "patch" },
- { "type": "feat", "release": "minor" },
- { "type": "breaking-changes", "release": "major" },
- { "scope": "no-release", "release": false }
- ]
- }
- ],
- "@semantic-release/release-notes-generator",
- "@semantic-release/github",
- [
- "@semantic-release/git",
- {
- "assets": [
- "ZDatabase.csproj"
- ],
- "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
- }
- ]
- ]
-}
\ No newline at end of file
diff --git a/ZWebAPI/Models/BatchUpdateModel.cs b/ZWebAPI/Models/BatchUpdateModel.cs
new file mode 100644
index 0000000..9f0d606
--- /dev/null
+++ b/ZWebAPI/Models/BatchUpdateModel.cs
@@ -0,0 +1,27 @@
+namespace ZWebAPI.Models
+{
+ ///
+ /// Batch update model for entities.
+ ///
+ /// The type of the entity.
+ /// The type of the key.
+ public class BatchUpdateModel
+ where TEntity : class
+ {
+ ///
+ /// Gets or sets the entities to delete.
+ ///
+ ///
+ /// The entities to delete.
+ ///
+ public TKey[]? EntitiesToDelete { get; set; }
+
+ ///
+ /// Gets or sets the entities to add or update.
+ ///
+ ///
+ /// The entities to add or update.
+ ///
+ public TEntity[]? EntitiesToInsertOrUpdate { get; set; }
+ }
+}
diff --git a/ZWebAPI/Models/RelationshipUpdateModel.cs b/ZWebAPI/Models/RelationshipUpdateModel.cs
new file mode 100644
index 0000000..f9ac495
--- /dev/null
+++ b/ZWebAPI/Models/RelationshipUpdateModel.cs
@@ -0,0 +1,25 @@
+namespace ZWebAPI.Models
+{
+ ///
+ /// Update model for entity relationships.
+ ///
+ /// The type of the key.
+ public class RelationshipUpdateModel
+ {
+ ///
+ /// Gets or sets the ids to add.
+ ///
+ ///
+ /// The ids to add.
+ ///
+ public IEnumerable IDsToAdd { get; set; } = Enumerable.Empty();
+
+ ///
+ /// Gets or sets the ids to remove.
+ ///
+ ///
+ /// The ids to remove.
+ ///
+ public IEnumerable IDsToRemove { get; set; } = Enumerable.Empty();
+ }
+}
diff --git a/ZWebAPI/ZWebAPI.csproj b/ZWebAPI/ZWebAPI.csproj
index e0e5af3..173f9a1 100644
--- a/ZWebAPI/ZWebAPI.csproj
+++ b/ZWebAPI/ZWebAPI.csproj
@@ -8,12 +8,15 @@
Ricardo Zambon
ZambonSoft
1.0.0
- https://github.com/RicardoZambon/ZSecurity
+ GPL-3.0-only
+ webapi;aspnetcore;rest;crud
+ https://github.com/RicardoZambon/ZWebAPI
+ git
README.md
True
ZWebAPI is a library that brings additional functionalities for web API projects.
ZambonSoft
- https://github.com/RicardoZambon/ZSecurity
+ https://github.com/RicardoZambon/ZWebAPI
@@ -27,8 +30,8 @@
-
-
+
+
diff --git a/release.config.cjs b/release.config.cjs
new file mode 100644
index 0000000..4bd1132
--- /dev/null
+++ b/release.config.cjs
@@ -0,0 +1,44 @@
+const isValidation = process.env.SEMANTIC_RELEASE_VALIDATION === 'true';
+
+const plugins = [
+ [
+ '@semantic-release/commit-analyzer',
+ {
+ preset: 'angular',
+ releaseRules: [
+ { type: 'bug', release: 'patch' },
+ { type: 'fix', release: 'patch' },
+ { type: 'refactor', release: 'patch' },
+ { type: 'feat', release: 'minor' },
+ { type: 'breaking-changes', release: 'major' },
+ { scope: 'no-release', release: false },
+ ],
+ },
+ ],
+ '@semantic-release/release-notes-generator',
+];
+
+if (!isValidation) {
+ plugins.push(
+ '@semantic-release/github',
+ [
+ '@semantic-release/git',
+ {
+ assets: ['ZWebAPI/ZWebAPI.csproj'],
+ message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
+ },
+ ],
+ );
+}
+
+module.exports = {
+ branches: isValidation
+ ? [process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || 'main']
+ : [
+ 'v+([0-9])?(.{+([0-9]),x}).x',
+ 'main',
+ { name: 'beta', prerelease: true },
+ { name: 'alpha', prerelease: true },
+ ],
+ plugins,
+};