Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd52cd7
Initial commit with task details
konard Aug 20, 2026
cde9620
docs(dev-log): preserve CI/CD evidence for issue #41 before any change
konard Aug 20, 2026
d913208
docs(dev-log): deep analysis of CI/CD false positives, negatives, war…
konard Aug 20, 2026
ef20bf6
ci: add bounded post-publish registry readback with verbose mode
konard Aug 20, 2026
e46dd0b
ci(csharp): use the bounded readback in both release paths
konard Aug 20, 2026
a797ada
ci(python): lint the release helper scripts too
konard Aug 20, 2026
097056a
ci(python): explain the PyPI publish failure and widen the readback
konard Aug 20, 2026
6b1e962
ci: verify the crates.io publish and test the shared scripts
konard Aug 20, 2026
fa867ad
ci: bump all actions to Node 24 majors and fix codecov inputs
konard Aug 20, 2026
b86f595
build: pack the NuGet readme and fix Python licence metadata
konard Aug 20, 2026
bf90ac2
chore(js): npm audit fix to clear 7 advisories (2 moderate, 5 high)
konard Aug 20, 2026
4ff3717
refactor(js): split the codec dispatch to clear ESLint complexity war…
konard Aug 20, 2026
9149a3f
ci: harden workflow permissions, timeouts and concurrency
konard Aug 20, 2026
1d8dc18
ci: lint every workflow with actionlint
konard Aug 20, 2026
d3738cf
ci: add security scanning and a broken-link check
konard Aug 20, 2026
d28060b
ci(links): exclude node_modules and bot-blocked npmjs.com from link c…
konard Aug 20, 2026
cee793c
ci: validate the actual merge result on pull requests (best practice #7)
github-actions[bot] Aug 20, 2026
1c3b6e5
ci(csharp): enforce the file-size limit and run the helper script tests
github-actions[bot] Aug 20, 2026
f1a50c9
ci(security): add committed-secret scanning (best practice #11)
github-actions[bot] Aug 20, 2026
b049685
ci: make the Python and C# release-note checks able to fail
github-actions[bot] Aug 20, 2026
67ea0a8
ci(python): test on macOS and Windows too
github-actions[bot] Aug 20, 2026
51f7ed4
chore: add release notes for the CI/CD fixes in all four languages
github-actions[bot] Aug 20, 2026
f6f143d
fix(ci): make change detection use package-relative paths
github-actions[bot] Aug 20, 2026
967430d
ci: gate the Python and C# jobs on change detection (template parity)
github-actions[bot] Aug 20, 2026
f82dbc5
docs(dev-log): record what was executed for issue #41
github-actions[bot] Aug 20, 2026
f9721dc
docs(dev-log): correct the upstream findings and link the filed reports
github-actions[bot] Aug 20, 2026
ebbd1d9
ci: quote $GITHUB_OUTPUT redirections flagged by shellcheck
github-actions[bot] Aug 20, 2026
e72ed2b
ci(security): replace unavailable dependency review with a NuGet audit
github-actions[bot] Aug 20, 2026
dde3a7c
docs(dev-log): record why dependency review was replaced by a NuGet a…
github-actions[bot] Aug 20, 2026
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
326 changes: 254 additions & 72 deletions .github/workflows/csharp.yml

Large diffs are not rendered by default.

174 changes: 140 additions & 34 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ on:
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Least privilege by default (best practice #10 / template parity): individual
# release jobs raise this to `contents: write` where they need it.
permissions:
contents: read

jobs:
# === DETECT CHANGES - determines which jobs should run ===
detect-changes:
timeout-minutes: 10
# Read-only check: a newer run supersedes this one.
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-detect-changes
cancel-in-progress: true
name: Detect Changes
runs-on: ubuntu-latest
if: github.event_name != 'workflow_dispatch'
Expand All @@ -53,7 +59,7 @@ jobs:
workflow-changed: ${{ steps.changes.outputs.workflow-changed }}
any-code-changed: ${{ steps.changes.outputs.any-code-changed }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand All @@ -69,11 +75,16 @@ jobs:
# === VERSION CHANGE CHECK ===
# Prohibit manual version changes in package.json - versions should only be changed by CI/CD
version-check:
timeout-minutes: 10
# Read-only check: a newer run supersedes this one.
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-version-check
cancel-in-progress: true
name: Check for Manual Version Changes
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand All @@ -87,17 +98,34 @@ jobs:
# === CHANGESET CHECK - only runs on PRs with code changes ===
# Docs-only PRs (./docs folder, markdown files) don't require changesets
changeset-check:
timeout-minutes: 10
# Read-only check: a newer run supersedes this one.
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-changeset-check
cancel-in-progress: true
name: Check for Changesets
runs-on: ubuntu-latest
needs: [detect-changes]
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

# Best practice #7: the `refs/pull/N/merge` preview goes stale as soon as
# the base branch moves, so checks would validate code that is not what
# will actually land. Merge the latest base branch first.
- name: Simulate fresh merge with base branch (PR only)
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
# Explicit shell: some of these jobs run on Windows runners, where
# the default shell is PowerShell.
shell: bash
run: bash scripts/simulate-fresh-merge.sh

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'

Expand All @@ -111,9 +139,12 @@ jobs:
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# Passed through the environment: a branch name is attacker-controlled
# and must never be interpolated straight into a shell script.
HEAD_REF: ${{ github.head_ref }}
run: |
# Skip changeset check for automated version PRs
if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
if [[ "$HEAD_REF" == "changeset-release/"* ]]; then
echo "Skipping changeset check for automated release PR"
exit 0
fi
Expand All @@ -124,24 +155,46 @@ jobs:
# === LINT AND FORMAT CHECK ===
# Lint runs independently of changeset-check - it's a fast check that should always run
lint:
timeout-minutes: 15
# Read-only check: a newer run supersedes this one.
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-lint
cancel-in-progress: true
name: Lint and Format Check
runs-on: ubuntu-latest
needs: [detect-changes]
if: |
always() && !cancelled() && (
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.mjs-changed == 'true' ||
needs.detect-changes.outputs.js-changed == 'true' ||
needs.detect-changes.outputs.docs-changed == 'true' ||
needs.detect-changes.outputs.package-changed == 'true' ||
needs.detect-changes.outputs.workflow-changed == 'true'
if: >-
${{
!cancelled() && (
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.mjs-changed == 'true' ||
needs.detect-changes.outputs.js-changed == 'true' ||
needs.detect-changes.outputs.docs-changed == 'true' ||
needs.detect-changes.outputs.package-changed == 'true' ||
needs.detect-changes.outputs.workflow-changed == 'true'
)
}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
# Full history: the merge simulation below needs the base branch.
fetch-depth: 0

# Best practice #7: the `refs/pull/N/merge` preview goes stale as soon as
# the base branch moves, so checks would validate code that is not what
# will actually land. Merge the latest base branch first.
- name: Simulate fresh merge with base branch (PR only)
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
# Explicit shell: some of these jobs run on Windows runners, where
# the default shell is PowerShell.
shell: bash
run: bash scripts/simulate-fresh-merge.sh

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'

Expand All @@ -161,22 +214,49 @@ jobs:
working-directory: ./js
run: npm run check:duplication

# `npm test` only covers `tests/`, so the CI helper scripts under
# `js/scripts` had no automated coverage before issue #41 even though the
# release jobs depend on them.
- name: Run CI script tests
working-directory: ./js
run: node --test scripts/*.test.mjs

# Test matrix: Node.js on multiple OS
test:
timeout-minutes: 30
# Read-only check: a newer run supersedes this one.
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-test-${{ matrix.os }}
cancel-in-progress: true
name: Test (Node.js on ${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: [detect-changes, changeset-check]
# Run if: push event, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR)
if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped')
if: ${{ !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
# Full history: the merge simulation below needs the base branch.
fetch-depth: 0

# Best practice #7: the `refs/pull/N/merge` preview goes stale as soon as
# the base branch moves, so checks would validate code that is not what
# will actually land. Merge the latest base branch first.
- name: Simulate fresh merge with base branch (PR only)
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
# Explicit shell: some of these jobs run on Windows runners, where
# the default shell is PowerShell.
shell: bash
run: bash scripts/simulate-fresh-merge.sh

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'

Expand All @@ -194,24 +274,32 @@ jobs:

# Release - only runs on main after tests pass (for push events)
release:
timeout-minutes: 45
# Writer: every job in the repository that pushes commits, tags or
# packages shares this group, so releases queue instead of racing, and a
# started release is never cancelled mid-publish.
concurrency:
group: main-writer-${{ github.repository }}
cancel-in-progress: false
queue: max
name: Release
needs: [lint, test]
# Use always() to ensure this job runs even if changeset-check was skipped
# Use !cancelled() so this job still runs when changeset-check was skipped
# This is needed because lint/test jobs have a transitive dependency on changeset-check
if: always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success'
if: ${{ !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success' }}
runs-on: ubuntu-latest
# Permissions required for npm OIDC trusted publishing
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
Expand Down Expand Up @@ -265,28 +353,38 @@ jobs:

# Manual Instant Release - triggered via workflow_dispatch with instant mode
instant-release:
timeout-minutes: 45
# Writer: every job in the repository that pushes commits, tags or
# packages shares this group, so releases queue instead of racing, and a
# started release is never cancelled mid-publish.
concurrency:
group: main-writer-${{ github.repository }}
cancel-in-progress: false
queue: max
name: Instant Release
needs: [lint, test]
# Note: always() is required to evaluate the condition when dependencies use always()
if: |
always() && !cancelled() &&
# Note: !cancelled() is required to evaluate the condition when dependencies may be skipped
if: >-
${{
!cancelled() &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.release_mode == 'instant' &&
needs.lint.result == 'success' &&
needs.test.result == 'success'
}}
runs-on: ubuntu-latest
# Permissions required for npm OIDC trusted publishing
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
Expand Down Expand Up @@ -327,19 +425,27 @@ jobs:

# Manual Changeset PR - creates a pull request with the changeset for review
changeset-pr:
timeout-minutes: 20
# Writer: every job in the repository that pushes commits, tags or
# packages shares this group, so releases queue instead of racing, and a
# started release is never cancelled mid-publish.
concurrency:
group: main-writer-${{ github.repository }}
cancel-in-progress: false
queue: max
name: Create Changeset PR
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '22'

Expand All @@ -360,7 +466,7 @@ jobs:
echo "Formatted changeset files"

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release'
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Broken Link Checker

# Best practice #12: documentation is validated in CI like code. Before issue
# #41 nothing checked the links in the README files that every registry page
# renders, so a dead link shipped to npm, PyPI, crates.io and NuGet unnoticed.

on:
push:
branches:
- main
paths:
- '**.md'
- '.github/workflows/links.yml'
- '.lycheeignore'
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**.md'
- '.github/workflows/links.yml'
- '.lycheeignore'
workflow_dispatch:

# Least-privilege default; this workflow only reads the repository.
permissions:
contents: read

jobs:
link-checker:
name: Check Links
runs-on: ubuntu-latest
# Typical run is well under a minute with the lychee cache; the timeout
# stops a slow external host from hanging the workflow.
timeout-minutes: 10
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-link-checker
cancel-in-progress: true
steps:
- uses: actions/checkout@v7

- name: Check links with lychee
uses: lycheeverse/lychee-action@v2
with:
# `docs/case-studies` and `dev/log` are research records that quote
# issues, runs and files belonging to other repositories; their links
# are evidence, not navigation, and must not gate this repository.
# `js/node_modules` only exists in local runs, but excluding it keeps
# the same command reproducible outside CI.
args: >-
--verbose
--no-progress
--cache
--max-cache-age 1d
--max-retries 3
--timeout 30
--exclude-path docs/case-studies
--exclude-path dev/log
--exclude-path js/node_modules
'./**/*.md'
fail: true
jobSummary: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading
Loading