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 @@ + + + + + +
Lerian

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.'); }