From 0d7f8dfb1d8a3393b465f8c98f675978a3d51cbf Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 29 Apr 2026 16:13:44 +0530 Subject: [PATCH 1/3] chore: standardize ruby release flow with back-merge and strict version checks --- .github/workflows/back-merge-pr.yml | 59 +++++++++++++++++ .github/workflows/check-branch.yml | 20 ------ .github/workflows/check-version-bump.yml | 82 ++++++++++++++++++++++++ .github/workflows/release-gem.yml | 3 + skills/code-review/SKILL.md | 2 +- skills/dev-workflow/SKILL.md | 4 +- skills/framework/SKILL.md | 2 +- 7 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/back-merge-pr.yml delete mode 100644 .github/workflows/check-branch.yml create mode 100644 .github/workflows/check-version-bump.yml diff --git a/.github/workflows/back-merge-pr.yml b/.github/workflows/back-merge-pr.yml new file mode 100644 index 0000000..cec0f26 --- /dev/null +++ b/.github/workflows/back-merge-pr.yml @@ -0,0 +1,59 @@ +# Opens a PR from master → development after changes land on master (back-merge). +# +# Org/repo Settings → Actions → General → Workflow permissions: read and write +# (so GITHUB_TOKEN can create pull requests). Or use a PAT in secret GH_TOKEN. + +name: Back-merge master to development + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + open-back-merge-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Open back-merge PR if needed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git fetch origin development master + + MASTER_SHA=$(git rev-parse origin/master) + DEV_SHA=$(git rev-parse origin/development) + + if [ "$MASTER_SHA" = "$DEV_SHA" ]; then + echo "master and development are at the same commit; nothing to back-merge." + exit 0 + fi + + EXISTING=$(gh pr list --repo "${{ github.repository }}" \ + --base development \ + --head master \ + --state open \ + --json number \ + --jq 'length') + + if [ "$EXISTING" -gt 0 ]; then + echo "An open PR from master to development already exists; skipping." + exit 0 + fi + + gh pr create --repo "${{ github.repository }}" \ + --base development \ + --head master \ + --title "chore: back-merge master into development" \ + --body "Automated back-merge after changes landed on \`master\`. Review and merge to keep \`development\` in sync." + + echo "Created back-merge PR master → development." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml deleted file mode 100644 index e79864e..0000000 --- a/.github/workflows/check-branch.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Check Branch' - -on: - pull_request: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Comment PR - if: github.base_ref == 'master' && github.head_ref != 'staging' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. - - name: Check branch - if: github.base_ref == 'master' && github.head_ref != 'staging' - run: | - echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." - exit 1 \ No newline at end of file diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml new file mode 100644 index 0000000..fe068ae --- /dev/null +++ b/.github/workflows/check-version-bump.yml @@ -0,0 +1,82 @@ +name: Check Version Bump + +on: + pull_request: + branches: [main, master] + +jobs: + check-version-bump: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Validate version and changelog updates + shell: bash + run: | + set -euo pipefail + + VERSION_FILE="lib/contentstack_utils/version.rb" + CHANGELOG_FILE="CHANGELOG.md" + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + mapfile -t CHANGED_FILES < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then + echo "No changed files detected." + exit 0 + fi + + is_ignored_change() { + local f="$1" + [[ "$f" =~ ^docs/ ]] && return 0 + [[ "$f" =~ ^\.github/ ]] && return 0 + [[ "$f" =~ (^|/)tests?/ ]] && return 0 + [[ "$f" =~ (^|/)spec/ ]] && return 0 + [[ "$f" =~ \.md$ ]] && [[ ! "$f" =~ (^|/)CHANGELOG\.md$ ]] && return 0 + return 1 + } + + has_release_impact=false + for file in "${CHANGED_FILES[@]}"; do + if ! is_ignored_change "$file"; then + has_release_impact=true + break + fi + done + + if [ "$has_release_impact" = false ]; then + echo "Skipping docs/test-only PR." + exit 0 + fi + + changed_file() { + local target="$1" + for file in "${CHANGED_FILES[@]}"; do + if [ "$file" = "$target" ]; then + return 0 + fi + done + return 1 + } + + changed_file "$VERSION_FILE" || { echo "Version bump required in $VERSION_FILE."; exit 1; } + changed_file "$CHANGELOG_FILE" || { echo "Matching changelog update required in $CHANGELOG_FILE."; exit 1; } + + head_version=$(sed -nE 's/.*VERSION\s*=\s*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p' "$VERSION_FILE" | sed -n '1p') + CHANGELOG_HEAD=$(sed -nE 's/^## v?([^[:space:]]+).*/\1/p' "$CHANGELOG_FILE" | head -1) + + [ -n "$CHANGELOG_HEAD" ] || { echo "::error::Could not find a top changelog heading like '## vX.Y.Z' in $CHANGELOG_FILE."; exit 1; } + [ "$CHANGELOG_HEAD" = "$head_version" ] || { echo "::error::$CHANGELOG_FILE top version ($CHANGELOG_HEAD) does not match project version ($head_version)."; exit 1; } + + base_version=$(git show "$BASE_SHA:$VERSION_FILE" | sed -nE 's/.*VERSION\s*=\s*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p' | sed -n '1p') + latest_tag=$(git tag --list 'v*' --sort=-version:refname | sed -n '1p') + latest_version="${latest_tag#v}" + [ -n "$latest_version" ] || latest_version="0.0.0" + + version_gt() { + python3 -c 'import sys;v=lambda s:[int(x) if x.isdigit() else 0 for x in (s.strip().lstrip("v").split("-",1)[0].split("+",1)[0].split(".")+["0","0","0"])[:3]];print("true" if v(sys.argv[1])>v(sys.argv[2]) else "false")' "$1" "$2" + } + + [ "$(version_gt "$head_version" "$base_version")" = "true" ] || { echo "Version must be greater than base version ($base_version). Found $head_version."; exit 1; } + [ "$(version_gt "$head_version" "$latest_version")" = "true" ] || { echo "Version must be greater than latest tag version ($latest_version). Found $head_version."; exit 1; } diff --git a/.github/workflows/release-gem.yml b/.github/workflows/release-gem.yml index 46473cb..1e388ee 100644 --- a/.github/workflows/release-gem.yml +++ b/.github/workflows/release-gem.yml @@ -6,6 +6,7 @@ on: jobs: build: + if: ${{ startsWith(github.event.release.tag_name, 'v') && !github.event.release.draft }} name: Build + Publish runs-on: ubuntu-latest permissions: @@ -14,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.release.tag_name }} - name: Set up Ruby 2.7 uses: ruby/setup-ruby@v1 with: diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index 2634370..b67a8b7 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -33,7 +33,7 @@ description: Use when reviewing or preparing a PR for this gem—behavior, tests ### Process -- Respect **CODEOWNERS** and branch policy (**`master`** vs **`staging`**) described in [dev-workflow](../dev-workflow/SKILL.md) +- Respect **CODEOWNERS** and branch policy (**feature/fix -> `development`**, release PRs **`development` -> `master`**) described in [dev-workflow](../dev-workflow/SKILL.md) ## References diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md index fdd7813..c8810f8 100644 --- a/skills/dev-workflow/SKILL.md +++ b/skills/dev-workflow/SKILL.md @@ -32,12 +32,12 @@ description: Use when setting up the dev environment, running build/test/docs, o ### Branches and PRs -- `.github/workflows/check-branch.yml` blocks merging into **`master`** unless the head branch is **`staging`** (organizational policy). Prefer PRs that follow team conventions for `master` / `staging`. +- Feature/fix PRs should target **`development`**. Release PRs are raised directly from **`development`** to **`master`**. - Use `CODEOWNERS` for required reviewers when applicable ### CI and automation (no RSpec workflow today) -- **Release:** `.github/workflows/release-gem.yml` — on GitHub **release created**, builds and pushes to RubyGems (note: workflow pins Ruby 2.7 for publish; align with gemspec minimum when changing) +- **Release:** `.github/workflows/release-gem.yml` — on GitHub **Release** created (`release: types: [created]`) for tag **`v*`** (draft releases skipped), checks out the tag, then builds and pushes to RubyGems (note: workflow pins Ruby 2.7 for publish; align with gemspec minimum when changing) - **Security / compliance:** CodeQL, policy scan, SCA scan — see `.github/workflows/` - **Issues:** Jira integration workflow in `.github/workflows/issues-jira.yml` diff --git a/skills/framework/SKILL.md b/skills/framework/SKILL.md index d102ad6..758effc 100644 --- a/skills/framework/SKILL.md +++ b/skills/framework/SKILL.md @@ -35,7 +35,7 @@ description: Use when changing the gemspec, Bundler setup, Ruby/runtime constrai ### Build and publish - Local artifact: `gem build contentstack_utils.gemspec` -- Publishing is triggered by GitHub **release** per `release-gem.yml` +- Publishing runs when a GitHub **Release** is created (`release: types: [created]`) for tag **`v*`** (draft releases skipped); see `release-gem.yml`. ## References From 505d08765ee517a7e0cba35ef5f71df9a77434dd Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 30 Apr 2026 11:58:55 +0530 Subject: [PATCH 2/3] fix: check-version bump triggered for all the PR --- .github/workflows/check-version-bump.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml index fe068ae..3002390 100644 --- a/.github/workflows/check-version-bump.yml +++ b/.github/workflows/check-version-bump.yml @@ -2,7 +2,6 @@ name: Check Version Bump on: pull_request: - branches: [main, master] jobs: check-version-bump: From 23bc113c4a2a2dd65f23ddfe763465316ab0a104 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 30 Apr 2026 15:10:48 +0530 Subject: [PATCH 3/3] ci: gate version-bump on src paths; validate semver vs latest tag only --- .github/workflows/check-version-bump.yml | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml index 3002390..676b3cf 100644 --- a/.github/workflows/check-version-bump.yml +++ b/.github/workflows/check-version-bump.yml @@ -1,3 +1,5 @@ +# Runs only when production code under lib/ changes. Version must be > latest v* tag (not vs base branch). + name: Check Version Bump on: @@ -26,26 +28,21 @@ jobs: exit 0 fi - is_ignored_change() { + is_production_source_change() { local f="$1" - [[ "$f" =~ ^docs/ ]] && return 0 - [[ "$f" =~ ^\.github/ ]] && return 0 - [[ "$f" =~ (^|/)tests?/ ]] && return 0 - [[ "$f" =~ (^|/)spec/ ]] && return 0 - [[ "$f" =~ \.md$ ]] && [[ ! "$f" =~ (^|/)CHANGELOG\.md$ ]] && return 0 - return 1 + [[ "$f" == lib/* ]] } - has_release_impact=false + has_source_changes=false for file in "${CHANGED_FILES[@]}"; do - if ! is_ignored_change "$file"; then - has_release_impact=true + if is_production_source_change "$file"; then + has_source_changes=true break fi done - if [ "$has_release_impact" = false ]; then - echo "Skipping docs/test-only PR." + if [ "$has_source_changes" = false ]; then + echo "Skipping: no lib/ production code changes." exit 0 fi @@ -68,7 +65,6 @@ jobs: [ -n "$CHANGELOG_HEAD" ] || { echo "::error::Could not find a top changelog heading like '## vX.Y.Z' in $CHANGELOG_FILE."; exit 1; } [ "$CHANGELOG_HEAD" = "$head_version" ] || { echo "::error::$CHANGELOG_FILE top version ($CHANGELOG_HEAD) does not match project version ($head_version)."; exit 1; } - base_version=$(git show "$BASE_SHA:$VERSION_FILE" | sed -nE 's/.*VERSION\s*=\s*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p' | sed -n '1p') latest_tag=$(git tag --list 'v*' --sort=-version:refname | sed -n '1p') latest_version="${latest_tag#v}" [ -n "$latest_version" ] || latest_version="0.0.0" @@ -77,5 +73,4 @@ jobs: python3 -c 'import sys;v=lambda s:[int(x) if x.isdigit() else 0 for x in (s.strip().lstrip("v").split("-",1)[0].split("+",1)[0].split(".")+["0","0","0"])[:3]];print("true" if v(sys.argv[1])>v(sys.argv[2]) else "false")' "$1" "$2" } - [ "$(version_gt "$head_version" "$base_version")" = "true" ] || { echo "Version must be greater than base version ($base_version). Found $head_version."; exit 1; } [ "$(version_gt "$head_version" "$latest_version")" = "true" ] || { echo "Version must be greater than latest tag version ($latest_version). Found $head_version."; exit 1; }