From de114d1484ae39ca2e29833fa17b7af9d205d855 Mon Sep 17 00:00:00 2001 From: Bedatty <79675696+bedatty@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:58:30 -0300 Subject: [PATCH] fix(release): merge develop into main (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(pr-validation): modularize workflow into composites under src/validate/ Extract all inline business logic from pr-validation.yml into 7 reusable composite actions under src/validate/. Add dry_run input, fix script injection risks (use env vars instead of direct interpolation), fix notify ref for external callers, and update conventions to prohibit workflow_dispatch on reusable workflows due to injection risk. * fix(pr-validation): address CodeRabbit and CodeQL review findings - Fix code-injection: move needs.*.result and inputs.dry_run to env vars in pr-checks-summary job (use process.env instead of ${{ }} interpolation) - Wire MANAGE_TOKEN into auto-labeler job (was hardcoded to github.token) - Include pr-changelog in Slack notification status and failed_jobs - Handle empty git diff output in pr-size (CHANGED_LINES defaults to 0) - Support all * wildcard patterns in pr-source-branch (not just /*) - Fix broken markdown links in docs (add -workflow suffix) - Fix docs examples to use @v1.2.3 placeholder instead of @v1.x.x - Update jobs table with non-draft condition for all gated jobs * fix(helm-update-chart): use VALUES_KEY for template file paths instead of COMP_NAME The workflow was using COMP_NAME to build configmap/secret template paths (e.g. templates/plugin-br-pix-indirect-btg-worker-inbound/configmap.yaml) but the actual directory structure uses VALUES_KEY names (e.g. templates/inbound/configmap.yaml). This caused the if [ -f ] check to silently fail, resulting in detected env vars never being injected into configmap/secret templates. Changes: - Use VALUES_KEY for CONFIGMAP_FILE and SECRET_FILE paths - Update create_secret_template to take VALUES_KEY as single arg - Add ::warning:: annotations when template files are not found Closes #167 * fix(helm-update-chart): quote GITHUB_OUTPUT and GITHUB_STEP_SUMMARY references Resolves SC2086 (double quote to prevent globbing) and SC2129 (group redirects) shellcheck warnings flagged by the PR lint analysis. * fix(helm-update-chart): resolve CodeQL medium findings - Pin crazy-max/ghaction-import-gpg and mikefarah/yq to commit SHAs - Move inputs.base_branch to env var to prevent code injection in step summary - Add inline comment dismissing untrusted-checkout false positive * docs(rules): enforce commit SHA pinning for third-party actions Update all rules and commands (Claude, Cursor, AGENTS.md) to require third-party actions to be pinned by commit SHA instead of mutable tags. LerianStudio org actions remain pinned by release tag. * refactor(pr-validation): extract pr-checks-summary composite and use branch refs for testing * fix(pr-validation): add missing README and fix broken doc link * refactor(pr-validation): optimize to 2-tier fail-fast model Consolidate 9 parallel jobs into 4 with a 2-tier architecture: - Tier 1 (blocking-checks): title, source-branch, description — no checkout, fail-fast - Tier 2 (advisory-checks): metadata, size, labels, changelog — shared checkout, only runs if Tier 1 passes Reduces runner cost (9 → 4 runners, 3 checkouts → 1) while providing faster feedback on blocking validation failures. * fix(pr-changelog): remove comment logic — changelog is auto-generated CHANGELOG.md is now generated by semantic-release, so the reminder comment is unnecessary noise. Removed the comment step, github-token and dry-run inputs from the composite. * fix(pr-validation): default enforce_source_branches to true The composite already auto-skips when the target branch is not in target_branches_for_source_check (default: main), so enabling by default is safe and avoids silent misconfiguration. * fix(pr-description): validate real content instead of raw length Rewrite pr-description composite to: - Extract content under "## Description" heading and strip HTML comments - Fail if description section is empty or below min-length - Fail if no "Type of Change" checkbox is checked - Remove github-token input (no API calls needed) - Consolidate two github-script steps into one Also pin amannn/action-semantic-pull-request to commit SHA in pr-title. * feat(pr-metadata): auto-assign PR author instead of warning Replace the warning-only assignee and linked issues checks with an actionable auto-assign: if no assignee is set, assign the PR author automatically. Bot accounts are skipped. * fix(pr-size): skip label update when unchanged and remove XL comment - Check current labels before removing/adding — skip entirely if the correct size label is already set - Only remove stale size labels that actually exist on the PR - Remove the XL comment (generic noise on every sync) * fix(pr-labels): pin actions/labeler to commit SHA * refactor(pr-validation): remove changelog check and pin all actions by SHA - Remove pr-changelog from workflow, summary, and inputs — CHANGELOG.md is auto-generated by semantic-release - Pin actions/github-script@v8 and actions/checkout@v6 to commit SHAs across all validate composites * fix(pr-checks-summary): use markdown tables grouped by tier Display results as two tables (Blocking / Advisory) instead of flat lines. Skipped checks now use ⏭️ instead of ⚠️ for clarity. * fix(pr-validation): address CodeRabbit review findings - Remove stale check_changelog references from docs and examples - Remove pr-changelog from jobs table and pr-checks-summary README - Fix related-workflow links to current doc naming - Make missing "Type of Change" section an error, not a warning - Add null-safety for pr.assignees in pr-metadata - Add dry-run gate to pr-metadata auto-assign - Fix yamllint inline-comment spacing in pr-labels * fix(pr-validation): sync defaults, fix caller, update docs - Align min_description_length default to 30 (matches composite) - Remove stale check_changelog from self-pr-validation.yml - Update metadata feature description in docs - Validate min-length input against NaN in pr-description * fix(pr-validation): pin composite refs to v1.19.1-beta.2 * fix(lint): enforce SHA pinning for externals, warnings for internals fix(lint): enforce SHA pinning for externals, warnings for internals * fix(pr-validation): pin composite refs to v1.20.0 * fix(pr-blocking-collect): add README and pin ref to v1.20.0 * fix(pr-blocking-collect): use branch ref for testing * docs(pr-blocking-collect): fix terminology — step outputs, not job outputs * fix(pr-validation): pin composite refs to v1.20.0 (#172) * fix(pr-validation): pin composite refs to v1.20.0 * fix(pr-blocking-collect): add README and pin ref to v1.20.0 * fix(pr-blocking-collect): use branch ref for testing * docs(pr-blocking-collect): fix terminology — step outputs, not job outputs * feat(release): fallback to PR when backmerge push fails When the semantic-release backmerge plugin fails to push directly to develop (non-fast-forward), create a PR from main→develop instead of failing the entire release. The release tag and GitHub release are already published at this point. - Add continue-on-error to semantic-release step - If release published but step failed → create backmerge PR - If release not published and step failed → propagate error - Check for existing backmerge PR to avoid duplicates * fix(pr-validation): pin composite refs to v1.20.1 * feat(release): extract backmerge fallback into reusable composite Create src/config/backmerge-pr composite that creates a PR when the semantic-release backmerge push fails (non-fast-forward). Checks for existing open PRs to avoid duplicates. Replace inline shell in release.yml with the composite call. * fix(release): use @develop ref for backmerge-pr composite * fix(backmerge-pr): use heredoc to avoid indentation in PR body * fix(pr-description): validate checkboxes only, not description content Simplify pr-description to only check: - At least one "Type of Change" checkbox is marked - At least one "Testing" checkbox is marked Remove min-length content validation that was blocking PRs with valid template usage (e.g., merge PRs with CodeRabbit summaries). * fix(pr-description): simplify to empty body check only * fix(ci): use @develop ref for pr-description, sync backmerge-pr --------- Co-authored-by: Gandalf --- .github/workflows/pr-validation.yml | 24 ++++----- .github/workflows/release.yml | 16 ++++++ docs/release-workflow.md | 2 +- src/config/backmerge-pr/README.md | 64 ++++++++++++++++++++++++ src/config/backmerge-pr/action.yml | 67 ++++++++++++++++++++++++++ src/validate/pr-description/README.md | 12 ++--- src/validate/pr-description/action.yml | 56 ++------------------- 7 files changed, 166 insertions(+), 75 deletions(-) create mode 100644 src/config/backmerge-pr/README.md create mode 100644 src/config/backmerge-pr/action.yml 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.'); }