Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d400e7742 | ||
|
|
8295f833f8 | ||
|
|
8d6645b0ef | ||
|
|
b2ff910a92 | ||
|
|
b70d2b482d | ||
|
|
24ef23e133 | ||
|
|
c8fb306e67 | ||
|
|
667c2f5a47 | ||
|
|
8230efd722 | ||
|
|
f420b0b894 | ||
|
|
b196ee89b7 | ||
|
|
b998afcd93 | ||
|
|
8c0bdb4b0b | ||
|
|
66d090b4e3 | ||
|
|
b779123946 | ||
|
|
ed36b31576 | ||
|
|
4871c0bab1 | ||
|
|
bfe567f075 | ||
|
|
328c43c225 | ||
|
|
56de595457 | ||
|
|
7505efd433 | ||
|
|
934126ae6e | ||
|
|
632f04afd2 | ||
|
|
edae01a152 | ||
|
|
d418f4f4d5 | ||
|
|
da677c42ed | ||
|
|
467db7736c | ||
|
|
aced6916ff | ||
|
|
dc0005f394 | ||
|
|
0da1146fcd | ||
|
|
a7b4481c6c | ||
|
|
90ec85386b | ||
|
|
9baaad40d1 | ||
|
|
7dcc7303c2 | ||
|
|
7edc6ec11d | ||
|
|
f2589b5af5 | ||
|
|
23b09b1a7e | ||
|
|
951f9a9e46 | ||
|
|
8949fd2d22 | ||
|
|
5d6f4f2135 | ||
|
|
21520eebd6 | ||
|
|
87e20ceec6 | ||
|
|
f7b874d3ef | ||
|
|
dedc77c883 | ||
|
|
8110b80318 | ||
|
|
9de905cba3 | ||
|
|
802fa70154 | ||
|
|
ebf397702d | ||
|
|
388e5f9239 | ||
|
|
d20cdd4930 | ||
|
|
a604b2053e | ||
|
|
c177c305ba | ||
|
|
e840a0c7c4 | ||
|
|
c9d5c7f95b | ||
|
|
4c9d02e90b | ||
|
|
d39395a1ff | ||
|
|
d45ddc315a | ||
|
|
7d87ce35bc | ||
|
|
d4805faee7 | ||
|
|
3deccdbcf3 | ||
|
|
9ad24ac2a7 | ||
|
|
0751728b05 | ||
|
|
e504011b76 | ||
|
|
4d9ea1e2e3 | ||
|
|
6796829908 | ||
|
|
67f7d99696 | ||
|
|
27e3184550 | ||
|
|
c98fd7b219 | ||
|
|
301be352ae | ||
|
|
7c180f6e4f | ||
|
|
e119186e34 | ||
|
|
a282002c30 | ||
|
|
a2c36d602f | ||
|
|
a45917f132 | ||
|
|
fe358b8eab | ||
|
|
60f84f7378 | ||
|
|
45f7b7ad98 | ||
|
|
85aa6d3b57 | ||
|
|
33e201c4b5 | ||
|
|
c2890e15c3 | ||
|
|
784e09e240 | ||
|
|
214f3fbc64 | ||
|
|
b622e7e4fb | ||
|
|
a352c42742 | ||
|
|
8eff28b80c | ||
|
|
9f37db18d1 | ||
|
|
0d94f8bfaf | ||
|
|
509a2e18d0 | ||
|
|
22f748161a | ||
|
|
59ad07209b | ||
|
|
67abecc252 | ||
|
|
ebd0017f6e | ||
|
|
417f15197e | ||
|
|
ce40103c4f | ||
|
|
2748fdfc48 | ||
|
|
53cc663bde | ||
|
|
13a2cd71c4 | ||
|
|
a60ccd389b | ||
|
|
fd95dc3915 | ||
|
|
990e379ea8 | ||
|
|
1f5c21c631 | ||
|
|
b4144564c8 | ||
|
|
4ad18dc963 | ||
|
|
881535af7d | ||
|
|
1e66fb6ab3 | ||
|
|
e1b9e5efad | ||
|
|
f6dbe0f33e | ||
|
|
28570b43e8 | ||
|
|
a01382d756 | ||
|
|
1d6e7f15b9 | ||
|
|
97cc3ca581 | ||
|
|
0bcae3e87b | ||
|
|
ebd228c3d7 | ||
|
|
4b83a74bdb | ||
|
|
b348b84b63 | ||
|
|
7f4a8d5974 | ||
|
|
4d8ca457ec | ||
|
|
d677e948f1 | ||
|
|
d4e6af50bd | ||
|
|
c609cafa9b | ||
|
|
5556b0fe0c | ||
|
|
eee04f4703 | ||
|
|
127c6c4b53 | ||
|
|
7d6a1e54e6 | ||
|
|
8433b2b637 | ||
|
|
87b2153794 | ||
|
|
0fc5b2a95c | ||
|
|
6c2f93e85d | ||
|
|
4f1604fea1 | ||
|
|
059766291b | ||
|
|
e471c1fc8b | ||
|
|
0fb91ef859 | ||
|
|
c0a0a34cbb | ||
|
|
3d44a6fdf9 | ||
|
|
0eee307622 | ||
|
|
6a4aa04b47 | ||
|
|
643f073945 | ||
|
|
5ba40c65fc | ||
|
|
dcbbed4160 | ||
|
|
e94e165593 | ||
|
|
34709be6cc | ||
|
|
83107bb31a | ||
|
|
3caa43a5bf | ||
|
|
1efe860a89 | ||
|
|
d49965549c | ||
|
|
8d85259025 | ||
|
|
097d0456cc | ||
|
|
a62fa93c77 | ||
|
|
a22dcf05f2 | ||
|
|
afa6fa5ca2 | ||
|
|
c6effef622 | ||
|
|
ab31fbb0aa | ||
|
|
7d9a2c6e70 | ||
|
|
a486d5df33 | ||
|
|
3c8a621817 | ||
|
|
0e0aca55af | ||
|
|
0f02584143 | ||
|
|
a3db45609a | ||
|
|
d60a2a217c | ||
|
|
3e9ff6b5cb | ||
|
|
05a7d337de | ||
|
|
fdca086a47 | ||
|
|
42a81beeac | ||
|
|
9284b9e0b1 | ||
|
|
eb91f4fef4 | ||
|
|
68acc2aa51 | ||
|
|
5570583f70 | ||
|
|
eb979625d1 |
@@ -1,4 +1,3 @@
|
||||
**/*
|
||||
!tests/Umbraco.Tests.Integration/bin/**
|
||||
!tests/Umbraco.Tests.UnitTests/bin/**
|
||||
**/node_modules
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
name: umb-bump-version
|
||||
description: Bump the Umbraco CMS version across all required files. Use when the user asks to bump, update, or set the version number — e.g., "bump version to 17.3.4", "set version to 18.0.0-rc", "update version". Accepts the target version as an argument.
|
||||
argument-hint: <version> (e.g., 17.3.4, 18.0.0-rc)
|
||||
---
|
||||
|
||||
# Bump Version - Umbraco CMS
|
||||
|
||||
Updates the Umbraco CMS version string across all files that track it.
|
||||
|
||||
**Do NOT use AskUserQuestion if a version argument is provided. Only ask if `$ARGUMENTS` is empty or cannot be parsed as a version.**
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$ARGUMENTS` - Required: the target version string (e.g., `17.3.4`, `18.0.0-rc`)
|
||||
|
||||
## Files to Update
|
||||
|
||||
The following 5 files must be updated with the new version:
|
||||
|
||||
| # | File | Field |
|
||||
|---|------|-------|
|
||||
| 1 | `version.json` | `"version"` |
|
||||
| 2 | `src/Umbraco.Web.UI.Client/package.json` | `"version"` |
|
||||
| 3 | `src/Umbraco.Web.UI.Client/package-lock.json` | top-level `"version"` AND `packages[""].version` |
|
||||
| 4 | `tests/Umbraco.Tests.AcceptanceTest/package.json` | `"version"` |
|
||||
| 5 | `tests/Umbraco.Tests.AcceptanceTest/package-lock.json` | top-level `"version"` AND `packages[""].version` |
|
||||
|
||||
**Note**: For major version bumps (e.g., 17.x to 18.x), `src/Umbraco.Web.UI.Login/package.json` has a caret-ranged dependency on `@umbraco-cms/backoffice` (e.g., `^17.2.0`) that will need manual updating. This skill does not handle that — major bumps involve many other changes beyond version strings.
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Parse and Validate the Version
|
||||
|
||||
Extract the version from `$ARGUMENTS`. It must be a valid semver-like string (e.g., `17.3.4`, `18.0.0-rc`, `17.4.0-preview.1`). If no version is provided or it cannot be parsed, ask the user for the target version.
|
||||
|
||||
### 2. Read the Current Version
|
||||
|
||||
Read `version.json` and extract the current `"version"` value. If the current version already equals the target version, report that the version is already set and stop — do not edit, stage, or commit anything.
|
||||
|
||||
Otherwise, display both versions:
|
||||
|
||||
```
|
||||
Bumping version: {current} -> {target}
|
||||
```
|
||||
|
||||
### 3. Update All Files
|
||||
|
||||
Update each of the 5 files listed above, replacing the old version with the new version. For each file:
|
||||
|
||||
- **`version.json`**: Replace the `"version"` value.
|
||||
- **`package.json` files**: Replace the `"version"` value (near the top of the file).
|
||||
- **`package-lock.json` files**: Replace BOTH the top-level `"version"` value AND the `"version"` inside the `"packages": { "": { ... } }` block. These are always in the first ~10 lines of the file.
|
||||
|
||||
Use targeted edits — do NOT rewrite entire files. Be precise to avoid changing version strings in dependency entries.
|
||||
|
||||
### 4. Verify
|
||||
|
||||
After all edits, grep for the target version value across the 5 files to confirm all updates landed correctly:
|
||||
|
||||
```bash
|
||||
grep -n "\"version\": \"{version}\"" version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
|
||||
```
|
||||
|
||||
Expect exactly 7 matches (one per `package.json` and `version.json`, two per `package-lock.json`).
|
||||
|
||||
### 5. Stage and Commit
|
||||
|
||||
Stage only the 5 changed files:
|
||||
|
||||
```bash
|
||||
git add version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
|
||||
```
|
||||
|
||||
Then commit with the message `Bump version to {version}.` — replacing `{version}` with the target version:
|
||||
|
||||
```bash
|
||||
git commit -m "Bump version to {version}."
|
||||
```
|
||||
|
||||
### 6. Report
|
||||
|
||||
Output a summary:
|
||||
|
||||
```
|
||||
Version bumped to {version} in:
|
||||
- version.json
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
- tests/Umbraco.Tests.AcceptanceTest/package.json
|
||||
- tests/Umbraco.Tests.AcceptanceTest/package-lock.json
|
||||
|
||||
Changes staged and committed.
|
||||
```
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
name: umb-review
|
||||
description: Automated PR code review for Umbraco CMS. Analyzes changed files for intent, impact on consumers, breaking changes, architecture compliance, and code quality. Non-interactive — outputs a full structured review. Use this skill whenever the user asks to review a branch, review a PR, check their changes for issues, analyze a diff, or validate breaking change patterns — even if they don't say "review" explicitly. Does NOT apply to writing new code, fixing bugs, refactoring, explaining architecture, writing tests, or reviewing documentation content.
|
||||
argument-hint: <target-branch>
|
||||
---
|
||||
|
||||
# PR Review - Umbraco CMS
|
||||
|
||||
Automated, non-interactive PR code review. Analyzes changed files for intent, impact on consumers, breaking changes, architecture compliance, and code quality.
|
||||
|
||||
**Do NOT use AskUserQuestion at any point. This skill runs fully autonomously.**
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$ARGUMENTS` - Optional: target branch to diff against (auto-detected from PR, falls back to `origin/main`)
|
||||
|
||||
## Instructions
|
||||
|
||||
### 0. Verify GH CLI is Available
|
||||
|
||||
Run `gh auth status`. If it fails, read `references/gh-cli-setup.md` and present the setup instructions to the user. Do not proceed with the review.
|
||||
|
||||
### 1. Resolve Target Branch
|
||||
|
||||
Determine the target branch for comparison using this priority order:
|
||||
|
||||
1. **Explicit argument**: If `$ARGUMENTS` is provided and non-empty, use it as the target branch
|
||||
2. **PR target branch**: If no argument, run `gh pr view --json baseRefName --jq '.baseRefName'` to detect the target branch of the current branch's open PR. If a PR exists, use `origin/{baseRefName}` as the target branch.
|
||||
3. **Fallback**: If no argument and no PR found (command fails or returns empty), default to `origin/main`
|
||||
|
||||
Store the resolved target branch for use in subsequent steps. Log which resolution method was used (e.g., "Target branch: `origin/v18/dev` (from PR #1234)").
|
||||
|
||||
### 2. Load Review Standards
|
||||
|
||||
#### 2a. Load coding preferences
|
||||
|
||||
Read the coding preferences and code review scoring criteria from:
|
||||
|
||||
- `references/coding-preferences.md` (relative to this skill file)
|
||||
|
||||
Parse and internalize all rules, conventions, scoring categories, and severity definitions. These are your review criteria.
|
||||
|
||||
#### 2b. Load area-specific documentation
|
||||
|
||||
Once the changed file list is known (after step 3a), determine which areas of the codebase are touched and load the relevant documentation. Execute this sub-step between 3a and 3b. This documentation takes precedence over sibling comparison for architectural and pattern validation.
|
||||
|
||||
**Resolution order for each changed file:**
|
||||
|
||||
1. **Find the nearest `CLAUDE.md`** — walk up from the changed file's directory toward the repository root. The first `CLAUDE.md` found is the area guide for that file. Read it.
|
||||
2. **Read referenced docs** — if the `CLAUDE.md` references documentation files (e.g., a `docs/` directory), use the descriptions in the `CLAUDE.md` to determine which docs are relevant to the type of code being changed, and read those. If unsure, read all referenced docs — the cost of reading is low, the cost of missing a convention is high.
|
||||
3. **Follow cross-references in loaded docs** — if a loaded doc references another doc as covering a complementary or related concern, and the changed files touch that concern, read the referenced doc too. Repeat until no new relevant cross-references remain.
|
||||
4. **Check for applicable skills** — review the available skills list. If a skill exists for the type of code being changed, read the skill file to understand the expected patterns, structure, and conventions it enforces. Do NOT invoke the skill — just use it as a reference for what the correct implementation should look like.
|
||||
|
||||
**Store all loaded documentation** for use in step 4. These docs define the authoritative patterns and conventions that the review evaluates against.
|
||||
|
||||
### 3. Gather Changed Files
|
||||
|
||||
#### 3a. Collect file list, stats, and diff
|
||||
|
||||
Run these git commands (where `{target}` is the resolved target branch):
|
||||
|
||||
```bash
|
||||
git diff {target}...HEAD --name-only --diff-filter=d # changed files (excluding deleted)
|
||||
git diff {target}...HEAD --stat # line counts per file
|
||||
git log {target}...HEAD --oneline # commit history
|
||||
git diff {target}...HEAD # full diff (primary review source)
|
||||
```
|
||||
|
||||
**If no changes found**: Output "No changes found between current branch and `{target}`. Nothing to review." and stop.
|
||||
|
||||
#### 3b. Filter out noise files
|
||||
|
||||
From the changed file list, classify each file as **noise** or **reviewable**.
|
||||
|
||||
**Noise files** (skip entirely — do not read, do not review):
|
||||
|
||||
| Pattern | Reason |
|
||||
| ---------------------------------------------------- | ------------------------------- |
|
||||
| `*.gen.ts`, `*.gen.cs` | Auto-generated API client code |
|
||||
| `*.generated.cs`, `*.Designer.cs` (in `Migrations/`) | Auto-generated models/snapshots |
|
||||
| `*/assets/lang/*.ts` (except `en.ts`) | Non-English translation files |
|
||||
| `*/mocks/data/*.ts` | Test fixture data |
|
||||
| `*/dist-cms/*`, `*/storybook-static/*` | Build output |
|
||||
| `*/TEMP/InMemoryAuto/*` | Runtime-generated models |
|
||||
| `package-lock.json` | Dependency lock file |
|
||||
| `appsettings-schema.*.json` | Generated JSON schema |
|
||||
|
||||
Log the skip list: "Skipped {N} noise files: {comma-separated list of filenames}"
|
||||
|
||||
#### 3c. Read reviewable changed files
|
||||
|
||||
Read the full file for every reviewable changed file.
|
||||
|
||||
#### 3d. Track file counts
|
||||
|
||||
Keep track of these numbers for the review output in step 7: total changed files, noise files skipped, and reviewable files read. Also record: distinct production layers touched, distinct project directories, and total lines changed — these feed step 3e.
|
||||
|
||||
#### 3e. Assess PR complexity
|
||||
|
||||
Follow the procedure in `references/complexity-assessment.md`. Store the triggered dimensions and suggestions for step 7.
|
||||
|
||||
#### 3f. Classify PR scope
|
||||
|
||||
Classify the PR to determine which review steps are relevant:
|
||||
|
||||
| Classification | Condition | Effect |
|
||||
| --------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| **Gen-only** | All reviewable files are `gen.ts` | Skip steps 5 and 6; step 4 reviews impact on other code only |
|
||||
| **Docs-only** | All reviewable files are `.md` | Skip steps 5 and 6; step 4 reviews intent and readability only |
|
||||
| **Test-only** | All reviewable files are in `tests/` | Skip steps 5 and 6; step 4 reviews intent, code quality, and test coverage only |
|
||||
| **Config-only** | All reviewable files are `.csproj`, `.props`, `.json` config, or CI/build files | Skip step 5; step 6 checks dependency version changes only |
|
||||
| **Standard** | Anything else | No skips — run all steps |
|
||||
|
||||
### 4. Raw Code Review
|
||||
|
||||
Review each changed file holistically. Think like a senior developer reading a colleague's PR. Note all findings without worrying about format or severity yet.
|
||||
|
||||
#### 4a. Read and reason about each file
|
||||
|
||||
For each changed file, reason about: What does this code do? Is it correct? What's missing — validation, error handling, notifications, cleanup, edge cases? Could this break anything for consumers?
|
||||
|
||||
#### 4b. Validate against documentation and patterns
|
||||
|
||||
Use a **docs-first** approach: classify the code by what it does, check it against documented conventions, and only fall back to sibling comparison when docs don't cover the pattern.
|
||||
|
||||
**Step 1 — Determine the correct approach from documentation, then check whether the PR matches**
|
||||
|
||||
A PR is a proposed solution, not the source of truth. This step has two parts that must happen in order — do not start part B until part A is complete.
|
||||
|
||||
**Part A — Before validating/judging the implementation**, determine what the correct approach is for each new class or file based on what it does. Use the documentation loaded in step 2b to identify the expected base classes, patterns, and conventions. Write down the expected approach. Classify based on what the code does, not based on what neighboring files look like.
|
||||
|
||||
**Part B — Now compare the PR's implementation** against the expected approach from Part A. If it deviates from the documented approach, flag it. If the documentation specifies reference examples, read those examples to verify the implementation matches.
|
||||
|
||||
**Pattern match is the leading finding.** If the documentation defines a pattern that fits what the code does, the first and most important finding is whether the code follows that pattern.
|
||||
|
||||
**Step 2 — Fall back to sibling comparison**
|
||||
|
||||
If the documentation does not cover the specific pattern, or for cross-cutting concerns not addressed in docs, fall back to sibling comparison:
|
||||
|
||||
1. **New method on existing class/interface**: Grep for the most similar existing method on the same class using `-A 80` to capture the full method body (e.g., `UpdateCurrentUserAsync` → grep for `UpdateAsync` in the same file with `-A 80`). Compare line by line for missing cross-cutting concerns: notifications/events, validation, scoping, authorization, error handling, audit logging.
|
||||
2. **New TS class**: Grep for siblings by base class (`extends {BaseClass}`) or by interface (`implements {Interface}`) or by name suffix (e.g., `CurrentUserController` → grep for `UserController`). Compare for missing concerns.
|
||||
3. **New CS class**: Grep for siblings by base class (`class {ClassName} : {BaseClass}`) or by interface (`class {ClassName} : {Interface}`) or by name suffix (e.g., `ManagementApiComposer` → grep for `ApiComposer`). Compare for missing concerns.
|
||||
|
||||
**Important:** Sibling comparison validates cross-cutting concerns, but it must not override documented conventions. If a sibling deviates from documented patterns, that sibling is wrong — do not copy its deviation.
|
||||
|
||||
Store your raw findings — they feed into step 7.
|
||||
|
||||
### 5. Impact Analysis
|
||||
|
||||
**Skip this step if PR scope is docs-only, test-only, or config-only.**
|
||||
|
||||
Follow the procedure in `references/impact-analysis.md`.
|
||||
|
||||
### 6. Breaking Changes Check
|
||||
|
||||
**Skip this step if PR scope is docs-only or test-only. If config-only, only check for dependency version changes that could break consumers.**
|
||||
|
||||
Follow the procedure in `references/breaking-changes.md`.
|
||||
|
||||
### 7. Consolidate and Output Review
|
||||
|
||||
Merge findings from step 4 (raw review), step 5 (impact analysis), and step 6 (breaking changes). For each finding, assign severity (Critical/Important/Suggestion) and verify it relates to changed code — not pre-existing issues. Before outputting, drop any finding about whitespace, blank lines, formatting, or comment wording. Then present the review in this exact format:
|
||||
|
||||
```markdown
|
||||
## PR Review
|
||||
|
||||
**Target:** `{target_branch}` · **Based on commit:** `{head_sha}`
|
||||
[If any skipped files, append: · **Skipped:** {skipped} files out of {total} total]
|
||||
[If step 3f classification is not "Standard", append: · **Classified as:** {classification}]
|
||||
|
||||
[1–2 sentences: what this PR accomplishes , keep it as short as possible, only highlight the primary essence.]
|
||||
|
||||
- **Modified public API:** {changed existing interfaces/types/classes/methods}
|
||||
[Omit bullet if none]
|
||||
- **Affected implementations (outside this PR):** {interfaces/types/classes/methods using modified public API}
|
||||
[Omit bullet if none]
|
||||
- **Breaking changes:** {violations with specifics}
|
||||
[Omit bullet if none]
|
||||
- **Other changes:** {changes not listed above that an Umbraco user, plugin developer, or API consumer would notice — e.g., behavior changes, default value changes, error message changes, new configuration options, removed functionality. Exclude internal renames, formatting, and private implementation details.}
|
||||
[Omit bullet if none]
|
||||
|
||||
[If step 3e triggered any dimensions, insert this block. Omit entirely if nothing triggered:]
|
||||
|
||||
> [!NOTE]
|
||||
> **Complexity advisory** — This PR may benefit from splitting.
|
||||
>
|
||||
> - **{Dimension}:** {Explanation and concrete split suggestion from step 3e}
|
||||
> [one bullet per triggered dimension]
|
||||
>
|
||||
> _This is an observation, not a blocker. The full review follows below._
|
||||
|
||||
---
|
||||
|
||||
### Critical
|
||||
|
||||
[Must fix before merge — security vulnerabilities, data loss, broken functionality, breaking changes without proper patterns]
|
||||
|
||||
- **`{file}:{line}`**: {problem} → {fix}
|
||||
|
||||
[Omit section if none]
|
||||
|
||||
### Important
|
||||
|
||||
[Should fix — performance issues, missing tests, architectural violations, pattern misuse]
|
||||
|
||||
- **`{file}:{line}`**: {observation} → {suggestion}
|
||||
|
||||
[Omit section if none]
|
||||
|
||||
### Suggestions
|
||||
|
||||
[Nice to have — readability, minor refactoring, alternative approaches]
|
||||
|
||||
- **`{file}:{line}`**: {detail}
|
||||
|
||||
[Omit section if none]
|
||||
|
||||
---
|
||||
|
||||
[One of:]
|
||||
|
||||
## Approved
|
||||
|
||||
This looks good to be merged as-is, but please do a manual sanity check and testing before merging.
|
||||
|
||||
## Approved with Suggestions for improvement
|
||||
|
||||
Good to go, but please carefully consider the importance of the suggestions.
|
||||
|
||||
## Request Changes
|
||||
|
||||
Critical and important issues must be addressed first.
|
||||
|
||||
## Needs re-work
|
||||
|
||||
This is in such a bad state that the feedback of this review is not sufficient to guide improvements, the PR cannot be approved.
|
||||
```
|
||||
|
||||
**Guidelines for the review output:**
|
||||
|
||||
— When reporting information, be extremely concise and sacrifice grammar for sake of concision.
|
||||
|
||||
- Only review code that was changed in the diff — pre-existing issues are out of scope. Focus on what compilers and linters cannot catch: behavioral side-effects (e.g., a changed default alters runtime behavior for consumers), architectural violations (e.g., a new dependency breaks layering), breaking changes for external consumers of the public API, and security implications. Leave type errors, missing imports, and broken references to CI.
|
||||
- Be specific — always reference file and line number
|
||||
- Explain WHY something is an issue, not just WHAT, but avoid stating the obvious.
|
||||
- For complex matters, provide concrete fix suggestions, including code snippets when helpful
|
||||
- Keep it constructive — the goal is to help, not gatekeep
|
||||
- Don't repeat the same finding for every occurrence — mention it once and note "same pattern in {other files}"
|
||||
- Focus on substantive issues only. Do NOT flag purely cosmetic or stylistic concerns. Specifically, never flag: code formatting or whitespace, comment grammar or wording, redundant-but-harmless syntax (e.g., optional chaining after a truthiness check), code duplication that doesn't cause bugs, or HTML template cosmetics. The only exception is when a stylistic issue has a concrete impact on performance or rendering. Note: missing JSDoc/documentation on public or exported APIs is a substantive finding (per coding preferences), not a cosmetic one — flag it as a Suggestion.
|
||||
- For breaking changes, reference the specific pattern from the CLAUDE.md that should be applied
|
||||
- Do not suggest changes that would themselves introduce breaking changes. If a suggestion would alter public API surface (e.g., changing return types, renaming public members), it is not appropriate for a PR targeting `main` within a major version. Only suggest non-breaking alternatives.
|
||||
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"skill_name": "umb-review",
|
||||
"evals": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "pr-22214-large-frontend-refactor",
|
||||
"prompt": "Review the changes in PR #22214 (branch origin/pr/22214 targeting main). This is a large frontend refactor migrating create entity actions to use entityCreateOptionAction extensions, with deprecations.",
|
||||
"expected_output": "A structured review that identifies frontend deprecation patterns, flags the large PR complexity, handles 75+ files correctly, checks for breaking changes in exported components, and produces the correct output format.",
|
||||
"pr_number": 22214,
|
||||
"pr_branch": "origin/pr/22214",
|
||||
"base_branch": "origin/main",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{"id": "deprecation-patterns-noted", "text": "Review identifies deprecation patterns (@deprecated, UmbDeprecation)"},
|
||||
{"id": "frontend-breaking-change-awareness", "text": "Checks frontend-specific breaking changes (exports, custom elements) not just backend"},
|
||||
{"id": "file-references-present", "text": "Findings reference specific files with line numbers"},
|
||||
{"id": "no-false-critical-on-deprecations", "text": "Properly deprecated code is NOT flagged as Critical breaking change"},
|
||||
{"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."},
|
||||
{"id": "manifest-alias-rename-detected", "text": "Alias renames (CreateOptions → Create) flagged as Critical breaking change"},
|
||||
{"id": "non-exported-deletions-dismissed", "text": "Deleted action classes NOT flagged as breaking (verified against package.json exports)"},
|
||||
{"id": "noise-files-filtered", "text": "Does not review noise files (generated files, lock files, etc.)"},
|
||||
{"id": "complexity-advisory-triggers", "text": "Review includes a complexity/split advisory for the large 75+ file scope"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "pr-21672-small-frontend-bugfix",
|
||||
"prompt": "Review the changes in PR #21672 (branch origin/pr/21672 targeting main). This is a small 4-file frontend bugfix implementing tab validation badges in the block editor.",
|
||||
"expected_output": "A clean review that correctly identifies this as a small focused bugfix, avoids false positives, and either approves or approves with minor suggestions.",
|
||||
"pr_number": 21672,
|
||||
"pr_branch": "origin/pr/21672",
|
||||
"base_branch": "origin/main",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{"id": "complexity-advisory-absent", "text": "Review does NOT include a complexity/split advisory"},
|
||||
{"id": "no-false-breaking-changes", "text": "Review does not flag breaking changes"},
|
||||
{"id": "proportionate-verdict", "text": "Verdict is 'Request Changes'"},
|
||||
{"id": "concise-review", "text": "Review output is under 200 lines"},
|
||||
{"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "pr-22217-small-backend-webhook",
|
||||
"prompt": "Review the changes in PR #22217 (branch origin/pr/22217 targeting v18/dev). This is a tiny 3-file backend change to the default webhook payload type.",
|
||||
"expected_output": "A concise review that correctly resolves v18/dev as target branch, handles the small change proportionately, and considers the behavioral impact of changing a default value.",
|
||||
"pr_number": 22217,
|
||||
"pr_branch": "origin/pr/22217",
|
||||
"base_branch": "origin/v18/dev",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{"id": "correct-target-branch", "text": "Review references 'v18/dev' as the target branch (not 'main')"},
|
||||
{"id": "default-value-change-noted", "text": "Review discusses the behavioral impact of changing the default payload type"},
|
||||
{"id": "proportionate-review", "text": "Review output is under 150 lines"},
|
||||
{"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."},
|
||||
{"id": "ignores-preexisting-issues", "text": "Does NOT flag the ~30 builder extension methods with Legacy defaults (pre-existing, not changed in the PR)"},
|
||||
{"id": "side-effect-detection", "text": "Flags stale WebhookSettings.cs docs as a side-effect of the constant value change"},
|
||||
{"id": "consumer-identification", "text": "Identifies affected consumers outside the PR (WebhookSettings, UmbracoBuilder, or WebhookEventCollectionBuilderExtensions)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "pr-22268-frontend-feature-workspace-modal",
|
||||
"prompt": "Review the changes in PR #22268 (branch origin/pr/22268 targeting main). This is a 29-file frontend feature adding a current user workspace modal.",
|
||||
"expected_output": "A review of a medium-sized new feature PR. Should assess the new code for architectural compliance, check for breaking changes (new exports, custom elements), and evaluate code quality without flagging pre-existing issues.",
|
||||
"pr_number": 22268,
|
||||
"pr_branch": "origin/pr/22268",
|
||||
"base_branch": "origin/main",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{"id": "complexity-advisory-triggers", "text": "Review includes a complexity/split advisory (3 layers: Core, API, Frontend across 27+ files)"},
|
||||
{"id": "breaking-changes-on-interface-additions", "text": "Flags new interface methods without default implementations as breaking changes (Pattern 3)"},
|
||||
{"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."},
|
||||
{"id": "diff-scoped", "text": "All findings reference code that was changed in the diff, not pre-existing issues"},
|
||||
{"id": "new-feature-assessed", "text": "Review assesses the new feature's architecture, patterns, or integration approach — not just absence of bugs"},
|
||||
{"id": "no-false-notification-finding", "text": "Review does NOT flag UpdateCurrentUserAsync as missing UserSavingNotification/UserSavedNotification — the sibling UpdateAsync also does not publish these notifications, so flagging their absence would be a false positive"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "pr-22215-frontend-architecture-violation",
|
||||
"prompt": "Review the changes in PR #22215 (branch origin/pr/22215 targeting main). This is a 2-file frontend feature adding user management to the user group workspace.",
|
||||
"expected_output": "A review that catches the architecture violation: the workspace context directly imports and calls UserService and UserGroupService (generated API clients) instead of going through a repository. In the Umbraco backoffice, workspace contexts access data via repositories, not by calling API services directly. The review should flag this as a significant architecture issue and request changes.",
|
||||
"pr_number": 22215,
|
||||
"pr_branch": "origin/pr/22215",
|
||||
"base_branch": "origin/main",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{"id": "service-bypass-detected", "text": "Review flags that the workspace context directly imports/calls UserService or UserGroupService instead of using a repository"},
|
||||
{"id": "repository-pattern-recommended", "text": "Review recommends using the repository pattern (going through a repository/data-source layer) rather than calling API services directly from the workspace context"},
|
||||
{"id": "verdict-request-changes", "text": "Verdict is 'Request Changes' (the architecture violation warrants requesting changes, not just approving with suggestions)"},
|
||||
{"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."},
|
||||
{"id": "no-false-breaking-changes", "text": "Review does not flag breaking changes (this PR only adds new code, no public API is removed or modified)"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
# Breaking Changes Reference
|
||||
|
||||
This document describes how to detect and validate breaking changes during PR review. It covers both backend (.NET) and frontend (TypeScript/Lit) patterns.
|
||||
|
||||
---
|
||||
|
||||
## Version Detection
|
||||
|
||||
**Always read `version.json`** at the repository root to determine the current major version. This drives the obsolete removal target calculation:
|
||||
|
||||
- Current major version: read from `version.json` → `version` field (e.g., `"17.4.0-rc"` → major version `17`)
|
||||
- Obsolete removal target: `current + 2` (e.g., if current is 17, removal is scheduled for Umbraco 19)
|
||||
- Format: `[Obsolete("... Scheduled for removal in Umbraco {current+2}.")]`
|
||||
|
||||
---
|
||||
|
||||
## Backend (.NET) Breaking Changes
|
||||
|
||||
### What Constitutes a Breaking Change
|
||||
|
||||
Any of these on a `public` or `protected` member:
|
||||
|
||||
- Removing or renaming a class, interface, struct, record, or enum
|
||||
- Removing or renaming a method, property, or field
|
||||
- Changing a method signature (parameters, return type)
|
||||
- Adding required parameters to an existing method
|
||||
- Adding methods to a public interface (without default implementation)
|
||||
- Changing a constructor signature on a public class
|
||||
- Removing or changing enum values
|
||||
- Changing type hierarchy (base class, implemented interfaces)
|
||||
|
||||
### Pattern 1: Obsolete Constructor + StaticServiceProvider
|
||||
|
||||
When a public class needs new dependencies, the existing constructor must be preserved.
|
||||
|
||||
**Correct pattern:**
|
||||
|
||||
```csharp
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MyService(IDependencyA depA)
|
||||
: this(
|
||||
depA,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDependencyB>())
|
||||
{
|
||||
}
|
||||
|
||||
public MyService(IDependencyA depA, IDependencyB depB)
|
||||
{
|
||||
_depA = depA;
|
||||
_depB = depB;
|
||||
}
|
||||
```
|
||||
|
||||
**Validation checklist:**
|
||||
|
||||
- [ ] Old constructor has `[Obsolete]` attribute with correct removal version
|
||||
- [ ] Old constructor calls new constructor via `: this(...)`
|
||||
- [ ] `StaticServiceProvider.Instance.GetRequiredService<T>()` used for new params only
|
||||
- [ ] DI registration uses the NEW constructor (old is for external consumers only)
|
||||
- [ ] Removal version is `{current_major + 2}`
|
||||
|
||||
**Common mistakes to flag:**
|
||||
|
||||
- Removing the old constructor entirely (breaking change!)
|
||||
- Old constructor NOT calling new constructor (code duplication)
|
||||
- Wrong removal version in `[Obsolete]`
|
||||
- Missing `StaticServiceProvider` resolution for new dependencies
|
||||
- DI registration still using the old constructor
|
||||
|
||||
### Pattern 2: Obsolete Method + New Overload
|
||||
|
||||
When a method signature needs to change, add the new overload and obsolete the old.
|
||||
|
||||
**Correct pattern:**
|
||||
|
||||
```csharp
|
||||
[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public void DoThing(string name)
|
||||
=> DoThing(name, extraParam: null);
|
||||
|
||||
public void DoThing(string name, string? extraParam)
|
||||
{
|
||||
// Real implementation here
|
||||
}
|
||||
```
|
||||
|
||||
**Validation checklist:**
|
||||
|
||||
- [ ] Old method has `[Obsolete]` attribute with correct removal version
|
||||
- [ ] Old method calls new method, providing defaults for new parameters
|
||||
- [ ] All internal callers updated to use the new method
|
||||
- [ ] No internal code references the obsolete method (except the delegation)
|
||||
|
||||
### Pattern 3: Default Interface Implementation
|
||||
|
||||
When adding methods to a public interface, provide a default implementation.
|
||||
|
||||
**Correct pattern:**
|
||||
|
||||
```csharp
|
||||
public interface IMyService
|
||||
{
|
||||
void ExistingMethod();
|
||||
|
||||
// New method with default implementation
|
||||
void NewMethod(string param)
|
||||
=> ExistingMethod(); // delegate to existing if possible
|
||||
}
|
||||
```
|
||||
|
||||
**Strategies for defaults (in order of preference):**
|
||||
|
||||
1. Use existing interface methods to satisfy the contract
|
||||
2. Return a sensible default (empty collection, null, etc.)
|
||||
3. Throw `NotImplementedException` if no reasonable default exists
|
||||
|
||||
**Validation checklist:**
|
||||
|
||||
- [ ] New interface method has a default implementation
|
||||
- [ ] TODO comment present: `// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.`
|
||||
- [ ] Default implementation is functionally correct (even if not optimal)
|
||||
- [ ] If `StaticServiceProvider` is used in default impl, noted as temporary
|
||||
|
||||
### Obsolete Attribute Validation
|
||||
|
||||
For any `[Obsolete]` attribute found in changed code:
|
||||
|
||||
1. **Format**: Must contain `"Scheduled for removal in Umbraco {version}."`
|
||||
2. **Version**: Must be `current_major + 2` (read from `version.json`)
|
||||
3. **Pragma**: Where obsolete members must call each other, `#pragma warning disable CS0618` / `#pragma warning restore CS0618` must be present
|
||||
|
||||
### Internal Caller Check
|
||||
|
||||
After finding obsolete patterns, verify:
|
||||
|
||||
- Search the codebase for usages of the obsolete member
|
||||
- **No internal code** (inside `src/`) should reference obsolete members
|
||||
- Only the obsolete member's own delegation (calling the new version) is acceptable
|
||||
- External consumers (outside the repo) get the deprecation period to migrate
|
||||
|
||||
---
|
||||
|
||||
## Frontend (TypeScript/Lit) Breaking Changes
|
||||
|
||||
The backoffice is published as `@umbraco-cms/backoffice` with 140+ named exports. Plugin developers depend on this public API surface.
|
||||
|
||||
**Critical frontend rule (does not apply to backend .NET where `public`/`protected` visibility determines the API surface): only symbols reachable through the `package.json` `exports` field are public API.** Anything not exported — whether classes, functions, constants, types, or entire files — is an internal implementation detail, even if other internal code imports it. Removing or changing unexported frontend symbols is not a breaking change. Before flagging a frontend deletion or rename as breaking, verify the symbol is reachable via `package.json` exports. If it is not, do not flag it.
|
||||
|
||||
### Custom Elements (Web Components)
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Renaming or removing a registered custom element tag (`umb-*`)
|
||||
- Removing elements from `HTMLElementTagNameMap`
|
||||
- Removing or changing `@property()` decorated fields on exported components
|
||||
- Removing event emissions (checked via `this.dispatchEvent`)
|
||||
- Removing CSS custom properties (`@cssprop` in JSDoc)
|
||||
- Removing CSS parts (`@csspart` in JSDoc)
|
||||
|
||||
**How to detect:**
|
||||
|
||||
- Check diff for removed `@customElement('umb-...')` decorators
|
||||
- Check diff for removed `@property()` fields on exported components
|
||||
- Check diff for removed entries in `HTMLElementTagNameMap` declarations
|
||||
|
||||
### Exported Types/Interfaces
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Removing exports from `package.json` `exports` field
|
||||
- Changing the shape of exported interfaces (removing properties, changing types)
|
||||
- Renaming exported types (consumers import by name)
|
||||
- Removing union type members
|
||||
- Changing generic type parameter constraints
|
||||
|
||||
**How to detect:**
|
||||
|
||||
- Check if `package.json` `exports` field is modified
|
||||
- Check diff for removed `export` statements
|
||||
- Check diff for changed interface/type shapes
|
||||
|
||||
### Manifest/Extension System
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Renaming a manifest `alias` value — plugin developers reference aliases by string in conditions, overwrites, and extension registry lookups. Alias renames are not caught by the compiler since they are string-based. A renamed alias silently breaks any plugin that references the old string.
|
||||
- Removing support for a manifest `type` that plugins use
|
||||
- Changing manifest `alias` resolution or validation
|
||||
- Removing or renaming manifest `kind` types
|
||||
- Changing extension bundle structure
|
||||
|
||||
**How to detect:**
|
||||
|
||||
- **Alias renames**: Compare `alias:` values in manifest files before and after. Changed alias strings are Critical — the old alias should be preserved as a deprecated entry.
|
||||
- Search for changes to manifest type definitions
|
||||
- Check for removed or renamed manifest kinds
|
||||
|
||||
### Context API
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Removing context tokens from exports
|
||||
- Changing the shape of data provided by a context
|
||||
- Removing context provider/consumer mechanisms
|
||||
|
||||
**How to detect:**
|
||||
|
||||
- Check for removed context token exports
|
||||
- Check for changes to context provider classes
|
||||
|
||||
### Controllers/Lifecycle
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Changing controller base class inheritance requirements
|
||||
- Removing controller lifecycle hooks
|
||||
- Breaking cleanup mechanisms in `disconnectedCallback()`
|
||||
|
||||
### Observable/State
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Removing observable properties from the public API
|
||||
- Changing observable emission patterns
|
||||
|
||||
### npm Publishing
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
- Changing version constraints that exclude previously-supported versions
|
||||
- Adding incompatible peer dependency constraints
|
||||
|
||||
**How to detect:**
|
||||
|
||||
- Check if `package.json` `peerDependencies` or `dependencies` changed
|
||||
- Verify version ranges are not narrowed
|
||||
|
||||
---
|
||||
|
||||
## Reporting Breaking Changes
|
||||
|
||||
When a breaking change is detected, report:
|
||||
|
||||
1. **What**: The specific change and which public symbol is affected
|
||||
2. **Pattern**: Which mitigation pattern should be applied (Pattern 1, 2, or 3 for backend)
|
||||
3. **Severity**: Critical (no mitigation present) or Important (mitigation present but incorrect)
|
||||
4. **Fix**: Concrete code suggestion showing the correct pattern
|
||||
|
||||
If no breaking changes are detected, state: "No breaking changes detected."
|
||||
@@ -1,168 +0,0 @@
|
||||
# Coding Preferences & Review Criteria
|
||||
|
||||
These are the coding preferences and code review standards used by the review skill. They define what the review evaluates against.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
- **Always create blackbox tests** for new/changed code
|
||||
- Choose the appropriate test level:
|
||||
- **Unit tests** for isolated logic
|
||||
- **Integration tests** for application services/use cases
|
||||
- **E2E tests** for API endpoints
|
||||
|
||||
### Test Class Naming
|
||||
|
||||
- Test classes must be postfixed with `Tests` (e.g., `OrderServiceTests`)
|
||||
- One test class per class under test
|
||||
|
||||
### Test Method Naming
|
||||
|
||||
**C# tests**: Use the `Can_`/`Cannot_` pattern with PascalCase underscore-separated words:
|
||||
|
||||
- `Can_Schedule_Publish_Invariant`
|
||||
- `Cannot_Delete_Non_Existing`
|
||||
- `Can_Schedule_Publish_Single_Culture`
|
||||
|
||||
Large test classes are split into partial files by method: `ContentServiceTests.Delete.cs`, `ContentServiceTests.Publish.cs`.
|
||||
|
||||
**TypeScript tests**: Use BDD-style `it()` with natural language descriptions:
|
||||
|
||||
- `it('should not allow the returned value to be lower than min')`
|
||||
- `it('converts string to camelCase')`
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Optional, but must be blackbox tests so refactoring does not break tests
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Every use case / application service must have integration tests
|
||||
- Tests run against real database (containerized or similar)
|
||||
- Test the full flow from application layer through infrastructure
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Every API endpoint must have E2E tests
|
||||
- Test realistic scenarios including error cases
|
||||
|
||||
---
|
||||
|
||||
## Trade-offs
|
||||
|
||||
When making decisions, prioritize:
|
||||
|
||||
- **Readability** over cleverness
|
||||
- **Flexibility** over rigidity
|
||||
- Explain trade-offs when deviating from these defaults
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- Communicate breaking changes at the **OpenAPI/openapi.json level**
|
||||
- Clearly document what changed and the migration path
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Document all public or exported types** (classes, interfaces, types, methods, properties)
|
||||
- Keep documentation in sync with code changes
|
||||
- Add **JS Docs** on all public frontend APIs (classes, methods, properties)
|
||||
- Focus on "why" and usage, not restating the obvious
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Use what's available in the codebase, unless there is no good choice
|
||||
- **Flag new dependencies** for review — new packages should be justified
|
||||
- Prefer well-maintained, widely-used packages
|
||||
|
||||
---
|
||||
|
||||
## Error Messages & Logging
|
||||
|
||||
- **User-facing errors**: Clear, friendly, actionable
|
||||
- **Log messages**: Technical, detailed, with context
|
||||
- Include correlation IDs and relevant data in logs
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- **Always check for security issues** using OWASP Top 10 as baseline
|
||||
- Flag potential vulnerabilities immediately
|
||||
- Suggest secure alternatives when spotting risky patterns
|
||||
- Apply principle of least privilege
|
||||
|
||||
---
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer **immutability** by default
|
||||
- Allow internal properties to be mutated, as long as they are not direct references coming from the outside
|
||||
|
||||
---
|
||||
|
||||
## Nullability
|
||||
|
||||
- **TypeScript / JavaScript**
|
||||
- Prefer `undefined` for optional/omitted values (e.g., optional parameters, props, and fields)
|
||||
- Use `null` only when the domain model explicitly encodes "no value" or "not set" (e.g., `string | null` from APIs/DB), and be consistent with existing types
|
||||
- Avoid mixing `null` and `undefined` for the same concept within the same model or API surface
|
||||
|
||||
- **C#**
|
||||
- use nullable types (e.g., `string?`, `int?`) where absence is valid
|
||||
- Prefer domain modeling (value objects, options/results, empty collections) over `null` where appropriate, but respect existing conventions in the codebase
|
||||
|
||||
---
|
||||
|
||||
## C# Specific
|
||||
|
||||
- use Notification pattern (not C# events), Composer pattern (DI registration), Scoping with `Complete()`, Attempt pattern for operation results.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
- Follow **Clean Architecture** principles
|
||||
- **Fail-fast** principle: detect and report errors as early as possible
|
||||
- Within the established layered architecture (Core/Infrastructure/Web/API), organize code by feature inside each layer where practical, while preserving dependency direction
|
||||
- One class per file
|
||||
- Avoid N+1 queries
|
||||
- Profile before optimizing non-critical paths
|
||||
|
||||
### Type Hierarchy Consistency
|
||||
|
||||
When parallel model types have inconsistent relationships to a shared base type:
|
||||
|
||||
**TypeScript**: manipulations via `Omit`, `Pick`, intersection overrides, or workarounds like `as unknown as` / double-casts to bridge type mismatches.
|
||||
|
||||
**C#**: hiding base members with `new` to change types, explicit interface implementations to mask mismatches, or downcasting base return types in derived classes.
|
||||
|
||||
- **Do NOT suggest** the PR code should deviate from its base type to match a sibling that already deviates. Copying the deviation spreads the problem.
|
||||
- **Do flag** the architectural inconsistency: parallel models should share a compatible base contract. The model that manipulates or deviates from the base type is the one that needs attention — not the one that extends it correctly.
|
||||
- **Frame the suggestion** as: "These related models have inconsistent type hierarchies. `{deviating type}` manipulates the base contract of `{base type}`, which forces shared consumers like `{shared utility}` to require a shape that conforming subtypes can't satisfy."
|
||||
|
||||
---
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow standard naming conventions for the language (C# or JS/TS)
|
||||
- Keep components small and focused on a single responsibility
|
||||
- Prefer early returns
|
||||
- Small functions
|
||||
- No nested ternaries
|
||||
|
||||
---
|
||||
|
||||
## Severity Levels
|
||||
|
||||
| Severity | Meaning |
|
||||
|----------|---------|
|
||||
| **Critical** | Must fix before merge — security vulnerabilities, data loss risks, broken functionality |
|
||||
| **Important** | Should fix — performance issues, missing tests, architectural violations |
|
||||
| **Suggestion** | Nice to have — readability, minor refactoring, alternative approaches |
|
||||
@@ -1,33 +0,0 @@
|
||||
# PR Complexity Assessment
|
||||
|
||||
Evaluate whether the PR's scope suggests it should be split. This assessment is **informational only** — it never blocks or shortens the review.
|
||||
|
||||
## Always check: Formatting mixed with logic
|
||||
|
||||
This check applies to every PR regardless of size or scope.
|
||||
|
||||
Run both commands and compare per-file line counts:
|
||||
```bash
|
||||
git diff {target}...HEAD --stat
|
||||
git diff {target}...HEAD --stat --ignore-all-space
|
||||
```
|
||||
|
||||
For any file where the whitespace-ignored diff is less than **half** the full diff size (and the full diff is over 50 lines), that file has significant formatting changes mixed with logic. Flag it with a split suggestion: "File(s) {list} contain significant formatting changes mixed with logic. Consider a separate formatting-only commit or PR to keep the functional diff reviewable."
|
||||
|
||||
## Multi-project scope check
|
||||
|
||||
Skip this section entirely if ALL production files reside in a single project directory or if the PR is docs-only, test-only, dependency-bump-only, or rename-only.
|
||||
|
||||
Otherwise, flag any dimension that applies:
|
||||
|
||||
| Dimension | Condition | Suggestion |
|
||||
|---|---|---|
|
||||
| **Size** | 30+ files OR 1500+ lines, spanning 2+ projects | "If changes in {projectA} and {projectB} are independently functional, they could be separate PRs." |
|
||||
| **Layer spread** | 3+ layers touched (Core/Infrastructure/Web/API/Frontend), 10+ files | "Consider splitting by layer — e.g., Core+Infrastructure first, then API/Frontend consumers." |
|
||||
| **Mixed intent** | 2+ intent categories (new feature, bugfix, refactor, dependency update) with 15+ files or 3+ projects | "Consider extracting the {secondary intent} into a separate PR." |
|
||||
|
||||
Intent categories — detect from diff characteristics, not commit messages:
|
||||
- **New feature**: new files or new `public`/`export` declarations
|
||||
- **Bug fix**: small targeted edits, no new files (don't co-flag with new feature)
|
||||
- **Refactor**: file renames, symbols moved but logic unchanged
|
||||
- **Dependency update**: changes to `.csproj`, `Directory.Packages.props`, `package.json`
|
||||
@@ -1,23 +0,0 @@
|
||||
# GH CLI Setup Instructions
|
||||
|
||||
The GitHub CLI (`gh`) is required for this review skill to detect PR target branches.
|
||||
|
||||
## Installation
|
||||
|
||||
Install via Homebrew:
|
||||
|
||||
```
|
||||
brew install gh
|
||||
```
|
||||
|
||||
Or see https://cli.github.com/ for other installation methods.
|
||||
|
||||
## Authentication
|
||||
|
||||
After installing, authorize by running this in the terminal (use the `!` prefix in Claude Code):
|
||||
|
||||
```
|
||||
! gh auth login
|
||||
```
|
||||
|
||||
Follow the prompts to authenticate with your GitHub account.
|
||||
@@ -1,153 +0,0 @@
|
||||
# Impact Analysis Reference
|
||||
|
||||
This document describes how to perform impact analysis during PR review. The goal is to look beyond the diff to understand how changes affect consumers in other parts of the codebase.
|
||||
|
||||
---
|
||||
|
||||
## 1. Extract Changed Public Symbols
|
||||
|
||||
Scan the diff output for changes to public API surface:
|
||||
|
||||
### Backend (.NET)
|
||||
|
||||
Look for added, modified, or removed lines containing:
|
||||
|
||||
- `public class`, `public abstract class`, `public sealed class`
|
||||
- `public interface`
|
||||
- `public record`, `public struct`, `public enum`
|
||||
- `public` or `protected` methods, properties, fields
|
||||
- `public static` members
|
||||
- Constructor signatures on public types
|
||||
|
||||
### Frontend (TypeScript/Lit)
|
||||
|
||||
Look for changes to:
|
||||
|
||||
- `export class`, `export interface`, `export type`, `export enum`
|
||||
- `export function`, `export const`
|
||||
- `@property()` decorated fields on exported components
|
||||
- `@customElement()` registrations
|
||||
- Entries in `package.json` `exports` field
|
||||
|
||||
Collect a list of all changed public symbol names (type names, method names, property names).
|
||||
|
||||
---
|
||||
|
||||
## 2. Search for Consumers
|
||||
|
||||
For each changed public symbol, search the `src/` directory for usages **outside the changed file itself**.
|
||||
|
||||
### Grep Strategy
|
||||
|
||||
Use the Grep tool with these settings:
|
||||
|
||||
```
|
||||
pattern: {symbol name}
|
||||
path: src/
|
||||
output_mode: files_with_matches
|
||||
head_limit: 20
|
||||
```
|
||||
|
||||
Use `head_limit: 20` to avoid overwhelming results — if there are more than 20 consumers, note "20+ consumers found" and list the first 20.
|
||||
|
||||
### What to Search For
|
||||
|
||||
For each changed type/method, search for:
|
||||
|
||||
- **Type references**: class name, interface name (e.g., `IContentService`)
|
||||
- **Method calls**: method name in context (e.g., `\.GetById\(` for a method rename)
|
||||
- **Constructor usage**: `new TypeName(`
|
||||
- **DI registrations**: `.AddSingleton<IType, Type>`, `.AddScoped<`, `.AddTransient<`
|
||||
- **Notification handlers**: if a notification type changed, search for `INotificationHandler<NotificationTypeName>` and `INotificationAsyncHandler<NotificationTypeName>`
|
||||
- **Interface implementations**: if an interface changed, search for `: IInterfaceName` or `IInterfaceName,`
|
||||
|
||||
### Excluding the Changed File
|
||||
|
||||
When reporting consumers, exclude files that are part of the PR's changes (they're already being reviewed). The interesting consumers are those **outside** the PR that may be affected.
|
||||
|
||||
---
|
||||
|
||||
## 3. Check Dependency Flow Direction
|
||||
|
||||
The Umbraco architecture enforces strict unidirectional dependencies:
|
||||
|
||||
```
|
||||
Api.Management / Api.Delivery (depend on Api.Common)
|
||||
↓
|
||||
Api.Common (depends on Web.Common)
|
||||
↓
|
||||
Web.Common (depends on Infrastructure)
|
||||
↓
|
||||
Infrastructure (depends on Core)
|
||||
↓
|
||||
Core (no dependencies)
|
||||
```
|
||||
|
||||
### Layer Mapping
|
||||
|
||||
Map each changed file to its architectural layer:
|
||||
|
||||
| Path prefix | Layer |
|
||||
|---|---|
|
||||
| `src/Umbraco.Core/` | Core |
|
||||
| `src/Umbraco.Infrastructure/` | Infrastructure |
|
||||
| `src/Umbraco.PublishedCache.*` | Infrastructure |
|
||||
| `src/Umbraco.Examine.Lucene/` | Infrastructure |
|
||||
| `src/Umbraco.Cms.Persistence.*` | Infrastructure |
|
||||
| `src/Umbraco.Web.Common/` | Web |
|
||||
| `src/Umbraco.Web.UI/` | Web (Application) |
|
||||
| `src/Umbraco.Web.Website/` | Web |
|
||||
| `src/Umbraco.Cms.Api.Common/` | API |
|
||||
| `src/Umbraco.Cms.Api.Management/` | API |
|
||||
| `src/Umbraco.Cms.Api.Delivery/` | API |
|
||||
| `src/Umbraco.Web.UI.Client/` | Frontend |
|
||||
| `tests/` | Test |
|
||||
|
||||
### Violation Detection
|
||||
|
||||
Flag if a change introduces:
|
||||
|
||||
- **Core depending on Infrastructure**: Core file importing/referencing Infrastructure types
|
||||
- **Core depending on Web/API**: Core file importing/referencing Web or API types
|
||||
- **Infrastructure depending on Web/API**: Infrastructure file importing Web or API types
|
||||
- **Cross-API dependencies**: Management API depending on Delivery API or vice versa
|
||||
|
||||
### How to Check
|
||||
|
||||
1. For each changed file, identify its layer
|
||||
2. Read the file's `using` statements (C#) or `import` statements (TS)
|
||||
3. Check if any imports reference a higher layer
|
||||
4. Also check if new parameters or return types come from higher layers
|
||||
|
||||
---
|
||||
|
||||
## 4. Flag Cross-Project Risks
|
||||
|
||||
### High-Risk Patterns
|
||||
|
||||
These changes have high ripple potential:
|
||||
|
||||
- **Interface changes in Core** — all implementations in Infrastructure must be updated
|
||||
- **Notification type changes** — all handlers across the codebase are affected
|
||||
- **Base class changes** — all derived classes are affected
|
||||
- **Composer changes** — can affect DI container and runtime behavior globally
|
||||
- **Shared model/DTO changes** — can affect serialization, API contracts, and consumers
|
||||
|
||||
### What to Report
|
||||
|
||||
For each cross-project risk found, report:
|
||||
|
||||
1. **What changed**: The specific symbol and how it changed
|
||||
2. **Who is affected**: List of consuming files/projects found via Grep
|
||||
3. **Risk level**: Whether the consumers will break (compile error), behave differently (runtime), or are unaffected
|
||||
4. **Recommendation**: Whether the PR should include updates to affected consumers
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Notes
|
||||
|
||||
- Use `head_limit: 20` on all Grep searches to cap results
|
||||
- Only search for symbols that actually changed (not every symbol in the file)
|
||||
- For very common type names (e.g., `IScope`, `ILogger`), consider adding more context to the search pattern to reduce false positives
|
||||
- Skip impact analysis for test files — they don't have external consumers
|
||||
- Skip impact analysis for private/internal members — they can't have external consumers
|
||||
@@ -102,7 +102,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
# Modifier preferences
|
||||
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
|
||||
dotnet_style_require_accessibility_modifiers = always:warning
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
|
||||
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
|
||||
dotnet_style_readonly_field = true:warning
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
|
||||
UMBRACO_CLIENT_SECRET=1234567890
|
||||
UMBRACO_BASE_URL=https://localhost:44339
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
UMBRACO_INCLUDE_TOOL_COLLECTIONS=data-type,document-type,document,media-type,media
|
||||
@@ -46,7 +46,6 @@
|
||||
*.xml text=auto
|
||||
*.resx text=auto
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.sh eol=lf
|
||||
|
||||
*.csproj text=auto merge=union
|
||||
*.vbproj text=auto merge=union
|
||||
@@ -55,9 +54,3 @@
|
||||
*.sln text=auto eol=crlf merge=union
|
||||
|
||||
*.gitattributes text=auto
|
||||
|
||||
# Generated files - hidden by default in GitHub diffs
|
||||
src/Umbraco.Web.UI.Client/src/packages/core/backend-api/** linguist-generated
|
||||
src/Umbraco.Web.UI.Login/src/api/** linguist-generated
|
||||
templates/UmbracoExtension/Client/src/api/** linguist-generated
|
||||
src/Umbraco.Cms.Api.Management/OpenApi.json linguist-generated
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# Umbraco CMS Build
|
||||
|
||||
This guide will explain how you can build the Umbraco CMS from the source code. You will most likely want to do this if your are setting up a local development environment for contributing code updates to the project. You will need this in order to develop and test your fix or feature.
|
||||
|
||||
## Are you sure?
|
||||
|
||||
In order to use Umbraco as a CMS and build your website with it, you should not build it yourself. If you're reading this then you're trying to contribute to Umbraco or you're debugging a complex issue.
|
||||
|
||||
- Are you about to [create a pull request for Umbraco][contribution guidelines]?
|
||||
- Are you trying to get to the bottom of a problem in your existing Umbraco installation?
|
||||
- Are you about to [create a pull request for Umbraco][contribution guidelines]?
|
||||
- Are you trying to get to the bottom of a problem in your existing Umbraco installation?
|
||||
|
||||
If the answer is yes, please read on. Otherwise, make sure to head on over [to the releases page](https://releases.umbraco.com) and start using Umbraco CMS as intended.
|
||||
If the answer is yes, please read on. Otherwise, make sure to head on over [to the download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended.
|
||||
|
||||
## Table of contents
|
||||
|
||||
↖️ You can jump to any section by using the "table of contents" button (  ) above.
|
||||
|
||||
## Working with the Umbraco source code
|
||||
|
||||
## Debugging source locally
|
||||
|
||||
Did you read ["Are you sure"](#are-you-sure)?
|
||||
|
||||
@@ -23,115 +22,82 @@ Did you read ["Are you sure"](#are-you-sure)?
|
||||
|
||||
If you want to run a build without debugging, see [Building from source](#building-from-source) below. This runs the build in the same way it is run on our build servers.
|
||||
|
||||
If you've got this far and are keen to get stuck in helping us fix a bug or implement a feature, great! Please read on...
|
||||
#### Debugging with VS Code
|
||||
|
||||
### Prerequisites
|
||||
In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed.
|
||||
|
||||
In order to work with the Umbraco source code locally, first make sure you have the following installed.
|
||||
* [Visual Studio Code](https://code.visualstudio.com/)
|
||||
* [dotnet SDK v7+](https://dotnet.microsoft.com/en-us/download)
|
||||
* [Node.js v14+](https://nodejs.org/en/download/)
|
||||
* npm v7+ (installed with Node.js)
|
||||
* [Git command line](https://git-scm.com/download/)
|
||||
|
||||
- Your favourite IDE: [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/), [Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- [dotnet SDK v9+](https://dotnet.microsoft.com/en-us/download)
|
||||
- [Node.js v20+](https://nodejs.org/en/download/)
|
||||
- npm v10+ (installed with Node.js)
|
||||
- [Git command line](https://git-scm.com/download/)
|
||||
Open the root folder of the repository in Visual Studio Code.
|
||||
|
||||
### Familiarizing yourself with the code
|
||||
To build the front end you'll need to open the command pallet (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd>) and run `>Tasks: Run Task` followed by `Client Watch` and then run the `Client Build` task in the same way.
|
||||
|
||||
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
|
||||
|
||||
There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`.
|
||||
|
||||
There are a few different ways to work locally when implementing features or fixing issues with the Umbraco CMS. Depending on whether you are working solely on the front-end, solely on the back-end, or somewhere in between, you may find different workflows work best for you.
|
||||
|
||||
Here are some suggestions based on how we work on developing Umbraco at HQ.
|
||||
|
||||
### First checkout
|
||||
|
||||
When you first clone the source code, build the whole solution via your IDE. You can then start the `Umbraco.Web.UI` project via the IDE or the command line and should find everything across front and back-end is built and running.
|
||||
You can also run the tasks manually on the command line:
|
||||
|
||||
```
|
||||
cd <solution root>\src\Umbraco.Web.UI
|
||||
dotnet run --no-build
|
||||
cd src\Umbraco.Web.UI.Client
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
When the page loads in your web browser, you can follow the installer to set up a database for debugging. When complete, you will have an empty Umbraco installation to begin working with. You may also wish to install a [starter kit][https://marketplace.umbraco.com/category/themes-&-starter-kits] to ease your debugging.
|
||||
If you just want to build the UI Client to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
### Back-end only changes
|
||||
|
||||
If you are working on back-end only features, when switching branches or pulling down the latest from GitHub, you will find the front-end getting rebuilt periodically when you look to build the back-end changes. This can take a while and slow you down. So if for a period of time you don't care about changes in the front-end, you can disable this build step.
|
||||
|
||||
Go to `Umbraco.Cms.StaticAssets.csproj` and comment out the following lines of MsBuild by adding a REM statement in front:
|
||||
The login screen is a different frontend build, for that one you can run it as follows:
|
||||
|
||||
```
|
||||
REM npm ci --no-fund --no-audit --prefer-offline
|
||||
REM npm run build:for:cms
|
||||
cd src\Umbraco.Web.UI.Login
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Just be careful not to include this change in your PR.
|
||||
If you just want to build the Login screen to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
### Front-end only changes
|
||||
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`:
|
||||
You might run into [Gulp quirks](#gulp-quirks).
|
||||
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
|
||||
|
||||
> [!NOTE]
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
|
||||
|
||||
Then run Umbraco from the command line.
|
||||
To run the C# portion of the project, either hit <kbd>F5</kbd> to begin debugging, or manually using the command line:
|
||||
|
||||
```
|
||||
cd <solution root>\src\Umbraco.Web.UI
|
||||
dotnet run --no-build
|
||||
dotnet watch --project .\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj
|
||||
```
|
||||
|
||||
In another terminal window, run the following to watch the front-end changes and launch Umbraco using the URL indicated from this task.
|
||||
**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
```
|
||||
cd <solution root>\src\Umbraco.Web.UI.Client
|
||||
npm run dev:server
|
||||
```
|
||||
When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging.
|
||||
|
||||
You'll find as you make changes to the front-end files, the updates will be picked up and your browser refreshed automatically.
|
||||
#### Debugging with Visual Studio
|
||||
|
||||
> [!NOTE]
|
||||
> The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
|
||||
In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed.
|
||||
|
||||
Whilst most of the backoffice code lives in `Umbraco.Web.UI.Client`, the login screen is in a separate project. If you do any work with that you can build with:
|
||||
* [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
|
||||
* [Node.js v14+](https://nodejs.org/en/download/)
|
||||
* npm v7+ (installed with Node.js)
|
||||
* [Git command line](https://git-scm.com/download/)
|
||||
|
||||
```
|
||||
cd <solution root>\src\Umbraco.Web.UI.Login
|
||||
npm run build
|
||||
```
|
||||
The easiest way to get started is to open `umbraco.sln` in Visual Studio.
|
||||
|
||||
In both front-end projects, if you've refreshed your branch from the latest on GitHub you may need to update front-end dependencies.
|
||||
To build the front end, you'll first need to run `cd src\Umbraco.Web.UI.Client && npm install` in the command line (or `cd src\Umbraco.Web.UI.Client; npm install` in PowerShell). Then find the Task Runner Explorer (View → Other Windows → Task Runner Explorer) and run the `build` task under `Gulpfile.js`. You may need to refresh the Task Runner Explorer before the tasks load.
|
||||
|
||||
To do that, run:
|
||||
If you're working on the backoffice, you may wish to run the `dev` command instead while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes.
|
||||
|
||||
```
|
||||
npm ci --no-fund --no-audit --prefer-offline
|
||||
```
|
||||
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
### Full-stack changes
|
||||
You might run into [Gulp quirks](#gulp-quirks).
|
||||
|
||||
If working across both front and back-end, follow both methods and use `dotnet watch`, or re-run `dotnet run` (or `dotnet build` followed by `dotnet run --no-build`) whenever you need to update the back-end code.
|
||||
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
|
||||
|
||||
Request and response models used by the management APIs are made available client-side as generated code. If you make changes to the management API, you can re-generate the typed client code with:
|
||||
"The rest" is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit <kbd>F5</kbd> to see the result.
|
||||
|
||||
```
|
||||
cd <solution root>\src\Umbraco.Web.UI.Client
|
||||
npm run generate:server-api-dev
|
||||
```
|
||||
**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
Please also update the `OpenApi.json` file held in the solution by copying and pasting the output from `/umbraco/swagger/management/swagger.json`.
|
||||
When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging.
|
||||
|
||||
## Building from source
|
||||
|
||||
@@ -139,7 +105,7 @@ Did you read ["Are you sure"](#are-you-sure)?
|
||||
|
||||
Do note that this is only required if you want to test out your custom changes in a separate site (not the one in the Umbraco.Web.UI), if you just want to test your changes you can run the included test site using: `dotnet run` from `src/Umbraco.Web.UI/`
|
||||
|
||||
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
|
||||
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
|
||||
|
||||
First enter the root of the project in a command line environment, and then use the following command to build the NuGet packages:
|
||||
|
||||
@@ -151,18 +117,17 @@ You can then add these as a local NuGet feed using the following command:
|
||||
|
||||
`dotnet nuget add source <Path to Build.Out folder> -n MyLocalFeed`
|
||||
|
||||
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
|
||||
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
|
||||
|
||||
### Cleaning up
|
||||
|
||||
Once the solution has been used to run a site, one may want to "reset" the solution in order to run a fresh new site again.
|
||||
|
||||
The easiest way to do this by deleting the following files and folders:
|
||||
* src/Umbraco.Web.UI/appsettings.json
|
||||
* src/Umbraco.Web.UI/umbraco/Data
|
||||
|
||||
- src/Umbraco.Web.UI/appsettings.json
|
||||
- src/Umbraco.Web.UI/umbraco/Data
|
||||
|
||||
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
|
||||
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
|
||||
|
||||
Next time you run a build the `appsettings.json` file will be re-created in its default state.
|
||||
|
||||
@@ -170,14 +135,13 @@ This will leave media files and views around, but in most cases, it will be enou
|
||||
|
||||
To perform a more complete clear, you will want to also delete the content of the media, views, scripts... directories.
|
||||
|
||||
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
|
||||
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
|
||||
|
||||
git clean -xdf .
|
||||
|
||||
For git documentation see:
|
||||
|
||||
- git [clean](https://git-scm.com/docs/git-clean)
|
||||
- git [reset](https://git-scm.com/docs/git-reset)
|
||||
* git [clean](<https://git-scm.com/docs/git-clean>)
|
||||
* git [reset](<https://git-scm.com/docs/git-reset>)
|
||||
|
||||
## Azure DevOps
|
||||
|
||||
@@ -191,5 +155,18 @@ The produced artifacts are published in a container that can be downloaded from
|
||||
|
||||
Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
|
||||
|
||||
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
|
||||
### Gulp Quirks
|
||||
|
||||
You may need to run the following commands to set up gulp properly:
|
||||
|
||||
```
|
||||
npm cache clean --force
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
|
||||
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
|
||||
[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our "
|
||||
[ disable browser caching ]: https://techwiser.com/disable-cache-google-chrome-firefox "Instructions on how to disable browser caching in Chrome and Firefox"
|
||||
|
||||
@@ -1,67 +1,58 @@
|
||||
# Contributing to Umbraco CMS
|
||||
|
||||
👍🎉 First of all, thanks for taking the time to contribute! 🎉👍
|
||||
👍🎉 First off, thanks for taking the time to contribute! 🎉👍
|
||||
|
||||
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## Getting Started
|
||||
We have a guide on [what to consider before you start](contributing-before-you-start.md) and more detailed guides at the end of this article.
|
||||
|
||||
## Contribution guide
|
||||
|
||||
This guide describes each step to make your first contribution:
|
||||
The following steps are a quick-start guide:
|
||||
|
||||
1. **Fork**
|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
|
||||
|
||||

|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
|
||||
|
||||

|
||||
|
||||
2. **Clone**
|
||||
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
|
||||
|
||||

|
||||
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
|
||||
|
||||

|
||||
|
||||
3. **Switch to the correct branch**
|
||||
|
||||
Switch to the `main` branch
|
||||
Switch to the `contrib` branch
|
||||
|
||||
4. **Branch out**
|
||||
4. **Build**
|
||||
|
||||
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
|
||||
Build your fork of Umbraco locally [as described in the build documentation](BUILD.md), you can build with any IDE that supports dotnet or the command line.
|
||||
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task|qa|improvement}/{issue}-{description}`.
|
||||
5. **Branch**
|
||||
|
||||
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
|
||||
|
||||
Don't commit to `main`, create a new branch first.
|
||||
|
||||
5. **Build or run a Development Server**
|
||||
|
||||
You can build or run a Development Server with any IDE that supports .NET or the command line.
|
||||
|
||||
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp/12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
|
||||
|
||||
6. **Change**
|
||||
|
||||
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
|
||||
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
|
||||
|
||||
7. **Commit and push**
|
||||
|
||||
Done? Yay! 🎉
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your branch. When it's ready, push the changes to your fork on GitHub.
|
||||
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
|
||||
8. **Create pull request**
|
||||
|
||||
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`), you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instructions.
|
||||
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`) you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instuctions.
|
||||
|
||||
Would you like to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
|
||||
Want to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
|
||||
|
||||
## Further contribution guides
|
||||
|
||||
- [Before you start](contributing-before-you-start.md)
|
||||
- [Finding your first issue](contributing-first-issue.md)
|
||||
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
|
||||
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
|
||||
- [Unwanted changes](contributing-unwanted-changes.md)
|
||||
- [Other ways to contribute](contributing-other-ways-to-contribute.md)
|
||||
|
||||
@@ -6,8 +6,8 @@ body:
|
||||
- type: input
|
||||
id: "version"
|
||||
attributes:
|
||||
label: "Which Umbraco version are you using?"
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Click the Umbraco logo in the top left corner of the backoffice to find the version you're using."
|
||||
label: "Which Umbraco version are you using? (Please write the *exact* version, example: 10.1.0)"
|
||||
description: "Use the help icon in the Umbraco backoffice to find the version you're using"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -4,11 +4,11 @@ contact_links:
|
||||
url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas
|
||||
about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans
|
||||
- name: ⁉️ Support Question
|
||||
url: https://forum.umbraco.com
|
||||
url: https://our.umbraco.com
|
||||
about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum.
|
||||
- name: 📖 Documentation Issue
|
||||
url: https://github.com/umbraco/UmbracoDocs/issues
|
||||
about: Documentation issues should be reported on the Umbraco documentation repository.
|
||||
- name: 🔐 Security Issue
|
||||
url: https://umbraco.com/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
|
||||
url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
|
||||
about: Discovered a Security Issue in Umbraco?
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# New backoffice
|
||||
|
||||
> **Warning**:
|
||||
> This is an early WIP and is set not to be packable since we don't want to release this yet. There will be breaking changes in these projects.
|
||||
|
||||
This solution folder contains the projects for the new backoffice. If you're looking to fix or improve the existing CMS, this is not the place to do it, although we do very much appreciate your efforts.
|
||||
|
||||
### Project structure
|
||||
|
||||
Since the new backoffice API is still very much a work in progress, we've created new projects for the new backoffice API:
|
||||
|
||||
* Umbrao.Cms.ManagementApi - The "presentation layer" for the management API
|
||||
* "New" versions of existing projects, should be merged with the existing projects when the new API is released:
|
||||
* Umbraco.New.Cms.Core
|
||||
* Umbraco.New.Cms.Infrastructure
|
||||
* Umbraco.New.Cms.Web.Common
|
||||
|
||||
This also means that we have to use "InternalsVisibleTo" for the new projects since these should be able to access the internal classes since they will when they get merged.
|
||||
@@ -1,63 +1,48 @@
|
||||
# [Umbraco CMS](https://umbraco.com)
|
||||
|
||||
[](../LICENSE.md)
|
||||
[](https://www.nuget.org/packages/Umbraco.Cms)
|
||||
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
|
||||
[](CONTRIBUTING.md)
|
||||
[](https://forum.umbraco.com)
|
||||
[](https://twitter.com/intent/follow?screen_name=umbraco)
|
||||
[](https://discord.gg/umbraco)
|
||||

|
||||
[](https://discord-chats.umbraco.com)
|
||||
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
|
||||
|
||||
|
||||
### Umbraco is a free and open source .NET content management system. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
|
||||
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
|
||||
|
||||
Learn more at [umbraco.com](https://umbraco.com)
|
||||
|
||||
<p align="center">
|
||||
<img src="img/logo.png" alt="Umbraco Logo" />
|
||||
<img src="img/logo.png" alt="Umbraco Logo" />
|
||||
</p>
|
||||
|
||||
## <a name="install"></a>Looking to install Umbraco?
|
||||
See the official [Umbraco website](https://umbraco.com) for an introduction, core mission and values of the product and team behind it.
|
||||
|
||||
You can get started using the following commands on Windows, Linux and MacOS (after installing the [.NET Runtime and SDK](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/requirements)):
|
||||
- [Getting Started](#getting-started)
|
||||
- [Documentation](#documentation)
|
||||
- [Community](#join-the-umbraco-community)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
```
|
||||
dotnet new install Umbraco.Templates
|
||||
dotnet new umbraco --name MyProject
|
||||
cd MyProject
|
||||
dotnet run
|
||||
```
|
||||
Please also see our [Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Getting Started
|
||||
|
||||
[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed.
|
||||
|
||||
If you want to DIY, then you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you.
|
||||
|
||||
## Documentation
|
||||
|
||||
Our [comprehensive documentation](https://docs.umbraco.com/umbraco-cms) takes you from the fundamentals on how to start with Umbraco to deploying it to production.
|
||||
The documentation for Umbraco CMS can be found [on Our Umbraco](https://docs.umbraco.com/). The source for the Umbraco docs is [open source as well](https://github.com/umbraco/UmbracoDocs) and we're happy to look at your documentation contributions.
|
||||
|
||||
Some important documentation links to get you started:
|
||||
## Join the Umbraco community
|
||||
|
||||
- [Installing Umbraco CMS](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/install)
|
||||
- [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco)
|
||||
- [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview)
|
||||
Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
|
||||
|
||||
## Backoffice Preview
|
||||
Besides "Our", we all support each other in our [Community Discord Server](https://discord.gg/umbraco) and on Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
|
||||
|
||||
Want to see the latest backoffice UI in action? Check out our live preview:
|
||||
|
||||
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
|
||||
|
||||
This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements. It runs from mock data and persistent edits are not supported.
|
||||
|
||||
## Get help
|
||||
|
||||
If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help.
|
||||
|
||||
## Looking to contribute back to Umbraco?
|
||||
|
||||
You came to the right place! Our GitHub repository is available for all kinds of contributions:
|
||||
|
||||
- [Create a bug report](https://github.com/umbraco/Umbraco-CMS/issues)
|
||||
- [Create a feature request](https://github.com/umbraco/Umbraco-CMS/discussions)
|
||||
## Contributing
|
||||
|
||||
Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).
|
||||
|
||||
### Tip: You should not run Umbraco from source code found here. Umbraco is extremely extensible and can do whatever you need. Instead, [install Umbraco as noted above](#looking-to-install-umbraco) and then [extend it any way you want to](https://docs.umbraco.com/umbraco-cms/extending/).
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Bellissima release instructions
|
||||
|
||||
## Build
|
||||
|
||||
> _See internal documentation on the build/release workflow._
|
||||
|
||||
## GitHub Release Notes
|
||||
|
||||
To generate release notes on GitHub.
|
||||
|
||||
- Go to the [**Releases** area](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases)
|
||||
- Press the [**"Draft a new release"** button](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases/new)
|
||||
- In the combobox for "Choose a tag", expand then select or enter the next version number, e.g. `release-14.2.0`
|
||||
- If the tag does not already exist, an option labelled "Create new tag: release-14.2.0 on publish" will appear, select that option
|
||||
- In the combobox for "Target: main", expand then select the release branch for the next version, e.g. `release/14.2`
|
||||
- In the combobox for "Previous tag: auto":
|
||||
- If the next release is an RC, then you can leave as `auto`
|
||||
- Otherwise, select the previous stable version, e.g. `release-14.1.1`
|
||||
- Press the **"Generate release notes"** button, this will populate the main textarea
|
||||
- Change the title to match the version, e.g. `14.2.0`
|
||||
- Check the details, view in the "Preview" tab
|
||||
- What type of release is this?
|
||||
- If it's an RC, then check "Set as a pre-release"
|
||||
- If it's stable, then check "Set as the latest release"
|
||||
- Once you're happy with the contents and ready to save...
|
||||
- If you need more time to review, press the **"Save draft"** button and you can come back to it later
|
||||
- If you are ready to make the release notes public, then press **"Publish release"** button! :tada:
|
||||
|
||||
> If you're curious about how the content is generated, take a look at the `release.yml` configuration:
|
||||
> https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/release.yml
|
||||
@@ -1,230 +0,0 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
## Thoughts, links, and questions
|
||||
|
||||
In the high probability that you are porting something from angular JS then here are a few helpful tips for using Lit:
|
||||
|
||||
Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
|
||||
|
||||
### What is the process of contribution?
|
||||
|
||||
- Read the [README](README.md) to learn how to get the project up and running
|
||||
- Find an issue marked as [community/up-for-grabs](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs) - note that some are also marked [good first issue](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which indicates they are simple to get started on
|
||||
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise if no API is required
|
||||
- A contribution should be made in a fork of the repository
|
||||
- Once a contribution is ready, a pull request should be made to this repository and HQ will assign a reviewer
|
||||
- A pull request should always indicate what part of a feature it tries to solve, i.e. does it close the targeted issue (if any) or does the developer expect Umbraco HQ to take over
|
||||
|
||||
## Contributing in general terms
|
||||
|
||||
A lot of the UI has already been migrated to the new backoffice. Generally speaking, one would find a feature on the projects board, locate the UI in the old backoffice (v11 is fine), convert it to Lit components using the UI library, put the business logic into a store/service, write tests, and make a pull request.
|
||||
|
||||
We are also very keen to receive contributions towards **documentation, unit testing, package development, accessibility, and just general testing of the UI.**
|
||||
|
||||
## The Management API
|
||||
|
||||
The management API is the colloquial term used to describe the new backoffice API. It is built as a .NET Web API, has a Swagger endpoint (/umbraco/swagger), and outputs an OpenAPI v3 schema, that the frontend consumes.
|
||||
|
||||
The frontend has an API formatter that takes the OpenAPI schema file and converts it into a set of TypeScript classes and interfaces.
|
||||
|
||||
**Current schema for API:**
|
||||
|
||||
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v15/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
|
||||
|
||||
**How to convert it:**
|
||||
|
||||
- Run `npm run generate:server-api`
|
||||
|
||||
## A contribution example
|
||||
|
||||
### Example: Published Cache Status Dashboard
|
||||
|
||||

|
||||
|
||||
### Boilerplate (example using Lit)
|
||||
|
||||
Links for Lit examples and documentation:
|
||||
|
||||
- [https://lit.dev](https://lit.dev)
|
||||
- [https://lit.dev/docs/](https://lit.dev/docs/)
|
||||
- [https://lit.dev/playground/](https://lit.dev/playground/)
|
||||
|
||||
### Functionality
|
||||
|
||||
**HTML**
|
||||
|
||||
The simplest approach is to copy over the HTML from the old backoffice into a new Lit element (check existing elements in the repository, e.g. if you are working with a dashboard, then check other dashboards, etc.). Once the HTML is inside the `render` method, it is often enough to simply replace `<umb-***>` elements with `<uui-***>` and replace a few of the attributes. In general, we try to build as much UI with Umbraco UI Library as possible.
|
||||
|
||||
**Controller**
|
||||
|
||||
The old AngularJS controllers will have to be converted into modern TypeScript and will have to use our new services and stores. We try to abstract as much away as possible, and mostly you will have to make API calls and let the rest of the system handle things like error handling and so on. In the case of this dashboard, we only have a few GET and POST requests. Looking at the new Management API, we find the PublishedCacheService, which is the new API controller to serve data to the dashboard.
|
||||
|
||||
To make the first button work, which simply just requests a new status from the server, we must make a call to `PublishedCacheService.getPublishedCacheStatus()`. An additional thing here is to wrap that in a friendly function called `tryExecuteAndNotify`, which is something we make available to developers to automatically handle the responses coming from the server and additionally use the Notifications to notify of any errors:
|
||||
|
||||
```typescript
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { PublishedCacheService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
private _getStatus() {
|
||||
const { data: status } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheStatus());
|
||||
|
||||
if (status) {
|
||||
// we now have the status
|
||||
console.log(status);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State (buttons, etc)
|
||||
|
||||
It is a good idea to make buttons indicate a loading state when awaiting an API call. All `<uui-button>` support the `.state` property, which you can set around API calls:
|
||||
|
||||
```typescript
|
||||
@state()
|
||||
private _buttonState: UUIButtonState = undefined;
|
||||
|
||||
private _getStatus() {
|
||||
this._buttonState = 'waiting';
|
||||
|
||||
[...await...]
|
||||
|
||||
this._buttonState = 'success';
|
||||
}
|
||||
```
|
||||
|
||||
## Making the dashboard visible
|
||||
|
||||
### Add to internal manifests
|
||||
|
||||
All items are declared in a `manifests.ts` file, which is located in each section directory.
|
||||
|
||||
To declare the Published Cache Status Dashboard as a new manifest, we need to add the section as a new json object that would look like this:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.PublishedStatus',
|
||||
name: 'Published Status Dashboard',
|
||||
elementName: 'umb-dashboard-published-status',
|
||||
element: () => import('./published-status/dashboard-published-status.element.js'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: 'Published Status',
|
||||
pathname: 'published-status',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_SECTION_ALIAS_CONDITION_ALIAS,
|
||||
match: 'Umb.Section.Settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
Let’s go through each of these properties…
|
||||
|
||||
- Type: can be one of the following:
|
||||
|
||||
- section - examples include: `Content`, `Media`
|
||||
- dashboard - a view within a section. Examples include: the welcome dashboard
|
||||
- propertyEditorUi
|
||||
- editorView
|
||||
- propertyAction
|
||||
- tree
|
||||
- editor
|
||||
- treeItemAction
|
||||
|
||||
- Alias: is the unique key used to identify this item.
|
||||
- Name: is the human-readable name for this item.
|
||||
|
||||
- ElementName: this is the customElementName declared on the element at the top of the file i.e
|
||||
|
||||
```typescript
|
||||
@customElement('umb-dashboard-published-status')
|
||||
```
|
||||
|
||||
- Js: references a function call to import the file that the element is declared within
|
||||
|
||||
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
|
||||
|
||||
- Meta: allows us to reference additional data - in our case, we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the URL
|
||||
|
||||
- Conditions: allows us to specify the conditions that must be met for the dashboard to be displayed. In our case, we are specifying that the dashboard will only be displayed within the Settings section
|
||||
|
||||
## API mock handlers
|
||||
|
||||
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. To hit the API, we need to add a mock handler to define the endpoints that our dashboard will call. In the case of the Published Cache Status section, we have several calls to work through. Let’s start by looking at the call to retrieve the current status of the cache:
|
||||
|
||||

|
||||
|
||||
From the existing functionality, we can see that this is a string message that is received as part of a `GET` request from the server.
|
||||
|
||||
So to define this, we must first add a handler for the Published Status called `published-status.handlers.ts` within the mocks/domains folder. In this file we will have code that looks like the following:
|
||||
|
||||
```typescript
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbracoPath } from "@umbraco-cms/backoffice/utils";
|
||||
|
||||
export const handlers = [
|
||||
rest.get(umbracoPath("/published-cache/status"), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<string>(
|
||||
"Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot."
|
||||
)
|
||||
);
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
||||
This is defining the `GET` path that we will call through the resource: `/published-cache/status`
|
||||
|
||||
It returns a `200 OK` response and a string value with the current “status” of the published cache for us to use within the element
|
||||
|
||||
An example `POST` is similar. Let’s take the “Refresh status” button as an example:
|
||||
|
||||

|
||||
|
||||
From our existing functionality, we can see that this makes a `POST` call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
|
||||
|
||||
```typescript
|
||||
rest.post(umbracoPath('/published-cache/reload'), async (_req, res, ctx) => {
|
||||
return res(
|
||||
// Simulate a 1 second delay for the benefit of the UI
|
||||
ctx.delay(1000)
|
||||
// Respond with a 201 status code
|
||||
ctx.status(201)
|
||||
);
|
||||
})
|
||||
```
|
||||
|
||||
Which is defining a new `POST` endpoint that we can add to the core API fetcher using the path `/published-cache/reload`.
|
||||
|
||||
This call returns a simple `OK` status code and no other object.
|
||||
|
||||
## Storybook stories
|
||||
|
||||
We try to make good Storybook stories for new components, which is a nice way to work with a component in an isolated state. Imagine you are working with a dialog on page 3 and have to navigate back to that every time you make a change - this is now eliminated with Storybook as you can just make a story that displays that step. Storybook can only show one component at a time, so it also helps us to isolate view logic into more and smaller components, which in turn are more testable.
|
||||
|
||||
In-depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
|
||||
|
||||
Reference: [https://ambitious-stone-0033b3603.1.azurestaticapps.net/](https://ambitious-stone-0033b3603.1.azurestaticapps.net/)
|
||||
|
||||
- Locally: `npm run storybook`
|
||||
|
||||
For Umbraco UI stories, please navigate to [https://uui.umbraco.com/](https://uui.umbraco.com/)
|
||||
|
||||
## Testing
|
||||
|
||||
There are two testing tools on the backoffice: unit testing and end-to-end testing.
|
||||
|
||||
### Unit testing
|
||||
|
||||
We are using a tool called Web Test Runner which spins up a bunch of browsers using Playwright with the well-known jasmine/chai syntax. It is expected that any new component/element has a test file named “<component>.test.ts”. It will automatically be picked up and there are a set of standard tests we apply to all components, which checks that they are registered correctly and they pass accessibility testing through Axe.
|
||||
|
||||
Working with playwright: [https://playwright.dev/docs/intro](https://playwright.dev/docs/intro)
|
||||
|
||||
## Putting it all together
|
||||
|
||||
When we are finished with the dashboard we will hopefully have something akin to this [real-world example of the actual dashboard that was migrated](https://github.com/umbraco/Umbraco.CMS.Backoffice/tree/main/src/backoffice/settings/dashboards/published-status).
|
||||
@@ -7,26 +7,9 @@ We recommend you to [sync with our repository][sync fork] before you submit your
|
||||
GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
|
||||

|
||||
|
||||
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `main`. This is the branch you should be targeting.
|
||||
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `contrib`. This is the branch you should be targeting.
|
||||
|
||||
We welcome PRs for features and bugfixes for different versions according to the [published support and EOL schedule][support-and-eol].
|
||||
|
||||
We don't have rules for naming PRs - so name them as you prefer. At HQ we do have a best practice on clear and concise PR naming, so if you would like to use the format feel free to do so.
|
||||
|
||||
Our convention of doing it is:
|
||||
|
||||
_Area: Description (closes #IssueID)_
|
||||
|
||||
1. Start by specifying the area. Fx the feature name(UFM, Tiptap etc.) or specific section (migrations, relations, segmentation).
|
||||
|
||||
2. In your description, where applicable, mention type of PR (Build, Bump, Fix, Refactor etc.).
|
||||
|
||||
4. Good practise is to make sure you describe specifically the change and/or impact of change.<br>
|
||||
Example: Writing "Extension Insights: Fixes CSS alignment" instead of "Fixed issue".
|
||||
|
||||
6. Add (closes #IssueID) behind description, if your PR resolves an issue.
|
||||
|
||||
That's it!
|
||||
Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
|
||||
|
||||
## The review process
|
||||
[review process]: #the-review-process
|
||||
@@ -65,5 +48,4 @@ There will be times that we really like your proposed changes and we’ll finish
|
||||
|
||||
[making larger changes]: contributing-before-you-start.md#making-large-changes
|
||||
[pr or package]: contributing-before-you-start.md#pull-request-or-package
|
||||
[Core collabs]: contributing-core-collabs-team.md
|
||||
[support-and-eol]: https://umbraco.com/products/knowledge-center/long-term-support-and-end-of-life/
|
||||
[Core collabs]: contributing-core-collabs-team.md
|
||||
@@ -1,8 +1,6 @@
|
||||
## Finding your first issue
|
||||
## Finding your first issue: Up for grabs
|
||||
|
||||
Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. In adding the label we will endeavour to provide some guidelines on how to go about the implementation, such that it aligns with the project. We encourage anyone to pick them up and help out.
|
||||
|
||||
You don't need to restrict yourselves to issues that are specifically marked as "up for grabs" though. If you are running into a bug you have reported or found on the [issue tracker][issue tracker], it's not necessary to wait for HQ response. Feel free to dive in and try to provide a fix, raising questions as you need if you have concerns about the modifications necessary to resolve the problem.
|
||||
Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out.
|
||||
|
||||
If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
|
||||
|
||||
@@ -13,18 +11,18 @@ Great question! The short version goes like this:
|
||||
1. **Fork**
|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo]
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
1. **Clone**
|
||||
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
1. **Switch to the correct branch**
|
||||
|
||||
Switch to the `main` branch
|
||||
Switch to the `contrib` branch
|
||||
|
||||
1. **Build**
|
||||
|
||||
@@ -32,7 +30,7 @@ Great question! The short version goes like this:
|
||||
|
||||
1. **Branch**
|
||||
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `main`, create a new branch first.
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
|
||||
|
||||
1. **Change**
|
||||
|
||||
@@ -42,7 +40,7 @@ Great question! The short version goes like this:
|
||||
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your new `temp` branch, and don't commit to `main`. Then you can push the changes up to your fork on GitHub.
|
||||
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
|
||||
#### Keeping your Umbraco fork in sync with the main repository
|
||||
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
|
||||
@@ -59,10 +57,10 @@ Then when you want to get the changes from the main repository:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
git rebase upstream/contrib
|
||||
```
|
||||
|
||||
In this command we're syncing with the `main` branch, but you can of course choose another one if needed.
|
||||
In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
|
||||
|
||||
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
|
||||
|
||||
@@ -79,7 +77,7 @@ You can get in touch with [the core contributors team][core collabs] in multiple
|
||||
|
||||
- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward.
|
||||
- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the [forum][forum]. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
|
||||
|
||||
<!-- Local -->
|
||||
@@ -90,7 +88,6 @@ You can get in touch with [the core contributors team][core collabs] in multiple
|
||||
|
||||
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
|
||||
[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
|
||||
[forum]: https://forum.umbraco.com/
|
||||
[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
|
||||
[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
|
||||
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
|
||||
[issue tracker]: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
|
||||
@@ -1,223 +0,0 @@
|
||||
# **Contributing to Localization in the Backoffice**
|
||||
|
||||
Do you want to help keep our translations accurate and up to standard? 🌍✨
|
||||
|
||||
Your input makes a real difference! By reviewing, refining, or suggesting improvements, you ensure that our translations remain clear, consistent, and user-friendly for everyone.
|
||||
|
||||
|
||||
## **How Can I Contribute?**
|
||||
|
||||
To contribute to localization in the Backoffice, follow this step-by-step guide:
|
||||
|
||||
|
||||
### **1. Change the Language in Backoffice**
|
||||
|
||||
|
||||
|
||||
1. Open the Backoffice, click on your profile icon in the top-right corner, and select "Edit."
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
2. Under "UI Culture," select the language you want to review from the dropdown menu.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### **2. Find a Translation Error**
|
||||
|
||||
|
||||
|
||||
1. Navigate through the Backoffice and check if everything is translated correctly.
|
||||
|
||||
2. When you find a translation error, right-click on it and select "Inspect."
|
||||
|
||||
3. Look for the nearest element that starts with `umb-` and has a name indicating something specific to the given location.
|
||||
|
||||
**Example:**
|
||||
|
||||
* The closest parent element should be specific, such as `umb-document-type-workspace-view-settings` instead of a generic element like `umb-property-layout.`
|
||||
|
||||
|
||||
### **3. Find the Code in VS Code**
|
||||
|
||||
|
||||
|
||||
1. Open VS Code and search for the nearest `umb-` element you identified.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
2. Scroll down to find `render() {` and look for the element label that needs updating.
|
||||
|
||||
|
||||

|
||||
|
||||
3. If the label is hardcoded, it must be updated.
|
||||
|
||||
**Example:**
|
||||
`label="Vary by culture"`
|
||||
|
||||
|
||||
### **4. Find the Correct Translation**
|
||||
|
||||
|
||||
|
||||
1. Open the `en.ts` or `en-us.ts` file and search for relevant keywords. \
|
||||
\
|
||||
**Example:**
|
||||
|
||||
* If the text is "Vary by culture," search for `vary`, `culture`, or `Vary by culture`.
|
||||
|
||||
|
||||
2. Once you find the translation, take the element name and search for it in the target language file (e.g., `da-dk.ts` for Danish).
|
||||
|
||||
|
||||

|
||||
|
||||
3. If a translation exists, insert it into the label element found earlier.
|
||||
|
||||
|
||||
|
||||
### **5. Insert the Translation**
|
||||
|
||||
To display the new translation correctly, insert the following code inside the label element:
|
||||
|
||||
`${this.localize.term('action_key')}`
|
||||
|
||||
Replace `action_key` with the correct translation key.
|
||||
|
||||
**Example:**
|
||||
|
||||
`${this.localize.term('contentTypeEditor_allowVaryByCulture')}`
|
||||
|
||||

|
||||
|
||||
|
||||
Save the changes and return to the Backoffice to see the update.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### **6. Commit and Push**
|
||||
|
||||
|
||||
|
||||
1. Commit your changes to a new temporary branch (avoid committing directly to `main`).
|
||||
|
||||
2. Push the changes to your fork on GitHub.
|
||||
|
||||
|
||||
### **7. Create a Pull Request**
|
||||
|
||||
|
||||
|
||||
1. In your forked repository on GitHub (`https://github.com/[YourUsername]/Umbraco-CMS`), a banner will appear stating that you pushed a new branch.
|
||||
|
||||
2. Click the button to create a pull request and follow the instructions.
|
||||
|
||||
|
||||
## **I Can’t Find the Correct Translation**
|
||||
|
||||
If you can’t find the translation you need, it may not exist yet. In this case, you can create a new action with related keys.
|
||||
|
||||
|
||||
### **1. Ensure It Doesn’t Already Exist**
|
||||
|
||||
Search thoroughly in `en.ts` or `en-us.ts` for all relevant keywords.
|
||||
|
||||
|
||||
### **2. Create an Action**
|
||||
|
||||
|
||||
|
||||
1. Choose a meaningful name for the action to avoid confusion. \
|
||||
\
|
||||
**Example:** Translation for the Data Type "Color Picker."
|
||||
|
||||
* **Good name:** `colorPickerConfigurations`
|
||||
* **Bad name:** `colorpicker`
|
||||
2. A specific action name prevents unnecessarily long key names.
|
||||
|
||||
3. Define the action:
|
||||
|
||||
|
||||
|
||||
### **3. Create Keys**
|
||||
|
||||
|
||||
|
||||
1. Use clear and descriptive key names. \
|
||||
\
|
||||
**Example:**
|
||||
* **Good name:** `colorsTitle`
|
||||
* **Bad name:** `colors`
|
||||
2. Add the necessary keys inside the action with proper translations.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## **I Can’t Find a <code>render()</code> Code in VS Code**
|
||||
|
||||
In some cases, such as Data Types, the label might not be inside `render()`. Instead, it may be in a manifest file.
|
||||
|
||||
|
||||
### 1. Search for the Text
|
||||
|
||||
Copy the text from the Backoffice and search for it in the code.
|
||||
|
||||
|
||||
### 2. Open the Manifest File
|
||||
|
||||
Once you find the relevant manifest file, open it to confirm you’re in the right place.
|
||||
|
||||
|
||||
### 3. Change the Label
|
||||
|
||||
In Markdown files, localization is slightly different. Instead of:
|
||||
`${this.localize.term('action_key')}`
|
||||
|
||||
Use: `#action_key`
|
||||
|
||||
**Example:**
|
||||
`#colorPickerConfigurations_showLabelTitle`
|
||||
|
||||
### 4. Change the Description
|
||||
|
||||
For descriptions in Markdown files, use:
|
||||
`{umbLocalize: action_key}`
|
||||
|
||||
**Example:**
|
||||
`{umbLocalize: colorPickerConfigurations_showLabelDescription}`
|
||||
|
||||
|
||||
### 5. Save and Verify
|
||||
|
||||
Once all changes are made, your manifest should look something like this:
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Thank you
|
||||
|
||||
Following these steps ensures that the Umbraco Backoffice remains accessible and user-friendly in all supported languages. Thanks for contributing! 🎉
|
||||
@@ -1 +0,0 @@
|
||||
The full development guide for this repository lives in [CLAUDE.md](../CLAUDE.md). Please read that file for complete instructions on architecture, build steps, testing, branching conventions, and coding patterns.
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,38 +0,0 @@
|
||||
# .github/release.yml
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
- duplicate
|
||||
- wontfix
|
||||
categories:
|
||||
- title: 🙌 Notable Changes
|
||||
labels:
|
||||
- category/notable
|
||||
- title: 💥 Breaking Changes
|
||||
labels:
|
||||
- category/breaking
|
||||
- title: 📄 Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- category/documentation
|
||||
- title: 🏠 Internal
|
||||
labels:
|
||||
- internal
|
||||
- title: 📦 Dependencies
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🌈 Accessibility Improvements
|
||||
labels:
|
||||
- accessibility
|
||||
- category/accessibility
|
||||
- title: 🚀 New Features
|
||||
labels:
|
||||
- type/feature
|
||||
- category/feature
|
||||
- type/enhancement
|
||||
- category/enhancement
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- '*'
|
||||
@@ -1,56 +0,0 @@
|
||||
name: Backoffice Static Web Apps CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
- src/Umbraco.Web.UI.Client/src/**
|
||||
- .github/workflows/azure-backoffice.yml
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name)
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- name: Build And Deploy
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
production_branch: main
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_GROUND_017B08103 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: "upload"
|
||||
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
|
||||
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
|
||||
app_location: "src/Umbraco.Web.UI.Client" # App source code path
|
||||
app_build_command: "npm run build:for:static"
|
||||
output_location: "dist" # Built app content directory - optional
|
||||
skip_api_build: true # Set to true if you do not have an Azure Functions API in your repo
|
||||
###### End of Repository/Build Configurations ######
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
- name: Close Pull Request
|
||||
id: closepullrequest
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
app_location: "src/Umbraco.Web.UI.Client"
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_GROUND_017B08103 }}
|
||||
action: "close"
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Storybook CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
- src/Umbraco.Web.UI.Client/src/**
|
||||
- .github/workflows/azure-storybook.yml
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- v*/dev
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name)
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build And Deploy
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
production_branch: main
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_SEA_0C7411A03 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: "upload"
|
||||
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
|
||||
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
|
||||
app_location: "src/Umbraco.Web.UI.Client" # App source code path
|
||||
app_build_command: "npm run storybook:build"
|
||||
output_location: "/storybook-static" # Built app content directory - optional
|
||||
skip_api_build: true # Set to true if you do not have an Azure Functions API in your repo
|
||||
###### End of Repository/Build Configurations ######
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
- name: Close Pull Request
|
||||
id: closepullrequest
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
app_location: "src/Umbraco.Web.UI.Client"
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_SEA_0C7411A03 }}
|
||||
action: "close"
|
||||
@@ -1,88 +0,0 @@
|
||||
name: Claude PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened]
|
||||
# NOTE: `pull_request_target` would let this workflow review fork PRs
|
||||
# (with access to secrets), but the action currently fails during OIDC
|
||||
# token exchange with "401 Unauthorized - Invalid OIDC token" on that
|
||||
# event. PR #579 added `pull_request_target` routing to the action, but
|
||||
# Anthropic's `/github-app-token-exchange` endpoint appears not to
|
||||
# accept the token claims produced by that event. Re-enable once the
|
||||
# upstream issue is resolved.
|
||||
# See: https://github.com/anthropics/claude-code-action/issues/347
|
||||
# https://github.com/anthropics/claude-code-action/issues/621
|
||||
# pull_request_target:
|
||||
# types: [opened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
review:
|
||||
# Skip fork PRs: secrets are not exposed on `pull_request` events from
|
||||
# forks, so the action would fail with a red check. Remove this clause
|
||||
# once upstream fork support lands (tracked in
|
||||
# https://github.com/anthropics/claude-code-action/issues/939) and we
|
||||
# can re-enable the `pull_request_target` trigger above.
|
||||
if: >-
|
||||
github.event.pull_request.draft == false
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
|
||||
|
||||
# Enable progress tracking
|
||||
track_progress: true
|
||||
|
||||
# Debug (set to true to show full output in logs, false to hide it and only post comments on the PR)
|
||||
show_full_output: false
|
||||
|
||||
additional_permissions: "actions: read"
|
||||
claude_args: "--model claude-sonnet-4-6 --allowedTools 'Bash(gh:*),Bash(git:*)'"
|
||||
prompt: |
|
||||
You are reviewing pull request #${{ github.event.pull_request.number }}
|
||||
in the Umbraco CMS repository.
|
||||
|
||||
Read and execute the review procedure defined in `.claude/skills/umb-review/SKILL.md`.
|
||||
|
||||
For each finding that references a specific file and line:
|
||||
- Post an individual inline PR comment on that line.
|
||||
- Format: **[Severity]** explanation, then suggestion.
|
||||
|
||||
For the overall summary (header, impact, verdict):
|
||||
- Post ONE top-level PR comment.
|
||||
|
||||
Do NOT use sticky/updating comments — post new individual comments.
|
||||
|
||||
After reviewing, apply labels to the PR based on changed files:
|
||||
- `area/frontend` — if files under `src/Umbraco.Web.UI.Client/` are changed
|
||||
- `area/backend` — if .cs files outside the frontend client are changed
|
||||
- `area/test` — if only test files are changed
|
||||
- `category/api` — if Management API or Delivery API files are changed
|
||||
- `category/breaking` — if breaking changes were detected in the review
|
||||
- `category/localization` — if localization/language files are changed
|
||||
- `category/test-automation` — if only test files are changed
|
||||
- `category/refactor` — if the PR is pure refactoring with no new features
|
||||
- `category/performance` — if performance-related changes are detected
|
||||
- `category/ux` — if user-facing changes are detected
|
||||
- `category/ui` — if changes to the UI layer are detected
|
||||
|
||||
Only apply labels you are confident about. Never remove existing labels.
|
||||
|
||||
Be friendly and constructive. This project values community contributions.
|
||||
Frame feedback as suggestions where possible.
|
||||
Reserve firm language for genuine Critical issues only.
|
||||
|
||||
Run fully autonomously. Do NOT ask questions.
|
||||
Only review changed files. Do not flag pre-existing issues.
|
||||
Do not suggest changes that would themselves introduce breaking changes.
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Claude
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned, labeled]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
|
||||
assignee_trigger: "claude"
|
||||
label_trigger: "claude"
|
||||
base_branch: "main"
|
||||
additional_permissions: "actions: read"
|
||||
claude_args: "--model claude-sonnet-4-6 --max-turns 50 --allowedTools 'Bash(gh:*),Bash(git:*),Bash(npm:*),Bash(dotnet:*)'"
|
||||
prompt: |
|
||||
You are an AI assistant for the Umbraco CMS repository, an open-source
|
||||
.NET CMS that welcomes community contributions.
|
||||
|
||||
You were triggered on issue/PR #${{ github.event.issue.number || github.event.pull_request.number }}.
|
||||
|
||||
Read the user's message and do what they ask. The trigger phrase
|
||||
`@claude` is stripped before you see the message, so common requests
|
||||
will look like:
|
||||
|
||||
- `review` — Review PR #${{ github.event.issue.number || github.event.pull_request.number }}.
|
||||
Use `gh pr diff ${{ github.event.issue.number || github.event.pull_request.number }}`
|
||||
and `gh pr view ${{ github.event.issue.number || github.event.pull_request.number }}`
|
||||
to read the changes. Do NOT use git diff or the umb-review skill.
|
||||
Focus on bugs, breaking changes, and architectural concerns.
|
||||
Post inline comments for specific issues and a brief summary.
|
||||
- `help` or a general question — Answer based on the codebase.
|
||||
Read CLAUDE.md files for project structure and conventions.
|
||||
- `fix ...` — Implement the requested fix on a new branch.
|
||||
- `label` — Apply appropriate labels to the PR or issue.
|
||||
|
||||
If the message is empty or just whitespace, treat it as `review`
|
||||
when on a PR, or `help` when on an issue.
|
||||
|
||||
If none of these match, read the user's message carefully and respond
|
||||
to what they actually asked for.
|
||||
|
||||
## Labeling
|
||||
|
||||
When labeling PRs (based on changed files):
|
||||
- `area/frontend`, `area/backend`, `area/test`
|
||||
- `category/api`, `category/breaking`, `category/localization`
|
||||
- `category/refactor`, `category/performance`, `category/ux`, `category/ui`
|
||||
- `category/test-automation`
|
||||
|
||||
When labeling issues (based on content):
|
||||
- `area/frontend`, `area/backend`, `area/test`
|
||||
- `affected/v14` through `affected/v17`, `affected/backoffice`
|
||||
- `category/api`, `category/localization`, `category/performance`
|
||||
- `category/ux`, `category/ui`
|
||||
|
||||
Only apply labels you are confident about. Never remove existing labels.
|
||||
|
||||
## Tone
|
||||
|
||||
Be friendly and constructive. Frame feedback as suggestions.
|
||||
Reserve firm language for genuine critical issues only.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Run fully autonomously. Do NOT ask questions.
|
||||
- Do not suggest changes that would introduce breaking changes.
|
||||
@@ -3,69 +3,58 @@ name: "Code scanning - action"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*/dev"
|
||||
- "*/main"
|
||||
- "main"
|
||||
- "release/*"
|
||||
- '*/dev'
|
||||
- '*/contrib'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches:
|
||||
- "*/dev"
|
||||
- "*/main"
|
||||
- "main"
|
||||
- "release/*"
|
||||
schedule:
|
||||
- cron: "33 2 * * 1"
|
||||
- '*/dev'
|
||||
- '*/contrib'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
dotnetVersion: 9.x
|
||||
dotnetIncludePreviewVersions: "preview"
|
||||
dotnetVersion: 8.x
|
||||
dotnetIncludePreviewVersions: true
|
||||
solution: umbraco.sln
|
||||
buildConfiguration: SkipTests
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
permissions:
|
||||
actions: read # for github/codeql-action/init to get workflow details
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/analyze to upload SARIF results
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: csharp
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# We use the setup-dotnet action to set up .NET Core, otherwise the CodeQL CLI will not work with preview versions.
|
||||
- name: Setup .NET from global.json
|
||||
uses: actions/setup-dotnet@v4
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
config-file: ./.github/config/codeql-config.yml
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: ./.github/config/codeql-config.yml
|
||||
- name: Use .NET ${{ env.dotnetVersion }}
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: ${{ env.dotnetVersion }}
|
||||
include-prerelease: ${{ env.dotnetIncludePreviewVersions }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
- name: Run dotnet restore
|
||||
run: dotnet restore ${{ env.solution }}
|
||||
|
||||
- name: Run dotnet build
|
||||
run: dotnet build ${{ env.solution }} --configuration ${{ env.buildConfiguration }} --no-restore -p:ContinuousIntegrationBuild=true
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Issue Deduplication
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to analyze for duplicates'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
deduplicate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check for duplicate issues
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
|
||||
|
||||
Issue: #${{ github.event.issue.number || inputs.issue_number }}
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Your task:
|
||||
1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number || inputs.issue_number }})
|
||||
2. Search for similar existing issues using mcp__github__search_issues with relevant keywords from the issue title and body
|
||||
3. Compare the new issue with existing ones to identify potential duplicates
|
||||
|
||||
Criteria for duplicates:
|
||||
- Same bug or error being reported
|
||||
- Same feature request (even if worded differently)
|
||||
- Same question being asked
|
||||
- Issues describing the same root problem
|
||||
|
||||
If you find duplicates:
|
||||
- Add a comment on the new issue linking to the original issue(s)
|
||||
- Apply the "duplicate" and "state/needs-investigation" labels to the new issue
|
||||
- Be polite and explain why it's a duplicate
|
||||
- Suggest the user follow the original issue for updates
|
||||
|
||||
If it's NOT a duplicate:
|
||||
- Don't add any comments
|
||||
- You may apply appropriate topic labels based on the issue content
|
||||
|
||||
Use these tools:
|
||||
- mcp__github__get_issue: Get issue details
|
||||
- mcp__github__search_issues: Search for similar issues
|
||||
- mcp__github__list_issues: List recent issues if needed
|
||||
- mcp__github__add_issue_comment: Add a comment if duplicate found
|
||||
- mcp__github__update_issue: Add labels
|
||||
|
||||
Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
|
||||
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
|
||||
|
||||
# Issues are opened by community members without write access, so the
|
||||
# default OIDC token exchange fails with "User does not have write
|
||||
# access on this repository". Pass `github_token` explicitly and set
|
||||
# `allowed_non_write_users` to bypass that check. Safe here because
|
||||
# `permissions:` and `--allowedTools` below are tightly scoped to
|
||||
# issue operations only.
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
|
||||
# Surface full SDK output (including tool calls and permission denials)
|
||||
# to diagnose why Claude sometimes only partially completes (e.g. labels
|
||||
# an issue but skips the comment). Safe to leave on — no secrets in output.
|
||||
show_full_output: true
|
||||
|
||||
claude_args: |
|
||||
--model claude-haiku-4-5 --allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__add_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"
|
||||
@@ -1,164 +0,0 @@
|
||||
name: Create a release discussions for each new version label
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *" # every hour
|
||||
workflow_dispatch: # allow manual runs
|
||||
permissions:
|
||||
contents: read
|
||||
discussions: write
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
reconcile:
|
||||
if: github.repository == 'umbraco/Umbraco-CMS'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Reconcile release/* labels → discussions
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const categoryName = "Releases";
|
||||
|
||||
// 24h cutoff
|
||||
const since = new Date(Date.now() - 24*60*60*1000).toISOString();
|
||||
core.info(`Scanning issues/PRs updated since ${since}`);
|
||||
|
||||
// fetch repo + discussion categories
|
||||
const repoData = await github.graphql(`
|
||||
query($owner:String!, $repo:String!){
|
||||
repository(owner:$owner, name:$repo){
|
||||
id
|
||||
discussionCategories(first:100){ nodes { id name } }
|
||||
}
|
||||
}
|
||||
`, { owner, repo });
|
||||
const repoId = repoData.repository.id;
|
||||
const category = repoData.repository.discussionCategories.nodes.find(c => c.name === categoryName);
|
||||
if (!category) {
|
||||
core.setFailed(`Discussion category "${categoryName}" not found`);
|
||||
return;
|
||||
}
|
||||
const categoryId = category.id;
|
||||
|
||||
// paginate issues/PRs updated in last 24h
|
||||
for await (const { data: items } of github.paginate.iterator(
|
||||
github.rest.issues.listForRepo,
|
||||
{ owner, repo, state: "all", since, per_page: 100 }
|
||||
)) {
|
||||
for (const item of items) {
|
||||
const releaseLabels = (item.labels || [])
|
||||
.map(l => (typeof l === "string" ? l : l.name)) // always get the name
|
||||
.filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes");
|
||||
if (releaseLabels.length === 0) continue;
|
||||
|
||||
core.info(`#${item.number}: ${releaseLabels.join(", ")}`);
|
||||
|
||||
for (const labelName of releaseLabels) {
|
||||
const version = labelName.substring("release/".length);
|
||||
const titleTarget = `Release: ${version}`;
|
||||
|
||||
// search discussions
|
||||
let discussionId = null;
|
||||
let cursor = null;
|
||||
while (true) {
|
||||
const page = await github.graphql(`
|
||||
query($owner:String!, $repo:String!, $cursor:String){
|
||||
repository(owner:$owner, name:$repo){
|
||||
discussions(first:50, after:$cursor){
|
||||
nodes{
|
||||
id
|
||||
title
|
||||
url
|
||||
category{ name }
|
||||
labels(first:50){ nodes{ name } }
|
||||
}
|
||||
pageInfo{ hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
`, { owner, repo, cursor });
|
||||
const nodes = page.repository.discussions.nodes;
|
||||
const byLabel = nodes.find(d =>
|
||||
d.category?.name === categoryName &&
|
||||
d.labels?.nodes?.some(l => l.name === labelName)
|
||||
);
|
||||
if (byLabel) { discussionId = byLabel.id; break; }
|
||||
const byTitle = nodes.find(d =>
|
||||
d.category?.name === categoryName &&
|
||||
d.title === titleTarget
|
||||
);
|
||||
if (byTitle) { discussionId = byTitle.id; break; }
|
||||
if (!page.repository.discussions.pageInfo.hasNextPage) break;
|
||||
cursor = page.repository.discussions.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
if (!discussionId) {
|
||||
core.info(`→ Creating discussion for ${labelName}`);
|
||||
const body =
|
||||
`**Release date:** TODO (YYYY-MM-DD)\n\n` +
|
||||
`### Links\n` +
|
||||
`- [Issues and pull requests marked for version ${version}](https://github.com/${owner}/${repo}/issues?q=label%3A${encodeURIComponent(labelName)})\n`;
|
||||
|
||||
const created = await github.graphql(`
|
||||
mutation($repoId:ID!, $catId:ID!, $title:String!, $body:String!){
|
||||
createDiscussion(input:{
|
||||
repositoryId:$repoId,
|
||||
categoryId:$catId,
|
||||
title:$title,
|
||||
body:$body
|
||||
}){ discussion{ id url } }
|
||||
}
|
||||
`, { repoId, catId: categoryId, title: titleTarget, body });
|
||||
|
||||
discussionId = created.createDiscussion.discussion.id;
|
||||
|
||||
// lock the discussion to prevent replies
|
||||
await github.graphql(`
|
||||
mutation($id:ID!){
|
||||
lockLockable(input:{ lockableId:$id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`, { id: discussionId });
|
||||
core.info(`🔒 Locked discussion ${discussionId}`);
|
||||
} else {
|
||||
core.info(`→ Found existing discussion for ${labelName}`);
|
||||
}
|
||||
|
||||
// ensure label exists
|
||||
let labelId;
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: labelName });
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: labelName, color: "0E8A16"
|
||||
});
|
||||
} else { throw e; }
|
||||
}
|
||||
const labelNode = await github.graphql(`
|
||||
query($owner:String!, $repo:String!, $name:String!){
|
||||
repository(owner:$owner, name:$repo){ label(name:$name){ id } }
|
||||
}
|
||||
`, { owner, repo, name: labelName });
|
||||
labelId = labelNode.repository.label?.id;
|
||||
if (!labelId) continue;
|
||||
|
||||
// add label to discussion
|
||||
await github.graphql(`
|
||||
mutation($id:ID!, $labels:[ID!]!){
|
||||
addLabelsToLabelable(input:{ labelableId:$id, labelIds:$labels }) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`, { id: discussionId, labels: [labelId] });
|
||||
|
||||
core.info(`✓ ${labelName} attached to discussion`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
name: Test Backoffice
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/**
|
||||
|
||||
# Allows GitHub to use this workflow to validate the merge queue
|
||||
merge_group:
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/Umbraco.Web.UI.Client
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund --prefer-offline
|
||||
- name: Check for circular dependencies
|
||||
run: node devops/circular/index.js src
|
||||
- run: npm run lint:errors
|
||||
- run: npm run generate:tsconfig
|
||||
- run: npm run generate:icons
|
||||
- run: npm run build:for:cms
|
||||
- run: npm run check:paths
|
||||
- run: npm run generate:jsonschema:dist
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/Umbraco.Web.UI.Client
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund --prefer-offline
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm test
|
||||
@@ -47,16 +47,17 @@ NDependOut/
|
||||
QueryResult.htm
|
||||
tools/docfx/
|
||||
|
||||
# Ignore rule for clearing out Belle (avoid rebuilding all the time)
|
||||
preserve.belle
|
||||
|
||||
# Ignore rule for output of generated documentation files from grunt docserve
|
||||
/src/Umbraco.Web.UI.Docs/api/
|
||||
/src/Umbraco.Web.UI.Docs/package-lock.json
|
||||
|
||||
# csharp-docs
|
||||
/build/csharp-docs/api/
|
||||
/build/csharp-docs/_site/
|
||||
|
||||
# Local config
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
!.claude/settings.json
|
||||
.env.local
|
||||
|
||||
# Build
|
||||
/build.out/
|
||||
/build.tmp/
|
||||
@@ -67,16 +68,13 @@ tools/docfx/
|
||||
/build/docs.zip
|
||||
/build/ui-docs.zip
|
||||
/build/csharp-docs.zip
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/auth
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/*
|
||||
!/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/errors
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/
|
||||
|
||||
# Environment specific data
|
||||
/src/Umbraco.Web.UI.Client/[Bb]uild/
|
||||
/src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/
|
||||
/src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css
|
||||
/src/Umbraco.Web.UI.Client/TESTS-*.xml
|
||||
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
|
||||
/src/Umbraco.Web.UI/App_Code/
|
||||
/src/Umbraco.Web.UI/App_Plugins/
|
||||
@@ -93,7 +91,6 @@ tools/docfx/
|
||||
|
||||
# Tests
|
||||
/tests/Umbraco.Tests.AcceptanceTest/.env
|
||||
/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth
|
||||
/tests/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf
|
||||
/tests/Umbraco.Tests.Integration.SqlCe/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
/tests/Umbraco.Tests.Integration/appsettings.Tests.Local.json
|
||||
@@ -102,22 +99,14 @@ tools/docfx/
|
||||
/tests/Umbraco.Tests.Integration/[Uu]mbraco/[Ll]ogs/
|
||||
/tests/Umbraco.Tests.Integration/Views/
|
||||
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
/BenchmarkDotNet.Artifacts/
|
||||
playwright-report
|
||||
trace.zip
|
||||
/tests/Umbraco.Tests.AcceptanceTest/results
|
||||
/tests/Umbraco.Tests.AcceptanceTest/dist
|
||||
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
/src/Umbraco.Cms.Targets/appsettings-schema.*.json
|
||||
/src/Umbraco.Cms.Targets/umbraco-package-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.*.json
|
||||
/src/Umbraco.Web.UI/umbraco-package-schema.json
|
||||
/src/Umbraco.Web.UI.Client/umbraco-package-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
.playwright-mcp/
|
||||
playwright-report
|
||||
trace.zip
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"umbraco-cms": {
|
||||
"command": "npx",
|
||||
"args": ["@umbraco-cms/mcp-dev@17"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +1,35 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Backoffice Launch (Vite + .NET Core)",
|
||||
"configurations": [
|
||||
"Backoffice Launch Vite (Chrome)",
|
||||
".NET Core Serve with External Auth (web)"
|
||||
],
|
||||
"stopAll": true,
|
||||
"presentation": {
|
||||
"group": "1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Backoffice Launch Vite (Chrome)",
|
||||
"request": "launch",
|
||||
"env": {
|
||||
"VITE_UMBRACO_USE_MSW": "${input:AskForMockServer}"
|
||||
},
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["vite"],
|
||||
"type": "node",
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
|
||||
"skipFiles": ["<node_internals>/**", "node_modules/**"],
|
||||
"smartStep": true,
|
||||
"autoAttachChildProcesses": true,
|
||||
"serverReadyAction": {
|
||||
"killOnServerStop": true,
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "Local: http://localhost:([0-9]+)",
|
||||
"uriFormat": "http://localhost:%s",
|
||||
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Backoffice Attach Vite (Chrome)",
|
||||
"request": "launch",
|
||||
"type": "chrome",
|
||||
"smartStep": true,
|
||||
"url": "http://localhost:5173/",
|
||||
"skipFiles": ["<node_internals>/**", "node_modules/**"],
|
||||
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
|
||||
"presentation": {
|
||||
"group": "2"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": ["run"],
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
|
||||
"stopAtEntry": false,
|
||||
"requireExactSource": false,
|
||||
"postDebugTask": "kill-umbraco-web-ui",
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}",
|
||||
"presentation": {
|
||||
"group": "3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Serve with External Auth (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": ["run"],
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
|
||||
"stopAtEntry": false,
|
||||
"requireExactSource": false,
|
||||
"checkForDevCert": true,
|
||||
"postDebugTask": "kill-umbraco-web-ui",
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "https://localhost:44339",
|
||||
"UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL": "https://localhost:44339",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
|
||||
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "AskForMockServer",
|
||||
"type": "promptString",
|
||||
"description": "Use Mock Service Worker (MSW) for Backoffice API calls (off requires a running server)?",
|
||||
"default": "off"
|
||||
}
|
||||
]
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": ["run"],
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
|
||||
"stopAtEntry": false,
|
||||
"requireExactSource": false,
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"backoffice",
|
||||
"pickable",
|
||||
"Pickable",
|
||||
"Umbraco",
|
||||
"unprovide",
|
||||
"Unproviding"
|
||||
],
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./src/Umbraco.Web.UI.Client/",
|
||||
"./src/Umbraco.Web.UI.Login/"
|
||||
]
|
||||
}
|
||||
@@ -1,87 +1,80 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"detail": "Builds the client and SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"dependsOn": ["Client Build", "Dotnet build"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Install",
|
||||
"detail": "install npm for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Build",
|
||||
"detail": "runs npm run build for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "build:for:cms",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Watch",
|
||||
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Dotnet build",
|
||||
"detail": "Dotnet build of SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/umbraco.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Dotnet watch",
|
||||
"detail": "Dotnet run and watch of Web.UI",
|
||||
"promptOnClose": true,
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "kill-umbraco-web-ui",
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"osx": {
|
||||
"command": "pkill -f Umbraco.Web.UI"
|
||||
},
|
||||
"linux": {
|
||||
"command": "pkill -f Umbraco.Web.UI"
|
||||
},
|
||||
"windows": {
|
||||
"command": "taskkill /IM Umbraco.Web.UI.exe /F"
|
||||
}
|
||||
}
|
||||
]
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"detail": "Builds the client and SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"dependsOn": [
|
||||
"Client Build",
|
||||
"Dotnet build"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Install",
|
||||
"detail": "install npm for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Build",
|
||||
"detail": "runs npm run build for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": [
|
||||
"$gulp-tsc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Client Watch",
|
||||
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": [
|
||||
"$gulp-tsc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Dotnet build",
|
||||
"detail": "Dotnet build of SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/umbraco.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Dotnet watch",
|
||||
"detail": "Dotnet run and watch of Web.UI",
|
||||
"promptOnClose": true,
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
# Umbraco CMS - Multi-Project Repository
|
||||
|
||||
Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production projects organized in a layered architecture with clear separation of concerns.
|
||||
|
||||
**Repository**: https://github.com/umbraco/Umbraco-CMS
|
||||
**License**: MIT
|
||||
**Main Branch**: `main`
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### What This Repository Contains
|
||||
|
||||
**21 Production Projects** organized in 3 main categories:
|
||||
|
||||
1. **Core Architecture** (Domain & Infrastructure)
|
||||
- `Umbraco.Core` - Interface contracts, domain models, notifications
|
||||
- `Umbraco.Infrastructure` - Service implementations, data access, caching
|
||||
|
||||
2. **Web & APIs** (Presentation Layer)
|
||||
- `Umbraco.Web.UI` - Main ASP.NET Core web application
|
||||
- `Umbraco.Web.Common` - Shared web functionality, controllers, middleware
|
||||
- `Umbraco.Cms.Api.Management` - Backoffice Management API (REST)
|
||||
- `Umbraco.Cms.Api.Delivery` - Content Delivery API (headless)
|
||||
- `Umbraco.Cms.Api.Common` - Shared API infrastructure
|
||||
|
||||
3. **Specialized Features** (Pluggable Modules)
|
||||
- Persistence: EF Core (modern), NPoco (legacy) for SQL Server & SQLite
|
||||
- Caching: `PublishedCache.HybridCache` (in-memory + distributed)
|
||||
- Search: `Examine.Lucene` (full-text search)
|
||||
- Imaging: `Imaging.ImageSharp` v1 & v2 (image processing)
|
||||
- Other: Static assets, targets, development tools
|
||||
|
||||
**6 Test Projects**:
|
||||
- `Umbraco.Tests.Common` - Shared test utilities
|
||||
- `Umbraco.Tests.UnitTests` - Unit tests
|
||||
- `Umbraco.Tests.Integration` - Integration tests
|
||||
- `Umbraco.Tests.Benchmarks` - Performance benchmarks
|
||||
- `Umbraco.Tests.AcceptanceTest` - E2E tests
|
||||
- `Umbraco.Tests.AcceptanceTest.UmbracoProject` - Test instance
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **.NET 10.0** - Target framework for all projects
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Entity Framework Core** - Modern ORM
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
|
||||
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for API documentation
|
||||
- **Lucene.NET** - Full-text search via Examine
|
||||
- **ImageSharp** - Image processing
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository Structure
|
||||
|
||||
```
|
||||
Umbraco-CMS/
|
||||
├── src/ # 21 production projects
|
||||
│ ├── Umbraco.Core/ # Domain contracts (interfaces only)
|
||||
│ │ └── CLAUDE.md # ⭐ Core architecture guide
|
||||
│ ├── Umbraco.Infrastructure/ # Service implementations
|
||||
│ ├── Umbraco.Web.Common/ # Web utilities
|
||||
│ ├── Umbraco.Web.UI/ # Main web application
|
||||
│ ├── Umbraco.Cms.Api.Management/ # Management API
|
||||
│ ├── Umbraco.Cms.Api.Delivery/ # Delivery API (headless)
|
||||
│ ├── Umbraco.Cms.Api.Common/ # Shared API infrastructure
|
||||
│ │ └── CLAUDE.md # ⭐ API patterns guide
|
||||
│ ├── Umbraco.PublishedCache.HybridCache/ # Content caching
|
||||
│ ├── Umbraco.Examine.Lucene/ # Search indexing
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore/ # EF Core data access
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore.Sqlite/
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore.SqlServer/
|
||||
│ ├── Umbraco.Cms.Persistence.Sqlite/ # Legacy SQLite
|
||||
│ ├── Umbraco.Cms.Persistence.SqlServer/ # Legacy SQL Server
|
||||
│ ├── Umbraco.Cms.Imaging.ImageSharp/ # Image processing v1
|
||||
│ ├── Umbraco.Cms.Imaging.ImageSharp2/ # Image processing v2
|
||||
│ ├── Umbraco.Cms.StaticAssets/ # Embedded assets
|
||||
│ ├── Umbraco.Cms.DevelopmentMode.Backoffice/
|
||||
│ ├── Umbraco.Cms.Targets/ # NuGet targets
|
||||
│ └── Umbraco.Cms/ # Meta-package
|
||||
│
|
||||
├── tests/ # 6 test projects
|
||||
│ ├── Umbraco.Tests.Common/
|
||||
│ ├── Umbraco.Tests.UnitTests/
|
||||
│ ├── Umbraco.Tests.Integration/
|
||||
│ ├── Umbraco.Tests.Benchmarks/
|
||||
│ ├── Umbraco.Tests.AcceptanceTest/
|
||||
│ └── Umbraco.Tests.AcceptanceTest.UmbracoProject/
|
||||
│
|
||||
├── templates/ # Project templates
|
||||
│ └── Umbraco.Templates/
|
||||
│
|
||||
├── tools/ # Build tools
|
||||
│ └── Umbraco.JsonSchema/
|
||||
│
|
||||
├── umbraco.sln # Main solution file
|
||||
├── Directory.Build.props # Shared build configuration
|
||||
├── Directory.Packages.props # Centralized package versions
|
||||
├── .editorconfig # Code style
|
||||
└── .globalconfig # Roslyn analyzers
|
||||
```
|
||||
|
||||
### Architecture Layers
|
||||
|
||||
**Dependency Flow** (unidirectional, always flows inward):
|
||||
|
||||
```
|
||||
Web.UI → Web.Common → Infrastructure → Core
|
||||
↓
|
||||
Api.Management → Api.Common → Infrastructure → Core
|
||||
↓
|
||||
Api.Delivery → Api.Common → Infrastructure → Core
|
||||
```
|
||||
|
||||
**Key Principle**: Core has NO dependencies (pure contracts). Infrastructure implements Core. Web/APIs depend on Infrastructure.
|
||||
|
||||
### Project Dependencies
|
||||
|
||||
**Core Layer**:
|
||||
- `Umbraco.Core` → No dependencies (only Microsoft.Extensions.*)
|
||||
|
||||
**Infrastructure Layer**:
|
||||
- `Umbraco.Infrastructure` → `Umbraco.Core`
|
||||
- `Umbraco.PublishedCache.*` → `Umbraco.Infrastructure`
|
||||
- `Umbraco.Examine.Lucene` → `Umbraco.Infrastructure`
|
||||
- `Umbraco.Cms.Persistence.*` → `Umbraco.Infrastructure`
|
||||
|
||||
**Web Layer**:
|
||||
- `Umbraco.Web.Common` → `Umbraco.Infrastructure` + caching + search
|
||||
- `Umbraco.Web.UI` → `Umbraco.Web.Common` + all features
|
||||
|
||||
**API Layer**:
|
||||
- `Umbraco.Cms.Api.Common` → `Umbraco.Web.Common`
|
||||
- `Umbraco.Cms.Api.Management` → `Umbraco.Cms.Api.Common`
|
||||
- `Umbraco.Cms.Api.Delivery` → `Umbraco.Cms.Api.Common`
|
||||
|
||||
---
|
||||
|
||||
## 3. Teamwork & Collaboration
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- **Main branch**: `main` (protected)
|
||||
- **Branch naming convention**: `v<version>/<type>/<description>`
|
||||
|
||||
**Format**: `v{major-version}/{type}/{kebab-case-description}`
|
||||
|
||||
**Version**: Read from `version.json` in the repository root. Use the major version number (e.g., `v17` for version 17.x.x).
|
||||
|
||||
**Types**:
|
||||
| Type | Use Case |
|
||||
|------|----------|
|
||||
| `feature` | New feature being introduced to the product |
|
||||
| `bugfix` | Fix to an existing issue with the product |
|
||||
| `qa` | Adding or updating unit, integration, or end-to-end tests |
|
||||
| `improvement` | Update to something that already exists but isn't broken (UI finessing, refactoring) |
|
||||
| `task` | Update that doesn't directly impact product behavior (dependency updates, build pipeline) |
|
||||
|
||||
**Description**: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
v17/bugfix/12345-correct-display-of-pending-migrations
|
||||
v17/feature/add-webhook-support
|
||||
v17/improvement/optimize-content-cache
|
||||
v17/qa/add-media-service-tests
|
||||
v17/task/update-ef-core-dependency
|
||||
```
|
||||
|
||||
See `.github/CONTRIBUTING.md` for full guidelines.
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
- **PR Template**: `.github/pull_request_template.md`
|
||||
- **Required CI Checks**:
|
||||
- All tests pass
|
||||
- Code formatting (dotnet format)
|
||||
- No build warnings
|
||||
- **Merge Strategy**: Squash and merge (via GitHub UI)
|
||||
- **Reviews**: Required from code owners
|
||||
|
||||
#### PR Naming Convention
|
||||
|
||||
Use the format: `Area: Description (closes #IssueID)`
|
||||
|
||||
**Examples**:
|
||||
| Area | Description | Issue |
|
||||
|------|-------------|-------|
|
||||
| Relations: | Move persistence of relations from repository into notification handlers | (closes #00000) |
|
||||
| Management API: | Correct the population of the parent for sibling items when retrieved under a folder | |
|
||||
| Docs: | Updated contributing guidelines to welcome contributions on bugfixes | |
|
||||
|
||||
**Area**: The feature or aspect affected (e.g., UFM, TipTap, Docs, Segmentation, Migrations). Helps readers quickly understand what is being changed.
|
||||
|
||||
**Description Best Practices**:
|
||||
- Include the area of change (Relations, Management API, etc.)
|
||||
- Describe the change and its impact
|
||||
- Be specific, not vague (describe "a golden retriever" not just "a dog")
|
||||
|
||||
**Issue Linking**: Add `(closes #IssueID)` to the title for readability, AND include a closing keyword on its own line in the PR body (e.g., `Fixes #IssueID`) so GitHub actually auto-links and auto-closes the issue on merge. GitHub only parses closing keywords (`closes`, `fixes`, `resolves`) from the PR body or commit messages — the title suffix is cosmetic and does **not** trigger auto-close on its own.
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Follow Conventional Commits format:
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
Types: feat, fix, docs, style, refactor, test, chore
|
||||
Scope: project name (core, web, api, etc.)
|
||||
|
||||
Examples:
|
||||
feat(core): add IContentService.GetByIds method
|
||||
fix(api): resolve null reference in schema handler
|
||||
docs(web): update routing documentation
|
||||
```
|
||||
|
||||
### Code Owners
|
||||
|
||||
Project ownership is distributed across teams. Check individual project directories for ownership.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture Patterns
|
||||
|
||||
### Core Architectural Decisions
|
||||
|
||||
1. **Layered Architecture with Dependency Inversion**
|
||||
- Core defines contracts (interfaces)
|
||||
- Infrastructure implements contracts that need Infrastructure-owned machinery
|
||||
- Web/APIs consume implementations via DI
|
||||
|
||||
**Where service implementations live**: Services whose dependencies are satisfiable from Core interfaces alone (repositories, scope, config, other Core services) live in `Umbraco.Core/Services/` — this covers the majority of domain services (`MemberService`, `ContentService`, `MediaService`, `ContentTypeService`, `EntityService`, `AuditService`, `ExternalMemberService`, etc.). Service implementations only live in `Umbraco.Infrastructure/Services/Implement/` when they genuinely need Infrastructure concerns — Examine indexes (`ContentSearchService`, `MediaSearchService`, `IndexedEntitySearchService`), log files (`LogViewerRepository`), packaging internals (`PackagingService`), webhook firing (`WebhookFiringService`), distributed-job coordination (`DistributedJobService`). When adding a new service, default to Core and only move to Infrastructure if a concrete dependency forces it.
|
||||
|
||||
2. **Interface-First Design**
|
||||
- All services defined as interfaces in Core
|
||||
- Enables testing, polymorphism, extensibility
|
||||
|
||||
3. **Notification Pattern** (not C# events)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "2. Notification System (Event Handling)"
|
||||
|
||||
4. **Composer Pattern** (DI registration)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "3. Composer Pattern (DI Registration)"
|
||||
|
||||
5. **Scoping Pattern** (Unit of Work)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "5. Scoping Pattern (Unit of Work)"
|
||||
|
||||
6. **Attempt Pattern** (operation results)
|
||||
- `Attempt<TResult, TStatus>` instead of exceptions
|
||||
- Strongly-typed operation status enums
|
||||
|
||||
### Key Design Patterns Used
|
||||
|
||||
- **Repository Pattern** - Data access abstraction
|
||||
- **Unit of Work** - Scoping for transactions
|
||||
- **Builder Pattern** - `ProblemDetailsBuilder` for API errors
|
||||
- **Strategy Pattern** - OpenAPI handlers (schema ID, operation ID)
|
||||
- **Options Pattern** - All configuration via `IOptions<T>`
|
||||
- **Factory Pattern** - Content type factories
|
||||
- **Mediator Pattern** - Notification aggregator
|
||||
|
||||
---
|
||||
|
||||
## 5. Avoiding Breaking Changes
|
||||
|
||||
No binary breaking changes are allowed within a major version. Three patterns are used:
|
||||
|
||||
### 5.1 Obsolete Constructor + StaticServiceProvider
|
||||
|
||||
When a public class needs new dependencies, obsolete the existing constructor and add a new one. The old constructor delegates to the new one, resolving missing deps via `StaticServiceProvider`.
|
||||
|
||||
```csharp
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MyService(IDependencyA depA)
|
||||
: this(
|
||||
depA,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDependencyB>())
|
||||
{
|
||||
}
|
||||
|
||||
public MyService(IDependencyA depA, IDependencyB depB)
|
||||
{
|
||||
_depA = depA;
|
||||
_depB = depB;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- `ContentCollectionPresentationFactory` - added `FlagProviderCollection`
|
||||
- `CacheInstructionService` - added `ILastSyncedManager`, `IRepositoryCacheVersionService`
|
||||
- `DocumentPresentationFactory` - added `FlagProviderCollection`
|
||||
|
||||
**Rules**:
|
||||
- Old constructor marked `[Obsolete("... Scheduled for removal in Umbraco {current-major+2}.")]`
|
||||
- Old constructor calls new constructor via `: this(...)`
|
||||
- Uses `StaticServiceProvider.Instance.GetRequiredService<T>()` for new params only
|
||||
- DI registration must use the NEW constructor (old is for external consumers only)
|
||||
|
||||
### 5.2 Obsolete Method + New Overload
|
||||
|
||||
When a public method signature needs to change, add the new method/overload and obsolete the old. The obsolete method should call the new one with suitable defaults.
|
||||
|
||||
```csharp
|
||||
[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public void DoThing(string name)
|
||||
=> DoThing(name, extraParam: null);
|
||||
|
||||
public void DoThing(string name, string? extraParam)
|
||||
{
|
||||
// Real implementation here
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Old method marked `[Obsolete]` with removal schedule
|
||||
- DRY: old method calls new method, providing defaults for new parameters
|
||||
- All internal callers must be updated to use the new method
|
||||
- No callers should remain on the obsolete method within the codebase
|
||||
|
||||
### 5.3 Default Interface Implementation
|
||||
|
||||
When adding methods to a public interface, provide a default implementation so existing external implementations don't break.
|
||||
|
||||
```csharp
|
||||
public interface IMyService
|
||||
{
|
||||
// Existing method
|
||||
void ExistingMethod();
|
||||
|
||||
// New method with default implementation
|
||||
void NewMethod(string param)
|
||||
=> ExistingMethod(); // delegate to existing if possible
|
||||
}
|
||||
```
|
||||
|
||||
**Strategies for the default** (in order of preference):
|
||||
1. **Use existing interface methods** to satisfy the contract (even if not optimal)
|
||||
2. **Return a sensible default** like empty collection, null, etc.
|
||||
3. **Throw `NotImplementedException`** if no reasonable default exists
|
||||
|
||||
**Example**: `IContentService.SaveBlueprint` - new overload with `IContent? createdFromContent` has a default impl that calls the old method (ignoring the new param).
|
||||
|
||||
**Example**: `IDocumentPresentationFactory.CreateCulturePublishScheduleModels` - full default implementation with logic, uses `StaticServiceProvider` for dependency resolution within the interface.
|
||||
|
||||
**Rules**:
|
||||
- Add `// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.` comment
|
||||
- Default impl should be functionally correct even if not optimal
|
||||
- If using `StaticServiceProvider` in a default impl, note this is temporary
|
||||
|
||||
### 5.4 General Rules
|
||||
|
||||
- **Removal policy**: Obsoleted members must remain for at least one full major version before removal. If obsoleted in version N, the earliest removal is version N+2. For example, something obsoleted in v17 is scheduled for removal in v19 (giving the whole of v18 as a deprecation period).
|
||||
- All `[Obsolete]` attributes must include **"Scheduled for removal in Umbraco {current+2}"**
|
||||
- Read `version.json` to determine the current major version
|
||||
- Suppress `CS0618` warnings where obsolete members must call each other:
|
||||
```csharp
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> OldMethod(param);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
```
|
||||
- Update ALL internal callers to use the new API - no internal code should use obsolete members
|
||||
|
||||
---
|
||||
|
||||
## 6. Project-Specific Notes
|
||||
|
||||
### Centralized Package Management
|
||||
|
||||
**NuGet package versions** are centralized in `Directory.Packages.props`. There are two `Directory.Packages.props` files in the source tree, with multi-level merging enabled so the test file inherits from the root:
|
||||
|
||||
| File | Scope |
|
||||
|------|-------|
|
||||
| `Directory.Packages.props` (root) | Production source code packages — referenced by all `src/**` projects |
|
||||
| `tests/Directory.Packages.props` | Test-only packages (NUnit, Moq, Bogus, BenchmarkDotNet, etc.) — adds entries on top of the inherited root file |
|
||||
|
||||
When updating dependencies, decide which file the package belongs in:
|
||||
- A package used only by test projects → `tests/Directory.Packages.props`
|
||||
- A package used by any production project (or by both production and tests) → root `Directory.Packages.props`
|
||||
|
||||
```xml
|
||||
<!-- Individual projects reference WITHOUT version -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
|
||||
<!-- Versions defined in Directory.Packages.props -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
```
|
||||
|
||||
**Opt-out**: `src/Umbraco.Web.UI/Umbraco.Web.UI.csproj` sets `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` and specifies versions inline (for `Microsoft.EntityFrameworkCore.Design`, `Microsoft.Build.Tasks.Core`, `Microsoft.ICU.ICU4C.Runtime`, etc.). Update those versions directly in that csproj when bumping. Two further `Directory.Packages.props` files exist under `templates/` for the project/extension templates and have their own version sets — keep `Microsoft.AspNetCore.OpenApi` aligned between the root file and `templates/UmbracoExtension/`.
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
|
||||
- `.editorconfig` - Code style rules
|
||||
- `.globalconfig` - Roslyn analyzer rules
|
||||
|
||||
### Persistence Layer - NPoco and EF Core
|
||||
|
||||
The repository contains BOTH (actively supported):
|
||||
- **Current**: NPoco-based persistence (`Umbraco.Cms.Persistence.Sqlite`, `Umbraco.Cms.Persistence.SqlServer`) - widely used and fully supported
|
||||
- **Future**: EF Core-based persistence (`Umbraco.Cms.Persistence.EFCore.*`) - migration in progress
|
||||
|
||||
**Note**: The codebase is actively migrating to EF Core, but NPoco remains the primary persistence layer and is not deprecated. Both are fully supported.
|
||||
|
||||
### Authentication: OpenIddict
|
||||
|
||||
All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
|
||||
- Reference tokens (not JWT) for better security
|
||||
- **Secure cookie-based token storage** (v17+) - tokens stored in HTTP-only cookies with `__Host-` prefix
|
||||
- Tokens are redacted from client-side responses and passed via secure cookies only (`[redacted]` placeholder)
|
||||
- ASP.NET Core Data Protection for token encryption
|
||||
- Configured in `Umbraco.Cms.Api.Common`
|
||||
- API requests must include credentials (`credentials: include` for fetch)
|
||||
|
||||
**Load Balancing Requirement**: All servers must share the same Data Protection key ring.
|
||||
|
||||
**Frontend auth pitfalls** — see `src/Umbraco.Web.UI.Client/docs/edge-cases.md` (Auth & Cross-tab section) and `docs/security.md`. Key points:
|
||||
- Never call `validateToken()` per API request — it revokes the previous reference token (ID2019 errors)
|
||||
- `window.opener` is set for ANY `window.open()` target, not only OAuth popups — scope guards to the pathname too
|
||||
- BroadcastChannel does not deliver messages to the sender's own tab
|
||||
|
||||
### Content Caching Strategy
|
||||
|
||||
**HybridCache** (`Umbraco.PublishedCache.HybridCache`):
|
||||
- In-memory cache + distributed cache support
|
||||
- Published content only (not draft)
|
||||
- Invalidated via notifications and cache refreshers
|
||||
|
||||
### API Versioning
|
||||
|
||||
APIs use `Asp.Versioning.Mvc`:
|
||||
- Management API: `/umbraco/management/api/v{version}/*`
|
||||
- Delivery API: `/umbraco/delivery/api/v{version}/*`
|
||||
- OpenAPI docs: `/umbraco/openapi/management.json`, `/umbraco/openapi/delivery.json`
|
||||
- Swagger UI: `/umbraco/openapi/`
|
||||
|
||||
### Updating `OpenApi.json` (Management API)
|
||||
|
||||
When a PR changes Management API controllers or models, the `OpenApi.json` file in the Management API project must be updated:
|
||||
|
||||
1. Run the Umbraco instance locally
|
||||
2. Open Swagger UI and navigate to the swagger.json link (e.g. `https://localhost:44339/umbraco/swagger/management/swagger.json`)
|
||||
3. Copy the full JSON content and paste it into `src/Umbraco.Cms.Api.Management/OpenApi.json`
|
||||
|
||||
**Important**: Commit only the substantive changes — not IDE-applied formatting (whitespace, reordering, etc.). Extraneous formatting diffs make PRs harder to review and merge-ups more error-prone.
|
||||
|
||||
### Backoffice npm Package
|
||||
|
||||
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
|
||||
|
||||
### SQL Server 2100-parameter limit
|
||||
|
||||
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
|
||||
|
||||
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
|
||||
|
||||
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
2. **Multi-Server**: Requires shared Data Protection key ring and synchronized clocks (NTP)
|
||||
3. **Database Support**: SQL Server, SQLite
|
||||
|
||||
---
|
||||
|
||||
## 7. CI/CD — Claude AI Assistant
|
||||
|
||||
Two GitHub Actions workflows powered by `anthropics/claude-code-action@v1`. Advisory only — does not block merging.
|
||||
|
||||
### Workflows
|
||||
|
||||
| File | Trigger | Purpose |
|
||||
|------|---------|---------|
|
||||
| `claude-review.yml` | `pull_request: [opened, ready_for_review]` | Auto-review every non-draft PR using the `umb-review` skill |
|
||||
| `claude.yml` | `@claude` comments, issue assign/label | Interactive assistant for PRs and issues |
|
||||
|
||||
### Auto-Review (`claude-review.yml`)
|
||||
|
||||
Runs the full `.claude/skills/umb-review/SKILL.md` procedure on every newly opened or un-drafted PR. Produces inline comments per finding and one summary comment with a verdict. Skips draft PRs. No turn limit.
|
||||
|
||||
### Interactive (`claude.yml`)
|
||||
|
||||
Responds to `@claude` mentions on PRs and issues. The trigger phrase is stripped before Claude sees the message, so:
|
||||
|
||||
- `@claude review` → light review using `gh pr diff` (not the umb-review skill)
|
||||
- `@claude fix ...` → implements a fix on a new branch
|
||||
- `@claude help` → answers questions about the codebase
|
||||
- `@claude label` → applies labels
|
||||
- `@claude` (empty) → defaults to `review` on PRs, `help` on issues
|
||||
|
||||
Also triggers on issue assignment to `claude` or adding the `claude` label. Gated: only runs when `@claude` appears in the comment/issue body. Max 25 turns.
|
||||
|
||||
**Allowed Bash tools**: `gh`, `git`, `npm`, `dotnet` (interactive only; auto-review allows `gh` and `git`).
|
||||
|
||||
### Labels
|
||||
|
||||
Both workflows apply labels based on content:
|
||||
|
||||
**On PRs** (based on changed files):
|
||||
|
||||
| Label | Condition |
|
||||
|-------|-----------|
|
||||
| `area/frontend` | Files under `src/Umbraco.Web.UI.Client/` |
|
||||
| `area/backend` | `.cs` files outside the frontend client |
|
||||
| `area/test` | Only test files changed |
|
||||
| `category/api` | Management or Delivery API files |
|
||||
| `category/breaking` | Breaking changes detected |
|
||||
| `category/localization` | Localization/language files |
|
||||
| `category/test-automation` | Only test files changed |
|
||||
| `category/refactor` | Pure refactoring, no new features |
|
||||
| `category/performance` | Performance-related changes |
|
||||
| `category/ux` | User-facing changes |
|
||||
| `category/ui` | UI layer changes |
|
||||
|
||||
**On Issues** (based on content): same `area/*` and `category/*` labels, plus `affected/v14` through `affected/v17` and `affected/backoffice`.
|
||||
|
||||
Labels are only added, never removed. Claude applies only labels it is confident about.
|
||||
|
||||
### Key Implementation Notes
|
||||
|
||||
- **Checkout required** — the action internally runs `git fetch origin main` for trusted file restoration. Without `actions/checkout`, it fails with `fatal: not a git repository`.
|
||||
- **`id-token: write` permission** — required for OIDC token exchange with the Claude GitHub App.
|
||||
- **Trigger phrase stripping** — the action strips `@claude` from comments before passing to Claude. Prompts must reference commands without the prefix (e.g., `review` not `@claude review`).
|
||||
- **PR number injection** — the interactive workflow injects the PR/issue number into the prompt via `${{ github.event.issue.number }}` since Claude can't discover it from `gh pr view` when checked out on `main`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Code Comment Policy
|
||||
|
||||
**Default to no comment.** Applies to all code in this repository — C#, TypeScript, Razor, build scripts. Well-named identifiers and small functions are the primary form of self-documentation; comments are a fallback for the rare cases where the code itself cannot carry the meaning.
|
||||
|
||||
### When NOT to comment
|
||||
|
||||
- **Don't restate what the code does.** A line calling `resetState()` does not need `// Reset state`. A method named `validateInput` does not need `// Validate input`.
|
||||
- **Don't narrate a sequence of calls.** If three lines run in order, the order is in the code — don't paraphrase it above.
|
||||
- **Don't reference the current task, fix, callers, or PR.** No `// Fix for X`, `// Used by Y`, `// Added for the Z flow`, `// See PR #1234`. That belongs in commit messages and PR descriptions; in source it rots as the codebase evolves.
|
||||
|
||||
### When a comment IS justified
|
||||
|
||||
Write a comment only when **removing it would leave a future reader confused**. Concretely:
|
||||
|
||||
- **A non-obvious WHY.** A hidden constraint, business rule, or ordering requirement that is not visible from the code.
|
||||
- **A workaround for a specific bug or platform quirk.** Link the issue (`(#21996)`, `https://...`) so the comment can be deleted once the upstream fix lands.
|
||||
- **A subtle invariant** that the type system or method names do not enforce.
|
||||
- **An edge case the code intentionally handles** that would surprise a reader (e.g. "must run before X because Y").
|
||||
- **API documentation** — XML doc comments on C# members, JSDoc on exported TypeScript symbols. Required for the public contract; still keep them concise.
|
||||
|
||||
### TODOs
|
||||
|
||||
Allowed, but cheap to write and cheaper to leave behind. Keep them short and trackable: `// TODO (V19): remove once obsolete overload is gone` or `// TODO: pagination [NL]`. A TODO should have an author or a version trigger.
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Practices
|
||||
|
||||
### Tests for a bug fix must fail before the fix
|
||||
|
||||
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Build solution
|
||||
dotnet build
|
||||
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run specific test category
|
||||
dotnet test --filter "Category=Integration"
|
||||
|
||||
# Format code
|
||||
dotnet format
|
||||
|
||||
# Pack all projects
|
||||
dotnet pack -c Release
|
||||
```
|
||||
|
||||
### Integration Test Database Configuration
|
||||
|
||||
Integration tests are configured in `tests/Umbraco.Tests.Integration/appsettings.Tests.json`.
|
||||
|
||||
The `Tests:Database:DatabaseType` setting controls which database is used:
|
||||
- `"SQLite"` (default) - No external dependencies
|
||||
- `"LocalDb"` - Uses SQL Server LocalDB, required for SQL Server-specific tests (e.g., page-level locking, `sys.dm_tran_locks`)
|
||||
|
||||
SQL Server-specific tests use `BaseTestDatabase.IsSqlite()` to skip when running on SQLite.
|
||||
|
||||
### Key Projects
|
||||
|
||||
| Project | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| **Umbraco.Core** | Library | Interface contracts and domain models |
|
||||
| **Umbraco.Infrastructure** | Library | Service implementations and data access |
|
||||
| **Umbraco.Web.UI** | Application | Main web application (Razor/MVC) |
|
||||
| **Umbraco.Cms.Api.Management** | Library | Management API (backoffice) |
|
||||
| **Umbraco.Cms.Api.Delivery** | Library | Delivery API (headless CMS) |
|
||||
| **Umbraco.Cms.Api.Common** | Library | Shared API infrastructure |
|
||||
| **Umbraco.PublishedCache.HybridCache** | Library | Published content caching |
|
||||
| **Umbraco.Examine.Lucene** | Library | Full-text search indexing |
|
||||
|
||||
### Important Files
|
||||
|
||||
- **Solution**: `umbraco.sln`
|
||||
- **Build Config**: `Directory.Build.props`, `Directory.Packages.props`
|
||||
- **Code Style**: `.editorconfig`, `.globalconfig`
|
||||
- **Documentation**: `/CLAUDE.md`, `/src/Umbraco.Core/CLAUDE.md`, `/src/Umbraco.Cms.Api.Common/CLAUDE.md`
|
||||
|
||||
### Project-Specific Documentation
|
||||
|
||||
For detailed information about individual projects, see their CLAUDE.md files:
|
||||
- **Core Architecture**: `/src/Umbraco.Core/CLAUDE.md` - Service contracts, notification patterns
|
||||
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
|
||||
- **Backoffice Frontend**: `/src/Umbraco.Web.UI.Client/CLAUDE.md` - Lit web components, extension system, auth client
|
||||
|
||||
**Important**: When working on backoffice client code (anything under `src/Umbraco.Web.UI.Client/`), read `/src/Umbraco.Web.UI.Client/CLAUDE.md` first. It contains action-specific checklists (deprecation, testing, security, etc.) that are not duplicated here.
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Official Docs**: https://docs.umbraco.com/
|
||||
- **Contributing Guide**: `.github/CONTRIBUTING.md`
|
||||
- **Issues**: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
- **Community**: https://forum.umbraco.com/
|
||||
- **Releases**: https://releases.umbraco.com/
|
||||
|
||||
---
|
||||
|
||||
**This repository follows a layered architecture with strict dependency rules. The Core defines contracts, Infrastructure implements them, and Web/APIs consume them. Each layer can be understood independently, but dependencies always flow inward toward Core.**
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Company>Umbraco HQ</Company>
|
||||
<Authors>Umbraco</Authors>
|
||||
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
|
||||
@@ -14,22 +14,11 @@
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
TODO: Fix and remove overrides:
|
||||
[NU5104] Warning As Error: A stable release of a package should not have a prerelease dependency. Either modify the version spec of dependency
|
||||
-->
|
||||
<NoWarn>$(NoWarn),NU5104,SA1309</NoWarn>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),NU5104,SA1600</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- SourceLink -->
|
||||
<PropertyGroup>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@@ -40,13 +29,13 @@
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO (V18): Set to true once this version is released. -->
|
||||
<PackageValidationBaselineVersion>18.0.0</PackageValidationBaselineVersion>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>13.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2,97 +2,103 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- TODO (V18): Bump Umbraco.Code to 3.0.0 stable before release of 18.0.0 -->
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="3.0.0-beta" />
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
|
||||
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.4.0" />
|
||||
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
|
||||
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="15.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.16.0" />
|
||||
<PackageVersion Include="Markdig" Version="1.1.3" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.4.0" />
|
||||
<PackageVersion Include="NPoco" Version="6.2.0" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="7.5.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.5.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.5.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="NPoco" Version="5.7.1" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="4.10.1" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
||||
<PackageVersion Include="Smidge.InMemory" Version="4.6.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.6.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Dazinator.Extensions.FileProviders references vulnerable versions of the following: -->
|
||||
<!-- TODO (V18): Remove these pinned dependencies when the Dazinator.Extensions.FileProviders dependency is removed. -->
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<!-- Markdown references vulnerable version of the following: -->
|
||||
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Examine (via Microsoft.AspNetCore.DataProtection 8.0.4) references a vulnerable version of the following: -->
|
||||
<!-- TODO: Remove this pinned dependency when Examine updates its Microsoft.AspNetCore.DataProtection reference. -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
|
||||
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
# MCP (Model Context Protocol) Setup
|
||||
|
||||
This repository includes configuration for [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, enabling AI tooling integration for Umbraco CMS development workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
MCP allows AI assistants (like Claude) to interact with external tools and services. This repository configures two MCP servers:
|
||||
|
||||
| Server | Purpose | Package |
|
||||
|--------|---------|---------|
|
||||
| **umbraco-cms** | Manage Umbraco content types, documents, and media | `@umbraco-cms/mcp-dev@17` |
|
||||
| **playwright** | Browser automation for testing and debugging | `@playwright/mcp@latest` |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Umbraco Locally
|
||||
|
||||
Ensure your local Umbraco instance is running at `https://localhost:44339` (or update the URL in your `.env.local`).
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Copy the example environment file and customize it:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local` with your local settings:
|
||||
|
||||
```env
|
||||
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
|
||||
UMBRACO_CLIENT_SECRET=<your-client-secret>
|
||||
UMBRACO_BASE_URL=https://localhost:44339
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
UMBRACO_INCLUDE_TOOL_COLLECTIONS=data-type,document-type,document,media-type,media
|
||||
```
|
||||
|
||||
### 3. Configure the OAuth Client in Umbraco
|
||||
|
||||
Create an OAuth client in your Umbraco instance with:
|
||||
- **Client ID**: `umbraco-back-office-mcp`
|
||||
- **Client Secret**: The value you set in `.env.local`
|
||||
- **Grant Type**: Client Credentials
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `UMBRACO_CLIENT_ID` | OAuth client ID configured in Umbraco | `umbraco-back-office-mcp` |
|
||||
| `UMBRACO_CLIENT_SECRET` | OAuth client secret (keep secure!) | `your-secure-secret` |
|
||||
| `UMBRACO_BASE_URL` | URL of your local Umbraco instance | `https://localhost:44339` |
|
||||
| `NODE_TLS_REJECT_UNAUTHORIZED` | Set to `0` for self-signed certificates (local dev only) | `0` |
|
||||
| `UMBRACO_INCLUDE_TOOL_COLLECTIONS` | Comma-separated list of tool collections to enable | `data-type,document-type,document` |
|
||||
|
||||
### Tool Collections
|
||||
|
||||
The `UMBRACO_INCLUDE_TOOL_COLLECTIONS` variable controls which Umbraco MCP tools are available:
|
||||
|
||||
- `data-type` - Manage data types (property editors)
|
||||
- `document-type` - Manage document types (content types)
|
||||
- `document` - Manage content/documents
|
||||
- `media-type` - Manage media types
|
||||
- `media` - Manage media items
|
||||
|
||||
## Security Considerations
|
||||
|
||||
> **Warning**: This configuration is for **local development only**.
|
||||
|
||||
### Self-Signed Certificates
|
||||
|
||||
`NODE_TLS_REJECT_UNAUTHORIZED=0` disables SSL certificate validation. This is necessary for self-signed certificates in local development but:
|
||||
|
||||
- **Never use in production**
|
||||
- Affects all HTTPS connections made by Node.js processes
|
||||
- Consider trusting your local development certificate instead
|
||||
|
||||
### Client Secrets
|
||||
|
||||
- Never commit real secrets to source control
|
||||
- The `.env.local` file is gitignored for this reason
|
||||
- Use strong, unique secrets even in development
|
||||
- The example value `1234567890` in `.env.example` is a placeholder only
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Umbraco-CMS/
|
||||
├── .mcp.json # MCP server configuration
|
||||
├── .env.example # Example environment variables (committed)
|
||||
├── .env.local # Your local environment variables (gitignored)
|
||||
├── .claude/
|
||||
│ ├── settings.json # Shared Claude AI permissions (committed)
|
||||
│ └── settings.local.json # Local Claude overrides (gitignored)
|
||||
├── .gitignore # Ignores .env.local and settings.local.json
|
||||
└── MCP.md # This documentation (you are here)
|
||||
```
|
||||
|
||||
## Claude AI Permissions
|
||||
|
||||
The `.claude/settings.json` file configures which MCP tools Claude can use automatically without prompting. This is shared across the team for consistent developer experience.
|
||||
|
||||
### Customizing Permissions Locally
|
||||
|
||||
Create `.claude/settings.local.json` to override permissions for your environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__umbraco__get-all-document-types"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" errors
|
||||
|
||||
- Ensure Umbraco is running at the configured `UMBRACO_BASE_URL`
|
||||
- Check that the port matches your local setup
|
||||
|
||||
### "Unauthorized" errors
|
||||
|
||||
- Verify the OAuth client is configured in Umbraco
|
||||
- Check that `UMBRACO_CLIENT_ID` and `UMBRACO_CLIENT_SECRET` match
|
||||
- Ensure the client has appropriate permissions
|
||||
|
||||
### "Certificate" errors
|
||||
|
||||
- For local development, set `NODE_TLS_REJECT_UNAUTHORIZED=0` in `.env.local`
|
||||
- Alternatively, trust your local development certificate
|
||||
|
||||
### MCP server not starting
|
||||
|
||||
- Ensure Node.js is installed (v22+ recommended, matching .nvmrc)
|
||||
- Run `npx @umbraco-cms/mcp-dev@17 --help` to verify the package works
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Model Context Protocol Documentation](https://modelcontextprotocol.io/)
|
||||
- [Umbraco MCP Package](https://www.npmjs.com/package/@umbraco-cms/mcp-dev)
|
||||
- [Playwright MCP](https://www.npmjs.com/package/@playwright/mcp)
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/claude-code)
|
||||
@@ -1,541 +0,0 @@
|
||||
Third-Party Notices
|
||||
===================
|
||||
|
||||
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
|
||||
|
||||
Third-party software may contain dependencies that are not explicitly listed here.
|
||||
|
||||
This notice is not a license and does not grant any rights to use the third-party software.
|
||||
|
||||
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
|
||||
|
||||
URL: https://github.com/openid/AppAuth-JS
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2017 Google Inc.
|
||||
|
||||
---
|
||||
|
||||
AutoFixture: Write maintainable unit tests, faster
|
||||
|
||||
URL: https://github.com/AutoFixture/AutoFixture
|
||||
License: MIT License
|
||||
Copyright: 2013 Mark Seemann
|
||||
|
||||
---
|
||||
|
||||
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
|
||||
|
||||
URL: https://github.com/dotnet/aspnet-api-versioning
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and contributors
|
||||
|
||||
---
|
||||
|
||||
Babel: A JavaScript compiler
|
||||
|
||||
URL: https://babeljs.io/
|
||||
License: MIT License
|
||||
Copyright: 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
---
|
||||
|
||||
BenchmarkDotNet: Powerful .NET library for benchmarking
|
||||
|
||||
URL: https://github.com/dotnet/BenchmarkDotNet
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Bogus: A simple and sane data generator for populating objects that supports different locales.
|
||||
|
||||
URL: https://github.com/bchavez/Bogus
|
||||
License: MIT License
|
||||
Copyright: 2015 Brian Chavez
|
||||
|
||||
---
|
||||
|
||||
CommandLineParser: Terse syntax C# command line parser for .NET
|
||||
|
||||
URL: https://github.com/commandlineparser/commandline
|
||||
License: MIT License
|
||||
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
|
||||
|
||||
---
|
||||
|
||||
cross-env: A CLI tool to set environment variables across platforms
|
||||
|
||||
URL: https://github.com/kentcdodds/cross-env
|
||||
License: MIT License
|
||||
Copyright: 2017 Kent C. Dodds
|
||||
|
||||
---
|
||||
|
||||
Dazinator.Extensions.FileProviders: A library for file provider extensions
|
||||
|
||||
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
|
||||
License: MIT License
|
||||
Copyright: 2016 Darrell
|
||||
|
||||
---
|
||||
|
||||
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
|
||||
|
||||
URL: https://github.com/cure53/DOMPurify
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
|
||||
|
||||
---
|
||||
|
||||
Element Internals Polyfill: A polyfill for the Element Internals API
|
||||
|
||||
URL: https://github.com/calebdwilliams/element-internals-polyfill
|
||||
License: MIT License
|
||||
Copyright: 2021 Caleb Williams
|
||||
|
||||
---
|
||||
|
||||
Eslint: A tool for identifying and reporting on patterns in JavaScript
|
||||
|
||||
URL: https://eslint.org/
|
||||
License: MIT License
|
||||
Copyright: OpenJS Foundation and other contributors
|
||||
|
||||
---
|
||||
|
||||
Examine: A search and indexing library for .NET
|
||||
|
||||
URL: https://github.com/Shazwazza/Examine
|
||||
License: Microsoft Public License (Ms-PL)
|
||||
Copyright: 2023 Shannon Deminick
|
||||
|
||||
---
|
||||
|
||||
Globals: A library for managing global variables in JavaScript
|
||||
|
||||
URL: https://github.com/sindresorhus/globals
|
||||
License: MIT License
|
||||
Copyright: Sindre Sorhus
|
||||
|
||||
---
|
||||
|
||||
Html Agility Pack: An HTML parser for .NET
|
||||
|
||||
URL: https://html-agility-pack.net/
|
||||
License: MIT License
|
||||
Copyright: ZZZ Projects Inc.
|
||||
|
||||
---
|
||||
|
||||
ImageSharp: A cross-platform library for processing images in .NET
|
||||
|
||||
URL: https://github.com/SixLabors/ImageSharp
|
||||
License: Apache License, Version 2.0 under the Six Labors Split License
|
||||
Copyright: Six Labors
|
||||
|
||||
---
|
||||
|
||||
jsdiff: A JavaScript text differencing implementation
|
||||
|
||||
URL: https://github.com/kpdecker/jsdiff
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2009-2015 Kevin Decker <kpdecker@gmail.com>
|
||||
|
||||
---
|
||||
|
||||
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
|
||||
|
||||
URL: https://github.com/json-everything/json-everything
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
|
||||
|
||||
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
|
||||
License: MIT License
|
||||
Copyright: 2017 Milosz Krajewski
|
||||
|
||||
---
|
||||
|
||||
Lit: A simple library for building fast, lightweight web components
|
||||
|
||||
URL: https://lit.dev
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2020 Google LLC. All rights reserved.
|
||||
|
||||
---
|
||||
|
||||
Lucide: Beautiful & consistent icons for the web
|
||||
|
||||
URL: https://lucide.dev/
|
||||
License: ISC License
|
||||
Copyright: 2013-2022 Cole Bemis
|
||||
Copyright: 2022 Lucide Contributors
|
||||
|
||||
---
|
||||
|
||||
Madge: A dependency graph generator for JavaScript
|
||||
|
||||
URL: https://github.com/pahen/madge
|
||||
License: MIT License
|
||||
Copyright: 2017 Patrik Henningsson
|
||||
|
||||
---
|
||||
|
||||
MailKit: A library for sending email in .NET
|
||||
|
||||
URL: https://github.com/jstedfast/MailKit
|
||||
License: MIT License
|
||||
Copyright: 2013-2024 .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Markdig: A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
|
||||
|
||||
URL: https://github.com/xoofx/markdig
|
||||
License: BSD-2-Clause license
|
||||
Copyright: 2018+, Alexandre Mutel. All rights reserved.
|
||||
|
||||
---
|
||||
|
||||
Markdown: A library for parsing and compiling Markdown
|
||||
|
||||
URL: https://github.com/hey-red/Markdown
|
||||
License: MIT License
|
||||
Copyright: 2018 red
|
||||
|
||||
---
|
||||
|
||||
marked: A markdown parser and compiler
|
||||
|
||||
URL: https://marked.js.org/
|
||||
License: MIT License
|
||||
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
|
||||
|
||||
---
|
||||
|
||||
Message Pack: The extremely fast MessagePack serializer for C#
|
||||
|
||||
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
|
||||
License: MIT License
|
||||
Copyright: 2017 Yoshifumi Kawai and contributors
|
||||
|
||||
---
|
||||
|
||||
Miniprofiler: A mini profiler for .NET
|
||||
|
||||
URL: https://github.com/MiniProfiler/dotnet
|
||||
License: MIT License
|
||||
Copyright: .NET MiniProfiler Contributors
|
||||
|
||||
---
|
||||
|
||||
Monaco Editor: A browser-based code editor
|
||||
|
||||
URL: https://microsoft.github.io/monaco-editor/
|
||||
License: MIT License
|
||||
Copyright: 2016-present Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Moq: A mocking library for .NET
|
||||
|
||||
URL: https://github.com/moq/moq
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
|
||||
|
||||
---
|
||||
|
||||
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
|
||||
|
||||
URL: https://mswjs.io/
|
||||
License: MIT License
|
||||
Copyright: 2018–present Artem Zakharchenko
|
||||
|
||||
---
|
||||
|
||||
NCrontab: A cron schedule parser for .NET
|
||||
|
||||
URL: https://github.com/atifaziz/NCrontab
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2001 The OpenSymphony Group
|
||||
Copyright: 2008 Atif Aziz
|
||||
|
||||
---
|
||||
|
||||
Nerdbank.GitVersioning: A library for versioning .NET projects
|
||||
|
||||
URL: https://github.com/dotnet/Nerdbank.GitVersioning
|
||||
License: MIT License
|
||||
Copyright: .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
NJsonSchema: A JSON schema validator for .NET
|
||||
|
||||
URL: https://github.com/RicoSuter/NJsonSchema
|
||||
License: MIT License
|
||||
Copyright: 2022 Rico Suter
|
||||
|
||||
---
|
||||
|
||||
NPoco: A micro ORM for .NET
|
||||
|
||||
URL: https://github.com/schotime/NPoco
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Schotime
|
||||
|
||||
---
|
||||
|
||||
NUnit: A unit testing framework for .NET
|
||||
|
||||
URL: https://github.com/nunit/nunit
|
||||
License: MIT License
|
||||
Copyright: Charlie Poole, Rob Prouse and Contributors
|
||||
|
||||
---
|
||||
|
||||
Open Web Components: A set of standards and libraries for building web components
|
||||
|
||||
URL: https://open-wc.org/
|
||||
License: MIT License
|
||||
Copyright: 2018 open-wc
|
||||
|
||||
---
|
||||
|
||||
Openapi-ts: The OpenAPI to TypeScript codegen
|
||||
|
||||
URL: https://github.com/hey-api/openapi-ts
|
||||
License: MIT License
|
||||
Copyright: Hey API
|
||||
|
||||
---
|
||||
|
||||
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
|
||||
|
||||
URL: https://github.com/openiddict/openiddict-core
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Kévin Chalet
|
||||
|
||||
---
|
||||
|
||||
Playwright: A Node.js library to automate browser testing
|
||||
|
||||
URL: https://playwright.dev/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2025 Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Playwright-msw: A library to wrap Mock Service Worker with Playwright
|
||||
|
||||
URL: https://github.com/valendres/playwright-msw
|
||||
License: MIT License
|
||||
Copyright: 2022 Peter Weller
|
||||
|
||||
---
|
||||
|
||||
Prettier: An opinionated code formatter
|
||||
|
||||
URL: https://prettier.io/
|
||||
License: MIT License
|
||||
Copyright: James Long and contributors
|
||||
|
||||
---
|
||||
|
||||
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
|
||||
|
||||
URL: https://github.com/remarkjs/remark-gfm
|
||||
License: MIT License
|
||||
Copyright: Titus Wormer
|
||||
|
||||
---
|
||||
|
||||
rxjs: Reactive Extensions for JavaScript
|
||||
|
||||
URL: https://rxjs.dev/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2015-present Ben Lesh <ben@benlesh.com>, Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
|
||||
|
||||
---
|
||||
|
||||
Serilog: A diagnostic logging library for .NET
|
||||
|
||||
URL: https://github.com/serilog/serilog
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Serilog Contributors
|
||||
|
||||
---
|
||||
|
||||
Simple Icons: A set of SVG icons for popular brands
|
||||
|
||||
URL: https://simpleicons.org/
|
||||
License: CC0 1.0 Universal License
|
||||
Copyright: Simple Icons Contributors
|
||||
|
||||
---
|
||||
|
||||
Storybook: A UI component explorer for Web Components
|
||||
|
||||
URL: https://storybook.js.org/
|
||||
License: MIT License
|
||||
Copyright: 2024 Storybook
|
||||
|
||||
---
|
||||
|
||||
StyleCop.Analyzers: Analyzers for StyleCop
|
||||
|
||||
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
|
||||
License: MIT License
|
||||
Copyright: Tunnel Vision Laboratories, LLC
|
||||
|
||||
---
|
||||
|
||||
SVGO: A tool for optimizing SVG files
|
||||
|
||||
URL: https://svgo.dev/
|
||||
License: MIT License
|
||||
Copyright: Kir Belevich
|
||||
|
||||
---
|
||||
|
||||
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
|
||||
|
||||
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
|
||||
License: MIT License
|
||||
Copyright: 2016 Richard Morris
|
||||
|
||||
---
|
||||
|
||||
Tiny Glob: A tiny globbing library for Node.js
|
||||
|
||||
URL: https://github.com/terkelg/tiny-glob
|
||||
License: MIT License
|
||||
Copyright: 2018 Terkel
|
||||
|
||||
---
|
||||
|
||||
Tiptap: A renderless rich-text editor for the web
|
||||
|
||||
URL: https://tiptap.dev/
|
||||
License: MIT License
|
||||
Copyright: 2025 Tiptap GmbH
|
||||
|
||||
---
|
||||
|
||||
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
|
||||
|
||||
URL: https://github.com/justkey007/tsc-alias
|
||||
License: MIT License
|
||||
Copyright: 2018 Justkey
|
||||
|
||||
---
|
||||
|
||||
Typedoc: A documentation generator for TypeScript projects
|
||||
|
||||
URL: https://typedoc.org/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: Gerrit Birkeland and Contributors
|
||||
|
||||
---
|
||||
|
||||
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
|
||||
|
||||
URL: https://www.typescriptlang.org/
|
||||
License: Apache License, Version 2.0
|
||||
Copyright: 2012-present Microsoft Corporation
|
||||
|
||||
---
|
||||
|
||||
Typescript-eslint: A set of tools for linting TypeScript code
|
||||
|
||||
URL: https://github.com/typescript-eslint/typescript-eslint
|
||||
License: MIT License
|
||||
Copyright: 2019 typescript-eslint and other contributors
|
||||
|
||||
---
|
||||
|
||||
Typescript-json-schema: A library for generating JSON schema from TypeScript types
|
||||
|
||||
URL: https://github.com/YousefED/typescript-json-schema
|
||||
License: BSD 3-Clause License
|
||||
Copyright: 2016 typescript-json-schema contributors
|
||||
|
||||
---
|
||||
|
||||
Umbraco.Code: Provides code-level tools for Umbraco
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco-Code
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
|
||||
|
||||
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
Umbraco UI Library: A set of UI components for building web applications
|
||||
|
||||
URL: https://uui.umbraco.com/
|
||||
License: MIT License
|
||||
Copyright: 2005-present Umbraco A/S
|
||||
|
||||
---
|
||||
|
||||
uuid: A library for generating unique identifiers
|
||||
|
||||
URL: https://github.com/uuidjs/uuid
|
||||
License: MIT License
|
||||
Copyright: 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
---
|
||||
|
||||
Vite: A fast build tool and development server for modern web projects
|
||||
|
||||
URL: https://vite.dev/
|
||||
License: MIT License
|
||||
Copyright: 2019-present VoidZero Inc. and Vite contributors
|
||||
|
||||
---
|
||||
|
||||
Vite-plugin-static-copy: A Vite plugin for copying static files
|
||||
|
||||
URL: https://github.com/sapphi-red/vite-plugin-static-copy
|
||||
License: MIT License
|
||||
Copyright: 2021 sapphi-red
|
||||
|
||||
---
|
||||
|
||||
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
|
||||
|
||||
URL: https://github.com/aleclarson/vite-tsconfig-paths
|
||||
License: MIT License
|
||||
Copyright: Alec Larson
|
||||
|
||||
---
|
||||
|
||||
Web Component Analyzer: A tool for analyzing web components
|
||||
|
||||
URL: https://github.com/runem/web-component-analyzer
|
||||
License: MIT License
|
||||
Copyright: 2019 Rune Mehlsen
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "src/Umbraco.Web.UI.Client"
|
||||
},
|
||||
{
|
||||
"path": "src/Umbraco.Web.UI.Login"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,13 +3,17 @@
|
||||
{
|
||||
"src": [
|
||||
{
|
||||
"src": "../../src/Umbraco.Cms/bin/Release",
|
||||
"src": "../../src",
|
||||
"files": [
|
||||
"**/Umbraco.*.dll"
|
||||
"**/*.csproj"
|
||||
],
|
||||
"exclude": [
|
||||
"**/Umbraco.Cms.StaticAssets.dll",
|
||||
"**/Umbraco.Cms.Targets.dll"
|
||||
"**/obj/**",
|
||||
"**/bin/**",
|
||||
"**/Umbraco.Web.csproj",
|
||||
"**/Umbraco.Web.UI.csproj",
|
||||
"**/Umbraco.Cms.StaticAssets.csproj",
|
||||
"**/JsonSchema.csproj"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta name="generator" content="docfx {{_docfxVersion}}">
|
||||
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
|
||||
<link rel="icon" type="image/png" href="https://our.umbraco.com/assets/images/app-icons/favicon.png">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.min.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/main.css">
|
||||
<meta property="docfx:navrel" content="{{_navRel}}">
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
parameters:
|
||||
- name: testFolder
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: buildConfiguration
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: additionalEnvironmentVariables
|
||||
type: string
|
||||
default: 'false'
|
||||
|
||||
steps:
|
||||
- pwsh: |
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
displayName: Restore project
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Update application to use necessary app settings
|
||||
- pwsh: |
|
||||
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
|
||||
$destinationPath = "UmbracoProject"
|
||||
$jsonFiles = Get-ChildItem -Path $sourcePath -Filter "*.json"
|
||||
if ($jsonFiles) {
|
||||
$jsonFiles | ForEach-Object {
|
||||
Write-Host "Copying: $($_.FullName)"
|
||||
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
|
||||
}
|
||||
} else {
|
||||
Write-Host "No JSON files found."
|
||||
}
|
||||
displayName: Update application to use necessary app settings
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Update application to use necessary App_Plugins
|
||||
- pwsh: |
|
||||
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
|
||||
$destinationPath = "UmbracoProject"
|
||||
$appPluginsFolders = Get-ChildItem -Path $sourcePath -Directory -Filter "App_Plugins"
|
||||
if ($appPluginsFolders) {
|
||||
foreach ($folder in $appPluginsFolders) {
|
||||
Write-Host "Copying folder: $($folder.FullName)"
|
||||
Copy-Item -Path $folder.FullName -Destination $destinationPath -Recurse -Force
|
||||
}
|
||||
} else {
|
||||
Write-Host "No App_Plugins found."
|
||||
}
|
||||
displayName: Update application to use necessary app plugins
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Update application to use necessary classes
|
||||
- pwsh: |
|
||||
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
|
||||
$destinationPath = "UmbracoProject"
|
||||
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs" -Recurse
|
||||
if ($csharpFiles) {
|
||||
$csharpFiles | ForEach-Object {
|
||||
$relativePath = $_.FullName.Substring($sourcePath.Length + 1)
|
||||
$targetPath = Join-Path -Path $destinationPath -ChildPath $relativePath
|
||||
$targetDir = Split-Path -Path $targetPath -Parent
|
||||
if (-not (Test-Path -Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
Write-Host "Copying: $($_.FullName) -> $targetPath"
|
||||
Copy-Item -Path $_.FullName -Destination $targetPath -Force
|
||||
}
|
||||
} else {
|
||||
Write-Host "No C# files found."
|
||||
}
|
||||
displayName: Update application to use necessary classes
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
dotnet build UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
condition: and(succeeded(), eq(variables['additionalEnvironmentVariables'], 'false'))
|
||||
@@ -1,45 +0,0 @@
|
||||
parameters:
|
||||
- name: SA_PASSWORD
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: buildConfiguration
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: additionalEnvironmentVariables
|
||||
type: string
|
||||
default: 'false'
|
||||
|
||||
- name: DatabaseType
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
# Skips the SQLServer setup if the databaseType does not match
|
||||
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
|
||||
# Start SQL Server Linux
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=${{ parameters.SA_PASSWORD }}" mcr.microsoft.com/mssql/server:2022-latest
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
# Start SQL Server LocalDB Windows
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Run application for Linux
|
||||
- bash: |
|
||||
nohup dotnet run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
|
||||
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
|
||||
displayName: Run application (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['additionalEnvironmentVariables'], 'false'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Run application for Windows
|
||||
- pwsh: |
|
||||
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
|
||||
displayName: Run application (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['additionalEnvironmentVariables'], 'false'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
@@ -1,105 +0,0 @@
|
||||
parameters:
|
||||
- name: ASPNETCORE_URLS
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: testCommand
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: port
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: AZUREB2CTESTUSEREMAIL
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: AZUREB2CTESTUSERPASSWORD
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: DatabaseType
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
# Ensures we have the package wait-on installed
|
||||
- pwsh: npm install wait-on
|
||||
displayName: Install wait-on package
|
||||
|
||||
# Wait for either the port of the aspnetcore url
|
||||
- pwsh: |
|
||||
$Port = "${{ parameters.port }}"
|
||||
$Url = "${{ parameters.ASPNETCORE_URLS }}"
|
||||
|
||||
if ($Port -ne "") {
|
||||
Write-Host "Waiting on TCP port $Port"
|
||||
npx wait-on -v --interval 1000 --timeout 120000 "tcp:$Port"
|
||||
} else {
|
||||
Write-Host "Waiting on URL $Url"
|
||||
npx wait-on -v --interval 1000 --timeout 120000 "$Url"
|
||||
}
|
||||
displayName: Wait for application
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: ${{ parameters.testCommand }}
|
||||
displayName: Run Playwright tests
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
CommitId: $(Build.SourceVersion)
|
||||
AgentOs: $(Agent.OS)
|
||||
AZUREADB2CTESTUSEREMAIL: ${{ parameters.AZUREB2CTESTUSEREMAIL }}
|
||||
AZUREADB2CTESTUSERPASSWORD: ${{ parameters.AZUREB2CTESTUSERPASSWORD }}
|
||||
|
||||
# Stop application
|
||||
- bash: kill -15 $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Linux)
|
||||
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Windows)
|
||||
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
displayName: Stop SQL Server Docker image (Linux)
|
||||
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB stop MSSQLLocalDB
|
||||
displayName: Stop SQL Server LocalDB (Windows)
|
||||
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
@@ -1,50 +0,0 @@
|
||||
parameters:
|
||||
- name: nodeVersion
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightUserEmail
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightPassword
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: ASPNETCORE_URLS
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: npm_config_cache
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download NuGet artifacts
|
||||
inputs:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ parameters.nodeVersion }}
|
||||
npm_config_cache: ${{ parameters.npm_config_cache }}
|
||||
PlaywrightUserEmail: ${{ parameters.PlaywrightUserEmail }}
|
||||
PlaywrightPassword: ${{ parameters.PlaywrightPassword }}
|
||||
ASPNETCORE_URLS: ${{ parameters.ASPNETCORE_URLS }}
|
||||
|
||||
# Install Template
|
||||
- pwsh: |
|
||||
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
dotnet new nugetconfig
|
||||
dotnet nuget add source ./nupkg --name Local
|
||||
dotnet new install Umbraco.Templates@$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --exclude-gitignore --no-restore --no-update-check
|
||||
displayName: Install Template
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
@@ -1,777 +0,0 @@
|
||||
name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
|
||||
|
||||
pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily 0AM build (main)
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
displayName: Skip integration tests
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
- name: skipDifferentAppSettingsAcceptanceTests
|
||||
displayName: Skip acceptance tests with different app settings
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
- name: skipDefaultConfigAcceptanceTests
|
||||
displayName: Skip tests with DefaultConfig
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Can we slow our tests down when running on SQLite? That way we might be able to avoid DB locks
|
||||
- name: skipSqliteAcceptanceTests
|
||||
displayName: Skip SQLite acceptance tests
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
solution: umbraco.sln
|
||||
buildConfiguration: Release
|
||||
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
stages:
|
||||
###############################################
|
||||
## Build
|
||||
###############################################
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: "windows-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- template: templates/backoffice-install.yml
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet restore
|
||||
inputs:
|
||||
command: restore
|
||||
projects: $(solution)
|
||||
- task: DotNetCoreCLI@2
|
||||
name: build
|
||||
displayName: Run dotnet build and generate NuGet packages
|
||||
inputs:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/nupkg
|
||||
artifactName: nupkg
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish build artifacts
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
|
||||
- job: B
|
||||
displayName: Build Bellissima Package
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run build:for:npm
|
||||
displayName: Run build:for:npm
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- bash: |
|
||||
echo "##[command]Running npm pack"
|
||||
echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)"
|
||||
mkdir $(Build.ArtifactStagingDirectory)/npm
|
||||
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm
|
||||
mv .npmrc $(Build.ArtifactStagingDirectory)/npm/
|
||||
displayName: Run npm pack
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Bellissima npm artifact
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/npm
|
||||
artifactName: npm
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration Tests
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), ${{ eq(parameters.skipIntegrationTests, false) }})
|
||||
jobs:
|
||||
# Integration Tests (SQLite)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
displayName: Integration Tests (SQLite)
|
||||
strategy:
|
||||
matrix:
|
||||
# Windows:
|
||||
# vmImage: 'windows-latest'
|
||||
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
LinuxPart1Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart2Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
LinuxPart4Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
macOSPart1Of4:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
macOSPart2Of4:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
macOSPart3Of4:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
macOSPart4Of4:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
Tests__Database__DatabaseType: "Sqlite"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
# Test
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
displayName: Integration Tests (SQL Server)
|
||||
variables:
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
strategy:
|
||||
matrix:
|
||||
# Windows is split into 5 parts (ManagementApi split in two to avoid memory pressure on LocalDb); Linux into 4.
|
||||
WindowsPart1Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
WindowsPart2Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
WindowsPart3Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
WindowsPart4Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# ManagementApi, heavier sub-namespaces. Trailing dots prevent "User." from matching "UserGroup." etc.
|
||||
testFilter: "FullyQualifiedName~ManagementApi & (FullyQualifiedName~ManagementApi.Element. | FullyQualifiedName~ManagementApi.User. | FullyQualifiedName~ManagementApi.Document. | FullyQualifiedName~ManagementApi.DataType. | FullyQualifiedName~ManagementApi.DocumentType. | FullyQualifiedName~ManagementApi.MediaType. | FullyQualifiedName~ManagementApi.Template.)"
|
||||
WindowsPart5Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# ManagementApi, remainder (complement of Part4). vstest filters do not support group
|
||||
testFilter: "FullyQualifiedName~ManagementApi & FullyQualifiedName!~ManagementApi.Element. & FullyQualifiedName!~ManagementApi.User. & FullyQualifiedName!~ManagementApi.Document. & FullyQualifiedName!~ManagementApi.DataType. & FullyQualifiedName!~ManagementApi.DocumentType. & FullyQualifiedName!~ManagementApi.MediaType. & FullyQualifiedName!~ManagementApi.Template."
|
||||
LinuxPart1Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart2Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
LinuxPart4Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
# Start SQL Server
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- powershell: |
|
||||
$maxAttempts = 12
|
||||
$attempt = 0
|
||||
$status = ""
|
||||
|
||||
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
|
||||
Start-Sleep -Seconds 5
|
||||
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
|
||||
$status = docker inspect -f '{{.State.Status}}' mssql
|
||||
|
||||
if ($status -ne 'running') {
|
||||
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
|
||||
if ($status -eq 'running') {
|
||||
Write-Host "SQL Server container is running"
|
||||
docker ps -a
|
||||
} else {
|
||||
Write-Host "SQL Server did not become ready in time. Last known status: $status"
|
||||
docker logs mssql
|
||||
exit 1
|
||||
}
|
||||
displayName: Wait for SQL Server to be ready (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Test
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
displayName: Stop SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB stop MSSQLLocalDB
|
||||
displayName: Stop SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
- stage: DefaultConfigE2E
|
||||
displayName: Default Config E2E Tests
|
||||
dependsOn: [Build, Integration]
|
||||
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
# Enable console logging in Release mode
|
||||
SERILOG__WRITETO__0__NAME: Async
|
||||
SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console
|
||||
# Set unattended install settings
|
||||
UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123!
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com
|
||||
# Custom Umbraco settings
|
||||
UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false
|
||||
UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true
|
||||
UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true
|
||||
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
|
||||
UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0
|
||||
UMBRACO__CMS__GLOBAL__USEHTTPS: true
|
||||
UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false
|
||||
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
jobs:
|
||||
# E2E Tests
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
timeoutInMinutes: 180
|
||||
condition: ${{ and(eq(parameters.skipDefaultConfigAcceptanceTests, false), eq(parameters.skipSqliteAcceptanceTests, false)) }}
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
|
||||
DatabaseType: SQLite
|
||||
additionalEnvironmentVariables: false
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "DefaultConfig"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment Template
|
||||
- template: nightly-E2E-setup-template.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
|
||||
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
|
||||
- pwsh: |
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
displayName: Restore project
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
condition: succeeded()
|
||||
|
||||
# Run application Template
|
||||
- template: nightly-E2E-run-application-template.yml
|
||||
parameters:
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
buildConfiguration: ${{ variables.buildConfiguration }}
|
||||
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
|
||||
|
||||
# Run tests Template
|
||||
- template: nightly-E2E-run-tests-template.yml
|
||||
parameters:
|
||||
testCommand: $(testCommand)
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
timeoutInMinutes: 180
|
||||
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
DatabaseType: SQLServer
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
additionalEnvironmentVariables: false
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "ubuntu-latest"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "ubuntu-latest"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "ubuntu-latest"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
testCommand: "npm run testWindows -- --shard=1/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
WindowsPart2Of3:
|
||||
testCommand: "npm run testWindows -- --shard=2/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
WindowsPart3Of3:
|
||||
testCommand: "npm run testWindows -- --shard=3/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment Template
|
||||
- template: nightly-E2E-setup-template.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
|
||||
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
|
||||
- pwsh: |
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
displayName: Restore project
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
condition: succeeded()
|
||||
|
||||
# Run application Template
|
||||
- template: nightly-E2E-run-application-template.yml
|
||||
parameters:
|
||||
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
|
||||
buildConfiguration: ${{ variables.buildConfiguration }}
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
|
||||
|
||||
# Run tests Template
|
||||
- template: nightly-E2E-run-tests-template.yml
|
||||
parameters:
|
||||
testCommand: $(testCommand)
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
- stage: AdditionalConfigE2E
|
||||
displayName: Additional Config E2E Tests
|
||||
dependsOn: [Build, DefaultConfigE2E]
|
||||
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
PlaywrightPassword: UmbracoAcceptance123!
|
||||
PlaywrightUserEmail: playwright@umbraco.com
|
||||
jobs:
|
||||
- job:
|
||||
displayName: E2E Tests with Different App settings (SQL Server)
|
||||
condition: ${{ eq(parameters.skipDifferentAppSettingsAcceptanceTests, false) }}
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
DatabaseType: SQLServer
|
||||
strategy:
|
||||
matrix:
|
||||
# UnattendedInstallConfig
|
||||
WindowsUnattendedInstallConfig:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "UnattendedInstallConfig"
|
||||
testCommand: "npx playwright test --project=unattendedInstallConfig --grep=InstallSQLServer"
|
||||
port: 44331
|
||||
additionalEnvironmentVariables: false
|
||||
# DeliveryApiConfig
|
||||
WindowsDeliveryApiConfig:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "DeliveryApi"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=deliveryApi"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxDeliveryApiConfig:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "DeliveryApi"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=deliveryApi"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# ExternalLogin AzureADB2C
|
||||
WindowsExternalLoginAzureADB2C:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "ExternalLogin\\AzureADB2C"
|
||||
testCommand: "npx playwright test --project=externalLoginAzureADB2C"
|
||||
port: 44331
|
||||
packageName: "Microsoft.AspNetCore.Authentication.OpenIdConnect"
|
||||
packageVersion: "9.0.8"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: true
|
||||
# ExtensionRegistry
|
||||
WindowsExtensionRegistry:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "ExtensionRegistry"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=extensionRegistry"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxExtensionRegistry:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "ExtensionRegistry"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=extensionRegistry"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# EntityDataPicker
|
||||
WindowsEntityDataPicker:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "EntityDataPicker"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=entityDataPicker"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxEntityDataPicker:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "EntityDataPicker"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=entityDataPicker"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# ContentSettingConfig
|
||||
WindowsContentSettingsConfig:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxContentSettingsConfig:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# SMTP
|
||||
LinuxSMTP:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "SMTP"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=smtp"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment Template
|
||||
- template: nightly-E2E-setup-template.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
PlaywrightUserEmail: ${{ variables.PlaywrightUserEmail }}
|
||||
PlaywrightPassword: ${{ variables.PlaywrightPassword }}
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
|
||||
# Install NuGet package if specified in the matrix
|
||||
- pwsh: |
|
||||
Write-Host "Installing package $(packageName) version $(packageVersion)"
|
||||
dotnet add package $(packageName) --version $(packageVersion)
|
||||
displayName: "Install NuGet package: $(packageName)"
|
||||
workingDirectory: $(Agent.BuildDirectory)/app/UmbracoProject
|
||||
condition: and(succeeded(), ne(variables['packageName'], ''), ne(variables['packageVersion'], ''))
|
||||
|
||||
# Build application Template
|
||||
- template: nightly-E2E-build-template.yml
|
||||
parameters:
|
||||
testFolder: $(testFolder)
|
||||
buildConfiguration: ${{ variables.buildConfiguration }}
|
||||
additionalEnvironmentVariables: $(additionalEnvironmentVariables)
|
||||
|
||||
# Build application for AzureADB2C
|
||||
- pwsh: |
|
||||
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application for AzureADB2C
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
env:
|
||||
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
|
||||
AZUREADB2CTENANT: $(AZUREB2CTENANT)
|
||||
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
|
||||
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
|
||||
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
|
||||
condition: and(succeeded(), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
|
||||
|
||||
# Run application Template
|
||||
- template: nightly-E2E-run-application-template.yml
|
||||
parameters:
|
||||
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
|
||||
additionalEnvironmentVariables: $(additionalEnvironmentVariables)
|
||||
buildConfiguration: ${{ variables.buildConfiguration }}
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
# Run application for Linux with additional Environment Variables for Azure AD
|
||||
- bash: |
|
||||
nohup dotnet run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
|
||||
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
|
||||
displayName: Run application (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
env:
|
||||
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
|
||||
AZUREADB2CTENANT: $(AZUREB2CTENANT)
|
||||
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
|
||||
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
|
||||
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
|
||||
|
||||
# Run application for Windows with additional Environment Variables for Azure AD
|
||||
- pwsh: |
|
||||
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
|
||||
displayName: Run application (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
env:
|
||||
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
|
||||
AZUREADB2CTENANT: $(AZUREB2CTENANT)
|
||||
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
|
||||
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
|
||||
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
|
||||
|
||||
# Start SMTP4dev via Docker for SMTP tests
|
||||
- bash: |
|
||||
echo "Starting SMTP4dev container..."
|
||||
docker run -d --name smtp4dev -p 5000:80 -p 25:25 rnwood/smtp4dev
|
||||
|
||||
echo "Waiting for SMTP4dev to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:5000/api/messages > /dev/null; then
|
||||
echo "SMTP4dev is ready"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i: Waiting for SMTP4dev..."
|
||||
sleep 2
|
||||
done
|
||||
displayName: Start SMTP4dev Docker container (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), contains(variables['testFolder'], 'SMTP'))
|
||||
|
||||
# Run tests Template
|
||||
- template: nightly-E2E-run-tests-template.yml
|
||||
parameters:
|
||||
testCommand: $(testCommand)
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
port: $(port)
|
||||
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
|
||||
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
# Stop SMTP4dev container
|
||||
- bash: |
|
||||
echo "Stopping SMTP4dev container..."
|
||||
docker stop smtp4dev
|
||||
docker rm smtp4dev
|
||||
displayName: Stop SMTP4dev Docker container
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), contains(variables['testFolder'], 'SMTP'))
|
||||
|
||||
- stage: NotifySlackBot
|
||||
displayName: Notify Slack on Failure
|
||||
dependsOn: DefaultConfigE2E
|
||||
# This stage will only run if the E2E tests fail or succeed with issues
|
||||
condition: or(eq(dependencies.DefaultConfigE2E.result, 'failed'), eq(dependencies.DefaultConfigE2E.result, 'succeededWithIssues'))
|
||||
jobs:
|
||||
- job: PostToSlack
|
||||
displayName: Send Slack Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
# We send a payload to the Slack webhook URL, which will post a message to a specific channel
|
||||
- bash: |
|
||||
PROJECT_NAME_ENCODED=$(echo -n "$SYSTEM_TEAMPROJECT" | jq -s -R -r @uri)
|
||||
PIPELINE_URL="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${PROJECT_NAME_ENCODED}/_build/results?buildId=${BUILD_BUILDID}&view=ms.vss-test-web.build-test-results-tab"
|
||||
|
||||
PAYLOAD="{
|
||||
\"attachments\": [
|
||||
{
|
||||
\"color\": \"#ff0000\",
|
||||
\"pretext\": \"Nightly E2E pipeline *${BUILD_DEFINITIONNAME}* (#${BUILD_BUILDNUMBER}) failed!\",
|
||||
\"title\": \"View Failed E2E Test Results\",
|
||||
\"title_link\": \"$PIPELINE_URL\",
|
||||
\"fields\": [
|
||||
{
|
||||
\"title\": \"Pipeline\",
|
||||
\"value\": \"${BUILD_DEFINITIONNAME}\",
|
||||
\"short\": true
|
||||
},
|
||||
{
|
||||
\"title\": \"Build ID\",
|
||||
\"value\": \"${BUILD_BUILDID}\",
|
||||
\"short\": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"
|
||||
|
||||
echo "Sending Slack message to: $PIPELINE_URL"
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data "$PAYLOAD" \
|
||||
"$SLACK_WEBHOOK_URL"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
|
||||
@@ -8,10 +8,10 @@ schedules:
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v10/dev
|
||||
- v12/dev
|
||||
- v13/dev
|
||||
- v16/dev
|
||||
- v18/dev
|
||||
- main
|
||||
- v14/dev
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSource: 'fromFile'
|
||||
versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc
|
||||
|
||||
- template: set-npm-version.yml
|
||||
parameters:
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
|
||||
- task: Cache@2
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
|
||||
restoreKeys: |
|
||||
"npm_client" | "$(Agent.OS)"
|
||||
"npm_client"
|
||||
path: $(npm_config_cache)
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
displayName: Run npm ci (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
- checkout: none
|
||||
|
||||
- bash: |
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URI)/api/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
|
||||
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
|
||||
else
|
||||
project_id=$(curl --no-progress-meter \
|
||||
-X PUT "$(DT_API_URI)/api/v1/project" \
|
||||
-X PUT "$(DT_API_URL)/v1/project" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
|
||||
@@ -42,14 +42,15 @@ jobs:
|
||||
artifact: ${{ project.artifact }}
|
||||
displayName: Download ${{ project.artifact }} artifact
|
||||
|
||||
- task: upload-bom-dtrack@1
|
||||
inputs:
|
||||
dtrackURI: $(DT_API_URI)
|
||||
dtrackAPIKey: $(DT_API_KEY)
|
||||
dtrackProjAutoCreate: true
|
||||
dtrackProjName: '${{ parameters.projectName }}-${{ project.name }}'
|
||||
dtrackProjVersion: ${{ parameters.umbracoVersion }}
|
||||
dtrackParentProjName: ${{ parameters.projectName }}
|
||||
dtrackParentProjVersion: ${{ parameters.umbracoVersion }}
|
||||
bomFilePath: '$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}'
|
||||
- script: |
|
||||
curl --no-progress-meter --fail-with-body \
|
||||
-X POST "$(DT_API_URL)/v1/bom" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "autoCreate=true" \
|
||||
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
|
||||
-F "projectVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "parentName=${{ parameters.projectName }}" \
|
||||
-F "parentVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
|
||||
displayName: Upload ${{ project.name }} BOM to Dependency Track
|
||||
|
||||
@@ -28,9 +28,7 @@ steps:
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}
|
||||
STORAGE_STATE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -47,7 +45,3 @@ steps:
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
|
||||
- script: npm run build
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Build test helpers
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
parameters:
|
||||
- name: workingDirectory
|
||||
type: string
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
echo "##[command]Install nbgv"
|
||||
dotnet tool install --tool-path . nbgv
|
||||
echo "##[command]Running nbgv get-version"
|
||||
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
|
||||
echo "##[command]Running npm version"
|
||||
echo "##[debug]Version: $PACKAGE_VERSION"
|
||||
cd ${{ parameters.workingDirectory }}
|
||||
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
|
||||
displayName: Set NPM Version
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"url": "https://context7.com/umbraco/umbraco-cms",
|
||||
"public_key": "pk_GTIgsrGAQiHNxCirZBDIM"
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
"version": "8.0.415",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the <see cref="IOutputExpansionStrategy"/> for the current HTTP request context.
|
||||
/// </summary>
|
||||
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequestContextOutputExpansionStrategyAccessor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for accessing request-scoped services from the current HTTP context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to access.</typeparam>
|
||||
public abstract class RequestContextServiceAccessorBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequestContextServiceAccessorBase{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the service from the current HTTP context's request services.
|
||||
/// </summary>
|
||||
/// <param name="requestStartNodeService">When this method returns, contains the service instance if found; otherwise, <c>null</c>.</param>
|
||||
/// <returns><c>true</c> if the service was found; otherwise, <c>false</c>.</returns>
|
||||
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
|
||||
{
|
||||
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
|
||||
return requestStartNodeService is not null;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Common.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute used to map a class to a specific API for OpenAPI documentation generation.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class MapToApiAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MapToApiAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiName">The name of the API to map to.</param>
|
||||
public MapToApiAttribute(string apiName) => ApiName = apiName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the API this class is mapped to.
|
||||
/// </summary>
|
||||
public string ApiName { get; }
|
||||
}
|
||||
|
||||
@@ -1,113 +1,37 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// A fluent builder for creating RFC 7807 <see cref="ProblemDetails"/> responses.
|
||||
/// </summary>
|
||||
public class ProblemDetailsBuilder
|
||||
{
|
||||
private string? _title;
|
||||
private string? _detail;
|
||||
private string? _type;
|
||||
private string? _operationStatus;
|
||||
private IDictionary<string, object>? _extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the title of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="title">A short, human-readable summary of the problem type.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithTitle(string title)
|
||||
{
|
||||
_title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the detail of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="detail">A human-readable explanation specific to this occurrence of the problem.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithDetail(string detail)
|
||||
{
|
||||
_detail = detail;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type of the problem details.
|
||||
/// </summary>
|
||||
/// <param name="type">A URI reference that identifies the problem type.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithType(string type)
|
||||
{
|
||||
_type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the operation status from an enum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type representing operation statuses.</typeparam>
|
||||
/// <param name="operationStatus">The operation status enum value.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithOperationStatus<TEnum>(TEnum operationStatus)
|
||||
where TEnum : Enum
|
||||
{
|
||||
_operationStatus = operationStatus.ToString();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds request model validation errors to the problem details.
|
||||
/// </summary>
|
||||
/// <param name="errors">A dictionary of field names to error messages.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithRequestModelErrors(IDictionary<string, string[]> errors)
|
||||
=> WithExtension(nameof(HttpValidationProblemDetails.Errors).ToFirstLowerInvariant(), errors);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom extension to the problem details.
|
||||
/// </summary>
|
||||
/// <param name="key">The extension key.</param>
|
||||
/// <param name="value">The extension value.</param>
|
||||
/// <returns>The current builder instance for method chaining.</returns>
|
||||
public ProblemDetailsBuilder WithExtension(string key, object value)
|
||||
{
|
||||
_extensions ??= new Dictionary<string, object>();
|
||||
_extensions[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="ProblemDetails"/> instance with all configured values.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="ProblemDetails"/> instance.</returns>
|
||||
public ProblemDetails Build()
|
||||
{
|
||||
var problemDetails = new ProblemDetails
|
||||
public ProblemDetails Build() =>
|
||||
new()
|
||||
{
|
||||
Title = _title,
|
||||
Detail = _detail,
|
||||
Type = _type ?? "Error",
|
||||
};
|
||||
|
||||
if (_operationStatus is not null)
|
||||
{
|
||||
problemDetails.Extensions["operationStatus"] = _operationStatus;
|
||||
}
|
||||
|
||||
if (_extensions is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> extension in _extensions)
|
||||
{
|
||||
problemDetails.Extensions[extension.Key] = extension.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return problemDetails;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
# Umbraco.Cms.Api.Common
|
||||
|
||||
Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
**Type**: Class Library (NuGet Package)
|
||||
**Target Framework**: .NET 10.0
|
||||
**Purpose**: Common API infrastructure - OpenAPI/Swagger, JSON serialization, OpenIddict authentication, problem details
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
|
||||
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for browsing API documentation
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Asp.Versioning** - API versioning
|
||||
- **System.Text.Json** - Polymorphic JSON serialization
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `Umbraco.Core` - Domain models and service contracts
|
||||
- `Umbraco.Web.Common` - Web functionality
|
||||
|
||||
### Project Structure (46 files)
|
||||
|
||||
```
|
||||
Umbraco.Cms.Api.Common/
|
||||
├── OpenApi/ # OpenAPI transformers and schema generators
|
||||
│ ├── UmbracoSchemaIdGenerator.cs # Generates schema IDs (e.g., "PagedUserModel")
|
||||
│ ├── UmbracoOperationIdTransformer.cs # Generates operation IDs
|
||||
│ ├── SortTagsAndPathsTransformer.cs # Sorts OpenAPI tags and paths
|
||||
│ ├── TagActionsByGroupNameTransformer.cs # Tags operations by controller group
|
||||
│ ├── FixFileReturnTypesTransformer.cs # Fixes file return type schemas
|
||||
│ ├── RequireNonNullablePropertiesSchemaTransformer.cs # Schema nullability
|
||||
│ └── OpenApiRouteTemplatePipelineFilter.cs # Adds OpenAPI endpoints
|
||||
├── Serialization/ # JSON type resolution
|
||||
│ └── UmbracoJsonTypeInfoResolver.cs
|
||||
├── Configuration/ # Options configuration
|
||||
│ ├── ConfigureUmbracoOpenApiOptionsBase.cs
|
||||
│ └── ConfigureOpenIddict.cs
|
||||
├── DependencyInjection/ # Service registration
|
||||
│ ├── UmbracoBuilderApiExtensions.cs
|
||||
│ └── UmbracoBuilderAuthExtensions.cs
|
||||
├── Builders/ # RFC 7807 problem details
|
||||
│ └── ProblemDetailsBuilder.cs
|
||||
├── ViewModels/Pagination/ # Common DTOs
|
||||
└── Security/ # Auth paths and handlers
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
1. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
|
||||
2. **Options Pattern** - All configuration via `IConfigureOptions<T>`
|
||||
|
||||
---
|
||||
|
||||
## 2. Commands
|
||||
|
||||
See "Quick Reference" section at bottom for common commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Patterns
|
||||
|
||||
### Schema ID Generation (OpenApi/UmbracoSchemaIdGenerator.cs)
|
||||
|
||||
Static utility class that generates OpenAPI schema IDs following Umbraco's naming conventions:
|
||||
|
||||
```csharp
|
||||
// Add "Model" suffix to avoid TypeScript name clashes
|
||||
if (name.EndsWith("Model") == false)
|
||||
{
|
||||
// because some models names clash with common classes in TypeScript (i.e. Document),
|
||||
// we need to add a "Model" postfix to all models
|
||||
name = $"{name}Model";
|
||||
}
|
||||
|
||||
// Remove invalid characters to prevent OpenAPI generation errors
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
```
|
||||
|
||||
**Generic Type Handling**: `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
|
||||
|
||||
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
|
||||
|
||||
```csharp
|
||||
// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference
|
||||
// properties, because "$ref" does not mix and match well with "nullable" in OpenAPI.
|
||||
if (type.IsInterface is false)
|
||||
{
|
||||
return new[] { type };
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Interfaces must return concrete types to avoid OpenAPI schema conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing
|
||||
|
||||
**Location**: No direct tests - tested via integration tests in consuming APIs
|
||||
|
||||
**How to test changes**:
|
||||
```bash
|
||||
# Run integration tests that exercise this library
|
||||
dotnet test tests/Umbraco.Tests.Integration/
|
||||
|
||||
# Verify OpenAPI generation
|
||||
# 1. Run the application: dotnet run --project src/Umbraco.Web.UI
|
||||
# 2. Navigate to /umbraco/openapi/ for Swagger UI
|
||||
# 3. Check schema IDs and operation IDs
|
||||
# OpenAPI JSON documents available at:
|
||||
# - /umbraco/openapi/management.json (Management API)
|
||||
# - /umbraco/openapi/delivery.json (Delivery API)
|
||||
```
|
||||
|
||||
**Focus areas when testing**:
|
||||
- OpenAPI document generation (schema IDs, operation IDs)
|
||||
- Polymorphic JSON serialization/deserialization
|
||||
- OpenIddict authentication flow
|
||||
- Problem details formatting
|
||||
|
||||
---
|
||||
|
||||
## 5. OpenIddict Authentication
|
||||
|
||||
### Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs)
|
||||
|
||||
**Reference Tokens over JWT** (line 76-80):
|
||||
```csharp
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
options
|
||||
.UseReferenceAccessTokens()
|
||||
.UseReferenceRefreshTokens();
|
||||
```
|
||||
|
||||
**Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection.
|
||||
|
||||
**Token Lifetime** (line 88-91):
|
||||
```csharp
|
||||
// Make the access token lifetime 25% of the refresh token lifetime
|
||||
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
|
||||
options.SetRefreshTokenLifetime(timeOut);
|
||||
```
|
||||
|
||||
**PKCE Required** (line 59-63):
|
||||
```csharp
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
.AllowAuthorizationCodeFlow()
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
```
|
||||
|
||||
**Endpoints**: Backoffice `/umbraco/management/api/v1/security/*`, Member `/umbraco/member/api/v1/security/*`
|
||||
|
||||
### Secure Cookie-Based Token Storage (v17+)
|
||||
|
||||
**Implementation** (DependencyInjection/HideBackOfficeTokensHandler.cs):
|
||||
|
||||
Back-office tokens are hidden from client-side JavaScript via HTTP-only cookies:
|
||||
|
||||
```csharp
|
||||
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
|
||||
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
|
||||
|
||||
// Tokens are encrypted via Data Protection and stored in cookies
|
||||
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
|
||||
context.Response.AccessToken = "[redacted]"; // Client sees redacted value
|
||||
```
|
||||
|
||||
**Key Security Features** (lines 143-165): `HttpOnly`, `IsEssential`, `Path="/"`, `Secure` (HTTPS), `__Host-` prefix
|
||||
|
||||
**Configuration**: `BackOfficeTokenCookieSettings.Enabled` (default: true in v17+)
|
||||
|
||||
**Implications**: Client-side cannot access tokens; encrypted with Data Protection; load balancing needs shared key ring; API requests need `credentials: include`
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Issues & Edge Cases
|
||||
|
||||
### Polymorphic Deserialization Requires `$type`
|
||||
|
||||
**Issue**: Deserializing to an interface without `$type` discriminator fails.
|
||||
|
||||
**Handled in** (Json/NamedSystemTextJsonInputFormatter.cs:24-29):
|
||||
```csharp
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
|
||||
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
|
||||
return await InputFormatterResult.FailureAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Clients must include `$type` property for interface types, or use concrete types.
|
||||
|
||||
### Schema ID Collisions with TypeScript
|
||||
|
||||
**Issue**: Type names like `Document` clash with TypeScript built-ins.
|
||||
|
||||
**Solution**: `UmbracoSchemaIdGenerator` adds "Model" suffix to all schema names.
|
||||
|
||||
### Generic Type Handling
|
||||
|
||||
**Issue**: `PagedViewModel<T>` needs flattened schema name.
|
||||
|
||||
**Solution**: `UmbracoSchemaIdGenerator.Generate()` flattens generic types:
|
||||
- `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
|
||||
|
||||
---
|
||||
|
||||
## 7. Extending This Library
|
||||
|
||||
### Adding Custom OpenAPI Transformers
|
||||
|
||||
OpenAPI transformers are scoped per-document. To customize a document, implement `IOpenApiDocumentTransformer`, `IOpenApiOperationTransformer`, or `IOpenApiSchemaTransformer` and register with your OpenAPI options.
|
||||
|
||||
For schema ID generation, use the static `UmbracoSchemaIdGenerator.Generate(Type)` method.
|
||||
|
||||
### Customizing Problem Details
|
||||
|
||||
```csharp
|
||||
var problemDetails = new ProblemDetailsBuilder()
|
||||
.WithTitle("Validation Failed")
|
||||
.WithDetail("The request contains errors")
|
||||
.WithType("ValidationError")
|
||||
.WithOperationStatus(MyOperationStatus.ValidationFailed)
|
||||
.WithRequestModelErrors(errors)
|
||||
.Build();
|
||||
|
||||
return BadRequest(problemDetails);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Project-Specific Notes
|
||||
|
||||
### Per-Document Transformer Scoping
|
||||
|
||||
With Microsoft.AspNetCore.OpenApi, transformers are configured per OpenAPI document. This means custom transformers only apply to the documents they're registered with, not globally. Each API (Management, Delivery) configures its own transformers via `ConfigureUmbracoOpenApiOptionsBase` subclasses.
|
||||
|
||||
### Performance: Subtype Caching
|
||||
|
||||
**Why**: Cache discovered subtypes (UmbracoJsonTypeInfoResolver.cs:14) to avoid expensive reflection calls
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Polymorphic Deserialization**:
|
||||
- Requires `$type` discriminator in JSON for interfaces
|
||||
- Only discovers types in Umbraco namespaces
|
||||
- Not all .NET types are discoverable
|
||||
|
||||
2. **OpenAPI Schema Generation**:
|
||||
- Generic types are flattened (e.g., `PagedViewModel<T>` → `PagedTModel`)
|
||||
- Type names may need "Model" suffix to avoid clashes
|
||||
|
||||
3. **OpenIddict Multi-Server**:
|
||||
- Requires shared Data Protection key ring
|
||||
- All servers must have synchronized clocks (NTP)
|
||||
- Reference tokens require database storage
|
||||
|
||||
### External Dependencies
|
||||
|
||||
**OpenIddict**:
|
||||
- OAuth 2.0 / OpenID Connect provider
|
||||
- Version: See `Directory.Packages.props`
|
||||
- Uses ASP.NET Core Data Protection for token encryption
|
||||
|
||||
**Microsoft.AspNetCore.OpenApi**:
|
||||
- OpenAPI 3.1.1 document generation
|
||||
- Custom transformers: `SchemaIdTransformer`, `OperationIdTransformer`, `MimeTypeDocumentTransformer`, `ServerTransformer`
|
||||
|
||||
**Swashbuckle.AspNetCore.SwaggerUI**:
|
||||
- Swagger UI for browsing and testing API endpoints
|
||||
- Accessed at `/umbraco/openapi/`
|
||||
|
||||
**Asp.Versioning**:
|
||||
- API versioning via `ApiVersion` attribute
|
||||
- API explorer integration for multi-version Swagger docs
|
||||
|
||||
### Configuration
|
||||
|
||||
**HTTPS**: `DisableTransportSecurityRequirement` for local dev only (ConfigureOpenIddict.cs:14). **Warning**: Never disable in production.
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
Consuming APIs call `builder.AddUmbracoOpenApi().AddUmbracoOpenIddict()`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Build project
|
||||
dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
|
||||
|
||||
# Pack for NuGet
|
||||
dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release
|
||||
|
||||
# Test via integration tests
|
||||
dotnet test tests/Umbraco.Tests.Integration/
|
||||
|
||||
# Check packages
|
||||
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated
|
||||
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable
|
||||
```
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose | File |
|
||||
|-------|---------|------|
|
||||
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
|
||||
| `UmbracoSchemaIdGenerator` | Generate OpenAPI schema IDs | OpenApi/UmbracoSchemaIdGenerator.cs |
|
||||
| `UmbracoOperationIdTransformer` | Generate operation IDs | OpenApi/UmbracoOperationIdTransformer.cs |
|
||||
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
|
||||
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
|
||||
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
|
||||
| `PagedViewModel<T>` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs |
|
||||
|
||||
### Important Files
|
||||
|
||||
- `Umbraco.Cms.Api.Common.csproj` - Project dependencies
|
||||
- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-31)
|
||||
- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 20-183)
|
||||
- `Security/Paths.cs` - API endpoint path constants
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Root documentation**: `/CLAUDE.md` - Repository overview
|
||||
- **Core patterns**: `/src/Umbraco.Core/CLAUDE.md` - Core contracts and patterns
|
||||
- **Official docs**: https://docs.umbraco.com/
|
||||
- **OpenIddict docs**: https://documentation.openiddict.com/
|
||||
|
||||
---
|
||||
|
||||
**This library is the foundation for all Umbraco CMS REST APIs. Focus on OpenAPI customization, authentication configuration, and polymorphic serialization when working here.**
|
||||
@@ -1,14 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="ApiBehaviorOptions"/> for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureApiBehaviorOptions : IConfigureOptions<ApiBehaviorOptions>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(ApiBehaviorOptions options) =>
|
||||
// disable ProblemDetails as default result type for every non-success response (i.e. 404)
|
||||
// - see https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the OpenAPI options for the Default API.
|
||||
/// </summary>
|
||||
internal class ConfigureDefaultApiOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string ApiName => DefaultApiConfiguration.ApiName;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiTitle => "Default API";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiVersion => "Latest";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiDescription => "All endpoints not defined under specific APIs";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ShouldInclude(ApiDescription apiDescription)
|
||||
{
|
||||
// Exclude controllers with ExcludeFromDefaultOpenApiDocumentAttribute
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<ExcludeFromDefaultOpenApiDocumentAttribute>() is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include if explicitly mapped to this document
|
||||
if (base.ShouldInclude(apiDescription))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Include endpoints not explicitly assigned to another document
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
|
||||
return string.IsNullOrEmpty(apiVersionMetadata.Name);
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,12 @@ using Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="MvcOptions"/> with named JSON input and output formatters for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureMvcJsonOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
|
||||
/// <param name="jsonOptions">The JSON options monitor.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ConfigureMvcJsonOptions(
|
||||
string jsonOptionsName,
|
||||
IOptionsMonitor<JsonOptions> jsonOptions,
|
||||
@@ -30,7 +21,6 @@ public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures OpenIddict server options for Umbraco authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Disables transport security requirement when HTTPS is not configured in global settings.
|
||||
/// Warning: This should only be used in development environments.
|
||||
/// </remarks>
|
||||
internal sealed class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
|
||||
{
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureOpenIddict"/> class.
|
||||
/// </summary>
|
||||
/// <param name="globalSettings">The global settings options.</param>
|
||||
public ConfigureOpenIddict(IOptions<GlobalSettings> globalSettings) => _globalSettings = globalSettings;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(OpenIddictServerAspNetCoreOptions options)
|
||||
=> options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for configuring OpenAPI options for Umbraco APIs.
|
||||
/// </summary>
|
||||
internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOptions<OpenApiOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name/identifier of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name/identifier of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiTitle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiDescription { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(OpenApiOptions options) => Configure(Options.DefaultName, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(string? name, OpenApiOptions options)
|
||||
{
|
||||
if (name != ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigureOpenApi(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the OpenAPI options for the specified API.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="OpenApiOptions"/> instance to configure.</param>
|
||||
protected virtual void ConfigureOpenApi(OpenApiOptions options)
|
||||
{
|
||||
options.AddDocumentTransformer((document, _, _) =>
|
||||
{
|
||||
document.Info = new OpenApiInfo
|
||||
{
|
||||
Title = ApiTitle,
|
||||
Version = ApiVersion,
|
||||
Description = ApiDescription,
|
||||
};
|
||||
document.Servers?.Clear();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
options.ShouldInclude = ShouldInclude;
|
||||
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
|
||||
|
||||
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
|
||||
|
||||
// Tag actions by group name and cleanup unused tags (caused by the tag changes)
|
||||
options
|
||||
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in this OpenAPI document.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to evaluate.</param>
|
||||
/// <returns><c>true</c> if the endpoint should be included; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool ShouldInclude(ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(ApiName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
|
||||
return apiVersionMetadata.Name == ApiName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOptions<ApiVersioningOptions> apiVersioningOptions,
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector)
|
||||
{
|
||||
_apiVersioningOptions = apiVersioningOptions;
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
DefaultApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "Default API",
|
||||
Version = "Latest",
|
||||
Description = "All endpoints not defined under specific APIs"
|
||||
});
|
||||
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value));
|
||||
swaggerGenOptions.DocInclusionPredicate((name, api) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(api.GroupName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
return controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
|
||||
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
|
||||
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
|
||||
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
|
||||
swaggerGenOptions.SupportNonNullableReferenceTypes();
|
||||
}
|
||||
|
||||
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
|
||||
private static string ActionOrderBy(ApiDescription apiDesc)
|
||||
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Contains default configuration values for the API.
|
||||
/// </summary>
|
||||
internal static class DefaultApiConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The default API name used for endpoints not assigned to a specific API.
|
||||
/// </summary>
|
||||
public const string ApiName = "default";
|
||||
}
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Handles secure storage of back-office authentication tokens in HTTP-only cookies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler intercepts OpenIddict token responses for the back-office client and stores
|
||||
/// access tokens, refresh tokens, and PKCE codes in encrypted HTTP-only cookies. The tokens
|
||||
/// are redacted from the response to prevent client-side JavaScript access.
|
||||
/// </remarks>
|
||||
internal sealed class HideBackOfficeTokensHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractRevocationRequestContext>,
|
||||
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
|
||||
INotificationHandler<UserLogoutSuccessNotification>
|
||||
{
|
||||
private const string RedactedTokenValue = "[redacted]";
|
||||
|
||||
// The __Host- prefix enforces secure cookies at browser level (requires Secure, Path=/, no Domain).
|
||||
// For local development over HTTP, we use a simpler prefix to avoid browser rejection.
|
||||
private const string SecureCookiePrefix = "__Host-";
|
||||
private readonly string _accessTokenCookieName = "umbAccessToken";
|
||||
private readonly string _refreshTokenCookieName = "umbRefreshToken";
|
||||
private readonly string _pkceCodeCookieName = "umbPkceCode";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
private readonly ILogger<HideBackOfficeTokensHandler> _logger;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HideBackOfficeTokensHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
/// <param name="dataProtectionProvider">The data protection provider for encrypting cookie values.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="backOfficeTokenCookieSettings">The back-office token cookie settings.</param>
|
||||
/// <param name="globalSettings">The global settings.</param>
|
||||
public HideBackOfficeTokensHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<HideBackOfficeTokensHandler> logger,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
IOptions<BackOfficeTokenCookieSettings> backOfficeTokenCookieSettings,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_dataProtectionProvider = dataProtectionProvider;
|
||||
_logger = logger;
|
||||
_backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
|
||||
_accessTokenCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
_refreshTokenCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
_pkceCodeCookieName += _backOfficeTokenCookieSettings.SiteName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when tokens (access and refresh tokens) are issued to a client. For the back-office client,
|
||||
/// we will intercept the response, write the tokens from the response into HTTP-only cookies, and redact the
|
||||
/// tokens from the response, so they are not exposed to the client.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyTokenResponseContext context)
|
||||
{
|
||||
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
if (context.Response.AccessToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, _accessTokenCookieName, context.Response.AccessToken);
|
||||
context.Response.AccessToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
if (context.Response.RefreshToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, _refreshTokenCookieName, context.Response.RefreshToken);
|
||||
context.Response.RefreshToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a PKCE code is issued to the client. For the back-office client, we will intercept the
|
||||
/// response, write the PKCE code from the response into a HTTP-only cookie, and redact the code from the response,
|
||||
/// so it's not exposed to the client.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyAuthorizationResponseContext context)
|
||||
{
|
||||
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (context.Response.Code is not null)
|
||||
{
|
||||
SetCookie(GetHttpContext(), _pkceCodeCookieName, context.Response.Code);
|
||||
context.Response.Code = RedactedTokenValue;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when requesting new tokens.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ExtractTokenRequestContext context)
|
||||
{
|
||||
if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
// Handle when the PKCE code is being exchanged for an access token.
|
||||
if (context.Request.Code == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, _pkceCodeCookieName, out var code))
|
||||
{
|
||||
context.Request.Code = code;
|
||||
|
||||
// We won't need the PKCE cookie after this, let's remove it.
|
||||
RemoveCookie(httpContext, _pkceCodeCookieName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PCKE codes should always be redacted. If we got here, someone might be trying to pass another PKCE
|
||||
// code. For security reasons, explicitly discard the code (if any) to be on the safe side.
|
||||
context.Request.Code = null;
|
||||
}
|
||||
|
||||
// Handle when a refresh token is being exchanged for a new access token.
|
||||
if (context.Request.RefreshToken == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, _refreshTokenCookieName, out var refreshToken))
|
||||
{
|
||||
context.Request.RefreshToken = refreshToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we got here, either the refresh token was not redacted, or nothing was found in the refresh token cookie.
|
||||
// If OpenIddict found a refresh token, it could be an old token that is potentially still valid. For security
|
||||
// reasons, we cannot accept that; at this point, we expect the refresh tokens to be explicitly redacted.
|
||||
context.Request.RefreshToken = null;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a token revocation request is received.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ExtractRevocationRequestContext context)
|
||||
{
|
||||
if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
// Determine which cookie to read based on the token type hint.
|
||||
var cookieName = context.Request.TokenTypeHint == OpenIddictConstants.TokenTypeHints.RefreshToken
|
||||
? _refreshTokenCookieName
|
||||
: _accessTokenCookieName;
|
||||
|
||||
if (context.Request.Token == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, cookieName, out var token))
|
||||
{
|
||||
context.Request.Token = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we got here, either the token was not redacted, or nothing was found in the expected cookie.
|
||||
// If OpenIddict found a token, it could be an old token that is potentially still valid. For security
|
||||
// reasons, we cannot accept that; at this point, we expect the tokens to be explicitly redacted.
|
||||
context.Request.Token = null;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when extracting the auth context for a client request.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessAuthenticationContext context)
|
||||
{
|
||||
// For the back-office client, this only happens when an access token is sent to the API.
|
||||
if (context.AccessToken != RedactedTokenValue)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (TryGetCookie(GetHttpContext(), _accessTokenCookieName, out var accessToken))
|
||||
{
|
||||
context.AccessToken = accessToken;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(UserLogoutSuccessNotification notification)
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext is null)
|
||||
{
|
||||
// For some reason there is no ambient HTTP context, so we can't clean up the cookies.
|
||||
// This is OK, because the tokens in the cookies have already been revoked at user sign-out,
|
||||
// so the cookie clean-up is mostly cosmetic.
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveCookie(httpContext, _accessTokenCookieName);
|
||||
RemoveCookie(httpContext, _refreshTokenCookieName);
|
||||
}
|
||||
|
||||
private HttpContext GetHttpContext()
|
||||
=> _httpContextAccessor.GetRequiredHttpContext();
|
||||
|
||||
private string GetCookieKey(HttpContext httpContext, string cookieName)
|
||||
=> _globalSettings.UseHttps || httpContext.Request.IsHttps
|
||||
? $"{SecureCookiePrefix}{cookieName}"
|
||||
: cookieName;
|
||||
|
||||
private void SetCookie(HttpContext httpContext, string cookieName, string value)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
|
||||
|
||||
RemoveCookie(httpContext, cookieName);
|
||||
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
|
||||
}
|
||||
|
||||
private void RemoveCookie(HttpContext httpContext, string cookieName)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
|
||||
}
|
||||
|
||||
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
|
||||
new()
|
||||
{
|
||||
// Prevent the client-side scripts from accessing the cookie.
|
||||
HttpOnly = true,
|
||||
|
||||
// Mark the cookie as essential to the application, to enforce it despite any
|
||||
// data collection consent options. This aligns with how ASP.NET Core Identity
|
||||
// does when writing cookies for cookie authentication.
|
||||
IsEssential = true,
|
||||
|
||||
// Cookie path must be root for optimal security.
|
||||
Path = "/",
|
||||
|
||||
// For optimal security, the cooke must be secure. However, Umbraco allows for running development
|
||||
// environments over HTTP, so we need to take that into account here.
|
||||
// Thus, we will make the cookie secure if:
|
||||
// - HTTPS is explicitly enabled by config (default for production environments), or
|
||||
// - The current request is over HTTPS (meaning the environment supports it regardless of config).
|
||||
Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps,
|
||||
|
||||
// SameSite is configurable (see BackOfficeTokenCookieSettings for defaults):
|
||||
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
|
||||
};
|
||||
|
||||
private bool TryGetCookie(HttpContext httpContext, string cookieName, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
if (httpContext.Request.Cookies.TryGetValue(key, out var cookieValue))
|
||||
{
|
||||
try
|
||||
{
|
||||
value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider);
|
||||
return true;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
// Decryption can fail if the data protection key ring has changed
|
||||
// (e.g., after deployment, app pool recycle, or slot swap).
|
||||
// Treat this as a missing cookie — the user will need to re-authenticate.
|
||||
_logger.LogWarning(ex, "Failed to decrypt back-office token cookie '{CookieName}'. The user will need to re-authenticate.", cookieName);
|
||||
RemoveCookie(httpContext, cookieName);
|
||||
}
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SameSiteMode ParseSameSiteMode(string sameSiteMode) =>
|
||||
Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result)
|
||||
? result
|
||||
: throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -6,18 +6,8 @@ using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IMvcBuilder"/>.
|
||||
/// </summary>
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds named JSON serialization options to the MVC builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The MVC builder.</param>
|
||||
/// <param name="settingsName">The name for the JSON options configuration.</param>
|
||||
/// <param name="configure">The action to configure the JSON options.</param>
|
||||
/// <returns>The MVC builder for method chaining.</returns>
|
||||
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
|
||||
{
|
||||
builder.Services.Configure(settingsName, configure);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for replacing the internal Microsoft.AspNetCore.OpenApi schema service registration.
|
||||
/// </summary>
|
||||
internal static class OpenApiSchemaServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The full name of the internal Microsoft type whose registration is replaced.
|
||||
/// Used for a stringly-typed <see cref="ServiceDescriptor"/> lookup because the type is not publicly accessible.
|
||||
/// </summary>
|
||||
internal const string OpenApiSchemaServiceFullName = "Microsoft.AspNetCore.OpenApi.OpenApiSchemaService";
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
|
||||
/// generation uses the named <see cref="JsonOptions"/> rather than the default HTTP JSON options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="documentName">The OpenAPI document key (matches the keyed singleton registered by <c>AddOpenApi(documentName)</c>).</param>
|
||||
/// <param name="jsonOptionsName">The named <see cref="JsonOptions"/> to use during schema generation for this document.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
|
||||
/// </remarks>
|
||||
public static IServiceCollection ReplaceOpenApiSchemaService(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string jsonOptionsName)
|
||||
=> services.ReplaceOpenApiSchemaService(
|
||||
documentName,
|
||||
sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
|
||||
/// generation uses the <see cref="JsonOptions"/> instance produced by the supplied factory. Use this overload when
|
||||
/// the options need to be resolved from the service provider, computed at the last moment, or built in a way that
|
||||
/// doesn't fit the named-options lookup.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="documentName">The OpenAPI document key.</param>
|
||||
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved. Receives the resolving <see cref="IServiceProvider"/> and returns the <see cref="JsonOptions"/> to use.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
|
||||
/// </remarks>
|
||||
public static IServiceCollection ReplaceOpenApiSchemaService(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
|
||||
{
|
||||
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
|
||||
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
|
||||
&& Equals(sd.ServiceKey, documentName))
|
||||
?? throw new InvalidOperationException(
|
||||
$"Could not find a registration for {OpenApiSchemaServiceFullName} keyed with '{documentName}'. "
|
||||
+ $"Ensure AddOpenApi(\"{documentName}\") has been called before {nameof(ReplaceOpenApiSchemaService)}, "
|
||||
+ "or check whether the internal Microsoft.AspNetCore.OpenApi registration shape has changed.");
|
||||
|
||||
services.Remove(descriptor);
|
||||
services.AddKeyedSingleton(
|
||||
descriptor.ServiceType,
|
||||
documentName,
|
||||
(sp, key) => ActivatorUtilities.CreateInstance(
|
||||
sp,
|
||||
descriptor.ServiceType,
|
||||
key,
|
||||
Options.Create(jsonOptionsFactory(sp))));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAPI services.
|
||||
/// </summary>
|
||||
public static class OpenApiServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
|
||||
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
|
||||
/// <param name="documentTitle">The title to display in the UI dropdown. Defaults to <paramref name="documentName"/> if not specified.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
|
||||
public static IServiceCollection AddOpenApiDocumentToUi(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string? documentTitle = null)
|
||||
=> services.AddOpenApiDocumentToUi(documentName, () => documentTitle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown, resolving the title lazily so
|
||||
/// callers (such as builder-pattern helpers) can defer it until SwaggerUI options are resolved.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
|
||||
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
|
||||
/// <param name="documentTitleFactory">Factory invoked when SwaggerUI options are resolved. Returning <c>null</c> falls back to <paramref name="documentName"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
|
||||
internal static IServiceCollection AddOpenApiDocumentToUi(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
Func<string?> documentTitleFactory)
|
||||
{
|
||||
services.AddOptions<SwaggerUIOptions>()
|
||||
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
|
||||
{
|
||||
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
|
||||
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitleFactory() ?? documentName);
|
||||
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Handles OpenIddict request processing to skip handling for non-authentication requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler prevents OpenIddict from processing every request to the server,
|
||||
/// limiting its scope to back-office and well-known OpenID Connect endpoints.
|
||||
/// </remarks>
|
||||
public class ProcessRequestContextHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ProcessRequestContext>, IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessRequestContext>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly string[] _pathsToHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcessRequestContextHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
|
||||
.EnsureStartsWith('/')
|
||||
.EnsureEndsWith('/');
|
||||
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration", "/.well-known/jwks"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server process request context event.
|
||||
/// </summary>
|
||||
/// <param name="context">The process request context.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
|
||||
{
|
||||
if (SkipOpenIddictHandlingForRequest())
|
||||
{
|
||||
context.SkipRequest();
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the validation process request context event.
|
||||
/// </summary>
|
||||
/// <param name="context">The process request context.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessRequestContext context)
|
||||
{
|
||||
if (SkipOpenIddictHandlingForRequest())
|
||||
{
|
||||
context.SkipRequest();
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private bool SkipOpenIddictHandlingForRequest()
|
||||
{
|
||||
var requestPath = _httpContextAccessor.HttpContext?.Request.Path.Value;
|
||||
if (requestPath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var path in _pathsToHandle)
|
||||
{
|
||||
if (requestPath.StartsWith(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,23 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure API services.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Umbraco API OpenAPI/Swagger UI services to the builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
internal static void AddUmbracoOpenApi(this IUmbracoBuilder builder)
|
||||
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(UmbracoJsonTypeInfoResolver)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Services.AddOptions<UmbracoOpenApiOptions>()
|
||||
.Configure<IHostingEnvironment, IWebHostEnvironment>((options, hostingEnv, webHostEnv) =>
|
||||
{
|
||||
options.Enabled = webHostEnv.IsProduction() is false;
|
||||
var backOfficePath = hostingEnv.GetBackOfficePath().TrimStart(Constants.CharArrays.ForwardSlash);
|
||||
options.RouteTemplate = $"{backOfficePath}/openapi/{{documentName}}.json";
|
||||
options.UiRoutePrefix = $"{backOfficePath}/openapi";
|
||||
});
|
||||
builder.AddUmbracoOpenApiDocument<ConfigureDefaultApiOptions>(DefaultApiConfiguration.ApiName, "Default API");
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
|
||||
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OpenApiRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
}
|
||||
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
|
||||
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
|
||||
/// <summary>
|
||||
/// Adds and configures an Umbraco OpenAPI document with shared transformers.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <param name="apiName">The name/identifier of the API.</param>
|
||||
/// <param name="apiTitle">The title of the API.</param>
|
||||
/// <param name="jsonOptionsName">
|
||||
/// Optional named <c>JsonOptions</c> to use for schema generation instead of the default HTTP JSON options.
|
||||
/// When specified, replaces the internal <c>OpenApiSchemaService</c> registration for this document.
|
||||
/// </param>
|
||||
/// <typeparam name="TConfigureOptions">The type used to configure the OpenAPI options.</typeparam>
|
||||
internal static void AddUmbracoOpenApiDocument<TConfigureOptions>(
|
||||
this IUmbracoBuilder builder,
|
||||
string apiName,
|
||||
string apiTitle,
|
||||
string? jsonOptionsName = null)
|
||||
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
builder.Services.AddOpenApi(apiName);
|
||||
builder.Services.ConfigureOptions<TConfigureOptions>();
|
||||
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
|
||||
|
||||
if (jsonOptionsName is not null)
|
||||
{
|
||||
builder.Services.ReplaceOpenApiSchemaService(apiName, jsonOptionsName);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
|
||||
using Umbraco.Cms.Infrastructure.HostedServices;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure authentication services.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds OpenIddict authentication services for Umbraco APIs.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The Umbraco builder for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Configures OpenIddict with authorization code flow (with PKCE), client credentials flow,
|
||||
/// reference tokens, and ASP.NET Core Data Protection for token encryption.
|
||||
/// </remarks>
|
||||
private static bool _initialized;
|
||||
|
||||
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OpenIddictCleanupJob)) is false)
|
||||
if (_initialized is false)
|
||||
{
|
||||
ConfigureOpenIddict(builder);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
return builder;
|
||||
@@ -47,20 +31,17 @@ public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
// Enable the authorization and token endpoints.
|
||||
// - important: member endpoints MUST be added before backoffice endpoints to ensure that auto-discovery works for members
|
||||
// FIXME: swap paths here so member API is first (see comment above)
|
||||
options
|
||||
.SetAuthorizationEndpointUris(
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetTokenEndpointUris(
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetEndSessionEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserInfoEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
@@ -69,16 +50,12 @@ public static class UmbracoBuilderAuthExtensions
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
|
||||
// Enable the client credentials flow.
|
||||
options.AllowClientCredentialsFlow();
|
||||
|
||||
// Register the ASP.NET Core host and configure for custom authentication endpoint.
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableTokenEndpointPassthrough()
|
||||
.EnableEndSessionEndpointPassthrough()
|
||||
.EnableUserInfoEndpointPassthrough();
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
@@ -86,17 +63,6 @@ public static class UmbracoBuilderAuthExtensions
|
||||
.UseReferenceAccessTokens()
|
||||
.UseReferenceRefreshTokens();
|
||||
|
||||
// Apply sliding window expiry based on the configured max login lifetime
|
||||
GlobalSettings globalSettings = builder.Config
|
||||
.GetSection(Constants.Configuration.ConfigGlobal)
|
||||
.Get<GlobalSettings>() ?? new GlobalSettings();
|
||||
TimeSpan timeOut = globalSettings.TimeOut;
|
||||
|
||||
// Make the access token lifetime 25% of the refresh token lifetime, to help ensure that new access tokens
|
||||
// are obtained by the client before the refresh token expires.
|
||||
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
|
||||
options.SetRefreshTokenLifetime(timeOut);
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT.
|
||||
// This is more secure, and has the added benefit of having a high throughput
|
||||
// but means that all servers (such as in a load balanced setup)
|
||||
@@ -119,38 +85,6 @@ public static class UmbracoBuilderAuthExtensions
|
||||
options
|
||||
.AddEncryptionKey(new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32))) // generate a cryptographically secure random 256-bits key
|
||||
.AddSigningKey(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048))); // generate RSA key with recommended size of 2048-bits
|
||||
|
||||
// Add custom handler for the "ProcessRequestContext" server event, to stop OpenIddict from handling
|
||||
// every last request to the server (including front-end requests).
|
||||
options.AddEventHandler<OpenIddictServerEvents.ProcessRequestContext>(configuration =>
|
||||
{
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractRevocationRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractRevocationRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
@@ -168,26 +102,8 @@ public static class UmbracoBuilderAuthExtensions
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT. (see note in AddServer)
|
||||
options.UseDataProtection();
|
||||
|
||||
// Add custom handler for the "ProcessRequestContext" validation event, to stop OpenIddict from handling
|
||||
// every last request to the server (including front-end requests).
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessRequestContext>(configuration =>
|
||||
{
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
|
||||
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
|
||||
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
|
||||
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
|
||||
builder.Services.AddHostedService<OpenIddictCleanup>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ActionDescriptor"/> to work with <see cref="MapToApiAttribute"/>.
|
||||
/// </summary>
|
||||
public static class ActionDescriptorApiCommonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the <see cref="ActionDescriptor"/> has a <see cref="MapToApiAttribute"/> with the specified API name.
|
||||
/// The check is made in runtime to support attributes added in runtime.
|
||||
/// </summary>
|
||||
/// <param name="actionDescriptor">The action descriptor to inspect.</param>
|
||||
/// <param name="apiName">The API name to check for.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <see cref="MapToApiAttribute"/> is present and matches the specified API name,
|
||||
/// or if the attribute is not present and the API name matches the default API name; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName)
|
||||
{
|
||||
var value = actionDescriptor.GetMapToApiAttributeValue();
|
||||
|
||||
return value == apiName
|
||||
|| (value is null && apiName == DefaultApiConfiguration.ApiName);
|
||||
}
|
||||
|
||||
private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor)
|
||||
{
|
||||
IEnumerable<MapToApiAttribute> mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType<MapToApiAttribute>() ?? [];
|
||||
|
||||
return mapToApiAttributes.SingleOrDefault()?.ApiName;
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,16 @@ using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="MethodInfo"/> to work with API-related attributes.
|
||||
/// </summary>
|
||||
public static class MethodInfoApiCommonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version values from <see cref="MapToApiVersionAttribute"/> applied to the method.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <returns>A pipe-separated string of API version values.</returns>
|
||||
public static string GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
|
||||
public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
|
||||
|
||||
return string.Join("|", mapToApis.SelectMany(x => x.Versions));
|
||||
return string.Join("|", mapToApis.SelectMany(x=>x.Versions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the API name from <see cref="MapToApiAttribute"/> applied to the method's declaring type.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <returns>The API name if the attribute is present; otherwise, <c>null</c>.</returns>
|
||||
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty<object>()).Cast<MapToApiAttribute>().ToArray();
|
||||
@@ -34,15 +22,6 @@ public static class MethodInfoApiCommonExtensions
|
||||
return mapToApis.SingleOrDefault()?.ApiName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the method's declaring type has a <see cref="MapToApiAttribute"/> with the specified API name.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The method info to inspect.</param>
|
||||
/// <param name="apiName">The API name to check for.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the attribute is present and matches the specified API name,
|
||||
/// or if the attribute is not present and the API name matches the default API name; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName)
|
||||
{
|
||||
var value = methodInfo.GetMapToApiAttributeValue();
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
namespace Umbraco.Cms.Api.Common.Filters;
|
||||
namespace Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute used to specify the named JSON serialization options for a controller.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class JsonOptionsNameAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonOptionsNameAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
|
||||
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the JSON options configuration.
|
||||
/// </summary>
|
||||
public string JsonOptionsName { get; }
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="HttpContext"/> related to JSON serialization.
|
||||
/// </summary>
|
||||
public static class HttpContextJsonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the named JSON options configuration for the current endpoint.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context.</param>
|
||||
/// <returns>The JSON options name if specified via <see cref="JsonOptionsNameAttribute"/>; otherwise, <c>null</c>.</returns>
|
||||
public static string? CurrentJsonOptionsName(this HttpContext context)
|
||||
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON input formatter that only processes requests for endpoints with matching named JSON options.
|
||||
/// </summary>
|
||||
internal sealed class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
internal class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedSystemTextJsonInputFormatter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
|
||||
/// <param name="options">The JSON options.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
|
||||
: base(options, logger) =>
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead(InputFormatterContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON output formatter that only processes responses for endpoints with matching named JSON options.
|
||||
/// </summary>
|
||||
internal sealed class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
|
||||
internal class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedSystemTextJsonOutputFormatter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options.</param>
|
||||
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
|
||||
}
|
||||
|
||||