contentstack_utils 1.2.3 → 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 +4 -4
- data/.cursor/rules/README.md +5 -0
- data/.github/workflows/back-merge-pr.yml +59 -0
- data/.github/workflows/check-version-bump.yml +76 -0
- data/.github/workflows/issues-jira.yml +104 -17
- data/.github/workflows/release-gem.yml +5 -2
- data/.gitignore +3 -1
- data/.ruby-version +1 -1
- data/AGENTS.md +49 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +38 -23
- data/contentstack_utils.gemspec +4 -4
- data/lib/contentstack_utils/endpoint.rb +96 -0
- data/lib/contentstack_utils/utils.rb +5 -0
- data/lib/contentstack_utils/version.rb +1 -1
- data/skills/README.md +16 -0
- data/skills/code-review/SKILL.md +44 -0
- data/skills/contentstack-utils/SKILL.md +51 -0
- data/skills/dev-workflow/SKILL.md +53 -0
- data/skills/framework/SKILL.md +44 -0
- data/skills/ruby-style/SKILL.md +42 -0
- data/skills/testing/SKILL.md +45 -0
- data/spec/lib/endpoint_spec.rb +268 -0
- metadata +38 -13
- data/.github/workflows/check-branch.yml +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 551ed8d05213501d037c6d1c67e2a318aea27d0f648818f65046ce33772e5a71
|
|
4
|
+
data.tar.gz: 732fc6138fb1a3369e978187d44d5fbbd72411d44b84fd11167cb4736942f671
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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: '
|
|
23
|
+
ruby-version: '3.1'
|
|
21
24
|
|
|
22
25
|
- name: Publish to RubyGems
|
|
23
26
|
run: |
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.3.11
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contentstack Utils Ruby – Agent guide
|
|
2
|
+
|
|
3
|
+
**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**.
|
|
4
|
+
|
|
5
|
+
## What this repo is
|
|
6
|
+
|
|
7
|
+
| Field | Detail |
|
|
8
|
+
|--------|--------|
|
|
9
|
+
| **Name:** | [contentstack/contentstack-utils-ruby](https://github.com/contentstack/contentstack-utils-ruby) |
|
|
10
|
+
| **Purpose:** | Ruby gem that renders Contentstack rich text and JSON RTE (including GraphQL-shaped payloads) to HTML, with pluggable rendering via `ContentstackUtils::Model::Options` subclasses. |
|
|
11
|
+
| **Out of scope (if any):** | This package does not ship an HTTP client or stack SDK; it pairs with the separate Contentstack Ruby delivery client for entry data and `_embedded_items`. |
|
|
12
|
+
|
|
13
|
+
## Tech stack (at a glance)
|
|
14
|
+
|
|
15
|
+
| Area | Details |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| Language | Ruby **≥ 3.1** (see `contentstack_utils.gemspec` and `.ruby-version` for local dev) |
|
|
18
|
+
| Build | **Bundler** + **RubyGems**; `contentstack_utils.gemspec`, `Gemfile` |
|
|
19
|
+
| Tests | **RSpec**; specs under `spec/**/*_spec.rb`, loaded via `spec/spec_helper.rb` |
|
|
20
|
+
| Lint / coverage | No RuboCop in-repo; **SimpleCov** in `spec/spec_helper.rb`; API docs via **YARD** (`.yardopts`, `rake yard`) |
|
|
21
|
+
| Runtime deps | **activesupport** (7.x), **nokogiri** (HTML / XML for legacy RTE strings) |
|
|
22
|
+
|
|
23
|
+
## Commands (quick reference)
|
|
24
|
+
|
|
25
|
+
| Command type | Command |
|
|
26
|
+
|--------------|---------|
|
|
27
|
+
| Install deps | `bundle install` |
|
|
28
|
+
| Build (default task) | `bundle exec rake` (runs **spec**) |
|
|
29
|
+
| Test | `bundle exec rake spec` or `bundle exec rspec` |
|
|
30
|
+
| Docs | `bundle exec rake yard` |
|
|
31
|
+
|
|
32
|
+
**CI / automation:** There is no dedicated workflow that runs `rspec` on every push; local verification is `bundle exec rake`. Other workflows include branch checks (PRs into `master`), release publish, CodeQL, policy/SCA scans—see `.github/workflows/`.
|
|
33
|
+
|
|
34
|
+
## Where the documentation lives: skills
|
|
35
|
+
|
|
36
|
+
| Skill | Path | What it covers |
|
|
37
|
+
|-------|------|----------------|
|
|
38
|
+
| Code review | `skills/code-review/SKILL.md` | PR checklist for this gem |
|
|
39
|
+
| Contentstack Utils SDK | `skills/contentstack-utils/SKILL.md` | Public API, models, CDA vs GQL paths, versioning |
|
|
40
|
+
| Development workflow | `skills/dev-workflow/SKILL.md` | Branches, Bundler/Rake, gem build, workflows |
|
|
41
|
+
| Framework & packaging | `skills/framework/SKILL.md` | Gemspec, dependencies, Ruby version, release |
|
|
42
|
+
| Ruby style and layout | `skills/ruby-style/SKILL.md` | Module layout, naming, matching existing code |
|
|
43
|
+
| Testing | `skills/testing/SKILL.md` | RSpec layout, mocks, SimpleCov, WebMock |
|
|
44
|
+
|
|
45
|
+
An index with “when to use” hints is in `skills/README.md`.
|
|
46
|
+
|
|
47
|
+
## Using Cursor (optional)
|
|
48
|
+
|
|
49
|
+
If you use **Cursor**, **`.cursor/rules/README.md`** is the only file under `.cursor/rules`; it points to **`AGENTS.md`**—same docs as everyone else.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.2.4](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.2.4) (2026-04-15)
|
|
11
|
+
- Fixed Security issues.
|
|
12
|
+
|
|
3
13
|
## [1.2.3](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.2.3) (2026-03-30)
|
|
4
14
|
- Fixed GQL JSON test helper parsing for hash-based fixtures by serializing Ruby hashes to JSON.
|
|
5
15
|
- Normalized non-doc fragment list fixtures into doc-root shape to keep nested list fragment specs stable.
|
data/Gemfile.lock
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
contentstack_utils (1.
|
|
5
|
-
activesupport (>=
|
|
6
|
-
nokogiri (>= 1.19)
|
|
4
|
+
contentstack_utils (1.3.0)
|
|
5
|
+
activesupport (>= 7.0, < 8)
|
|
6
|
+
nokogiri (~> 1.19, >= 1.19.2)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activesupport (
|
|
11
|
+
activesupport (7.2.3.1)
|
|
12
12
|
base64
|
|
13
|
+
benchmark (>= 0.3)
|
|
13
14
|
bigdecimal
|
|
14
15
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
15
16
|
connection_pool (>= 2.2.5)
|
|
16
17
|
drb
|
|
17
18
|
i18n (>= 1.6, < 2)
|
|
18
|
-
json
|
|
19
19
|
logger (>= 1.4.2)
|
|
20
|
-
minitest (>= 5.1)
|
|
20
|
+
minitest (>= 5.1, < 6)
|
|
21
21
|
securerandom (>= 0.3)
|
|
22
22
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
23
|
-
|
|
24
|
-
addressable (2.8.9)
|
|
23
|
+
addressable (2.9.0)
|
|
25
24
|
public_suffix (>= 2.0.2, < 8.0)
|
|
26
25
|
base64 (0.3.0)
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
benchmark (0.5.0)
|
|
27
|
+
bigdecimal (4.1.1)
|
|
28
|
+
concurrent-ruby (1.3.7)
|
|
29
29
|
connection_pool (3.0.2)
|
|
30
30
|
crack (1.0.1)
|
|
31
31
|
bigdecimal
|
|
@@ -36,17 +36,27 @@ GEM
|
|
|
36
36
|
hashdiff (1.2.1)
|
|
37
37
|
i18n (1.14.8)
|
|
38
38
|
concurrent-ruby (~> 1.0)
|
|
39
|
-
json (2.19.3)
|
|
40
39
|
logger (1.7.0)
|
|
41
|
-
minitest (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
nokogiri (1.19.
|
|
40
|
+
minitest (5.27.0)
|
|
41
|
+
nokogiri (1.19.3-aarch64-linux-gnu)
|
|
42
|
+
racc (~> 1.4)
|
|
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)
|
|
45
56
|
racc (~> 1.4)
|
|
46
|
-
prism (1.9.0)
|
|
47
57
|
public_suffix (7.0.5)
|
|
48
58
|
racc (1.8.1)
|
|
49
|
-
rake (13.
|
|
59
|
+
rake (13.4.2)
|
|
50
60
|
rexml (3.4.4)
|
|
51
61
|
rspec (3.13.2)
|
|
52
62
|
rspec-core (~> 3.13.0)
|
|
@@ -70,16 +80,21 @@ GEM
|
|
|
70
80
|
simplecov_json_formatter (0.1.4)
|
|
71
81
|
tzinfo (2.0.6)
|
|
72
82
|
concurrent-ruby (~> 1.0)
|
|
73
|
-
uri (1.1.1)
|
|
74
83
|
webmock (3.26.2)
|
|
75
84
|
addressable (>= 2.8.0)
|
|
76
85
|
crack (>= 0.3.2)
|
|
77
86
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
78
|
-
yard (0.9.
|
|
87
|
+
yard (0.9.44)
|
|
79
88
|
|
|
80
89
|
PLATFORMS
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
98
|
|
|
84
99
|
DEPENDENCIES
|
|
85
100
|
contentstack_utils!
|
|
@@ -87,7 +102,7 @@ DEPENDENCIES
|
|
|
87
102
|
rspec (~> 3.13)
|
|
88
103
|
simplecov (~> 0.22)
|
|
89
104
|
webmock (~> 3.23)
|
|
90
|
-
yard (
|
|
105
|
+
yard (>= 0.9.44)
|
|
91
106
|
|
|
92
107
|
BUNDLED WITH
|
|
93
|
-
2.
|
|
108
|
+
2.5.22
|
data/contentstack_utils.gemspec
CHANGED
|
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
|
|
|
9
9
|
s.authors = [%q{Contentstack}]
|
|
10
10
|
s.email = ["support@contentstack.com"]
|
|
11
11
|
|
|
12
|
-
s.required_ruby_version = '>= 3.
|
|
12
|
+
s.required_ruby_version = '>= 3.1'
|
|
13
13
|
|
|
14
14
|
s.license = "MIT"
|
|
15
15
|
s.homepage = "https://github.com/contentstack/contentstack-utils-ruby"
|
|
@@ -21,12 +21,12 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
s.test_files = s.files.grep(%r{^spec/})
|
|
22
22
|
s.require_paths = ["lib"]
|
|
23
23
|
|
|
24
|
-
s.add_dependency 'activesupport', '>=
|
|
25
|
-
s.add_dependency 'nokogiri', '>= 1.19'
|
|
24
|
+
s.add_dependency 'activesupport', '>= 7.0', '< 8'
|
|
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', '
|
|
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)
|
data/skills/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Skills – Contentstack Utils Ruby
|
|
2
|
+
|
|
3
|
+
Source of truth for detailed guidance. Read [AGENTS.md](../AGENTS.md) first, then open the skill that matches your task.
|
|
4
|
+
|
|
5
|
+
## When to use which skill
|
|
6
|
+
|
|
7
|
+
| Skill folder | Use when |
|
|
8
|
+
|--------------|----------|
|
|
9
|
+
| `code-review` | Preparing or reviewing a PR |
|
|
10
|
+
| `contentstack-utils` | Changing rendering behavior, JSON RTE / GQL paths, options API, or public `lib/` entry points |
|
|
11
|
+
| `dev-workflow` | Setting up the repo, running tests/docs, opening PRs, understanding CI and release |
|
|
12
|
+
| `framework` | Gemspec, Bundler, Ruby version constraints, activesupport/nokogiri dependencies, gem build/release |
|
|
13
|
+
| `ruby-style` | File layout, modules, or staying consistent with existing Ruby style in this repo |
|
|
14
|
+
| `testing` | Adding or changing specs, fixtures, mocks, or coverage |
|
|
15
|
+
|
|
16
|
+
Each folder contains `SKILL.md` with YAML frontmatter (`name`, `description`).
|