From b53b4ac531bd63ef8b215e478fa53d30c212b825 Mon Sep 17 00:00:00 2001 From: Charles Mulder Date: Tue, 21 Jul 2026 16:15:50 +0100 Subject: [PATCH 1/3] ci(release): add rebase step before versioning to prevent push race when jobs queue --- .github/actions/release/action.yaml | 31 +++++++++++++++++++++++++++-- .github/workflows/ci.yml | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index ed98d15df..2e15fac5d 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -32,11 +32,22 @@ runs: git config commit.gpgsign true git config tag.gpgsign true git config user.signingkey $(gpg --list-secret-keys --with-colons "${{ inputs.gh_email }}" | awk -F: '/^sec:/ {print $5; exit}') + - name: Fast-forward to latest main + shell: bash + # Checkout is pinned to github.sha. If a prior release pushed commits/tags while this job + # was queued, those tags won't be reachable from our stale HEAD. Both verify-versions.sh and + # nx release use --merged HEAD for tag resolution, so HEAD must be current. This rebase is + # always a fast-forward (no local commits yet). + run: | + git fetch origin main + git rebase origin/main - name: Verify version alignment shell: bash run: ./.github/scripts/verify-versions.sh - name: Release shell: bash + # Skip publish to avoid orphaned npm versions if push fails. + # Publish happens after push succeeds (see "Publish to npm" step below). env: GH_TOKEN: ${{ inputs.gh_token }} GITHUB_TOKEN: ${{ inputs.gh_token }} @@ -47,7 +58,7 @@ runs: # https://docs.npmjs.com/generating-provenance-statements#using-third-party-package-publishing-tools # https://philna.sh/blog/2026/01/28/trusted-publishing-npm/ NPM_CONFIG_PROVENANCE: true - run: npx nx release --yes + run: npx nx release --skip-publish - name: Push release commit and tags shell: bash # nx release (unified command) creates the git commit and tags locally but does not push them, @@ -55,8 +66,24 @@ runs: # createRelease (GitHub/GitLab releases) is enabled. Since we use createReleaseInGitHub: false, # we push manually here so that the version bumps in package.json reach the remote and # nacho-bot can create PRs in consuming repos. + # Rebase before push: if a PR merged to main while nx release was running, the version + # commit needs to be on top of latest main to avoid non-fast-forward rejection. This rebase + # replays only the single version commit created by nx release (merge commit that triggered + # this workflow is always an ancestor of origin/main). # HUSKY=0 disables pre-push hooks in CI — tests are run separately by the CI pipeline. env: HUSKY: '0' - run: git push --follow-tags + run: | + git fetch origin main + git rebase origin/main + git push --follow-tags + - name: Publish to npm + shell: bash + # Publish only after push succeeds to prevent orphaned npm versions. + # If push failed, re-running this job is safe — no npm versions published yet. + # https://docs.npmjs.com/generating-provenance-statements#using-third-party-package-publishing-tools + # https://philna.sh/blog/2026/01/28/trusted-publishing-npm/ + env: + NPM_CONFIG_PROVENANCE: true + run: npx nx release publish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7fba3122..09c9783be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,7 @@ jobs: # Only run on push to main branch (never on PRs, workflow_dispatch, etc.) if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: npm-publish + # Serialize release jobs to prevent concurrent git pushes to master. concurrency: group: ${{ github.workflow }}-release cancel-in-progress: false From e10ab3a74dd11c5172beaa00e2f79d589f65b941 Mon Sep 17 00:00:00 2001 From: Charles Mulder Date: Wed, 22 Jul 2026 11:55:52 +0100 Subject: [PATCH 2/3] ci(verify-versions): distinguish registry and version errors --- .github/actions/release/action.yaml | 1 - .github/scripts/verify-versions.sh | 29 +++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml index 2e15fac5d..ba847fe6b 100644 --- a/.github/actions/release/action.yaml +++ b/.github/actions/release/action.yaml @@ -47,7 +47,6 @@ runs: - name: Release shell: bash # Skip publish to avoid orphaned npm versions if push fails. - # Publish happens after push succeeds (see "Publish to npm" step below). env: GH_TOKEN: ${{ inputs.gh_token }} GITHUB_TOKEN: ${{ inputs.gh_token }} diff --git a/.github/scripts/verify-versions.sh b/.github/scripts/verify-versions.sh index a03294ec8..45c0fa8a4 100755 --- a/.github/scripts/verify-versions.sh +++ b/.github/scripts/verify-versions.sh @@ -13,6 +13,9 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" mismatch=0 +registry_error=0 +npm_stderr_file="$(mktemp)" +trap 'rm -f "$npm_stderr_file"' EXIT # clean up temp file on exit # Column widths for alignment printf "%-60s %-12s %-12s %-15s %s\n" "PACKAGE" "DISK" "GIT TAG" "NPM" "STATUS" printf '%.0s-' {1..110} @@ -54,13 +57,25 @@ for dir in "$REPO_ROOT"/packages/*/; do [[ -z "$tag_ver" ]] && tag_ver="NONE" # --- npm registry version --- - npm_ver="$(npm view "$pkg" version --fetch-timeout=10000 2>/dev/null)" || true - [[ -z "$npm_ver" ]] && npm_ver="NOT_ON_NPM" + npm_ver="$(npm view "$pkg" version --fetch-timeout=10000 2>"$npm_stderr_file")" && npm_rc=0 || npm_rc=$? + if [[ $npm_rc -ne 0 ]]; then + if grep -q 'E404' "$npm_stderr_file"; then + npm_ver="NOT_ON_NPM" + else + npm_ver="REGISTRY_ERROR" + fi + fi # --- Compare --- if [[ "$disk_ver" == "$tag_ver" && "$disk_ver" == "$npm_ver" ]]; then status="OK" marker="✓" + elif [[ "$npm_ver" == "REGISTRY_ERROR" ]]; then + status="REGISTRY_ERROR" + marker="✗" + mismatch=1 + registry_error=1 + echo "::error::${pkg}: npm registry lookup failed (timeout, auth, or outage). Cannot verify npm version." elif [[ "$tag_ver" == "NONE" && "$npm_ver" == "NOT_ON_NPM" ]]; then # New package: no tag, not published. Flag as informational but still a mismatch. status="NEW_PACKAGE" @@ -80,6 +95,16 @@ done echo "" if [[ $mismatch -ne 0 ]]; then echo "Version verification FAILED. Fix mismatches before releasing." + if [[ $registry_error -ne 0 ]]; then + echo "" + echo "npm registry lookup failed for one or more packages. Check registry status," + echo "network connectivity, and npm authentication before re-running." + else + echo "" + echo "If disk and tag match but npm is behind, a prior npm publish likely failed." + echo "Pull latest main with tags, build, and run 'npx nx release publish' to recover." + echo "Subsequent releases will remain blocked until the missing version is published." + fi exit 1 else echo "All package versions are aligned across disk, git tags, and npm registry." From ff7338db60d2325ce435b62c3eb2185f99be3efd Mon Sep 17 00:00:00 2001 From: Charles Mulder Date: Thu, 23 Jul 2026 12:24:32 +0100 Subject: [PATCH 3/3] ci: commitlint verify PR title --- .github/workflows/pr-title.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/pr-title.yml diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..fc00cbf5a --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,27 @@ +name: PR title lint +permissions: + contents: read +on: + pull_request: + branches: + - main + types: [opened, edited, reopened] + +jobs: + pr-title-lint: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + fetch-depth: 0 + - uses: './.github/actions/setup-environment' + - name: Install + shell: bash + run: npm ci + - name: Validate PR title with commitlint + shell: bash + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: echo "$PR_TITLE" | npx commitlint --verbose