diff --git a/.gitattributes b/.gitattributes index 6c9e3ad8..7d7d1a92 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,15 +9,19 @@ *.fsx text eol=lf # Scripts -# PowerShell scripts: LF line endings in the index. Required for the -# `#!/usr/bin/env pwsh` shebang to work on Linux/macOS — the kernel -# parses CR as part of the interpreter name (looking for `pwsh\r`). -# BOM avoidance is enforced separately via `.editorconfig` -# (charset = utf-8 in the global [*] section); git attributes can -# only normalize line endings, not byte-order marks. -# Modern PowerShell 7+ handles LF on Windows transparently; Git's -# autocrlf still gives Windows users CRLF in their working tree if -# desired without forcing CRLF into the index. +# PowerShell scripts: enforce LF line endings in both the index AND +# the working tree (the explicit `eol=lf` overrides any user-level +# `core.autocrlf=true` setting that would otherwise convert to CRLF +# on checkout). BOM-free UTF-8 encoding is enforced separately by +# .editorconfig's global `charset = utf-8` — `.gitattributes` can +# normalize line endings (and control text/binary, diff, and merge +# behavior) but has no attribute for byte-order marks. +# +# LF is required for the `#!/usr/bin/env pwsh` shebang to work on +# Linux/macOS — the kernel parses CR as part of the interpreter name +# (looking for `pwsh\r`), and a UTF-8 BOM before `#!` would prevent +# shebang recognition entirely. Modern PowerShell 7+ handles LF on +# Windows transparently. *.ps1 text eol=lf diff --git a/.github/ISSUE_TEMPLATE/maintenance-task.yaml b/.github/ISSUE_TEMPLATE/maintenance-task.yaml new file mode 100644 index 00000000..9c8695d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance-task.yaml @@ -0,0 +1,76 @@ +name: "✨ Maintenance task" +description: "Track an actionable improvement under the Maintenance framework (security, performance, testing, cleanup, docs, API, or CI/CD)." +title: "[Maintenance] : " +labels: [maintenance-task] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## Maintenance framework sub-issue + + This template creates a **`maintenance-task`** sub-issue under this repo's parent `Maintenance: ` issue. The new issue will auto-appear in the cross-repo Maintenance project board (URL listed in the parent `Maintenance: ` issue body). + + - Pick exactly **one** category in the dropdown below. The corresponding `maintenance - ` label will need to be added manually after creation (issue forms don't yet support dynamic label addition). + - Fill in **Scope** (what's done & why), **Acceptance** (when do we close this?), and **Links** (PRs, scan output, related issues). + - If you're closing this issue via a PR, include `Fixes #` in the PR body so the auto-add workflow marks the project item as Done. + + - type: dropdown + id: category + attributes: + label: Category + description: "Pick exactly one. After creation, also add the corresponding `maintenance - ` label." + options: + - "security — scans, finding fixes, dependency vulnerability audit" + - "performance — profile, benchmark, optimize, validate gains" + - "testing — coverage, integration/smoke/mutation tests, fixtures" + - "cleanup — refactor for reuse / quality / efficiency" + - "docs — XML doc coverage, README, CHANGELOG, samples" + - "API — public/internal surface audit, breaking-change vigilance" + - "CI/CD — Docker, CI workflow, build/publish pipeline" + validations: + required: true + + - type: textarea + id: scope + attributes: + label: Scope + description: "What needs to be done? What's the motivation?" + placeholder: | + e.g. + - Profile the hot path in `Extractor.ExtractAsync` and identify the dominant allocations. + - Goal: reduce per-record allocations to enable streaming larger inputs. + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: Acceptance criteria + description: "When do we close this issue? Concrete observable outcomes." + placeholder: | + e.g. + - Benchmark X shows <50% of current allocations. + - No regression in existing benchmark suite (>5%). + - Updated benchmark numbers committed. + validations: + required: true + + - type: textarea + id: links + attributes: + label: Links + description: "Related PRs, scan output, prior issues, external references." + placeholder: | + - Related PR: #... + - Scan output: ... + - Related issue: #... + validations: + required: false + + - type: markdown + attributes: + value: | + --- + + 💡 **Tip for agents (Copilot etc.):** When opening a maintenance-task sub-issue, use this template and pick the matching category. After creation, add the `maintenance - ` label manually. See `.github/copilot-instructions.md` for the full Maintenance framework convention. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7f072991..f3c82434 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,6 +4,18 @@ Fixes/Complete # (issue) + + + ## Type of change Please delete options that are not relevant. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8d791e43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,99 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security + +## [0.13.1] - 2026-06-19 + +Canonical maintenance round + binding-stability fix. No public API or +runtime behavior change vs v0.13.0. This release is the prerequisite +the downstream ETL family (`ETL-Test-Kit`, `ETL-Xml`, `ETL-Json`, +`ETL-FixedWidth`, `Etl-DbClient`, the in-development +`ETL-Csv`/`ETL-SqlBulkCopy`/`ETL-Transformers`) consumes by NuGet +reference — bumping it first lets each downstream pilot inherit the +canonical fixes cleanly rather than each fighting mixed-state +dependencies. + +### Added + +- **D8** — `verify-docs-build` job in `release.yaml` runs DocFX during + the release pipeline before the NuGet push, so a docs build failure + now blocks the package from shipping. +- **D8** — docs site version picker assets + (`docfx_project/public/version-picker.js`, + `docfx_project/versions.json`, + `docs/DOCFX-VERSION-PICKER.md`). +- **A1** — `PublicApiAnalyzers` scaffolding (analyzers activate when + `PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt` are present + alongside the csproj). +- **CI3** — canonical NuGet package metadata: `Authors`, `Copyright`, + `RepositoryType`, SourceLink, snupkg symbol packages, deterministic + CI build flag, and `EmbedUntrackedSources` hoisted to + `Directory.Build.props`. +- **T3** — Stryker mutation-testing workflow (`stryker.yaml`). +- **T1** — coverage report published to docs site. +- **S1** — CodeQL `security-extended` query pack. +- **D6** — versions.json preservation guard on the docs deploy. + +### Changed + +- **C1** — fleet-wide template-drift sync: workflow files (`pr.yaml`, + `release.yaml`, `docfx.yaml`, `codeql.yaml`, + `build-all-versions.yaml`, `stryker.yaml`), `.editorconfig`, + `BannedSymbols.txt`, `Directory.Build.props`, and per-context + `tests/Directory.Build.props` consolidated to the canonical baseline. +- **Nullable** — `enable` consolidated into + `Directory.Build.props` (was per-csproj); per-project opt-out via + override still supported. +- **CI2** — Dependabot `github-actions` ecosystem added. +- **D3** — repo scripts hardened (`Setup-Labels.ps1`, + `Fix-BranchRuleset.ps1`). +- `github/codeql-action/init` and `analyze` bumped v3 → v4 (Node.js + 20 → 24 deprecation). +- **Docs** — README accuracy pass: corrected the Target Frameworks + table (dropped the untargeted .NET 4.7.0 / 4.7.1 rows, added the + missing .NET Standard 2.0 row), analyzer count 7 → 8 + (`Microsoft.CodeAnalysis.PublicApiAnalyzers`), and the build + prerequisite (.NET 8.0 → .NET 10.0 SDK). `CONTRIBUTING.md` analyzer + list updated to match. + +### Removed + +- `REPO-INSTRUCTIONS.md` — the repo-template post-setup bootstrap + checklist ("once you have completed the checklist below you can + delete this file"); setup is long complete. + +### Fixed + +- **Docs** — corrected stale XML-doc `` references found in a + code-review pass: `LoaderBase` / `TransformerBase` examples referenced + a non-existent `MaxItemCount` (corrected to `MaximumItemCount`), and + `SystemProgressTimer` pointed at a non-existent `ManualProgressTimer` + type (corrected to a resolvable `IProgressTimer` reference). +- **C4** — restored explicit `1.0.0.0` + and added a prerelease-safe `` (regex-strip property + function) to the src csproj. The original C4 fanout had dropped + these on the rationale that the hardcoded values were "stale" + relative to released package versions — but that staleness was the + correct binding-stability behaviour for libraries that ship a + `net462` TFM. Without an explicit pin, SDK-derived `AssemblyVersion` + would change on every minor/patch release, breaking .NET Framework + consumers without a binding redirect. (See DateTime-Extensions v1.3.1 + for the post-mortem on what happens when this regression reaches a + release.) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87188fd2..75fbd876 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ You can contribute in several ways: This project maintains **extremely high code quality standards** through multiple layers of static analysis and automated enforcement. -### The 7 Analyzers +### The 8 Analyzers All code is analyzed by these tools during build: @@ -62,9 +62,11 @@ All code is analyzed by these tools during build: - Advanced C# pattern detection 3. **AsyncFixer** - - Detects async/await anti-patterns - - Ensures proper `ConfigureAwait()` usage - - Prevents fire-and-forget async calls + - Detects common async/await anti-patterns (AsyncFixer01–05) + - Flags missing or incorrect cancellation-token propagation + - Prevents fire-and-forget async calls (`async void` outside event handlers) + - NOTE: `ConfigureAwait()` enforcement is handled by Meziantou's + MA0004 / SonarAnalyzer S3216 / CA2007, not by AsyncFixer. 4. **Microsoft.VisualStudio.Threading.Analyzers** - Thread safety enforcement @@ -85,6 +87,10 @@ All code is analyzed by these tools during build: - Security vulnerability detection - Code smell identification +8. **Microsoft.CodeAnalysis.PublicApiAnalyzers** + - Tracks the declared public API surface (`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`) + - Flags unintended additions, removals, or signature changes as breaking-change risks + ### Async-First Enforcement This library **prohibits synchronous blocking calls** via `BannedSymbols.txt`. The following APIs are **banned**: @@ -145,7 +151,7 @@ var now = DateTimeOffset.UtcNow; ## Build and Test Instructions ### Prerequisites -- .NET 8.0 SDK or later +- .NET 10.0 SDK or later (required for the repo's net10.0 target; older SDKs cannot load the csproj) - PowerShell Core (optional, for formatting scripts) ### Build the Project @@ -185,7 +191,7 @@ dotnet format --verify-no-changes pwsh ./scripts/format.ps1 ``` -See [README-FORMATTING.md](docs/README-FORMATTING.md) for detailed formatting rules. +See [docs/README-FORMATTING.md](docs/README-FORMATTING.md) for detailed formatting rules. --- @@ -237,4 +243,3 @@ Please be respectful and considerate in all interactions. See [CODE_OF_CONDUCT.m --- Thank you for contributing! 🎉 - diff --git a/README.md b/README.md index 76146162..8980c3ad 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ is no new runtime behavior, no buffering, and no additional allocations per item | Fluent Pipeline | `Pipeline.Extract(...).Transform(...).Load(...).RunAsync()` with compile-time stage typing | | Progress Reporting | Built-in `IProgress` support with configurable reporting intervals | | Cancellation | Full `CancellationToken` support across all operations | -| Multi-TFM | Targets .NET Framework 4.6.2+, .NET Standard 2.0+, and .NET 5.0–10.0 | +| Multi-TFM | Targets .NET Framework 4.6.2–4.8.1, .NET Standard 2.0, and .NET 5.0–10.0 | | Skip & Limit | `SkipItemCount` and `MaximumItemCount` for partial extraction/loading | | Thread Safety | `Interlocked`-based counters for safe concurrent progress tracking | @@ -116,15 +116,15 @@ is no new runtime behavior, no buffering, and no additional allocations per item | Framework | Versions | |-----------|----------| -| .Net Framework | .net 4.6.2, .net 4.7.0, .net 4.7.1, .net 4.7.2, .net 4.8, .net 4.8.1 | -| .Net Core | | -| .Net | .net 5.0, .net 6.0, .net 7.0, .net 8.0, .net 9.0, .net 10.0 | +| .NET Framework | 4.6.2, 4.7.2, 4.8, 4.8.1 | +| .NET Standard | 2.0 | +| .NET | 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 | --- ## 🔍 Code Quality & Static Analysis -This project enforces **strict code quality standards** through **7 specialized analyzers** and custom async-first rules: +This project enforces **strict code quality standards** through **8 specialized analyzers** and custom async-first rules: ### Analyzers in Use @@ -135,6 +135,7 @@ This project enforces **strict code quality standards** through **7 specialized 5. **Microsoft.CodeAnalysis.BannedApiAnalyzers** - Prevents usage of banned synchronous APIs 6. **Meziantou.Analyzer** - Comprehensive code quality rules 7. **SonarAnalyzer.CSharp** - Industry-standard code analysis +8. **Microsoft.CodeAnalysis.PublicApiAnalyzers** - Tracks the public API surface to catch unintended breaking changes ### Async-First Enforcement @@ -155,7 +156,7 @@ This library uses **`BannedSymbols.txt`** to prohibit synchronous APIs and enfor ## 🛠️ Building from Source ### Prerequisites -- [.NET 8.0 SDK](https://dotnet.microsoft.com/download) or later +- [.NET 10.0 SDK](https://dotnet.microsoft.com/download) or later (required to build all target frameworks) - Optional: [PowerShell Core](https://github.com/PowerShell/PowerShell) for formatting scripts ### Build Steps diff --git a/REPO-INSTRUCTIONS.md b/REPO-INSTRUCTIONS.md deleted file mode 100644 index 3632a85b..00000000 --- a/REPO-INSTRUCTIONS.md +++ /dev/null @@ -1,265 +0,0 @@ -# Setting Up Your Repository - -## Automated Setup (Recommended) - -**NEW:** This template now includes automated setup scripts that handle configuration for you! - -### Quick Setup - -```powershell -pwsh ./scripts/setup.ps1 -``` - -**Note:** There are multiple scripts in this template: -- `scripts/setup.ps1` - Main repository setup (replaces placeholders, configures license) -- `scripts/Setup-BranchRuleset.ps1` - Branch protection configuration (run after setup) -- `scripts/Setup-GitHubPages.ps1` - GitHub Pages and DocFX documentation setup (optional) - -The main setup script will: -1. ✅ Prompt for all required information (with examples and defaults) -2. ✅ Auto-detect git repository information where possible -3. ✅ Replace placeholders in core template files (see TEMPLATE-PLACEHOLDERS.md for details and any manual steps, including DocFX docs) -4. ✅ Delete the template README.md -5. ✅ Rename README-TEMPLATE.md to README.md -6. ✅ Set up your chosen LICENSE (MIT, Apache 2.0, or MPL 2.0) -7. ✅ Remove unused license templates -8. ✅ **Optionally create a default .slnx solution file** with proper folder structure (requires Visual Studio 2022 17.10+) -9. ✅ Validate all replacements -10. ✅ Optionally clean up template-specific files - -**For detailed placeholder documentation, see [TEMPLATE-PLACEHOLDERS.md](TEMPLATE-PLACEHOLDERS.md)** -**For license selection guidance, see [LICENSE-SELECTION.md](LICENSE-SELECTION.md)** - ---- - -## Manual Setup Instructions - -After you create your repo from the template you will still need to configure some settings. -Below is a list of what needs to be done. Once you have completed the checklist below you can delete this file - -## Creating Your Repository - -1. On the `Repositories` page click `New` -1. On the `Create a new repository` page enter - 1. `Repository name` - 2. `Description` - 3. Select `Public` or `Private` -1. `Start with a template` select `Chris-Wolfgang/repo-template` -1. `Include all branches` set `On` - this will include the `develop` branch. If you don't want the `develop` branch or if there are other branches you don't want you can leave this `off` and create the `develop` branch in your new repository - - -## Add Branch Protection Rules - -> **Note:** Branch protection is now configured using a local PowerShell script. After setting up your repository, run the script to configure branch protection: -> ```powershell -> pwsh ./scripts/Setup-BranchRuleset.ps1 -> ``` -> The script includes interactive prompts that allow you to choose between **single developer** or **multi-developer** repository settings during execution. Simply run the script and select option [1] for single-developer mode (no approvals required) or option [2] for multi-developer mode (requires 1+ approval and code owner review). - -If you need to manually configure branch protection instead: - -1. Go to your repository’s Settings → Branches. -2. Under “Branch protection rules,” click `Add branch ruleset` -3. `Ruleset Name` enter `main` -4. `Target branches` click `Add target` -5. Select `Include by pattern` -6. `Branch naming pattern` enter `main` -7. Click `Add Inclusion pattern` - - -## Security Settings - -Prevent Merging When Checks Fail -These settings require that all checks in the pr.yaml file succeed before you can merge a branch into main - -> **Note for Single-Developer Repositories:** This template is configured for single-developer use. The branch protection script (`scripts/Setup-BranchRuleset.ps1`) includes interactive prompts that allow you to choose between single-developer or multi-developer settings during execution. Simply run the script and select option [1] for single-developer mode (no PR approvals required) or option [2] for multi-developer mode (requires 1+ approval and code owner review). -**Note:** The pr.yaml workflow uses `pull_request_target` to always run from the trusted main branch, even for PRs from feature branches. This prevents malicious workflow modifications in untrusted PR branches while still testing the PR's code. - -> **Branch protection is now configured via local script!** Run `pwsh ./scripts/Setup-BranchRuleset.ps1` to automatically configure all required settings. Manual configuration below is only needed if you prefer not to use the automated script. - -1. Go to your repository’s Settings → Branches. -2. Under “Branch protection rules,” edit the rule for main. -3. Check “Require status checks to pass before merging.” -4. In the "Status checks that are required" list, select the status check contexts produced by your PR workflow jobs. These options appear after the workflow has run at least once on `main`. For example: - - "Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" - - "Stage 2: Windows Tests (.NET 5.0-10.0, Framework 4.6.2-4.8.1)" - - "Stage 3: macOS Tests (.NET 6.0-10.0)" - - "Security Scan (DevSkim)" - -5. Enable “Require branches to be up to date before merging.” -6. Check `Restrict deletions` -7. Check `Require a pull request before merging` - 1. Check `Dismiss stale pull request approvals when new commits are pushed` - 3. **For multi-developer repos:** Check `Require review from Code Owners` and set required approvals to 1 or more -8. Check `Block force pushes` -9. Check `Require code scanning` - - -## Add Custom Labels - -Run the label setup script once after creating your repository: - -```powershell -pwsh -File ./scripts/Setup-Labels.ps1 -``` - -This creates the following labels used by Dependabot and workflows: - -1. `dependabot - security` -2. `dependabot-dependencies` -3. `dependencies` -4. `dotnet` - -Requires the [GitHub CLI](https://cli.github.com/) to be installed and authenticated (`gh auth login`). - - -## Creating the project - -### Automated Solution Creation (Recommended) - -If you used the automated setup script (`pwsh ./scripts/setup.ps1`), you had the option to create a default solution file automatically. The script creates a `.slnx` format solution (requires Visual Studio 2022 version 17.10+) with the following structure: -- Empty solution folders for `/benchmarks/`, `/examples/`, `/src/`, and `/tests/` -- A `/.root/` folder containing all repository configuration files (preserves directory structure) - -If you chose to create a solution during setup, skip to step 2 below. - -### Manual Solution Creation - -If you didn't create a solution during setup or prefer the traditional `.sln` format: - -1. Create a blank solution and save it in the root folder - ```bash - dotnet new sln -n YourSolutionName - ``` -2. Add new projects to the solution. Each application project will be in its own folder in the /src folder -3. Add one or more test projects each in its own folder in the /tests folder -4. If the solution will have benchmark project add each project in its own folder under /benchmarks - -``` -root -├── MySolution.sln -├── src -│ ├── MyApp -│ │ └── MyApp.csproj -│ └── MyLib -│ └── MyLib.csproj -├── tests -│ ├── MyApp.Tests -│ │ └── MyApp.Tests.csproj -│ └── MyLib.Tests -│ └── MyLib.Tests.csproj -└── benchmarks - └── MyApp.Benchmarks - └── MyApp.Benchmarks.csproj -``` - - -## Configure Release Workflow (Optional) - -If you plan to publish NuGet packages using the automated release workflow, you need to configure the following: - -### Add NuGet API Key Secret - -1. Go to your repository's Settings → Secrets and variables → Actions -2. Click **"New repository secret"** -3. **Name:** `NUGET_API_KEY` -4. **Value:** Your NuGet.org API key - - Get your key from [NuGet.org Account → API Keys](https://www.nuget.org/account/apikeys) - - Recommended scopes: **Push new packages and package versions** - - Set expiration date (recommended: 1 year) -5. Click **"Add secret"** - -**Note:** The release workflow automatically publishes packages to NuGet.org when you publish a GitHub Release (typically associated with a version tag like `v1.0.0`). See [RELEASE-WORKFLOW-SETUP.md](docs/RELEASE-WORKFLOW-SETUP.md) for detailed information about the release workflow, testing, and troubleshooting. - - -## Update Template Files - -After creating your repository from the template, update the following files with your project-specific information: - -### Update README.md - -1. Open `README.md` in the root folder -2. Replace the template content with your project's description -3. Add installation instructions, usage examples, and other relevant information - -### Update CONTRIBUTING.md - -1. Open `CONTRIBUTING.md` -2. Ensure any project name placeholders (for example, `Wolfgang.Etl.Abstractions`) have been replaced with your actual project name (the automated setup scripts should normally do this for you) -3. Review and adjust contribution guidelines as needed for your project - -### Update CODEOWNERS - -1. Open `.github/CODEOWNERS` -2. Replace `@Chris-Wolfgang` with your GitHub username or team names -3. Uncomment and customize the example rules if you want different owners for specific directories - -**Note:** The CODEOWNERS file determines who is automatically requested for review when someone opens a pull request. - -### Setup GitHub Pages for Documentation (Optional) - -If you want to publish your DocFX documentation to GitHub Pages automatically when you publish a GitHub Release: - -1. Run the GitHub Pages setup script: - ```powershell - pwsh ./scripts/Setup-GitHubPages.ps1 - ``` - - The script will: - - **Prompt if you want to set up GitHub Pages** for documentation - - **Auto-detect repository information** (name, description, URLs) - - **Prompt for project details** needed for DocFX configuration - - **Replace placeholders** in DocFX files (Wolfgang.Etl.Abstractions, https://Chris-Wolfgang.github.io/ETL-Abstractions/, etc.) - - Create a `gh-pages` branch if it doesn't exist - - Configure GitHub Pages to serve from the `gh-pages` branch - - Verify that the DocFX workflow is reachable via `workflow_call` from `release.yaml` - - **Note:** If you've already run `scripts/setup.ps1`, the DocFX placeholders are already configured, and this script will skip the configuration step. - -2. After setup, documentation will be automatically published when you publish a GitHub Release: - 1. Go to your repository's **Releases** page - 2. Click **"Draft a new release"** - 3. Choose or create a version tag (e.g., `v1.0.0`) - 4. Click **"Publish release"** - -3. The documentation will be available at: `https://[username].github.io/[repo-name]/` - -**Note:** The DocFX workflow (`.github/workflows/docfx.yaml`) is configured to trigger via: -- **`workflow_call`**: Called automatically by `release.yaml` after a GitHub Release is published (passes the release tag as the version) -- **`workflow_dispatch`**: Manual trigger for ad-hoc builds or dry-runs (available from the Actions tab) - -**Alternative Approach:** If you prefer to configure DocFX placeholders separately from GitHub Pages setup, you can run `scripts/setup.ps1` first (which handles all template placeholders including DocFX), then run `scripts/Setup-GitHubPages.ps1` just to set up the gh-pages branch and GitHub Pages settings. - -### Update Documentation (Optional) - -If you're using DocFX for documentation: -1. Review and customize the generated table of contents in `docfx_project/docs/toc.yml` as needed (the setup scripts already point this to your repository) -2. Customize the rest of the documentation content in `docfx_project/` -### Multi-Version DocFX Documentation - -This repository is configured for versioned documentation using DocFX. The setup consists of: - -#### Key Files -| File | Purpose | -|------|---------| -| `docfx_project/docfx.json` | Per-build DocFX configuration included in this template and used by CI workflows to build docs. Uses `default` + `modern` templates with dark mode enabled (`colorMode: dark`). | -| `docfx_project/logo.svg` | Default repository logo used by DocFX. You can optionally copy this to the repo root as `logo.svg` if you want a root-level logo as well. | - -#### How Versioning Works -- CI workflows discover documentation versions **dynamically at runtime** by querying git tags that match the SemVer pattern `v*.*.*` (e.g. `v1.0.0`, `v0.3.0`). No manual version list is maintained in any config file. -- The `.github/workflows/build-all-versions.yaml` workflow enumerates all matching tags and builds documentation for each — no file updates are required when a new release is published. -- Each release triggers `.github/workflows/release.yaml` (on a published GitHub Release), which calls `.github/workflows/docfx.yaml` via `workflow_call` to build docs and deploy them to the `gh-pages` branch under `versions//`. You can also run `docfx.yaml` directly via `workflow_dispatch` from the Actions tab for ad-hoc builds. -- After every versioned deploy, a `versions.json` is generated and written to `gh-pages`, powering the version-switcher dropdown. -- `versions/latest/` always mirrors the most recent stable release; the site root (`/`) hosts the version-picker landing page that links to the latest and all other available documentation versions. - -#### Adding a New Version -When you publish a new release (e.g. `v1.0.0`): -1. Create and push a version tag (e.g. `v1.0.0`) to the repository. -2. Publish a GitHub Release for that tag — this triggers `release.yaml`, which calls `docfx.yaml` via `workflow_call` to automatically build and publish the docs. You can also run `docfx.yaml` directly via `workflow_dispatch` for ad-hoc or dry-run builds. -3. To backfill all historical versions at once, run the **Build All Versioned Docs** workflow manually from the Actions tab. - -#### Dark Theme -The DocFX modern template is configured to default to dark mode. This is controlled by: -- `"colorMode": "dark"` in `docfx_project/docfx.json` → `build.globalMetadata` -- `"_enableDarkMode": true` enables the light/dark toggle so visitors can switch themes - diff --git a/docs/README-FORMATTING.md b/docs/README-FORMATTING.md index 9ddea552..b420e1ac 100644 --- a/docs/README-FORMATTING.md +++ b/docs/README-FORMATTING.md @@ -4,7 +4,7 @@ This repository uses `dotnet format` to enforce consistent C# code style. ## Prerequisites -The `dotnet format` command is **built into the .NET SDK** starting with .NET 6 and later. Since this project requires .NET 8.0 SDK or later, you already have `dotnet format` available — no separate tool installation is needed. +The `dotnet format` command is **built into the .NET SDK** starting with .NET 6 — no separate tool installation is needed. In practice `dotnet format` still has to load and evaluate the project, so you need an SDK new enough to handle this repo's target frameworks: use the SDK version(s) installed by `.github/workflows/pr.yaml` (and `global.json` if present). The latest stable .NET SDK is generally a safe choice. > **Note:** The standalone `dotnet-format` global tool was deprecated when `dotnet format` was integrated into the .NET 6 SDK in August 2021. @@ -56,10 +56,10 @@ Most IDEs automatically read `.editorconfig`: ## Formatting Rules -Authoritative rules live in `.editorconfig` (and `.gitattributes` for line endings, which may override the `.editorconfig` defaults for specific file types — e.g. forcing CRLF on `*.ps1`). The list below is a quick orientation; check those files for the binding values: +Authoritative rules live in `.editorconfig` (and `.gitattributes` for line endings, which enforces LF across all text file types in this repo — including `*.ps1`, which historically used CRLF but is now LF for cross-platform shebang compatibility). The list below is a quick orientation; check those files for the binding values: - **Indentation**: 4 spaces for C#, 2 for XML/JSON (per `.editorconfig`) - **Braces**: Opening brace on its own line -- **Line endings**: LF for source/docs, with file-type overrides in `.gitattributes` (e.g. CRLF for `*.ps1`) +- **Line endings**: LF for all text files (per `.gitattributes`), including PowerShell scripts - **Trailing whitespace**: Removed - **Using directives**: System namespaces first, sorted alphabetically diff --git a/docs/RELEASE-WORKFLOW-SETUP.md b/docs/RELEASE-WORKFLOW-SETUP.md index 45eeb1d9..3aa8fba6 100644 --- a/docs/RELEASE-WORKFLOW-SETUP.md +++ b/docs/RELEASE-WORKFLOW-SETUP.md @@ -212,7 +212,7 @@ Before creating a production GitHub Release (e.g., `v1.0.0`): If you encounter issues not covered in this guide: -1. Check the [Actions tab](../../actions) for detailed logs +1. Check the Actions tab of this repository on GitHub for detailed logs 2. Review artifacts uploaded by failed jobs 3. Consult the [GitHub Actions documentation](https://docs.github.com/en/actions) 4. Open an issue in this repository with: diff --git a/docs/WORKFLOW_SECURITY.md b/docs/WORKFLOW_SECURITY.md index e27dea70..12c06a88 100644 --- a/docs/WORKFLOW_SECURITY.md +++ b/docs/WORKFLOW_SECURITY.md @@ -62,6 +62,8 @@ In addition to the overwrite step, a separate "Detect protected configuration fi "BannedSymbols.txt" "*.globalconfig" "*.ruleset" + ".github/workflows/*.yml" + ".github/workflows/*.yaml" ) # Copy each configuration file from main branch if it exists diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props deleted file mode 100644 index a53b408b..00000000 --- a/examples/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - false - - diff --git a/examples/Net4.8/Example1-BasicETL/Example1-BasicETL.csproj b/examples/Net4.8/Example1-BasicETL/Example1-BasicETL.csproj index 9a2feb28..d7459919 100644 --- a/examples/Net4.8/Example1-BasicETL/Example1-BasicETL.csproj +++ b/examples/Net4.8/Example1-BasicETL/Example1-BasicETL.csproj @@ -8,7 +8,6 @@ 8 true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj b/examples/Net4.8/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj index 4316f4ff..273b2973 100644 --- a/examples/Net4.8/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj +++ b/examples/Net4.8/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj @@ -8,7 +8,6 @@ 8 true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj b/examples/Net4.8/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj index 232b5775..826b920c 100644 --- a/examples/Net4.8/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj +++ b/examples/Net4.8/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj @@ -8,7 +8,6 @@ 8 true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj b/examples/Net4.8/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj index 28450714..e34ec595 100644 --- a/examples/Net4.8/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj +++ b/examples/Net4.8/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj @@ -8,7 +8,6 @@ latest true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj b/examples/Net4.8/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj index a0a34ffa..60140d49 100644 --- a/examples/Net4.8/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj +++ b/examples/Net4.8/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj @@ -8,7 +8,6 @@ latest true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj b/examples/Net4.8/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj index 45b3f05e..9da90757 100644 --- a/examples/Net4.8/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj +++ b/examples/Net4.8/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj @@ -8,7 +8,6 @@ latest true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj b/examples/Net4.8/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj index 53cb685b..50216572 100644 --- a/examples/Net4.8/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj +++ b/examples/Net4.8/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj @@ -8,7 +8,6 @@ latest true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net4.8/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj b/examples/Net4.8/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj index 5fdc3fb2..18135746 100644 --- a/examples/Net4.8/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj +++ b/examples/Net4.8/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj @@ -8,7 +8,6 @@ latest true true - enable true false $(NoWarn);CA2007 diff --git a/examples/Net8.0/Example1-BasicETL/Example1-BasicETL.csproj b/examples/Net8.0/Example1-BasicETL/Example1-BasicETL.csproj index 9a9aa817..780a4107 100644 --- a/examples/Net8.0/Example1-BasicETL/Example1-BasicETL.csproj +++ b/examples/Net8.0/Example1-BasicETL/Example1-BasicETL.csproj @@ -5,7 +5,6 @@ net8.0 Example1_BasicETL enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj b/examples/Net8.0/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj index cc13f612..d3f1d69e 100644 --- a/examples/Net8.0/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj +++ b/examples/Net8.0/Example2-WithCancellationToken/Example2-WithCancellationToken.csproj @@ -5,7 +5,6 @@ net8.0 Example2_WithCancellationToken enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj b/examples/Net8.0/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj index 9ea861bc..886608c1 100644 --- a/examples/Net8.0/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj +++ b/examples/Net8.0/Example3-WithGracefulCancellation/Example3-WithGracefulCancellation.csproj @@ -5,7 +5,6 @@ net8.0 Example3_WithGracefulCancellation enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj b/examples/Net8.0/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj index 5e9a9366..7e1297bd 100644 --- a/examples/Net8.0/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj +++ b/examples/Net8.0/Example4a-WithExtractorProgress/Example4a-WithExtractorProgress.csproj @@ -5,7 +5,6 @@ net8.0 Example4a_WithExtractorProgress enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj b/examples/Net8.0/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj index f544645a..96156244 100644 --- a/examples/Net8.0/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj +++ b/examples/Net8.0/Example4b-WithTransformerProgress/Example4b-WithTransformerProgress.csproj @@ -5,7 +5,6 @@ net8.0 Example4b_WithTransformerProgress enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj b/examples/Net8.0/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj index 12b6b994..874fc7e9 100644 --- a/examples/Net8.0/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj +++ b/examples/Net8.0/Example4c-WithLoaderProgress/Example4c-WithLoaderProgress.csproj @@ -5,7 +5,6 @@ net8.0 Example4c_WithLoaderProgress enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj b/examples/Net8.0/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj index d1d651a2..d678396a 100644 --- a/examples/Net8.0/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj +++ b/examples/Net8.0/Example5a-ExtractorWithProgressAndCancellation/Example5a-ExtractorWithProgressAndCancellation.csproj @@ -5,7 +5,6 @@ net8.0 Example5a_ExtractorWithProgressAndCancellation enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/examples/Net8.0/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj b/examples/Net8.0/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj index 889e0a50..a759717b 100644 --- a/examples/Net8.0/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj +++ b/examples/Net8.0/Example6-ReducingDuplicateCode/Example6-ReducingDuplicateCode.csproj @@ -5,7 +5,6 @@ net8.0 Example6_ReducingDuplicateCode enable - enable CA2007 1.0.0 Copyright {copyright year} {author} diff --git a/scripts/Fix-BranchRuleset.ps1 b/scripts/Fix-BranchRuleset.ps1 new file mode 100644 index 00000000..31632679 --- /dev/null +++ b/scripts/Fix-BranchRuleset.ps1 @@ -0,0 +1,281 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Fixes branch rulesets by disabling existing ones and recreating with the correct configuration. + +.DESCRIPTION + This script inspects the existing branch rulesets for a repository, disables all of them, + and renames any ruleset named "Protect main branch" to "Protect main branch (old)" so that + Setup-BranchRuleset.ps1 can create a fresh ruleset without conflicts. + + The script presents a plan of all changes before executing and prompts for confirmation. + +.PARAMETER Repository + The repository in owner/repo format. If not provided, uses the current repository. + +.PARAMETER Force + Skip the confirmation prompt and proceed automatically. Alias: -y + +.EXAMPLE + .\Fix-BranchRuleset.ps1 + Inspects and fixes rulesets for the current repository with interactive confirmation + +.EXAMPLE + .\Fix-BranchRuleset.ps1 -Force + Inspects and fixes rulesets without prompting for confirmation + +.EXAMPLE + .\Fix-BranchRuleset.ps1 -Repository "Chris-Wolfgang/my-repo" + Inspects and fixes rulesets for a specific repository + +.NOTES + Requires: GitHub CLI (gh) authenticated with admin permissions + Install gh: https://cli.github.com/ +#> + +[CmdletBinding()] +param( + [Parameter()] + [string]$Repository = "{{GITHUB_USERNAME}}/{{REPO_NAME}}", + + [Parameter()] + [Alias("y")] + [switch]$Force +) + +# Check if gh CLI is installed +try { + $null = gh --version +} catch { + Write-Error "GitHub CLI (gh) is not installed or not in PATH." + Write-Host "Install from: https://cli.github.com/" -ForegroundColor Yellow + exit 1 +} + +# Check if authenticated +try { + $null = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Not authenticated with GitHub CLI." + Write-Host "Run: gh auth login" -ForegroundColor Yellow + exit 1 + } +} catch { + Write-Error "Failed to check GitHub CLI authentication status." + exit 1 +} + +# Normalize Repository: strip leading "@" and trailing ".git" that can +# leak in from SSH remotes (e.g. git@github.com:owner/repo.git -> @owner/repo). +# Both prefixes make gh api /repos/... calls fail with 404. +if ($Repository) { + $Repository = $Repository.TrimStart('@') + if ($Repository.EndsWith('.git')) { $Repository = $Repository.Substring(0, $Repository.Length - 4) } +} + +# Determine repository +if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}" -or -not $Repository) { + Write-Host "Detecting current repository..." -ForegroundColor Cyan + try { + $repoInfo = gh repo view --json nameWithOwner | ConvertFrom-Json + $Repository = $repoInfo.nameWithOwner + Write-Host "Using repository: $Repository" -ForegroundColor Green + } catch { + if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}") { + Write-Error "Could not detect repository. Please run the setup script first to replace placeholders, or specify -Repository parameter." + } else { + Write-Error "Could not detect repository. Please run from within a git repository or specify -Repository parameter." + } + exit 1 + } +} else { + Write-Host "Using specified repository: $Repository" -ForegroundColor Green +} + +# Fetch all rulesets +Write-Host "`nFetching existing rulesets..." -ForegroundColor Cyan + +try { + # Capture stderr to a temp file so gh's progress/warnings can't poison + # the JSON stream on stdout (mixing them via 2>&1 can break ConvertFrom-Json + # even on a successful API call). + $rulesetsErr = [System.IO.Path]::GetTempFileName() + try { + # Don't use --paginate here: it concatenates multiple JSON array + # payloads when results span pages, which breaks ConvertFrom-Json. + # Rulesets are typically few per repo; per_page=100 in a single + # call is enough and produces valid JSON. + $rulesetsJson = gh api ` + -H "Accept: application/vnd.github+json" ` + -H "X-GitHub-Api-Version: 2022-11-28" ` + "/repos/$Repository/rulesets?per_page=100" ` + 2> $rulesetsErr + } finally { + if (Test-Path -LiteralPath $rulesetsErr) { + $errText = (Get-Content -LiteralPath $rulesetsErr -Raw -ErrorAction SilentlyContinue) + Remove-Item -LiteralPath $rulesetsErr -Force + } + } + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to fetch rulesets (exit code $LASTEXITCODE). gh stderr: $errText" + exit 1 + } + + $rulesets = $rulesetsJson | ConvertFrom-Json +} catch { + Write-Error "Failed to fetch rulesets: $($_.Exception.Message)" + exit 1 +} + +if (-not $rulesets -or $rulesets.Count -eq 0) { + Write-Host "No rulesets found for $Repository. Nothing to fix." -ForegroundColor Green + exit 0 +} + +# Build the plan +$plan = @() +$targetRulesetName = "Protect main branch" + +Write-Host "`nFound $($rulesets.Count) ruleset(s):" -ForegroundColor Cyan +Write-Host "" + +foreach ($ruleset in $rulesets) { + $status = if ($ruleset.enforcement -eq "disabled") { "disabled" } else { $ruleset.enforcement } + Write-Host " [$($ruleset.id)] $($ruleset.name) (enforcement: $status)" -ForegroundColor Gray + + $actions = @() + + # If this is the target name, rename it + if ($ruleset.name -eq $targetRulesetName) { + $actions += @{ + type = "rename" + description = "Rename '$($ruleset.name)' -> '$($ruleset.name) (old)'" + newName = "$($ruleset.name) (old)" + } + } + + # If not already disabled, disable it + if ($ruleset.enforcement -ne "disabled") { + $actions += @{ + type = "disable" + description = "Disable '$($ruleset.name)' (currently: $status)" + } + } + + if ($actions.Count -gt 0) { + $plan += @{ + ruleset = $ruleset + actions = $actions + } + } +} + +Write-Host "" + +# Present the plan +if ($plan.Count -eq 0) { + Write-Host "All rulesets are already disabled and none need renaming. Nothing to do." -ForegroundColor Green + exit 0 +} + +Write-Host "Planned changes:" -ForegroundColor Yellow +Write-Host "" + +$stepNumber = 1 +foreach ($item in $plan) { + foreach ($action in $item.actions) { + Write-Host " $stepNumber. $($action.description)" -ForegroundColor White + $stepNumber++ + } +} + +Write-Host "" + +# Prompt for confirmation +if ($Force) { + Write-Host "Auto-confirmed via -Force flag." -ForegroundColor Green +} else { + $response = Read-Host "Proceed with these changes? (y/N)" + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Host "Cancelled. No changes were made." -ForegroundColor Yellow + exit 0 + } +} + +Write-Host "" + +# Execute the plan +$errors = 0 + +foreach ($item in $plan) { + $ruleset = $item.ruleset + $rulesetId = $ruleset.id + + # Build the update payload — apply rename and disable together in one API call + $updatePayload = @{} + + foreach ($action in $item.actions) { + switch ($action.type) { + "rename" { + $updatePayload["name"] = $action.newName + } + "disable" { + $updatePayload["enforcement"] = "disabled" + } + } + } + + if ($updatePayload.Count -gt 0) { + $descriptions = ($item.actions | ForEach-Object { $_.description }) -join " + " + Write-Host " Updating ruleset [$rulesetId]: $descriptions..." -ForegroundColor Cyan + + $jsonPayload = $updatePayload | ConvertTo-Json -Depth 5 + $tempFile = [System.IO.Path]::GetTempFileName() + $jsonPayload | Out-File -FilePath $tempFile -Encoding utf8NoBOM + + try { + $result = gh api ` + --method PUT ` + -H "Accept: application/vnd.github+json" ` + -H "X-GitHub-Api-Version: 2022-11-28" ` + "/repos/$Repository/rulesets/$rulesetId" ` + --input $tempFile 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Host " Done." -ForegroundColor Green + } else { + Write-Host " Failed: $result" -ForegroundColor Red + $errors++ + } + } catch { + Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red + $errors++ + } finally { + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + } + } +} + +Write-Host "" + +if ($errors -gt 0) { + Write-Host "$errors action(s) failed. Review the errors above." -ForegroundColor Red + exit 1 +} else { + Write-Host "All changes applied successfully." -ForegroundColor Green + Write-Host "" + + # Invoke Setup-BranchRuleset.ps1 to create a fresh ruleset + $setupScript = Join-Path $PSScriptRoot "Setup-BranchRuleset.ps1" + if (Test-Path $setupScript) { + Write-Host "Running Setup-BranchRuleset.ps1 to create a fresh ruleset..." -ForegroundColor Cyan + Write-Host "" + & $setupScript -Repository $Repository + } else { + Write-Host "Setup-BranchRuleset.ps1 not found. Run it manually to create a fresh ruleset." -ForegroundColor Yellow + Write-Host "View rulesets at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan + } +} diff --git a/scripts/Setup-GitHubPages.ps1 b/scripts/Setup-GitHubPages.ps1 deleted file mode 100644 index 334266da..00000000 --- a/scripts/Setup-GitHubPages.ps1 +++ /dev/null @@ -1,719 +0,0 @@ -#!/usr/bin/env pwsh -#Requires -Version 7.0 - -<# -.SYNOPSIS - Sets up GitHub Pages with DocFX for automatic documentation publishing on GitHub Release. - -.DESCRIPTION - This script automates the setup of GitHub Pages for a .NET repository using DocFX. - It performs the following tasks: - 1. Prompts if you want to set up GitHub Pages for documentation - 2. Reads repository-specific information automatically where possible - 3. Prompts for any missing information needed for DocFX configuration - 4. Replaces placeholders in docfx.json and documentation markdown files - 5. Creates a gh-pages branch if it doesn't already exist - 6. Configures GitHub Pages settings to serve from the gh-pages branch - 7. Verifies the DocFX workflow is reachable via workflow_call from release.yaml - - Run this script locally after creating a new repository from the template. - -.PARAMETER Repository - The repository in owner/repo format. If not provided, uses the current repository. - -.PARAMETER EnablePages - If specified, automatically enables GitHub Pages without prompting. - -.PARAMETER SkipPrompt - If specified, skips the initial prompt asking if you want to set up GitHub Pages. - -.EXAMPLE - .\Setup-GitHubPages.ps1 - Sets up GitHub Pages for the current repository with interactive prompts - -.EXAMPLE - .\Setup-GitHubPages.ps1 -Repository "Chris-Wolfgang/my-repo" - Sets up GitHub Pages for a specific repository - -.EXAMPLE - .\Setup-GitHubPages.ps1 -EnablePages -SkipPrompt - Sets up GitHub Pages and automatically enables it without any prompts - -.NOTES - Requires: - - GitHub CLI (gh) authenticated with sufficient permissions - - Git installed and available in PATH - Install gh: https://cli.github.com/ -#> - -[CmdletBinding()] -param( - [Parameter()] - [string]$Repository = "{{GITHUB_USERNAME}}/{{REPO_NAME}}", - - [Parameter()] - [switch]$EnablePages, - - [Parameter()] - [switch]$SkipPrompt -) - -# Enable strict mode -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Color output functions -function Write-Success { - param([string]$Message) - Write-Host "✅ $Message" -ForegroundColor Green -} - -function Write-Info { - param([string]$Message) - Write-Host "ℹ️ $Message" -ForegroundColor Cyan -} - -function Write-Warning-Custom { - param([string]$Message) - Write-Host "⚠️ $Message" -ForegroundColor Yellow -} - -function Write-Error-Custom { - param([string]$Message) - Write-Host "❌ $Message" -ForegroundColor Red -} - -function Write-Step { - param([string]$Message) - Write-Host "`n🔧 $Message" -ForegroundColor Magenta -} - -# Helper function to read input with default value -function Read-Input { - param( - [Parameter(Mandatory)] - [string]$Prompt, - - [string]$Default = '', - - [string]$Example = '', - - [switch]$Required - ) - - $displayPrompt = $Prompt - if ($Default) { - $displayPrompt += " [$Default]" - } - if ($Example -and $Example -ne $Default) { - $displayPrompt += " (e.g., $Example)" - } - $displayPrompt += ": " - - do { - Write-Host $displayPrompt -NoNewline -ForegroundColor Yellow - $input = Read-Host - - if ([string]::IsNullOrWhiteSpace($input)) { - if ($Default) { - return $Default - } - if ($Required) { - Write-Warning-Custom "This field is required. Please enter a value." - continue - } - return '' - } - - return $input.Trim() - } while ($true) -} - -# Banner -Write-Host @" - -╔═══════════════════════════════════════════════════════════════════╗ -║ ║ -║ GitHub Pages Setup - DocFX Documentation Publishing ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════╝ - -"@ -ForegroundColor Cyan - -# Initial prompt to confirm setup -if (-not $SkipPrompt) { - Write-Host "`n📚 This script will set up GitHub Pages for your repository documentation." -ForegroundColor Cyan - Write-Host "" - Write-Host "The setup process will:" -ForegroundColor Gray - Write-Host " • Configure DocFX documentation files with your project information" -ForegroundColor Gray - Write-Host " • Create a gh-pages branch for hosting documentation" -ForegroundColor Gray - Write-Host " • Enable GitHub Pages in repository settings" -ForegroundColor Gray - Write-Host " • Verify the DocFX workflow configuration" -ForegroundColor Gray - Write-Host "" - - $response = Read-Host "Do you want to set up GitHub Pages for documentation? (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Info "Setup cancelled. You can run this script again anytime." - exit 0 - } - - Write-Host "" -} - -# Check if gh CLI is installed -Write-Step "Checking prerequisites..." -try { - $null = gh --version - Write-Success "GitHub CLI (gh) is installed" -} catch { - Write-Error-Custom "GitHub CLI (gh) is not installed or not in PATH." - Write-Host "Install from: https://cli.github.com/" -ForegroundColor Yellow - exit 1 -} - -# Check if git is installed -try { - $null = git --version - Write-Success "Git is installed" -} catch { - Write-Error-Custom "Git is not installed or not in PATH." - Write-Host "Install from: https://git-scm.com/" -ForegroundColor Yellow - exit 1 -} - -# Check if we're in a git repository -try { - $null = git rev-parse --git-dir 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Not in a git repository." - Write-Host "Please run this script from within a git repository." -ForegroundColor Yellow - exit 1 - } - Write-Success "Running in a git repository" -} catch { - Write-Error-Custom "Not in a git repository." - Write-Host "Please run this script from within a git repository." -ForegroundColor Yellow - exit 1 -} - -# Check if authenticated -try { - $null = gh auth status 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Not authenticated with GitHub CLI." - Write-Host "Run: gh auth login" -ForegroundColor Yellow - exit 1 - } - Write-Success "Authenticated with GitHub CLI" -} catch { - Write-Error-Custom "Failed to check GitHub CLI authentication status." - exit 1 -} - -# Determine repository -if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}" -or -not $Repository) { - # Placeholders not replaced or no repository specified - auto-detect - Write-Info "Detecting current repository..." - try { - $repoInfo = gh repo view --json nameWithOwner | ConvertFrom-Json - $Repository = $repoInfo.nameWithOwner - Write-Success "Using repository: $Repository" - } catch { - if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}") { - Write-Error-Custom "Could not detect repository. Please run the setup script (scripts/setup.ps1 or scripts/setup.sh) first to replace placeholders, or specify -Repository parameter." - } else { - Write-Error-Custom "Could not detect repository. Please run from within a git repository or specify -Repository parameter." - } - exit 1 - } -} else { - Write-Success "Using specified repository: $Repository" -} - -Write-Host "`n📚 Setting up GitHub Pages for: $Repository" -ForegroundColor Cyan - -# Configure DocFX files -Write-Step "Configuring DocFX documentation files..." - -# Check if docfx.json has placeholders that need to be replaced -$docfxJsonPath = "docfx_project/docfx.json" -$needsDocFxConfig = $false - -if (Test-Path $docfxJsonPath) { - $docfxContent = Get-Content $docfxJsonPath -Raw - if ($docfxContent -match '{{[^}]+}}') { - $needsDocFxConfig = $true - Write-Info "DocFX configuration files contain placeholders that need to be replaced" - } else { - Write-Success "DocFX configuration files are already configured" - } -} else { - Write-Warning-Custom "docfx.json not found at $docfxJsonPath" - Write-Info "Skipping DocFX configuration" -} - -if ($needsDocFxConfig) { - Write-Host "" - Write-Host "📝 Gathering project information for DocFX configuration..." -ForegroundColor Cyan - Write-Host "" - - # Parse repository information - $repoOwner = $Repository -split '/' | Select-Object -First 1 - $repoName = $Repository -split '/' | Select-Object -Last 1 - $githubRepoUrl = "https://github.com/$Repository" - - # Try to get repository description from GitHub - try { - $repoFullInfo = gh repo view --json description,nameWithOwner | ConvertFrom-Json - $autoDescription = $repoFullInfo.description - if ([string]::IsNullOrWhiteSpace($autoDescription)) { - $autoDescription = "A .NET library/application" - } - } catch { - $autoDescription = "A .NET library/application" - } - - # Calculate default documentation URL - $defaultDocsUrl = "https://$repoOwner.github.io/$repoName/" - - # Prompt for project information - $projectName = Read-Input ` - -Prompt "Project Name" ` - -Default $repoName ` - -Example $repoName ` - -Required - - $projectDescription = Read-Input ` - -Prompt "Project Description" ` - -Default $autoDescription ` - -Example $autoDescription - - $packageName = Read-Input ` - -Prompt "NuGet Package Name (if publishing to NuGet)" ` - -Default $projectName ` - -Example $projectName - - $docsUrl = Read-Input ` - -Prompt "Documentation URL (GitHub Pages)" ` - -Default $defaultDocsUrl ` - -Example $defaultDocsUrl - - # Ensure docsUrl ends with / - if (-not $docsUrl.EndsWith('/')) { - $docsUrl += '/' - } - - # Summary - Write-Host "" - Write-Host "Configuration Summary:" -ForegroundColor Cyan - Write-Host " Project Name: $projectName" -ForegroundColor Gray - Write-Host " Description: $projectDescription" -ForegroundColor Gray - Write-Host " Package Name: $packageName" -ForegroundColor Gray - Write-Host " Repository URL: $githubRepoUrl" -ForegroundColor Gray - Write-Host " Documentation URL: $docsUrl" -ForegroundColor Gray - Write-Host "" - - $confirm = Read-Host "Proceed with this configuration? (Y/n)" - if ($confirm -and $confirm -ne 'Y' -and $confirm -ne 'y') { - Write-Warning-Custom "Configuration cancelled." - exit 0 - } - - # Create replacements hashtable - $replacements = @{ - '{{PROJECT_NAME}}' = $projectName - '{{PROJECT_DESCRIPTION}}' = $projectDescription - '{{PACKAGE_NAME}}' = $packageName - '{{GITHUB_REPO_URL}}' = $githubRepoUrl - '{{DOCS_URL}}' = $docsUrl - } - - # Files to update - $filesToUpdate = @( - 'docfx_project/docfx.json', - 'docfx_project/index.md', - 'docfx_project/api/index.md', - 'docfx_project/api/README.md', - 'docfx_project/docs/toc.yml', - 'docfx_project/docs/introduction.md', - 'docfx_project/docs/getting-started.md' - ) - - # Replace placeholders in files - Write-Host "" - Write-Info "Replacing placeholders in DocFX files..." - $filesUpdated = 0 - - foreach ($file in $filesToUpdate) { - if (Test-Path $file) { - $content = Get-Content $file -Raw -ErrorAction SilentlyContinue - if ($content) { - $originalContent = $content - - foreach ($placeholder in $replacements.Keys) { - $pattern = [regex]::Escape($placeholder) - $content = [regex]::Replace( - $content, - $pattern, - [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $replacements[$placeholder] } - ) - } - - if ($content -ne $originalContent) { - Set-Content -Path $file -Value $content -NoNewline -Encoding UTF8 - Write-Success " Updated: $file" - $filesUpdated++ - } - } - } - } - - if ($filesUpdated -gt 0) { - Write-Success "Successfully updated $filesUpdated DocFX file(s)" - } else { - Write-Info "No files needed updating" - } - - Write-Host "" -} - -# Check if gh-pages branch exists -Write-Step "Checking for gh-pages branch..." -try { - $branches = git ls-remote --heads origin gh-pages 2>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Error checking for gh-pages branch. Git exited with code $LASTEXITCODE.`nOutput:`n$branches" - exit 1 - } - - $ghPagesBranchExists = -not [string]::IsNullOrWhiteSpace($branches) - - if ($ghPagesBranchExists) { - Write-Success "gh-pages branch already exists" - } else { - Write-Info "gh-pages branch does not exist yet" - - # Check for uncommitted changes before creating gh-pages branch - $gitStatus = git status --porcelain 2>&1 - if (-not [string]::IsNullOrWhiteSpace($gitStatus)) { - Write-Warning-Custom "You have uncommitted changes in your working directory." - Write-Info "Please commit or stash your changes before proceeding." - Write-Info "Uncommitted changes:`n$gitStatus" - $response = Read-Host "Do you want to continue anyway? This may cause data loss. (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Info "Aborting gh-pages branch creation." - exit 0 - } - } - - # Store the current branch name before switching - $originalBranch = git rev-parse --abbrev-ref HEAD 2>&1 - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($originalBranch) -or - $originalBranch -match '(fatal|error|warning|usage:)') { - Write-Warning-Custom "Could not determine current branch name. Will attempt to return to 'main' after creating gh-pages." - $originalBranch = "main" # Default fallback - } - - # Create gh-pages branch - Write-Step "Creating gh-pages branch..." - - # Create an orphan branch (no history) - $checkoutOutput = git checkout --orphan gh-pages 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to create orphan gh-pages branch. Git output:`n$checkoutOutput" - throw "Git checkout --orphan failed" - } - - # Remove all files from staging - $rmOutput = git rm -rf . 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to remove files from staging. Git output:`n$rmOutput" - throw "Git rm failed" - } - - # Create a placeholder index.html - $placeholderHtml = @" - - - - - Documentation - - -

Documentation Coming Soon

-

This site will contain the project documentation once it is generated.

-

Documentation is automatically published when you publish a GitHub Release.

- - -"@ - Set-Content -Path "index.html" -Value $placeholderHtml -Encoding UTF8 - - # Commit and push - $addOutput = git add index.html 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to stage index.html. Git output:`n$addOutput" - throw "Git add failed" - } - - $commitOutput = git commit -m "Initialize gh-pages branch" 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to commit gh-pages branch. Git output:`n$commitOutput" - throw "Git commit failed" - } - - $pushOutput = git push origin gh-pages 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to push gh-pages branch. Git output:`n$pushOutput" - throw "Git push failed" - } - - # Switch back to original branch - try { - $checkoutBackOutput = git checkout $originalBranch 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning-Custom "Failed to switch back to original branch '$originalBranch'. Git output:`n$checkoutBackOutput" - # Try to detect the default branch as fallback - $defaultBranchOutput = git symbolic-ref refs/remotes/origin/HEAD 2>&1 - if ($LASTEXITCODE -eq 0 -and $defaultBranchOutput -and - $defaultBranchOutput -notmatch '(fatal|error|warning|usage:)') { - $defaultBranch = $defaultBranchOutput | ForEach-Object { $_ -replace '^refs/remotes/origin/', '' } - $checkoutDefaultOutput = git checkout $defaultBranch 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning-Custom "Failed to checkout default branch '$defaultBranch'. Git output:`n$checkoutDefaultOutput" - } - } else { - # Try main then master as last resort - $checkoutMainOutput = git checkout main 2>&1 - if ($LASTEXITCODE -ne 0) { - $checkoutMasterOutput = git checkout master 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Warning-Custom "Could not switch back to any default branch. You may need to manually switch branches." - } - } - } - } - } catch { - Write-Warning-Custom "Could not switch back to original branch. You may need to manually switch branches." - } - - Write-Success "Created and pushed gh-pages branch" - } -} catch { - Write-Error-Custom "Failed to check or create gh-pages branch: $_" - Write-Host "You may need to create the gh-pages branch manually." -ForegroundColor Yellow -} - -# Check and enable GitHub Pages -Write-Step "Configuring GitHub Pages settings..." -try { - # Get current Pages configuration - $pagesInfo = gh api "/repos/$Repository/pages" 2>&1 - - if ($LASTEXITCODE -eq 0) { - $pagesConfig = $pagesInfo | ConvertFrom-Json - Write-Success "GitHub Pages is already enabled" - Write-Info " Source: $($pagesConfig.source.branch)/$($pagesConfig.source.path)" - if ($pagesConfig.html_url) { - Write-Info " URL: $($pagesConfig.html_url)" - } - - # Check if it's configured to use gh-pages branch - if ($pagesConfig.source.branch -ne "gh-pages") { - Write-Warning-Custom "GitHub Pages is not configured to use the gh-pages branch" - if (-not $EnablePages) { - $response = Read-Host "Would you like to update it to use gh-pages branch? (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Info "Skipping GitHub Pages branch update" - } else { - $EnablePages = $true - } - } - - if ($EnablePages) { - # Update Pages to use gh-pages branch - $pagesConfigUpdate = @{ - source = @{ - branch = "gh-pages" - path = "/" - } - } | ConvertTo-Json - - $tempFile = [System.IO.Path]::GetTempFileName() - $pagesConfigUpdate | Out-File -FilePath $tempFile -Encoding utf8NoBOM - - try { - $updateOutput = gh api --method PUT "/repos/$Repository/pages" --input $tempFile 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to update GitHub Pages configuration. GitHub CLI output:`n$updateOutput" - } else { - Write-Success "Updated GitHub Pages to use gh-pages branch" - } - } catch { - Write-Error-Custom "Failed to update GitHub Pages configuration: $_" - } finally { - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } - } - } - } - } else { - # Pages not enabled, try to enable it - Write-Info "GitHub Pages is not enabled yet" - - if (-not $EnablePages) { - $response = Read-Host "Would you like to enable GitHub Pages now? (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Info "Skipping GitHub Pages setup" - Write-Info "You can enable it later in: Settings → Pages" - } else { - $EnablePages = $true - } - } - - if ($EnablePages) { - # Enable Pages with gh-pages branch - $pagesConfig = @{ - source = @{ - branch = "gh-pages" - path = "/" - } - } | ConvertTo-Json - - $tempFile = [System.IO.Path]::GetTempFileName() - $pagesConfig | Out-File -FilePath $tempFile -Encoding utf8NoBOM - - try { - $enableOutput = gh api --method POST "/repos/$Repository/pages" --input $tempFile 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Failed to enable GitHub Pages. GitHub CLI output:`n$enableOutput" - Write-Host "You may need to enable it manually in: Settings → Pages" -ForegroundColor Yellow - } else { - Write-Success "Enabled GitHub Pages with gh-pages branch" - - # Get the Pages URL - Start-Sleep -Seconds 2 - $pagesUrlInfo = gh api "/repos/$Repository/pages" 2>&1 - if ($LASTEXITCODE -eq 0) { - $pagesUrlData = $pagesUrlInfo | ConvertFrom-Json - if ($pagesUrlData.html_url) { - Write-Info " URL: $($pagesUrlData.html_url)" - } - } - } - } catch { - Write-Error-Custom "Failed to enable GitHub Pages: $_" - Write-Host "You may need to enable it manually in: Settings → Pages" -ForegroundColor Yellow - } finally { - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } - } - } - } -} catch { - Write-Warning-Custom "Could not check GitHub Pages configuration" - Write-Info "You may need to enable GitHub Pages manually in: Settings → Pages" -} - -# Verify DocFX workflow configuration -Write-Step "Verifying DocFX workflow configuration..." -$workflowPath = ".github/workflows/docfx.yaml" - -if (Test-Path $workflowPath) { - $workflowContent = Get-Content $workflowPath -Raw - $normalizedWorkflowContent = $workflowContent -replace "`r`n", "`n" - - # Check if workflow is triggered via workflow_call (called by release.yaml) - $hasWorkflowCall = $normalizedWorkflowContent -match 'workflow_call:' - - if ($hasWorkflowCall) { - Write-Success "DocFX workflow is configured to be called via workflow_call from release.yaml" - } else { - Write-Warning-Custom "DocFX workflow does not appear to be configured for workflow_call" - Write-Info "The DocFX workflow should be invoked by release.yaml via workflow_call" - Write-Info " after a GitHub Release is published." - Write-Info "" - Write-Info "To enable automatic documentation publishing on GitHub Release:" - Write-Info " 1. Edit $workflowPath" - Write-Info " 2. Ensure the 'on:' section includes:" - Write-Info "" - Write-Host @" - on: - workflow_call: - inputs: - version: - description: 'Version tag for documentation (e.g., v1.0.0).' - required: false - default: '' - type: string - workflow_dispatch: -"@ -ForegroundColor DarkGray - Write-Info "" - Write-Info " 3. In release.yaml, add a job that calls docfx.yaml:" - Write-Info "" - Write-Host @" - trigger-docs: - needs: validate-release - permissions: - contents: write - uses: ./.github/workflows/docfx.yaml - with: - version: `${{ github.event.release.tag_name }} -"@ -ForegroundColor DarkGray - Write-Info "" - } -} else { - Write-Warning-Custom "DocFX workflow not found at $workflowPath" - Write-Info "Ensure you have a DocFX workflow configured" -} - -# Summary -Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan -Write-Host "📋 Setup Summary" -ForegroundColor Cyan -Write-Host ("=" * 70) -ForegroundColor Cyan - -Write-Host "`n✅ Completed Tasks:" -ForegroundColor Green -if ($needsDocFxConfig -and $filesUpdated -gt 0) { - Write-Host " • Configured DocFX files with project information" -ForegroundColor Gray -} -Write-Host " • Verified/Created gh-pages branch" -ForegroundColor Gray -if ($EnablePages) { - Write-Host " • Configured GitHub Pages settings" -ForegroundColor Gray -} -Write-Host " • Verified DocFX workflow configuration" -ForegroundColor Gray - -Write-Host "`n📝 Next Steps:" -ForegroundColor Yellow -if ($needsDocFxConfig -and $filesUpdated -gt 0) { - Write-Host " 1. Review and customize the generated documentation in docfx_project/" -ForegroundColor Gray - Write-Host " 2. Publish a GitHub Release to trigger documentation deployment" -ForegroundColor Gray - Write-Host " 3. Check the Actions tab to see the documentation build" -ForegroundColor Gray - Write-Host " 4. Visit your documentation site once published" -ForegroundColor Gray -} else { - Write-Host " 1. Ensure docfx_project/docfx.json is configured for your project" -ForegroundColor Gray - Write-Host " 2. Ensure .github/workflows/docfx.yaml has workflow_call in its 'on:' triggers and is called by release.yaml" -ForegroundColor Gray - Write-Host " 3. Publish a GitHub Release to trigger documentation deployment" -ForegroundColor Gray - Write-Host " 4. Check the Actions tab to see the documentation build" -ForegroundColor Gray -} - -Write-Host "`n🔗 Useful Links:" -ForegroundColor Cyan -Write-Host " • Repository: https://github.com/$Repository" -ForegroundColor Blue -Write-Host " • Actions: https://github.com/$Repository/actions" -ForegroundColor Blue -Write-Host " • Settings → Pages: https://github.com/$Repository/settings/pages" -ForegroundColor Blue - -# Get Pages URL if available -try { - $pagesUrlOutput = gh api "/repos/$Repository/pages" 2>&1 - if ($LASTEXITCODE -eq 0) { - $pagesUrlInfo = $pagesUrlOutput | ConvertFrom-Json - if ($pagesUrlInfo.html_url) { - Write-Host " • Documentation: $($pagesUrlInfo.html_url)" -ForegroundColor Blue - } - } -} catch { - # Silently ignore if we can't get the URL -} - -Write-Host "`n🎉 GitHub Pages setup complete!" -ForegroundColor Green -Write-Host "" diff --git a/scripts/Setup-Labels.ps1 b/scripts/Setup-Labels.ps1 index bb3a6419..e8b70f54 100644 --- a/scripts/Setup-Labels.ps1 +++ b/scripts/Setup-Labels.ps1 @@ -1,3 +1,4 @@ +#!/usr/bin/env pwsh <# .SYNOPSIS Creates custom GitHub labels for the repository. @@ -7,10 +8,16 @@ other workflows. Run this locally once after creating a new repo from the template. Labels created: - - dependabot - security (red) - - dependabot-dependencies (orange) - - dependencies (blue) - - dotnet (purple) + - dependencies (blue) — applied automatically by Dependabot to every update PR + - maintenance (steel) — kind label, applied to the per-repo parent Maintenance issue + - maintenance-task (steel) — kind label, applied to every Maintenance sub-issue + - maintenance - security (red) — category: scans, finding fixes, dependency vuln audit + - maintenance - performance (green) — category: profile, benchmark, optimize, validate + - maintenance - testing (gold) — category: coverage, integration/smoke/mutation tests + - maintenance - cleanup (brown) — category: refactor for reuse / quality / efficiency + - maintenance - docs (blue) — category: XML docs, README, CHANGELOG, samples + - maintenance - API (orange) — category: public/internal surface audit + - maintenance - CI/CD (pink) — category: Docker, CI workflow, build/publish pipeline .PARAMETER Repository The repository in owner/repo format. If not provided, uses the current repository. @@ -31,6 +38,11 @@ [CmdletBinding()] param( [Parameter()] + # Accept empty (auto-detect from `gh repo view`), the template placeholder + # (replaced by setup.ps1), or a strict owner/repo format. Rejecting URLs + # and malformed inputs here surfaces the problem at parameter binding + # instead of as a confusing 404 from gh api downstream. + [ValidatePattern('^$|^\{\{GITHUB_USERNAME\}\}/\{\{REPO_NAME\}\}$|^[^/@\s]+/[^/@\s]+$')] [string]$Repository ) @@ -74,10 +86,21 @@ if (-not $Repository) { Write-Host "`n🏷️ Creating labels for: $Repository`n" -ForegroundColor Cyan $labels = @( - @{ name = "dependabot - security"; color = "b60205"; description = "Security update from Dependabot" }, - @{ name = "dependabot-dependencies"; color = "d93f0b"; description = "Dependency update from Dependabot" }, + # Dependabot — applies `dependencies` automatically per .github/dependabot.yml @{ name = "dependencies"; color = "0366d6"; description = "Pull requests that update a dependency file" }, - @{ name = "dotnet"; color = "512bd4"; description = ".NET related changes" } + + # Maintenance framework — kind labels (neutral steel: the meta is colorless) + @{ name = "maintenance"; color = "9aa7b3"; description = "Per-repo parent Maintenance issue (living improvement menu)" }, + @{ name = "maintenance-task"; color = "5a6c7d"; description = "A Maintenance sub-issue — actionable improvement work" }, + + # Maintenance framework — category labels (applied to sub-issues) + @{ name = "maintenance - security"; color = "c4161c"; description = "Maintenance: scans, finding fixes, dependency vulnerability audit" }, + @{ name = "maintenance - performance"; color = "2cbe4e"; description = "Maintenance: profile, benchmark, optimize, validate gains" }, + @{ name = "maintenance - testing"; color = "f9c513"; description = "Maintenance: coverage %, integration/smoke/mutation tests, fixtures" }, + @{ name = "maintenance - cleanup"; color = "a2845e"; description = "Maintenance: refactor for reuse, quality, efficiency" }, + @{ name = "maintenance - docs"; color = "0075ca"; description = "Maintenance: XML doc coverage, README, CHANGELOG, samples" }, + @{ name = "maintenance - API"; color = "ed7d31"; description = "Maintenance: public/internal surface audit, breaking-change vigilance" }, + @{ name = "maintenance - CI/CD"; color = "ec6cb9"; description = "Maintenance: Docker, CI workflow, build/publish pipeline" } ) $created = 0 diff --git a/scripts/Validate-DocsDeploy.sh b/scripts/Validate-DocsDeploy.sh new file mode 100644 index 00000000..ab6fd0d0 --- /dev/null +++ b/scripts/Validate-DocsDeploy.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +# Validate-DocsDeploy.sh +# +# Validates the gh-pages branch contents after a DocFX deployment. +# Checks that the root contains index.html and versions.json, that +# versions.json is correctly structured, that every referenced version +# folder exists with an index.html, and that no known stale DocFX root +# artifacts remain. +# +# Usage: +# bash scripts/Validate-DocsDeploy.sh +# +# Requirements: git, python3 + +set -euo pipefail + +PASS=0 +FAIL=0 + +check_pass() { echo " ✅ $1"; PASS=$((PASS + 1)); } +check_fail() { echo " ❌ $1"; FAIL=$((FAIL + 1)); } +check_warn() { echo " ⚠️ $1"; } + +echo "" +echo "╔══════════════════════════════════════════════════════╗" +echo "║ DocFX Deployment Validation ║" +echo "╚══════════════════════════════════════════════════════╝" +echo "" + +# ------------------------------------------------------------------ +# 1. Verify the gh-pages branch exists on the remote +# ------------------------------------------------------------------ +echo "1. Checking gh-pages branch..." +if ! ls_remote_output=$(git ls-remote --heads origin gh-pages 2>&1); then + check_fail "Could not query 'origin' for the gh-pages branch — \`git ls-remote\` exited non-zero" + echo " $ls_remote_output" + echo "" + echo "Total: $PASS passed, $FAIL failed" + exit 1 +fi +if ! echo "$ls_remote_output" | grep -q gh-pages; then + check_fail "gh-pages branch does not exist on remote" + echo "" + echo "Total: $PASS passed, $FAIL failed" + exit 1 +fi +check_pass "gh-pages branch exists on remote" + +# ------------------------------------------------------------------ +# 2. Set up a temporary worktree to inspect the branch contents +# ------------------------------------------------------------------ +# Use an explicit template so this works on BSD/macOS mktemp (which rejects +# `mktemp -d` with no template), not only GNU coreutils. +# Reserve a unique path via mktemp -d (handles BSD/macOS too), then rmdir it +# so `git worktree add` can create it cleanly. The directory must NOT exist +# at the moment of worktree-add or git errors with "already exists". +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gh-pages-validate.XXXXXX") +rmdir "$WORK_DIR" +cleanup() { + git worktree remove "$WORK_DIR" --force 2>/dev/null || true + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +# Always fetch the latest gh-pages from origin so we validate what's actually +# deployed, not a stale local copy. Use a detached worktree pointing at +# `origin/gh-pages` directly so we don't depend on (and don't update) any +# local `gh-pages` branch the caller might have around. +if ! git fetch origin gh-pages; then + check_fail "Failed to fetch origin gh-pages" + exit 1 +fi +git worktree add --detach "$WORK_DIR" origin/gh-pages + +echo "" +echo "2. Checking required root files..." + +if [ -f "$WORK_DIR/index.html" ]; then + check_pass "index.html exists at root" +else + check_fail "index.html is MISSING from root" +fi + +if [ -f "$WORK_DIR/versions.json" ]; then + check_pass "versions.json exists at root" +else + check_fail "versions.json is MISSING from root (version picker will not work)" +fi + +if [ -f "$WORK_DIR/.nojekyll" ]; then + check_pass ".nojekyll exists (Jekyll processing disabled)" +else + # The canonical DocFX deploy workflow always creates .nojekyll; missing means + # the deploy was botched, not a soft warning. + check_fail ".nojekyll is MISSING from root (GitHub Pages will apply Jekyll processing)" +fi + +# ------------------------------------------------------------------ +# 3. Validate versions.json structure +# ------------------------------------------------------------------ +echo "" +echo "3. Validating versions.json..." + +STEP3_OK=0 +if [ -f "$WORK_DIR/versions.json" ]; then + if python3 - "$WORK_DIR/versions.json" <<'PYEOF' +import json, sys + +path = sys.argv[1] +try: + with open(path) as f: + data = json.load(f) +except json.JSONDecodeError as e: + print(f" ❌ versions.json is not valid JSON: {e}") + sys.exit(1) + +if not isinstance(data, list): + print(" ❌ versions.json must be a JSON array") + sys.exit(1) + +for i, entry in enumerate(data): + if not isinstance(entry, dict): + print(f" ❌ Entry [{i}] is not a JSON object: {entry!r}") + sys.exit(1) + version = entry.get("version") + url = entry.get("url") + if not isinstance(version, str) or not version: + print(f" ❌ Entry [{i}] has missing or non-string 'version': {entry!r}") + sys.exit(1) + if not isinstance(url, str) or not url: + print(f" ❌ Entry [{i}] has missing or non-string 'url': {entry!r}") + sys.exit(1) + +print(f" ✅ versions.json is valid ({len(data)} version(s))") +for v in data: + print(f" {v['version']:20s} -> {v['url']}") +PYEOF + then + PASS=$((PASS + 1)) + STEP3_OK=1 + else + FAIL=$((FAIL + 1)) + fi +fi + +# ------------------------------------------------------------------ +# 4. Verify every version entry has a matching folder with index.html +# ------------------------------------------------------------------ +echo "" +echo "4. Checking version folders match versions.json..." + +# Derive the repository name from the origin remote URL so we can strip the +# project-Pages-site prefix (e.g., '/MyRepo/versions/v1.0.0/') from URLs in +# versions.json before mapping them to filesystem paths under gh-pages. +# On a user/org root Pages site there is no prefix; for project Pages sites +# the prefix is '//'. Either way, after stripping the prefix the URL +# should map directly to a folder on gh-pages. +# +# Use shell parameter expansion rather than sed regex — BSD/macOS sed +# doesn't support the lazy quantifier '+?' and ERE flag spellings differ +# across implementations. Parameter expansion is POSIX and portable. +REPO_NAME="" +REPO_URL=$(git remote get-url origin 2>/dev/null || true) +if [ -n "$REPO_URL" ]; then + REPO_URL=${REPO_URL%.git} # strip optional trailing .git + # Take everything after the last '/' or ':' — handles both + # HTTPS (https://github.com/owner/repo) and SSH + # (git@github.com:owner/repo) remotes. Without the colon + # split, SSH-style remotes without /repo after a / fail. + REPO_NAME=${REPO_URL##*[/:]} +fi + +if [ ! -f "$WORK_DIR/versions.json" ]; then + echo " ⏭️ Skipped — no versions.json present (step 3 did not run)" +elif [ "$STEP3_OK" -ne 1 ]; then + echo " ⏭️ Skipped — versions.json failed validation in step 3" +else + FOLDER_CHECK_RESULT=0 + python3 - "$WORK_DIR" "$REPO_NAME" <<'PYEOF' || FOLDER_CHECK_RESULT=1 +import json, os, sys + +work_dir = sys.argv[1] +repo_name = sys.argv[2] if len(sys.argv) > 2 else "" + +# Sentinel returned when a URL cannot be mapped to a safe folder name. +UNSAFE = object() + +def url_to_folder(url, repo_name): + """Map a versions.json URL to a folder path relative to the gh-pages root. + + Returns None for root-level aliases (no separate folder), UNSAFE for + URLs that would escape the gh-pages root, or a relative folder string.""" + if not url or url == "/": + return None # Root-level alias — no separate folder to check + # Strip the project-Pages prefix '//' if present. + if repo_name and url.startswith(f"/{repo_name}/"): + url = url[len(f"/{repo_name}/"):] + folder = url.strip("/") + if not folder: + return None + # Reject anything that could escape the gh-pages root via parent-dir + # traversal, backslash injection, or absolute paths. This is defense + # against a malformed (or hostile) versions.json on the deployed site. + parts = folder.split("/") + if any(p in ("", "..", ".") or "\\" in p for p in parts): + return UNSAFE + return folder + +with open(os.path.join(work_dir, "versions.json")) as f: + versions = json.load(f) + +missing = [] +for v in versions: + ver = v["version"] + url = v["url"] + folder_name = url_to_folder(url, repo_name) + if folder_name is None: + # Entry points at the site root (typically 'latest' on a user/org Pages + # site). The root index.html is already validated in step 2. + continue + if folder_name is UNSAFE: + missing.append(f"{ver} (url {url!r} would escape gh-pages root — rejected)") + continue + folder = os.path.join(work_dir, folder_name) + # Belt-and-suspenders: verify the resolved real path is still under + # work_dir (catches symlink shenanigans or anything the segment check missed). + real_folder = os.path.realpath(folder) + real_root = os.path.realpath(work_dir) + if os.path.commonpath([real_folder, real_root]) != real_root: + missing.append(f"{ver} (resolved path '{real_folder}' is outside gh-pages root — rejected)") + continue + if not os.path.isdir(folder): + missing.append(f"{ver} (folder '{folder_name}/' not found)") + elif not os.path.isfile(os.path.join(folder, "index.html")): + missing.append(f"{ver} (index.html missing in '{folder_name}/')") + +if missing: + for m in missing: + print(f" ❌ {m}") + sys.exit(1) +else: + print(f" ✅ All versioned folders exist and contain index.html") +PYEOF + + if [ "$FOLDER_CHECK_RESULT" -eq 0 ]; then + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)) + fi +fi + +# ------------------------------------------------------------------ +# 5. Check for known stale DocFX root artifacts +# ------------------------------------------------------------------ +echo "" +echo "5. Checking for stale DocFX root artifacts..." + +# The 'public/' directory is a known DocFX build artifact that should never +# appear at the gh-pages root; its presence indicates a previous deploy did +# not clean up properly. +STALE_PATTERNS=("public") +found_stale=false + +for p in "${STALE_PATTERNS[@]}"; do + if [ -e "$WORK_DIR/$p" ]; then + check_warn "Potentially stale artifact found at root: '$p'" + found_stale=true + fi +done + +if [ "$found_stale" = "false" ]; then + check_pass "No known stale DocFX artifacts found at root" +fi + +# ------------------------------------------------------------------ +# Summary +# ------------------------------------------------------------------ +echo "" +echo "────────────────────────────────────────────────────────" +echo " Results: $PASS passed, $FAIL failed" +echo "────────────────────────────────────────────────────────" +echo "" + +if [ "$FAIL" -gt 0 ]; then + echo "❌ Validation FAILED – review the issues listed above." + exit 1 +else + echo "✅ Validation PASSED" + exit 0 +fi diff --git a/scripts/build-pr.ps1 b/scripts/build-pr.ps1 index d7fd64c1..cb0b8fa3 100644 --- a/scripts/build-pr.ps1 +++ b/scripts/build-pr.ps1 @@ -84,7 +84,17 @@ if (-not $SkipTests -and $failed.Count -eq 0) { $testProjects = @(Get-ChildItem -Path './tests' -Recurse -File -Include '*.csproj', '*.vbproj', '*.fsproj' -ErrorAction SilentlyContinue) if ($testProjects.Count -eq 0) { - Write-Host "No test projects found in ./tests — skipping" + # If ./src has projects, fail — silent skip would diverge from CI's + # strict gate. If neither ./src nor ./tests has projects (template-pack + # / in-dev repos), the skip is legitimate. + $srcHasProjects = @(Get-ChildItem -Path './src' -Recurse -File -Include '*.csproj','*.vbproj','*.fsproj' -ErrorAction SilentlyContinue).Count -gt 0 + if ($srcHasProjects) { + Write-Fail "./tests has no test projects but ./src contains projects — refusing to silently skip the coverage gate." + $failed += "Tests" + } + else { + Write-Host "No test projects found in ./tests and no ./src projects — skipping (template-pack / in-dev shape)." + } } else { foreach ($testProj in $testProjects) { @@ -164,7 +174,23 @@ if (-not $SkipTests -and -not $SkipCoverage -and $failed.Count -eq 0) { $rgPath = Get-Command reportgenerator -ErrorAction SilentlyContinue if (-not $rgPath) { Write-Host "Installing ReportGenerator..." - dotnet tool install -g dotnet-reportgenerator-globaltool + dotnet tool update -g dotnet-reportgenerator-globaltool 2>$null + if ($LASTEXITCODE -ne 0) { dotnet tool install -g dotnet-reportgenerator-globaltool } + # Ensure global tools dir is on PATH for this session. The .NET + # installer normally adds it to the user's profile, but a fresh + # shell or a pwsh-invoked-from-script session may not have it yet. + $globalToolsDir = if ($IsWindows -or $env:OS -eq 'Windows_NT') { + Join-Path $env:USERPROFILE '.dotnet\tools' + } else { + Join-Path $HOME '.dotnet/tools' + } + if (Test-Path $globalToolsDir -PathType Container) { + $sep = [IO.Path]::PathSeparator + $pathSegments = $env:PATH -split [regex]::Escape($sep) + if ($pathSegments -notcontains $globalToolsDir) { + $env:PATH = "$globalToolsDir$sep$env:PATH" + } + } } reportgenerator ` @@ -202,7 +228,11 @@ if (-not $SkipTests -and -not $SkipCoverage -and $failed.Count -eq 0) { } } else { - Write-Host "Coverage report not generated — skipping threshold check" + # Diverged from pr.yaml behavior in the past — that would let a local + # "All checks passed" silently hide ReportGenerator failures while CI + # rejected the same situation. Fail loudly here too, so local matches CI. + Write-Fail "Coverage report not generated (CoverageReport/Summary.txt missing) — ReportGenerator likely failed." + $failed += "Coverage" } } } @@ -259,7 +289,7 @@ if (-not $SkipSecurity) { $dest = Join-Path $env:LOCALAPPDATA "gitleaks" New-Item -ItemType Directory -Force -Path $dest | Out-Null $zip = Join-Path $env:TEMP $archive - Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing + Invoke-WebRequest -Uri $url -OutFile $zip Expand-Archive -Path $zip -DestinationPath $dest -Force Remove-Item $zip -ErrorAction SilentlyContinue $env:PATH = "$dest;$env:PATH" @@ -267,7 +297,15 @@ if (-not $SkipSecurity) { else { $archive = "gitleaks_${version}_linux_x64.tar.gz" $url = "https://github.com/gitleaks/gitleaks/releases/download/v${version}/$archive" - curl -sSfL $url | tar xz -C /usr/local/bin gitleaks + # Install to a user-writable location instead of /usr/local/bin + # (which would require sudo for most local dev shells). $HOME/.local/bin + # is on PATH by default on most Linux distros and macOS; if not, prepend it. + $localBin = Join-Path $HOME ".local/bin" + New-Item -ItemType Directory -Force -Path $localBin | Out-Null + curl -sSfL $url | tar xz -C $localBin gitleaks + if (-not ($env:PATH -split [IO.Path]::PathSeparator | Where-Object { $_ -eq $localBin })) { + $env:PATH = "$localBin$([IO.Path]::PathSeparator)$env:PATH" + } } } diff --git a/scripts/format.ps1 b/scripts/format.ps1 index 0a296dc5..21cb4015 100644 --- a/scripts/format.ps1 +++ b/scripts/format.ps1 @@ -11,12 +11,17 @@ If specified, only checks formatting without making changes (like CI does). .EXAMPLE - pwsh ./scripts/format.ps1 - Formats all code in the repository. + .\scripts\format.ps1 + Formats all code in the repository (invoke from repo root). .EXAMPLE - pwsh ./scripts/format.ps1 -Check + .\scripts\format.ps1 -Check Checks formatting without making changes. + +.NOTES + The script resolves the solution from the repo root regardless of + the caller's current directory, so invoking it from any working + directory inside the repo works. #> param( @@ -25,6 +30,12 @@ param( $ErrorActionPreference = "Stop" +# Pin cwd to the repo root so the solution lookup below works regardless +# of where the caller invokes the script from (repo root, scripts/, etc). +$repoRoot = (Resolve-Path -Path (Join-Path $PSScriptRoot '..')).Path +Push-Location $repoRoot +try { + Write-Host "🎨 Code Formatting Script" -ForegroundColor Cyan Write-Host "" @@ -38,9 +49,11 @@ if ($LASTEXITCODE -ne 0) Write-Host "❌ dotnet format is not available!" -ForegroundColor Red Write-Host "" Write-Host "The 'dotnet format' command is built into the .NET SDK starting with .NET 6." -ForegroundColor Yellow - Write-Host "This project requires .NET 8.0 SDK or later." -ForegroundColor Yellow + Write-Host "You need an SDK new enough to load this repo's target frameworks — see" -ForegroundColor Yellow + Write-Host ".github/workflows/pr.yaml (and global.json if present) for the SDK" -ForegroundColor Yellow + Write-Host "versions CI uses. The latest stable .NET SDK is generally a safe choice." -ForegroundColor Yellow Write-Host "" - Write-Host "Please install the .NET 8.0 SDK or later from:" -ForegroundColor Yellow + Write-Host "Install the .NET SDK from:" -ForegroundColor Yellow Write-Host "https://dotnet.microsoft.com/download" -ForegroundColor Cyan Write-Host "" exit 1 @@ -78,7 +91,7 @@ if ($Check) { Write-Host "" Write-Host "❌ Formatting issues detected!" -ForegroundColor Red - Write-Host "Run '.\format.ps1' (without -Check) to fix them automatically." -ForegroundColor Yellow + Write-Host "Run '.\scripts\format.ps1' (without -Check) to fix them automatically." -ForegroundColor Yellow exit 1 } } @@ -102,3 +115,6 @@ else exit 1 } } +} finally { + Pop-Location +} diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 deleted file mode 100644 index 37a218fd..00000000 --- a/scripts/setup.ps1 +++ /dev/null @@ -1,1042 +0,0 @@ -#!/usr/bin/env pwsh -#Requires -Version 7.0 - -<# -.SYNOPSIS - Automated setup script for .NET repository template -.DESCRIPTION - This script automates the process of configuring a new repository created from this template. - It prompts for project information, replaces placeholders, sets up the license, and validates changes. - - The script automatically ensures it runs from the repository root directory: - - If run from the scripts/ directory, it will automatically change to the repository root - - If run from any other location, it will display an error and exit - -.EXAMPLE - # Recommended: Run from repository root - pwsh ./scripts/setup.ps1 - -.EXAMPLE - # Also works: Run from scripts directory (auto-corrects to root) - cd scripts - pwsh ./setup.ps1 - -.NOTES - Requires PowerShell Core 7.0 or later (cross-platform) -#> - -[CmdletBinding()] -param() - -# Enable strict mode -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Color output functions -function Write-Success { - param([string]$Message) - Write-Host "✅ $Message" -ForegroundColor Green -} - -function Write-Info { - param([string]$Message) - Write-Host "ℹ️ $Message" -ForegroundColor Cyan -} - -function Write-TemplateWarning { - param([string]$Message) - Write-Host "⚠️ $Message" -ForegroundColor Yellow -} - -function Write-TemplateError { - param([string]$Message) - Write-Host "❌ $Message" -ForegroundColor Red -} - -function Write-Step { - param([string]$Message) - Write-Host "`n🔧 $Message" -ForegroundColor Magenta -} - -# Banner -function Show-Banner { - Write-Host @" - -╔════════════════════════════════════════════════════════════════╗ -║ ║ -║ .NET Repository Template - Automated Setup ║ -║ ║ -╚════════════════════════════════════════════════════════════════╝ - -"@ -ForegroundColor Cyan -} - -# Ensure script is running from repository root -function Set-RepositoryRoot { - # Get the directory where the script is located - $scriptDir = Split-Path -Parent $PSCommandPath - - # If we're in the scripts directory, move up one level to the repository root - if ((Split-Path -Leaf $scriptDir) -eq 'scripts') { - $repoRoot = Split-Path -Parent $scriptDir - Set-Location $repoRoot - Write-Info "Changed working directory to repository root: $repoRoot" - } - - # Verify we're in the repository root by checking for key marker files - $markerFiles = @('README.md', '.gitignore', 'CONTRIBUTING.md') - $foundMarkers = @($markerFiles | Where-Object { Test-Path $_ }) - - if ($foundMarkers.Count -lt 2) { - Write-TemplateError "This script must be run from the repository root directory." - Write-Host "Expected to find key files like: $($markerFiles -join ', ')" -ForegroundColor Red - Write-Host "Current directory: $(Get-Location)" -ForegroundColor Yellow - Write-Host "" - Write-Host "Please run the script from the repository root:" -ForegroundColor Yellow - Write-Host " pwsh ./scripts/setup.ps1" -ForegroundColor Cyan - throw "Script not running from repository root" - } -} - -# Auto-detect git information -function Get-GitInfo { - $gitInfo = @{ - RemoteUrl = '' - RepoName = '' - Username = '' - UserEmail = '' - FullName = '' - } - - try { - # Get remote URL - $remoteUrl = git remote get-url origin 2>$null - if ($remoteUrl) { - $gitInfo.RemoteUrl = $remoteUrl -replace '\.git$', '' - - # Extract repo name - if ($remoteUrl -match '/([^/]+?)(?:\.git)?$') { - $gitInfo.RepoName = $matches[1] - } - - # Extract username (for GitHub URLs) - if ($remoteUrl -match 'github\.com[:/]([^/]+)/') { - $gitInfo.Username = "@$($matches[1])" - } - } - - # Get git user name - $userName = git config user.name 2>$null - if ($userName) { - $gitInfo.FullName = $userName - } - - # Get git user email - $userEmail = git config user.email 2>$null - if ($userEmail) { - $gitInfo.UserEmail = $userEmail - } - } - catch { - Write-Warning "Could not auto-detect git information" - } - - return $gitInfo -} - -# Prompt for input with default and example -function Read-Input { - param( - [string]$Prompt, - [string]$Default = '', - [string]$Example = '', - [switch]$Required - ) - - $message = $Prompt - if ($Example) { - $message += "`n Example: $Example" - } - if ($Default) { - $message += "`n Default: $Default" - } - $message += "`n > " - - do { - Write-Host $message -NoNewline -ForegroundColor Yellow - $userInput = Read-Host - - if ([string]::IsNullOrWhiteSpace($userInput) -and $Default) { - return $Default - } - - if ([string]::IsNullOrWhiteSpace($userInput) -and $Required) { - Write-TemplateError "This field is required. Please enter a value." - continue - } - - return $userInput - } while ($true) -} - -# Replace placeholders in a file -function Replace-Placeholders { - param( - [string]$FilePath, - [hashtable]$Replacements - ) - - if (-not (Test-Path $FilePath)) { - Write-Warning "File not found: $FilePath" - return - } - - $content = Get-Content $FilePath -Raw - $modified = $false - - foreach ($key in $Replacements.Keys) { - $placeholder = "{{$key}}" - if ($content -match [regex]::Escape($placeholder)) { - $pattern = [regex]::Escape($placeholder) - $content = [regex]::Replace( - $content, - $pattern, - [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $Replacements[$key] } - ) - $modified = $true - } - } - - if ($modified) { - Set-Content -Path $FilePath -Value $content - Write-Success "Updated: $FilePath" - } -} - -# Main setup function -function Start-Setup { - Show-Banner - - # Ensure we're in the repository root - Set-RepositoryRoot - - Write-Info "This script will configure your new repository." - Write-Info "It will prompt you for project information and replace all placeholders." - Write-Host "" - - # Auto-detect git info - Write-Step "Auto-detecting git repository information..." - $gitInfo = Get-GitInfo - - if ($gitInfo.RemoteUrl) { - Write-Success "Detected repository: $($gitInfo.RemoteUrl)" - } - - # Collect project information - Write-Step "Collecting project information..." - Write-Host "" - - # Ask if creating NuGet package - Write-Host "Will this project be published as a NuGet package? (Y/n): " -NoNewline -ForegroundColor Yellow - $createNugetPackage = Read-Host - if ([string]::IsNullOrEmpty($createNugetPackage) -or $createNugetPackage -eq 'Y' -or $createNugetPackage -eq 'y') { - $isNugetPackage = $true - } - else { - $isNugetPackage = $false - } - Write-Host "" - - $projectName = Read-Input ` - -Prompt "Project Name (e.g., Wolfgang.Extensions.IAsyncEnumerable)" ` - -Example "MyCompany.MyLibrary" ` - -Required - - $projectDescription = Read-Input ` - -Prompt "Project Description (one-line description)" ` - -Example "High-performance extension methods for IAsyncEnumerable" ` - -Required - - if ($isNugetPackage) { - $packageName = Read-Input ` - -Prompt "NuGet Package Name" ` - -Default $projectName ` - -Example $projectName - } - else { - $packageName = $projectName - } - - $githubRepoUrl = Read-Input ` - -Prompt "GitHub Repository URL" ` - -Default $gitInfo.RemoteUrl ` - -Example "https://github.com/username/repo-name" ` - -Required - - # Extract repo name from URL if not already detected - $repoName = $gitInfo.RepoName - if ([string]::IsNullOrWhiteSpace($repoName) -and $githubRepoUrl -match '/([^/]+?)(?:\.git)?$') { - $repoName = $matches[1] - } - if ([string]::IsNullOrWhiteSpace($repoName)) { - $repoName = Read-Input ` - -Prompt "Repository Name" ` - -Example "my-repo-name" ` - -Required - } - - $githubUsername = Read-Input ` - -Prompt "GitHub Username (with @)" ` - -Default $gitInfo.Username ` - -Example "@YourUsername" ` - -Required - - # Ensure @ prefix - if ($githubUsername -notmatch '^@') { - $githubUsername = "@$githubUsername" - } - - # Normalize GitHub URL and generate docs URL - # Handle SSH URLs (git@github.com:org/repo.git) and HTTPS URLs - # Remove trailing .git and normalize to https://github.com// - $normalizedUrl = $githubRepoUrl - - # Convert SSH URL to HTTPS format - if ($normalizedUrl -match '^git@github\.com:(.+)$') { - $normalizedUrl = "https://github.com/$($matches[1])" - } - - # Remove trailing .git - $normalizedUrl = $normalizedUrl -replace '\.git$', '' - - # Extract owner and repo from normalized HTTPS URL - $docsUrl = $normalizedUrl -replace 'https://github\.com/([^/]+)/([^/]+).*', 'https://$1.github.io/$2/' - - $docsUrl = Read-Input ` - -Prompt "Documentation URL (GitHub Pages)" ` - -Default $docsUrl ` - -Example "https://username.github.io/repo-name/" - - # Get copyright holder - $copyrightHolder = Read-Input ` - -Prompt "Copyright Holder Name" ` - -Default $gitInfo.FullName ` - -Example "John Doe" ` - -Required - - $currentYear = (Get-Date).Year - $year = Read-Input ` - -Prompt "Copyright Year" ` - -Default $currentYear.ToString() ` - -Example $currentYear.ToString() - - if ($isNugetPackage) { - $nugetStatus = Read-Input ` - -Prompt "NuGet Package Status" ` - -Default "Coming soon to NuGet.org" ` - -Example "Available on NuGet.org" - } - else { - $nugetStatus = "Not applicable" - } - - # License selection - Write-Step "Selecting License..." - Write-Host "" - Write-Host "Available licenses:" -ForegroundColor Yellow - Write-Host " 1) MIT - Most permissive, simple, business-friendly" - Write-Host " 2) Apache-2.0 - Permissive with patent grant" - Write-Host " 3) MPL-2.0 - Weak copyleft, file-level" - Write-Host "" - Write-Host "For detailed comparison, see LICENSE-SELECTION.md" -ForegroundColor Cyan - Write-Host "" - - do { - Write-Host "Select license (1-3): " -NoNewline -ForegroundColor Yellow - $licenseChoice = Read-Host - - switch ($licenseChoice) { - '1' { - $licenseType = 'MIT' - $licenseFile = 'LICENSE-MIT.txt' - break - } - '2' { - $licenseType = 'Apache-2.0' - $licenseFile = 'LICENSE-APACHE-2.0.txt' - break - } - '3' { - $licenseType = 'MPL-2.0' - $licenseFile = 'LICENSE-MPL-2.0.txt' - break - } - default { - Write-TemplateError "Invalid choice. Please enter 1, 2, or 3." - continue - } - } - break - } while ($true) - - Write-Success "Selected: $licenseType License" - - # Template repository info (for REPO-INSTRUCTIONS.md) - $templateRepoOwner = Read-Input ` - -Prompt "Template Repository Owner (the GitHub user/org that owns the template you used)" ` - -Default "Chris-Wolfgang" ` - -Example "YourUsername" - - $templateRepoName = Read-Input ` - -Prompt "Template Repository Name (the name of the template repository you used)" ` - -Default "repo-template" ` - -Example "my-template" - - # Solution creation - Write-Step "Solution Creation" - Write-Host "" - Write-Host "Create a default solution? (y/N): " -NoNewline -ForegroundColor Yellow - $createSolution = Read-Host - - $solutionName = '' - if ($createSolution -eq 'y' -or $createSolution -eq 'Y') { - $isValidSolutionName = $false - while (-not $isValidSolutionName) { - $solutionName = Read-Input ` - -Prompt "Solution Name" ` - -Default $repoName ` - -Example $repoName ` - -Required - - $invalidFileNameChars = [System.IO.Path]::GetInvalidFileNameChars() - if ($solutionName.IndexOfAny($invalidFileNameChars) -ne -1) { - $invalidCharsDisplay = -join $invalidFileNameChars - Write-Error "Solution name contains invalid characters. Please avoid any of: $invalidCharsDisplay" -ErrorAction Continue - } - else { - $isValidSolutionName = $true - } - } - } - - # Summary - Write-Step "Configuration Summary" - Write-Host "" - Write-Host "Project Information:" -ForegroundColor Cyan - Write-Host " Project Name: $projectName" - Write-Host " Description: $projectDescription" - Write-Host " Package Name: $packageName" - Write-Host " Repository URL: $githubRepoUrl" - Write-Host " Repository Name: $repoName" - Write-Host " GitHub Username: $githubUsername" - Write-Host " Documentation URL: $docsUrl" - Write-Host " License: $licenseType" - Write-Host " Copyright Holder: $copyrightHolder" - Write-Host " Copyright Year: $year" - Write-Host " NuGet Status: $nugetStatus" - Write-Host " Template Owner: $templateRepoOwner" - Write-Host " Template Name: $templateRepoName" - if ($solutionName) { - Write-Host " Solution Name: $solutionName" - } - Write-Host "" - - Write-Host "Proceed with configuration? (Y/n): " -NoNewline -ForegroundColor Yellow - $confirm = Read-Host - if ($confirm -and $confirm -ne 'Y' -and $confirm -ne 'y') { - Write-Warning "Setup cancelled." - exit 0 - } - - # Create replacements hashtable - $replacements = @{ - 'PROJECT_NAME' = $projectName - 'PROJECT_DESCRIPTION' = $projectDescription - 'PACKAGE_NAME' = $packageName - 'GITHUB_REPO_URL' = $githubRepoUrl - 'REPO_NAME' = $repoName - 'GITHUB_USERNAME' = $githubUsername - 'DOCS_URL' = $docsUrl - 'LICENSE_TYPE' = $licenseType - 'YEAR' = $year - 'COPYRIGHT_HOLDER' = $copyrightHolder - 'NUGET_STATUS' = $nugetStatus - 'TEMPLATE_REPO_OWNER' = $templateRepoOwner - 'TEMPLATE_REPO_NAME' = $templateRepoName - } - - # Perform setup - Write-Step "Performing setup..." - Write-Host "" - - $totalSteps = if ($solutionName) { 5 } else { 4 } - - # Step 1: README swap - Write-Info "Step 1/${totalSteps}: Swapping README files..." - if (Test-Path 'README.md') { - Remove-Item 'README.md' -Force - Write-Success "Deleted template README.md" - } - - if (Test-Path 'README-TEMPLATE.md') { - Rename-Item 'README-TEMPLATE.md' 'README.md' - Write-Success "Renamed README-TEMPLATE.md → README.md" - } - else { - Write-Error "README-TEMPLATE.md not found!" - exit 1 - } - - # Step 2: Replace placeholders - Write-Info "Step 2/${totalSteps}: Replacing placeholders in files..." - - $filesToUpdate = @( - 'README.md', - 'CONTRIBUTING.md', - '.github/CODEOWNERS', - 'REPO-INSTRUCTIONS.md', - 'scripts/Setup-BranchRuleset.ps1', - 'docfx_project/docfx.json', - 'docfx_project/index.md', - 'docfx_project/api/index.md', - 'docfx_project/api/README.md', - 'docfx_project/docs/toc.yml', - 'docfx_project/docs/introduction.md', - 'docfx_project/docs/getting-started.md', - 'BannedSymbols.txt' - ) - - foreach ($file in $filesToUpdate) { - Replace-Placeholders -FilePath $file -Replacements $replacements - } - - # Step 3: Set up LICENSE - Write-Info "Step 3/${totalSteps}: Setting up LICENSE file..." - - if (Test-Path $licenseFile) { - # Read license template - $licenseContent = Get-Content $licenseFile -Raw - - # Replace placeholders using safe regex replacement with MatchEvaluator - $licenseContent = [regex]::Replace( - $licenseContent, - [regex]::Escape('{{YEAR}}'), - [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $year } - ) - $licenseContent = [regex]::Replace( - $licenseContent, - [regex]::Escape('{{COPYRIGHT_HOLDER}}'), - [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $copyrightHolder } - ) - - # Save as LICENSE - Set-Content -Path 'LICENSE' -Value $licenseContent -NoNewline - Write-Success "Created LICENSE file ($licenseType)" - - # Delete all license templates - Remove-Item 'LICENSE-MIT.txt' -Force -ErrorAction SilentlyContinue - Remove-Item 'LICENSE-APACHE-2.0.txt' -Force -ErrorAction SilentlyContinue - Remove-Item 'LICENSE-MPL-2.0.txt' -Force -ErrorAction SilentlyContinue - Write-Success "Removed license template files" - } - else { - Write-Error "License template file not found: $licenseFile" - exit 1 - } - - # Step 4: Create solution (if requested) - if ($solutionName) { - Write-Info "Step 4/${totalSteps}: Creating solution file..." - - # Create blank solution in .slnx format - # Note: .slnx format requires Visual Studio 2022 version 17.10 or later - $solutionFileName = "$solutionName.slnx" - - # Build the solution XML structure - $xmlBuilder = New-Object System.Text.StringBuilder - [void]$xmlBuilder.AppendLine('') - - # Build .root folder with all remaining files - # Exclude files and directories that have their own solution folders or are build artifacts - # Note: .git directory is excluded separately below - $excludePatterns = @( - 'obj', # Build output - 'bin', # Build output - 'TestResults', # Test artifacts - 'CoverageReport', # Coverage artifacts - 'node_modules', # Node dependencies - '*.user', # User-specific files - '*.suo', # Visual Studio user options - '*.sln', # Solution files (prevent including solution in itself) - '*.slnx', # Solution files (prevent including solution in itself) - '*.env', # Environment files (may contain secrets) - '*.key', # Key files (may contain secrets) - '*.pem', # Certificate files (may contain secrets) - 'secrets*', # Secret files - 'benchmarks', # Has its own solution folder - 'examples', # Has its own solution folder - 'src', # Has its own solution folder - 'tests', # Has its own solution folder - 'docfx_project' # Documentation source (built separately) - ) - - # Get current directory for relative path calculation - $currentDir = Get-Location - - # Helper function to get relative path safely - function Get-SafeRelativePath { - param($FullPath) - try { - # Use Resolve-Path with -Relative for safe relative path calculation - $rel = Resolve-Path -Path $FullPath -Relative -ErrorAction Stop - # Remove leading .\ or ./ prefix properly - if ($rel.StartsWith('.\')) { - $rel = $rel.Substring(2) - } - elseif ($rel.StartsWith('./')) { - $rel = $rel.Substring(2) - } - return $rel.Replace('\', '/') - } - catch { - # Fallback: manual calculation - $path = $FullPath - if ($path.StartsWith($currentDir.Path, [System.StringComparison]::OrdinalIgnoreCase)) { - $baseLength = $currentDir.Path.Length - # Ensure we only strip the base path when it's a complete directory component - if ($path.Length -eq $baseLength -or - ($path.Length -gt $baseLength -and - ($path[$baseLength] -eq [System.IO.Path]::DirectorySeparatorChar -or - $path[$baseLength] -eq [System.IO.Path]::AltDirectorySeparatorChar))) { - # Remove the base path and any leading separator - $path = $path.Substring($baseLength) - if ($path.StartsWith('\') -or $path.StartsWith('/')) { - $path = $path.Substring(1) - } - } - } - return $path.Replace('\', '/') - } - } - - # Get all files in the repository - $allFiles = Get-ChildItem -Recurse -File -Force | Where-Object { - # Get relative path safely - $relativePath = Get-SafeRelativePath $_.FullName - - # Exclude files under .git directory specifically (not .github) - if ($relativePath -like '.git/*') { - return $false - } - - # Exclude hidden files (starting with .) except those in .github directory - $fileName = [System.IO.Path]::GetFileName($relativePath) - $isInGitHubDir = $relativePath -like '.github/*' - if ($fileName.StartsWith('.') -and -not $isInGitHubDir) { - return $false - } - - # Exclude files matching patterns using precise matching - $shouldExclude = $false - $pathSegments = $relativePath -split '[\\/]+' - $fileExtension = [System.IO.Path]::GetExtension($relativePath) - - foreach ($pattern in $excludePatterns) { - # Handle extension patterns like '*.user' or '*.suo' - if ($pattern.StartsWith('*.')) { - $ext = $pattern.Substring(1) - if ($fileExtension -ieq $ext) { - $shouldExclude = $true - break - } - } - # Handle wildcard patterns like 'secrets*' - elseif ($pattern.Contains('*')) { - if ($relativePath -like $pattern) { - $shouldExclude = $true - break - } - } - # Treat as a path segment name and match against segments - else { - if ($pathSegments -contains $pattern) { - $shouldExclude = $true - break - } - } - } - - -not $shouldExclude - } - - # Group files by directory for .root structure - # Cache relative paths to avoid recalculating - $filesByDirectory = @{} - $relativePathCache = @{} - - foreach ($file in $allFiles) { - # Get relative path safely (use cached if available) - if (-not $relativePathCache.ContainsKey($file.FullName)) { - $relativePathCache[$file.FullName] = Get-SafeRelativePath $file.FullName - } - $relativePath = $relativePathCache[$file.FullName] - $directory = Split-Path $relativePath -Parent - if ([string]::IsNullOrEmpty($directory)) { - $directory = '.' - } - else { - $directory = $directory.Replace('\', '/') - } - - if (-not $filesByDirectory.ContainsKey($directory)) { - $filesByDirectory[$directory] = @() - } - $filesByDirectory[$directory] += $relativePath - } - - # Sort directories to ensure proper nesting order - $sortedDirectories = $filesByDirectory.Keys | Sort-Object - - # Build folder structure with XML escaping - foreach ($directory in $sortedDirectories) { - if ($directory -eq '.') { - # Root files - [void]$xmlBuilder.AppendLine(' ') - foreach ($filePath in ($filesByDirectory[$directory] | Sort-Object)) { - $escapedPath = [System.Security.SecurityElement]::Escape($filePath) - [void]$xmlBuilder.AppendLine(" ") - } - [void]$xmlBuilder.AppendLine(' ') - } - else { - # Subdirectory files - $folderName = "/.root/$directory/" - $escapedFolderName = [System.Security.SecurityElement]::Escape($folderName) - [void]$xmlBuilder.AppendLine(" ") - foreach ($filePath in ($filesByDirectory[$directory] | Sort-Object)) { - $escapedPath = [System.Security.SecurityElement]::Escape($filePath) - [void]$xmlBuilder.AppendLine(" ") - } - [void]$xmlBuilder.AppendLine(' ') - } - } - - # Add solution folders for benchmarks, examples, src, tests (only if directories exist) - # These are added after .root to prioritize configuration files in solution explorer - $solutionFolders = @('benchmarks', 'examples', 'src', 'tests') - foreach ($folder in $solutionFolders) { - if (Test-Path -Path $folder -PathType Container) { - [void]$xmlBuilder.AppendLine(" ") - } - } - - [void]$xmlBuilder.AppendLine('') - - # Write solution file with error handling - try { - Set-Content -Path $solutionFileName -Value $xmlBuilder.ToString() -ErrorAction Stop - Write-Success "Created solution file: $solutionFileName" - - # Show summary - $fileCount = $allFiles.Count - $folderCount = $filesByDirectory.Keys.Count - Write-Info "Added $fileCount files in $folderCount folders to .root/" - } - catch { - Write-TemplateWarning "Failed to create solution file '$solutionFileName'. Repository setup will continue." - Write-TemplateWarning "Error: $($_.Exception.Message)" - # Clear solutionFileName so Next Steps won't reference it - $solutionFileName = '' - } - } - - # Step 5: Validation - Write-Info "Step ${totalSteps}/${totalSteps}: Validating changes..." - - # Core placeholders that should have been replaced by the script - # Note: YEAR and COPYRIGHT_HOLDER are handled in LICENSE file generation, not in FILES_TO_UPDATE - $corePlaceholders = @( - 'PROJECT_NAME', 'PROJECT_DESCRIPTION', 'PACKAGE_NAME', - 'GITHUB_REPO_URL', 'REPO_NAME', 'GITHUB_USERNAME', - 'DOCS_URL', 'LICENSE_TYPE', - 'NUGET_STATUS', 'TEMPLATE_REPO_OWNER', 'TEMPLATE_REPO_NAME' - ) - - # Optional placeholders that users fill in manually as they develop - $optionalPlaceholderDescriptions = @{ - 'QUICK_START_EXAMPLE' = 'Code example showing basic usage' - 'FEATURES_TABLE' = 'Markdown table listing features' - 'FEATURE_EXAMPLES' = 'Code examples demonstrating features' - 'TARGET_FRAMEWORKS' = 'List of supported .NET frameworks' - 'ACKNOWLEDGMENTS' = 'Credits for libraries/tools used' - } - - # Collect placeholders grouped by placeholder name - $corePlaceholdersByName = @{} - $optionalPlaceholdersByName = @{} - - foreach ($file in $filesToUpdate) { - if (Test-Path $file) { - $content = Get-Content $file -Raw - $matches = [regex]::Matches($content, '\{\{([A-Z_]+)\}\}') - foreach ($match in $matches) { - $placeholderName = $match.Groups[1].Value - - # Categorize placeholder - if ($corePlaceholders -contains $placeholderName) { - if (-not $corePlaceholdersByName.ContainsKey($placeholderName)) { - $corePlaceholdersByName[$placeholderName] = @() - } - if ($corePlaceholdersByName[$placeholderName] -notcontains $file) { - $corePlaceholdersByName[$placeholderName] += $file - } - } - elseif ($optionalPlaceholderDescriptions.ContainsKey($placeholderName)) { - if (-not $optionalPlaceholdersByName.ContainsKey($placeholderName)) { - $optionalPlaceholdersByName[$placeholderName] = @() - } - if ($optionalPlaceholdersByName[$placeholderName] -notcontains $file) { - $optionalPlaceholdersByName[$placeholderName] += $file - } - } - } - } - } - - # Report core placeholders that weren't replaced (this is an error) - if ($corePlaceholdersByName.Count -gt 0) { - Write-TemplateError "Error: The following required placeholders were not replaced:" - Write-Host "" - foreach ($placeholderName in ($corePlaceholdersByName.Keys | Sort-Object)) { - Write-Host " {{$placeholderName}}" -ForegroundColor Red - Write-Host " Found in:" -ForegroundColor Gray - foreach ($file in $corePlaceholdersByName[$placeholderName]) { - Write-Host " - $file" -ForegroundColor Gray - } - Write-Host "" - } - Write-Warning "This indicates the script did not replace all required placeholders. Please review the files and replace these manually." - Write-Host "" - exit 1 - } - else { - Write-Success "All required placeholders replaced successfully!" - } - - # Report optional placeholders that need manual updates - if ($optionalPlaceholdersByName.Count -gt 0) { - Write-Host "" - Write-Info "Optional content placeholders to fill in as you develop your project:" - Write-Host "" - - foreach ($placeholderName in ($optionalPlaceholdersByName.Keys | Sort-Object)) { - $description = $optionalPlaceholderDescriptions[$placeholderName] - - Write-Host " {{$placeholderName}}" -ForegroundColor Yellow - Write-Host " Description: $description" -ForegroundColor Gray - Write-Host " Found in:" -ForegroundColor Gray - foreach ($file in $optionalPlaceholdersByName[$placeholderName]) { - Write-Host " - $file" -ForegroundColor Gray - } - Write-Host "" - } - Write-Info "See TEMPLATE-PLACEHOLDERS.md for details on each placeholder." - } - - # Optional cleanup - Write-Step "Cleanup" - Write-Host "" - Write-Host "Remove template-specific files? (y/N)" -ForegroundColor Yellow - Write-Host " Files to remove:" -ForegroundColor Gray - Write-Host " - scripts/setup.ps1 (this script)" -ForegroundColor Gray - Write-Host " - LICENSE-SELECTION.md" -ForegroundColor Gray - Write-Host "" - Write-Host " Note: TEMPLATE-PLACEHOLDERS.md will remain for your reference." -ForegroundColor Cyan - Write-Host " Delete it manually when you've reviewed it and no longer need it." -ForegroundColor Cyan - Write-Host "" - Write-Host "Remove template files? (y/N): " -NoNewline -ForegroundColor Yellow - $cleanup = Read-Host - - if ($cleanup -eq 'y' -or $cleanup -eq 'Y') { - $filesToRemove = @( - 'scripts/setup.ps1', - 'LICENSE-SELECTION.md' - ) - - foreach ($file in $filesToRemove) { - if (Test-Path $file) { - Remove-Item $file -Force - Write-Success "Removed: $file" - } - } - } - else { - Write-Info "Keeping template files. You can remove them manually later." - } - - # Success! - Write-Host "" - Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Green - Write-Host "║ ║" -ForegroundColor Green - Write-Host "║ 🎉 Setup Complete! 🎉 ║" -ForegroundColor Green - Write-Host "║ ║" -ForegroundColor Green - Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Green - Write-Host "" - - # Git operations - Write-Step "Git Operations" - Write-Host "" - - # Step 1: Create branch and commit changes - Write-Host "Create a branch and commit these changes? (Y/n): " -NoNewline -ForegroundColor Yellow - $commitChanges = Read-Host - if ([string]::IsNullOrEmpty($commitChanges) -or $commitChanges -eq 'Y' -or $commitChanges -eq 'y') { - # Generate branch name - $branchName = "setup/configure-from-template-$(Get-Date -Format 'yyyyMMdd-HHmmss')" - - Write-Info "Step 1/4: Creating branch '$branchName'..." - git checkout -b $branchName - if ($LASTEXITCODE -eq 0) { - Write-Success "Branch created successfully!" - Write-Host "" - - Write-Info "Step 2/4: Committing changes..." - git add . - if ($LASTEXITCODE -eq 0) { - git commit -m "Configure repository from template" - if ($LASTEXITCODE -eq 0) { - Write-Success "Changes committed successfully!" - Write-Host "" - - # Step 3: Push to GitHub - Write-Info "Step 3/4: Pushing branch to GitHub..." - git push -u origin $branchName - if ($LASTEXITCODE -eq 0) { - Write-Success "Branch pushed to GitHub successfully!" - Write-Host "" - - # Step 4: Create Pull Request - Write-Info "Step 4/4: Creating pull request..." - - # Check if gh command is available - try { - $null = Get-Command gh -ErrorAction Stop - - gh pr create --title "Configure repository from template" --body "This PR contains the initial repository configuration from the template setup script.`n`nPlease review the changes, make any necessary adjustments, and merge to main when ready." --base main --head $branchName - if ($LASTEXITCODE -eq 0) { - Write-Success "Pull request created successfully!" - Write-Host "" - - # Get PR URL (best-effort; fall back to generic instruction on failure) - $prUrl = gh pr view $branchName --json url --jq .url 2>$null - if ($LASTEXITCODE -eq 0 -and $prUrl) { - Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan - Write-Host "║ ║" -ForegroundColor Cyan - Write-Host "║ 📋 Review Required ║" -ForegroundColor Cyan - Write-Host "║ ║" -ForegroundColor Cyan - Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan - Write-Host "" - Write-Host "Branch: $branchName" -ForegroundColor Yellow - Write-Host "Pull Request: $prUrl" -ForegroundColor Yellow - Write-Host "" - Write-Info "Please review the pull request, make any necessary changes, and merge it to main before continuing with development." - Write-Host "" - } - else { - Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan - Write-Host "║ ║" -ForegroundColor Cyan - Write-Host "║ 📋 Review Required ║" -ForegroundColor Cyan - Write-Host "║ ║" -ForegroundColor Cyan - Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan - Write-Host "" - Write-Host "Branch: $branchName" -ForegroundColor Yellow - Write-Host "" - Write-Info "Please review the pull request, make any necessary changes, and merge it to main before continuing with development." - Write-Info "You can view the pull request with: gh pr view $branchName --web" - Write-Host "" - } - } - else { - Write-TemplateWarning "Failed to create pull request. You can create it manually with:" - Write-Host " gh pr create --title ""Configure repository from template"" --body ""Initial setup"" --base main --head $branchName" -ForegroundColor Gray - Write-Host "" - } - } - catch { - Write-TemplateWarning "GitHub CLI (gh) is not installed or not available in PATH." - Write-TemplateWarning "Please install it from https://cli.github.com/ to enable automatic PR creation." - Write-Host "" - Write-Info "You can create the pull request manually with:" - Write-Host " gh pr create --title ""Configure repository from template"" --body ""Initial setup"" --base main --head $branchName" -ForegroundColor Gray - Write-Host "" - } - } - else { - Write-TemplateWarning "Push failed. You can push manually later with:" - Write-Host " git push -u origin $branchName" -ForegroundColor Gray - Write-Host "" - } - } - else { - Write-TemplateWarning "Commit failed. You can commit manually later with:" - Write-Host " git commit -m ""Configure repository from template""" -ForegroundColor Gray - Write-Host " git push -u origin $branchName" -ForegroundColor Gray - Write-Host "" - } - } - else { - Write-TemplateWarning "Git add failed. You can commit manually later with:" - Write-Host " git add ." -ForegroundColor Gray - Write-Host " git commit -m ""Configure repository from template""" -ForegroundColor Gray - Write-Host " git push -u origin $branchName" -ForegroundColor Gray - Write-Host "" - } - } - else { - Write-TemplateWarning "Failed to create branch. You can create it manually with:" - Write-Host " git checkout -b $branchName" -ForegroundColor Gray - Write-Host " git add ." -ForegroundColor Gray - Write-Host " git commit -m ""Configure repository from template""" -ForegroundColor Gray - Write-Host " git push -u origin $branchName" -ForegroundColor Gray - Write-Host "" - } - } - else { - Write-Info "Skipping branch creation and commit. You can do this manually later with:" - Write-Host " git checkout -b setup/configure-from-template-" -ForegroundColor Gray - Write-Host " git add ." -ForegroundColor Gray - Write-Host " git commit -m ""Configure repository from template""" -ForegroundColor Gray - Write-Host " git push -u origin setup/configure-from-template-" -ForegroundColor Gray - Write-Host " gh pr create --title ""Configure repository from template"" --base main" -ForegroundColor Gray - Write-Host "" - } - - # Next steps - Write-Host "✅ Next Steps:" -ForegroundColor Cyan - Write-Host "" - Write-Host "1. Configure branch protection (see REPO-INSTRUCTIONS.md if kept)" -ForegroundColor Yellow - Write-Host "" - Write-Host "2. Start developing!" -ForegroundColor Yellow - if ($solutionName) { - Write-Host " # Solution file created: $solutionName.slnx" -ForegroundColor Gray - Write-Host " # Add your projects to src/ and tests/" -ForegroundColor Gray - } - else { - Write-Host " dotnet new sln -n $projectName" -ForegroundColor Gray - Write-Host " # Add your projects to src/ and tests/" -ForegroundColor Gray - } - Write-Host "" - - Write-Info "Your repository is now configured and ready for development!" - Write-Host "" -} - -# Run setup -try { - Start-Setup -} -catch { - Write-Error "Setup failed: $_" - Write-Host $_.ScriptStackTrace -ForegroundColor Red - exit 1 -} diff --git a/src/Wolfgang.Etl.Abstractions/LoaderBase.cs b/src/Wolfgang.Etl.Abstractions/LoaderBase.cs index 1d38dd12..23a61df3 100644 --- a/src/Wolfgang.Etl.Abstractions/LoaderBase.cs +++ b/src/Wolfgang.Etl.Abstractions/LoaderBase.cs @@ -77,7 +77,7 @@ public int ReportingInterval /// The specified value is less than 1. /// /// - /// foreach (var item in items.Skip(SkipItemCount).Take(MaxItemCount)) + /// foreach (var item in items.Skip(SkipItemCount).Take(MaximumItemCount)) /// { /// // Process the item /// } @@ -112,7 +112,7 @@ public int MaximumItemCount /// The specified value is less than 0. /// /// - /// foreach (var item in items.Skip(SkipItemCount).Take(MaxItemCount)) + /// foreach (var item in items.Skip(SkipItemCount).Take(MaximumItemCount)) /// { /// // Process the item /// } diff --git a/src/Wolfgang.Etl.Abstractions/PublicAPI.Shipped.txt b/src/Wolfgang.Etl.Abstractions/PublicAPI.Shipped.txt new file mode 100644 index 00000000..ae760802 --- /dev/null +++ b/src/Wolfgang.Etl.Abstractions/PublicAPI.Shipped.txt @@ -0,0 +1,120 @@ +#nullable enable +Wolfgang.Etl.Abstractions.ExtractorBase +Wolfgang.Etl.Abstractions.ExtractorBase.CurrentItemCount.get -> int +Wolfgang.Etl.Abstractions.ExtractorBase.CurrentSkippedItemCount.get -> int +Wolfgang.Etl.Abstractions.ExtractorBase.IncrementCurrentItemCount() -> void +Wolfgang.Etl.Abstractions.ExtractorBase.IncrementCurrentSkippedItemCount() -> void +Wolfgang.Etl.Abstractions.ExtractorBase.MaximumItemCount.get -> int +Wolfgang.Etl.Abstractions.ExtractorBase.MaximumItemCount.set -> void +Wolfgang.Etl.Abstractions.ExtractorBase.ReportingInterval.get -> int +Wolfgang.Etl.Abstractions.ExtractorBase.ReportingInterval.set -> void +Wolfgang.Etl.Abstractions.ExtractorBase.SkipItemCount.get -> int +Wolfgang.Etl.Abstractions.ExtractorBase.SkipItemCount.set -> void +Wolfgang.Etl.Abstractions.IExtractAsync +Wolfgang.Etl.Abstractions.IExtractAsync.ExtractAsync() -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.IExtractStage +Wolfgang.Etl.Abstractions.IExtractStage.Load(Wolfgang.Etl.Abstractions.ILoadAsync loader) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.IExtractStage.Load(Wolfgang.Etl.Abstractions.ILoadWithCancellationAsync loader) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.IExtractStage.Load(Wolfgang.Etl.Abstractions.ILoadWithProgressAndCancellationAsync loader) -> Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress +Wolfgang.Etl.Abstractions.IExtractStage.Load(Wolfgang.Etl.Abstractions.ILoadWithProgressAsync loader) -> Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress +Wolfgang.Etl.Abstractions.IExtractStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithProgressAndCancellationAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStageWithProgress +Wolfgang.Etl.Abstractions.IExtractStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithProgressAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStageWithProgress +Wolfgang.Etl.Abstractions.IExtractStage.Transform(Wolfgang.Etl.Abstractions.ITransformAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.IExtractStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithCancellationAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.IExtractStageWithProgress +Wolfgang.Etl.Abstractions.IExtractStageWithProgress.WithProgress(System.IProgress progress) -> Wolfgang.Etl.Abstractions.IExtractStage +Wolfgang.Etl.Abstractions.IExtractWithCancellationAsync +Wolfgang.Etl.Abstractions.IExtractWithCancellationAsync.ExtractAsync(System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.IExtractWithProgressAndCancellationAsync +Wolfgang.Etl.Abstractions.IExtractWithProgressAndCancellationAsync.ExtractAsync(System.IProgress progress, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.IExtractWithProgressAsync +Wolfgang.Etl.Abstractions.IExtractWithProgressAsync.ExtractAsync(System.IProgress progress) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.ILoadAsync +Wolfgang.Etl.Abstractions.ILoadAsync.LoadAsync(System.Collections.Generic.IAsyncEnumerable items) -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.ILoadWithCancellationAsync +Wolfgang.Etl.Abstractions.ILoadWithCancellationAsync.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.ILoadWithProgressAndCancellationAsync +Wolfgang.Etl.Abstractions.ILoadWithProgressAndCancellationAsync.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.ILoadWithProgressAsync +Wolfgang.Etl.Abstractions.ILoadWithProgressAsync.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress) -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.IPipeline.Name.get -> string? +Wolfgang.Etl.Abstractions.IPipeline.RunAsync() -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.IPipeline.RunAsync(System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +Wolfgang.Etl.Abstractions.IPipeline.WithName(string name) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress +Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress.WithProgress(System.IProgress progress) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.IProgressTimer +Wolfgang.Etl.Abstractions.IProgressTimer.Elapsed -> System.Action? +Wolfgang.Etl.Abstractions.IProgressTimer.Start(int intervalMilliseconds) -> void +Wolfgang.Etl.Abstractions.IProgressTimer.StopTimer() -> void +Wolfgang.Etl.Abstractions.ITransformAsync +Wolfgang.Etl.Abstractions.ITransformAsync.TransformAsync(System.Collections.Generic.IAsyncEnumerable items) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.ITransformStage.Load(Wolfgang.Etl.Abstractions.ILoadAsync loader) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.ITransformStage.Load(Wolfgang.Etl.Abstractions.ILoadWithCancellationAsync loader) -> Wolfgang.Etl.Abstractions.IPipeline +Wolfgang.Etl.Abstractions.ITransformStage.Load(Wolfgang.Etl.Abstractions.ILoadWithProgressAndCancellationAsync loader) -> Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress +Wolfgang.Etl.Abstractions.ITransformStage.Load(Wolfgang.Etl.Abstractions.ILoadWithProgressAsync loader) -> Wolfgang.Etl.Abstractions.IPipelineWithLoadProgress +Wolfgang.Etl.Abstractions.ITransformStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithProgressAndCancellationAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStageWithProgress +Wolfgang.Etl.Abstractions.ITransformStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithProgressAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStageWithProgress +Wolfgang.Etl.Abstractions.ITransformStage.Transform(Wolfgang.Etl.Abstractions.ITransformAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.ITransformStage.Transform(Wolfgang.Etl.Abstractions.ITransformWithCancellationAsync transformer) -> Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.ITransformStageWithProgress +Wolfgang.Etl.Abstractions.ITransformStageWithProgress.WithProgress(System.IProgress progress) -> Wolfgang.Etl.Abstractions.ITransformStage +Wolfgang.Etl.Abstractions.ITransformWithCancellationAsync +Wolfgang.Etl.Abstractions.ITransformWithCancellationAsync.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.ITransformWithProgressAndCancellationAsync +Wolfgang.Etl.Abstractions.ITransformWithProgressAndCancellationAsync.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.ITransformWithProgressAsync +Wolfgang.Etl.Abstractions.ITransformWithProgressAsync.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress) -> System.Collections.Generic.IAsyncEnumerable +Wolfgang.Etl.Abstractions.LoaderBase +Wolfgang.Etl.Abstractions.LoaderBase.CurrentItemCount.get -> int +Wolfgang.Etl.Abstractions.LoaderBase.CurrentSkippedItemCount.get -> int +Wolfgang.Etl.Abstractions.LoaderBase.IncrementCurrentItemCount() -> void +Wolfgang.Etl.Abstractions.LoaderBase.IncrementCurrentSkippedItemCount() -> void +Wolfgang.Etl.Abstractions.LoaderBase.MaximumItemCount.get -> int +Wolfgang.Etl.Abstractions.LoaderBase.MaximumItemCount.set -> void +Wolfgang.Etl.Abstractions.LoaderBase.ReportingInterval.get -> int +Wolfgang.Etl.Abstractions.LoaderBase.ReportingInterval.set -> void +Wolfgang.Etl.Abstractions.LoaderBase.SkipItemCount.get -> int +Wolfgang.Etl.Abstractions.LoaderBase.SkipItemCount.set -> void +Wolfgang.Etl.Abstractions.Pipeline +Wolfgang.Etl.Abstractions.Report +Wolfgang.Etl.Abstractions.Report.CurrentItemCount.get -> int +Wolfgang.Etl.Abstractions.Report.Report(int currentItemCount) -> void +Wolfgang.Etl.Abstractions.TransformerBase +Wolfgang.Etl.Abstractions.TransformerBase.CurrentItemCount.get -> int +Wolfgang.Etl.Abstractions.TransformerBase.CurrentSkippedItemCount.get -> int +Wolfgang.Etl.Abstractions.TransformerBase.IncrementCurrentItemCount() -> void +Wolfgang.Etl.Abstractions.TransformerBase.IncrementCurrentSkippedItemCount() -> void +Wolfgang.Etl.Abstractions.TransformerBase.MaximumItemCount.get -> int +Wolfgang.Etl.Abstractions.TransformerBase.MaximumItemCount.set -> void +Wolfgang.Etl.Abstractions.TransformerBase.ReportingInterval.get -> int +Wolfgang.Etl.Abstractions.TransformerBase.ReportingInterval.set -> void +Wolfgang.Etl.Abstractions.TransformerBase.SkipItemCount.get -> int +Wolfgang.Etl.Abstractions.TransformerBase.SkipItemCount.set -> void +abstract Wolfgang.Etl.Abstractions.ExtractorBase.CreateProgressReport() -> TProgress +abstract Wolfgang.Etl.Abstractions.ExtractorBase.ExtractWorkerAsync(System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +abstract Wolfgang.Etl.Abstractions.LoaderBase.CreateProgressReport() -> TProgress +abstract Wolfgang.Etl.Abstractions.LoaderBase.LoadWorkerAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +abstract Wolfgang.Etl.Abstractions.TransformerBase.CreateProgressReport() -> TProgress +abstract Wolfgang.Etl.Abstractions.TransformerBase.TransformWorkerAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +static Wolfgang.Etl.Abstractions.Pipeline.Extract(Wolfgang.Etl.Abstractions.IExtractWithProgressAndCancellationAsync extractor) -> Wolfgang.Etl.Abstractions.IExtractStageWithProgress +static Wolfgang.Etl.Abstractions.Pipeline.Extract(Wolfgang.Etl.Abstractions.IExtractWithProgressAsync extractor) -> Wolfgang.Etl.Abstractions.IExtractStageWithProgress +static Wolfgang.Etl.Abstractions.Pipeline.Extract(Wolfgang.Etl.Abstractions.IExtractAsync extractor) -> Wolfgang.Etl.Abstractions.IExtractStage +static Wolfgang.Etl.Abstractions.Pipeline.Extract(Wolfgang.Etl.Abstractions.IExtractWithCancellationAsync extractor) -> Wolfgang.Etl.Abstractions.IExtractStage +virtual Wolfgang.Etl.Abstractions.ExtractorBase.CreateProgressTimer(System.IProgress progress) -> Wolfgang.Etl.Abstractions.IProgressTimer +virtual Wolfgang.Etl.Abstractions.ExtractorBase.ExtractAsync() -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.ExtractorBase.ExtractAsync(System.IProgress progress) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.ExtractorBase.ExtractAsync(System.IProgress progress, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.ExtractorBase.ExtractAsync(System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.LoaderBase.CreateProgressTimer(System.IProgress progress) -> Wolfgang.Etl.Abstractions.IProgressTimer +virtual Wolfgang.Etl.Abstractions.LoaderBase.LoadAsync(System.Collections.Generic.IAsyncEnumerable items) -> System.Threading.Tasks.Task +virtual Wolfgang.Etl.Abstractions.LoaderBase.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress) -> System.Threading.Tasks.Task +virtual Wolfgang.Etl.Abstractions.LoaderBase.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +virtual Wolfgang.Etl.Abstractions.LoaderBase.LoadAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task +virtual Wolfgang.Etl.Abstractions.TransformerBase.CreateProgressTimer(System.IProgress progress) -> Wolfgang.Etl.Abstractions.IProgressTimer +virtual Wolfgang.Etl.Abstractions.TransformerBase.TransformAsync(System.Collections.Generic.IAsyncEnumerable items) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.TransformerBase.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.TransformerBase.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.IProgress progress, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable +virtual Wolfgang.Etl.Abstractions.TransformerBase.TransformAsync(System.Collections.Generic.IAsyncEnumerable items, System.Threading.CancellationToken token) -> System.Collections.Generic.IAsyncEnumerable diff --git a/src/Wolfgang.Etl.Abstractions/PublicAPI.Unshipped.txt b/src/Wolfgang.Etl.Abstractions/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/Wolfgang.Etl.Abstractions/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Wolfgang.Etl.Abstractions/SystemProgressTimer.cs b/src/Wolfgang.Etl.Abstractions/SystemProgressTimer.cs index 0ec2307f..b455eff5 100644 --- a/src/Wolfgang.Etl.Abstractions/SystemProgressTimer.cs +++ b/src/Wolfgang.Etl.Abstractions/SystemProgressTimer.cs @@ -15,8 +15,9 @@ namespace Wolfgang.Etl.Abstractions; /// ExtractorBase.CreateProgressTimer, /// TransformerBase.CreateProgressTimer, and /// LoaderBase.CreateProgressTimer. -/// In unit tests, override CreateProgressTimer to return a -/// ManualProgressTimer instead. +/// In unit tests, override CreateProgressTimer to return a custom +/// implementation (for example, a manually +/// controlled fake whose ticks the test fires on demand). /// internal sealed class SystemProgressTimer : IProgressTimer { diff --git a/src/Wolfgang.Etl.Abstractions/TransformerBase.cs b/src/Wolfgang.Etl.Abstractions/TransformerBase.cs index 5802b03d..fa7826dd 100644 --- a/src/Wolfgang.Etl.Abstractions/TransformerBase.cs +++ b/src/Wolfgang.Etl.Abstractions/TransformerBase.cs @@ -79,7 +79,7 @@ public int ReportingInterval /// The specified value is less than 1. /// /// - /// foreach (var item in items.Skip(SkipItemCount).Take(MaxItemCount)) + /// foreach (var item in items.Skip(SkipItemCount).Take(MaximumItemCount)) /// { /// // Transform each item and return it /// } @@ -115,7 +115,7 @@ public int MaximumItemCount /// The specified value is less than 0. /// /// - /// foreach (var item in items.Skip(SkipItemCount).Take(MaxItemCount)) + /// foreach (var item in items.Skip(SkipItemCount).Take(MaximumItemCount)) /// { /// // Transform each item and return it /// } diff --git a/src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj b/src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj index 74f956b0..a297e0a7 100644 --- a/src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj +++ b/src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj @@ -2,17 +2,20 @@ net462;net472;net48;net481;netstandard2.0;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 latest - enable - 0.13.0 + 0.13.1 + + 1.0.0.0 + $([System.Text.RegularExpressions.Regex]::Replace("$(Version)", "[-+].*$", "")).0 False $(AssemblyName) - Chris Wolfgang Contains interfaces and base classes used to build ETL applications - Copyright 2025 Chris Wolfgang https://github.com/Chris-Wolfgang/ETL-Abstractions README.md https://github.com/Chris-Wolfgang/ETL-Abstractions - 1.0.0 MIT True ETL-Abstractions.png diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props deleted file mode 100644 index a53b408b..00000000 --- a/tests/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - false - - diff --git a/tests/Wolfgang.Etl.Abstractions.Tests.Unit/Wolfgang.Etl.Abstractions.Tests.Unit.csproj b/tests/Wolfgang.Etl.Abstractions.Tests.Unit/Wolfgang.Etl.Abstractions.Tests.Unit.csproj index b3e112c4..88c03cf7 100644 --- a/tests/Wolfgang.Etl.Abstractions.Tests.Unit/Wolfgang.Etl.Abstractions.Tests.Unit.csproj +++ b/tests/Wolfgang.Etl.Abstractions.Tests.Unit/Wolfgang.Etl.Abstractions.Tests.Unit.csproj @@ -4,7 +4,6 @@ 0.13.0 latest enable - enable false true Copyright 2025 Chris Wolfgang