Skip to content

feat(scripts): detect modified-but-unreleased upstream deps during release#436

Draft
sandersaares wants to merge 24 commits into
mainfrom
feat/release-deps-upstream-scan
Draft

feat(scripts): detect modified-but-unreleased upstream deps during release#436
sandersaares wants to merge 24 commits into
mainfrom
feat/release-deps-upstream-scan

Conversation

@sandersaares
Copy link
Copy Markdown
Member

@sandersaares sandersaares commented May 20, 2026

Problem

When releasing a workspace crate, an author may have also modified one of its
upstream workspace dependencies but forgotten to release the dependency too.
Locally everything builds via path-references, but once published the released
crate resolves against the last released version of each dependency on
crates.io — missing the new changes.

Crucially, those upstream modifications may have been introduced in an
earlier PR that merged to main without a version bump, not necessarily
the current PR. A diff of the current branch vs the PR base would never see
them, but they are just as unreleased as same-PR changes.

The existing cascade in release-crate.ps1 propagates bumps downstream
(target -> its dependents) but never upstream. This PR closes that gap.

Solution

Two layers of automation, sharing logic via the new scripts/lib/releasing.ps1 library.

1. Interactive layer — release-crate.ps1

After the existing downstream cascade finishes, the script scans the release
set
(every crate whose version differs from the PR base ref) for transitive
workspace dependencies that:

  • live in this workspace,
  • have unreleased modifications (see "Per-crate baseline" below),
  • are not themselves in the release set.

For each finding the author is prompted [y/N] and (if yes) picks a bump
kind. Accepted crates are queued and released next, each with their own
cascade and re-scan. Declined crates are recorded so they aren''t re-prompted.

2. CI layer — release-deps job

.github/workflows/main.yml gains a release-deps job that runs
scripts/check-unreleased-dependencies.ps1 (the same analysis,
non-interactive). When findings exist it posts a sticky PR comment listing
each modified-but-unreleased dependency with the chain that reaches it, so
reviewers can sanity-check materiality decisions. When there are no findings
the script exits fast, so the job always runs and stays cheap.

Per-crate baseline

"Modified" is evaluated per crate against that crate''s own last release
boundary, not against the PR base ref. For each crate the analysis runs:

git log -1 --format=%H -G ''^(version|publish)\s*='' -- crates/<folder>/Cargo.toml

That returns the most recent commit whose diff touched the crate''s top-level
version = or publish = line. Any change under crates/<folder>/ newer
than that commit — committed, working-tree, or untracked — is treated as
unreleased and feeds into the BFS from each release-set crate.

The release-set itself (Get-CratesWithVersionBumps) intentionally still
diffs against the PR base ref — that''s the correct anchor for "what is this
PR releasing".

Tags are not consulted: a CI clone or partial fetch may not have them, and
the Cargo.toml edit is the canonical cause of a release while a tag is
downstream evidence.

Notable design points

  • scripts/lib/releasing.ps1 is a pure dot-source library (no top-level
    param(), no side effects). Both entry-point scripts dot-source it.
  • Dependency graph uses Cargo metadata''s normal + build deps; dev-dependencies
    are excluded (cannot affect downstream consumers via crates.io).
  • Cascade re-bump is idempotent: re-cascading into a crate already at a
    sufficient version appends a maintenance bullet to its existing changelog
    section instead of double-bumping. Bullet format is
    Now requires \<version>\ of \<target>\ (consistent across both the
    fresh-bump and re-cascade emission paths).
  • All git invocations go through Invoke-Git (array-argument, no
    Invoke-Expression). The legacy wrapper was dropped in commit 2.

Manual test matrix

T1-T16 (single-PR / base-ref-relative) and N1-N10 (multi-PR / per-crate
baseline) scenarios are documented and reproducible from
scripts/tests/RELEASE-DEPS-TEST-CASES.md. All were exercised end-to-end against
a scratch worktree before commit; the N-series in particular covers the
prior-PR-without-bump case that motivated the per-crate baseline.

Files

  • new: scripts/lib/releasing.ps1, scripts/check-unreleased-dependencies.ps1,
    scripts/tests/RELEASE-DEPS-TEST-CASES.md
  • modified: scripts/release-crate.ps1, .github/workflows/main.yml

sandersaares and others added 2 commits May 20, 2026 14:44
…lease

When releasing a crate, an author may have also modified one of its upstream
workspace dependencies but forgotten to release the dependency too. Locally
everything builds via path-references, but once published the released crate
resolves to the last released version on crates.io, missing the new changes.

This adds two layers of automation:

1. Interactive layer (release-crate.ps1):
   After the existing downstream cascade finishes, scan the release set for
   transitive workspace dependencies that have file changes vs the PR base ref
   but are not themselves being released. Prompt the author (y/N + bump kind)
   for each finding so material changes get an extra release queued, while
   immaterial changes (formatting, doc tweaks) can be declined.

2. CI layer (scripts/check-unreleased-dependencies.ps1 +
   .github/workflows/main.yml release-deps job):
   Runs the same analysis non-interactively and posts a sticky PR comment
   listing any findings so reviewers can sanity-check materiality decisions.

Shared logic lives in the new scripts/lib/releasing.ps1 library, dot-sourced
by both entry-point scripts. Workspace dependency types kind=normal and
kind=build are tracked; kind=dev is excluded (cannot affect downstream
consumers via crates.io). Cascade re-bump is idempotent: re-cascading into a
crate already at a sufficient version appends a maintenance bullet to its
existing changelog section instead of double-bumping.

The .delta.toml Cargo.toml trip-wire is documented as a dependency of the
release-deps CI gate so any version bump touches Cargo.toml -> skip=false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the three remaining Invoke-GitCommand callsites in release-crate.ps1
(tag --list, git log, remote get-url) to the array-argument Invoke-Git wrapper
in releasing.ps1, then delete the legacy wrapper. Removes the last use of
Invoke-Expression-based git invocation in the release tooling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (074a147) to head (3b36946).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #436   +/-   ##
=======================================
  Coverage   100.0%   100.0%           
=======================================
  Files         286      286           
  Lines       22879    22978   +99     
=======================================
+ Hits        22879    22978   +99     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ralfbiedert
Copy link
Copy Markdown
Collaborator

Whops, I missed the draft on this

sandersaares and others added 2 commits May 21, 2026 13:53
The release-deps job previously depended on the delta job and used
`delta.outputs.skip != 'true'` as a compute optimization to skip the
analysis when no crate was affected. The optimization was structurally
fragile - it relied on the implicit invariant that Cargo.toml and
scripts/* remain in .delta.toml's trip_wire_patterns - and only saved a
few seconds when there are no findings.

Drop both the needs: [delta] dependency and the delta.skip gate. The
check-unreleased-dependencies.ps1 script already exits fast (no
findings -> no markdown, no comment) when there is nothing to report,
so always running it is safe and simple.

Also remove the corresponding NOTE in .delta.toml since that
dependency no longer exists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sandersaares sandersaares marked this pull request as ready for review May 21, 2026 11:06
Copilot AI review requested due to automatic review settings May 21, 2026 11:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens the workspace crate release tooling by detecting “modified-but-unreleased” upstream workspace dependencies that could be missed when publishing to crates.io, and surfaces the same signal in CI via a new informational job.

Changes:

  • Extracts shared release/dependency-graph and git helper logic into a dot-sourced PowerShell library (scripts/lib/releasing.ps1).
  • Extends scripts/release-crate.ps1 to (optionally) scan for modified-but-unreleased upstream workspace deps after the existing downstream cascade, with interactive prompting to release them.
  • Adds a CI-only analyzer script and a new workflow job that posts/removes a sticky PR comment when such unreleased upstream dependency changes are detected.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
scripts/release-crate.ps1 Adds base-ref/non-interactive options, refactors cascade flow, and performs a post-release upstream dependency scan.
scripts/lib/releasing.ps1 New shared library providing safe git invocation, SemVer helpers, workspace metadata, and unreleased-dependency analysis.
scripts/check-unreleased-dependencies.ps1 New CI companion script that emits a markdown report + step output for unreleased upstream dependency changes.
.github/workflows/main.yml Adds release-deps job to run the CI analyzer and post/remove a sticky PR comment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/release-crate.ps1 Outdated
Comment thread scripts/check-unreleased-dependencies.ps1
- release-crate.ps1: drop -AllowFailure on the fetch in the base-ref resolver
  so the surrounding try/catch can actually trigger and emit a warning on fetch
  failure (previously the catch was unreachable because -AllowFailure returns
  $null instead of throwing).

- check-unreleased-dependencies.ps1: route Get-RepoRoot through Invoke-Git
  instead of shelling out directly, matching the design described in the PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 21, 2026 12:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread scripts/release-crate.ps1 Outdated
sandersaares and others added 2 commits May 21, 2026 15:33
…rades them

When the post-release dep scan runs a nested `Invoke-ReleaseFlow` for an
upstream crate the user opts to release, the resulting cascade may upgrade a
crate that was already in the release set (e.g., the initial release patch-bumped
`foo` and the nested major release of an upstream dep now requires a major on
`foo`). Previously the merge skipped duplicates outright, so `Show-ReleaseSummary`
and the final `feat(crate): release v<version>` message reported a stale
version that did not match what was actually written to `Cargo.toml`.

Replace the skip-on-duplicate logic with an in-place update: keep the original
`OldVersion` (the pre-PR baseline) and adopt the latest `NewVersion` from the
nested cascade. New crates are still appended as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous logic compared the working tree against the PR base ref to
decide which workspace crates had unreleased modifications. That missed a
real scenario: an earlier PR merges a source change to `bytesbuf` without
bumping its version, and a later PR bumps `bytesbuf_io` (which depends on
`bytesbuf`). On crates.io the published `bytesbuf_io` resolves to the last
released `bytesbuf`, which does *not* include the unreleased modification.
Because the modification predates the PR's base ref, the old `BaseRef`-
relative scan saw nothing to flag.

Switch each crate's "modification baseline" to its own most-recent commit
that touched `version =` or `publish =` in its `Cargo.toml`, derived via
`git log -1 -G '^(version|publish)\s*='`. Any change under `crates/<folder>/`
newer than that commit (committed, working-tree, or untracked) is treated
as unreleased.

`Get-CratesWithVersionBumps` (release-set detection) intentionally still
diffs against the PR base ref — that's the correct anchor for "what is this
PR releasing".

Replaces `Get-GitFileChangeSet` / `Get-CratesWithFileChanges` with the new
`Get-CrateLastReleaseBaseline` + `Get-CratesWithUnreleasedChanges` helpers
and rewrites `Get-UnreleasedModifiedDependencies` to consume the per-crate
modification map.

Also persists the manual test plan as `scripts/RELEASE-DEPS-TEST-CASES.md`
so future agents can re-run T1-T16 (original PR-vs-base coverage) and
N1-N10 (multi-PR baseline coverage) when the logic changes. All N1-N9
scenarios were verified in a scratch worktree before this commit; N10
(brand-new crate) is structurally covered by the new-crate branch in
`Get-CratesWithVersionBumps`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 21, 2026 14:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread scripts/check-unreleased-dependencies.ps1 Outdated
Comment thread scripts/release-crate.ps1
sandersaares and others added 2 commits May 21, 2026 18:32
Two real defects flagged on the latest review pass:

1. `check-unreleased-dependencies.ps1` was supposed to alphabetically sort
   the release-set listing in the sticky PR comment, but the chain
   `@(Get-CratesWithVersionBumps ...) | Sort-Object` silently broke. The
   helper returns its HashSet via `Write-Output -NoEnumerate` so callers
   can use `.Contains()`. That wrapping makes `Sort-Object` receive a
   single object (the HashSet itself), so the sort is a no-op and the
   foreach below iterates the HashSet in insertion order. Fixed by
   unwrapping with `... | ForEach-Object { $_ }` before sorting.

2. `Add-CascadeBulletToVersionSection`'s `if ($subStart -ge 0)` branch
   built the new file content with
   `@($lines[0..($insertAt - 1)]) + @($bullet) + @($lines[$insertAt..($lines.Count - 1)])`.
   When `$insertAt` equals `$lines.Count` (target sub-header is the last
   content in the file, no bullets yet, no trailing blank lines), the
   right-hand slice becomes `$lines[N..N-1]` which is a reverse-range that
   silently aliases to the last element — so the last line was duplicated.
   Reproduced on a synthesised changelog before fixing. Mirrored the EOF
   guard that the `else` branch already has.

Verified by direct PowerShell repros for both: sort now yields
alphabetical order, EOF insertion no longer duplicates the sub-header
line, and the non-EOF + idempotency paths are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Anticipates a future home for other script-related test material. The doc
is unchanged; only its location moves. References inside the doc point at
other scripts (`scripts/lib/releasing.ps1`, `scripts/release-crate.ps1`,
`scripts/check-unreleased-dependencies.ps1`) from the repo root and remain
valid after the move.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 04:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread scripts/check-unreleased-dependencies.ps1 Outdated
Comment thread .github/workflows/main.yml Outdated
Comment thread scripts/release-crate.ps1 Outdated
Comment thread scripts/tests/RELEASE-DEPS-TEST-CASES.md
sandersaares and others added 2 commits May 22, 2026 08:13
…e set

Documentation drift after the per-crate baseline refactor (cedd750). Four
Copilot review comments on PR #436 flagged that synopses, parameter docs,
and the user-facing PR-comment / interactive-warning text still said
"modified vs the PR base ref" — but only the release-set anchor uses
BaseRef; modifications are evaluated per crate against each crate's own
last `version =` / `publish =` commit.

Updated:
- `scripts/check-unreleased-dependencies.ps1` — synopsis, description,
  BaseRef parameter docs, and the markdown body line ("unreleased
  modifications — changes newer than their last `version =` or `publish =`
  bump").
- `scripts/release-crate.ps1` — BaseRef parameter comment, the
  `Invoke-PostReleaseDepScan` function docstring, and the interactive
  warning text. (Switched the warning from a `"..."` to a `'...'` literal
  so the inline `` `version =` `` / `` `publish =` `` backticks aren't
  interpreted as PowerShell escape sequences — `` `v `` is vertical tab.)
- `.github/workflows/main.yml` — `release-deps` job comment.

Self-reviewed the rest of the PR diff for similar drift; the remaining
"vs base" mentions (release-set BFS docstring, "Fetch base ref" workflow
step name, "What this means" sticky-comment block) are legitimate and
unchanged. Re-ran N1-N9 against the updated scripts in a scratch worktree:
9/9 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capture the non-obvious lessons learned while building the N-series
and T-series test harnesses, so future maintainers/agents who need to
rebuild a harness from scratch don't have to relearn them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 05:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread scripts/release-crate.ps1
Comment thread scripts/release-crate.ps1
sandersaares and others added 2 commits May 22, 2026 08:55
The replacement string `\\\\` was producing two literal backslashes
before each escaped metacharacter (e.g. `1.2.3` -> `1\\.2\\.3`), because
`\` is *not* a special character in .NET regex replacement-string syntax
— it's literal — and the two `\\` characters in the PowerShell single-
quoted string both pass through verbatim. The correct replacement is
`\\`: one literal backslash plus the group-1 backreference, matching
`[regex]::Escape` semantics and matching the sibling pattern at
`Add-CascadeBulletToVersionSection`.

This was latent because the only inputs (crate names) never contain any
regex metacharacters, so the double-escape never fired. Fixed for
consistency and to prevent future surprises if the helper is reused.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
scripts/release-crate.ps1 now fully automates the version-bump cascade
(non-dev workspace dependents, root Cargo.toml updates, breaking-vs-patch
classification) that the prompt was guiding humans/agents through. Also
drops the now-stale reference to the prompt from release-crate.ps1's
synopsis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 07:04
@sandersaares sandersaares marked this pull request as draft May 22, 2026 07:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread scripts/release-crate.ps1
}
}

Set-Content -LiteralPath $ChangelogFile -Value $new -Encoding utf8
Comment thread scripts/release-crate.ps1
Comment on lines 23 to +29
is applied: enough to refresh the workspace-pinned version, but without overstating
the change to downstream consumers.
Dev-only dependents are skipped — they automatically pick up the new workspace version.
This mirrors the guidance in `.github/prompts/bump-crate-version.prompt.md`.
3. Changelog Generation: A CHANGELOG.md entry is generated for the target and every cascaded
dependent. Cascaded crates that have no other commits since their last release get a single
`bump \`<target>\` to <new-version>` entry under `🔧 Maintenance` (or `⚠️ Breaking` for
major bumps).
`Now requires <new-version> of \`<target>\`` entry under `🔧 Maintenance` (or `⚠️ Breaking`
for major bumps).
sandersaares and others added 9 commits May 22, 2026 10:16
Replace "file(s)", "crate(s)", "finding(s)", "commit(s)",
"package(s)", "test group(s)", "example(s)" with the bare plural.
Accept the cosmetic "1 crates" case rather than the wishy-washy
parenthetical. Applies the rule consistently across all script outputs
(both .ps1 and .rs), not just the lines in this PR's diff.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nvoke-PostReleaseDepScan

The interactive post-release scan computes $new (findings to prompt for) once
at the top of each outer iteration. Within the foreach over $new, accepting a
release calls Invoke-ReleaseFlow, which can cascade-bump other crates that are
still pending in the same $new. The loop previously continued prompting for
those crates, producing the misleading "Leaving X unreleased" message even
though X had just been released via cascade.

Track the current release set in a HashSet seeded from
Get-CratesWithVersionBumps and grown after every nested Invoke-ReleaseFlow.
Before each Read-Host, skip with a clear "cascade-bumped by a prior release in
this run (now at <version>) — skipping prompt" message when the entry is now
in the release set.

Adds a T17 row to the manual test cases doc covering the regression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces scripts/tests/Pester/ with:
  * Run-Tests.ps1 entry point that validates Pester 5.7+ availability,
    discovers *.Tests.ps1 under unit/, integration/, scenarios/, and
    emits NUnit XML for CI consumption.
  * _common/TestHelpers.ps1 with Get-OxiRepoRoot for deterministic path
    resolution (set via OXI_TEST_COMMON in Run-Tests.ps1).
  * _common/New-SyntheticWorkspace.ps1 — a synthetic Cargo-workspace
    fixture builder with 9 topology presets (Linear2/3/4, Diamond4,
    Macros3, FanOut5, UpDown5, Mixed6, Detached) and ad-hoc -Spec
    support. Workspaces use workspace inheritance (foo.workspace = true)
    to mirror production layout and avoid a latent Update-CrateVersion
    bug pinned by Phase 5.
  * unit/releasing/Smoke.Tests.ps1 sanity check for shared-library
    loading.
  * integration/Topology-Presets.Tests.ps1 — one round-trip test per
    preset covering BFS, dev-dep filtering, publish=false filtering,
    and disconnected-component bleed prevention.

Adds a 'just test-scripts' recipe; the existing 'just install-tools'
recipe now installs Pester 5.7+ idempotently. DEVELOPMENT.md lists
Pester among the prerequisites and points at 'just test-scripts'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pulls the inline entrypoint of release-crate.ps1 into two new functions so
Pester scenarios can drive the full release flow in-process:

  * Invoke-WorkspaceCheck wraps the post-release 'cargo check --workspace'
    call. Tests Mock it; production code calls it inside Invoke-ReleaseMain.
  * Invoke-ReleaseMain wraps input validation, pre-flight checks, GitHub
    remote detection, base-ref resolution, the workflow, and the workspace
    check. Returns the array of release records so tests can assert on
    final state. The script-level execution block is now a single line
    that invokes Invoke-ReleaseMain with the script-level parameters.

Also fixes a latent bug surfaced during refactor validation: when
Test-GitRef sets \ = '' because the base ref could not be
resolved, downstream calls to Invoke-CascadeStep, Invoke-ReleaseFlow,
and Invoke-PostReleaseDepScan would fail parameter binding with a
misleading 'Cannot bind argument' error. The internal short-circuit
'if (\ is empty) return' never fires because parameter
validation rejects the empty string before the body runs. Adding
[AllowEmptyString()] to BaseRef on all three functions lets the
intended skip path execute.

No behavior change for callers that always pass a valid BaseRef.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 55 unit tests covering pure helpers in scripts/lib/releasing.ps1 and
scripts/release-crate.ps1:

  * SemVer arithmetic: Compare-SemanticVersions, Get-NextVersion (incl.
    Cargo 0.x.y and 0.0.z rules), Get-BumpKindFromVersions,
    Test-IsBreakingChange.
  * Input validation: Test-ValidVersion, Test-ValidCrateName.
  * Workspace metadata helpers: Test-CrateExposesTarget,
    Get-CrateFolderForPath.
  * release-crate.ps1 helpers: Sort-KeysByPreferredOrder,
    Format-ConventionalCommits (header grouping, breaking-change lift,
    PR-link injection, ignored-type filtering, miscellaneous bucket).

To dot-source release-crate.ps1 without running its entrypoint, the
script now skips Invoke-ReleaseMain when the env var
OXI_RELEASE_CRATE_NOEXEC is set to "1". CrateName is no longer mandatory
at the script level — Invoke-ReleaseMain validates it after dispatch.
Production behavior is unchanged when the env var is unset.

Found two latent bugs (tracked for Phase 8 bug-bash):
  * Compare-SemanticVersions hits an infinite loop on single-segment
    inputs like "1" because PowerShell scalar += 0 never promotes to an
    array. Production callers never pass such values (Test-ValidVersion
    rejects them) so the bug is dormant, but the pad-to-3 loop is unsafe
    and should be rewritten using [System.Version] or explicit array
    construction.
  * Update-CrateVersion crate-level version-rewrite regex is overly
    broad and would clobber declared versions of inline workspace deps
    (Phase 2 smoke notes). Dormant in production because real crates use
    workspace inheritance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 27 unit tests covering helpers that touch git or the filesystem in
scripts/lib/releasing.ps1 and scripts/release-crate.ps1:

  * Test-GitRef — branch / HEAD / non-existent / SHA cases.
  * Get-CurrentVersion — happy path and missing-file error.
  * Get-CrateVersionFromRef — HEAD, prior commit, and non-existent
    crate folder.
  * Get-CrateLastReleaseBaseline — multi-commit history with source
    edits surrounding a version-changing commit (verifies the right
    commit SHA is returned).
  * Get-WorkspaceCrates — Mixed6 preset coverage of publish=false and
    dev-deps exclusion.
  * Get-AllTransitiveDependents — Diamond4 dedup and Mixed6
    publish=false exclusion.
  * Get-CratesWithUnreleasedChanges — committed / working-tree /
    untracked file paths, and publish=false skip.
  * Get-CratesWithVersionBumps — version-differs and empty-set cases.
  * Add-CascadeBulletToVersionSection — Maintenance vs Breaking
    sub-header selection, missing version section, missing changelog.

Each Describe block calls Invalidate-WorkspaceMetadataCache in BeforeAll
to avoid bleed from a prior fixture; tests that build their own ad-hoc
workspace inside an It also invalidate first.

Full suite is now 93/93 green in ~82s on Windows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 20 Pester integration tests under scripts/tests/Pester/integration/:

- 13 BFS/topology tests re-encoding the N1..N9 scenarios from the manual
  RELEASE-DEPS-TEST-CASES.md harness (plus Linear4/Diamond4/Detached
  topology variants).
- 3 Update-CrateVersion tests, including a pin for the inline-dep
  workspace-version clobbering bug logged in Phase 8.
- 4 Invoke-CascadeStep tests (fresh bump, sufficient pre-bump, upgrade
  insufficient pre-bump, missing crate warning).

Two defensive fixes to support these tests:

- `Invoke-CascadeStep` `-PrBaseUrl` gained `[AllowEmptyString()]`
  so empty strings pass through (production already calls it with empty
  values when `OXI_PR_BASE_URL` is unset). Same class of latent issue
  as the BaseRef fix in Phase 2.
- `Invoke-Git` now disables `\$PSNativeCommandUseErrorActionPreference`
  locally; the function manages exit codes manually via `\$LASTEXITCODE`
  and was throwing prematurely under Run-Tests.ps1's strict-mode default.

Suite size: 113 tests (Phases 1-5), full green in ~140s.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a PSD1-driven scenario runner that exercises the full
release-crate.ps1 entrypoint under mocked prompts and a synthetic
workspace, and lands seven initial scenarios covering smoke, clean
upstream, accept/decline mix, decline-all, non-interactive mode,
diamond aggregation, and the cascade-mid-foreach skip path.

Each .scenario.psd1 declares its topology (preset or custom spec),
history transformations, the release invocation, scripted prompt
answers, and an expected outcome (released crates + versions + raised
prompts + unconsumed answers). The runner mocks Read-Host,
Invoke-WorkspaceCheck, and Test-InteractiveSession, replays the
scenario history, then calls Invoke-ReleaseMain in-process so each
scenario runs in under a second.

While building the scenarios I uncovered a real UX bug in
Get-UnreleasedModifiedDependencies: \ was a plain Hashtable,
whose enumeration order is non-deterministic across processes. That
made the post-release prompt order flaky in production (users would
see prompts in different orders run-to-run) and impossible to assert
in tests. Fix is two parts:

  - \ is now an [ordered]@{} keyed on folder, so BFS
    insertion order is preserved when iterating .Values.
  - The release-set foreach now sorts the input HashSet
    alphabetically so the BFS roots themselves are visited in a
    stable order.

OrderedDictionary uses .Contains(key) instead of .ContainsKey(key);
updated the membership check accordingly.

113 prior tests + 7 scenarios = 120 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new `script-tests` job to .github/workflows/main.yml that runs
the Pester suite (`scripts/tests/Pester/`) on ubuntu-latest and
windows-latest. The job has no delta gate (delta tracks Rust crates and
would mark script-only PRs as skippable, which is exactly the opposite
of what we want here). It installs Pester 5.7.1+ inline using the same
incantation as `just install-tools` and reuses the shared Setup
composite action for cargo (the synthetic-workspace fixture builder
shells out to `cargo metadata`).

The new job is added to `required-checks.needs` so branch protection
keeps requiring only that single context per AGENTS.md policy. It also
has a strategy.matrix, so being part of the fan-in correctly avoids the
stuck-context bug for the matrix-expanded contexts.

Also tightens documentation:
- DEVELOPMENT.md: add `just test-scripts` to the Linux validation list
  for symmetry with the Windows list (already had it).
- RELEASE-DEPS-TEST-CASES.md: reframe as a historical/reference
  document; most behaviours are now pinned by the automated Pester
  suite, but the prose specification is still useful when diagnosing
  against the real workspace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants