From a826a20aff8da5eb29779c5655206952550891a7 Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Wed, 1 Apr 2026 12:27:58 +0200 Subject: [PATCH 1/3] fix(ci): automate PR-title based releases and remove legacy workflows Switch release automation to merged PR title semantics with concurrency cancellation, extract PHP-specific version bump logic into a script, and remove obsolete release branch/prerelease workflows. Made-with: Cursor --- .github/RELEASE_SETUP.md | 109 ++++++++++++++ .github/workflows/ci.yml | 13 +- .github/workflows/initiate_release.yml | 47 ------ .github/workflows/prerelease.yml | 47 ------ .github/workflows/release.yml | 87 +++++++++-- README.md | 12 ++ scripts/release/bump_version.php | 193 +++++++++++++++++++++++++ 7 files changed, 401 insertions(+), 107 deletions(-) create mode 100644 .github/RELEASE_SETUP.md delete mode 100644 .github/workflows/initiate_release.yml delete mode 100644 .github/workflows/prerelease.yml create mode 100644 scripts/release/bump_version.php diff --git a/.github/RELEASE_SETUP.md b/.github/RELEASE_SETUP.md new file mode 100644 index 00000000..882581a2 --- /dev/null +++ b/.github/RELEASE_SETUP.md @@ -0,0 +1,109 @@ +# Release Setup Guide + +This guide explains how to set up automatic publishing to Packagist using GitHub Actions. + +## Prerequisites + +1. **Packagist Account**: Create an account at [packagist.org](https://packagist.org) +2. **GitHub Repository**: Ensure this repository is public on GitHub +3. **Packagist API Token**: Generate a token for automatic updates + +## Setup Steps + +### 1. Create Packagist Account and Submit Package + +1. Go to [packagist.org](https://packagist.org) and create an account +2. Click "Submit" and enter your repository URL: `https://github.com/GetStream/getstream-php` +3. Packagist will automatically detect the package name from `composer.json` + +### 2. Generate Packagist API Token + +1. Go to your Packagist profile: https://packagist.org/profile/ +2. Click "Show API Token" in the "API Token" section +3. Copy the token (it looks like: `pkg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`) + +### 3. Add GitHub Secrets + +1. Go to your GitHub repository: https://github.com/GetStream/getstream-php +2. Click "Settings" โ†’ "Secrets and variables" โ†’ "Actions" +3. Click "New repository secret" +4. Add these secrets: + + - **Name**: `PACKAGIST_TOKEN` + **Value**: Your Packagist API token from step 2 + + - **Name**: `STREAM_API_KEY` + **Value**: Your Stream API key (for running tests) + + - **Name**: `STREAM_API_SECRET` + **Value**: Your Stream API secret (for running tests) + +### 4. Enable Packagist Auto-Update + +1. In your Packagist package page, go to "Settings" +2. Enable "Update by GitHub Hook" +3. Add the GitHub webhook URL: `https://packagist.org/api/github?username=YOUR_USERNAME&packageName=getstream/getstream-php` + +## How It Works + +### Automatic Publishing + +When a PR is merged into `main` or `master`, the release workflow will: + +1. Parse the PR title using Conventional Commit style. +2. Decide the bump type: + - `feat:` => minor + - `fix:` or `bug:` => patch + - `feat!:` / `fix!:` / `BREAKING CHANGE` => major +3. Update `composer.json` and `src/Constant.php` via `scripts/release/bump_version.php` +4. Commit version files, create a `vX.Y.Z` tag, create a GitHub release +5. Trigger Packagist update + +### Creating a Release + +1. Open a PR with a Conventional Commit style title, for example: + - `feat: add feed search endpoint` + - `fix: handle nil reaction id` + - `feat!: remove deprecated batch API` +2. Merge the PR into `main` or `master`. +3. GitHub Actions will automatically perform release + Packagist update. + +Titles like `chore:`, `docs:`, `test:` do not trigger a release. + +### Manual Publishing (if needed) + +If automatic publishing fails, you can manually trigger Packagist updates: + +```bash +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_PACKAGIST_TOKEN" \ + -d '{"repository":{"url":"https://github.com/GetStream/getstream-php"}}' \ + https://packagist.org/api/update-package?username=YOUR_USERNAME +``` + +## Troubleshooting + +### Common Issues + +1. **"Package not found"**: Ensure the package is submitted to Packagist first +2. **"Invalid token"**: Verify the Packagist token is correct +3. **"Tests failing"**: Check that all tests pass before creating a release +4. **"Version already exists"**: Use a new version number + +### Checking Status + +- **GitHub Actions**: https://github.com/GetStream/getstream-php/actions +- **Packagist Package**: https://packagist.org/packages/getstream/getstream-php +- **Test Installation**: `composer require getstream/getstream-php` + +## Security Notes + +- Never commit API tokens to the repository +- Use GitHub Secrets for all sensitive data +- Regularly rotate your Packagist token +- Monitor the Actions logs for any security issues + + + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73c36c61..6d2b2aae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,23 @@ on: branches: [master, main] concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: + check-pr-title: + name: Validate PR title + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: aslafy-z/conventional-pr-title-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test: name: ๐Ÿงช Test & lint + needs: check-pr-title + if: always() && (github.event_name != 'pull_request' || needs.check-pr-title.result == 'success') environment: ci runs-on: ubuntu-latest diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml deleted file mode 100644 index 3e380758..00000000 --- a/.github/workflows/initiate_release.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Create release PR - -on: - workflow_dispatch: - inputs: - version: - description: "The new version number. Example: 1.0.1" - required: true - type: string - -jobs: - init_release: - name: ๐Ÿš€ Create release PR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Update version in composer.json and Constant.php - env: - VERSION: ${{ github.event.inputs.version }} - run: | - # Update composer.json - sed -i 's/"version": ".*"/"version": "v'$VERSION'"/' composer.json - # Update Constant.php (remove 'v' prefix for constant) - sed -i "s/const VERSION = '.*'/const VERSION = '$VERSION'/" src/Constant.php - git config --global user.name 'github-actions' - git config --global user.email 'release@getstream.io' - git checkout -q -b "release-$VERSION" - git add composer.json src/Constant.php - git commit -am "chore(release): $VERSION" - git push -q -u origin "release-$VERSION" - - - name: Open pull request - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr create \ - -t "Release ${{ github.event.inputs.version }}" \ - -b "# :rocket: ${{ github.event.inputs.version }} - Make sure to use squash & merge when merging! - Once this is merged, another job will kick off automatically and publish the package. - - ## Changes - - Updated version in composer.json to v${{ github.event.inputs.version }} - - Updated version in Constant.php to ${{ github.event.inputs.version }}" diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml deleted file mode 100644 index 75a50e5d..00000000 --- a/.github/workflows/prerelease.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Pre-release - -on: - release: - types: [prereleased] - -jobs: - prerelease: - name: ๐Ÿš€ Pre-release - environment: ci - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Update Packagist - env: - PACKAGIST_TOKEN: ${{ secrets.PACKAGIST_TOKEN }} - PACKAGIST_USERNAME: ${{ vars.PACKAGIST_USERNAME }} - run: | - if [ -z "$PACKAGIST_TOKEN" ]; then - echo "โš ๏ธ PACKAGIST_TOKEN secret is not set. Skipping Packagist update." - exit 0 - fi - - if [ -z "$PACKAGIST_USERNAME" ]; then - echo "โš ๏ธ PACKAGIST_USERNAME var is not set. Skipping Packagist update." - exit 0 - fi - - echo "๐Ÿ”„ Updating Packagist package..." - response=$(curl -s -w "\n%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $PACKAGIST_USERNAME:$PACKAGIST_TOKEN" \ - -d '{"repository":{"url":"https://github.com/GetStream/getstream-php"}}' \ - "https://packagist.org/api/update-package") - - http_code=$(echo "$response" | tail -n1) - body=$(echo "$response" | sed '$d') - - if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 202 ]; then - echo "โœ… Packagist update triggered successfully" - echo "$body" - else - echo "โŒ Failed to update Packagist (HTTP $http_code)" - echo "$body" - exit 1 - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08207f8f..d4039e73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,38 +4,101 @@ on: pull_request: types: [closed] branches: + - main - master +concurrency: + group: release-${{ github.event.pull_request.base.ref }} + cancel-in-progress: true + +permissions: + contents: write + jobs: release: name: ๐Ÿš€ Release environment: ci - if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') + if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - - uses: actions/github-script@v7 + ref: ${{ github.event.pull_request.base.ref }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - script: | - // Getting the release version from the PR source branch - // Source branch looks like this: release-1.0.0 - const version = context.payload.pull_request.head.ref.split('-')[1] - core.exportVariable('VERSION', version) + php-version: '8.1' + tools: composer:v2 + + - name: Skip when PR is already released + id: already_released + run: | + if git log --oneline --grep="(pr #${{ github.event.pull_request.number }})" -n 1 | grep -q "chore(release):"; then + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + + - name: Determine and apply version bump + id: release_meta + if: steps.already_released.outputs.value != 'true' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + php scripts/release/bump_version.php \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --output "$GITHUB_OUTPUT" + + - name: Stop when PR does not require release + if: steps.already_released.outputs.value == 'true' || steps.release_meta.outputs.should_release != 'true' + run: | + if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then + echo "PR #${{ github.event.pull_request.number }} is already released; skipping." + exit 0 + fi + echo "No release type found in PR title; skipping." + exit 0 + + - name: Commit version files + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add composer.json src/Constant.php + if git diff --cached --quiet; then + echo "No version changes to commit." + exit 0 + fi + git commit -m "chore(release): v${{ steps.release_meta.outputs.version }} (pr #${{ github.event.pull_request.number }})" + git push origin "HEAD:${{ github.event.pull_request.base.ref }}" + + - name: Create release tag + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' + run: | + git tag "${{ steps.release_meta.outputs.tag }}" + git push origin "${{ steps.release_meta.outputs.tag }}" - name: Create release on GitHub + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' uses: ncipollo/release-action@v1 with: - tag: ${{ env.VERSION }} + tag: ${{ steps.release_meta.outputs.tag }} token: ${{ secrets.GITHUB_TOKEN }} body: | - Release ${{ env.VERSION }} - - Install with: `composer require getstream/getstream-php:^${{ env.VERSION }}` + Release v${{ steps.release_meta.outputs.version }} + + - Bump type: `${{ steps.release_meta.outputs.bump }}` + - Previous: `${{ steps.release_meta.outputs.previous_version }}` + - Next: `${{ steps.release_meta.outputs.version }}` + + Install with: `composer require getstream/getstream-php:^${{ steps.release_meta.outputs.version }}` - name: Update Packagist + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' env: PACKAGIST_TOKEN: ${{ secrets.PACKAGIST_TOKEN }} PACKAGIST_USERNAME: ${{ vars.PACKAGIST_USERNAME }} diff --git a/README.md b/README.md index 75522b27..c68d092f 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,18 @@ This creates clean, typed models with automatic JSON handling - no boilerplate c ## Development +### Release Workflow + +Releases are automated when a pull request is merged into `main` or `master`. + +- PR titles must follow Conventional Commit format (for example: `feat: ...`, `fix: ...`). +- Version bump is derived from PR title/body: + - `feat:` => minor + - `fix:` or `bug:` => patch + - `feat!:` / `fix!:` / `BREAKING CHANGE` => major +- Non-release types like `chore:`, `docs:`, `test:` do not create a release. +- The release workflow updates `composer.json` and `src/Constant.php`, pushes a tag, creates a GitHub release, and triggers Packagist. + ### Linting and Code Quality ```bash diff --git a/scripts/release/bump_version.php b/scripts/release/bump_version.php new file mode 100644 index 00000000..4d359e4a --- /dev/null +++ b/scripts/release/bump_version.php @@ -0,0 +1,193 @@ + $value) { + if (str_starts_with($value, $prefix)) { + return (string) substr($value, strlen($prefix)); + } + + if ($value === '--' . $name && isset($argv[$index + 1])) { + return (string) $argv[$index + 1]; + } + } + + return $default; +} + +function runCommand(string $command): string +{ + $result = shell_exec($command); + return $result === null ? '' : trim($result); +} + +function findLatestSemverTag(): string +{ + $tagsRaw = runCommand('git tag --list --sort=-version:refname'); + if ($tagsRaw === '') { + return '0.0.0'; + } + + $tags = preg_split('/\R/', $tagsRaw) ?: []; + foreach ($tags as $tag) { + $tag = trim($tag); + if ($tag === '') { + continue; + } + + $normalized = ltrim($tag, 'v'); + if (preg_match('/^\d+\.\d+\.\d+$/', $normalized) === 1) { + return $normalized; + } + } + + return '0.0.0'; +} + +function determineBumpType(string $title, string $body): string +{ + $title = trim($title); + $body = trim($body); + + if (preg_match('/BREAKING[ -]CHANGE/i', $body) === 1) { + return 'major'; + } + + if (preg_match('/^([a-z]+)(\([^)]+\))?(!)?:/i', $title, $matches) !== 1) { + return 'none'; + } + + $type = strtolower($matches[1]); + $isBreakingTitle = isset($matches[3]) && $matches[3] === '!'; + if ($isBreakingTitle) { + return 'major'; + } + + if ($type === 'feat') { + return 'minor'; + } + + if ($type === 'fix' || $type === 'bug') { + return 'patch'; + } + + return 'none'; +} + +function incrementVersion(string $version, string $bump): string +{ + $parts = array_map('intval', explode('.', $version)); + $major = $parts[0] ?? 0; + $minor = $parts[1] ?? 0; + $patch = $parts[2] ?? 0; + + if ($bump === 'major') { + $major++; + $minor = 0; + $patch = 0; + } elseif ($bump === 'minor') { + $minor++; + $patch = 0; + } elseif ($bump === 'patch') { + $patch++; + } + + return sprintf('%d.%d.%d', $major, $minor, $patch); +} + +function updateComposerVersion(string $path, string $version): void +{ + $raw = file_get_contents($path); + if ($raw === false) { + throw new ReleaseScriptException('Could not read composer.json'); + } + + $composer = json_decode($raw, true); + if (!is_array($composer)) { + throw new ReleaseScriptException('Could not parse composer.json'); + } + + $composer['version'] = 'v' . $version; + $updated = json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($updated === false) { + throw new ReleaseScriptException('Could not encode composer.json'); + } + + file_put_contents($path, $updated . PHP_EOL); +} + +function updateConstantVersion(string $path, string $version): void +{ + $raw = file_get_contents($path); + if ($raw === false) { + throw new ReleaseScriptException('Could not read Constant.php'); + } + + $updated = preg_replace( + "/public const VERSION = '[^']+';/", + "public const VERSION = '" . $version . "';", + $raw, + 1 + ); + + if ($updated === null) { + throw new ReleaseScriptException('Regex failed while updating Constant.php'); + } + + file_put_contents($path, $updated); +} + +function writeOutputs(string $outputPath, array $values): void +{ + if ($outputPath === '') { + foreach ($values as $key => $value) { + echo $key . '=' . $value . PHP_EOL; + } + return; + } + + $lines = []; + foreach ($values as $key => $value) { + $lines[] = $key . '=' . $value; + } + file_put_contents($outputPath, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND); +} + +$title = getArgValue($argv, 'title'); +$body = getArgValue($argv, 'body'); +$outputPath = getArgValue($argv, 'output'); + +$bump = determineBumpType($title, $body); +if ($bump === 'none') { + writeOutputs($outputPath, [ + 'should_release' => 'false', + 'bump' => 'none', + ]); + exit(0); +} + +$currentVersion = findLatestSemverTag(); +$nextVersion = incrementVersion($currentVersion, $bump); + +updateComposerVersion('composer.json', $nextVersion); +updateConstantVersion('src/Constant.php', $nextVersion); + +writeOutputs($outputPath, [ + 'should_release' => 'true', + 'bump' => $bump, + 'previous_version' => $currentVersion, + 'version' => $nextVersion, + 'tag' => 'v' . $nextVersion, +]); From bfb90190b7517070b329c1acf21b2708b34a0d5e Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Wed, 1 Apr 2026 12:40:46 +0200 Subject: [PATCH 2/3] update readme --- .github/RELEASE_SETUP.md | 8 +++++--- README.md | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/RELEASE_SETUP.md b/.github/RELEASE_SETUP.md index 882581a2..d5112ae3 100644 --- a/.github/RELEASE_SETUP.md +++ b/.github/RELEASE_SETUP.md @@ -51,6 +51,8 @@ This guide explains how to set up automatic publishing to Packagist using GitHub When a PR is merged into `main` or `master`, the release workflow will: 1. Parse the PR title using Conventional Commit style. + - Required ticket format: `type: [FEEDS-1234] description` + - Keep `feat`/`fix`/`bug` at the beginning of the title 2. Decide the bump type: - `feat:` => minor - `fix:` or `bug:` => patch @@ -62,9 +64,9 @@ When a PR is merged into `main` or `master`, the release workflow will: ### Creating a Release 1. Open a PR with a Conventional Commit style title, for example: - - `feat: add feed search endpoint` - - `fix: handle nil reaction id` - - `feat!: remove deprecated batch API` + - `feat: [FEEDS-1350] add feed search endpoint` + - `fix: [FEEDS-1402] handle nil reaction id` + - `feat!: [FEEDS-1410] remove deprecated batch API` 2. Merge the PR into `main` or `master`. 3. GitHub Actions will automatically perform release + Packagist update. diff --git a/README.md b/README.md index c68d092f..430dea72 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ This creates clean, typed models with automatic JSON handling - no boilerplate c Releases are automated when a pull request is merged into `main` or `master`. - PR titles must follow Conventional Commit format (for example: `feat: ...`, `fix: ...`). +- Ticket prefix is required in the subject: `type: [FEEDS-1234] description`. +- Keep the commit type first so release automation can parse it. - Version bump is derived from PR title/body: - `feat:` => minor - `fix:` or `bug:` => patch @@ -158,6 +160,12 @@ Releases are automated when a pull request is merged into `main` or `master`. - Non-release types like `chore:`, `docs:`, `test:` do not create a release. - The release workflow updates `composer.json` and `src/Constant.php`, pushes a tag, creates a GitHub release, and triggers Packagist. +Examples: + +- `feat: [FEEDS-1350] add feed retention endpoint` +- `fix: [FEEDS-1402] handle missing reaction id` +- `feat!: [FEEDS-1410] remove deprecated follow API` + ### Linting and Code Quality ```bash From ecdae380ae0ea5d513b6d40f8572218ccaad5d1c Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Wed, 1 Apr 2026 13:04:37 +0200 Subject: [PATCH 3/3] fix(ci): address release review feedback Harden release version resolution for mixed tag formats, avoid composer.json reformatting by updating only the version field, and pass PR body via file to prevent argument injection edge cases. Made-with: Cursor --- .github/workflows/release.yml | 4 ++- scripts/release/bump_version.php | 55 +++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4039e73..4f34f6f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,9 +48,11 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} run: | + PR_BODY_FILE=$(mktemp) + printf '%s' "$PR_BODY" > "$PR_BODY_FILE" php scripts/release/bump_version.php \ --title "$PR_TITLE" \ - --body "$PR_BODY" \ + --body-file "$PR_BODY_FILE" \ --output "$GITHUB_OUTPUT" - name: Stop when PR does not require release diff --git a/scripts/release/bump_version.php b/scripts/release/bump_version.php index 4d359e4a..09a21db1 100644 --- a/scripts/release/bump_version.php +++ b/scripts/release/bump_version.php @@ -6,6 +6,7 @@ * Computes next release version from PR title/body and updates version files. * Usage: * php scripts/release/bump_version.php --title "feat: add x" --body "..." --output "$GITHUB_OUTPUT" + * php scripts/release/bump_version.php --title "feat: add x" --body-file "/tmp/body.txt" --output "$GITHUB_OUTPUT" */ final class ReleaseScriptException extends RuntimeException { @@ -35,25 +36,27 @@ function runCommand(string $command): string function findLatestSemverTag(): string { - $tagsRaw = runCommand('git tag --list --sort=-version:refname'); + $tagsRaw = runCommand('git tag --list'); if ($tagsRaw === '') { return '0.0.0'; } $tags = preg_split('/\R/', $tagsRaw) ?: []; - foreach ($tags as $tag) { - $tag = trim($tag); - if ($tag === '') { - continue; - } + $versions = []; - $normalized = ltrim($tag, 'v'); + foreach ($tags as $tag) { + $normalized = ltrim(trim($tag), 'v'); if (preg_match('/^\d+\.\d+\.\d+$/', $normalized) === 1) { - return $normalized; + $versions[] = $normalized; } } - return '0.0.0'; + if ($versions === []) { + return '0.0.0'; + } + + usort($versions, 'version_compare'); + return end($versions) ?: '0.0.0'; } function determineBumpType(string $title, string $body): string @@ -114,18 +117,18 @@ function updateComposerVersion(string $path, string $version): void throw new ReleaseScriptException('Could not read composer.json'); } - $composer = json_decode($raw, true); - if (!is_array($composer)) { - throw new ReleaseScriptException('Could not parse composer.json'); - } + $updated = preg_replace( + '/"version":\s*"[^"]*"/', + '"version": "v' . $version . '"', + $raw, + 1 + ); - $composer['version'] = 'v' . $version; - $updated = json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($updated === false) { - throw new ReleaseScriptException('Could not encode composer.json'); + if ($updated === null || $updated === $raw) { + throw new ReleaseScriptException('Could not update version in composer.json'); } - file_put_contents($path, $updated . PHP_EOL); + file_put_contents($path, $updated); } function updateConstantVersion(string $path, string $version): void @@ -165,8 +168,22 @@ function writeOutputs(string $outputPath, array $values): void file_put_contents($outputPath, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND); } +function resolveBody(array $argv): string +{ + $bodyFile = getArgValue($argv, 'body-file'); + if ($bodyFile !== '') { + $raw = file_get_contents($bodyFile); + if ($raw === false) { + throw new ReleaseScriptException('Could not read body-file'); + } + return $raw; + } + + return getArgValue($argv, 'body'); +} + $title = getArgValue($argv, 'title'); -$body = getArgValue($argv, 'body'); +$body = resolveBody($argv); $outputPath = getArgValue($argv, 'output'); $bump = determineBumpType($title, $body);