From 716eb73e3f0929e504df94a650850e9b06f15a08 Mon Sep 17 00:00:00 2001 From: huyplb Date: Mon, 7 Sep 2026 12:49:22 -0600 Subject: [PATCH 1/2] ci: let release tags actually trigger a release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tagging a release has been a no-op since v0.2.73. v0.2.111 and v0.2.220 were both tagged and pushed, and neither fired Release Gate, npm Publish or Web Release — zero workflow runs, no error anywhere to notice. The cause is that a release tag lands on the version-bump commit, whose message ends in `[skip ci]`. GitHub honours that marker for *every* push event touching the commit, and a tag push is a push event, so the marker suppressed the release along with the branch build it was meant to skip. v0.2.73 released fine because its tag sat on an ordinary fix commit. `[skip ci]` was not what prevented the bump loop — the `if:` on the bump job already refuses to bump a "chore: bump version" commit. The marker only saved CI on a commit that changes four version fields and nothing else. So the saving moves to where it cannot take releases down with it: each push-triggered workflow skips bump commits by message. The guard is written `github.event_name != 'push' || …` so pull_request runs are never affected, and it is only on the five `branches: [main]` workflows — the three tag workflows are left unguarded, which is the entire point. Verified by construction here, and half of it by this PR's own checks: they run on pull_request, so a green run proves the guard does not skip PR builds. The other half — a tag firing a release — is only provable by the next tag, since nothing in CI can push one. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-gate.yml | 36 +++++++++++++++++++++++ .github/workflows/codeql.yml | 9 ++++++ .github/workflows/dependency-security.yml | 27 +++++++++++++++++ .github/workflows/deps-backdoor-scan.yml | 9 ++++++ .github/workflows/secret-scan.yml | 9 ++++++ .github/workflows/version-bump.yml | 13 +++++++- 6 files changed, 102 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-gate.yml b/.github/workflows/build-gate.yml index 8435c8ca..80db3051 100644 --- a/.github/workflows/build-gate.yml +++ b/.github/workflows/build-gate.yml @@ -23,6 +23,15 @@ concurrency: jobs: typecheck: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Typecheck runs-on: ubuntu-latest steps: @@ -44,6 +53,15 @@ jobs: - run: npm run typecheck unit-tests: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Unit tests runs-on: ubuntu-latest steps: @@ -63,6 +81,15 @@ jobs: - run: npx vitest run frontend-build: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Frontend build runs-on: ubuntu-latest steps: @@ -86,6 +113,15 @@ jobs: - run: npm run build -w @foxschema/web lint: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: ESLint runs-on: ubuntu-latest steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 12f5a741..308b3edd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,6 +19,15 @@ permissions: jobs: analyze: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: CodeQL — JavaScript / TypeScript runs-on: ubuntu-latest diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index 13e0086e..9ab2e7b0 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -14,6 +14,15 @@ permissions: jobs: # ── npm audit ────────────────────────────────────────────────────────────── npm-audit: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: npm audit runs-on: ubuntu-latest steps: @@ -68,6 +77,15 @@ jobs: # ── ESLint security rules ────────────────────────────────────────────────── eslint-security: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: ESLint security runs-on: ubuntu-latest steps: @@ -99,6 +117,15 @@ jobs: # ── node_modules backdoor / unexpected port scan ─────────────────────────── # Dedicated deep scan also lives in deps-backdoor-scan.yml (scheduled weekly). deps-backdoor-scan: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Deps backdoor / port scan runs-on: ubuntu-latest steps: diff --git a/.github/workflows/deps-backdoor-scan.yml b/.github/workflows/deps-backdoor-scan.yml index c050a645..42eb8214 100644 --- a/.github/workflows/deps-backdoor-scan.yml +++ b/.github/workflows/deps-backdoor-scan.yml @@ -22,6 +22,15 @@ permissions: jobs: scan-node-modules: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Scan node_modules (ports / backdoors) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index f256199f..96a49afc 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -18,6 +18,15 @@ permissions: jobs: gitleaks: + # A version-bump commit changes four package.json version fields and + # nothing else, so there is nothing here for it to check. It used to carry + # `[skip ci]` for this, but that marker also suppresses the *tag* push that + # rides on the same commit, which silently disabled every release — see + # version-bump.yml. Skipping by message keeps the saving without that cost. + # Guarded on event_name so pull_request runs are never skipped. + if: >- + github.event_name != 'push' + || !startsWith(github.event.head_commit.message, 'chore: bump version') name: Gitleaks runs-on: ubuntu-latest steps: diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index a6d03551..dfd39d04 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -83,5 +83,16 @@ jobs: packages/db/package.json \ apps/web/package.json \ apps/cli/package.json - git commit -m "chore: bump version to $V [skip ci]" + # No [skip ci]. GitHub honours that marker on *every* push event for + # the commit, tag pushes included — and the release tag lands on + # exactly this commit, so the marker silently disabled Release Gate, + # npm Publish and Web Release. v0.2.111 and v0.2.220 were both tagged + # and neither fired anything; the last tag that ran a release was + # v0.2.73, whose commit was an ordinary fix. + # + # The loop this marker looked like it was preventing is already + # prevented by the `if:` on this job, which refuses to bump a + # "chore: bump version" commit. The CI it saved is now saved by a + # matching guard on each push-triggered workflow. + git commit -m "chore: bump version to $V" git push From 7052c4cc6c21d0919065b732054ce5efd87849c9 Mon Sep 17 00:00:00 2001 From: huyplb Date: Mon, 7 Sep 2026 17:08:42 -0600 Subject: [PATCH 2/2] ci: quote the guard expression so the workflow parses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first form used a folded scalar and GitHub created no runs at all for the branch — not skipped jobs, no runs — while an unrelated PR opened minutes later ran all four workflows normally. The expression matches on 'chore: bump version', and that colon-space is a mapping indicator to YAML. Written bare inside ${{ }} it makes the parser read the line as a nested mapping: mapping values are not allowed here ... vent.head_commit.message, 'chore: bump version') }} Double-quoting the whole value settles it, and single quotes inside stay readable. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-gate.yml | 16 ++++------------ .github/workflows/codeql.yml | 4 +--- .github/workflows/dependency-security.yml | 12 +++--------- .github/workflows/deps-backdoor-scan.yml | 4 +--- .github/workflows/secret-scan.yml | 4 +--- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-gate.yml b/.github/workflows/build-gate.yml index 80db3051..4f83bf5f 100644 --- a/.github/workflows/build-gate.yml +++ b/.github/workflows/build-gate.yml @@ -29,9 +29,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Typecheck runs-on: ubuntu-latest steps: @@ -59,9 +57,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Unit tests runs-on: ubuntu-latest steps: @@ -87,9 +83,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Frontend build runs-on: ubuntu-latest steps: @@ -119,9 +113,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: ESLint runs-on: ubuntu-latest steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 308b3edd..07d92ca3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,9 +25,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: CodeQL — JavaScript / TypeScript runs-on: ubuntu-latest diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index 9ab2e7b0..a3ad6794 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -20,9 +20,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: npm audit runs-on: ubuntu-latest steps: @@ -83,9 +81,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: ESLint security runs-on: ubuntu-latest steps: @@ -123,9 +119,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Deps backdoor / port scan runs-on: ubuntu-latest steps: diff --git a/.github/workflows/deps-backdoor-scan.yml b/.github/workflows/deps-backdoor-scan.yml index 42eb8214..5e136662 100644 --- a/.github/workflows/deps-backdoor-scan.yml +++ b/.github/workflows/deps-backdoor-scan.yml @@ -28,9 +28,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Scan node_modules (ports / backdoors) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 96a49afc..5d84ff35 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -24,9 +24,7 @@ jobs: # rides on the same commit, which silently disabled every release — see # version-bump.yml. Skipping by message keeps the saving without that cost. # Guarded on event_name so pull_request runs are never skipped. - if: >- - github.event_name != 'push' - || !startsWith(github.event.head_commit.message, 'chore: bump version') + if: "${{ github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: bump version') }}" name: Gitleaks runs-on: ubuntu-latest steps: