diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..906956d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+**/.git
+**/.gitea
+**/.github
+**/.vs
+**/bin
+**/obj
+artifacts
+Dockerfile*
+*.user
+*.suo
diff --git a/.gitea/workflows/release-docker.yml b/.gitea/workflows/release-docker.yml
new file mode 100644
index 0000000..fc6101d
--- /dev/null
+++ b/.gitea/workflows/release-docker.yml
@@ -0,0 +1,46 @@
+name: Release Docker Image
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+env:
+ REGISTRY: reg.sabp.ir
+ IMAGE_NAME: sabp-apps/devops-test
+ RELEASE_TAG: ${{ gitea.event.release.tag_name }}
+ REF_NAME: ${{ gitea.ref_name }}
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Login to Harbor
+ shell: bash
+ env:
+ HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
+ HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
+ run: |
+ set -euo pipefail
+ if [ -z "${HARBOR_USERNAME:-}" ] || [ -z "${HARBOR_PASSWORD:-}" ]; then
+ echo "HARBOR_USERNAME and HARBOR_PASSWORD secrets are required."
+ exit 1
+ fi
+
+ echo "$HARBOR_PASSWORD" | docker login "$REGISTRY" --username "$HARBOR_USERNAME" --password-stdin
+
+ - name: Build and push image
+ shell: bash
+ run: |
+ set -euo pipefail
+ tag="${RELEASE_TAG:-$REF_NAME}"
+ tag="${tag#v}"
+ image="$REGISTRY/$IMAGE_NAME"
+
+ docker build --pull -t "$image:$tag" -t "$image:latest" .
+ docker push "$image:$tag"
+ docker push "$image:latest"
diff --git a/.gitea/workflows/release-nuget.yml b/.gitea/workflows/release-nuget.yml
new file mode 100644
index 0000000..f3c109d
--- /dev/null
+++ b/.gitea/workflows/release-nuget.yml
@@ -0,0 +1,87 @@
+name: Release NuGet Packages
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ NUGET_SOURCE_URL: https://nuget.sabp.ir/v3/index.json
+ PACKAGE_OUTPUT: ./artifacts/nuget
+ PACKAGE_PROJECTS: DevOpsPackageTest/DevOpsPackageTest.csproj
+ RELEASE_TAG: ${{ gitea.event.release.tag_name }}
+ REF_NAME: ${{ gitea.ref_name }}
+
+jobs:
+ pack-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+
+ - name: Restore selected projects
+ shell: bash
+ run: |
+ set -euo pipefail
+ for project in $PACKAGE_PROJECTS; do
+ dotnet restore "$project"
+ done
+
+ - name: Build selected projects
+ shell: bash
+ run: |
+ set -euo pipefail
+ for project in $PACKAGE_PROJECTS; do
+ dotnet build "$project" --configuration Release --no-restore
+ done
+
+ - name: Pack selected projects
+ shell: bash
+ run: |
+ set -euo pipefail
+ mkdir -p "$PACKAGE_OUTPUT"
+
+ version="${RELEASE_TAG:-$REF_NAME}"
+ version="${version#v}"
+
+ for project in $PACKAGE_PROJECTS; do
+ dotnet pack "$project" \
+ --configuration Release \
+ --no-build \
+ --output "$PACKAGE_OUTPUT" \
+ -p:PackageVersion="$version" \
+ -p:ContinuousIntegrationBuild=true
+ done
+
+ - name: Push packages to BaGet
+ shell: bash
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ run: |
+ set -euo pipefail
+ if [ -z "${NUGET_API_KEY:-}" ]; then
+ echo "NUGET_API_KEY secret is required."
+ exit 1
+ fi
+
+ shopt -s nullglob
+ packages=("$PACKAGE_OUTPUT"/*.nupkg)
+ if [ ${#packages[@]} -eq 0 ]; then
+ echo "No NuGet packages were produced."
+ exit 1
+ fi
+
+ for package in "${packages[@]}"; do
+ dotnet nuget push "$package" \
+ --source "$NUGET_SOURCE_URL" \
+ --api-key "$NUGET_API_KEY" \
+ --skip-duplicate
+ done
diff --git a/CI_CHECKLIST.md b/CI_CHECKLIST.md
new file mode 100644
index 0000000..43fb80c
--- /dev/null
+++ b/CI_CHECKLIST.md
@@ -0,0 +1,20 @@
+# CI/CD Checklists
+
+## Can do in codebase
+
+- [x] Update Gitea release workflow to pack `DevOpsPackageTest/DevOpsPackageTest.csproj`
+- [x] Update Gitea release workflow to restore/build/pack selected project paths only
+- [x] Add Dockerfile for `DevOpsTest`
+- [x] Add `.dockerignore` to keep build context clean
+- [x] Add package metadata to `DevOpsPackageTest.csproj`
+
+## Cannot do from codebase alone
+
+- [ ] Create or verify Gitea repository secrets: `NUGET_API_KEY`, `HARBOR_USERNAME`, `HARBOR_PASSWORD`
+- [ ] Create the actual Gitea release and confirm workflow triggers
+- [ ] Confirm package upload success on `https://nuget.sabp.ir`
+- [ ] Confirm Harbor project, credentials, and push permissions on `reg.sabp.ir`
+- [ ] Confirm the published image is visible in Harbor and can be pulled by the production VM
+- [ ] Run end-to-end smoke tests against the real registry and package server
+- [ ] Validate the workflow against the real Gitea Actions runner environment
+
diff --git a/DevOpsPackageTest/Class1.cs b/DevOpsPackageTest/Class1.cs
new file mode 100644
index 0000000..33c7b72
--- /dev/null
+++ b/DevOpsPackageTest/Class1.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace DevOpsPackageTest
+{
+ public class Class1
+ {
+
+ }
+}
diff --git a/DevOpsPackageTest/DevOpsPackageTest.csproj b/DevOpsPackageTest/DevOpsPackageTest.csproj
new file mode 100644
index 0000000..ffc7b52
--- /dev/null
+++ b/DevOpsPackageTest/DevOpsPackageTest.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.1
+ enable
+ true
+ false
+ DevOpsPackageTest
+ SABP
+ DevOps package test project for SABP CI/CD validation.
+ git
+
+
+
diff --git a/DevOpsTest.csproj b/DevOpsTest.csproj
index 6568b3d..a3a34b6 100644
--- a/DevOpsTest.csproj
+++ b/DevOpsTest.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
enable
enable
diff --git a/DevOpsTest.sln b/DevOpsTest.sln
index 726161d..0a2b9b8 100644
--- a/DevOpsTest.sln
+++ b/DevOpsTest.sln
@@ -1,10 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.14.36616.10 d17.14
+VisualStudioVersion = 17.14.36616.10
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevOpsTest", "DevOpsTest.csproj", "{F5CD1119-847D-4C65-B472-C34A25F778CB}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevOpsPackageTest", "DevOpsPackageTest\DevOpsPackageTest.csproj", "{BFABC423-0F62-D597-5CE8-75498604700E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{F5CD1119-847D-4C65-B472-C34A25F778CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5CD1119-847D-4C65-B472-C34A25F778CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5CD1119-847D-4C65-B472-C34A25F778CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFABC423-0F62-D597-5CE8-75498604700E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFABC423-0F62-D597-5CE8-75498604700E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFABC423-0F62-D597-5CE8-75498604700E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFABC423-0F62-D597-5CE8-75498604700E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..cc3967d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+# syntax=docker/dockerfile:1
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+COPY DevOpsTest.csproj ./
+RUN dotnet restore DevOpsTest.csproj
+
+COPY . ./
+RUN dotnet publish DevOpsTest.csproj \
+ --configuration Release \
+ --no-restore \
+ --output /app/publish \
+ /p:UseAppHost=false
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+
+ENV ASPNETCORE_URLS=http://+:8080 \
+ ASPNETCORE_ENVIRONMENT=Production
+
+EXPOSE 8080
+
+COPY --from=build /app/publish ./
+ENTRYPOINT ["dotnet", "DevOpsTest.dll"]