contentstack_utils 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f14e384ea5f5505482d0ff84d8d0ec0f6e445a9ed35e9027fcb1c6c98f34973a
4
- data.tar.gz: 8c769520c8e662221e4b706e5d49050b1e3071966fb8d9787b1f27c7703c897e
3
+ metadata.gz: 551ed8d05213501d037c6d1c67e2a318aea27d0f648818f65046ce33772e5a71
4
+ data.tar.gz: 732fc6138fb1a3369e978187d44d5fbbd72411d44b84fd11167cb4736942f671
5
5
  SHA512:
6
- metadata.gz: edc506f638312d2d964bd9a24a4dea528a04e2b94bd509b8acbd61e1270b330dccb59c4ac8ace9e962e737a634b0fb062185d378691502a1f1b9ada466ee0eb4
7
- data.tar.gz: 48cc4d4351208b30f1b2cd00156d8fb811d06140402b6e7ae0d43ec7bc7a0d55881bb7b594606e03b2869bebb995dba5e75d796f23c3748813c986114b998da6
6
+ metadata.gz: e8caa3855dd5be8e6e7e23b462f7263e1ca1d41691fcbe638a949d4cc4aabdd79365aa9ed1dc30a6b296382bb2fe65ed71df0c274b5b3198638215cc09fe80a1
7
+ data.tar.gz: 0be5f0880dcf07efc45d61597f49b0c42161cbc5a0bcabc6d752300fb0360bac501d035784096b73c1341468104bc16086490e736e3b24484fe7ce3bb97dbff1
@@ -0,0 +1,59 @@
1
+ # Opens a PR from master → development after changes land on master (back-merge).
2
+ #
3
+ # Org/repo Settings → Actions → General → Workflow permissions: read and write
4
+ # (so GITHUB_TOKEN can create pull requests). Or use a PAT in secret GH_TOKEN.
5
+
6
+ name: Back-merge master to development
7
+
8
+ on:
9
+ push:
10
+ branches: [master]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+ pull-requests: write
16
+
17
+ jobs:
18
+ open-back-merge-pr:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Open back-merge PR if needed
27
+ env:
28
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+ run: |
30
+ set -euo pipefail
31
+ git fetch origin development master
32
+
33
+ MASTER_SHA=$(git rev-parse origin/master)
34
+ DEV_SHA=$(git rev-parse origin/development)
35
+
36
+ if [ "$MASTER_SHA" = "$DEV_SHA" ]; then
37
+ echo "master and development are at the same commit; nothing to back-merge."
38
+ exit 0
39
+ fi
40
+
41
+ EXISTING=$(gh pr list --repo "${{ github.repository }}" \
42
+ --base development \
43
+ --head master \
44
+ --state open \
45
+ --json number \
46
+ --jq 'length')
47
+
48
+ if [ "$EXISTING" -gt 0 ]; then
49
+ echo "An open PR from master to development already exists; skipping."
50
+ exit 0
51
+ fi
52
+
53
+ gh pr create --repo "${{ github.repository }}" \
54
+ --base development \
55
+ --head master \
56
+ --title "chore: back-merge master into development" \
57
+ --body "Automated back-merge after changes landed on \`master\`. Review and merge to keep \`development\` in sync."
58
+
59
+ echo "Created back-merge PR master → development."
@@ -0,0 +1,76 @@
1
+ # Runs only when production code under lib/ changes. Version must be > latest v* tag (not vs base branch).
2
+
3
+ name: Check Version Bump
4
+
5
+ on:
6
+ pull_request:
7
+
8
+ jobs:
9
+ check-version-bump:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+ - name: Validate version and changelog updates
16
+ shell: bash
17
+ run: |
18
+ set -euo pipefail
19
+
20
+ VERSION_FILE="lib/contentstack_utils/version.rb"
21
+ CHANGELOG_FILE="CHANGELOG.md"
22
+ BASE_SHA="${{ github.event.pull_request.base.sha }}"
23
+ HEAD_SHA="${{ github.event.pull_request.head.sha }}"
24
+
25
+ mapfile -t CHANGED_FILES < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
26
+ if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then
27
+ echo "No changed files detected."
28
+ exit 0
29
+ fi
30
+
31
+ is_production_source_change() {
32
+ local f="$1"
33
+ [[ "$f" == lib/* ]]
34
+ }
35
+
36
+ has_source_changes=false
37
+ for file in "${CHANGED_FILES[@]}"; do
38
+ if is_production_source_change "$file"; then
39
+ has_source_changes=true
40
+ break
41
+ fi
42
+ done
43
+
44
+ if [ "$has_source_changes" = false ]; then
45
+ echo "Skipping: no lib/ production code changes."
46
+ exit 0
47
+ fi
48
+
49
+ changed_file() {
50
+ local target="$1"
51
+ for file in "${CHANGED_FILES[@]}"; do
52
+ if [ "$file" = "$target" ]; then
53
+ return 0
54
+ fi
55
+ done
56
+ return 1
57
+ }
58
+
59
+ changed_file "$VERSION_FILE" || { echo "Version bump required in $VERSION_FILE."; exit 1; }
60
+ changed_file "$CHANGELOG_FILE" || { echo "Matching changelog update required in $CHANGELOG_FILE."; exit 1; }
61
+
62
+ head_version=$(sed -nE 's/.*VERSION\s*=\s*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p' "$VERSION_FILE" | sed -n '1p')
63
+ CHANGELOG_HEAD=$(sed -nE 's/^## v?\[?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' "$CHANGELOG_FILE" | head -1)
64
+
65
+ [ -n "$CHANGELOG_HEAD" ] || { echo "::error::Could not find a top changelog heading like '## vX.Y.Z' in $CHANGELOG_FILE."; exit 1; }
66
+ [ "$CHANGELOG_HEAD" = "$head_version" ] || { echo "::error::$CHANGELOG_FILE top version ($CHANGELOG_HEAD) does not match project version ($head_version)."; exit 1; }
67
+
68
+ latest_tag=$(git tag --list 'v*' --sort=-version:refname | sed -n '1p')
69
+ latest_version="${latest_tag#v}"
70
+ [ -n "$latest_version" ] || latest_version="0.0.0"
71
+
72
+ version_gt() {
73
+ 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"
74
+ }
75
+
76
+ [ "$(version_gt "$head_version" "$latest_version")" = "true" ] || { echo "Version must be greater than latest tag version ($latest_version). Found $head_version."; exit 1; }
@@ -2,30 +2,117 @@ name: Create Jira Ticket for Github Issue
2
2
 
3
3
  on:
4
4
  issues:
5
- types: [opened]
5
+ types: [opened, reopened]
6
6
 
7
7
  jobs:
8
8
  issue-jira:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
+ - name: Create Jira Issue
12
+ id: create_jira
13
+ uses: actions/github-script@v9
14
+ with:
15
+ script: |
16
+ const baseUrl = process.env.JIRA_BASE_URL;
17
+ const userEmail = process.env.JIRA_USER_EMAIL;
18
+ const jiraToken = process.env.JIRA_API_TOKEN;
19
+ const jiraProject = process.env.JIRA_PROJECT;
20
+ const jiraIssueType = process.env.JIRA_ISSUE_TYPE;
21
+ const jiraFields = JSON.parse(process.env.ISSUES_JIRA_FIELDS);
22
+
23
+ let requestBody = JSON.stringify({
24
+ fields: {
25
+ ...jiraFields,
26
+ "project": {
27
+ "key": jiraProject
28
+ },
29
+ "issuetype": {
30
+ "name": jiraIssueType
31
+ },
32
+ "summary": "Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }}",
33
+ "description": {
34
+ "version": 1,
35
+ "type": "doc",
36
+ "content": [
37
+ {
38
+ "type": "paragraph",
39
+ "content": [
40
+ {
41
+ "type": "text",
42
+ "text": "Github Issue",
43
+ "marks": [
44
+ {
45
+ "type": "strong"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "type": "text",
51
+ "text": ": "
52
+ },
53
+ {
54
+ "type": "text",
55
+ "text": "${{ github.event.issue.html_url }}",
56
+ "marks": [
57
+ {
58
+ "type": "link",
59
+ "attrs": {
60
+ "href": "${{ github.event.issue.html_url }}"
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "type": "paragraph",
69
+ "content": [
70
+ {
71
+ "type": "text",
72
+ "text": "Description",
73
+ "marks": [
74
+ {
75
+ "type": "strong"
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "type": "text",
81
+ "text": ":"
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "type": "codeBlock",
87
+ "content": [
88
+ {
89
+ "type": "text",
90
+ "text": `${{ github.event.issue.body }}`
91
+ }
92
+ ]
93
+ }
94
+ ]
95
+ }
96
+ }
97
+ });
11
98
 
12
- - name: Login to Jira
13
- uses: atlassian/gajira-login@master
99
+ const response = await fetch(`${baseUrl}/rest/api/3/issue`, {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'Authorization': `Basic ${btoa(userEmail + ":" + jiraToken)}`
104
+ },
105
+ body: requestBody
106
+ });
107
+ if (!response.ok) {
108
+ throw new Error(`JIRA API error! Status: ${response.status}`);
109
+ }
110
+ const data = await response.json();
111
+ console.log('Jira Issue Created:', data.key);
14
112
  env:
15
113
  JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
16
114
  JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
17
115
  JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
18
-
19
- - name: Create Jira Issue
20
- id: create_jira
21
- uses: atlassian/gajira-create@master
22
- with:
23
- project: ${{ secrets.JIRA_PROJECT }}
24
- issuetype: ${{ secrets.JIRA_ISSUE_TYPE }}
25
- summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }}
26
- description: |
27
- *GitHub Issue:* ${{ github.event.issue.html_url }}
28
-
29
- *Description:*
30
- ${{ github.event.issue.body }}
31
- fields: "${{ secrets.ISSUES_JIRA_FIELDS }}"
116
+ JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}
117
+ JIRA_ISSUE_TYPE: ${{ secrets.JIRA_ISSUE_TYPE }}
118
+ ISSUES_JIRA_FIELDS: "${{ secrets.ISSUES_JIRA_FIELDS }}"
@@ -6,6 +6,7 @@ on:
6
6
 
7
7
  jobs:
8
8
  build:
9
+ if: ${{ startsWith(github.event.release.tag_name, 'v') && !github.event.release.draft }}
9
10
  name: Build + Publish
10
11
  runs-on: ubuntu-latest
11
12
  permissions:
@@ -14,10 +15,12 @@ jobs:
14
15
 
15
16
  steps:
16
17
  - uses: actions/checkout@v3
17
- - name: Set up Ruby 2.7
18
+ with:
19
+ ref: ${{ github.event.release.tag_name }}
20
+ - name: Set up Ruby 3.1
18
21
  uses: ruby/setup-ruby@v1
19
22
  with:
20
- ruby-version: '2.7'
23
+ ruby-version: '3.1'
21
24
 
22
25
  - name: Publish to RubyGems
23
26
  run: |
data/.gitignore CHANGED
@@ -8,4 +8,6 @@ coverage
8
8
  .DS_Store
9
9
  .bundle/
10
10
  **/rspec_results.html
11
- .dccache
11
+ .dccache
12
+ vendor/
13
+ lib/contentstack_utils/assets/regions.json
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.4
1
+ 3.3.11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.3.0) (2026-06-29)
4
+ - Added `ContentstackUtils::Endpoint.get_contentstack_endpoint` for dynamic endpoint resolution based on region and service.
5
+ - Added `ContentstackUtils.get_contentstack_endpoint` as a backward-compatible proxy.
6
+ - Added `ContentstackUtils::Endpoint.refresh_regions` for manual region metadata refresh.
7
+ - Added runtime fallback to automatically download `regions.json` from the Contentstack Regions Registry when not present locally.
8
+ - Fixed security vulnerabilities: upgraded yard to 0.9.44 (Directory Traversal) and concurrent-ruby to 1.3.7 (Infinite loop, Improper Locking, Wrap-around Error).
9
+
3
10
  ## [1.2.4](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.2.4) (2026-04-15)
4
11
  - Fixed Security issues.
5
12
 
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contentstack_utils (1.2.4)
4
+ contentstack_utils (1.3.0)
5
5
  activesupport (>= 7.0, < 8)
6
- nokogiri (>= 1.13, < 1.19)
6
+ nokogiri (~> 1.19, >= 1.19.2)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
@@ -25,8 +25,8 @@ GEM
25
25
  base64 (0.3.0)
26
26
  benchmark (0.5.0)
27
27
  bigdecimal (4.1.1)
28
- concurrent-ruby (1.3.6)
29
- connection_pool (2.5.5)
28
+ concurrent-ruby (1.3.7)
29
+ connection_pool (3.0.2)
30
30
  crack (1.0.1)
31
31
  bigdecimal
32
32
  rexml
@@ -38,11 +38,25 @@ GEM
38
38
  concurrent-ruby (~> 1.0)
39
39
  logger (1.7.0)
40
40
  minitest (5.27.0)
41
- nokogiri (1.18.10-arm64-darwin)
41
+ nokogiri (1.19.3-aarch64-linux-gnu)
42
42
  racc (~> 1.4)
43
- public_suffix (6.0.2)
43
+ nokogiri (1.19.3-aarch64-linux-musl)
44
+ racc (~> 1.4)
45
+ nokogiri (1.19.3-arm-linux-gnu)
46
+ racc (~> 1.4)
47
+ nokogiri (1.19.3-arm-linux-musl)
48
+ racc (~> 1.4)
49
+ nokogiri (1.19.3-arm64-darwin)
50
+ racc (~> 1.4)
51
+ nokogiri (1.19.3-x86_64-darwin)
52
+ racc (~> 1.4)
53
+ nokogiri (1.19.3-x86_64-linux-gnu)
54
+ racc (~> 1.4)
55
+ nokogiri (1.19.3-x86_64-linux-musl)
56
+ racc (~> 1.4)
57
+ public_suffix (7.0.5)
44
58
  racc (1.8.1)
45
- rake (13.4.1)
59
+ rake (13.4.2)
46
60
  rexml (3.4.4)
47
61
  rspec (3.13.2)
48
62
  rspec-core (~> 3.13.0)
@@ -70,11 +84,17 @@ GEM
70
84
  addressable (>= 2.8.0)
71
85
  crack (>= 0.3.2)
72
86
  hashdiff (>= 0.4.0, < 2.0.0)
73
- yard (0.9.41)
87
+ yard (0.9.44)
74
88
 
75
89
  PLATFORMS
76
- arm64-darwin-22
77
- arm64-darwin-25
90
+ aarch64-linux-gnu
91
+ aarch64-linux-musl
92
+ arm-linux-gnu
93
+ arm-linux-musl
94
+ arm64-darwin
95
+ x86_64-darwin
96
+ x86_64-linux-gnu
97
+ x86_64-linux-musl
78
98
 
79
99
  DEPENDENCIES
80
100
  contentstack_utils!
@@ -82,7 +102,7 @@ DEPENDENCIES
82
102
  rspec (~> 3.13)
83
103
  simplecov (~> 0.22)
84
104
  webmock (~> 3.23)
85
- yard (~> 0.9.38)
105
+ yard (>= 0.9.44)
86
106
 
87
107
  BUNDLED WITH
88
- 2.3.26
108
+ 2.5.22
@@ -22,11 +22,11 @@ Gem::Specification.new do |s|
22
22
  s.require_paths = ["lib"]
23
23
 
24
24
  s.add_dependency 'activesupport', '>= 7.0', '< 8'
25
- s.add_dependency 'nokogiri', '>= 1.13', '< 1.19'
25
+ s.add_dependency 'nokogiri', '~> 1.19', '>= 1.19.2'
26
26
 
27
27
  s.add_development_dependency 'rake', '~> 13.0'
28
28
  s.add_development_dependency 'rspec', '~> 3.13'
29
29
  s.add_development_dependency 'webmock', '~> 3.23'
30
30
  s.add_development_dependency 'simplecov', '~> 0.22'
31
- s.add_development_dependency 'yard', '~> 0.9.38'
31
+ s.add_development_dependency 'yard', '>= 0.9.44'
32
32
  end
@@ -0,0 +1,96 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module ContentstackUtils
6
+ module Endpoint
7
+ REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'
8
+ REGIONS_FILE = File.expand_path('../assets/regions.json', __FILE__)
9
+
10
+ @regions_data = nil
11
+
12
+ class << self
13
+ def get_contentstack_endpoint(region: 'us', service: '', omit_https: false)
14
+ raise ArgumentError, 'Empty region provided' if region.nil? || region.to_s.strip.empty?
15
+
16
+ normalized = region.to_s.strip.downcase
17
+ regions = load_regions
18
+
19
+ region_row = find_region_by_id_or_alias(regions, normalized)
20
+ raise ArgumentError, "Invalid region: #{region}" if region_row.nil?
21
+
22
+ endpoints = region_row['endpoints']
23
+
24
+ if service.nil? || service.to_s.strip.empty?
25
+ return omit_https ? strip_https_from_map(endpoints) : endpoints.dup
26
+ end
27
+
28
+ url = endpoints[service.to_s]
29
+ raise ArgumentError, "Service \"#{service}\" not found for region \"#{region}\"" if url.nil?
30
+
31
+ omit_https ? strip_https(url) : url
32
+ end
33
+
34
+ def refresh_regions
35
+ download_and_save(REGIONS_FILE)
36
+ @regions_data = nil
37
+ load_regions
38
+ true
39
+ end
40
+
41
+ def reset_cache
42
+ @regions_data = nil
43
+ end
44
+
45
+ private
46
+
47
+ def load_regions
48
+ return @regions_data if @regions_data
49
+
50
+ unless File.exist?(REGIONS_FILE)
51
+ download_and_save(REGIONS_FILE)
52
+ end
53
+
54
+ raw = File.read(REGIONS_FILE)
55
+ parsed = JSON.parse(raw)
56
+ raise RuntimeError, 'Invalid regions data: missing "regions" key' unless parsed.is_a?(Hash) && parsed['regions']
57
+
58
+ @regions_data = parsed['regions']
59
+ rescue JSON::ParserError => e
60
+ raise RuntimeError, "Failed to parse regions data: #{e.message}"
61
+ rescue Errno::ENOENT => e
62
+ raise RuntimeError, "Failed to read regions file: #{e.message}"
63
+ end
64
+
65
+ def download_and_save(dest)
66
+ uri = URI.parse(REGIONS_URL)
67
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 30) do |http|
68
+ http.get(uri.request_uri)
69
+ end
70
+
71
+ raise RuntimeError, "Failed to download regions: HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
72
+
73
+ parsed = JSON.parse(response.body)
74
+ raise RuntimeError, 'Downloaded regions data is invalid' unless parsed.is_a?(Hash) && parsed['regions']
75
+
76
+ FileUtils.mkdir_p(File.dirname(dest))
77
+ File.write(dest, JSON.pretty_generate(parsed))
78
+ rescue StandardError => e
79
+ raise RuntimeError, "Failed to fetch region metadata: #{e.message}"
80
+ end
81
+
82
+ def find_region_by_id_or_alias(regions, input)
83
+ regions.find { |r| r['id'] == input } ||
84
+ regions.find { |r| r['alias']&.any? { |a| a.downcase == input } }
85
+ end
86
+
87
+ def strip_https(url)
88
+ url.sub(%r{\Ahttps?://}, '')
89
+ end
90
+
91
+ def strip_https_from_map(endpoints)
92
+ endpoints.transform_values { |url| strip_https(url) }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,6 +1,7 @@
1
1
  require_relative './model/options.rb'
2
2
  require_relative './model/metadata.rb'
3
3
  require_relative './support/helper.rb'
4
+ require_relative './endpoint.rb'
4
5
  require 'nokogiri'
5
6
 
6
7
  module ContentstackUtils
@@ -136,6 +137,10 @@ module ContentstackUtils
136
137
  return nil
137
138
  end
138
139
 
140
+ def self.get_contentstack_endpoint(region: 'us', service: '', omit_https: false)
141
+ Endpoint.get_contentstack_endpoint(region: region, service: service, omit_https: omit_https)
142
+ end
143
+
139
144
  module GQL
140
145
  include ContentstackUtils
141
146
  def self.json_to_html(content, options)
@@ -1,3 +1,3 @@
1
1
  module ContentstackUtils
2
- VERSION = "1.2.4"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -33,7 +33,7 @@ description: Use when reviewing or preparing a PR for this gem—behavior, tests
33
33
 
34
34
  ### Process
35
35
 
36
- - Respect **CODEOWNERS** and branch policy (**`master`** vs **`staging`**) described in [dev-workflow](../dev-workflow/SKILL.md)
36
+ - Respect **CODEOWNERS** and branch policy (**feature/fix -> `development`**, release PRs **`development` -> `master`**) described in [dev-workflow](../dev-workflow/SKILL.md)
37
37
 
38
38
  ## References
39
39
 
@@ -32,12 +32,12 @@ description: Use when setting up the dev environment, running build/test/docs, o
32
32
 
33
33
  ### Branches and PRs
34
34
 
35
- - `.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`.
35
+ - Feature/fix PRs should target **`development`**. Release PRs are raised directly from **`development`** to **`master`**.
36
36
  - Use `CODEOWNERS` for required reviewers when applicable
37
37
 
38
38
  ### CI and automation (no RSpec workflow today)
39
39
 
40
- - **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)
40
+ - **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)
41
41
  - **Security / compliance:** CodeQL, policy scan, SCA scan — see `.github/workflows/`
42
42
  - **Issues:** Jira integration workflow in `.github/workflows/issues-jira.yml`
43
43
 
@@ -35,7 +35,7 @@ description: Use when changing the gemspec, Bundler setup, Ruby/runtime constrai
35
35
  ### Build and publish
36
36
 
37
37
  - Local artifact: `gem build contentstack_utils.gemspec`
38
- - Publishing is triggered by GitHub **release** per `release-gem.yml`
38
+ - Publishing runs when a GitHub **Release** is created (`release: types: [created]`) for tag **`v*`** (draft releases skipped); see `release-gem.yml`.
39
39
 
40
40
  ## References
41
41
 
@@ -0,0 +1,268 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ContentstackUtils::Endpoint do
4
+ before(:each) do
5
+ described_class.reset_cache
6
+ end
7
+
8
+ # ── Default region ──────────────────────────────────────────────────────────
9
+
10
+ describe '.get_contentstack_endpoint with defaults' do
11
+ it 'returns a hash when no service is given' do
12
+ result = described_class.get_contentstack_endpoint
13
+ expect(result).to be_a(Hash)
14
+ end
15
+
16
+ it 'default region includes contentDelivery and contentManagement' do
17
+ result = described_class.get_contentstack_endpoint
18
+ expect(result).to have_key('contentDelivery')
19
+ expect(result).to have_key('contentManagement')
20
+ end
21
+
22
+ it 'default region contentDelivery is NA CDN' do
23
+ result = described_class.get_contentstack_endpoint
24
+ expect(result['contentDelivery']).to eq('https://cdn.contentstack.io')
25
+ end
26
+ end
27
+
28
+ # ── NA aliases ──────────────────────────────────────────────────────────────
29
+
30
+ describe 'NA region aliases' do
31
+ %w[na us aws-na aws_na NA US AWS-NA AWS_NA].each do |alias_val|
32
+ it "alias \"#{alias_val}\" resolves contentDelivery to NA CDN" do
33
+ result = described_class.get_contentstack_endpoint(region: alias_val, service: 'contentDelivery')
34
+ expect(result).to eq('https://cdn.contentstack.io')
35
+ end
36
+ end
37
+ end
38
+
39
+ # ── All 7 regions – contentDelivery ─────────────────────────────────────────
40
+
41
+ describe 'contentDelivery endpoints per region' do
42
+ {
43
+ 'na' => 'https://cdn.contentstack.io',
44
+ 'eu' => 'https://eu-cdn.contentstack.com',
45
+ 'au' => 'https://au-cdn.contentstack.com',
46
+ 'azure-na' => 'https://azure-na-cdn.contentstack.com',
47
+ 'azure-eu' => 'https://azure-eu-cdn.contentstack.com',
48
+ 'gcp-na' => 'https://gcp-na-cdn.contentstack.com',
49
+ 'gcp-eu' => 'https://gcp-eu-cdn.contentstack.com'
50
+ }.each do |region, expected_url|
51
+ it "region \"#{region}\" resolves contentDelivery to #{expected_url}" do
52
+ result = described_class.get_contentstack_endpoint(region: region, service: 'contentDelivery')
53
+ expect(result).to eq(expected_url)
54
+ end
55
+ end
56
+ end
57
+
58
+ # ── All 7 regions – contentManagement ───────────────────────────────────────
59
+
60
+ describe 'contentManagement endpoints per region' do
61
+ {
62
+ 'na' => 'https://api.contentstack.io',
63
+ 'eu' => 'https://eu-api.contentstack.com',
64
+ 'au' => 'https://au-api.contentstack.com',
65
+ 'azure-na' => 'https://azure-na-api.contentstack.com',
66
+ 'azure-eu' => 'https://azure-eu-api.contentstack.com',
67
+ 'gcp-na' => 'https://gcp-na-api.contentstack.com',
68
+ 'gcp-eu' => 'https://gcp-eu-api.contentstack.com'
69
+ }.each do |region, expected_url|
70
+ it "region \"#{region}\" resolves contentManagement to #{expected_url}" do
71
+ result = described_class.get_contentstack_endpoint(region: region, service: 'contentManagement')
72
+ expect(result).to eq(expected_url)
73
+ end
74
+ end
75
+ end
76
+
77
+ # ── Service keys present ─────────────────────────────────────────────────────
78
+
79
+ describe 'all service keys present' do
80
+ let(:expected_services) do
81
+ %w[
82
+ application contentDelivery contentManagement auth
83
+ graphqlDelivery preview graphqlPreview images assets
84
+ automate launch developerHub brandKit genAI
85
+ personalizeManagement personalizeEdge composableStudio
86
+ ]
87
+ end
88
+
89
+ it 'EU region returns all expected service keys' do
90
+ result = described_class.get_contentstack_endpoint(region: 'eu')
91
+ expected_services.each do |svc|
92
+ expect(result).to have_key(svc), "expected key #{svc} to be present"
93
+ end
94
+ end
95
+
96
+ it 'NA region additionally includes assetManagement' do
97
+ result = described_class.get_contentstack_endpoint(region: 'na')
98
+ expect(result).to have_key('assetManagement')
99
+ end
100
+ end
101
+
102
+ # ── omit_https flag ──────────────────────────────────────────────────────────
103
+
104
+ describe 'omit_https option' do
105
+ it 'strips https:// from a single service URL' do
106
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: true)
107
+ expect(result).to eq('cdn.contentstack.io')
108
+ end
109
+
110
+ it 'strips https:// from all endpoints when no service given' do
111
+ result = described_class.get_contentstack_endpoint(region: 'na', omit_https: true)
112
+ result.each_value do |url|
113
+ expect(url).not_to start_with('https://')
114
+ expect(url).not_to start_with('http://')
115
+ end
116
+ end
117
+
118
+ it 'retains https:// when omit_https is false (default)' do
119
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: false)
120
+ expect(result).to start_with('https://')
121
+ end
122
+
123
+ it 'strips https:// from EU contentManagement' do
124
+ result = described_class.get_contentstack_endpoint(region: 'eu', service: 'contentManagement', omit_https: true)
125
+ expect(result).to eq('eu-api.contentstack.com')
126
+ end
127
+ end
128
+
129
+ # ── Case-insensitive aliases ─────────────────────────────────────────────────
130
+
131
+ describe 'case-insensitive alias resolution' do
132
+ it 'resolves uppercase AWS-NA alias' do
133
+ result = described_class.get_contentstack_endpoint(region: 'AWS-NA', service: 'contentDelivery')
134
+ expect(result).to eq('https://azure-na-cdn.contentstack.com').or eq('https://cdn.contentstack.io')
135
+ end
136
+
137
+ it 'resolves azure_na alias' do
138
+ result = described_class.get_contentstack_endpoint(region: 'azure_na', service: 'contentDelivery')
139
+ expect(result).to eq('https://azure-na-cdn.contentstack.com')
140
+ end
141
+
142
+ it 'resolves gcp_eu alias' do
143
+ result = described_class.get_contentstack_endpoint(region: 'gcp_eu', service: 'contentDelivery')
144
+ expect(result).to eq('https://gcp-eu-cdn.contentstack.com')
145
+ end
146
+
147
+ it 'resolves AZURE-EU alias' do
148
+ result = described_class.get_contentstack_endpoint(region: 'AZURE-EU', service: 'contentDelivery')
149
+ expect(result).to eq('https://azure-eu-cdn.contentstack.com')
150
+ end
151
+
152
+ it 'resolves GCP-NA alias' do
153
+ result = described_class.get_contentstack_endpoint(region: 'GCP-NA', service: 'contentDelivery')
154
+ expect(result).to eq('https://gcp-na-cdn.contentstack.com')
155
+ end
156
+ end
157
+
158
+ # ── No-service returns full endpoints hash ───────────────────────────────────
159
+
160
+ describe 'full endpoints map returned when no service specified' do
161
+ it 'AU returns a hash with more than 1 entry' do
162
+ result = described_class.get_contentstack_endpoint(region: 'au')
163
+ expect(result).to be_a(Hash)
164
+ expect(result.size).to be > 1
165
+ end
166
+
167
+ it 'AU contentDelivery is correct in the map' do
168
+ result = described_class.get_contentstack_endpoint(region: 'au')
169
+ expect(result['contentDelivery']).to eq('https://au-cdn.contentstack.com')
170
+ end
171
+
172
+ it 'returns a copy — mutations do not affect cached data' do
173
+ result = described_class.get_contentstack_endpoint(region: 'na')
174
+ result['contentDelivery'] = 'mutated'
175
+ fresh = described_class.get_contentstack_endpoint(region: 'na')
176
+ expect(fresh['contentDelivery']).to eq('https://cdn.contentstack.io')
177
+ end
178
+ end
179
+
180
+ # ── Error cases ──────────────────────────────────────────────────────────────
181
+
182
+ describe 'error cases' do
183
+ it 'raises ArgumentError for empty string region' do
184
+ expect { described_class.get_contentstack_endpoint(region: '') }
185
+ .to raise_error(ArgumentError, 'Empty region provided')
186
+ end
187
+
188
+ it 'raises ArgumentError for whitespace-only region' do
189
+ expect { described_class.get_contentstack_endpoint(region: ' ') }
190
+ .to raise_error(ArgumentError, 'Empty region provided')
191
+ end
192
+
193
+ it 'raises ArgumentError for unknown region' do
194
+ expect { described_class.get_contentstack_endpoint(region: 'invalid-region') }
195
+ .to raise_error(ArgumentError, 'Invalid region: invalid-region')
196
+ end
197
+
198
+ it 'raises ArgumentError for unknown service' do
199
+ expect { described_class.get_contentstack_endpoint(region: 'na', service: 'unknownService') }
200
+ .to raise_error(ArgumentError, 'Service "unknownService" not found for region "na"')
201
+ end
202
+
203
+ it 'raises ArgumentError for valid region but unknown service' do
204
+ expect { described_class.get_contentstack_endpoint(region: 'eu', service: 'nonExistentService') }
205
+ .to raise_error(ArgumentError, /Service "nonExistentService" not found/)
206
+ end
207
+ end
208
+
209
+ # ── Additional service spot-checks ──────────────────────────────────────────
210
+
211
+ describe 'additional service endpoints' do
212
+ it 'resolves auth endpoint for NA' do
213
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'auth')
214
+ expect(result).to eq('https://auth-api.contentstack.com')
215
+ end
216
+
217
+ it 'resolves graphqlDelivery endpoint for EU' do
218
+ result = described_class.get_contentstack_endpoint(region: 'eu', service: 'graphqlDelivery')
219
+ expect(result).to eq('https://eu-graphql.contentstack.com')
220
+ end
221
+
222
+ it 'resolves preview endpoint for azure-na' do
223
+ result = described_class.get_contentstack_endpoint(region: 'azure-na', service: 'preview')
224
+ expect(result).to eq('https://azure-na-rest-preview.contentstack.com')
225
+ end
226
+
227
+ it 'resolves application endpoint for gcp-eu' do
228
+ result = described_class.get_contentstack_endpoint(region: 'gcp-eu', service: 'application')
229
+ expect(result).to eq('https://gcp-eu-app.contentstack.com')
230
+ end
231
+ end
232
+ end
233
+
234
+ # ── Utils proxy ────────────────────────────────────────────────────────────────
235
+
236
+ RSpec.describe ContentstackUtils do
237
+ before(:each) do
238
+ ContentstackUtils::Endpoint.reset_cache
239
+ end
240
+
241
+ describe '.get_contentstack_endpoint' do
242
+ it 'returns same value as Endpoint.get_contentstack_endpoint for default args' do
243
+ expect(described_class.get_contentstack_endpoint)
244
+ .to eq(ContentstackUtils::Endpoint.get_contentstack_endpoint)
245
+ end
246
+
247
+ it 'proxy returns contentDelivery for NA' do
248
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery')
249
+ expect(result).to eq('https://cdn.contentstack.io')
250
+ end
251
+
252
+ it 'proxy respects omit_https option' do
253
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: true)
254
+ expect(result).to eq('cdn.contentstack.io')
255
+ end
256
+
257
+ it 'proxy returns full endpoints map when no service given' do
258
+ result = described_class.get_contentstack_endpoint(region: 'eu')
259
+ expect(result).to be_a(Hash)
260
+ expect(result['contentManagement']).to eq('https://eu-api.contentstack.com')
261
+ end
262
+
263
+ it 'proxy raises ArgumentError for invalid region' do
264
+ expect { described_class.get_contentstack_endpoint(region: 'bad-region') }
265
+ .to raise_error(ArgumentError, /Invalid region/)
266
+ end
267
+ end
268
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contentstack_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Contentstack
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-15 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -34,22 +34,22 @@ dependencies:
34
34
  name: nokogiri
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '1.13'
40
- - - "<"
37
+ - - "~>"
41
38
  - !ruby/object:Gem::Version
42
39
  version: '1.19'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.19.2
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: '1.13'
50
- - - "<"
47
+ - - "~>"
51
48
  - !ruby/object:Gem::Version
52
49
  version: '1.19'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.19.2
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: rake
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -110,16 +110,16 @@ dependencies:
110
110
  name: yard
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
- - - "~>"
113
+ - - ">="
114
114
  - !ruby/object:Gem::Version
115
- version: 0.9.38
115
+ version: 0.9.44
116
116
  type: :development
117
117
  prerelease: false
118
118
  version_requirements: !ruby/object:Gem::Requirement
119
119
  requirements:
120
- - - "~>"
120
+ - - ">="
121
121
  - !ruby/object:Gem::Version
122
- version: 0.9.38
122
+ version: 0.9.44
123
123
  description: Contentstack Ruby client for the Content Delivery API
124
124
  email:
125
125
  - support@contentstack.com
@@ -128,7 +128,8 @@ extensions: []
128
128
  extra_rdoc_files: []
129
129
  files:
130
130
  - ".cursor/rules/README.md"
131
- - ".github/workflows/check-branch.yml"
131
+ - ".github/workflows/back-merge-pr.yml"
132
+ - ".github/workflows/check-version-bump.yml"
132
133
  - ".github/workflows/codeql-analysis.yml"
133
134
  - ".github/workflows/issues-jira.yml"
134
135
  - ".github/workflows/policy-scan.yml"
@@ -149,6 +150,7 @@ files:
149
150
  - SECURITY.md
150
151
  - contentstack_utils.gemspec
151
152
  - lib/contentstack_utils.rb
153
+ - lib/contentstack_utils/endpoint.rb
152
154
  - lib/contentstack_utils/interface/renderable.rb
153
155
  - lib/contentstack_utils/model/metadata.rb
154
156
  - lib/contentstack_utils/model/options.rb
@@ -162,6 +164,7 @@ files:
162
164
  - skills/framework/SKILL.md
163
165
  - skills/ruby-style/SKILL.md
164
166
  - skills/testing/SKILL.md
167
+ - spec/lib/endpoint_spec.rb
165
168
  - spec/lib/model/metadata_spec.rb
166
169
  - spec/lib/model/option_spec.rb
167
170
  - spec/lib/utils_spec.rb
@@ -190,11 +193,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
193
  - !ruby/object:Gem::Version
191
194
  version: '0'
192
195
  requirements: []
193
- rubygems_version: 3.1.6
196
+ rubygems_version: 3.3.27
194
197
  signing_key:
195
198
  specification_version: 4
196
199
  summary: Contentstack Ruby Utils for
197
200
  test_files:
201
+ - spec/lib/endpoint_spec.rb
198
202
  - spec/lib/model/metadata_spec.rb
199
203
  - spec/lib/model/option_spec.rb
200
204
  - spec/lib/utils_spec.rb
@@ -1,20 +0,0 @@
1
- name: 'Check Branch'
2
-
3
- on:
4
- pull_request:
5
-
6
- jobs:
7
- check_branch:
8
- runs-on: ubuntu-latest
9
- steps:
10
- - name: Comment PR
11
- if: github.base_ref == 'master' && github.head_ref != 'staging'
12
- uses: thollander/actions-comment-pull-request@v2
13
- with:
14
- message: |
15
- 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.
16
- - name: Check branch
17
- if: github.base_ref == 'master' && github.head_ref != 'staging'
18
- run: |
19
- 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."
20
- exit 1