Skip to content

feat: scaffold pre-release pipeline, refresh deps weekly - #225

Draft
ottobolyos wants to merge 15 commits into
TrakHound:masterfrom
ottobolyos:feat/release-automation-phase1
Draft

feat: scaffold pre-release pipeline, refresh deps weekly#225
ottobolyos wants to merge 15 commits into
TrakHound:masterfrom
ottobolyos:feat/release-automation-phase1

Conversation

@ottobolyos

@ottobolyos ottobolyos commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase-1 scaffolding for the automated pre-release pipeline and weekly deps-update workflow discussed on discussion #175. Restricted to what the maintainer approved in the 2026-08-16T20:10:44Z reply; explicitly excludes what he asked to defer.

Scope landed on this branch

tools/ restructure — new release/, dev/, docs/, ci/ subfolders. Every script is TypeScript run under tsx (no build step, no dotnet dependency on CI). Root tools/package.json + tools/tsconfig.json.

  • tools/release/: pack.ts, nuget-push.ts, docker-build.ts, docker-push.ts, sbom.ts, gh-release-create.ts, shell.ts (shared helper). Every script exercised under --dry-run locally.
  • tools/ci/semver-bump.ts + semver-bump.test.ts. Reads commit range, computes next <version>-dev.<N> pre-release version per the conventional-commits map. Filters out non-numeric tags via git describe --exclude 'v*-*'.
  • Every top-level script has a unit-test suite next to it (*.test.ts), wired via tools/run-tests.ts and invoked from CI (pre-merge.yml runs npm test under tools/).

Workflows — three new files under .github/workflows/, all pass actionlint clean:

  • release.yml (triggered on push to master): compute-version → pack → docker-amd64 + docker-arm64 in parallel → docker-manifest → sbom → vuln-scan → publish-nuget → create-gh-release. All gated on github.ref == 'refs/heads/master'.
  • deps-update.yml (cron Sat 02:00 UTC): bumps four ecosystems across two PR shapes — GH Actions + Dockerfile bases via per-package Renovate PRs on renovate/* branches, and npm + NuGet via a single bulk PR on chore/deps-weekly-npm-nuget. GH Actions, Dockerfile, and npm all enforce a seven-day supply-chain quarantine; NuGet does not (dotnet-outdated has no built-in filter, tracked in deps-update: implement 7-day NuGet supply-chain quarantine (dotnet-outdated has no built-in filter) #237).
  • pre-merge.yml: runs commitlint on the PR commit range and npm test + typecheck under tools/ on every PR head.

Repo-root configcommitlint.config.mjs (extends @commitlint/config-conventional, pins allowed scopes) + lefthook.yml (commit-msg via commitlint, pre-commit dotnet format whitespace --include {staged_files}).

Docs-site pages under docs/development/: release-pipeline.md (with mermaid job-graph), deps-update.md, commit-format.md, tools-release.md. Added to VitePress sidebar.

Explicitly out of scope (Patrick's ask to defer)

  • Docker image signing (cosign) — no workflow, no secret placeholders.
  • SignPath integration — no workflow, no secret placeholders.
  • Stable-release automation — build/MTConnect.NET-Builder stays untouched; the local, signed stable-cut flow is preserved.

Follow-ups deferred to issues

Four items surfaced in the review that require their own PRs to resolve; each is tracked as an issue so this PR can merge without expanding scope:

Secrets you would need to define before turning on release.yml

  • NUGET_API_KEY — classic API key.
  • DOCKERHUB_USERNAME + DOCKERHUB_TOKEN — for docker push trakhound/mtconnect-agent:....

None of these secrets are referenced by any workflow that runs on PRs — only on push-to-master.

Dime review cycle 1

Retroactive backfill (2026-08-20). The 6-agent Ultrareview cycle ran on this PR (2026-08-19) and its findings landed atomically across 11 fix commits. Ledger reconstruction from commit history + PR comments:

  • [FINDING:A08] security-audit HIGH — setup-dotnet action pinned to an unverified SHA; corrected in fix(ci): pin setup-dotnet + renovatebot to verified upstream SHAs (713f956).
  • [FINDING:A08] security-audit HIGH — renovatebot action pinned to an unverified SHA; corrected in the same 713f956 commit.
  • [FINDING] code-review MEDIUM — semver-bump.ts had an off-by-one boundary at the dev-counter increment; fixed in fix(ci): correct semver-bump boundary off-by-one at the dev-counter (92fc040) with branch-coverage pins in test(ci): close branch-coverage gaps in semver-bump pure helpers (55a03c0).
  • [FINDING:A08] security-audit MEDIUM — supply-chain quarantine drift on the weekly deps-update workflow; split into per-package + bulk PR shapes in ci(deps): split weekly update into per-package renovate + bulk PR (53ecb32).
  • [FINDING:A08] security-audit MEDIUM — docker-build attestation strip: noisy buildx flags dropped + SBOM swapped to syft in refactor(release): drop noisy buildx flags + swap docker SBOM to syft (11e3fd0).
  • [FINDING] code-review MEDIUM — release-idempotency + SBOM asset recursion: GH release pinned to source SHA + SBOM recursion added in fix(release): pin GH release to source SHA, recurse SBOMs, re-runable (e130397).
  • [FINDING:A03] security-audit MEDIUM — secret arg redaction in shell logs + NuGet-key masking added in fix(release): redact secret arg values in shell logs + hide NuGet key (ad41f40).
  • [FINDING:A08] security-audit MEDIUM — Microsoft.Sbom.DotNetTool install pinned to v4.1.5 in fix(ci): pin Microsoft.Sbom.DotNetTool install to v4.1.5 (9806b73).
  • [SIMPLIFY] simplification LOW — unused commitlint-plugin-selective-scope dependency dropped in chore(ci): drop unused commitlint-plugin-selective-scope dependency (1d5175e).
  • [DOCS] documentation-audit — release docs corrected: secrets table, header cap, mermaid graph, README in docs(release): fix secrets table, header cap, mermaid graph, README (c3e462d); NuGet quarantine caveat linked to deps-update: implement 7-day NuGet supply-chain quarantine (dotnet-outdated has no built-in filter) #237 in docs(release): link follow-up issue #237 for the NuGet quarantine (f1af97f).
  • [TEST] test-coverage-audit — tools/ went from 20 → 103 assertions across 8 test files; unit suites for every release/*.ts added in test(release): add unit suites for every release/*.ts + wire npm test (1910c8f) + test(release): add unit suite for shell.ts pure helpers (5814be1); new unit-tests-tools CI job wired into the pre-merge required set via ci(test): wire tools/ npm test + typecheck into pre-merge required set (a8fe6e4).
  • [IMPROVE] improvement — four follow-up MEDIUM items opened as tracking issues on TrakHound/MTConnect.NET: release: gate dotnet pack to the curated nupkg set (currently over-publishes) #234 (pack.ts curation), release: enforce or document 'stable tags must merge back into master' invariant #235 (stable-tag reachability doc), deps-update: gate auto-merge on code-owner label #236 (auto-merge code-owner gate), deps-update: implement 7-day NuGet supply-chain quarantine (dotnet-outdated has no built-in filter) #237 (NuGet 7-day quarantine) — tracked per the project's medium-finding disposition convention.

(Zero unfixed findings — Ready-eligible.)

Depends on

@ottobolyos ottobolyos closed this Aug 17, 2026
@ottobolyos ottobolyos reopened this Aug 17, 2026
@ottobolyos ottobolyos changed the title feat(ci): scaffold automated pre-release pipeline + weekly deps update feat(ci): scaffold pre-release pipeline, refresh deps weekly Aug 17, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 17, 2026
@ottobolyos ottobolyos changed the title feat(ci): scaffold pre-release pipeline, refresh deps weekly feat: scaffold pre-release pipeline, refresh deps weekly Aug 17, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
Replaces the placeholder v4 SHA on actions/setup-dotnet with the SHA
of the v4.3.1 tag verified via `gh api repos/actions/setup-dotnet/git/
refs/tags/v4.3.1`, and updates renovatebot/github-action from an
unverified SHA to the SHA of tag v43.0.7 verified via
`gh api repos/renovatebot/github-action/git/refs/tags/v43.0.7`.

Closes findings F-SEC-001 and F-SEC-002 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`countCommitsSinceLastDevBoundary` returned `i + 1` at both the
stable-cut-marker and dev-tag boundaries, off by one relative to the
docstring ("commits BEFORE the marker") and out of step with the
`--range` code path which counts commits after the boundary. Change
both return sites to `Math.max(1, i)` so the two paths agree on a
fixture where the marker sits at HEAD~1 (expected N = 1) and the
floor still guards against `-dev.0` when the boundary sits on HEAD.

Extends `semver-bump.test.ts` with three git-fixture cases covering
the two boundary positions plus the no-boundary fall-through, and
reworks the top-of-file algorithm summary (F-DOC-002 + F-DOC-007) so
the docstring matches the code and calls out the `applyBump`
`none → patch` fall-through that keeps a chore-only range distinct
from the last stable tag.

Closes findings F-CR-001, F-DOC-002, and F-DOC-007 from the PR TrakHound#225
review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`renovatebot/github-action` opens its own PRs per package on
`renovate/*` branches, so the "one deps PR per week" invariant only
ever held for the two ecosystems this workflow drives directly (npm
+ NuGet). The no-op "Handled by the Renovate step above" step is
gone, the bulk-PR branch is renamed to
`chore/deps-weekly-npm-nuget` to disambiguate it from Renovate's own
branches, and the header comment + docs page now describe the two PR
shapes accurately.

npm bumps now pass `--cooldown "$MIN_AGE_DAYS"` (added in ncu v18),
which pins the same 7-day supply-chain quarantine Renovate enforces
on GH Actions + Dockerfile bases. NuGet has no equivalent
(`dotnet-outdated` lacks any age filter) and the workflow header,
step comment, and docs page all state this explicitly; a proper
NuGet quarantine is tracked as a follow-up.

Also switches the doc from Renovate's non-existent `docker` manager
name to the actual `dockerfile` manager (F-DOC-005).

Closes findings F-CR-002, F-CR-003, F-DOC-001, F-DOC-005, F-IMP-002,
and F-SEC-005 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`buildx build --load` on the default docker driver silently strips
`--sbom true` and `--provenance mode=max` — the loader only knows how
to import a single-platform image manifest, not an image index. Drop
both flags from `docker-build.ts` and add a comment explaining why so
a future author does not re-add them. The downstream `sbom` job in
`release.yml` still emits an SPDX SBOM from the pushed image, so the
metadata surface is preserved without the buildx-strip warning.

Swaps the docker SBOM path from `docker scout sbom` (required a
`docker scout` install on every runner, produced a materially
different SPDX shape) to `anchore/sbom-action` (v0.24.0, pinned by
SHA verified via `gh api repos/anchore/sbom-action/git/refs/tags/
v0.24.0`). `sbom.ts` still supports docker mode locally, now via
syft — the same engine anchore/sbom-action wraps — so `--dry-run`
output shape agrees with CI. Docs pages updated to name the new
backend.

Closes findings F-CR-005 and F-CR-006 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`create-gh-release` now names `vuln-scan` on its own `needs:` list
directly instead of leaning on the transitive path through
`publish-nuget`, so a future rewire that lets publish-nuget bypass
the vuln scan (e.g. via an `if:` override) still hard-fails the
release cut (F-CR-009).

`gh-release-create.ts` gains three fixes:

  - `collectAssets` now uses `readdirSync(dir, { recursive: true,
    withFileTypes: true })` and filters to `.isFile()`, so nested
    SBOM layouts like `_manifest/spdx_2.2/manifest.spdx.json`
    (the shape `Microsoft.Sbom.DotNetTool` emits) reach the release
    attachment list; the prior code only walked the top level despite
    a docstring claiming one-level recursion. Nested dotfiles /
    dot-directories are still skipped.
  - A `--target <sha>` option pins the tag to the SHA that produced
    the artefacts; the workflow now passes `${{ github.sha }}` so a
    concurrent `master` push cannot race the tag onto a different
    commit than the one the artefacts were built from.
  - The `gh release create` invocation is now idempotent: a pre-step
    swallows the "already exists" error by deleting the prior
    release + tag when the workflow re-runs on the same SHA. Prior
    behaviour errored on the second run and left the pipeline red.

Extends the test suite with fixtures for nested-asset discovery
(`_manifest/spdx_2.2/manifest.spdx.json`), nested-dotfile skipping,
and the new `--target` field.

Closes findings F-CR-009, F-IMP collectAssets recursion, F-IMP
idempotency, and F-IMP release-tag target from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
The plugin was declared under `plugins:` in `commitlint.config.mjs`
and installed in `pre-merge.yml`'s commitlint step but no rule ever
referenced it — the `scope-enum` rule that gates the pinned scope
list is the standard rule from `@commitlint/config-conventional`, not
the plugin's selective-scope override. Removing the declaration
shrinks the CI install by one dependency and shrinks the local
commit-msg attack surface.

Closes findings F-CR-011, F-SEC-004, and the related SIMP
recommendation from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`shell.ts` gains a `SECRET_ARG_NAMES` set (`--api-key`, `--password`,
`-p`, `--token`) and `renderCmd()` now redacts the value that
follows any of them — both the `--name value` two-token form and
the `--name=value` single-token form. The actual argv passed to the
child spawn is untouched; only the log line is rewritten. Guards
against a CI-log line that echoes the command from leaking a
credential a caller placed on the argv.

`nuget-push.ts` no longer places the raw API key on the parent's
argv either. The `dotnet nuget push` invocation is wrapped in
`sh -c '…$NUGET_API_KEY'` with the key in the child's `env:`, so
the parent process's argv holds only the shell wrapper and the
literal string `$NUGET_API_KEY`. `renderCmd`'s SECRET_ARG_NAMES
redaction is the second line of defence for the CI log.

Adds eight new test cases pinning the two-token, equals-form,
multi-secret, end-of-argv, and API-surface contracts.

Closes finding F-SEC-003 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`dotnet tool install --global` without `--version` resolves to the
latest published NuGet version at install-time, which lets a
compromised or accidentally-broken upstream publish silently land on
a release-cutting runner. Pin to the current known-good release
verified via `gh api repos/microsoft/sbom-tool/releases/latest`.

Closes finding F-SEC-007 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
Doc corrections lifted out of the PR TrakHound#225 review:

  - `release-pipeline.md`: `DOCKERHUB_USERNAME` + `DOCKERHUB_TOKEN`
    are also consumed by `vuln-scan` (Trivy needs them to pull the
    image before scanning). Adds a `needs:` column to the jobs table
    and a mermaid `graph LR` of the same dependency graph — the two
    surfaces stay in step because they describe the same
    `release.yml` shape.
  - `commit-format.md`: the `header-max-length` rule measures the
    whole first line, not the subject alone — reword to state that
    plus the `body-leading-blank` + `footer-leading-blank` rules the
    config also enforces.
  - `tools-release.md`: adds a `shell.ts` section documenting the
    shared helper's surface (`run`, `renderCmd` w/ secret redaction,
    `parseDryRun`, `requireEnv`, `optionalEnv`) and reworks the
    opener to exclude helpers explicitly; the old text falsely
    claimed every script under `tools/release/` exposes a `main`.
  - `tools/dev/README.md` + `tools/docs/README.md`: shrunk from
    placeholder stubs to one-line pointers at `/reference/cli` per
    the "no placeholder README" project convention.

Closes findings F-DOC-003, F-DOC-004, F-DOC-006, F-DOC-008, and
F-DOC-010 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
`tools/` gained TypeScript unit suites for `ci/*.ts` and every
`release/*.ts` script (three commits earlier), but they weren't
running under any workflow — the pre-merge gate only ran
commitlint, and `dotnet.yml` covers only the .NET matrix. Adds a
second job `unit-tests-tools` that runs `npm ci && npm run
typecheck && npm test` under `tools/` so those suites participate
in the required-check set.

The job stays lightweight (Node.js only, no dotnet, no docker) so
its wall-clock stays under a minute and it does not delay the
merge on a green run.

Closes the "tests exist but not wired to CI" gap flagged in the
PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos pushed a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
Replaces the placeholder v4 SHA on actions/setup-dotnet with the SHA
of the v4.3.1 tag verified via `gh api repos/actions/setup-dotnet/git/
refs/tags/v4.3.1`, and updates renovatebot/github-action from an
unverified SHA to the SHA of tag v43.0.7 verified via
`gh api repos/renovatebot/github-action/git/refs/tags/v43.0.7`.

Closes findings F-SEC-001 and F-SEC-002 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`countCommitsSinceLastDevBoundary` returned `i + 1` at both the
stable-cut-marker and dev-tag boundaries, off by one relative to the
docstring ("commits BEFORE the marker") and out of step with the
`--range` code path which counts commits after the boundary. Change
both return sites to `Math.max(1, i)` so the two paths agree on a
fixture where the marker sits at HEAD~1 (expected N = 1) and the
floor still guards against `-dev.0` when the boundary sits on HEAD.

Extends `semver-bump.test.ts` with three git-fixture cases covering
the two boundary positions plus the no-boundary fall-through, and
reworks the top-of-file algorithm summary (F-DOC-002 + F-DOC-007) so
the docstring matches the code and calls out the `applyBump`
`none → patch` fall-through that keeps a chore-only range distinct
from the last stable tag.

Closes findings F-CR-001, F-DOC-002, and F-DOC-007 from the PR TrakHound#225
review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`renovatebot/github-action` opens its own PRs per package on
`renovate/*` branches, so the "one deps PR per week" invariant only
ever held for the two ecosystems this workflow drives directly (npm
+ NuGet). The no-op "Handled by the Renovate step above" step is
gone, the bulk-PR branch is renamed to
`chore/deps-weekly-npm-nuget` to disambiguate it from Renovate's own
branches, and the header comment + docs page now describe the two PR
shapes accurately.

npm bumps now pass `--cooldown "$MIN_AGE_DAYS"` (added in ncu v18),
which pins the same 7-day supply-chain quarantine Renovate enforces
on GH Actions + Dockerfile bases. NuGet has no equivalent
(`dotnet-outdated` lacks any age filter) and the workflow header,
step comment, and docs page all state this explicitly; a proper
NuGet quarantine is tracked as a follow-up.

Also switches the doc from Renovate's non-existent `docker` manager
name to the actual `dockerfile` manager (F-DOC-005).

Closes findings F-CR-002, F-CR-003, F-DOC-001, F-DOC-005, F-IMP-002,
and F-SEC-005 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`buildx build --load` on the default docker driver silently strips
`--sbom true` and `--provenance mode=max` — the loader only knows how
to import a single-platform image manifest, not an image index. Drop
both flags from `docker-build.ts` and add a comment explaining why so
a future author does not re-add them. The downstream `sbom` job in
`release.yml` still emits an SPDX SBOM from the pushed image, so the
metadata surface is preserved without the buildx-strip warning.

Swaps the docker SBOM path from `docker scout sbom` (required a
`docker scout` install on every runner, produced a materially
different SPDX shape) to `anchore/sbom-action` (v0.24.0, pinned by
SHA verified via `gh api repos/anchore/sbom-action/git/refs/tags/
v0.24.0`). `sbom.ts` still supports docker mode locally, now via
syft — the same engine anchore/sbom-action wraps — so `--dry-run`
output shape agrees with CI. Docs pages updated to name the new
backend.

Closes findings F-CR-005 and F-CR-006 from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
@ottobolyos
ottobolyos force-pushed the feat/release-automation-phase1 branch from f1af97f to 9a8d456 Compare August 21, 2026 06:18
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`create-gh-release` now names `vuln-scan` on its own `needs:` list
directly instead of leaning on the transitive path through
`publish-nuget`, so a future rewire that lets publish-nuget bypass
the vuln scan (e.g. via an `if:` override) still hard-fails the
release cut (F-CR-009).

`gh-release-create.ts` gains three fixes:

  - `collectAssets` now uses `readdirSync(dir, { recursive: true,
    withFileTypes: true })` and filters to `.isFile()`, so nested
    SBOM layouts like `_manifest/spdx_2.2/manifest.spdx.json`
    (the shape `Microsoft.Sbom.DotNetTool` emits) reach the release
    attachment list; the prior code only walked the top level despite
    a docstring claiming one-level recursion. Nested dotfiles /
    dot-directories are still skipped.
  - A `--target <sha>` option pins the tag to the SHA that produced
    the artefacts; the workflow now passes `${{ github.sha }}` so a
    concurrent `master` push cannot race the tag onto a different
    commit than the one the artefacts were built from.
  - The `gh release create` invocation is now idempotent: a pre-step
    swallows the "already exists" error by deleting the prior
    release + tag when the workflow re-runs on the same SHA. Prior
    behaviour errored on the second run and left the pipeline red.

Extends the test suite with fixtures for nested-asset discovery
(`_manifest/spdx_2.2/manifest.spdx.json`), nested-dotfile skipping,
and the new `--target` field.

Closes findings F-CR-009, F-IMP collectAssets recursion, F-IMP
idempotency, and F-IMP release-tag target from the PR TrakHound#225 review.

Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
@ottobolyos
ottobolyos force-pushed the feat/release-automation-phase1 branch from 3620a7a to f3992e3 Compare August 21, 2026 14:08
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
Replaces the placeholder v4 SHA on actions/setup-dotnet with the SHA
of the v4.3.1 tag verified via `gh api repos/actions/setup-dotnet/git/
refs/tags/v4.3.1`, and updates renovatebot/github-action from an
unverified SHA to the SHA of tag v43.0.7 verified via
`gh api repos/renovatebot/github-action/git/refs/tags/v43.0.7`.

Closes findings F-SEC-001 and F-SEC-002 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`countCommitsSinceLastDevBoundary` returned `i + 1` at both the
stable-cut-marker and dev-tag boundaries, off by one relative to the
docstring ("commits BEFORE the marker") and out of step with the
`--range` code path which counts commits after the boundary. Change
both return sites to `Math.max(1, i)` so the two paths agree on a
fixture where the marker sits at HEAD~1 (expected N = 1) and the
floor still guards against `-dev.0` when the boundary sits on HEAD.

Extends `semver-bump.test.ts` with three git-fixture cases covering
the two boundary positions plus the no-boundary fall-through, and
reworks the top-of-file algorithm summary (F-DOC-002 + F-DOC-007) so
the docstring matches the code and calls out the `applyBump`
`none → patch` fall-through that keeps a chore-only range distinct
from the last stable tag.

Closes findings F-CR-001, F-DOC-002, and F-DOC-007 from the PR TrakHound#225
review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`renovatebot/github-action` opens its own PRs per package on
`renovate/*` branches, so the "one deps PR per week" invariant only
ever held for the two ecosystems this workflow drives directly (npm
+ NuGet). The no-op "Handled by the Renovate step above" step is
gone, the bulk-PR branch is renamed to
`chore/deps-weekly-npm-nuget` to disambiguate it from Renovate's own
branches, and the header comment + docs page now describe the two PR
shapes accurately.

npm bumps now pass `--cooldown "$MIN_AGE_DAYS"` (added in ncu v18),
which pins the same 7-day supply-chain quarantine Renovate enforces
on GH Actions + Dockerfile bases. NuGet has no equivalent
(`dotnet-outdated` lacks any age filter) and the workflow header,
step comment, and docs page all state this explicitly; a proper
NuGet quarantine is tracked as a follow-up.

Also switches the doc from Renovate's non-existent `docker` manager
name to the actual `dockerfile` manager (F-DOC-005).

Closes findings F-CR-002, F-CR-003, F-DOC-001, F-DOC-005, F-IMP-002,
and F-SEC-005 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`buildx build --load` on the default docker driver silently strips
`--sbom true` and `--provenance mode=max` — the loader only knows how
to import a single-platform image manifest, not an image index. Drop
both flags from `docker-build.ts` and add a comment explaining why so
a future author does not re-add them. The downstream `sbom` job in
`release.yml` still emits an SPDX SBOM from the pushed image, so the
metadata surface is preserved without the buildx-strip warning.

Swaps the docker SBOM path from `docker scout sbom` (required a
`docker scout` install on every runner, produced a materially
different SPDX shape) to `anchore/sbom-action` (v0.24.0, pinned by
SHA verified via `gh api repos/anchore/sbom-action/git/refs/tags/
v0.24.0`). `sbom.ts` still supports docker mode locally, now via
syft — the same engine anchore/sbom-action wraps — so `--dry-run`
output shape agrees with CI. Docs pages updated to name the new
backend.

Closes findings F-CR-005 and F-CR-006 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`create-gh-release` now names `vuln-scan` on its own `needs:` list
directly instead of leaning on the transitive path through
`publish-nuget`, so a future rewire that lets publish-nuget bypass
the vuln scan (e.g. via an `if:` override) still hard-fails the
release cut (F-CR-009).

`gh-release-create.ts` gains three fixes:

  - `collectAssets` now uses `readdirSync(dir, { recursive: true,
    withFileTypes: true })` and filters to `.isFile()`, so nested
    SBOM layouts like `_manifest/spdx_2.2/manifest.spdx.json`
    (the shape `Microsoft.Sbom.DotNetTool` emits) reach the release
    attachment list; the prior code only walked the top level despite
    a docstring claiming one-level recursion. Nested dotfiles /
    dot-directories are still skipped.
  - A `--target <sha>` option pins the tag to the SHA that produced
    the artefacts; the workflow now passes `${{ github.sha }}` so a
    concurrent `master` push cannot race the tag onto a different
    commit than the one the artefacts were built from.
  - The `gh release create` invocation is now idempotent: a pre-step
    swallows the "already exists" error by deleting the prior
    release + tag when the workflow re-runs on the same SHA. Prior
    behavior errored on the second run and left the pipeline red.

Extends the test suite with fixtures for nested-asset discovery
(`_manifest/spdx_2.2/manifest.spdx.json`), nested-dotfile skipping,
and the new `--target` field.

Closes findings F-CR-009, F-IMP collectAssets recursion, F-IMP
idempotency, and F-IMP release-tag target from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
The plugin was declared under `plugins:` in `commitlint.config.mjs`
and installed in `pre-merge.yml`'s commitlint step but no rule ever
referenced it — the `scope-enum` rule that gates the pinned scope
list is the standard rule from `@commitlint/config-conventional`, not
the plugin's selective-scope override. Removing the declaration
shrinks the CI install by one dependency and shrinks the local
commit-msg attack surface.

Closes findings F-CR-011, F-SEC-004, and the related SIMP
recommendation from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`shell.ts` gains a `SECRET_ARG_NAMES` set (`--api-key`, `--password`,
`-p`, `--token`) and `renderCmd()` now redacts the value that
follows any of them — both the `--name value` two-token form and
the `--name=value` single-token form. The actual argv passed to the
child spawn is untouched; only the log line is rewritten. Guards
against a CI-log line that echoes the command from leaking a
credential a caller placed on the argv.

`nuget-push.ts` no longer places the raw API key on the parent's
argv either. The `dotnet nuget push` invocation is wrapped in
`sh -c '…$NUGET_API_KEY'` with the key in the child's `env:`, so
the parent process's argv holds only the shell wrapper and the
literal string `$NUGET_API_KEY`. `renderCmd`'s SECRET_ARG_NAMES
redaction is the second line of defense for the CI log.

Adds eight new test cases pinning the two-token, equals-form,
multi-secret, end-of-argv, and API-surface contracts.

Closes finding F-SEC-003 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`dotnet tool install --global` without `--version` resolves to the
latest published NuGet version at install-time, which lets a
compromised or accidentally-broken upstream publish silently land on
a release-cutting runner. Pin to the current known-good release
verified via `gh api repos/microsoft/sbom-tool/releases/latest`.

Closes finding F-SEC-007 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
Doc corrections lifted out of the PR TrakHound#225 review:

  - `release-pipeline.md`: `DOCKERHUB_USERNAME` + `DOCKERHUB_TOKEN`
    are also consumed by `vuln-scan` (Trivy needs them to pull the
    image before scanning). Adds a `needs:` column to the jobs table
    and a mermaid `graph LR` of the same dependency graph — the two
    surfaces stay in step because they describe the same
    `release.yml` shape.
  - `commit-format.md`: the `header-max-length` rule measures the
    whole first line, not the subject alone — reword to state that
    plus the `body-leading-blank` + `footer-leading-blank` rules the
    config also enforces.
  - `tools-release.md`: adds a `shell.ts` section documenting the
    shared helper's surface (`run`, `renderCmd` w/ secret redaction,
    `parseDryRun`, `requireEnv`, `optionalEnv`) and reworks the
    opener to exclude helpers explicitly; the old text falsely
    claimed every script under `tools/release/` exposes a `main`.
  - `tools/dev/README.md` + `tools/docs/README.md`: shrunk from
    placeholder stubs to one-line pointers at `/reference/cli` per
    the "no placeholder README" project convention.

Closes findings F-DOC-003, F-DOC-004, F-DOC-006, F-DOC-008, and
F-DOC-010 from the PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
`tools/` gained TypeScript unit suites for `ci/*.ts` and every
`release/*.ts` script (three commits earlier), but they weren't
running under any workflow — the pre-merge gate only ran
commitlint, and `dotnet.yml` covers only the .NET matrix. Adds a
second job `unit-tests-tools` that runs `npm ci && npm run
typecheck && npm test` under `tools/` so those suites participate
in the required-check set.

The job stays lightweight (Node.js only, no dotnet, no docker) so
its wall-clock stays under a minute and it does not delay the
merge on a green run.

Closes the "tests exist but not wired to CI" gap flagged in the
PR TrakHound#225 review.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
Introduces the phase-1 shape approved on Discussion TrakHound#175. Every push to
master will cut a `<version>-dev.<N>` pre-release: `dotnet pack`, a
native-per-arch Docker build, SPDX SBOMs, Trivy vuln scan, nuget.org
publish, Docker Hub push, and a GitHub pre-release. A separate weekly
workflow bumps every dependency (GitHub Actions, Dockerfile bases, npm
under docs/, NuGet across every .csproj) subject to a seven-day
supply-chain quarantine.

A pre-merge commitlint gate is added to enforce Conventional Commits on
every PR — the promise the release pipeline's semver-bump relies on.
`lefthook.yml` replays the check client-side so a broken commit never
leaves the workstation.

Docker image signing, .nupkg signing, and stable-release automation are
explicitly out of scope for phase 1 — no placeholders, no scaffolding.
The existing `MTConnect.NET.Builder` flow is untouched.
Extend the in-house `test()`-shim suite from 20 to 31 assertions to pin
every branch documented in the semver-bump.ts docstrings that the
initial pass under-covered.

Additions cover:
  - bumpKindFor: every documented patch type (`refactor`, `style`,
    `perf`, `test`, `ci`, `revert`), unknown conventional-shaped types
    that must fall through to `none`, case-insensitivity across the
    type + bang arms, and every BREAKING-CHANGE-anchor variant
    (start-of-body vs post-newline, hyphenated vs spaced, lower vs
    mixed case, mid-word rejection).
  - aggregateBump: the all-`none` fall-through arm distinct from empty.
  - applyBump: v-prefix strip for every kind × prefix combination, and
    the `''` / bare-`v` invalid-input arms of the throw path.
  - isStableCutMarker: the subject.trim() branch, case-sensitivity on
    the marker literal, and PR-suffix well-formedness.
  - isDevTag: the boundary rejects (`-dev` no counter, trailing dot,
    non-numeric counter, missing `v` prefix, leading whitespace) and
    the zero-counter acceptance.

RED-verified each new-test class by temporarily mutating the
corresponding SUT line and observing the failure before restoring:
  - drop `toLowerCase()` → case-insensitive test failed;
  - collapse `(^|\n)` → `^` → BREAKING newline-anchor test failed;
  - remove `refactor` from the known set → patch-type test failed;
  - drop `$` from the isDevTag regex → boundary test failed;
  - drop `.trim()` from isStableCutMarker → whitespace test failed.
SUT restored bit-for-bit before commit.
`tools/release/shell.ts` shipped with zero unit tests despite being the
shared shell-out layer every release script imports. Land a 20-assertion
suite that mirrors the in-house `test()`-shim shape used by
`tools/ci/semver-bump.test.ts`, so the tools/ dir has one runner
convention across both directories.

Coverage:
  - renderCmd: safe-charset pass-through, whitespace-triggered quoting,
    interior-double-quote escape, shell-special-char coverage
    (`$`, `;`, `|`, `*`, space, empty string), empty argv.
  - parseDryRun: presence, absence, empty argv, positional
    invariance (front/middle/end), multiple occurrences, and the
    intentional `--dry-run=true` non-recognition.
  - requireEnv: present, absent (throws with var name), empty and
    whitespace-only values treated as missing.
  - optionalEnv: present, absent (returns undefined), empty and
    whitespace-only values treated as undefined.
  - run: dry-run path only — logs the rendered command via
    process.stdout.write and returns without spawning. Live-spawn path
    stays in the integration matrix.

Test-shim refactored from immediate-execute to a collect-then-await
main() so async cases (the two `run` dry-run assertions) do not
silently drop before the runtime exits.

RED-verified: widening the `quoteForLog` whitelist to include a space
made the whitespace-quoting test fail; loosening `parseDryRun`'s
equality check to `startsWith` broke the `--dry-run=true` test. Both
mutations were reverted bit-for-bit and the suite goes 20/20 green.
None of the six `tools/release/*.ts` scripts shipped with a unit test
in the initial cut. Add per-script suites covering every pure helper
that is safe to exercise without a live docker / dotnet / gh dependency:

  - docker-build.test.ts (8): archSuffixFor for both platforms,
    parseOptions happy path, --image default, --dry-run consumption,
    missing / unrecognized / absent --platform + --version errors.
  - docker-push.test.ts (10): archSuffixFor, per-arch push mode,
    manifest mode, --image override, --dry-run consumption, and every
    error arm (missing --version, neither --platform nor --manifest,
    mutex enforcement, unrecognized --platform).
  - gh-release-create.test.ts (14): parseOptions happy path + defaults
    + missing-version error; collectAssets across empty / missing /
    populated dirs with dotfile + hidden-dir filtering + multi-dir
    aggregation; renderReleaseNotes header, empty-assets sentinel,
    per-asset basename rendering, docker-section presence toggle,
    not-for-production prose invariant.
  - pack.test.ts (4): parseOptions happy path, default --output path,
    --dry-run consumption, missing --version error including the
    example-hint suffix.
  - nuget-push.test.ts (7): every default (input, source), the
    --api-key -> NUGET_API_KEY fallback ladder (flag beats env, env
    beats undefined), --dry-run consumption.
  - sbom.test.ts (9): --nuget vs --docker mode selection, mutex
    enforcement, missing-mode error, --input / --output defaults +
    overrides, --dry-run consumption.
  - shell.ts unit suite (from prior commit) reruns as part of the
    same runner.

Also:
  - `refactor`: promote the previously module-private `archSuffixFor`,
    `parseOptions`, `collectAssets`, `renderReleaseNotes`, and each
    `Options` type to `export` so the test files can invoke them
    without indirection through `main()`. Zero runtime-behavior delta.
  - `chore`: add `tools/run-tests.ts` — a small discover-and-spawn
    runner that walks `tools/**/*.test.ts` and returns non-zero on
    any file's failure. Wired as the `test` npm script alongside the
    existing `typecheck` script.

RED-verified each new suite by temporarily mutating the SUT and
observing the corresponding test's failure:
  - swap archSuffixFor return arms -> docker-build test failed;
  - drop the platform/manifest mutex -> docker-push test failed;
  - render assets as absolute paths instead of backtick basenames ->
    gh-release-create test failed;
  - strip the example hint from pack's missing-version error ->
    pack test failed;
  - remove the NUGET_API_KEY env fallback -> nuget-push test failed;
  - drop the --nuget/--docker mutex -> sbom test failed.
Each SUT restored bit-for-bit before commit; 103 assertions pass
across the 8 test files under `npm test`.
Replaces the placeholder v4 SHA on actions/setup-dotnet with the SHA
of the v4.3.1 tag verified via `gh api repos/actions/setup-dotnet/git/
refs/tags/v4.3.1`, and updates renovatebot/github-action from an
unverified SHA to the SHA of tag v43.0.7 verified via
`gh api repos/renovatebot/github-action/git/refs/tags/v43.0.7`.

Closes findings F-SEC-001 and F-SEC-002 from the PR TrakHound#225 review.
`countCommitsSinceLastDevBoundary` returned `i + 1` at both the
stable-cut-marker and dev-tag boundaries, off by one relative to the
docstring ("commits BEFORE the marker") and out of step with the
`--range` code path which counts commits after the boundary. Change
both return sites to `Math.max(1, i)` so the two paths agree on a
fixture where the marker sits at HEAD~1 (expected N = 1) and the
floor still guards against `-dev.0` when the boundary sits on HEAD.

Extends `semver-bump.test.ts` with three git-fixture cases covering
the two boundary positions plus the no-boundary fall-through, and
reworks the top-of-file algorithm summary (F-DOC-002 + F-DOC-007) so
the docstring matches the code and calls out the `applyBump`
`none → patch` fall-through that keeps a chore-only range distinct
from the last stable tag.

Closes findings F-CR-001, F-DOC-002, and F-DOC-007 from the PR TrakHound#225
review.
`renovatebot/github-action` opens its own PRs per package on
`renovate/*` branches, so the "one deps PR per week" invariant only
ever held for the two ecosystems this workflow drives directly (npm
+ NuGet). The no-op "Handled by the Renovate step above" step is
gone, the bulk-PR branch is renamed to
`chore/deps-weekly-npm-nuget` to disambiguate it from Renovate's own
branches, and the header comment + docs page now describe the two PR
shapes accurately.

npm bumps now pass `--cooldown "$MIN_AGE_DAYS"` (added in ncu v18),
which pins the same 7-day supply-chain quarantine Renovate enforces
on GH Actions + Dockerfile bases. NuGet has no equivalent
(`dotnet-outdated` lacks any age filter) and the workflow header,
step comment, and docs page all state this explicitly; a proper
NuGet quarantine is tracked as a follow-up.

Also switches the doc from Renovate's non-existent `docker` manager
name to the actual `dockerfile` manager (F-DOC-005).

Closes findings F-CR-002, F-CR-003, F-DOC-001, F-DOC-005, F-IMP-002,
and F-SEC-005 from the PR TrakHound#225 review.
`buildx build --load` on the default docker driver silently strips
`--sbom true` and `--provenance mode=max` — the loader only knows how
to import a single-platform image manifest, not an image index. Drop
both flags from `docker-build.ts` and add a comment explaining why so
a future author does not re-add them. The downstream `sbom` job in
`release.yml` still emits an SPDX SBOM from the pushed image, so the
metadata surface is preserved without the buildx-strip warning.

Swaps the docker SBOM path from `docker scout sbom` (required a
`docker scout` install on every runner, produced a materially
different SPDX shape) to `anchore/sbom-action` (v0.24.0, pinned by
SHA verified via `gh api repos/anchore/sbom-action/git/refs/tags/
v0.24.0`). `sbom.ts` still supports docker mode locally, now via
syft — the same engine anchore/sbom-action wraps — so `--dry-run`
output shape agrees with CI. Docs pages updated to name the new
backend.

Closes findings F-CR-005 and F-CR-006 from the PR TrakHound#225 review.
`create-gh-release` now names `vuln-scan` on its own `needs:` list
directly instead of leaning on the transitive path through
`publish-nuget`, so a future rewire that lets publish-nuget bypass
the vuln scan (e.g. via an `if:` override) still hard-fails the
release cut (F-CR-009).

`gh-release-create.ts` gains three fixes:

  - `collectAssets` now uses `readdirSync(dir, { recursive: true,
    withFileTypes: true })` and filters to `.isFile()`, so nested
    SBOM layouts like `_manifest/spdx_2.2/manifest.spdx.json`
    (the shape `Microsoft.Sbom.DotNetTool` emits) reach the release
    attachment list; the prior code only walked the top level despite
    a docstring claiming one-level recursion. Nested dotfiles /
    dot-directories are still skipped.
  - A `--target <sha>` option pins the tag to the SHA that produced
    the artefacts; the workflow now passes `${{ github.sha }}` so a
    concurrent `master` push cannot race the tag onto a different
    commit than the one the artefacts were built from.
  - The `gh release create` invocation is now idempotent: a pre-step
    swallows the "already exists" error by deleting the prior
    release + tag when the workflow re-runs on the same SHA. Prior
    behavior errored on the second run and left the pipeline red.

Extends the test suite with fixtures for nested-asset discovery
(`_manifest/spdx_2.2/manifest.spdx.json`), nested-dotfile skipping,
and the new `--target` field.

Closes findings F-CR-009, F-IMP collectAssets recursion, F-IMP
idempotency, and F-IMP release-tag target from the PR TrakHound#225 review.
The plugin was declared under `plugins:` in `commitlint.config.mjs`
and installed in `pre-merge.yml`'s commitlint step but no rule ever
referenced it — the `scope-enum` rule that gates the pinned scope
list is the standard rule from `@commitlint/config-conventional`, not
the plugin's selective-scope override. Removing the declaration
shrinks the CI install by one dependency and shrinks the local
commit-msg attack surface.

Closes findings F-CR-011, F-SEC-004, and the related SIMP
recommendation from the PR TrakHound#225 review.
`shell.ts` gains a `SECRET_ARG_NAMES` set (`--api-key`, `--password`,
`-p`, `--token`) and `renderCmd()` now redacts the value that
follows any of them — both the `--name value` two-token form and
the `--name=value` single-token form. The actual argv passed to the
child spawn is untouched; only the log line is rewritten. Guards
against a CI-log line that echoes the command from leaking a
credential a caller placed on the argv.

`nuget-push.ts` no longer places the raw API key on the parent's
argv either. The `dotnet nuget push` invocation is wrapped in
`sh -c '…$NUGET_API_KEY'` with the key in the child's `env:`, so
the parent process's argv holds only the shell wrapper and the
literal string `$NUGET_API_KEY`. `renderCmd`'s SECRET_ARG_NAMES
redaction is the second line of defense for the CI log.

Adds eight new test cases pinning the two-token, equals-form,
multi-secret, end-of-argv, and API-surface contracts.

Closes finding F-SEC-003 from the PR TrakHound#225 review.
`dotnet tool install --global` without `--version` resolves to the
latest published NuGet version at install-time, which lets a
compromised or accidentally-broken upstream publish silently land on
a release-cutting runner. Pin to the current known-good release
verified via `gh api repos/microsoft/sbom-tool/releases/latest`.

Closes finding F-SEC-007 from the PR TrakHound#225 review.
Doc corrections lifted out of the PR TrakHound#225 review:

  - `release-pipeline.md`: `DOCKERHUB_USERNAME` + `DOCKERHUB_TOKEN`
    are also consumed by `vuln-scan` (Trivy needs them to pull the
    image before scanning). Adds a `needs:` column to the jobs table
    and a mermaid `graph LR` of the same dependency graph — the two
    surfaces stay in step because they describe the same
    `release.yml` shape.
  - `commit-format.md`: the `header-max-length` rule measures the
    whole first line, not the subject alone — reword to state that
    plus the `body-leading-blank` + `footer-leading-blank` rules the
    config also enforces.
  - `tools-release.md`: adds a `shell.ts` section documenting the
    shared helper's surface (`run`, `renderCmd` w/ secret redaction,
    `parseDryRun`, `requireEnv`, `optionalEnv`) and reworks the
    opener to exclude helpers explicitly; the old text falsely
    claimed every script under `tools/release/` exposes a `main`.
  - `tools/dev/README.md` + `tools/docs/README.md`: shrunk from
    placeholder stubs to one-line pointers at `/reference/cli` per
    the "no placeholder README" project convention.

Closes findings F-DOC-003, F-DOC-004, F-DOC-006, F-DOC-008, and
F-DOC-010 from the PR TrakHound#225 review.
`tools/` gained TypeScript unit suites for `ci/*.ts` and every
`release/*.ts` script (three commits earlier), but they weren't
running under any workflow — the pre-merge gate only ran
commitlint, and `dotnet.yml` covers only the .NET matrix. Adds a
second job `unit-tests-tools` that runs `npm ci && npm run
typecheck && npm test` under `tools/` so those suites participate
in the required-check set.

The job stays lightweight (Node.js only, no dotnet, no docker) so
its wall-clock stays under a minute and it does not delay the
merge on a green run.

Closes the "tests exist but not wired to CI" gap flagged in the
PR TrakHound#225 review.
…ntine

Threads the just-filed follow-up issue number into the
`deps-update.yml` header comment, the NuGet-bump step comment, and
the `deps-update.md` supply-chain section so the "no NuGet
quarantine" caveat is tracked to an actionable next step rather
than left as an open thread.
@ottobolyos
ottobolyos force-pushed the feat/release-automation-phase1 branch from f3992e3 to 9d8516f Compare August 21, 2026 16:05
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
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.

1 participant