diff --git a/.github/workflows/release-finish.yml b/.github/workflows/release-finish.yml
new file mode 100644
index 0000000..6dabf39
--- /dev/null
+++ b/.github/workflows/release-finish.yml
@@ -0,0 +1,66 @@
+# Reusable workflow — publishes a draft release or deletes it on failure.
+# Pair with release-start.yml: call this after your gate jobs succeed or fail.
+---
+name: Release - Finish
+
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: |
+ JSON array of runner(s) to use.
+ See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job.
+ type: string
+ default: '["ubuntu-latest"]'
+ required: false
+ tag:
+ description: "The tag of the draft release to publish or delete (output of the release-start.yml workflow)."
+ type: string
+ required: true
+ publish:
+ description: "Set to true to publish the draft, false to delete it."
+ type: boolean
+ default: true
+ required: false
+
+permissions: {}
+
+jobs:
+ publish:
+ if: ${{ !inputs.publish }}
+ name: Publish draft release
+ runs-on: ${{ fromJson(inputs.runs-on) }}
+ permissions:
+ contents: write
+ steps:
+ - id: local-workflow-actions
+ uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@f24ce3360a8abf9bf386a62ab13d0ae5de5f9d13 # 0.31.7
+ with:
+ actions-path: actions
+
+ - id: create-release
+ uses: ./self-workflow/actions/release/create
+ with:
+ publish: true
+ tag: ${{ inputs.tag }}
+
+ # jscpd:ignore-start
+ - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@f24ce3360a8abf9bf386a62ab13d0ae5de5f9d13 # 0.31.7
+ if: always() && steps.local-workflow-actions.outputs.repository
+ with:
+ actions-path: actions
+ repository: ${{ steps.local-workflow-actions.outputs.repository }}
+ ref: ${{ steps.local-workflow-actions.outputs.ref }}
+ # jscpd:ignore-end
+ cancel:
+ if: ${{ inputs.publish }}
+ name: Cancel release
+ runs-on: ${{ fromJson(inputs.runs-on) }}
+ permissions:
+ contents: write
+ steps:
+ - env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ shell: bash
+ run: gh release delete "${{ inputs.tag }}" --cleanup-tag --yes
diff --git a/.github/workflows/release-start.md b/.github/workflows/release-start.md
new file mode 100644
index 0000000..561f5f3
--- /dev/null
+++ b/.github/workflows/release-start.md
@@ -0,0 +1,110 @@
+
+
+# GitHub Workflow: Release - Start
+
+
+

+
+
+---
+
+
+
+
+[](https://github.com/hoverkraft-tech/ci-github-publish/releases)
+[](http://choosealicense.com/licenses/mit/)
+[](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-publish?style=social)
+[](https://github.com/hoverkraft-tech/ci-github-publish/blob/main/CONTRIBUTING.md)
+
+
+
+
+## Overview
+
+Reusable release workflow
+This workflow delegates release tasks by reusing a shared release workflow, ensuring standardized publishing across projects.
+
+### Permissions
+
+- **`contents`**: `write`
+- **`id-token`**: `write`
+- **`pull-requests`**: `read`
+
+
+
+
+## Usage
+
+```yaml
+name: Release
+on:
+ push:
+ branches:
+ - main
+permissions: {}
+jobs:
+ release:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@84e8ace407055e7a40ba6670a8c697e1ce2dfafa # 0.20.1
+ permissions: {}
+ with:
+ # JSON array of runner(s) to use.
+ # See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job.
+ #
+ # Default: `["ubuntu-latest"]`
+ runs-on: '["ubuntu-latest"]'
+
+ # Whether to mark the release as a prerelease
+ # See ../../actions/release/create/README.md for more information.
+ prerelease: false
+```
+
+
+
+
+## Inputs
+
+### Workflow Dispatch Inputs
+
+| **Input** | **Description** | **Required** | **Type** | **Default** |
+| ---------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- |
+| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` |
+| | See . | | | |
+| **`prerelease`** | Whether to mark the release as a prerelease | **false** | **boolean** | `false` |
+| | See ../../actions/release/create/README.md for more information. | | | |
+
+
+
+
+
+
+
+
+
+
+## Contributing
+
+Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-publish/blob/main/CONTRIBUTING.md) for more details.
+
+
+
+
+
+
+## License
+
+This project is licensed under the MIT License.
+
+SPDX-License-Identifier: MIT
+
+Copyright © 2026 hoverkraft-tech
+
+For more details, see the [license](http://choosealicense.com/licenses/mit/).
+
+
+
+
+---
+
+This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor).
+
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release-start.yml
similarity index 63%
rename from .github/workflows/release.yml
rename to .github/workflows/release-start.yml
index 06903e6..a4e2abd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release-start.yml
@@ -1,9 +1,29 @@
-# Reusable release workflow
-# This workflow delegates release tasks by reusing a shared release workflow, ensuring standardized publishing across projects.
+# Reusable release workflow — creates a draft release and outputs the tag.
+# Pair with release-finish.yml to publish or delete the draft after running your own gate.
---
-name: Release
+name: Release - Start
on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: |
+ JSON array of runner(s) to use.
+ See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job.
+ type: string
+ default: '["ubuntu-latest"]'
+ required: false
+ prerelease:
+ description: |
+ Whether to mark the release as a prerelease.
+ See ../../actions/release/create/README.md for more information.
+ type: boolean
+ default: false
+ required: false
+ outputs:
+ tag:
+ description: "The tag of the draft release."
+ value: ${{ jobs.release.outputs.tag }}
workflow_dispatch:
inputs:
#checkov:skip=CKV_GHA_7: required
@@ -16,7 +36,7 @@ on:
required: false
prerelease:
description: |
- Whether to mark the release as a prerelease
+ Whether to mark the release as a prerelease.
See ../../actions/release/create/README.md for more information.
type: boolean
default: false
@@ -44,6 +64,7 @@ jobs:
uses: ./self-workflow/actions/release/create
with:
prerelease: ${{ inputs.prerelease }}
+ publish: false
# jscpd:ignore-start
- uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@f24ce3360a8abf9bf386a62ab13d0ae5de5f9d13 # 0.31.7
diff --git a/.github/workflows/release.md b/.github/workflows/release.md
index 0852868..111f5c4 100644
--- a/.github/workflows/release.md
+++ b/.github/workflows/release.md
@@ -1,110 +1,184 @@
-
+# Release — combining `release-start` and `release-finish`
-# GitHub Workflow: Release
+The release process is split into two composable reusable workflows:
-
-

-
+| Workflow | Purpose |
+| ----------------------------------------- | --------------------------------------------------------------------------------------- |
+| [`release-start.yml`](release-start.md) | Creates a **draft** release via Release Drafter and outputs the `tag`. |
+| [`release-finish.yml`](release-finish.md) | **Publishes** the draft (or **deletes** it). Controlled by the `publish` boolean input. |
----
-
-
-
-
-[](https://github.com/hoverkraft-tech/ci-github-publish/releases)
-[](http://choosealicense.com/licenses/mit/)
-[](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-publish?style=social)
-[](https://github.com/hoverkraft-tech/ci-github-publish/blob/main/CONTRIBUTING.md)
-
-
-
+Between them, you own the **gate**: any job(s) you want to run before deciding whether to publish. GitHub Actions `needs` wires it all together — no polling, no scripting.
-## Overview
-
-Reusable release workflow
-This workflow delegates release tasks by reusing a shared release workflow, ensuring standardized publishing across projects.
-
-### Permissions
+```txt
+release-start → [your gate job(s)] → release-finish
+ ↓ ↑
+ outputs tag publish: gate succeeded?
+```
-- **`contents`**: `write`
-- **`id-token`**: `write`
-- **`pull-requests`**: `read`
+---
-
-
+## Use case 1 — no gate (publish immediately)
-## Usage
+The simplest setup: draft is published right away.
```yaml
-name: Release
-on:
- push:
- branches:
- - main
-permissions: {}
jobs:
- release:
- uses: hoverkraft-tech/ci-github-publish/.github/workflows/release.yml@84e8ace407055e7a40ba6670a8c697e1ce2dfafa # 0.20.1
- permissions: {}
+ start:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@main
+ permissions:
+ contents: write
+ pull-requests: read
+ id-token: write
+
+ finish:
+ needs: start
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-finish.yml@main
+ permissions:
+ contents: write
with:
- # JSON array of runner(s) to use.
- # See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job.
- #
- # Default: `["ubuntu-latest"]`
- runs-on: '["ubuntu-latest"]'
-
- # Whether to mark the release as a prerelease
- # See ../../actions/release/create/README.md for more information.
- prerelease: false
+ tag: ${{ needs.start.outputs.tag }}
+ publish: true
```
-
-
-
-## Inputs
-
-### Workflow Dispatch Inputs
+---
-| **Input** | **Description** | **Required** | **Type** | **Default** |
-| ---------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- |
-| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` |
-| | See . | | | |
-| **`prerelease`** | Whether to mark the release as a prerelease | **false** | **boolean** | `false` |
-| | See ../../actions/release/create/README.md for more information. | | | |
+## Use case 2 — gate: automated test suite
-
-
-
-
-
-
-
-
+Run your test suite before committing to publish. If tests fail, the draft is deleted.
-## Contributing
+```yaml
+jobs:
+ start:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@main
+ permissions:
+ contents: write
+ pull-requests: read
+ id-token: write
+
+ tests:
+ needs: start
+ uses: ./.github/workflows/tests.yml # your own workflow
+
+ finish:
+ needs: [start, tests]
+ if: always() && needs.start.result == 'success'
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-finish.yml@main
+ permissions:
+ contents: write
+ with:
+ tag: ${{ needs.start.outputs.tag }}
+ publish: ${{ needs.tests.result == 'success' }}
+```
-Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-publish/blob/main/CONTRIBUTING.md) for more details.
+---
-
-
-
-
+## Use case 3 — gate: deploy to staging and run smoke tests
-## License
+Draft a release, deploy it to staging with the candidate tag, run smoke tests, then decide.
-This project is licensed under the MIT License.
+```yaml
+jobs:
+ start:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@main
+ permissions:
+ contents: write
+ pull-requests: read
+ id-token: write
+
+ deploy-staging:
+ needs: start
+ uses: ./.github/workflows/deploy.yml
+ with:
+ environment: staging
+ tag: ${{ needs.start.outputs.tag }}
+
+ smoke-tests:
+ needs: deploy-staging
+ uses: ./.github/workflows/smoke-tests.yml
+
+ finish:
+ needs: [start, smoke-tests]
+ if: always() && needs.start.result == 'success'
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-finish.yml@main
+ permissions:
+ contents: write
+ with:
+ tag: ${{ needs.start.outputs.tag }}
+ publish: ${{ needs.smoke-tests.result == 'success' }}
+```
-SPDX-License-Identifier: MIT
+---
-Copyright © 2026 hoverkraft-tech
+## Use case 4 — gate: manual approval via environment protection
-For more details, see the [license](http://choosealicense.com/licenses/mit/).
+Use a GitHub [environment with required reviewers](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#required-reviewers) to gate the publish step.
-
-
+```yaml
+jobs:
+ start:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@main
+ permissions:
+ contents: write
+ pull-requests: read
+ id-token: write
+
+ approve:
+ needs: start
+ runs-on: ubuntu-latest
+ environment: production # ← reviewers must approve before this job runs
+ steps:
+ - run: echo "Approved for ${{ needs.start.outputs.tag }}"
+
+ finish:
+ needs: [start, approve]
+ if: always() && needs.start.result == 'success'
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-finish.yml@main
+ permissions:
+ contents: write
+ with:
+ tag: ${{ needs.start.outputs.tag }}
+ publish: ${{ needs.approve.result == 'success' }}
+```
---
-This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor).
+## Use case 5 — gate: multiple parallel gates
-
+Run several independent checks in parallel; publish only if all pass.
+
+```yaml
+jobs:
+ start:
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-start.yml@main
+ permissions:
+ contents: write
+ pull-requests: read
+ id-token: write
+
+ unit-tests:
+ needs: start
+ uses: ./.github/workflows/unit-tests.yml
+
+ integration-tests:
+ needs: start
+ uses: ./.github/workflows/integration-tests.yml
+
+ security-scan:
+ needs: start
+ uses: ./.github/workflows/security-scan.yml
+
+ finish:
+ needs: [start, unit-tests, integration-tests, security-scan]
+ if: always() && needs.start.result == 'success'
+ uses: hoverkraft-tech/ci-github-publish/.github/workflows/release-finish.yml@main
+ permissions:
+ contents: write
+ with:
+ tag: ${{ needs.start.outputs.tag }}
+ publish: >-
+ ${{
+ needs.unit-tests.result == 'success' &&
+ needs.integration-tests.result == 'success' &&
+ needs.security-scan.result == 'success'
+ }}
+```
diff --git a/README.md b/README.md
index ba9d181..6ff38dd 100644
--- a/README.md
+++ b/README.md
@@ -109,7 +109,11 @@ _Reusable workflows for managing release process._
#### - [Prepare release](.github/workflows/prepare-release.md)
-#### - [Release](.github/workflows/release.md)
+#### - [Release](.github/workflows/release.md) - combining start + finish
+
+#### - [Release - Start](.github/workflows/release-start.md)
+
+#### - [Release - Finish](.github/workflows/release-finish.md)
#### - [Release actions](.github/workflows/release-actions.md)