diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index 11b997af..7fd06151 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -41,10 +41,6 @@ on:
description: 'Require scope in PR title'
type: boolean
default: false
- min_description_length:
- description: 'Minimum PR description content length (after stripping template boilerplate)'
- type: number
- default: 30
enable_auto_labeler:
description: 'Enable automatic labeling based on changed files'
type: boolean
@@ -93,7 +89,7 @@ jobs:
id: source-branch
if: inputs.enforce_source_branches
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-source-branch@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-source-branch@v1.20.1
with:
github-token: ${{ secrets.MANAGE_TOKEN || github.token }}
allowed-branches: ${{ inputs.allowed_source_branches }}
@@ -103,7 +99,7 @@ jobs:
- name: Validate PR title
id: title
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-title@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-title@v1.20.1
with:
github-token: ${{ github.token }}
types: ${{ inputs.pr_title_types }}
@@ -113,13 +109,11 @@ jobs:
- name: Validate PR description
id: description
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-description@v1.20.0
- with:
- min-length: ${{ inputs.min_description_length }}
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-description@develop
- name: Collect results and enforce blocking
id: collect
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-blocking-collect@fix/pin-refs-v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-blocking-collect@v1.20.1
with:
source-branch-outcome: ${{ steps.source-branch.outcome || 'skipped' }}
title-outcome: ${{ steps.title.outcome }}
@@ -145,7 +139,7 @@ jobs:
- name: Check PR metadata
id: metadata
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-metadata@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-metadata@v1.20.1
with:
github-token: ${{ secrets.MANAGE_TOKEN || github.token }}
dry-run: ${{ inputs.dry_run && 'true' || 'false' }}
@@ -153,7 +147,7 @@ jobs:
- name: Check PR size
id: size
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-size@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-size@v1.20.1
with:
github-token: ${{ secrets.MANAGE_TOKEN || github.token }}
base-ref: ${{ github.base_ref }}
@@ -163,7 +157,7 @@ jobs:
id: labels
if: inputs.enable_auto_labeler && !inputs.dry_run
continue-on-error: true
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-labels@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-labels@v1.20.1
with:
github-token: ${{ secrets.MANAGE_TOKEN || github.token }}
config-path: ${{ inputs.labeler_config_path }}
@@ -186,7 +180,7 @@ jobs:
steps:
- name: PR Checks Summary
- uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-checks-summary@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-checks-summary@v1.20.1
with:
source-branch-result: ${{ needs.blocking-checks.outputs.source-branch-result || 'skipped' }}
title-result: ${{ needs.blocking-checks.outputs.title-result || 'skipped' }}
@@ -201,7 +195,7 @@ jobs:
name: Notify
needs: [blocking-checks, advisory-checks, pr-checks-summary]
if: always() && github.event.pull_request.draft != true && !inputs.dry_run
- uses: LerianStudio/github-actions-shared-workflows/.github/workflows/slack-notify.yml@v1.20.0
+ uses: LerianStudio/github-actions-shared-workflows/.github/workflows/slack-notify.yml@v1.20.1
with:
status: ${{ (needs.blocking-checks.outputs.source-branch-result == 'failure' || needs.blocking-checks.outputs.title-result == 'failure' || needs.blocking-checks.outputs.description-result == 'failure') && 'failure' || 'success' }}
workflow_name: "PR Validation"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d25bf101..ac959c33 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -150,6 +150,7 @@ jobs:
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v6
id: semantic
+ continue-on-error: true
with:
ci: false
semantic_version: ${{ inputs.semantic_version }}
@@ -164,6 +165,21 @@ jobs:
GIT_COMMITTER_NAME: ${{ secrets.LERIAN_CI_CD_USER_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }}
+ # ----------------- Backmerge Fallback -----------------
+ - name: Backmerge PR fallback
+ if: steps.semantic.outcome == 'failure' && steps.semantic.outputs.new_release_published == 'true'
+ uses: LerianStudio/github-actions-shared-workflows/src/config/backmerge-pr@develop
+ with:
+ github-token: ${{ steps.app-token.outputs.token }}
+ source-branch: ${{ github.ref_name }}
+ version: ${{ steps.semantic.outputs.new_release_version }}
+
+ - name: Fail if release itself failed
+ if: steps.semantic.outcome == 'failure' && steps.semantic.outputs.new_release_published != 'true'
+ run: |
+ echo "::error::Semantic release failed before publishing a new version"
+ exit 1
+
# Slack notification
notify:
name: Notify
diff --git a/docs/release-workflow.md b/docs/release-workflow.md
index c7e3fc58..cf39f0c4 100644
--- a/docs/release-workflow.md
+++ b/docs/release-workflow.md
@@ -8,7 +8,7 @@ Reusable workflow for semantic versioning and automated release management. Crea
- **GPG signing**: Signed commits and tags for security
- **GitHub App authentication**: Higher rate limits and better security
- **Hotfix support**: Separate configuration for hotfix branches
-- **Backmerge support**: Automatic backmerging of releases
+- **Backmerge support**: Automatic backmerging of releases (falls back to creating a PR if the direct push fails due to branch divergence)
- **Conventional commits**: Enforces commit message standards
## Usage
diff --git a/src/config/backmerge-pr/README.md b/src/config/backmerge-pr/README.md
new file mode 100644
index 00000000..9b9db596
--- /dev/null
+++ b/src/config/backmerge-pr/README.md
@@ -0,0 +1,64 @@
+
+
+  |
+ backmerge-pr |
+
+
+
+Creates a PR to backmerge a source branch into a target branch when a direct push fails. Checks for existing open PRs to avoid duplicates.
+
+Typically used as a fallback in the release workflow when the `@saithodev/semantic-release-backmerge` plugin fails to push directly (non-fast-forward).
+
+## Inputs
+
+| Input | Description | Required | Default |
+|-------|-------------|----------|---------|
+| `github-token` | GitHub token with pull-requests write permission | Yes | |
+| `source-branch` | Source branch to merge from (e.g., main) | Yes | |
+| `target-branch` | Target branch to merge into | No | `develop` |
+| `version` | Release version for the PR title | Yes | |
+
+## Outputs
+
+| Output | Description |
+|--------|-------------|
+| `pr-url` | URL of the created or existing PR |
+| `pr-number` | Number of the created or existing PR |
+
+## Usage as composite step
+
+```yaml
+- name: Create backmerge PR
+ uses: LerianStudio/github-actions-shared-workflows/src/config/backmerge-pr@v1.x.x
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ source-branch: main
+ target-branch: develop
+ version: ${{ steps.semantic.outputs.new_release_version }}
+```
+
+## Usage in release workflow (fallback pattern)
+
+```yaml
+- name: Semantic Release
+ uses: cycjimmy/semantic-release-action@v6
+ id: semantic
+ continue-on-error: true
+ ...
+
+- name: Backmerge PR fallback
+ if: steps.semantic.outcome == 'failure' && steps.semantic.outputs.new_release_published == 'true'
+ uses: LerianStudio/github-actions-shared-workflows/src/config/backmerge-pr@v1.x.x
+ with:
+ github-token: ${{ steps.app-token.outputs.token }}
+ source-branch: ${{ github.ref_name }}
+ version: ${{ steps.semantic.outputs.new_release_version }}
+```
+
+## Required permissions
+
+```yaml
+permissions:
+ contents: read
+ pull-requests: write
+```
diff --git a/src/config/backmerge-pr/action.yml b/src/config/backmerge-pr/action.yml
new file mode 100644
index 00000000..fe9adc25
--- /dev/null
+++ b/src/config/backmerge-pr/action.yml
@@ -0,0 +1,67 @@
+name: Backmerge PR
+description: "Creates a PR to backmerge a source branch into a target branch when a direct push fails."
+
+inputs:
+ github-token:
+ description: GitHub token with pull-requests write permission
+ required: true
+ source-branch:
+ description: Source branch to merge from (e.g., main)
+ required: true
+ target-branch:
+ description: Target branch to merge into (e.g., develop)
+ required: false
+ default: develop
+ version:
+ description: Release version for the PR title (e.g., 1.20.1)
+ required: true
+
+outputs:
+ pr-url:
+ description: URL of the created PR (empty if PR already existed or was not needed)
+ value: ${{ steps.create-pr.outputs.pr_url }}
+ pr-number:
+ description: Number of the created or existing PR
+ value: ${{ steps.create-pr.outputs.pr_number }}
+
+runs:
+ using: composite
+ steps:
+ - name: Create backmerge PR
+ id: create-pr
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.github-token }}
+ SOURCE_BRANCH: ${{ inputs.source-branch }}
+ TARGET_BRANCH: ${{ inputs.target-branch }}
+ VERSION: ${{ inputs.version }}
+ run: |
+ # Check if a backmerge PR already exists
+ EXISTING_PR=$(gh pr list --base "${TARGET_BRANCH}" --head "${SOURCE_BRANCH}" --state open --json number,url --jq '.[0]')
+ if [ -n "$EXISTING_PR" ]; then
+ PR_NUM=$(echo "$EXISTING_PR" | jq -r '.number')
+ PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
+ echo "::notice::Backmerge PR #${PR_NUM} already exists: ${PR_URL}"
+ echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT"
+ echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ PR_BODY="## Description
+
+ Automated backmerge of release \`${VERSION}\` from \`${SOURCE_BRANCH}\` to \`${TARGET_BRANCH}\`.
+
+ The automatic backmerge push failed because \`${TARGET_BRANCH}\` has diverged from \`${SOURCE_BRANCH}\`. This PR needs a manual merge to resolve any conflicts.
+
+ > **Note:** This PR was created automatically by the release workflow."
+
+ PR_URL=$(gh pr create \
+ --base "${TARGET_BRANCH}" \
+ --head "${SOURCE_BRANCH}" \
+ --title "chore(release): backmerge ${VERSION}" \
+ --body "${PR_BODY}")
+
+ PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$')
+ echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT"
+ echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
+ echo "::notice::Backmerge push failed — created PR #${PR_NUM}: ${PR_URL}"
diff --git a/src/validate/pr-description/README.md b/src/validate/pr-description/README.md
index b8e57fd1..8c7c6047 100644
--- a/src/validate/pr-description/README.md
+++ b/src/validate/pr-description/README.md
@@ -5,16 +5,14 @@
-Validates that the PR description has real content beyond template boilerplate:
+Validates that the PR template checkboxes are properly filled:
-- **Description section**: extracts content under `## Description`, strips HTML comments, and checks minimum length
-- **Type of Change**: verifies at least one checkbox is checked (`- [x]`)
+- **Type of Change**: at least one checkbox must be checked (`- [x]`)
+- **Testing**: at least one checkbox must be checked (`- [x]`)
## Inputs
-| Input | Description | Required | Default |
-|-------|-------------|----------|---------|
-| `min-length` | Minimum content length in characters (after stripping template boilerplate) | No | `30` |
+None.
## Usage as composite step
@@ -25,8 +23,6 @@ jobs:
steps:
- name: Validate PR Description
uses: LerianStudio/github-actions-shared-workflows/src/validate/pr-description@v1.x.x
- with:
- min-length: "50"
```
## Required permissions
diff --git a/src/validate/pr-description/action.yml b/src/validate/pr-description/action.yml
index 899e9ad5..c50aa304 100644
--- a/src/validate/pr-description/action.yml
+++ b/src/validate/pr-description/action.yml
@@ -1,60 +1,14 @@
name: Validate PR Description
-description: "Validates that the PR description has real content beyond template boilerplate."
-
-inputs:
- min-length:
- description: Minimum content length in characters (after stripping template boilerplate)
- required: false
- default: "30"
+description: "Checks that the PR description is not empty."
runs:
using: composite
steps:
- - name: Validate PR description
+ - name: Check description
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- MIN_LENGTH: ${{ inputs.min-length }}
with:
script: |
- const body = context.payload.pull_request.body || '';
- const minLength = parseInt(process.env.MIN_LENGTH, 10);
- if (isNaN(minLength) || minLength <= 0) {
- core.setFailed(`Invalid min-length input: '${process.env.MIN_LENGTH}'`);
- return;
- }
- const errors = [];
- const warnings = [];
-
- // --- Extract content under "## Description" heading ---
- const descriptionMatch = body.match(/## Description\s*\n([\s\S]*?)(?=\n## |\n---\s*$|$)/);
- const descriptionContent = descriptionMatch ? descriptionMatch[1].trim() : '';
-
- // Strip HTML comments
- const cleaned = descriptionContent.replace(//g, '').trim();
-
- if (cleaned.length === 0) {
- errors.push('The "Description" section is empty. Please summarize what this PR does and why.');
- } else if (cleaned.length < minLength) {
- errors.push(`The "Description" section is too short (${cleaned.length} chars, minimum ${minLength}). Please provide more detail.`);
- }
-
- // --- Check that at least one "Type of Change" checkbox is checked ---
- const typeMatch = body.match(/## Type of Change\s*\n([\s\S]*?)(?=\n## |$)/);
- if (typeMatch) {
- const typeSection = typeMatch[1];
- const checked = typeSection.match(/- \[x\]/gi);
- if (!checked) {
- errors.push('No "Type of Change" checkbox is checked. Please mark at least one.');
- }
- } else {
- errors.push('Missing "Type of Change" section. Please use the PR template.');
- }
-
- // --- Report ---
- for (const w of warnings) {
- core.warning(w);
- }
-
- if (errors.length > 0) {
- core.setFailed(errors.join('\n'));
+ const body = (context.payload.pull_request.body || '').trim();
+ if (body.length === 0) {
+ core.setFailed('PR description is empty. Please provide a description.');
}