Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@

# Copilot Instructions

## Code Style Rules

### Line Length

- All `.cs` source files must adhere to the following rule:
- No line of code should exceed **120 characters** in length.
- This includes comments, string literals, and code.
- Exception: automatically generated files may be ignored if they cannot be reformatted safely.
- **How to measure (raw file characters, per-line only):**
- Count based on **raw file characters**, not editor rendering.
- **Tabs count as 4 characters** for measurement.
- Trailing whitespace must be removed.
- **Per-physical-line measurement ONLY.** The unit of measurement is a **single newline-delimited line**.
- **Never** add or aggregate the lengths of multiple lines.
- A wrapped invocation is compliant if **each** physical line is ≤ 120 characters.
- Ignore soft wrapping (on-screen wrapping that doesn't insert a newline).

### Code Formatting

- Single-line instructions must follow each other with **no blank lines** in between.
- **New rule (clarified):** A multi-line instruction must be preceded by **exactly one blank line _only when it begins a new statement_**.
Do **not** require a blank line before a multi-line **continuation** of an existing statement.
- If a multi-line instruction is followed by further instructions, it must also be followed by **exactly one blank line**.
- **Exception (block first statement):** If a statement is the **first statement inside a block**—i.e., directly after `{`—**no preceding blank line** is required.
- Any C# `return` statement must be preceded by **exactly one blank line** unless it is the first statement in a block.
- If a constructor/method name would push a line past 120 characters, move `new`, the call, or the arguments to the next line.
- Always format so that **no single physical line exceeds 120 characters**, even when calls span multiple lines.
- **Definition of a blank line (updated):**
A blank line is any physical line that contains **no visible characters**. After trimming whitespace, the line must be empty.
Lines containing only spaces or tabs **are valid blank lines**.
- **Method separation:** Method declarations must be preceded by **exactly one blank line** after the closing brace of the previous member.
- **Argument indentation:**
- For multi-line method or constructor calls, the first line ends before the first argument.
- Each wrapped argument line must be indented **one additional indentation level** (usually 4 spaces).
- Do **not** use extra indentation levels.
- The closing `)` must align with the start of the call.
- **Continuation clarification (applies across all checks):**
- A line is considered a **continuation of the same statement** and must **not** be flagged for a missing blank line when **both** are true:
1) The previous non-empty trimmed line **does not** end with `;` or `}`, **and**
2) The current line, after trimming leading whitespace, **starts with a continuation indicator**, such as:
`.`, `??`, `?`, `:`, `+`, `-`, `*`, `/`, `%`, `&&`, `||`, `=>`, `,`, `)`, `]`,
or any identifier/keyword when the previous line ends with an incomplete construct (e.g., open `(`, interpolated start `$"`, method/constructor call, LINQ chain).
- Only when a **new statement** begins (i.e., the previous trimmed line **ends** with `;` or `}`) and the **next statement is multi-line** should an **exactly one** blank line be required before it.

### Enforcement

- Copilot should **not generate code** that exceeds the 120-character line limit.
- When writing new C# code, Copilot should:
- Break up long method/constructor calls across multiple lines.
- Use string interpolation or verbatim strings with proper line breaks where needed.
- Format long LINQ queries across multiple lines.
- Wrap parameters and arguments for readability.
- Insert a blank line before any `return` following other statements.
- Prefer moving `new` or the method invocation to the next line when appropriate.

### Review Guidelines (strict)

- Copilot must:
- Evaluate **each physical line independently**.
- Flag a violation only when a **single physical line** exceeds 120 characters.
- When flagging, include line number and measured character count.
- Suggest multiline formatting only when the offending line exceeds 120.
- **Not** flag whitespace-only lines; they are valid blank lines.
- **Continuation Detection (unambiguous):** Do **not** require a blank line before a line that is a continuation of the same statement.
Treat a line as a continuation when **both** of the following hold:
1) The previous non-empty trimmed line **does not** end with `;` or `}`, **and**
2) The current line (after trimming leading whitespace) **begins with** a continuation indicator:
`.`, `??`, `?`, `:`, `+`, `-`, `*`, `/`, `%`, `&&`, `||`, `=>`, `,`, `)`, `]`,
or any identifier/keyword when the previous line ends with an incomplete construct (e.g., open `(`, start of `$"`, method/constructor call, LINQ chain).
- Flag missing blank lines before `return` **only** when `return` is the first token on the line **and** the previous non-empty trimmed line ended with `;` or `}`.
- **Operator lines:**
- Measure compliance per physical line.
- Do not combine operator lines with continuations.
- Operator-at-end style is preferred.
- **Block-first statement exemption:**
- Do not require a preceding blank line if the previous meaningful line ends with `{`.

### Examples

#### ✅ Correct (first statement inside a block; no blank line required)

```csharp
public void Foo()
{
DoSomething(
x,
y);
}
```

#### ❌ Incorrect (blank line required between two statements)

```csharp
DoSomething();
DoSomethingElse(
x,
y);
```

#### ✅ Correct (wrapped invocation; each line ≤ 120)

```csharp
Validate(
createException: () => new InvalidDecisionPollException(
message: "Invalid decisionPoll. Please correct the errors and try again."),
(Rule: IsInvalid(decisionPoll.Id), Parameter: nameof(DecisionPoll.Id)));
```

#### ❌ Incorrect (single line > 120)

```csharp
Validate(createException: () => new InvalidDecisionPollException(message: "Invalid decisionPoll. Please correct the errors and try again."));
```

---

### Code Formatting Rule Examples

#### ✅ Correct (return with blank line)

```csharp
var user = users.FirstOrDefault(u => u.Id == id);

return user;
```

#### ❌ Incorrect

```csharp
var user = users.FirstOrDefault(u => u.Id == id);
return user;
```

---

### Argument Indentation Examples

#### ✅ Correct

```csharp
DoSomething(
firstArgument: "value1",
secondArgument: "value2",
thirdArgument: "value3");
```

#### ❌ Incorrect (extra indentation)

```csharp
DoSomething(
firstArgument: "value1",
secondArgument: "value2",
thirdArgument: "value3");
```

#### ❌ Incorrect (misaligned closing parenthesis)

```csharp
DoSomething(
firstArgument: "value1",
secondArgument: "value2",
thirdArgument: "value3"
);
```

---

### More Formatting Examples

#### ✅ Correct

```csharp
var filteredUsers = users
.Where(u => u.IsActive && u.LastLoginDate >= DateTime.UtcNow.AddDays(-30))
.OrderByDescending(u => u.LastLoginDate)
.Select(u => new
{
u.Id,
u.Name,
u.Email,
LastSeen = u.LastLoginDate.ToString("yyyy-MM-dd HH:mm:ss")
})
.ToList();
```

#### ❌ Incorrect

```csharp
var filteredUsers = users.Where(u => u.IsActive && u.LastLoginDate >= DateTime.UtcNow.AddDays(-30)).OrderByDescending(u => u.LastLoginDate).Select(u => new { u.Id, u.Name, u.Email, LastSeen = u.LastLoginDate.ToString("yyyy-MM-dd HH:mm:ss") }).ToList();
```

---

### Rationale

- **Per-line measurement** prevents false positives in wrapped calls.
- **Tabs count as 4 characters** ensures consistent line-length calculation.
- **Whitespace-only blank lines count as blank** and match VS behaviour.
- **Return visibility** improves readability.
- **Argument indentation** improves consistency.
- **Block-first patterns** avoid unnecessary whitespace noise.

---

## Supporting .editorconfig Settings

```ini
[*.{cs,vb,ts,tsx}]
guidelines = 120
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
end_of_line = crlf

dotnet_sort_system_directives_first = true
```
150 changes: 150 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Build
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
- closed
branches:
- main
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Enable long paths for Git
run: git config --system core.longpaths true
- name: Check out
uses: actions/checkout@v3
- name: Setup .Net
uses: actions/setup-dotnet@v3
with:
dotnet-version: 10.0.100
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run Unit Tests
run: >-
$projects = Get-ChildItem -Path . -Filter "*Tests.Unit*.csproj" -Recurse

foreach ($project in $projects) {
Write-Host "Running tests for: $($project.FullName)"
dotnet test $project.FullName --no-build --verbosity normal
}
shell: pwsh
- name: Run Acceptance Tests
run: >-
$projects = Get-ChildItem -Path . -Filter "*Tests.Acceptance*.csproj" -Recurse

foreach ($project in $projects) {
Write-Host "Running tests for: $($project.FullName)"
dotnet test $project.FullName --no-build --verbosity normal
}
add_tag:
name: Tag and Release
runs-on: ubuntu-latest
needs:
- build
if: >-
needs.build.result == 'success' &&

github.event.pull_request.merged &&

github.event.pull_request.base.ref == 'main' &&

startsWith(github.event.pull_request.title, 'RELEASES:') &&

contains(github.event.pull_request.labels.*.name, 'RELEASES')
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.PAT_FOR_TAGGING }}
- name: Configure Git
run: >-
git config user.name "GitHub Action"

git config user.email "action@github.com"
- name: Extract Version
id: extract_version
run: >
# Running on Linux/Unix

sudo apt-get install xmlstarlet

version_number=$(xmlstarlet sel -t -v "//Version" -n NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj)

echo "$version_number"

echo "version_number<<EOF" >> $GITHUB_OUTPUT

echo "$version_number" >> $GITHUB_OUTPUT

echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: Display Version
run: 'echo "Version number: ${{ steps.extract_version.outputs.version_number }}"'
- name: Extract Package Release Notes
id: extract_package_release_notes
run: >
# Running on Linux/Unix

sudo apt-get install xmlstarlet

package_release_notes=$(xmlstarlet sel -t -v "//PackageReleaseNotes" -n NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj)

echo "$package_release_notes"

echo "package_release_notes<<EOF" >> $GITHUB_OUTPUT

echo "$package_release_notes" >> $GITHUB_OUTPUT

echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: Display Package Release Notes
run: 'echo "Package Release Notes: ${{ steps.extract_package_release_notes.outputs.package_release_notes }}"'
- name: Create GitHub Tag
run: >-
git tag -a "v${{ steps.extract_version.outputs.version_number }}" -m "Release - v${{ steps.extract_version.outputs.version_number }}"

git push origin --tags
- name: Create GitHub Release
uses: actions/create-release@v1
with:
tag_name: v${{ steps.extract_version.outputs.version_number }}
release_name: Release - v${{ steps.extract_version.outputs.version_number }}
body: >-
## Release - v${{ steps.extract_version.outputs.version_number }}


### Release Notes

${{ steps.extract_package_release_notes.outputs.package_release_notes }}
env:
GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGGING }}
publish:
name: Publish to NuGet
runs-on: ubuntu-latest
needs:
- add_tag
if: needs.add_tag.result == 'success'
steps:
- name: Check out
uses: actions/checkout@v3
- name: Setup .Net
uses: actions/setup-dotnet@v3
with:
dotnet-version: 10.0.100
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Pack NuGet Package
run: dotnet pack --configuration Release --include-symbols
- name: Push NuGet Package
run: dotnet nuget push **/bin/Release/**/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_ACCESS }} --skip-duplicate
Loading
Loading