diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6dc6bf7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Git +.git +.gitignore +.gitattributes + +# Build outputs +bin/ +obj/ +*.dll +*.exe +*.pdb + +# IDE +.vs/ +.vscode/ +*.user +*.sln.iml +.idea/ + +# CI/CD +.github/ +Jenkinsfile + +# Package managers +*.lock + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +LICENSE + +# Test results +TestResults/ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..4244372 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,109 @@ +# Copilot Instructions for MyNumber.NET + +## Project Overview +MyNumber.NET is a .NET 8.0 library for validating and generating Japanese My Number (Social Security/Tax ID). The solution contains four projects: +- **MyNumberNET**: Core validation/generation library +- **MyNumberNET_ApiServer**: ASP.NET Core REST API wrapper +- **MyNumberNET_CLI**: Command-line interface with NLog logging +- **MyNumberNET_Test**: xUnit unit tests + +## Architecture & Key Patterns + +### Core Validation Logic +The `MyNumber` static class in [MyNumberNET/MyNumber.cs](MyNumberNET/MyNumber.cs) implements the My Number algorithm: +- **VerifyNumber()**: Validates 12-digit arrays using check digit algorithm (positions 1-6 use weight 2-7, positions 7-11 use weight 1-6) +- **CalculateCheckDigits()**: Computes 12th digit from first 11 (modulo 11 formula: if sum%11 ≤ 1, checkdigit=0; else checkdigit=11-sum%11) +- Throws `MyNumberMalformedException` for invalid input (null, wrong length, non-digits) + +### MyNumberValue - Type-Safe Wrapper +`MyNumberValue` readonly struct (in [MyNumberNET/MyNumber.cs](MyNumberNET/MyNumber.cs)) wraps validated numbers: +- Immutable and always valid on construction +- Multiple constructors: from int[], from 12 individual digits, or `FromFirstElevenDigits()` (auto-calculates check digit) +- `TryParse(string)` handles separator formats (spaces, hyphens, underscores) +- `ToString(format)` supports: "N" (plain), "S" (spaced), "H" (hyphenated), "G" (grouped) + +### API Layer Pattern +[MyNumberNET_ApiServer/Controllers/MyNumberController.cs](MyNumberNET_ApiServer/Controllers/MyNumberController.cs) duplicates input validation before calling business logic - this is intentional to prevent malformed requests from reaching the library. + +## Critical Workflows + +### Building & Testing +```powershell +dotnet build # Build solution +dotnet test MyNumberNET_Test # Run tests +dotnet run --project MyNumberNET_ApiServer # Start API on port 5000+ +dotnet run --project MyNumberNET_CLI -- generate 10 # CLI with args +``` + +### CLI Commands +Located in [MyNumberNET_CLI/Program.cs](MyNumberNET_CLI/Program.cs) switch statement: +- `generate [count]` - Random valid My Numbers +- `check [number]` - Validate single number +- `complete [11digits]` - Calculate check digit +- `rangen [min] [max]` - Numerical range generation +- `ranges [min] [max]` - Sequential range generation + +### Version Management +All versions defined in [Directory.Build.props](Directory.Build.props): +- Update `MyNumberNETVersion`, `MyNumberNETCliVersion`, `MyNumberNETTestVersion` for releases +- Target framework: .NET 8.0 (specified in [global.json](global.json)) + +## Integration Points & Dependencies + +### Exception Hierarchy +- Base: `MyNumber.MyNumberException(message)` +- Specific: `MyNumber.MyNumberMalformedException(message)` - thrown on validation failure +- API returns `BadRequest` + error message on exceptions; never propagates exceptions + +### NLog Configuration +CLI project uses [MyNumberNET_CLI/nlog.config](MyNumberNET_CLI/nlog.config) for logging. Logger accessed via `LogManager.GetCurrentClassLogger()`. + +### No External Dependencies +Core library only uses System.* namespaces - no NuGet dependencies. Keep it minimal. + +## Code Patterns + +### Error Handling +1. Validate inputs early (null, length, digit range) +2. Throw `MyNumberMalformedException` with descriptive messages +3. In API: catch exceptions and return `BadRequest(ex.Message)` + +### Digit Array Conventions +- 12-digit arrays: indices 0-10 are data, 11 is check digit +- Use `Truncate()` private method to extract first 11 digits +- Always clone arrays when storing (immutability) + +### Testing Strategy +Test files: [MyNumberNET_Test/MyNumberValueTests.cs](MyNumberNET_Test/MyNumberValueTests.cs), [MyNumberNET_Test/MyNumberControllerTests.cs](MyNumberNET_Test/MyNumberControllerTests.cs) +- Test both valid and malformed inputs +- Verify exception messages and types +- Test all format options for ToString()## Containerization & Deployment + +### Docker +[MyNumberNET_ApiServer/Dockerfile](MyNumberNET_ApiServer/Dockerfile) uses multi-stage build: +1. **Build stage**: Restores, builds, and publishes using .NET 8.0 SDK +2. **Runtime stage**: Runs on .NET 8.0 ASP.NET Core runtime (smaller image) +3. **Port**: Exposes 5000 (configurable via `ASPNETCORE_URLS` env var) +4. **Health check**: Built-in liveness probe every 30 seconds + +Local Docker usage: +```bash +docker build -f MyNumberNET_ApiServer/Dockerfile -t mynumberapi:latest . +docker run -p 5000:5000 mynumberapi:latest +``` + +### GitHub Container Registry +Images pushed to `ghcr.io/hsaito/mynumberapi` on main branch pushes and tags (v* releases). +Tags: branch name, semantic version (v1.0.0), git SHA. + +## CI/CD + +### GitHub Actions Workflows +- **[.github/workflows/dotnet-ci.yml](.github/workflows/dotnet-ci.yml)**: Cross-platform build/test (Windows, Linux, macOS) +- **[.github/workflows/docker-build.yml](.github/workflows/docker-build.yml)**: Docker image build and push to ghcr.io + - Automatically tags releases (semantic versioning supported) + - PR builds test image without pushing + - Uses Buildx for efficient multi-stage builds + +### Legacy CI +[Jenkinsfile](Jenkinsfile) provides additional build/test/package orchestration. Deterministic builds enabled via [Directory.Build.props](Directory.Build.props) for reproducible artifacts. diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..b3d1748 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,75 @@ +name: Docker Build & Push + +permissions: + contents: read + packages: write + +on: + push: + branches: [ "main" ] + tags: [ "v*" ] + pull_request: + branches: [ "main" ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/mynumberapi + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./MyNumberNET_ApiServer/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + test-docker-image: + needs: build-and-push + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./MyNumberNET_ApiServer/Dockerfile + push: false + cache-from: type=gha diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index de0ff5a..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,30 +0,0 @@ -pipeline { - agent any - - stages { - stage('Build') { - steps { - echo 'Building..' - powershell label: '', script: '''dotnet restore; dotnet build''' - } - } - - stage('Test') { - steps { - echo 'Testing..' - powershell label: '', script: '''dotnet restore; dotnet test -c Release''' - } - } - stage('Deploy') { - steps { - echo 'Deploying..' - powershell label: '', script: '''dotnet restore; dotnet publish -c Release''' - powershell label: '', script: '''dotnet pack -c Release''' - archiveArtifacts artifacts: 'MyNumberNET/bin/Release/netstandard2.0/publish/**' - archiveArtifacts artifacts: 'MyNumberNET/bin/Release/*.nupkg' - archiveArtifacts artifacts: 'MyNumberNET_CLI/bin/Release/netcoreapp2.1/publish/' - } - } - - } -} diff --git a/MyNumberNET_ApiServer/Dockerfile b/MyNumberNET_ApiServer/Dockerfile new file mode 100644 index 0000000..1012b1c --- /dev/null +++ b/MyNumberNET_ApiServer/Dockerfile @@ -0,0 +1,43 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy project files +COPY ["MyNumberNET/MyNumberNET.csproj", "MyNumberNET/"] +COPY ["MyNumberNET_ApiServer/MyNumberNET_ApiServer.csproj", "MyNumberNET_ApiServer/"] +COPY ["Directory.Build.props", "."] +COPY ["global.json", "."] + +# Restore dependencies +RUN dotnet restore "MyNumberNET_ApiServer/MyNumberNET_ApiServer.csproj" + +# Copy source files +COPY ["MyNumberNET/", "MyNumberNET/"] +COPY ["MyNumberNET_ApiServer/", "MyNumberNET_ApiServer/"] + +# Build application +RUN dotnet build "MyNumberNET_ApiServer/MyNumberNET_ApiServer.csproj" -c Release --no-restore + +# Publish application +RUN dotnet publish "MyNumberNET_ApiServer/MyNumberNET_ApiServer.csproj" -c Release -o /app/publish --no-build + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +# Copy published application from build stage +COPY --from=build /app/publish . + +# Set environment variables for ASP.NET Core +ENV ASPNETCORE_URLS=http://+:5000 +ENV ASPNETCORE_ENVIRONMENT=Production + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD dotnet /app/MyNumberNET_ApiServer.dll || exit 1 + +# Start application +ENTRYPOINT ["dotnet", "MyNumberNET_ApiServer.dll"]