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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ generated_code = true

# C# files
[*.cs]
# Encoding (*)
# No end_of_line: the repository stores most .cs files with LF and relies on core.autocrlf; declaring crlf
# makes editors rewrite every line of a file checked out with LF.
charset = utf-8-bom

# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
Expand Down
30 changes: 28 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,36 @@ configuration passed to the frontend.
internals of `Bravo` through `InternalsVisibleTo`. The build stamps the version from git history, so a clone
or a CI checkout must be complete: with a shallow clone the build fails or produces a wrong version.

## Code

Which rule applies, in order of precedence:

1. `.editorconfig` and the analyzers, for everything they cover. Editors apply `.editorconfig`, agent file tools
do not: write `.cs` files as UTF-8 with BOM, keep the line endings of a file you edit, and use CRLF in a new
one. A different encoding or line ending shows the whole file as changed.
2. The Microsoft [.NET design guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/) for
names and API design in code you add or change, also where the surrounding code differs.
3. Code you do not otherwise change is not changed for conformance alone. Align a type when a pull request
changes it for another reason, with the rename in its own commit.
4. Contracts that installed clients and other tools depend on win over all of the above.

## Documentation

| Location | Content |
| --- | --- |
| `docs/design/` | Current behavior that the code cannot state: invariants, ownership, external constraints, each with the consequence of breaking it |
| `docs/adr/` | One decision per file, with its reasons and rejected alternatives |

Planned work and its status are not kept in this repository.

Update a document in the same pull request that changes what it describes, and delete statements that no
longer hold. Do not add or change a rule in this file, or a decision in `docs/adr/`, unless the maintainer
asked for it.

## Read before you write

| Document | Read it before |
| --- | --- |
| [docs/design/code-conventions.md](docs/design/code-conventions.md) | creating or editing any `.cs` file. Encoding and design rules apply from its first line, and a wrong encoding rewrites the whole file. |
| [docs/design/versioning.md](docs/design/versioning.md) | touching `version.json`, the build number, or any code that compares versions. |
| [docs/documentation-guidelines.md](docs/documentation-guidelines.md) | writing or updating anything under `docs/`. |
| [ADR 0001](docs/adr/0001-releases-without-update-channels.md) | touching the update check, the update feed, or the release of a version. |
| [ADR 0002](docs/adr/0002-shared-msi-upgrade-code.md) | changing `UpgradeCode`, `MajorUpgrade` or the product version in `installer/wix`. |
45 changes: 45 additions & 0 deletions docs/adr/0001-releases-without-update-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 0001. Releases without update channels

- Status: Accepted
- Date: 2026-10-19

## Context

The update check reads one of two feeds, selected by the user or by the `UpdateChannel` policy:
`Stable` reads `bravo-public/currentversion.json`, `Dev` reads `bravo-internal/currentversion.json`, both on
`bravorelease.blob.core.windows.net`. The selector is shown to every user, `Dev` serves untested internal
builds, and the channel spreads over settings, policy templates, the API, the frontend and every translation.

Releases are published on GitHub, and a preview carries its prerelease tag in `version.json`
([versioning](../design/versioning.md)). A preview and the release that follows it share `Major.Minor.Patch`
and differ only in the fourth field of the file version.

## Decision

- Every release is published on GitHub, as a prerelease or as a stable release.
- The update check reads `bravo-public/currentversion.json` only. The feed lists stable releases only;
previews have no feed entry and are downloaded from the GitHub release page.
- The feed `version` is the four-field numeric file version of the release, for example `1.1.0.25`.
- The channel is removed from the application: the selector, the user setting, the `UpdateChannel` policy, the
API parameter and the strings.
- `bravo-internal/currentversion.json` is updated once, to the first release without channels, and then left
unchanged.

## Rejected alternatives

- A separate preview feed, or an opt-in to preview notifications: it reintroduces the channel under another
name, for a preview audience that downloads builds by hand.
- Reading releases from the GitHub API instead of the feed: installed clients read the feed regardless.

## Consequences

- Installed clients on `Dev` read only `bravo-internal`: its single update moves them to a release that reads
`bravo-public`.
- Both feed URLs are compiled into installed clients and stay available while those clients exist.
- A three-field `version` parses with an undefined fourth field and compares lower than any build of the same
`Major.Minor.Patch`, so a preview user is never notified of the release. A non-numeric `version` makes every
installed client fail in `Version.Parse`.
- A `1.0.x` release is no longer possible: installed `1.0` builds carry dated file versions such as
`1.0.9517.31194`, which compare higher than `1.0.14` in the feed and in the MSI.
- An `UpdateChannel` value set by an administrator is ignored; the release notes say so.
- Existing `usersettings.json` files keep reading correctly: the serializer ignores the removed property.
53 changes: 53 additions & 0 deletions docs/adr/0002-shared-msi-upgrade-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 0002. One UpgradeCode for the per-user and the per-machine MSI

- Status: Accepted
- Date: 2026-10-19

## Context

Bravo ships two MSI packages from the same build: [Bravo.wxs](../../installer/wix/src/Bravo/Bravo.wxs),
installed per machine, and [Bravo-perUser.wxs](../../installer/wix/src/Bravo/Bravo-perUser.wxs), installed
per user. Both declare the same `UpgradeCode`, take the MSI `ProductVersion` from the file version
([versioning](../design/versioning.md)), and author `MajorUpgrade` with `AllowSameVersionUpgrades="yes"` and
`AllowDowngrades="no"`.

Windows Installer behavior that constrains the choice, from the Microsoft documentation:

| Behavior | Source |
| --- | --- |
| A major upgrade is never installed across installation contexts: a per-user product is upgraded only by a per-user installation, a per-machine product only by a per-machine one | [Major Upgrades](https://learn.microsoft.com/windows/win32/msi/major-upgrades) |
| Only the first three fields of `ProductVersion` are compared; the fourth is ignored | [ProductVersion](https://learn.microsoft.com/windows/win32/msi/productversion) |
| `FindRelatedProducts` runs at the first installation only, not in maintenance mode or uninstallation | [FindRelatedProducts](https://learn.microsoft.com/windows/win32/msi/findrelatedproducts-action) |
| One `UpgradeCode` can cover the versions and languages of a single product; each stand-alone product has its own | [Using an UpgradeCode](https://learn.microsoft.com/windows/win32/msi/using-an-upgradecode) |
| A dual-purpose package can install in either context; the context of the first installation is used for all later updates, repairs and removal | [Single Package Authoring](https://learn.microsoft.com/windows/win32/msi/single-package-authoring) |

`AllowSameVersionUpgrades="yes"` makes WiX set `msidbUpgradeAttributesVersionMaxInclusive`, so an installed
product with the same three fields is treated as an older version and removed
([WiX MajorUpgrade](https://docs.firegiant.com/wix/schema/wxs/majorupgrade/)). Without it, two packages that
differ only in the fourth field install side by side as two products. The inclusive maximum triggers the ICE61
warning ([ICE61](https://learn.microsoft.com/windows/win32/msi/ice61)), which the
[packaging pipeline](../../.azure/pipelines/build-bravo.yaml) suppresses.

## Decision

Keep one `UpgradeCode` for both packages. They are one product in two installation contexts, like a
dual-purpose package.

## Rejected alternatives

- A separate `UpgradeCode` per package: the two contexts never upgrade each other in either case, so nothing
changes for users. The new package would no longer find installations made with the current code, and would
install beside them, unless its `Upgrade` table kept the current code permanently.

## Consequences

- Each package upgrades installations of its own context. A per-user and a per-machine installation can
coexist on the same computer, under distinct product names in Programs and Features, and neither removes
the other.
- Builds of the same `Major.Minor.Patch` replace each other in either direction, only because of
`AllowSameVersionUpgrades`: a preview upgrades to its release, and a preview installed over its release is
not blocked as a downgrade.
- A lower `Major.Minor.Patch` is blocked by `AllowDowngrades="no"`; for the consequence on `1.0.x` see
[ADR 0001](0001-releases-without-update-channels.md).
- The `UpgradeCode` does not tell the two packages apart; the product name and the product code do. Bravo has
no winget manifest.
36 changes: 0 additions & 36 deletions docs/design/code-conventions.md

This file was deleted.

42 changes: 0 additions & 42 deletions docs/documentation-guidelines.md

This file was deleted.

Loading