diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 4bd171ed..56dc29c6 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -10,11 +10,33 @@ concurrency: cancel-in-progress: false jobs: + guard: + if: ${{ github.ref == 'refs/heads/dev' }} + runs-on: [self-hosted, linux, x64] + outputs: + should_release: ${{ steps.release-guard.outputs.should_release }} + + steps: + - name: Check release recursion guard + id: release-guard + shell: bash + run: | + SHOULD_RELEASE=true + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + HEAD_SUBJECT=$(jq -r '.head_commit.message // "" | split("\n")[0]' "$GITHUB_EVENT_PATH") + case "$HEAD_SUBJECT" in + "chore(release): "*) + SHOULD_RELEASE=false + echo "Skipping generated dev release commit: $HEAD_SUBJECT" + ;; + esac + fi + echo "should_release=$SHOULD_RELEASE" >> "$GITHUB_OUTPUT" + release: - # Release commits must still report PR CI when dev is promoted to main. - # Skip recursive dev releases by subject instead of GitHub's global [skip ci]. - if: ${{ github.ref == 'refs/heads/dev' && (github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release): ')) }} - runs-on: ubuntu-latest + needs: guard + if: ${{ needs.guard.outputs.should_release == 'true' }} + runs-on: [self-hosted, linux, x64] permissions: contents: write diff --git a/tests/unit/scripts/github/dev-release-workflow.test.ts b/tests/unit/scripts/github/dev-release-workflow.test.ts index 86dbb948..e0f4b2cc 100644 --- a/tests/unit/scripts/github/dev-release-workflow.test.ts +++ b/tests/unit/scripts/github/dev-release-workflow.test.ts @@ -24,6 +24,8 @@ describe('dev release workflow', () => { expect(workflow).toContain('name: Dev Release'); expect(workflow).toContain('branches: [dev]'); + expect(workflow.match(/runs-on: \[self-hosted, linux, x64\]/g)).toHaveLength(2); + expect(workflow).not.toContain('runs-on: ubuntu-latest'); expect(checkoutSection).toContain("token: ${{ secrets.PAT_TOKEN }}"); expect(checkoutSection).not.toContain('token: ${{ github.token }}'); expect(releaseSection).toContain('GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}'); @@ -39,7 +41,11 @@ describe('dev release workflow', () => { const workflow = fs.readFileSync(workflowPath, 'utf8'); const script = fs.readFileSync(scriptPath, 'utf8'); - expect(workflow).toContain("!startsWith(github.event.head_commit.message, 'chore(release): ')"); + expect(workflow).toContain('should_release: ${{ steps.release-guard.outputs.should_release }}'); + expect(workflow).toContain('"chore(release): "*)'); + expect(workflow).toContain("needs.guard.outputs.should_release == 'true'"); + expect(workflow).not.toContain('github.event.head_commit.message'); + expect(workflow).not.toContain('!startsWith('); expect(workflow).not.toContain("contains(github.event.head_commit.message, '[skip ci]')"); expect(script).toContain('LEGACY_CURRENT_RELEASE_SUBJECT="${CURRENT_RELEASE_SUBJECT} [skip ci]"'); expect(script).toContain('[[ "$HEAD_SUBJECT" == "$CURRENT_RELEASE_SUBJECT" ]]');