diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..976cd9f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,49 @@ +name: Bug report +description: Something does not work as documented. +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for the report. Security issues must not be filed here — use the repository's **Security** tab → **Report a vulnerability**. + - type: input + id: package + attributes: + label: Package and version + placeholder: DragoAnt.Example 1.2.3 + validations: + required: true + - type: input + id: runtime + attributes: + label: Target framework and OS + placeholder: net9.0 on Ubuntu 24.04 + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal reproduction + description: The smallest code, input and configuration that shows the problem. + render: csharp + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected result + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual result + description: Include the full exception and stack trace, if any. + validations: + required: true + - type: textarea + id: notes + attributes: + label: Anything else + description: Workarounds you found, the last version that worked, links. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ffd677f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Report a security vulnerability + url: https://github.com/DragoAnt/.github/blob/main/SECURITY.md + about: Report vulnerabilities privately through the repository's Security tab, never in a public issue. + - name: Getting help + url: https://github.com/DragoAnt/.github/blob/main/SUPPORT.md + about: Where to ask questions and what to include. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..0bea6aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,30 @@ +name: Feature request +description: Suggest a new capability or an improvement. +labels: + - enhancement +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to do, and what gets in the way today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: The API or behavior you have in mind. A code sample of the intended usage helps most. + render: csharp + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: checkboxes + id: contribute + attributes: + label: Contribution + options: + - label: I am willing to open a pull request for this. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2592424 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: + - actions/* + - NuGet/* + - rhysd/* diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5a33d15 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## What and why + + + +## How it was tested + + + +## Checklist + +- [ ] Tests cover the change (a regression test for a bug fix). +- [ ] The README / package README is updated if usage changed. +- [ ] No breaking change, or the breaking change is described above. diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..4acaba5 --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,31 @@ +name: actionlint + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: actionlint-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + actionlint: + name: Lint workflows + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: actionlint + uses: docker://rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 # 1.7.12 + with: + args: -color .github/workflows/actionlint.yml .github/workflows/dotnet-build.yml workflow-templates/dotnet-ci.yml workflow-templates/dotnet-release.yml diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml new file mode 100644 index 0000000..9560c11 --- /dev/null +++ b/.github/workflows/dotnet-build.yml @@ -0,0 +1,289 @@ +name: dotnet-build + +on: + workflow_call: + inputs: + solution: + description: Solution or project file to build. Empty picks the single *.slnx, then *.sln, at the repository root. + type: string + default: '' + dotnet-version: + description: Extra SDKs/runtimes to install (one per line), on top of the SDK pinned in global-json-file. + type: string + default: | + 8.0.x + 9.0.x + global-json-file: + description: global.json that pins the SDK. Ignored when the file does not exist. + type: string + default: global.json + configuration: + description: Build configuration. + type: string + default: Release + version: + description: Package version passed as -p:Version. Empty leaves versioning to the repository's own MSBuild logic. + type: string + default: '' + test-arguments: + description: Extra arguments for dotnet test (Microsoft.Testing.Platform). The default fits xUnit v3; JUnit files feed the job summary, Cobertura files the coverage report. + type: string + default: --report-xunit-trx --report-xunit-junit --coverage --coverage-output-format cobertura + coverage-threshold: + description: Minimum merged line coverage in percent; 0 only reports. + type: number + default: 0 + coverage-assembly-filters: + description: ReportGenerator -assemblyfilters value. + type: string + default: -*.Tests;-*.Tests.*;-*.Benchmarks + reportgenerator-version: + description: dotnet-reportgenerator-globaltool version. + type: string + default: 5.5.11 + pack: + description: Run dotnet pack and upload the packages artifact. + type: boolean + default: false + runs-on: + description: Runner label. + type: string + default: ubuntu-24.04 + timeout-minutes: + description: Job timeout. + type: number + default: 30 + outputs: + packages-artifact: + description: Name of the uploaded packages artifact (set when pack is true). + value: ${{ jobs.build.outputs.packages-artifact }} + +permissions: + contents: read + +jobs: + build: + name: Build and test + runs-on: ${{ inputs.runs-on }} + timeout-minutes: ${{ inputs.timeout-minutes }} + outputs: + packages-artifact: ${{ steps.pack.outputs.artifact }} + env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + CONFIGURATION: ${{ inputs.configuration }} + VERSION: ${{ inputs.version }} + RESULTS_DIR: ${{ github.workspace }}/TestResults + COVERAGE_DIR: ${{ github.workspace }}/coverage + PACKAGES_DIR: ${{ github.workspace }}/packages + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + + - name: Resolve inputs + id: resolve + env: + SOLUTION: ${{ inputs.solution }} + GLOBAL_JSON: ${{ inputs.global-json-file }} + run: | + if [ -z "$SOLUTION" ]; then + for pattern in '*.slnx' '*.sln'; do + mapfile -t found < <(find . -maxdepth 1 -name "$pattern" -printf '%f\n' | sort) + if [ "${#found[@]}" -gt 1 ]; then + echo "::error::several $pattern files at the repository root (${found[*]}); set the 'solution' input" + exit 1 + fi + if [ "${#found[@]}" -eq 1 ]; then SOLUTION="${found[0]}"; break; fi + done + fi + if [ -n "$SOLUTION" ] && [ ! -f "$SOLUTION" ]; then + echo "::error::solution '$SOLUTION' not found" + exit 1 + fi + echo "solution=$SOLUTION" >> "$GITHUB_OUTPUT" + if [ -n "$GLOBAL_JSON" ] && [ -f "$GLOBAL_JSON" ]; then + echo "global-json=$GLOBAL_JSON" >> "$GITHUB_OUTPUT" + fi + echo "Solution: ${SOLUTION:-}" + + - name: Install .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ inputs.dotnet-version }} + global-json-file: ${{ steps.resolve.outputs.global-json }} + + - name: NuGet cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.NUGET_PACKAGES }} + key: nuget-${{ runner.os }}-${{ hashFiles('global.json', '**/Directory.Packages.props', '**/*.csproj', '**/packages.lock.json') }} + restore-keys: | + nuget-${{ runner.os }}- + + - name: Build properties + env: + SOLUTION: ${{ steps.resolve.outputs.solution }} + run: | + props="-p:ContinuousIntegrationBuild=true" + if [ -n "$VERSION" ]; then props="$props -p:Version=$VERSION"; fi + { + echo "BUILD_PROPS=$props" + echo "SOLUTION=$SOLUTION" + } >> "$GITHUB_ENV" + + - name: Restore + run: | + # shellcheck disable=SC2086 + dotnet restore ${SOLUTION:+"$SOLUTION"} $BUILD_PROPS + + - name: Build + run: | + # shellcheck disable=SC2086 + dotnet build ${SOLUTION:+"$SOLUTION"} -c "$CONFIGURATION" --no-restore $BUILD_PROPS + + - name: Test + env: + TEST_ARGUMENTS: ${{ inputs.test-arguments }} + run: | + # shellcheck disable=SC2086 + dotnet test ${SOLUTION:+--solution "$SOLUTION"} -c "$CONFIGURATION" --no-build --no-progress \ + --results-directory "$RESULTS_DIR" $TEST_ARGUMENTS + + - name: Test summary + if: ${{ !cancelled() }} + shell: python + run: | + import glob, os, re + import xml.etree.ElementTree as ET + + results = os.environ["RESULTS_DIR"] + rows, failures = [], [] + for path in sorted(glob.glob(os.path.join(results, "**", "*.junit.xml"), recursive=True)): + for suite in ET.parse(path).getroot().iter("testsuite"): + name = suite.get("name", "") + parts = re.split(r"[\\/]", name) + tfm = next((p for p in reversed(parts[:-1]) if re.fullmatch(r"net[\d.]+.*", p)), "") + assembly = re.sub(r"\.(dll|exe)$", "", parts[-1]) + total = int(suite.get("tests", 0)) + failed = int(suite.get("failures", 0)) + int(suite.get("errors", 0)) + skipped = int(suite.get("skipped", 0) or suite.get("disabled", 0)) + rows.append((assembly, tfm, total - failed - skipped, failed, skipped, float(suite.get("time", 0)))) + for case in suite.iter("testcase"): + problem = case.find("failure") + if problem is None: + problem = case.find("error") + if problem is not None: + message = (problem.get("message") or problem.text or "").strip().splitlines() + failures.append((assembly, tfm, case.get("name", ""), message[0][:300] if message else "")) + + lines = ["## Test results", ""] + if not rows: + lines.append("No JUnit results found — add a JUnit report option to `test-arguments` to get this table.") + else: + passed, failed, skipped = (sum(r[i] for r in rows) for i in (2, 3, 4)) + icon = ":x:" if failed else ":white_check_mark:" + lines += [f"{icon} **{passed} passed**, **{failed} failed**, {skipped} skipped", "", + "| Assembly | TFM | Passed | Failed | Skipped | Time |", + "| --- | --- | ---: | ---: | ---: | ---: |"] + lines += [f"| {a} | {t} | {p} | {f} | {s} | {d:.1f}s |" for a, t, p, f, s, d in rows] + if failures: + lines += ["", "### Failed tests", "", "| Test | TFM | Message |", "| --- | --- | --- |"] + for a, t, n, m in failures[:50]: + cell = m.replace("|", r"\|") + lines.append(f"| `{n}` | {t} | {cell} |") + if len(failures) > 50: + lines.append(f"| … {len(failures) - 50} more | | |") + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary: + summary.write("\n".join(lines) + "\n\n") + + - name: Coverage report + id: coverage + if: ${{ !cancelled() && hashFiles('TestResults/**/*.cobertura.xml') != '' }} + env: + RG_VERSION: ${{ inputs.reportgenerator-version }} + ASSEMBLY_FILTERS: ${{ inputs.coverage-assembly-filters }} + run: | + dotnet tool install dotnet-reportgenerator-globaltool --version "$RG_VERSION" --tool-path "$RUNNER_TEMP/reportgenerator" + "$RUNNER_TEMP/reportgenerator/reportgenerator" \ + "-reports:$RESULTS_DIR/**/*.cobertura.xml" \ + "-targetdir:$COVERAGE_DIR" \ + "-reporttypes:MarkdownSummaryGithub;Cobertura;HtmlInline" \ + "-assemblyfilters:$ASSEMBLY_FILTERS" + cat "$COVERAGE_DIR/SummaryGithub.md" >> "$GITHUB_STEP_SUMMARY" + rate=$(grep -oE ']*line-rate="[0-9.]+"' "$COVERAGE_DIR/Cobertura.xml" | grep -oE 'line-rate="[0-9.]+"' | grep -oE '[0-9.]+') + echo "line-rate=$rate" >> "$GITHUB_OUTPUT" + + - name: Coverage threshold + if: ${{ !cancelled() && inputs.coverage-threshold > 0 }} + env: + THRESHOLD: ${{ inputs.coverage-threshold }} + LINE_RATE: ${{ steps.coverage.outputs.line-rate }} + run: | + if [ -z "$LINE_RATE" ]; then + echo "::error::coverage-threshold is $THRESHOLD% but no coverage was collected" + exit 1 + fi + awk -v rate="$LINE_RATE" -v min="$THRESHOLD" 'BEGIN { + pct = rate * 100 + printf "Line coverage %.1f%% (minimum %s%%)\n", pct, min + if (pct + 1e-9 < min) { printf "::error::line coverage %.1f%% is below the %s%% threshold\n", pct, min; exit 1 } + }' + + - name: Upload test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-results + path: ${{ env.RESULTS_DIR }} + if-no-files-found: ignore + retention-days: 14 + + - name: Upload coverage + if: ${{ !cancelled() && steps.coverage.outcome == 'success' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage + path: ${{ env.COVERAGE_DIR }} + retention-days: 14 + + - name: Pack + id: pack + if: ${{ inputs.pack }} + run: | + # shellcheck disable=SC2086 + dotnet pack ${SOLUTION:+"$SOLUTION"} -c "$CONFIGURATION" --no-build -o "$PACKAGES_DIR" $BUILD_PROPS + mapfile -t packages < <(find "$PACKAGES_DIR" -maxdepth 1 -name '*.nupkg' -printf '%f\n' | sort) + if [ "${#packages[@]}" -eq 0 ]; then + echo "::error::dotnet pack produced no packages" + exit 1 + fi + { + echo "## Packages" + echo + for package in "${packages[@]}"; do echo "- \`$package\`"; done + } >> "$GITHUB_STEP_SUMMARY" + if [ -n "$VERSION" ]; then + for package in "${packages[@]}"; do + case "$package" in + *."$VERSION".nupkg) ;; + *) echo "::error::$package does not carry version $VERSION"; exit 1 ;; + esac + done + fi + echo "artifact=packages" >> "$GITHUB_OUTPUT" + + - name: Upload packages + if: ${{ inputs.pack }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: packages + path: | + ${{ env.PACKAGES_DIR }}/*.nupkg + ${{ env.PACKAGES_DIR }}/*.snupkg + if-no-files-found: error + retention-days: 30 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b5114ba --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement through a private report to the maintainers through any DragoAnt repository's **Security** tab → **Report a vulnerability** form (start the title with `Code of conduct:`), or by [reporting the content to GitHub](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam). All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2996a0b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing to DragoAnt + +Thank you for helping. This guide applies to every DragoAnt repository that does not ship its own `CONTRIBUTING.md`; a repository's own guide wins where the two differ. + +## Before you start + +- **Small fixes** (typos, docs, an obvious bug) — open a pull request directly. +- **Anything larger** — open an issue first so the approach can be agreed before you write the code. +- **Security issues** — report them privately, see [SECURITY.md](./SECURITY.md). + +## Build and test + +Most repositories import shared MSBuild files from a git submodule, so clone with submodules: + +```sh +git clone --recurse-submodules https://github.com/DragoAnt/.git +``` + +Install the .NET SDK pinned in the repository's `global.json`, plus the runtimes of every target framework the projects list, then run the same steps as CI from the repository root: + +```sh +dotnet restore +dotnet build -c Release --no-restore +dotnet test -c Release --no-build +``` + +## Pull requests + +- Branch from the default branch and target it. +- Add or update tests for every behavior change, and a regression test for every bug fix. +- Keep the build warning-free. +- Update the README (and the package README, if the repository has one) when usage changes. +- Keep a pull request to one topic; unrelated clean-ups go in their own pull request. +- The `ci` workflow must pass. + +## Releases + +Maintainers publish packages to nuget.org by creating a GitHub release whose tag is the package version (`v1.2.3` or `v1.2.3-beta.1`). + +## Code of conduct + +Everyone taking part follows the [Code of Conduct](./CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md new file mode 100644 index 0000000..efd7010 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# DragoAnt/.github + +Organization defaults for every DragoAnt repository. + +| Path | What it does | +| --- | --- | +| [profile/README.md](./profile/README.md) | The organization's front page on GitHub. | +| [CONTRIBUTING.md](./CONTRIBUTING.md), [SECURITY.md](./SECURITY.md), [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), [SUPPORT.md](./SUPPORT.md) | Default community health files, shown in any repository that has none of its own. | +| [.github/ISSUE_TEMPLATE](./.github/ISSUE_TEMPLATE), [.github/pull_request_template.md](./.github/pull_request_template.md) | Default issue forms and pull request template. | +| [.github/workflows/dotnet-build.yml](./.github/workflows/dotnet-build.yml) | Reusable workflow: restore, build, test, coverage, pack. | +| [workflow-templates](./workflow-templates) | `ci` and `release` starters on each repository's **Actions → New workflow** page. | + +## Reusable workflow `dotnet-build.yml` + +Restores, builds and tests a .NET solution on Microsoft.Testing.Platform, writes a test table and a coverage summary to the job summary, and uploads the `test-results`, `coverage` and (with `pack: true`) `packages` artifacts. It needs only `contents: read`. + +```yaml +jobs: + build: + uses: DragoAnt/.github/.github/workflows/dotnet-build.yml@ # main + with: + version: 0.0.0-ci.${{ github.run_number }} + coverage-threshold: 70 +``` + +| Input | Default | Meaning | +| --- | --- | --- | +| `solution` | single `*.slnx`, else `*.sln`, at the root | Solution or project to build. | +| `dotnet-version` | `8.0.x` and `9.0.x` | Extra SDKs, one per line, on top of the SDK pinned in `global.json`. | +| `global-json-file` | `global.json` | Ignored when absent. | +| `configuration` | `Release` | Build configuration. | +| `version` | empty | Passed as `-p:Version`; empty leaves versioning to the repository. | +| `test-arguments` | xUnit v3 TRX + JUnit reports, Cobertura coverage | Extra `dotnet test` arguments. JUnit files feed the test table. | +| `coverage-threshold` | `0` | Minimum merged line coverage in percent; `0` only reports. | +| `coverage-assembly-filters` | `-*.Tests;-*.Tests.*;-*.Benchmarks` | ReportGenerator assembly filters. | +| `pack` | `false` | Pack and upload the `packages` artifact. | +| `runs-on` | `ubuntu-24.04` | Runner label. | +| `timeout-minutes` | `30` | Job timeout. | + +Pin the reference to a full commit SHA in each caller; Dependabot's `github-actions` updates keep it current. The workflow templates reference `@main` so a new repository starts from the latest version — pin it after adding. + +## Publishing to nuget.org + +The `release` template publishes when a GitHub release is published. One-time setup per repository: + +1. **Settings → Environments → New environment** `nuget`, with yourself as required reviewer and a `v*` tag deployment rule. +2. **nuget.org → Trusted Publishing → Add policy**: owner `DragoAnt`, the repository, workflow file `release.yml`, environment `nuget`. +3. Optional: a repository variable `NUGET_USER` when the nuget.org account that owns the policy is not `DragoAnt`. + +Then create a release whose tag is the version: `v1.2.3`, or `v1.2.3-beta.1` with **Set as a pre-release** ticked. + +## License + +[MIT](./LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c4cc9f7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security policy + +This policy applies to every DragoAnt repository that does not ship its own `SECURITY.md`. + +## Supported versions + +Fixes are released for the latest published version of each package. Older major versions get a fix only when the issue is severe and the upgrade path is not practical. + +## Reporting a vulnerability + +Report vulnerabilities **privately** through GitHub: open the affected repository's **Security** tab and choose **Report a vulnerability**. Please do not open a public issue, discussion or pull request for a vulnerability. + +Include: + +- the package name and version, and the target framework; +- a minimal reproduction (input, configuration, code); +- the impact you observed or expect. + +## What happens next + +- We acknowledge the report within 7 days. +- We confirm the issue, agree on a fix and a disclosure date with you, and keep you informed while we work on it. +- The fix ships as a new package version together with a published GitHub security advisory. You are credited unless you ask not to be. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..4f30b64 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,8 @@ +# Getting help + +- **Usage questions** — read the repository's README first, then search its issues. Still stuck? Open an issue with the **Question** label and a minimal example. +- **Bugs** — open an issue using the **Bug report** template: package version, target framework, a minimal reproduction, and the expected and actual results. +- **Feature ideas** — open an issue using the **Feature request** template and describe the problem before the solution. +- **Security issues** — never in a public issue; see [SECURITY.md](./SECURITY.md). + +All DragoAnt packages are maintained in spare time. There is no guaranteed response time, but every well-described issue is read. diff --git a/profile/README.md b/profile/README.md new file mode 100644 index 0000000..8a1da65 --- /dev/null +++ b/profile/README.md @@ -0,0 +1,23 @@ +# DragoAnt + +Small, focused .NET libraries for System.Text.Json, Entity Framework Core and dependency injection — MIT-licensed and published on [nuget.org](https://www.nuget.org/profiles/DragoAnt). + +## Packages + +| Repository | What it is | NuGet | +| --- | --- | --- | +| [Extensions.System.Text.Json](https://github.com/DragoAnt/Extensions.System.Text.Json) | Mask or extract JSON values by property-path rules in one streaming pass | [![NuGet](https://img.shields.io/nuget/v/DragoAnt.System.Text.Json.Observer?label=DragoAnt.System.Text.Json.Observer)](https://www.nuget.org/packages/DragoAnt.System.Text.Json.Observer) | +| [Extensions.EntityFrameworkCore](https://github.com/DragoAnt/Extensions.EntityFrameworkCore) | Conventions, static and historical migrations, entity definitions for EF Core | [![NuGet](https://img.shields.io/nuget/v/DragoAnt.EntityFrameworkCore?label=DragoAnt.EntityFrameworkCore)](https://www.nuget.org/packages/DragoAnt.EntityFrameworkCore) | +| [Extensions.DependencyInjection](https://github.com/DragoAnt/Extensions.DependencyInjection) | Extensions for `Microsoft.Extensions.DependencyInjection` | [![NuGet](https://img.shields.io/nuget/v/DragoAnt.Extensions.DependencyInjection?label=DragoAnt.Extensions.DependencyInjection)](https://www.nuget.org/packages/DragoAnt.Extensions.DependencyInjection) | +| [Shared](https://github.com/DragoAnt/Shared) | Common helpers, ASP.NET Core, CSV and Mermaid utilities | [![NuGet](https://img.shields.io/nuget/v/DragoAnt.Shared?label=DragoAnt.Shared)](https://www.nuget.org/packages/DragoAnt.Shared) | +| [Extensions.T4](https://github.com/DragoAnt/Extensions.T4) | Utilities for generating code with T4 templates | [![NuGet](https://img.shields.io/nuget/v/DragoAnt.Extensions.T4?label=DragoAnt.Extensions.T4)](https://www.nuget.org/packages/DragoAnt.Extensions.T4) | + +## Build tooling + +- [MSBuildKit](https://github.com/DragoAnt/MSBuildKit) — build defaults, nuget.org-ready packaging, versioning from release tags, tests and coverage for .NET repositories. +- [MSBuild.Routine](https://github.com/DragoAnt/MSBuild.Routine) — the shared MSBuild files the DragoAnt repositories import as a submodule. +- [MonoRepo](https://github.com/DragoAnt/MonoRepo) — swaps `PackageReference` for `ProjectReference` to develop across several repositories at once. + +## Contributing + +Issues and pull requests are welcome — see the [contributing guide](https://github.com/DragoAnt/.github/blob/main/CONTRIBUTING.md). Report vulnerabilities privately as described in the [security policy](https://github.com/DragoAnt/.github/blob/main/SECURITY.md). diff --git a/workflow-templates/dotnet-ci.properties.json b/workflow-templates/dotnet-ci.properties.json new file mode 100644 index 0000000..a9a954d --- /dev/null +++ b/workflow-templates/dotnet-ci.properties.json @@ -0,0 +1,14 @@ +{ + "name": "DragoAnt .NET CI", + "description": "Build and test a .NET solution on every push and pull request, with a test and coverage summary, using the DragoAnt reusable dotnet-build workflow.", + "iconName": "octicon check-circle", + "categories": [ + "Continuous integration", + "C#" + ], + "filePatterns": [ + "\\.slnx?$", + "\\.csproj$", + "global\\.json$" + ] +} diff --git a/workflow-templates/dotnet-ci.yml b/workflow-templates/dotnet-ci.yml new file mode 100644 index 0000000..ad95759 --- /dev/null +++ b/workflow-templates/dotnet-ci.yml @@ -0,0 +1,22 @@ +name: ci + +on: + push: + branches: + - $default-branch + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: Build and test + uses: DragoAnt/.github/.github/workflows/dotnet-build.yml@main + with: + version: 0.0.0-ci.${{ github.run_number }} diff --git a/workflow-templates/dotnet-release.properties.json b/workflow-templates/dotnet-release.properties.json new file mode 100644 index 0000000..a833f38 --- /dev/null +++ b/workflow-templates/dotnet-release.properties.json @@ -0,0 +1,13 @@ +{ + "name": "DragoAnt NuGet release", + "description": "Publish NuGet packages to nuget.org when a GitHub release is published: version from the tag, build, test and pack once, then push with nuget.org Trusted Publishing.", + "iconName": "octicon package", + "categories": [ + "Deployment", + "C#" + ], + "filePatterns": [ + "\\.slnx?$", + "\\.csproj$" + ] +} diff --git a/workflow-templates/dotnet-release.yml b/workflow-templates/dotnet-release.yml new file mode 100644 index 0000000..fa4e8e0 --- /dev/null +++ b/workflow-templates/dotnet-release.yml @@ -0,0 +1,98 @@ +name: release + +on: + release: + types: + - published + +permissions: + contents: read + +concurrency: + group: release-${{ github.event.release.tag_name }} + cancel-in-progress: false + +jobs: + version: + name: Version from tag + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Validate tag + id: version + env: + TAG: ${{ github.event.release.tag_name }} + PRERELEASE: ${{ github.event.release.prerelease }} + run: | + version="${TAG#[vV]}" + ident='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)' + semver="^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-$ident(\.$ident)*)?$" + if ! [[ "$version" =~ $semver ]]; then + echo "::error::release tag '$TAG' is not a SemVer version (vMAJOR.MINOR.PATCH[-prerelease], no build metadata)" + exit 1 + fi + if [[ "$version" == 9999.* ]]; then + echo "::error::9999.x is the local development version and is never published" + exit 1 + fi + if [[ "$PRERELEASE" == "true" && "$version" != *-* ]]; then + echo "::error::the release is marked pre-release but '$TAG' has no -suffix" + exit 1 + fi + if [[ "$PRERELEASE" != "true" && "$version" == *-* ]]; then + echo "::error::'$TAG' is a pre-release version; tick 'Set as a pre-release' on the release" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Package version: $version" + + build: + name: Build, test, pack + needs: version + uses: DragoAnt/.github/.github/workflows/dotnet-build.yml@main + with: + version: ${{ needs.version.outputs.version }} + pack: true + + publish: + name: Publish to nuget.org + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: + name: nuget + url: https://www.nuget.org/profiles/${{ vars.NUGET_USER || 'DragoAnt' }} + permissions: + id-token: write + steps: + - name: Download packages + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.build.outputs.packages-artifact }} + path: packages + + - name: NuGet login (Trusted Publishing) + id: login + continue-on-error: true + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 + with: + user: ${{ vars.NUGET_USER || 'DragoAnt' }} + + - name: Push + env: + OIDC_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + FALLBACK_KEY: ${{ secrets.NUGET_ORG_API_KEY }} + run: | + if [ -n "$OIDC_KEY" ]; then + key="$OIDC_KEY" + echo "Pushing with a short-lived nuget.org Trusted Publishing key." + elif [ -n "$FALLBACK_KEY" ]; then + key="$FALLBACK_KEY" + echo "::warning::nuget.org Trusted Publishing login failed, falling back to the NUGET_ORG_API_KEY secret. Add a Trusted Publishing policy on nuget.org (this repository, workflow file of this workflow, environment 'nuget') and delete the secret." + else + echo "::error::nuget.org Trusted Publishing login failed and no NUGET_ORG_API_KEY secret is available. Add a Trusted Publishing policy on nuget.org for this repository, this workflow file and environment 'nuget' (or set the NUGET_USER variable if the policy owner is not 'DragoAnt')." + exit 1 + fi + dotnet nuget push "packages/*.nupkg" --api-key "$key" --source https://api.nuget.org/v3/index.json --skip-duplicate