diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b96a6e6dc8..0e5ecf7bc5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,11 +21,13 @@ Stryker.NET is a mutation testing framework for .NET projects. It allows you to ## Contributing Workflow ### Code Standards + - Follow the repository's `.editorconfig` and [Microsoft C# coding guidelines](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions) - Create or edit unit tests or integration tests for all changes - Update documentation when adding features ### Pull Request Title Convention + When creating or updating pull requests, **always** use Angular-style conventional commit format for PR titles: - Format: `(): ` - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` @@ -38,13 +40,15 @@ When creating or updating pull requests, **always** use Angular-style convention **Why**: The project uses squash merging, so the PR title becomes the commit message in the main branch history. ### Running Tests -- **Unit tests**: Run `dotnet test` in the `/src` directory + +- **Unit tests**: Run `dotnet test` in the `/src` directory or use your IDE's test runner - **Integration tests**: - On **Windows**: Run `.\integration-tests.ps1` in the root of the repo (PowerShell 7 recommended) - On **macOS/Linux**: Run `pwsh ./integration-tests.ps1` in the root of the repo (requires [PowerShell 7](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell)) -- Always run unit tests and integration tests before committing changes +- Always run unit tests and integration after making a change to ensure nothing is broken ### Testing Locally + To test Stryker.NET on a project: 1. In `Stryker.CLI`, open `properties > Debug` 2. Create a new Debug profile @@ -55,31 +59,3 @@ To test Stryker.NET on a project: **Note**: Running Stryker on itself doesn't work as assemblies will be in use. To run Stryker on the stryker codebase, use the official nuget release via `dotnet tool install dotnet-stryker` and then `dotnet stryker`. -## Adding a New Mutator - -See the full guide in [adding_a_mutator.md](../adding_a_mutator.md). - -### Key Points for Mutators -1. **Purpose**: Generate mutations that look like possible human errors, not just any mutation -2. **Performance**: Keep mutators fast - they're called on every syntax element -3. **Buildable**: Generated mutations should compile in most situations -4. **Killable**: Avoid mutations that always raise exceptions or are semantically equivalent -5. **General**: Mutators should work for all projects, not be framework-specific - -### Implementation Steps -1. Create a class inheriting from `MutatorBase` and implementing `IMutator` -2. Specify the expected `SyntaxNode` class you can mutate (e.g., `StatementSyntax`) -3. Override the `MutationLevel` property (typically `Complete` or `Advanced`) -4. Override `ApplyMutation` to generate mutations -5. Add an entry in the `Mutator` enum -6. Create an instance in the `CsharpMutantOrchestrator` constructor -7. Add comprehensive unit tests -8. Update [docs/mutations.md](../docs/mutations.md) - -### Mutator Guidelines -- Use Roslyn APIs to work with syntax trees, not text transformations -- Each mutator is called on every syntax element recursively -- Return an empty list or `yield break` if no mutation can be generated -- Mutators must not throw exceptions -- Support various C# syntax versions (expression body vs block statement) -- Invest in unit tests early - look at existing mutator tests for examples diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..6c66042f41 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,172 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Override release version (e.g. 4.15.0). Leave empty to auto-detect from conventional commits.' + required: false + type: string + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install changelog tools + run: npm install -g conventional-changelog-cli@5 conventional-recommended-bump@6 semver@7 + + - name: Determine version + id: version + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + OLD_PREFIX=$(node -p "require('./package.json').versionPrefix") + OLD_SUFFIX=$(node -p "require('./package.json').versionSuffix") + + if [ -n "$INPUT_VERSION" ]; then + NEW_VERSION="$INPUT_VERSION" + else + BUMP_TYPE=$(conventional-recommended-bump -p angular -t "dotnet-stryker@") + NEW_VERSION=$(semver "$OLD_PREFIX" -i "$BUMP_TYPE") + fi + + NEW_PREFIX="${NEW_VERSION%%-*}" + [[ "$NEW_VERSION" == *-* ]] && NEW_SUFFIX="${NEW_VERSION#*-}" || NEW_SUFFIX="" + + echo "old_prefix=$OLD_PREFIX" >> "$GITHUB_OUTPUT" + echo "old_suffix=$OLD_SUFFIX" >> "$GITHUB_OUTPUT" + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "new_prefix=$NEW_PREFIX" >> "$GITHUB_OUTPUT" + echo "new_suffix=$NEW_SUFFIX" >> "$GITHUB_OUTPUT" + + - name: Generate changelog + run: conventional-changelog -p angular -t "dotnet-stryker@" > /tmp/release-notes.md + + - name: Update CHANGELOG.md + run: | + node << 'EOF' + const fs = require('fs'); + const releaseNotes = fs.readFileSync('/tmp/release-notes.md', 'utf8').trim(); + const changelog = fs.readFileSync('CHANGELOG.md', 'utf8'); + const marker = ''; + if (!changelog.includes(marker)) { + console.error('CHANGELOG.md is missing the marker'); + process.exit(1); + } + fs.writeFileSync('CHANGELOG.md', changelog.replace(marker, `${marker}\n\n${releaseNotes}`)); + EOF + + - name: Update csproj version numbers + env: + OLD_PREFIX: ${{ steps.version.outputs.old_prefix }} + NEW_PREFIX: ${{ steps.version.outputs.new_prefix }} + NEW_SUFFIX: ${{ steps.version.outputs.new_suffix }} + run: | + node << 'EOF' + const fs = require('fs'); + const { OLD_PREFIX, NEW_PREFIX, NEW_SUFFIX } = process.env; + const csprojFiles = [ + 'src/Stryker.Core/Stryker.Core/Stryker.Core.csproj', + 'src/Stryker.CLI/Stryker.CLI/Stryker.CLI.csproj', + ]; + for (const file of csprojFiles) { + let content = fs.readFileSync(file, 'utf8'); + content = content.replace( + `${OLD_PREFIX}`, + `${NEW_PREFIX}` + ); + content = content.replace( + /.*<\/VersionSuffix>/, + `${NEW_SUFFIX}` + ); + fs.writeFileSync(file, content); + } + EOF + + - name: Update azure-pipelines.yml + env: + OLD_PREFIX: ${{ steps.version.outputs.old_prefix }} + OLD_SUFFIX: ${{ steps.version.outputs.old_suffix }} + NEW_PREFIX: ${{ steps.version.outputs.new_prefix }} + run: | + node << 'EOF' + const fs = require('fs'); + const { OLD_PREFIX, OLD_SUFFIX, NEW_PREFIX } = process.env; + const oldVersion = OLD_SUFFIX ? `${OLD_PREFIX}-${OLD_SUFFIX}` : OLD_PREFIX; + let content = fs.readFileSync('azure-pipelines.yml', 'utf8'); + content = content.replace( + `VersionBuildNumber: $[counter('${oldVersion}', 1)]`, + `VersionBuildNumber: $[counter('${NEW_PREFIX}', 1)]` + ); + content = content.replace( + `PackageVersion: '${oldVersion}'`, + `PackageVersion: '${NEW_PREFIX}'` + ); + fs.writeFileSync('azure-pipelines.yml', content); + EOF + + - name: Update package.json + env: + OLD_PREFIX: ${{ steps.version.outputs.old_prefix }} + OLD_SUFFIX: ${{ steps.version.outputs.old_suffix }} + NEW_VERSION: ${{ steps.version.outputs.new_version }} + NEW_PREFIX: ${{ steps.version.outputs.new_prefix }} + NEW_SUFFIX: ${{ steps.version.outputs.new_suffix }} + run: | + node << 'EOF' + const fs = require('fs'); + const { OLD_PREFIX, OLD_SUFFIX, NEW_VERSION, NEW_PREFIX, NEW_SUFFIX } = process.env; + const oldVersion = OLD_SUFFIX ? `${OLD_PREFIX}-${OLD_SUFFIX}` : OLD_PREFIX; + let content = fs.readFileSync('package.json', 'utf8'); + content = content.replace(`"version": "${oldVersion}",`, `"version": "${NEW_VERSION}",`); + content = content.replace(`"versionPrefix": "${OLD_PREFIX}",`, `"versionPrefix": "${NEW_PREFIX}",`); + content = content.replace(`"versionSuffix": "${OLD_SUFFIX}",`, `"versionSuffix": "${NEW_SUFFIX}",`); + fs.writeFileSync('package.json', content); + EOF + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Commit changes + env: + NEW_VERSION: ${{ steps.version.outputs.new_version }} + run: | + git add . + git commit -m "Publish" -m "" -m "- dotnet-stryker@${NEW_VERSION}" + + - name: Create git tag + env: + NEW_VERSION: ${{ steps.version.outputs.new_version }} + run: git tag -a "dotnet-stryker@${NEW_VERSION}" -F /tmp/release-notes.md + + - name: Push changes and tags + run: git push --follow-tags + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_VERSION: ${{ steps.version.outputs.new_version }} + run: | + { + cat /tmp/release-notes.md + printf "\n---\n\n**NuGet:** https://www.nuget.org/packages/dotnet-stryker/%s\n" "${NEW_VERSION}" + } > /tmp/gh-release-notes.md + gh release create "dotnet-stryker@${NEW_VERSION}" \ + --title "dotnet-stryker@${NEW_VERSION}" \ + --notes-file /tmp/gh-release-notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 00aff7ae1f..449b09f022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,4 +15,4 @@ For historical release notes prior to the consolidated changelog, see the packag ### Bug Fixes * **baseline:** S3 Provider Cleanup ([#3491](https://github.com/stryker-mutator/stryker-net/issues/3491)) ([4a40c42](https://github.com/stryker-mutator/stryker-net/commit/4a40c42ded7ffdd343f991388ddc4da111ee9d04)) -* handle null Messages in TestRunAccumulator.Aggregate ([#3513](https://github.com/stryker-mutator/stryker-net/issues/3513)) ([58a89b7](https://github.com/stryker-mutator/stryker-net/commit/58a89b73236e53e3419bc91fa1f36402c69835c7)), closes [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) \ No newline at end of file +* handle null Messages in TestRunAccumulator.Aggregate ([#3513](https://github.com/stryker-mutator/stryker-net/issues/3513)) ([58a89b7](https://github.com/stryker-mutator/stryker-net/commit/58a89b73236e53e3419bc91fa1f36402c69835c7)), closes [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) [#3510](https://github.com/stryker-mutator/stryker-net/issues/3510) diff --git a/RELEASING.md b/RELEASING.md index 87dc37d89e..60bfb9ac74 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,8 +2,8 @@ Releasing a new version of the Stryker.NET packages can be done by following these steps: 1. Clone the repo or checkout (and pull) the master branch. 2. Look at the commits since the last release and determine the next version number. If we are on 0.11.0 and there are only bugfixes and refactorings: we go to 0.11.1. If there are also new features or breaking changes, we go to 0.12.0. -3. Run `npm run prepare-release` from the root of the repo and enter the new version number. -4. Verify that the commit is on GitHub and the releases in GitHub have been made. +3. Run `npm run prepare-release` from the root of the repo and enter the new version number. This will automatically create GitHub releases for the new tags with the correct release notes from the changelogs. +4. Verify that the commit is on GitHub and that the GitHub releases for the new version have been created. 5. Wait for build on master to complete on Azure Pipelines and then start the Production environment on Azure Pipelines. 6. Approve the Production environment on Azure Pipelines (this approval prevents accidental releases). diff --git a/prepare-release.js b/prepare-release.js index e470ac5b5a..7087a35084 100644 --- a/prepare-release.js +++ b/prepare-release.js @@ -90,7 +90,7 @@ const bump = promisify(conventionalRecommendedBump); console.log('Tagging commit'); const tmpTagFile = '.release-notes.md'; fs.writeFileSync(tmpTagFile, releaseNotes); - exec(`git tag -a dotnet-stryker@${newVersionNumber} -F ${tmpTagFile}`); + exec(`git tag -a dotnet-stryker@${newVersionNumber} --cleanup=verbatim -F ${tmpTagFile}`); fs.unlinkSync(tmpTagFile); } diff --git a/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json b/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json index 33a4ab245e..0ececa06a2 100644 --- a/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json +++ b/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json @@ -858,7 +858,7 @@ "Microsoft.Extensions.DependencyInjection": "[10.0.5, )", "NuGet.Protocol": "[7.3.0, )", "YamlDotNet": "[17.0.1, )", - "stryker": "[4.14.0, )" + "stryker": "[4.14.1, )" } }, "stryker": { diff --git a/src/Stryker.Configuration/packages.lock.json b/src/Stryker.Configuration/packages.lock.json index 235249f0f2..2e34312bf6 100644 --- a/src/Stryker.Configuration/packages.lock.json +++ b/src/Stryker.Configuration/packages.lock.json @@ -257,7 +257,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[10.0.5, )", "Mono.Cecil": "[0.11.6, )", "ResXResourceReader.NetStandard": "[1.3.0, )", - "Stryker.Abstractions": "[4.14.0, )" + "Stryker.Abstractions": "[1.0.0, )" } }, "Buildalyzer": { diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json b/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json index 41c241bb87..b71b58bff5 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json +++ b/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json @@ -872,7 +872,7 @@ "Stryker.TestRunner": "[1.0.0, )", "Stryker.Utilities": "[1.0.0, )", "TestableIO.System.IO.Abstractions.TestingHelpers": "[22.1.1, )", - "stryker": "[4.14.0, )" + "stryker": "[4.14.1, )" } }, "stryker.datacollector": { diff --git a/src/Stryker.TestRunner.VsTest/packages.lock.json b/src/Stryker.TestRunner.VsTest/packages.lock.json index 0e85471711..708f745590 100644 --- a/src/Stryker.TestRunner.VsTest/packages.lock.json +++ b/src/Stryker.TestRunner.VsTest/packages.lock.json @@ -286,7 +286,7 @@ "type": "Project", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "[18.4.0, )", - "Stryker.Abstractions": "[4.14.0, )" + "Stryker.Abstractions": "[1.0.0, )" } }, "stryker.utilities": { @@ -298,7 +298,7 @@ "Microsoft.Extensions.Logging.Abstractions": "[10.0.5, )", "Mono.Cecil": "[0.11.6, )", "ResXResourceReader.NetStandard": "[1.3.0, )", - "Stryker.Abstractions": "[4.14.0, )" + "Stryker.Abstractions": "[1.0.0, )" } }, "Buildalyzer": {