Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand All @@ -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 }}
Expand All @@ -145,15 +139,15 @@ 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' }}

- 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 }}
Expand All @@ -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 }}
Expand All @@ -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' }}
Expand All @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@
gpg_fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}

steps:
- uses: actions/create-github-app-token@v2

Check failure on line 106 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

External action not pinned by SHA: - uses: actions/create-github-app-token@v2 (use full commit SHA with a # vX.Y.Z comment)
id: app-token
with:
app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }}
private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@v6

Check failure on line 113 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

External action not pinned by SHA: uses: actions/checkout@v6 (use full commit SHA with a # vX.Y.Z comment)
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
Expand All @@ -121,7 +121,7 @@
git reset --hard origin/${{ github.ref_name }}

- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v7

Check failure on line 124 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

External action not pinned by SHA: uses: crazy-max/ghaction-import-gpg@v7 (use full commit SHA with a # vX.Y.Z comment)
id: import_gpg
with:
gpg_private_key: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY }}
Expand All @@ -133,7 +133,7 @@
git_commit_gpgsign: true

- name: Setup Node.js
uses: actions/setup-node@v6

Check failure on line 136 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

External action not pinned by SHA: uses: actions/setup-node@v6 (use full commit SHA with a # vX.Y.Z comment)
with:
node-version: '20'

Expand All @@ -148,8 +148,9 @@
@semantic-release/exec

- name: Semantic Release
uses: cycjimmy/semantic-release-action@v6

Check failure on line 151 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Pinned Actions Check

External action not pinned by SHA: uses: cycjimmy/semantic-release-action@v6 (use full commit SHA with a # vX.Y.Z comment)
id: semantic
continue-on-error: true
with:
ci: false
semantic_version: ${{ inputs.semantic_version }}
Expand All @@ -164,6 +165,21 @@
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
Expand Down
2 changes: 1 addition & 1 deletion docs/release-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions src/config/backmerge-pr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td><img src="https://github.com/LerianStudio.png" width="72" alt="Lerian" /></td>
<td><h1>backmerge-pr</h1></td>
</tr>
</table>

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
```
67 changes: 67 additions & 0 deletions src/config/backmerge-pr/action.yml
Original file line number Diff line number Diff line change
@@ -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}"
12 changes: 4 additions & 8 deletions src/validate/pr-description/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
</tr>
</table>

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

Expand All @@ -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
Expand Down
56 changes: 5 additions & 51 deletions src/validate/pr-description/action.yml
Original file line number Diff line number Diff line change
@@ -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(/<!--[\s\S]*?-->/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.');
}
Loading