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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
11 changes: 7 additions & 4 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ name: Publish project release action
# Branch-scoped self-publisher: a push to main or develop, or a manual dispatch, publishes that branch.
#
# - Trigger: a push that changes a shipped input (the on.push.paths inclusion list below - library source,
# embedded data, version floor, or build configuration), or a workflow_dispatch on the target branch.
# - Inclusion-only: add a path above when a new input starts affecting the shipped package. Dependency bumps
# (Directory.Packages.props) and GitHub Actions bumps are not listed, so routine Dependabot churn does not
# republish. Ship a pending dependency update by promoting develop to main, or by dispatching.
# embedded data, version floor, build configuration, or package versions), or a workflow_dispatch.
# - Inclusion-only: add a path to that list when a new input starts affecting the shipped package. Package versions
# (Directory.Packages.props) ARE listed: a NuGet package cannot be rebuilt on a cadence (a version can't be
# re-pushed), so a dependency bump must republish to keep the package's declared dependencies current - this
# closes the stale/vulnerable-dependency window. GitHub Actions bumps are not listed (they do not ship in
# the package), so an Actions Dependabot bump does not republish (a package-version bump does).
# - Output: main publishes a stable release, develop a prerelease. A dispatch force-publishes its branch.
# - Gate: the publish job needs the same validate-task the PR runs, so nothing publishes that would fail
# validation, on any path (push, dispatch, or force-push).
Expand All@@ -19,6 +21,7 @@ on:
- 'LanguageData/**'
- 'version.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
workflow_dispatch:

# Ref-independent group so concurrent publishes serialize, and cancel-in-progress: false so a publish is
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,17 @@ concurrency:

jobs:

# `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fails).
validate:
name: Validate job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/validate-task.yml

# Build and pack the library in its branch configuration to prove it ships, publishing nothing. Runs on
# every push, so a change to build-release-task is exercised head-resolved in the PR that makes it.
smoke-build:
name: Smoke build job
if: ${{ !github.event.deleted }}
uses: ./.github/workflows/build-release-task.yml
permissions:
contents: read
Expand All@@ -42,7 +45,7 @@ jobs:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [validate, smoke-build]
if: always()
if: ${{ always() && !github.event.deleted }}
steps:
- name: Check workflow results step
run: |
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,10 +68,5 @@ jobs:
HISTORY.md
incremental_files_only: false

# Lint the workflow YAML, including publish-release which the smoke path never runs. The action
# vendors actionlint (SHA-pinned, shellcheck included), so there is no hand-rolled download. The
# actionlint version is pinned for reproducibility - bump it alongside the action.
- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0
with:
version: 1.7.12
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ The repo runs a review loop on every PR: local agent iteration plus remote autom

`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized).

**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or non-shipped dependencies does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.
**Merging a shipped change releases.** A merge to `main` or `develop` that changes a shipped input - including a dependency bump (`Directory.Packages.props`), so the published package's dependencies stay current - auto-publishes that branch (see [`WORKFLOW.md`](./WORKFLOW.md)); a merge confined to tests, tooling, docs, CI, or GitHub-Actions bumps does not. Releasing is a configured consequence of merging a shipped change, so weigh the release impact before merging to `main`. Never manually force a publish (`workflow_dispatch`) without explicit maintainer instruction.

### Expected Review Loop

Expand Down
72 changes: 43 additions & 29 deletions WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,11 @@ Each guarantee names the **failure it prevents**, so the reason survives a reimp

A run targets **one branch, the one it was triggered on** (`github.ref_name`): `main` builds a stable
release, `develop` a prerelease. The version is computed once and threaded downstream. A pull request
builds and tests but never publishes. The package **publishes itself** when a shipped input changes (the
source, the embedded data, the version floor, or the build configuration), so releases track the code
without a person cutting them. A maintainer dispatches only to force a release. Dependabot and codegen
builds and tests but never publishes. The package **publishes itself** when a shipped input changes - the
source, the embedded data, the version floor, the build configuration, or the package versions
(`Directory.Packages.props`) - so releases track the code without a person cutting them. Listing the package
versions means a dependency bump republishes too, keeping the package's declared dependencies current. A
maintainer dispatches only to force a release. Dependabot and codegen
pull requests merge themselves once their checks pass.

### Glossary
Expand All@@ -37,11 +39,13 @@ pull requests merge themselves once their checks pass.
the **base** branch's copy, while a `push`/`workflow_dispatch` event resolves it from the **pushed**
head. Self-testing (section 3) depends on this.
- **Shipped input** - a file that changes what the package ships: the library source (`LanguageTags/**`),
the embedded data (`LanguageData/**`), the version floor (`version.json`), or the build configuration
(`Directory.Build.props`). It is an explicit **inclusion list** (the publisher's `on.push.paths`), so a
change confined to tests, the codegen tool, dependencies, GitHub Actions, docs, or CI is **not** a
shipped input. Dependency bumps are excluded by policy to avoid republish churn (frequent, and not each
worth a release), so they ship on the next promotion or a dispatch.
the embedded data (`LanguageData/**`), the version floor (`version.json`), the build configuration
(`Directory.Build.props`), or the package versions (`Directory.Packages.props`). It is an explicit
**inclusion list** (the publisher's `on.push.paths`), so a change confined to tests, the codegen tool,
GitHub Actions, docs, or CI is **not** a shipped input. Package versions are included because a NuGet
version cannot be re-pushed (no scheduled rebuild like a Docker image), so a dependency bump must
republish to keep the package's declared dependencies current and close the stale/vulnerable-dependency
window. GitHub Actions bumps stay excluded - they do not ship in the package.
Comment thread
ptr727 marked this conversation as resolved.
- **GitHub App token** - a short-lived installation token from `actions/create-github-app-token`, minted
from the App credentials (`CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY`). Automation that must
trigger downstream workflows or write to bot pull requests uses **this token, not `GITHUB_TOKEN`**: a
Expand DownExpand Up@@ -148,8 +152,10 @@ skips the delete still reclaims its artifact. The run's artifact set is never bl
A pull request validates fast and never publishes. Validation is a reusable `validate-task` holding two
jobs, `unit-test` (build and test) and `lint` (the editor's checks, enforced in CI). The pull request runs
it as a `validate` job alongside `smoke-build` (build and pack the library to prove it ships, uploading and
pushing nothing). Both run unconditionally, no paths filter, so a reusable-workflow change is always
exercised head-resolved. Packaging validation as one task lets the publisher run the identical gate (D4.6).
pushing nothing). Both run on every push with no paths filter (a branch-deletion push is the one exception -
a `!github.event.deleted` guard skips them, since `github.sha` is all-zeros and checkout would fail), so a
reusable-workflow change is always exercised head-resolved. Packaging validation as one task lets the
publisher run the identical gate (D4.6).
One required aggregator gates the merge. See D1.

### Self-testing workflows, and the required-context invariant
Expand DownExpand Up@@ -182,9 +188,10 @@ Two things publish:

- **An automatic release on a shipped change.** The publisher runs on `push` to `main`/`develop` with the
`on.push.paths` inclusion list (`LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props`), so it triggers only when a shipped input changed. `Directory.Packages.props`
and `.github/**` are not listed, so dependency and Actions bumps do not republish. The merge-bot merges
with the App token, so its merge commits reach this push trigger.
`Directory.Build.props`, `Directory.Packages.props`), so it triggers only when a shipped input changed.
`.github/**` is not listed, so Actions bumps do not republish; `Directory.Packages.props` is listed, so a
dependency bump republishes to keep the package's dependencies current. The merge-bot merges with the App
token, so its merge commits reach this push trigger.
- **A manual release on demand.** A `workflow_dispatch` on a branch publishes it immediately, whatever
changed - the "release now" control.

Expand All@@ -206,7 +213,8 @@ D4.

The library is self-maintaining: data and dependencies stay current on both branches, each shipped change
releases automatically, and a person steps in only for a breaking change (a red check) or to force a
release by dispatch. A merged dependency bump does not itself publish. See D8.
release by dispatch. A merged dependency bump republishes (its `Directory.Packages.props` change is a
shipped input), keeping the published package's dependencies current. See D8.

### Single-target output seam

Expand DownExpand Up@@ -237,10 +245,12 @@ applicable guarantee is not operational (section 1).
### D1 - Pull-request fast feedback

- **D1.1 Every push builds, lints, and tests.** Output: on any push the `validate` job - the reusable
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` all run unconditionally,
no paths filter. `smoke-build` builds and packs the library in its branch configuration through the same
`build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested because
a filter excluded it; a build/packaging break slipping through.*
`validate-task`, holding the `unit-test` and `lint` jobs - and `smoke-build` run with no paths filter.
The one exception is a branch-deletion push: a `!github.event.deleted` guard skips every job (and the
aggregator skips too, so the required check is not left pending), because `github.sha` is all-zeros and a
checkout/build would fail. `smoke-build` builds and packs the library in its branch configuration through
the same `build-release-task` the publisher uses. *Prevents: a reusable-workflow change shipping untested
because a filter excluded it; a build/packaging break slipping through; a branch-deletion push failing CI.*
- **D1.2 Unit tests always run.** Output: the `unit-test` job (in `validate-task`) runs `dotnet test`
(build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here), and the aggregator reaches
it through the `validate` job it `needs:`.
Expand DownExpand Up@@ -291,11 +301,12 @@ applicable guarantee is not operational (section 1).
- **D4.1 Publish only by dispatch or a shipped-input change.** Output: the publisher is reachable via (a)
`workflow_dispatch` on a branch (force-publish, guarded to `main`/`develop`), or (b) a `push` to
`main`/`develop` matching the **`on.push.paths` inclusion list** of shipped inputs (`LanguageTags/**`,
`LanguageData/**`, `version.json`, `Directory.Build.props`). The list is inclusion-only: it does not
list `Directory.Packages.props`, `.github/**`, docs, tests, or the codegen tool, so a dependency bump,
a GitHub Actions bump, or a docs change does not republish. There is no `schedule` and no
`PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (dependency bump, actions
bump, docs) cutting a release.*
`LanguageData/**`, `version.json`, `Directory.Build.props`, `Directory.Packages.props`). The list is
inclusion-only: it does not list `.github/**`, docs, tests, or the codegen tool, so a GitHub Actions bump
or a docs change does not republish. `Directory.Packages.props` **is** listed, so a dependency bump
republishes (a NuGet version can't be re-pushed, so deps must republish to stay current). There is no
`schedule` and no `PUBLISH_ON_MERGE`. *Prevents: a blind scheduled republish; a no-impact change (actions
bump, docs) cutting a release; and a stale/vulnerable dependency lingering in the published package.*
- **D4.2 Publish exactly the triggering branch.** Output: the run publishes only `github.ref_name`
(`develop` -> prerelease, `main` -> stable; a shipped change or dispatch on `main` cuts a stable release
by design). *Prevents: a publish shipping the wrong branch.*
Expand DownExpand Up@@ -377,9 +388,10 @@ applicable guarantee is not operational (section 1).
- **D8.2 Dependabot auto-merges on green, every tier.** Output: every Dependabot pull request, any
ecosystem and semver-major included, auto-merges once the required checks pass, with no version-tier
exception. A failing check blocks the merge and surfaces via GitHub's check-failure notification. A
merged dependency bump does **not** itself publish (dependencies are not in the shipped-input inclusion
list, D4.1); it ships with the next shipped change or a dispatch. *Prevents: a breaking update merging
unverified; a safe update stalled waiting for a human; and dependency churn cutting needless releases.*
merged dependency bump **republishes** (`Directory.Packages.props` is a shipped input, D4.1), keeping the
published package's declared dependencies current; a GitHub-Actions bump does not. *Prevents: a breaking
update merging unverified; a safe update stalled waiting for a human; and a stale/vulnerable dependency
lingering in the published package.*
- **D8.3 Codegen is deterministic and content-gated.** Output: codegen regenerates `LanguageData/` purely
from its upstream sources (no per-run timestamps/GUIDs), opens a pull request only when the data changed,
and auto-merges it on green. The merged data is a shipped input, so the publisher releases it (D4.1).
Expand DownExpand Up@@ -425,7 +437,8 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
other consumer reading it via `needs:` outputs (a second invocation that recomputes is the defect; a
commit checkout that only compiles is allowed).
- **D1:** the PR workflow runs on `push` with no paths filter; the `validate` job (the reusable
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` both run unconditionally; the smoke call
`validate-task`, holding `unit-test` + `lint`) and `smoke-build` run on every push except a branch deletion
(every job, the aggregator included, carries a `!github.event.deleted` guard); the smoke call
sets publish off and `smoke: true`; every build `upload-artifact` is gated `!smoke`; the `lint` job runs
CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on
README/HISTORY, and `actionlint`; the aggregator `needs:` `validate` + `smoke-build` and blocks on any
Expand All@@ -436,7 +449,7 @@ guarantee, each pass/fail/N-A with a `file:line` citation:
`publicReleaseRefSpec` is `^refs/heads/main$`.
- **D4:** the publisher's triggers are `workflow_dispatch` and a `push` to `main`/`develop` with an
`on.push.paths` inclusion list of exactly `LanguageTags/**`, `LanguageData/**`, `version.json`,
`Directory.Build.props` (no `Directory.Packages.props`, no `.github/**`); no `schedule`, no
`Directory.Build.props`, `Directory.Packages.props` (no `.github/**`); no `schedule`, no
`PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same
`validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only
`github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean
Expand DownExpand Up@@ -477,11 +490,12 @@ determined by NBGV from the checkout state in section 3.*
| S8 | branch/version classification disagree (e.g. `main` carries `-g`) | validate-release fails loud; build/publish skip | D2.2 |
| S9 | merged codegen `LanguageData/**` change | shipped input changed -> that branch **auto-publishes** | D4.1, D8.3 |
| S10 | merged GitHub-Actions version bump only | `.github/workflows/**` is not a shipped input -> **no publish** | D4.1 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is not in the inclusion list -> **no publish**; ships on the next shipped change or a dispatch | D4.1, D8.2 |
| S11 | merged dependency bump, any kind (e.g. `Microsoft.Extensions.Logging.Abstractions` or `xunit.v3`) | `Directory.Packages.props` is a shipped input -> that branch **auto-publishes**, keeping the package's declared dependencies current | D4.1, D8.2 |
| S12 | PR with a CSharpier, dotnet-format, markdown, spelling, or workflow-YAML violation | the `lint` job fails -> aggregator blocks the merge | D1.3, D1.5 |
| S13 | `version.json` floor bump merged to a branch | version floor is a shipped input -> **auto-publish** that branch at the new floor | D3.3, D4.1, D4.2 |
| S14 | Dependabot **major** bump whose tests fail | required check fails -> auto-merge does **not** complete; no merge, no publish; maintainer notified | D8.2 |
| S15 | `develop` -> `main` promotion (merge commit) carrying a shipped change | the merge commit's diff (`before..after`, `before` = prior `main` tip) includes the promoted shipped input -> `main` **auto-publishes the stable release**; a promotion carrying only non-shipped changes does not | D4.1, D4.2, D8.1 |
| S16 | a branch is **deleted** (a push event with `github.sha` all-zeros) | the `!github.event.deleted` guard skips `validate`, `smoke-build`, and the aggregator -> no failed CI run, no pending required check | D1.1 |

### 5C. Live probe (where warranted, never publishing)

Expand Down
Loading