From 563d9531d27c7946bc9f69191fd18f09ad5799f4 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:38:29 +0530 Subject: [PATCH 1/3] Added cursor rules and skill files --- .cursor/rules/README.md | 5 +++ AGENTS.md | 49 ++++++++++++++++++++++++++ skills/README.md | 19 ++++++++++ skills/code-review/SKILL.md | 56 ++++++++++++++++++++++++++++++ skills/contentstack-utils/SKILL.md | 47 +++++++++++++++++++++++++ skills/dev-workflow/SKILL.md | 39 +++++++++++++++++++++ skills/framework/SKILL.md | 35 +++++++++++++++++++ skills/php-style/SKILL.md | 42 ++++++++++++++++++++++ skills/testing/SKILL.md | 38 ++++++++++++++++++++ 9 files changed, 330 insertions(+) create mode 100644 .cursor/rules/README.md create mode 100644 AGENTS.md create mode 100644 skills/README.md create mode 100644 skills/code-review/SKILL.md create mode 100644 skills/contentstack-utils/SKILL.md create mode 100644 skills/dev-workflow/SKILL.md create mode 100644 skills/framework/SKILL.md create mode 100644 skills/php-style/SKILL.md create mode 100644 skills/testing/SKILL.md diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md new file mode 100644 index 00000000..0c75565b --- /dev/null +++ b/.cursor/rules/README.md @@ -0,0 +1,5 @@ +# Cursor (optional) + +**Cursor** users: start at the repo root **[`AGENTS.md`](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`** (universal for any editor or tool). + +This folder only points contributors here so nothing editor-specific duplicates the canonical docs. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b1a6e272 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Contentstack PHP SDK – Agent guide + +**Universal entry point** for anyone automating or assisting work in this repo—AI agents (Cursor, Copilot, CLI tools), reviewers, and contributors. Conventions and detailed guidance live in **`skills/*/SKILL.md`**, not in editor-specific config, so the same instructions apply whether or not you use Cursor. + +## What this repo is + +- **Name:** [contentstack-php](https://github.com/contentstack/contentstack-php) +- **Purpose:** PHP library for the Contentstack **Delivery API**—stack initialization, content types, entries, assets, queries, sync, live preview config, and RTE rendering via the optional **`contentstack/utils`** package. +- **Out of scope:** This package is not the Management API client; it focuses on read/delivery patterns documented in the SDK README. + +## Tech stack (at a glance) + +| Area | Details | +|------|---------| +| Language | PHP (see `composer.json` `require.php`; supports legacy 5.5+ per package) | +| Layout | PSR-4 `Contentstack\` → `src/`; helper `src/Support/helper.php` | +| Tests | PHPUnit 10 → `test/` (`phpunit.xml`) | +| Docs | Doctum → `composer run generate:docs` (`config.php`) | +| Dependency | `contentstack/utils` for `Utils`, `Option`, RTE rendering helpers | + +## Commands (quick reference) + +```bash +composer install +composer test +# or: vendor/bin/phpunit +composer run generate:docs +``` + +Coverage and reports (when configured in `phpunit.xml`) write under `./tmp/`. + +## Where the real documentation lives: skills + +Read these **`SKILL.md` files** for full conventions—**this is the source of truth** for implementation and review: + +| Skill | Path | What it covers | +|-------|------|----------------| +| **Development workflow** | [`skills/dev-workflow/SKILL.md`](skills/dev-workflow/SKILL.md) | Branches, CI expectations, Composer test/docs, PR notes | +| **Contentstack PHP SDK / Utils** | [`skills/contentstack-utils/SKILL.md`](skills/contentstack-utils/SKILL.md) | `Contentstack::Stack`, `Stack`, queries, entries, assets, regions, `contentstack/utils`, semver | +| **PHP style & repo layout** | [`skills/php-style/SKILL.md`](skills/php-style/SKILL.md) | PSR-4, namespaces, docblocks, `src/` layout, PHP version constraints | +| **Testing** | [`skills/testing/SKILL.md`](skills/testing/SKILL.md) | PHPUnit, `test/` layout, constants/helpers, credentials hygiene | +| **Code review** | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist (API, errors, deps/SCA, tests), Blocker/Major/Minor | +| **Framework / build** | [`skills/framework/SKILL.md`](skills/framework/SKILL.md) | `composer.json`, autoload, Doctum, optional CI workflows | + +An index with short “when to use” hints is in [`skills/README.md`](skills/README.md). + +## Using Cursor + +If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—same source of truth as everyone else; no separate `.mdc` rule files. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..d8f3bdc9 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,19 @@ +# Skills – Contentstack PHP SDK + +**This directory is the source of truth** for conventions (workflow, SDK API, style, tests, review, build). Read **`AGENTS.md`** at the repo root for the index and quick commands; each skill is a folder with **`SKILL.md`** (YAML frontmatter: `name`, `description`). + +## When to use which skill + +| Skill folder | Use when | +|--------------|----------| +| **dev-workflow** | Branches, `composer test` / docs, GitHub workflows, PR expectations | +| **contentstack-utils** | `Contentstack`, `Stack`, queries, entries, assets, sync, Composer `contentstack/utils` | +| **php-style** | PSR-4, namespaces, docblocks, `src/` structure, PHP compatibility | +| **testing** | PHPUnit, `test/` helpers, offline vs live tests, secrets | +| **code-review** | PR checklist, Blocker/Major/Minor, API and dependency gates | +| **framework** | `composer.json`, autoload, Doctum, dev dependencies | + +## How to use these docs + +- **Humans / any AI tool:** Start at **`AGENTS.md`**, then open the relevant **`skills//SKILL.md`**. +- **Cursor users:** **`.cursor/rules/README.md`** only points to **`AGENTS.md`** so guidance stays universal—no duplicate `.mdc` rule sets. diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md new file mode 100644 index 00000000..5030c9c2 --- /dev/null +++ b/skills/code-review/SKILL.md @@ -0,0 +1,56 @@ +--- +name: code-review +description: PR checklist—API stability, docs, errors, compatibility, dependencies/SCA, tests; Blocker/Major/Minor. +--- + +# Code review – Contentstack PHP SDK + +## When to use + +- Reviewing a PR, self-review before submit, or automated review prompts. + +## Instructions + +Work through the checklist below. Optionally tag findings: **Blocker**, **Major**, **Minor**. + +### API design and stability + +- [ ] **Public API:** New or changed public methods/classes under `src/` are necessary, semver-conscious, and documented (`README.md` / `CHANGELOG.md` when user-visible). +- [ ] **Backward compatibility:** No breaking changes unless explicitly justified (e.g. major version). Prefer additive options and default config shapes. +- [ ] **Naming:** Matches existing SDK terminology (`Stack`, `ContentType`, `Query`, `Entry`, regions, etc.). + +### Error handling and robustness + +- [ ] **Errors:** New failure paths use or extend `CSException` / domain messages where appropriate; callers get actionable context. +- [ ] **Input:** Validate or document preconditions for public parameters; avoid silent failures on malformed API responses where the SDK should surface errors. +- [ ] **HTTP / JSON:** Parsing stays tolerant of documented Delivery API shapes; regressions for edge payloads are called out. + +### Dependencies and security + +- [ ] **Dependencies:** `composer.json` changes are justified; versions do not introduce known vulnerabilities. +- [ ] **SCA:** Address security findings (e.g. Snyk, org scanners) in the PR or via an agreed follow-up. + +### Testing + +- [ ] **Coverage:** New or modified behavior in `src/` has tests under `test/` when feasible. +- [ ] **Quality:** Tests are readable, deterministic, and follow existing helper/constants patterns. + +### Severity (optional) + +| Level | Examples | +|-------|----------| +| **Blocker** | Breaking public API without approval; security issue; no tests for new code where tests are practical | +| **Major** | Inconsistent errors; README examples that do not match real APIs | +| **Minor** | Style; minor docs | + +### Detailed review themes + +- **API:** Breaking public signatures without semver / CHANGELOG alignment. +- **Errors:** Exception changes that confuse callers without a version strategy. +- **README:** Examples must match real `Contentstack::Stack` and query APIs. +- **Dependencies:** New packages need justification and license awareness. + +## References + +- `skills/testing/SKILL.md` +- `skills/contentstack-utils/SKILL.md` diff --git a/skills/contentstack-utils/SKILL.md b/skills/contentstack-utils/SKILL.md new file mode 100644 index 00000000..194ff54a --- /dev/null +++ b/skills/contentstack-utils/SKILL.md @@ -0,0 +1,47 @@ +--- +name: contentstack-utils +description: PHP Delivery SDK API—Contentstack::Stack, queries, entries, assets, regions, sync; RTE via Composer package contentstack/utils. +--- + +# Contentstack PHP SDK (and Utils integration) + +## When to use + +- Implementing or changing Delivery API behavior, query builders, entries, assets, or sync. +- Updating `README.md` / `CHANGELOG.md` for user-visible behavior. +- Assessing semver impact of public API changes or **`contentstack/utils`** usage. + +## Main entry (consumer API) + +- Consumers use **`Contentstack\Contentstack::Stack(...)`** to obtain a **`Contentstack\Stack\Stack`** instance (API key, delivery token, environment, optional region / branch / live preview config). +- Query and fetch patterns flow through **content types**, **queries**, **entries**, and **assets** under `src/Stack/`. +- **RTE / JSON rendering:** `Contentstack::renderContent` and related helpers delegate to the **`contentstack/utils`** Composer package (`Contentstack\Utils\*`, `Option`). Do not reimplement utils behavior inside this SDK without strong reason. + +## Customization and options + +- Stack **config** array supports region, branch, live preview host/enable flags—preserve backward compatibility when extending keys. +- **ContentstackRegion** (or string region values per existing API) must stay consistent with documented Contentstack regions. + +## Data and HTTP + +- The SDK builds requests to Contentstack Delivery endpoints; keep URL construction, headers, and query parameters aligned with product documentation. +- **Result** parsing should match documented JSON shapes; guard against missing keys where the API allows omission. + +## Errors + +- Use **`Contentstack\Error\CSException`** and **`ErrorMessages`** patterns for domain failures. +- Avoid suppressing errors on public code paths without documentation. + +## Related package + +- **`contentstack/utils`** is a required dependency for RTE features shipped through this repo’s public surface; version ranges in `composer.json` should stay coherent with utils releases. + +## Docs and versioning + +- Follow **semver** for public API changes; update **CHANGELOG** and tag/release process per team norms. + +## References + +- [Contentstack](https://www.contentstack.com/) +- [PHP Delivery SDK docs](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/php) +- `skills/php-style/SKILL.md`, `skills/framework/SKILL.md` diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md new file mode 100644 index 00000000..136ebda7 --- /dev/null +++ b/skills/dev-workflow/SKILL.md @@ -0,0 +1,39 @@ +--- +name: dev-workflow +description: Branches, CI expectations, Composer test/docs, PR hygiene for Contentstack PHP SDK. +--- + +# Development workflow – Contentstack PHP SDK + +## When to use + +- Setting up locally, opening a PR, or aligning with repository automation. +- Answering “how do we run tests?” or “why did the branch check fail?” + +## Branches + +- Use feature branches (e.g. `feat/...`, `fix/...`, ticket branches). +- **`.github/workflows/check-branch.yml`** blocks pull requests into **`master`** unless the head branch is **`staging`** (SRE policy). Prefer PRs to **`staging`** when targeting a master release train. +- **Policy / SCA** workflows may run on PRs (see `.github/workflows/policy-scan.yml`, `sca-scan.yml`). + +## Running tests and build + +- **Install:** `composer install` +- **Tests:** `composer test` or `vendor/bin/phpunit` (config: `phpunit.xml`, suite under `test/`). +- Tests may require Contentstack credentials or mocks as documented in `test/README.md`—do not commit secrets. + +## Documentation generation + +- **Doctum:** `composer run generate:docs` (see `config.php`, output per Doctum defaults). + +## Pull requests + +- Build/tests pass locally where applicable (`composer test`). +- Follow the **code-review** skill (`skills/code-review/SKILL.md`) before merge. +- Prefer backward-compatible public API; call out breaking changes and semver. +- Update **`CHANGELOG.md`** when behavior visible to integrators changes. + +## References + +- `skills/testing/SKILL.md` +- `skills/code-review/SKILL.md` diff --git a/skills/framework/SKILL.md b/skills/framework/SKILL.md new file mode 100644 index 00000000..501b3247 --- /dev/null +++ b/skills/framework/SKILL.md @@ -0,0 +1,35 @@ +--- +name: framework +description: Composer package, PSR-4 autoload, Doctum docs, PHP version constraints, dev dependencies. +--- + +# Framework / build – Contentstack PHP SDK + +## When to use + +- Editing `composer.json`, autoload blocks, or scripts. +- Changing minimum PHP version or required packages (e.g. `contentstack/utils`, PHPUnit). +- Updating documentation generation (`config.php`, Doctum). + +## Composer + +- **Package name:** `contentstack/contentstack` (see `composer.json`). +- **Autoload:** PSR-4 `Contentstack\` → `src/` plus `files` entry for `src/Support/helper.php`—keep in sync when moving classes. +- **Scripts:** `test` → PHPUnit; `generate:docs` → Doctum. + +## PHP version + +- `require.php` sets minimum language level; raising it is a **compatibility decision**—document in CHANGELOG and coordinate releases. + +## Documentation + +- **Doctum** scans `./src` per `config.php`; adjust only when layout changes. + +## CI / automation + +- This repo may not define a dedicated PHPUnit workflow file; still run **`composer test`** before merge. Branch and policy workflows live under `.github/workflows/`. + +## References + +- `skills/php-style/SKILL.md` +- `skills/dev-workflow/SKILL.md` diff --git a/skills/php-style/SKILL.md b/skills/php-style/SKILL.md new file mode 100644 index 00000000..92f89db6 --- /dev/null +++ b/skills/php-style/SKILL.md @@ -0,0 +1,42 @@ +--- +name: php-style +description: PSR-4 layout, namespaces, docblocks, PHP compatibility, src/ structure for this SDK. +--- + +# PHP style and repo layout – Contentstack PHP SDK + +## When to use + +- Editing any PHP under `src/` or `test/`. +- Adding classes or changing namespace boundaries. + +## Layout + +- **Library code:** `src/` with namespace **`Contentstack\...`** matching PSR-4 paths. +- **Stack / Delivery:** `src/Stack/` (e.g. `Stack.php`, `ContentType/`, `Assets.php`, `BaseQuery.php`). +- **Config / errors / support:** `src/Config/`, `src/Error/`, `src/Support/`. +- **Entry point:** `src/Contentstack.php` exposes static factory and utils-facing helpers. + +## PHP language + +- Honor **minimum PHP** from `composer.json`; avoid syntax and standard-library features unavailable on that floor unless the constraint is explicitly raised. +- **`#[\AllowDynamicProperties]`** and legacy patterns may exist for backward compatibility—do not remove without a semver plan. + +## Documentation blocks + +- Existing files use **PHPDoc** (`@param`, `@return`, `@package`, etc.); new public APIs should include equivalent documentation for IDEs and Doctum. + +## Naming + +- **Classes:** `PascalCase` matching file names under PSR-4. +- **Methods:** `camelCase` consistent with surrounding code. +- Preserve established **public** names even when imperfect unless doing a **semver-major** cleanup with maintainer agreement. + +## Imports and visibility + +- Prefer `use` statements for classes in other namespaces; keep **public** surface explicit (public methods on classes intended for consumers). + +## References + +- `skills/framework/SKILL.md` (Composer, autoload) +- `skills/testing/SKILL.md` diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md new file mode 100644 index 00000000..097e2aa8 --- /dev/null +++ b/skills/testing/SKILL.md @@ -0,0 +1,38 @@ +--- +name: testing +description: PHPUnit—test/ layout, helpers, constants, coverage output, secrets hygiene. +--- + +# Testing – Contentstack PHP SDK + +## When to use + +- Adding or changing tests under `test/`. +- Debugging failing tests or coverage gaps. + +## Runner and config + +- **PHPUnit 10** via `vendor/bin/phpunit` or `composer test`. +- Configuration: **`phpunit.xml`**—suite directory `test/`, source coverage for `src/`, reports under `./tmp/` when generated. + +## Layout and naming + +- Tests live as `*Test.php` (e.g. `EntriesTest.php`, `AssetsTest.php`, `SyncTest.php`) alongside helpers such as `constants.php`, `utility.php`, `REST.php`. +- Reuse existing helpers before introducing parallel fixtures. + +## Integration vs unit + +- Some tests may exercise HTTP against Contentstack (see `test/README.md` **Prerequisite**); treat credentials as **local-only** or CI secrets—never commit API keys or tokens. + +## Coverage + +- Clover/HTML/text reports are configured in `phpunit.xml`; ensure `tmp/` (or chosen output dirs) stays out of version control if generated locally (see `.gitignore`). + +## Secrets + +- Tests must not embed real **delivery tokens** or **API keys** in the repository. + +## References + +- `skills/dev-workflow/SKILL.md` +- `skills/code-review/SKILL.md` From 26046ce740b155d310b30efdb7ac9c33894f0155 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 29 Apr 2026 08:13:00 +0530 Subject: [PATCH 2/3] chore: align release workflows with new development-to-main process --- .github/workflows/back-merge-pr.yml | 54 +++++++++++++++ .github/workflows/check-branch.yml | 20 ------ .github/workflows/check-version-bump.yml | 86 ++++++++++++++++++++++++ skills/dev-workflow/SKILL.md | 2 +- 4 files changed, 141 insertions(+), 21 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 00000000..02b378ce --- /dev/null +++ b/.github/workflows/back-merge-pr.yml @@ -0,0 +1,54 @@ +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: ${{ github.token }} + run: | + set -euo pipefail + BASE_BRANCH="development" + SOURCE_BRANCH="master" + + git fetch origin "$BASE_BRANCH" "$SOURCE_BRANCH" + + if ! git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then + echo "Base branch '$BASE_BRANCH' does not exist on origin; skipping." + exit 0 + fi + + SOURCE_SHA=$(git rev-parse "origin/$SOURCE_BRANCH") + BASE_SHA=$(git rev-parse "origin/$BASE_BRANCH") + + if [ "$SOURCE_SHA" = "$BASE_SHA" ]; then + echo "$SOURCE_BRANCH and $BASE_BRANCH are at the same commit; nothing to back-merge." + exit 0 + fi + + EXISTING=$(gh pr list --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --state open --json number --jq 'length') + + if [ "$EXISTING" -gt 0 ]; then + echo "An open PR from $SOURCE_BRANCH to $BASE_BRANCH already exists; skipping." + exit 0 + fi + + gh pr create --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --title "chore: back-merge $SOURCE_BRANCH into $BASE_BRANCH" --body "Automated back-merge after changes landed on \\`$SOURCE_BRANCH\\`. Review and merge to keep \\`$BASE_BRANCH\\` in sync." + + echo "Created back-merge PR $SOURCE_BRANCH -> $BASE_BRANCH." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml deleted file mode 100644 index 2332f0d0..00000000 --- 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 staging 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 staging branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." - exit 1 diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml new file mode 100644 index 00000000..8e710002 --- /dev/null +++ b/.github/workflows/check-version-bump.yml @@ -0,0 +1,86 @@ +name: Check Version Bump + +on: + pull_request: + +jobs: + version-bump: + name: Version & Changelog bump + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed files and version bump + id: detect + run: | + if git rev-parse HEAD^2 >/dev/null 2>&1; then + FILES=$(git diff --name-only HEAD^1 HEAD^2) + else + FILES=$(git diff --name-only HEAD~1 HEAD) + fi + VERSION_FILES_CHANGED=false + echo "$FILES" | grep -qx 'package.json' && VERSION_FILES_CHANGED=true + echo "$FILES" | grep -qx 'CHANGELOG.md' && VERSION_FILES_CHANGED=true + echo "version_files_changed=$VERSION_FILES_CHANGED" >> $GITHUB_OUTPUT + # Only lib/, webpack/, dist/, package.json count as release-affecting; .github/ and test/ do not + CODE_CHANGED=false + echo "$FILES" | grep -qE '^lib/|^webpack/|^dist/' && CODE_CHANGED=true + echo "$FILES" | grep -qx 'package.json' && CODE_CHANGED=true + echo "code_changed=$CODE_CHANGED" >> $GITHUB_OUTPUT + + - name: Skip when only test/docs/.github changed + if: steps.detect.outputs.code_changed != 'true' + run: | + echo "No release-affecting files changed (e.g. only test/docs/.github). Skipping version-bump check." + exit 0 + + - name: Fail when version bump was missed + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed != 'true' + run: | + echo "::error::This PR has code changes but no version bump. Please bump the version in package.json and add an entry in CHANGELOG.md." + exit 1 + + - name: Setup Node + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Check version bump + if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' + run: | + set -e + PKG_VERSION=$(node -p "require('./package.json').version.replace(/^v/, '')") + if [ -z "$PKG_VERSION" ]; then + echo "::error::Could not read version from package.json" + exit 1 + fi + git fetch --tags --force 2>/dev/null || true + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + if [ -z "$LATEST_TAG" ]; then + echo "No existing tags found. Skipping version-bump check (first release)." + exit 0 + fi + LATEST_VERSION="${LATEST_TAG#v}" + LATEST_VERSION="${LATEST_VERSION%%-*}" + if [ "$(printf '%s\n' "$LATEST_VERSION" "$PKG_VERSION" | sort -V | tail -1)" != "$PKG_VERSION" ]; then + echo "::error::Version bump required: package.json version ($PKG_VERSION) is not greater than latest tag ($LATEST_TAG). Please bump the version in package.json." + exit 1 + fi + if [ "$PKG_VERSION" = "$LATEST_VERSION" ]; then + echo "::error::Version bump required: package.json version ($PKG_VERSION) equals latest tag ($LATEST_TAG). Please bump the version in package.json." + exit 1 + fi + CHANGELOG_VERSION=$(sed -nE 's/^## \[v?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "::error::Could not find a version entry in CHANGELOG.md (expected line like '## [v1.0.0](...)')." + exit 1 + fi + if [ "$CHANGELOG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::CHANGELOG version mismatch: CHANGELOG.md top version ($CHANGELOG_VERSION) does not match package.json version ($PKG_VERSION). Please add or update the CHANGELOG entry for $PKG_VERSION." + exit 1 + fi + echo "Version bump check passed: package.json and CHANGELOG.md are at $PKG_VERSION (latest tag: $LATEST_TAG)." diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md index 136ebda7..7311c71e 100644 --- a/skills/dev-workflow/SKILL.md +++ b/skills/dev-workflow/SKILL.md @@ -13,7 +13,7 @@ description: Branches, CI expectations, Composer test/docs, PR hygiene for Conte ## Branches - Use feature branches (e.g. `feat/...`, `fix/...`, ticket branches). -- **`.github/workflows/check-branch.yml`** blocks pull requests into **`master`** unless the head branch is **`staging`** (SRE policy). Prefer PRs to **`staging`** when targeting a master release train. +- Release flow is direct **`development` -> `master`** (no `staging` intermediate branch). - **Policy / SCA** workflows may run on PRs (see `.github/workflows/policy-scan.yml`, `sca-scan.yml`). ## Running tests and build From 4fd8b01b62515c54ddcb40a42c80d7a9451157e9 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:44:45 +0530 Subject: [PATCH 3/3] Added support for endpoint integration --- .gitignore | 3 +- composer.json | 5 +- composer.lock | 564 +++++++++++++++++++++-------------- scripts/download-regions.php | 79 +++++ src/Contentstack.php | 21 +- src/ContentstackRegion.php | 12 +- src/Endpoint.php | 229 ++++++++++++++ src/Stack/Stack.php | 33 +- test/EndpointTest.php | 334 +++++++++++++++++++++ 9 files changed, 1046 insertions(+), 234 deletions(-) create mode 100644 scripts/download-regions.php create mode 100644 src/Endpoint.php create mode 100644 test/EndpointTest.php diff --git a/.gitignore b/.gitignore index 041dc5af..acbb377a 100755 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ tmp/ test/result.json stdout build -cache \ No newline at end of file +cache +src/assets/regions.json \ No newline at end of file diff --git a/composer.json b/composer.json index 3c999aaf..c2361934 100644 --- a/composer.json +++ b/composer.json @@ -30,8 +30,11 @@ "code-lts/doctum": "^5.3" }, "scripts": { + "post-install-cmd": ["@php scripts/download-regions.php"], + "post-update-cmd": ["@php scripts/download-regions.php"], "generate:docs": "vendor/bin/doctum.php update ./config.php", - "test": "vendor/bin/phpunit" + "test": "vendor/bin/phpunit", + "refresh-regions": "@php scripts/download-regions.php" }, "require": { "php" : ">=5.5.0", diff --git a/composer.lock b/composer.lock index a1db1410..b26b624d 100644 --- a/composer.lock +++ b/composer.lock @@ -146,26 +146,26 @@ "packages-dev": [ { "name": "code-lts/cli-tools", - "version": "v1.6.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/code-lts/cli-tools.git", - "reference": "399bb23041a3d3d9dbf340f5c7c6ceb129fc65ae" + "reference": "d4e4f00060468b34c129f4a5424171b4a43a9d82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/code-lts/cli-tools/zipball/399bb23041a3d3d9dbf340f5c7c6ceb129fc65ae", - "reference": "399bb23041a3d3d9dbf340f5c7c6ceb129fc65ae", + "url": "https://api.github.com/repos/code-lts/cli-tools/zipball/d4e4f00060468b34c129f4a5424171b4a43a9d82", + "reference": "d4e4f00060468b34c129f4a5424171b4a43a9d82", "shasum": "" }, "require": { "ondram/ci-detector": "^4.0", - "php": "^7.2 || ^8.0", - "symfony/console": "^5 || ^6 || ^7" + "php": "^8.1", + "symfony/console": "^6.4.27 || ^7.3 || ^8" }, "require-dev": { - "phpstan/phpstan": "^1.4.6", - "phpunit/phpunit": "^8 || ^9 || ^10|| ^11", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5.60 || ^11 || ^12.4", "wdes/coding-standard": "^3.2" }, "type": "library", @@ -206,42 +206,42 @@ "issues": "https://github.com/code-lts/cli-tools/issues", "source": "https://github.com/code-lts/cli-tools" }, - "time": "2024-08-20T10:39:29+00:00" + "time": "2026-01-22T09:28:58+00:00" }, { "name": "code-lts/doctum", - "version": "v5.5.4", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/code-lts/doctum.git", - "reference": "fbece8ccb38b863b487672ce19f40081c0a75655" + "reference": "fc3cd98ab569f8ad3f060010a2f627c1777409bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/code-lts/doctum/zipball/fbece8ccb38b863b487672ce19f40081c0a75655", - "reference": "fbece8ccb38b863b487672ce19f40081c0a75655", + "url": "https://api.github.com/repos/code-lts/doctum/zipball/fc3cd98ab569f8ad3f060010a2f627c1777409bf", + "reference": "fc3cd98ab569f8ad3f060010a2f627c1777409bf", "shasum": "" }, "require": { - "code-lts/cli-tools": "^1.4.0", - "erusev/parsedown": "^1.7", - "nikic/php-parser": "^4.10", - "php": "^7.2.20 || ^8.0", - "phpdocumentor/reflection-docblock": "~5.3", - "phpdocumentor/type-resolver": "1.6.*", - "symfony/console": "~3.4|~4.3|^5|^6", - "symfony/filesystem": "~3.4|~4.3|^5|^6", - "symfony/finder": "~3.4|~4.3|^5|^6", - "symfony/process": "~3.4|~4.3|^5|^6", - "symfony/yaml": "~3.4|~4.3|^5|^6", - "twig/twig": "^3.0", - "wdes/php-i18n-l10n": "^4.0" + "code-lts/cli-tools": "^1.7", + "nikic/php-parser": "^5.6.2", + "parsedown/parsedown": "^1.7.5", + "php": "^8.1", + "phpdocumentor/reflection-docblock": "^6.0.1", + "symfony/console": "^6.4.27||^7.3||^8", + "symfony/filesystem": "^6.4.24||^7.3||^8", + "symfony/finder": "^6.4.27||^7.3||^8", + "symfony/process": "^6.4.26||^7.3||^8", + "symfony/string": "^6.4.26||^7.3||^8", + "symfony/yaml": "^6.4.26||^7.3||^8", + "twig/twig": "^3.22", + "wdes/php-i18n-l10n": "^4.2" }, "require-dev": { - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7 || ^8 || ^9", - "wdes/coding-standard": "^3.3.1" + "phpstan/phpstan": "^2.1.36", + "phpstan/phpstan-phpunit": "^2.0.11", + "phpunit/phpunit": "^10.5 || ^11 || ^12.5.6", + "wdes/coding-standard": "^3.3.2" }, "bin": [ "bin/doctum.php" @@ -292,57 +292,55 @@ "type": "tidelift" } ], - "time": "2023-12-16T22:06:08+00:00" + "time": "2026-02-06T13:20:42+00:00" }, { - "name": "erusev/parsedown", - "version": "1.7.4", + "name": "doctrine/deprecations", + "version": "1.1.6", "source": { "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=5.3.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "phpunit/phpunit": "^4.8.35" + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" }, "type": "library", "autoload": { - "psr-0": { - "Parsedown": "" + "psr-4": { + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", - "keywords": [ - "markdown", - "parser" - ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", "support": { - "issues": "https://github.com/erusev/parsedown/issues", - "source": "https://github.com/erusev/parsedown/tree/1.7.x" + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2019-12-30T22:54:17+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "myclabs/deep-copy", @@ -406,30 +404,37 @@ }, { "name": "nikic/php-parser", - "version": "v4.19.5", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", - "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, "autoload": { "psr-4": { "PhpParser\\": "lib/PhpParser" @@ -451,9 +456,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-12-06T11:45:25+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "ondram/ci-detector", @@ -533,6 +538,56 @@ }, "time": "2024-03-12T13:22:30+00:00" }, + { + "name": "parsedown/parsedown", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/parsedown/parsedown.git", + "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/parsedown/parsedown/zipball/96baaad00f71ba04d76e45b4620f54d3beabd6f7", + "reference": "96baaad00f71ba04d76e45b4620f54d3beabd6f7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5|^8.5|^9.6" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/parsedown/parsedown/issues", + "source": "https://github.com/parsedown/parsedown/tree/1.8.0" + }, + "time": "2026-02-16T11:41:01+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -706,28 +761,36 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -751,47 +814,50 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d" + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/48f445a408c131e38cab1c235aa6d2bb7a0bb20d", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.0", "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -812,9 +878,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2022-10-14T12:47:21+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { "name": "phpmyadmin/twig-i18n-extension", @@ -872,6 +938,53 @@ }, "time": "2025-09-27T16:32:20+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", @@ -1195,16 +1308,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.60", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", - "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -1225,7 +1338,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -1276,7 +1389,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -1300,7 +1413,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T07:50:42+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "psr/container", @@ -1525,16 +1638,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -1590,7 +1703,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -1610,7 +1723,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -2310,47 +2423,47 @@ }, { "name": "symfony/console", - "version": "v6.4.31", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f9f8a889f54c264f9abac3fc0f7a371ffca51997", - "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2384,7 +2497,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.31" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -2404,20 +2517,20 @@ "type": "tidelift" } ], - "time": "2025-12-22T08:30:34+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -2430,7 +2543,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2455,7 +2568,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -2466,34 +2579,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.30", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/441c6b69f7222aadae7cbf5df588496d5ee37789", - "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2521,7 +2638,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.30" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -2541,27 +2658,27 @@ "type": "tidelift" } ], - "time": "2025-11-26T14:43:45+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", - "version": "v6.4.31", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", - "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2589,7 +2706,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.31" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -2609,20 +2726,20 @@ "type": "tidelift" } ], - "time": "2025-12-11T14:52:17+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2672,7 +2789,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -2692,20 +2809,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -2754,7 +2871,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -2774,20 +2891,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -2839,7 +2956,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -2859,20 +2976,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { @@ -2924,7 +3041,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -2944,24 +3061,24 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/process", - "version": "v6.4.31", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8541b7308fca001320e90bca8a73a28aa5604a6e", - "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -2989,7 +3106,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.31" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -3009,20 +3126,20 @@ "type": "tidelift" } ], - "time": "2025-12-15T19:26:35+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -3040,7 +3157,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3076,7 +3193,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -3096,20 +3213,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v7.4.0", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", "shasum": "" }, "require": { @@ -3167,7 +3284,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.0" + "source": "https://github.com/symfony/string/tree/v7.4.13" }, "funding": [ { @@ -3187,32 +3304,32 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-05-23T15:23:29+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.30", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331" + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/8207ae83da19ee3748d6d4f567b4d9a7c656e331", - "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -3243,7 +3360,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.30" + "source": "https://github.com/symfony/yaml/tree/v7.4.13" }, "funding": [ { @@ -3263,7 +3380,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T11:50:18+00:00" + "time": "2026-05-25T06:06:12+00:00" }, { "name": "theseer/tokenizer", @@ -3317,16 +3434,16 @@ }, { "name": "twig/twig", - "version": "v3.22.2", + "version": "v3.27.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2" + "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2", - "reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", + "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", "shasum": "" }, "require": { @@ -3336,7 +3453,8 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -3380,7 +3498,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.22.2" + "source": "https://github.com/twigphp/Twig/tree/v3.27.1" }, "funding": [ { @@ -3392,7 +3510,7 @@ "type": "tidelift" } ], - "time": "2025-12-14T11:28:47+00:00" + "time": "2026-05-30T17:09:26+00:00" }, { "name": "wdes/php-i18n-l10n", @@ -3455,23 +3573,23 @@ }, { "name": "webmozart/assert", - "version": "1.12.1", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", "shasum": "" }, "require": { "ext-ctype": "*", "ext-date": "*", "ext-filter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.2" }, "suggest": { "ext-intl": "", @@ -3480,8 +3598,12 @@ }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -3497,6 +3619,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -3507,9 +3633,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.1" + "source": "https://github.com/webmozarts/assert/tree/2.4.0" }, - "time": "2025-10-29T15:56:20+00:00" + "time": "2026-05-20T13:07:01+00:00" } ], "aliases": [], @@ -3521,5 +3647,5 @@ "php": ">=5.5.0" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/scripts/download-regions.php b/scripts/download-regions.php new file mode 100644 index 00000000..99e563eb --- /dev/null +++ b/scripts/download-regions.php @@ -0,0 +1,79 @@ + true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($response !== false && $httpCode === 200) { + $data = $response; + } elseif ($curlError) { + fwrite(STDERR, "contentstack/contentstack: curl error: {$curlError}\n"); + } +} + +// --- Attempt 2: file_get_contents fallback ---------------------------------- +if ($data === null) { + $ctx = stream_context_create([ + 'http' => [ + 'timeout' => 30, + 'ignore_errors' => false, + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + ]); + $data = @file_get_contents($url, false, $ctx); +} + +// --- Validate and write ----------------------------------------------------- +if ($data === false || $data === null) { + fwrite(STDERR, "contentstack/contentstack: Warning — could not download regions.json. " . + "The SDK will attempt to download it at runtime on first use.\n"); + exit(0); // non-fatal: runtime fallback in Endpoint::loadRegions() handles it +} + +$decoded = json_decode($data, true); +if (!is_array($decoded) || !isset($decoded['regions']) || !is_array($decoded['regions'])) { + fwrite(STDERR, "contentstack/contentstack: Warning — downloaded data is not a valid regions.json.\n"); + exit(0); +} + +if (file_put_contents($dest, $data) === false) { + fwrite(STDERR, "contentstack/contentstack: Warning — could not write regions.json to {$dest}.\n"); + exit(0); +} + +$regionCount = count($decoded['regions']); +echo "contentstack/contentstack: regions.json downloaded ({$regionCount} regions).\n"; diff --git a/src/Contentstack.php b/src/Contentstack.php index 8ac41b41..b818a493 100755 --- a/src/Contentstack.php +++ b/src/Contentstack.php @@ -14,6 +14,7 @@ */ namespace Contentstack; +use Contentstack\Endpoint; use Contentstack\Stack\Stack; use Contentstack\Utils\Utils; use Contentstack\Utils\Model\Option; @@ -53,7 +54,25 @@ public static function Stack($api_key = '', return new Stack($api_key, $access_token, $environment, $config); } - public static function renderContent(string $content, Option $option): string + /** + * Resolve a Contentstack service endpoint URL for a given region. + * + * @param string $region Region ID or alias (e.g. 'us', 'eu', 'azure-na', 'gcp-eu'). + * @param string $service Optional service key (e.g. 'contentDelivery', 'contentManagement'). + * When empty, all endpoints for the region are returned as an array. + * @param bool $omitHttps When true, strips the 'https://' prefix from returned URL(s). + * + * @return string|array + */ + public static function getContentstackEndpoint( + string $region = 'us', + string $service = '', + bool $omitHttps = false + ) { + return Endpoint::getContentstackEndpoint($region, $service, $omitHttps); + } + + public static function renderContent(string $content, Option $option): string { return Utils::renderContent($content, $option); } diff --git a/src/ContentstackRegion.php b/src/ContentstackRegion.php index bf99ea99..abfe28a3 100755 --- a/src/ContentstackRegion.php +++ b/src/ContentstackRegion.php @@ -27,9 +27,11 @@ * */ class ContentstackRegion { - const EU= "eu"; - const US= "us"; - const AZURE_NA= "azure-na"; - const AZURE_EU= "azure-eu"; - const GCP_NA= "gcp-na"; + const US = "us"; + const EU = "eu"; + const AU = "au"; + const AZURE_NA = "azure-na"; + const AZURE_EU = "azure-eu"; + const GCP_NA = "gcp-na"; + const GCP_EU = "gcp-eu"; } \ No newline at end of file diff --git a/src/Endpoint.php b/src/Endpoint.php new file mode 100644 index 00000000..fc3a9488 --- /dev/null +++ b/src/Endpoint.php @@ -0,0 +1,229 @@ +|null */ + private static $regionsData = null; + + /** @var string */ + const REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'; + + /** + * Resolve a Contentstack service endpoint URL for a given region. + * + * @param string $region Region ID or alias (e.g. 'us', 'eu', 'azure-na', 'gcp-eu'). + * Defaults to 'us' (AWS North America). + * @param string $service Optional service key (e.g. 'contentDelivery', + * 'contentManagement', 'auth', 'graphqlDelivery'). + * When empty, all endpoints for the region are returned. + * @param bool $omitHttps When true, strips the 'https://' prefix from every URL. + * + * @return string|array Single URL string when $service is provided, + * associative array of all service URLs otherwise. + * + * @throws \InvalidArgumentException When region is empty, unknown, or service is not found. + * @throws \RuntimeException When regions.json cannot be read or parsed. + */ + public static function getContentstackEndpoint( + string $region = 'us', + string $service = '', + bool $omitHttps = false + ) { + if ($region === '') { + throw new \InvalidArgumentException( + 'Empty region provided. Please put valid region.' + ); + } + + $data = self::loadRegions(); + $normalized = strtolower(trim($region)); + $regionRow = self::findRegionByIdOrAlias($data['regions'], $normalized); + + if ($regionRow === null) { + throw new \InvalidArgumentException("Invalid region: {$region}"); + } + + if ($service !== '') { + if (!array_key_exists($service, $regionRow['endpoints'])) { + throw new \InvalidArgumentException( + "Service \"{$service}\" not found for region \"{$regionRow['id']}\"" + ); + } + $url = $regionRow['endpoints'][$service]; + return $omitHttps ? self::stripHttps($url) : $url; + } + + $endpoints = $regionRow['endpoints']; + return $omitHttps ? self::stripHttpsFromMap($endpoints) : $endpoints; + } + + /** + * Load and cache regions.json. + * + * Resolution order: + * 1. In-memory static cache (zero I/O after first call) + * 2. src/assets/regions.json on disk (written by composer install script) + * 3. Live download from artifacts.contentstack.com (fallback) + * + * @return array + */ + private static function loadRegions(): array + { + if (self::$regionsData !== null) { + return self::$regionsData; + } + + $path = __DIR__ . '/assets/regions.json'; + + if (!file_exists($path)) { + self::downloadAndSave($path); + } + + if (!file_exists($path)) { + throw new \RuntimeException( + 'contentstack/contentstack: regions.json not found and could not be downloaded. ' . + 'Run "composer install" or "composer refresh-regions" and ensure network access.' + ); + } + + $raw = file_get_contents($path); + if ($raw === false) { + throw new \RuntimeException( + 'contentstack/contentstack: Could not read regions.json.' + ); + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded) || !isset($decoded['regions'])) { + throw new \RuntimeException( + 'contentstack/contentstack: regions.json is corrupt. ' . + 'Run "composer refresh-regions" to re-download it.' + ); + } + + self::$regionsData = $decoded; + return self::$regionsData; + } + + /** + * Download regions.json from the Contentstack CDN and save to disk. + * Tries the PHP curl extension first, falls back to file_get_contents. + * Silent on failure — the caller decides whether a missing file is fatal. + * + * @param string $dest Absolute path to write the file to. + */ + private static function downloadAndSave(string $dest): void + { + $dir = dirname($dest); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $data = null; + + if (extension_loaded('curl')) { + $ch = curl_init(self::REGIONS_URL); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($response !== false && $httpCode === 200) { + $data = $response; + } + } + + if ($data === null) { + $ctx = stream_context_create(['http' => ['timeout' => 30]]); + $data = @file_get_contents(self::REGIONS_URL, false, $ctx); + } + + if (!$data) { + return; + } + + $decoded = json_decode($data, true); + if (is_array($decoded) && isset($decoded['regions'])) { + file_put_contents($dest, $data); + } + } + + /** + * Find a region entry by its id or any alias (case-insensitive). + * + * @param array> $regions + * @param string $input Already lowercased input. + * @return array|null + */ + private static function findRegionByIdOrAlias(array $regions, string $input): ?array + { + foreach ($regions as $row) { + if ($row['id'] === $input) { + return $row; + } + } + foreach ($regions as $row) { + foreach ($row['alias'] as $alias) { + if (strtolower($alias) === $input) { + return $row; + } + } + } + return null; + } + + /** + * Strip the https:// (or http://) scheme from a URL string. + */ + private static function stripHttps(string $url): string + { + return (string) preg_replace('/^https?:\/\//', '', $url); + } + + /** + * Strip https:// from every value in an endpoint map. + * + * @param array $endpoints + * @return array + */ + private static function stripHttpsFromMap(array $endpoints): array + { + $result = []; + foreach ($endpoints as $key => $url) { + $result[$key] = self::stripHttps($url); + } + return $result; + } + + /** + * Reset the internal region cache (intended for testing only). + */ + public static function resetCache(): void + { + self::$regionsData = null; + } +} diff --git a/src/Stack/Stack.php b/src/Stack/Stack.php index c24ef04f..e3eb5d78 100755 --- a/src/Stack/Stack.php +++ b/src/Stack/Stack.php @@ -14,6 +14,7 @@ * */ namespace Contentstack\Stack; +use Contentstack\Endpoint; use Contentstack\Support\Utility; use Contentstack\Stack\ContentType; use Contentstack\Stack\Assets; @@ -54,15 +55,33 @@ class Stack * * */ public function __construct( - $api_key = '', - $delivery_token = '', - $environment = '', + $api_key = '', + $delivery_token = '', + $environment = '', $config = array('region'=> 'us', 'branch'=> '', 'live_preview' => array()) ) { $previewHost = 'api.contentstack.io'; - if ($config && $config !== "undefined" && array_key_exists('region', $config) && $config['region'] !== "undefined" && $config['region'] !== "us" ) { - $this->host = $config['region'].'-cdn.contentstack.com'; - $previewHost = $config['region'].'-api.contentstack.com'; + + $region = (is_array($config) && isset($config['region']) && $config['region'] !== 'undefined') + ? (string) $config['region'] + : 'us'; + + // Explicit host in config takes precedence over region-derived host. + if (is_array($config) && !empty($config['host'])) { + $this->host = $config['host']; + $previewHost = !empty($config['previewHost']) ? $config['previewHost'] : $previewHost; + } else { + try { + $this->host = Endpoint::getContentstackEndpoint($region, 'contentDelivery', true); + $previewHost = Endpoint::getContentstackEndpoint($region, 'contentManagement', true); + } catch (\InvalidArgumentException $e) { + // Unknown region — fall back to the legacy pattern so custom-region + // code written before this feature was added continues to work. + if ($region !== 'us' && $region !== '') { + $this->host = $region . '-cdn.contentstack.com'; + $previewHost = $region . '-api.contentstack.com'; + } + } } $this->header = Utility::validateInput( 'stack', array('api_key' => $api_key, @@ -74,7 +93,7 @@ public function __construct( $this->environment = $this->header['environment']; unset($this->header['environment']); $livePreview = array('enable' => false, 'host' => $previewHost); - $this->live_preview = $config['live_preview'] ? array_merge($livePreview, $config['live_preview']) : $livePreview; + $this->live_preview = (!empty($config['live_preview'])) ? array_merge($livePreview, $config['live_preview']) : $livePreview; $this->proxy = array_key_exists("proxy",$config) ? $config['proxy'] : array('proxy'=>array()); $this->timeout = array_key_exists("timeout",$config) ? $config['timeout'] : '3000'; $this->retryDelay = array_key_exists("retryDelay",$config) ? $config['retryDelay'] : '3000'; diff --git a/test/EndpointTest.php b/test/EndpointTest.php new file mode 100644 index 00000000..5b3a5591 --- /dev/null +++ b/test/EndpointTest.php @@ -0,0 +1,334 @@ +assertIsArray($endpoints); + $this->assertArrayHasKey('contentDelivery', $endpoints); + $this->assertArrayHasKey('contentManagement', $endpoints); + } + + public function testDefaultRegionContentDelivery(): void + { + $url = Endpoint::getContentstackEndpoint('us', 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function testDefaultRegionContentManagement(): void + { + $url = Endpoint::getContentstackEndpoint('us', 'contentManagement'); + $this->assertSame('https://api.contentstack.io', $url); + } + + // ------------------------------------------------------------------------- + // Region aliases resolve to the same region + // ------------------------------------------------------------------------- + + /** + * @dataProvider naAliasProvider + */ + public function testNaRegionAliasesResolveToSameEndpoint(string $alias): void + { + $url = Endpoint::getContentstackEndpoint($alias, 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public static function naAliasProvider(): array + { + return [ + 'id na' => ['na'], + 'alias us' => ['us'], + 'alias aws-na' => ['aws-na'], + 'alias aws_na' => ['aws_na'], + 'upper NA' => ['NA'], + 'upper US' => ['US'], + ]; + } + + // ------------------------------------------------------------------------- + // All seven regions — contentDelivery spot-checks + // ------------------------------------------------------------------------- + + /** + * @dataProvider regionContentDeliveryProvider + */ + public function testContentDeliveryUrlByRegion(string $region, string $expected): void + { + $url = Endpoint::getContentstackEndpoint($region, 'contentDelivery'); + $this->assertSame($expected, $url); + } + + public static function regionContentDeliveryProvider(): array + { + return [ + 'na' => ['na', 'https://cdn.contentstack.io'], + 'eu' => ['eu', 'https://eu-cdn.contentstack.com'], + 'au' => ['au', 'https://au-cdn.contentstack.com'], + 'azure-na' => ['azure-na', 'https://azure-na-cdn.contentstack.com'], + 'azure-eu' => ['azure-eu', 'https://azure-eu-cdn.contentstack.com'], + 'gcp-na' => ['gcp-na', 'https://gcp-na-cdn.contentstack.com'], + 'gcp-eu' => ['gcp-eu', 'https://gcp-eu-cdn.contentstack.com'], + ]; + } + + /** + * @dataProvider regionContentManagementProvider + */ + public function testContentManagementUrlByRegion(string $region, string $expected): void + { + $url = Endpoint::getContentstackEndpoint($region, 'contentManagement'); + $this->assertSame($expected, $url); + } + + public static function regionContentManagementProvider(): array + { + return [ + 'na' => ['na', 'https://api.contentstack.io'], + 'eu' => ['eu', 'https://eu-api.contentstack.com'], + 'au' => ['au', 'https://au-api.contentstack.com'], + 'azure-na' => ['azure-na', 'https://azure-na-api.contentstack.com'], + 'azure-eu' => ['azure-eu', 'https://azure-eu-api.contentstack.com'], + 'gcp-na' => ['gcp-na', 'https://gcp-na-api.contentstack.com'], + 'gcp-eu' => ['gcp-eu', 'https://gcp-eu-api.contentstack.com'], + ]; + } + + // ------------------------------------------------------------------------- + // ContentstackRegion constants resolve correctly + // ------------------------------------------------------------------------- + + public function testRegionConstantUS(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::US, 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function testRegionConstantEU(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::EU, 'contentDelivery'); + $this->assertSame('https://eu-cdn.contentstack.com', $url); + } + + public function testRegionConstantAU(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::AU, 'contentDelivery'); + $this->assertSame('https://au-cdn.contentstack.com', $url); + } + + public function testRegionConstantAzureNA(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::AZURE_NA, 'contentDelivery'); + $this->assertSame('https://azure-na-cdn.contentstack.com', $url); + } + + public function testRegionConstantAzureEU(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::AZURE_EU, 'contentDelivery'); + $this->assertSame('https://azure-eu-cdn.contentstack.com', $url); + } + + public function testRegionConstantGcpNA(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::GCP_NA, 'contentDelivery'); + $this->assertSame('https://gcp-na-cdn.contentstack.com', $url); + } + + public function testRegionConstantGcpEU(): void + { + $url = Endpoint::getContentstackEndpoint(ContentstackRegion::GCP_EU, 'contentDelivery'); + $this->assertSame('https://gcp-eu-cdn.contentstack.com', $url); + } + + // ------------------------------------------------------------------------- + // omitHttps flag + // ------------------------------------------------------------------------- + + public function testOmitHttpsStripsSchemeFromSingleService(): void + { + $url = Endpoint::getContentstackEndpoint('eu', 'contentDelivery', true); + $this->assertSame('eu-cdn.contentstack.com', $url); + } + + public function testOmitHttpsStripsSchemeFromAllServices(): void + { + $endpoints = Endpoint::getContentstackEndpoint('na', '', true); + $this->assertIsArray($endpoints); + foreach ($endpoints as $key => $url) { + $this->assertStringNotContainsString('https://', $url, "Service {$key} still has https://"); + $this->assertStringNotContainsString('http://', $url, "Service {$key} still has http://"); + } + } + + public function testOmitHttpsFalseRetainsScheme(): void + { + $url = Endpoint::getContentstackEndpoint('na', 'contentManagement', false); + $this->assertStringStartsWith('https://', $url); + } + + // ------------------------------------------------------------------------- + // Return-all-endpoints (no service) + // ------------------------------------------------------------------------- + + public function testNoServiceReturnsArray(): void + { + $result = Endpoint::getContentstackEndpoint('au'); + $this->assertIsArray($result); + $this->assertGreaterThan(1, count($result)); + } + + public function testNoServiceContainsCorrectUrls(): void + { + $endpoints = Endpoint::getContentstackEndpoint('au'); + $this->assertSame('https://au-cdn.contentstack.com', $endpoints['contentDelivery']); + $this->assertSame('https://au-api.contentstack.com', $endpoints['contentManagement']); + } + + // ------------------------------------------------------------------------- + // Case-insensitive alias matching + // ------------------------------------------------------------------------- + + public function testUppercaseAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('AWS-NA', 'contentDelivery'); + $this->assertSame('https://cdn.contentstack.io', $url); + } + + public function testUnderscoreAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('azure_na', 'contentDelivery'); + $this->assertSame('https://azure-na-cdn.contentstack.com', $url); + } + + public function testGcpUnderscoreAliasResolves(): void + { + $url = Endpoint::getContentstackEndpoint('gcp_eu', 'contentManagement'); + $this->assertSame('https://gcp-eu-api.contentstack.com', $url); + } + + // ------------------------------------------------------------------------- + // Error cases + // ------------------------------------------------------------------------- + + public function testEmptyRegionThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Empty region provided'); + Endpoint::getContentstackEndpoint(''); + } + + public function testUnknownRegionThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid region: invalid-region'); + Endpoint::getContentstackEndpoint('invalid-region'); + } + + public function testUnknownServiceThrowsInvalidArgument(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Service "unknownService" not found'); + Endpoint::getContentstackEndpoint('na', 'unknownService'); + } + + // ------------------------------------------------------------------------- + // Contentstack::getContentstackEndpoint() proxy + // ------------------------------------------------------------------------- + + public function testContentstackProxyReturnsSameResult(): void + { + $viaEndpoint = Endpoint::getContentstackEndpoint('eu', 'contentDelivery'); + $viaProxy = Contentstack::getContentstackEndpoint('eu', 'contentDelivery'); + $this->assertSame($viaEndpoint, $viaProxy); + } + + public function testContentstackProxyDefaultRegion(): void + { + $url = Contentstack::getContentstackEndpoint('us', 'contentManagement'); + $this->assertSame('https://api.contentstack.io', $url); + } + + public function testContentstackProxyOmitHttps(): void + { + $url = Contentstack::getContentstackEndpoint('gcp-na', 'contentDelivery', true); + $this->assertSame('gcp-na-cdn.contentstack.com', $url); + } + + public function testContentstackProxyAllEndpoints(): void + { + $endpoints = Contentstack::getContentstackEndpoint('azure-eu'); + $this->assertIsArray($endpoints); + $this->assertArrayHasKey('contentDelivery', $endpoints); + } + + // ------------------------------------------------------------------------- + // Stack host resolution via Endpoint + // ------------------------------------------------------------------------- + + public function testStackUsHostResolvesToDefaultCdn(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'us']); + $this->assertSame('cdn.contentstack.io', $stack->getHost()); + } + + public function testStackEuHostResolvesViaEndpoint(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'eu']); + $this->assertSame('eu-cdn.contentstack.com', $stack->getHost()); + } + + public function testStackAuHostResolvesViaEndpoint(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'au']); + $this->assertSame('au-cdn.contentstack.com', $stack->getHost()); + } + + public function testStackAzureNaHostResolvesViaEndpoint(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'azure-na']); + $this->assertSame('azure-na-cdn.contentstack.com', $stack->getHost()); + } + + public function testStackGcpEuHostResolvesViaEndpoint(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'gcp-eu']); + $this->assertSame('gcp-eu-cdn.contentstack.com', $stack->getHost()); + } + + public function testStackRegionConstantAuResolvesCorrectly(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => ContentstackRegion::AU]); + $this->assertSame('au-cdn.contentstack.com', $stack->getHost()); + } + + public function testStackExplicitHostOverridesRegion(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', [ + 'region' => 'eu', + 'host' => 'custom.cdn.example.com', + ]); + $this->assertSame('custom.cdn.example.com', $stack->getHost()); + } + + public function testStackSetHostStillWorks(): void + { + $stack = Contentstack::Stack('api_key', 'delivery_token', 'env', ['region' => 'eu']); + $stack->setHost('override.cdn.example.com'); + $this->assertSame('override.cdn.example.com', $stack->getHost()); + } +}