Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Files excluded from `composer archive` / Packagist distribution.
# Consumers installing via `composer require` only get src/ + composer.json
# + LICENSE + README + CHANGELOG — dev tooling stays in the repo.

/tests export-ignore
/examples export-ignore
/.github export-ignore
/phpunit.xml.dist export-ignore
/phpstan.neon.dist export-ignore
/.php-cs-fixer.dist.php export-ignore
/.php-version export-ignore
/.gitignore export-ignore
/.gitattributes export-ignore
199 changes: 199 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build:
name: PHP ${{ matrix.php }} — install + cs-check + phpstan + phpunit + smoke
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Matches composer.json `php: >=8.1`. 8.1 is our floor; 8.3 is
# current stable; 8.4 is latest.
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
extensions: json, mbstring, curl, openssl
coverage: none
tools: composer:v2

- name: composer.json → Packagist only (no private-registry drift)
run: |
# composer.json must not declare arbitrary `repositories` — that
# bypasses Packagist. `type: composer` at packagist.org is the
# single default we allow.
if command -v jq >/dev/null 2>&1; then
bad=$(jq -c '.repositories // [] | map(select(.type != null and .type != "composer"))' composer.json)
if [ "$bad" != "[]" ] && [ -n "$bad" ]; then
echo "::error::composer.json has non-Packagist repositories: $bad"
exit 1
fi
fi

- name: Cache composer deps
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: vendor
key: composer-${{ matrix.php }}-${{ hashFiles('composer.json', 'composer.lock') }}
restore-keys: |
composer-${{ matrix.php }}-

- name: composer install
run: composer install --no-interaction --no-progress --prefer-dist

- name: PHP-CS-Fixer (dry-run)
run: vendor/bin/php-cs-fixer fix --dry-run --diff --no-interaction

- name: PHPStan
run: vendor/bin/phpstan analyse --no-progress

- name: PHPUnit
run: vendor/bin/phpunit --testdox --no-coverage

# Smoke: autoload the package and construct a Client. Catches broken
# PSR-4 mapping / missing class / class-name typo BEFORE any consumer
# sees them. Analogous to the JS / Ruby equivalents.
- name: Load smoke (autoload + Client construction)
run: |
php -r '
require "vendor/autoload.php";
if (!defined("Bybit\\Bybit::VERSION")) {
throw new RuntimeException("Bybit\\Bybit::VERSION not defined");
}
$cli = new Bybit\Client(new Bybit\Configuration());
if (!$cli instanceof Bybit\Client) {
throw new RuntimeException("Client construction failed");
}
printf("smoke OK: bybit-connector-php %s on PHP %s\n", Bybit\Bybit::VERSION, PHP_VERSION);
'

lowest-deps:
# Verify the SDK still builds and tests pass against the LOWEST versions
# of every `require` constraint (guzzle ^7.5 → 7.5.0, etc.). The regular
# `build` job resolves highest-compatible and would silently start using
# a Guzzle 7.6+-only API without bumping the constraint, which would break
# users pinned to 7.5.
name: PHP 8.1 — prefer-lowest dep resolve + phpunit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.1'
extensions: json, mbstring, curl, openssl
coverage: none
tools: composer:v2

- name: composer update --prefer-lowest
run: composer update --no-interaction --no-progress --prefer-dist --prefer-lowest --prefer-stable

- name: PHPUnit
run: vendor/bin/phpunit --testdox --no-coverage

audit:
name: composer audit (advisories on runtime deps)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
tools: composer:v2

- name: composer install
run: composer install --no-interaction --no-progress --prefer-dist

# `composer audit` (Composer 2.4+) queries the FriendsOfPHP / Packagist
# advisory database and exits non-zero on any advisory. Runtime deps
# only (not require-dev) via `--no-dev`.
- name: composer audit --no-dev
run: composer audit --no-dev

secret-scan:
name: gitleaks (CLI)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0

# Run the gitleaks binary directly. The `gitleaks/gitleaks-action@v2`
# wrapper requires a paid license for organizations and pins a deprecated
# node20 runtime — the underlying MIT-licensed CLI has neither restriction.
- name: Install gitleaks
env:
GITLEAKS_VERSION: '8.21.2'
run: |
set -eu
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version

- name: Scan for secrets
run: gitleaks detect --source . --redact --verbose --exit-code 1 --no-banner

pack-check:
name: composer archive dry-run (LICENSE / README / src present, no test leak)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
tools: composer:v2

- name: composer install
run: composer install --no-interaction --no-progress --prefer-dist

- name: Build archive
run: composer archive --format=tar --dir=/tmp/pack --file=pack

- name: Verify archive contents
run: |
set -eu
echo "Inspecting /tmp/pack/pack.tar"
tar -tf /tmp/pack/pack.tar | sort > /tmp/pack-files.txt
echo "--- packed files ---"
cat /tmp/pack-files.txt

# Required entries
for need in LICENSE README.md CHANGELOG.md composer.json src/Bybit.php src/Configuration.php src/Client.php src/Session.php src/Authentication.php src/RestApi/BaseService.php; do
if ! grep -qx "$need" /tmp/pack-files.txt; then
echo "::error::Missing from archive: $need"
exit 1
fi
done

# Files that must NEVER ship in the distributed package. `composer archive`
# respects `.gitattributes` `export-ignore`; this is a belt-and-suspenders
# regression guard.
for bad in tests phpunit.xml.dist phpstan.neon.dist .php-cs-fixer.dist.php .github .git; do
if grep -qE "^${bad}(/|$)" /tmp/pack-files.txt; then
echo "::error::Archive contains unexpected entry: $bad"
exit 1
fi
done
echo "pack check OK"
157 changes: 157 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Publish to Packagist

on:
push:
tags:
# Accept both `1.2.3` and conventional `v1.2.3`. Strip the leading `v`
# before comparing against Bybit\Bybit::VERSION below.
- 'v?[0-9]+.[0-9]+.[0-9]+'

# Serialize tag pushes: two tags in quick succession would otherwise race on
# `gh release create` and Packagist re-crawl. cancel-in-progress: false so a
# late tag never aborts a valid publish already underway.
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write # for creating the GitHub Release

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
# Do NOT persist GITHUB_TOKEN into .git/config — nothing in this job
# runs `git push`, and `composer install` executes third-party code
# under contents:write scope. `gh release create` gets its token
# explicitly via env below.
persist-credentials: false

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
extensions: json, mbstring, curl, openssl
coverage: none
tools: composer:v2

- name: Validate tag is on main branch
run: |
set -eu
if ! git merge-base --is-ancestor "$GITHUB_SHA" "$(git rev-parse origin/main)"; then
echo "Error: tag '$GITHUB_REF_NAME' is not reachable from origin/main"
exit 1
fi
echo "Branch check passed: tag is on main"

- name: Validate tag matches src/Bybit.php VERSION constant
run: |
set -eu
composer install --no-interaction --no-progress --prefer-dist
PKG_VERSION=$(php -r 'require "vendor/autoload.php"; echo Bybit\Bybit::VERSION;')
# Strip an optional leading `v` (e.g. `v0.1.0` → `0.1.0`) so both tag
# conventions produce the same comparison against the VERSION constant.
TAG_VERSION="${GITHUB_REF_NAME#v}"
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "Error: tag '$GITHUB_REF_NAME' (parsed as '$TAG_VERSION') does not match Bybit\\Bybit::VERSION '$PKG_VERSION'"
exit 1
fi
echo "Version check passed: $TAG_VERSION"

- name: PHP-CS-Fixer + PHPStan + PHPUnit must pass before publish
run: |
vendor/bin/php-cs-fixer fix --dry-run --diff --no-interaction
vendor/bin/phpstan analyse --no-progress
vendor/bin/phpunit --no-coverage

- name: composer audit --no-dev (no live advisories on runtime deps)
run: composer audit --no-dev

- name: Verify archive builds cleanly
run: |
set -eu
composer archive --format=tar --dir=/tmp/pack --file=pack
tar -tf /tmp/pack/pack.tar | sort > /tmp/files.txt
for need in LICENSE README.md composer.json src/Bybit.php src/Client.php src/Session.php src/Authentication.php; do
if ! grep -qx "$need" /tmp/files.txt; then
echo "::error::Missing from release archive: $need"
exit 1
fi
done
# Mirror ci.yml's forbidden-entry check as a last-gate belt-and-suspenders
# before the tarball ships to Packagist.
for bad in tests phpunit.xml.dist phpstan.neon.dist .php-cs-fixer.dist.php .github .git; do
if grep -qE "^${bad}(/|$)" /tmp/files.txt; then
echo "::error::Archive contains unexpected entry: $bad"
exit 1
fi
done
echo "archive OK"

# Order matters: create the GitHub Release FIRST, then poke Packagist.
# If Packagist accepts but the Release step later fails, users can
# `composer require` a version whose GitHub Release 404s — split state
# that requires manual reconciliation. Reverse order keeps the visible
# (GitHub Release) side ahead of the searchable (Packagist) side.
- name: Create GitHub Release with CHANGELOG excerpt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
# Extract the section for THIS version from CHANGELOG.md — everything
# between the "## <version>" header and the next "## " header. Falls
# back to a placeholder if the section isn't found (e.g. author forgot
# to update CHANGELOG.md — publish should still succeed).
if grep -q "^## ${VERSION}\b" CHANGELOG.md; then
NOTES=$(awk -v tag="^## ${VERSION}\\b" '
$0 ~ tag { flag=1; print; next }
flag && /^## / { exit }
flag { print }
' CHANGELOG.md)
else
NOTES="See CHANGELOG.md for details."
fi
# `gh release create` fails if the release already exists; that's
# fine — a re-push of the same tag is either intentional (in which
# case delete the release first) or a mistake (in which case
# failing is correct).
printf '%s' "$NOTES" | gh release create "$TAG" --title "$TAG" --notes-file -

# Packagist auto-mirrors public GitHub tags when the repo is registered
# AND the Packagist webhook is configured on the GitHub repo. This step
# is a manual "poke" that forces Packagist to re-crawl immediately
# rather than waiting for the next scheduled sync — same idea as
# `pod trunk push` for CocoaPods.
#
# Requires TWO secrets on the repo settings:
# - PACKAGIST_USERNAME (your packagist.org login)
# - PACKAGIST_API_TOKEN (from packagist.org → Profile → Show API Token)
# If either secret is missing, this step logs a note and skips — the
# tag itself is still what triggers Packagist's automatic mirroring.
- name: Notify Packagist to re-crawl
env:
PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }}
PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }}
run: |
set -eu
if [ -z "${PACKAGIST_USERNAME:-}" ] || [ -z "${PACKAGIST_API_TOKEN:-}" ]; then
echo "::notice::PACKAGIST_USERNAME / PACKAGIST_API_TOKEN not set — skipping manual poke. Packagist will still auto-mirror on the next scheduled crawl."
exit 0
fi
REPO_URL="https://github.com/${GITHUB_REPOSITORY}"
RESP=$(curl -s -o /tmp/packagist.out -w '%{http_code}' \
-XPOST -H 'content-type:application/json' \
"https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_API_TOKEN}" \
-d "{\"repository\":{\"url\":\"${REPO_URL}\"}}")
echo "Packagist API responded ${RESP}"
cat /tmp/packagist.out || true
if [ "$RESP" != "202" ] && [ "$RESP" != "200" ]; then
echo "::error::Packagist re-crawl request failed with HTTP ${RESP}"
exit 1
fi
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/vendor/
/coverage/
/build/
/tmp/
/log/
composer.lock
.phpunit.result.cache
.phpunit.cache/
.php-cs-fixer.cache
.phpstan.cache
.DS_Store
.env
.env.local
.idea/
.vscode/
Loading
Loading