diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..57c0cc9
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,66 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version_bump:
+ description: 'Version bump type (auto = detect from conventional commits)'
+ required: true
+ type: choice
+ default: auto
+ options:
+ - auto
+ - patch
+ - minor
+ - major
+ repo:
+ description: 'Upstream repository to release (empty = all)'
+ required: false
+ type: choice
+ default: ''
+ options:
+ - ''
+ - core
+ - contrib
+ filter:
+ description: 'Filter downstream repositories by prefix'
+ required: false
+ type: string
+ default: ''
+
+jobs:
+ release:
+ name: Create releases
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: json, simplexml
+ coverage: none
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run release
+ env:
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ run: |
+ ARGS="--non-interactive"
+ if [ "${{ inputs.version_bump }}" != "auto" ]; then
+ ARGS="$ARGS --version-bump=${{ inputs.version_bump }}"
+ fi
+ if [ -n "${{ inputs.repo }}" ]; then
+ ARGS="$ARGS --repo=${{ inputs.repo }}"
+ fi
+ if [ -n "${{ inputs.filter }}" ]; then
+ ARGS="$ARGS --filter=${{ inputs.filter }}"
+ fi
+ bin/otel release:run $ARGS
diff --git a/src/Console/Application/Application.php b/src/Console/Application/Application.php
index 1024b37..1e86e4c 100644
--- a/src/Console/Application/Application.php
+++ b/src/Console/Application/Application.php
@@ -7,6 +7,7 @@
use OpenTelemetry\DevTools\Console\Command\Packages\ValidateInstallationCommand;
use OpenTelemetry\DevTools\Console\Command\Packages\ValidatePackagesCommand;
use OpenTelemetry\DevTools\Console\Command\Release\PeclCommand;
+use OpenTelemetry\DevTools\Console\Command\Release\PeclPrCommand;
use OpenTelemetry\DevTools\Console\Command\Release\PeclTagCommand;
use OpenTelemetry\DevTools\Console\Command\Release\ReleaseCommand;
use OpenTelemetry\DevTools\Console\Command\Release\ReleaseListCommand;
@@ -41,6 +42,7 @@ private function initCommands(): void
new ReleaseCommand(),
new ReleaseListCommand(),
new PeclCommand(),
+ new PeclPrCommand(),
new PeclTagCommand(),
]);
}
diff --git a/src/Console/Command/Release/AbstractReleaseCommand.php b/src/Console/Command/Release/AbstractReleaseCommand.php
index 7447296..b9248da 100644
--- a/src/Console/Command/Release/AbstractReleaseCommand.php
+++ b/src/Console/Command/Release/AbstractReleaseCommand.php
@@ -73,6 +73,15 @@ protected function post(string $url, string $body): ResponseInterface
return $this->client->sendRequest($request);
}
+ protected function put(string $url, string $body): ResponseInterface
+ {
+ $request = new Request('PUT', $url, $this->headers(), $body);
+ $this->output->isVerbose() && $this->output->writeln("[HTTP] PUT {$url}");
+ $this->output->isVeryVerbose() && $this->output->writeln("[HTTP body] {$body}");
+
+ return $this->client->sendRequest($request);
+ }
+
protected function get_latest_release(Repository $repository): ?Release
{
$release_url = "https://api.github.com/repos/{$repository->downstream}/releases/latest";
diff --git a/src/Console/Command/Release/PeclCommand.php b/src/Console/Command/Release/PeclCommand.php
index d980f2d..236d31a 100644
--- a/src/Console/Command/Release/PeclCommand.php
+++ b/src/Console/Command/Release/PeclCommand.php
@@ -31,6 +31,10 @@ protected function configure(): void
->setName('release:pecl')
->setDescription('Update auto-instrumentation package.xml for PECL release')
->addOption('force', ['f'], InputOption::VALUE_NONE, 'force')
+ ->addOption('version', null, InputOption::VALUE_OPTIONAL, 'new version (skips prompt)')
+ ->addOption('stability', null, InputOption::VALUE_OPTIONAL, 'release stability: stable|beta (skips prompt)', null)
+ ->addOption('output-file', null, InputOption::VALUE_OPTIONAL, 'write updated package.xml to this file path instead of stdout')
+ ->addOption('update-header', null, InputOption::VALUE_OPTIONAL, 'path to local php_opentelemetry.h to update PHP_OPENTELEMETRY_VERSION')
;
}
@@ -80,7 +84,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* @psalm-suppress PossiblyNullPropertyFetch
*/
- private function process(Repository $repository, SimpleXMLElement $xml): void
+ protected function process(Repository $repository, SimpleXMLElement $xml): void
{
$cnt = count($repository->commits);
$this->output->writeln("Last release {$repository->latestRelease->version} @ {$repository->latestRelease->timestamp}");
@@ -91,22 +95,28 @@ private function process(Repository $repository, SimpleXMLElement $xml): void
$prev = ($repository->latestRelease === null)
? '-nothing-'
: $repository->latestRelease->version;
- $question = new Question("Latest={$prev}, enter new tag (blank to skip):", null);
$helper = new QuestionHelper();
- $newVersion = $helper->ask($this->input, $this->output, $question);
+ $newVersion = $this->input->getOption('version');
+ if (!$newVersion) {
+ $question = new Question("Latest={$prev}, enter new tag (blank to skip):", null);
+ $newVersion = $helper->ask($this->input, $this->output, $question);
+ }
if (!$newVersion) {
$this->output->writeln("[SKIP] not going to release {$repository->downstream}");
return;
}
- $question = new ChoiceQuestion(
- 'Is this a beta or stable release?',
- ['stable', 'beta'],
- 'stable',
- );
- $stability = $helper->ask($this->input, $this->output, $question);
+ $stability = $this->input->getOption('stability');
+ if (!$stability) {
+ $question = new ChoiceQuestion(
+ 'Is this a beta or stable release?',
+ ['stable', 'beta'],
+ 'stable',
+ );
+ $stability = $helper->ask($this->input, $this->output, $question);
+ }
//new release data
$release = [
@@ -122,7 +132,20 @@ private function process(Repository $repository, SimpleXMLElement $xml): void
],
'notes' => $this->format_notes($newVersion),
];
- $this->output->writeln($this->convertPackageXml($xml, $release));
+ $xmlContent = $this->convertPackageXml($xml, $release);
+
+ $outputFile = $this->input->getOption('output-file');
+ if ($outputFile) {
+ file_put_contents($outputFile, $xmlContent);
+ $this->output->writeln("[WRITTEN] package.xml -> {$outputFile}");
+ } else {
+ $this->output->writeln($xmlContent);
+ }
+
+ $headerFile = $this->input->getOption('update-header');
+ if ($headerFile) {
+ $this->update_header_file($headerFile, $newVersion);
+ }
}
protected function convertPackageXml(SimpleXMLElement $xml, array $new): string
@@ -158,7 +181,29 @@ protected function convertPackageXml(SimpleXMLElement $xml, array $new): string
return $pretty->saveXML();
}
- private function format_notes(string $version): string
+ private function update_header_file(string $path, string $version): void
+ {
+ if (!file_exists($path)) {
+ $this->output->writeln("[ERROR] Header file not found: {$path}");
+
+ return;
+ }
+ $contents = file_get_contents($path);
+ $updated = preg_replace(
+ '/(#define PHP_OPENTELEMETRY_VERSION ")[^"]+(")/m',
+ '${1}' . $version . '${2}',
+ $contents,
+ );
+ if ($updated === $contents) {
+ $this->output->writeln('[WARN] PHP_OPENTELEMETRY_VERSION define not found in header file');
+
+ return;
+ }
+ file_put_contents($path, $updated);
+ $this->output->writeln("[UPDATED] PHP_OPENTELEMETRY_VERSION -> {$version} in {$path}");
+ }
+
+ protected function format_notes(string $version): string
{
return sprintf('See https://github.com/%s/%s/releases/tag/%s', self::OWNER, self::REPO, $version);
}
diff --git a/src/Console/Command/Release/PeclPrCommand.php b/src/Console/Command/Release/PeclPrCommand.php
new file mode 100644
index 0000000..685bf99
--- /dev/null
+++ b/src/Console/Command/Release/PeclPrCommand.php
@@ -0,0 +1,267 @@
+setName('release:pecl:pr')
+ ->setDescription('Create a GitHub PR to update package.xml and php_opentelemetry.h for a PECL release')
+ ->addOption('token', ['t'], InputOption::VALUE_OPTIONAL, 'github token')
+ ->addOption('version', null, InputOption::VALUE_REQUIRED, 'new version (required)')
+ ->addOption('stability', null, InputOption::VALUE_OPTIONAL, 'release stability: stable|beta', 'stable')
+ ->addOption('base-branch', null, InputOption::VALUE_OPTIONAL, 'base branch to create PR against', 'main')
+ ->addOption('dry-run', null, InputOption::VALUE_NONE, 'dry run, do not make any changes')
+ ;
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->token = $input->getOption('token');
+ $this->dry_run = $input->getOption('dry-run');
+ $this->client = Psr18ClientDiscovery::find();
+ $this->registerInputAndOutput($input, $output);
+
+ $version = $input->getOption('version');
+ if (!$version) {
+ $this->output->writeln('--version is required');
+
+ return Command::FAILURE;
+ }
+
+ $stability = $input->getOption('stability');
+ $baseBranch = $input->getOption('base-branch');
+ $releaseBranch = "release/pecl-{$version}";
+
+ $project = new Project(self::REPOSITORY);
+ $repository = new Repository();
+ $repository->upstream = $project;
+ $repository->downstream = $project;
+
+ // Fetch and transform package.xml
+ $xmlContent = $this->fetch_and_convert_package_xml($repository, $version, $stability);
+ if ($xmlContent === null) {
+ return Command::FAILURE;
+ }
+
+ // Fetch and update php_opentelemetry.h
+ $headerContent = $this->fetch_and_update_header($version);
+ if ($headerContent === null) {
+ return Command::FAILURE;
+ }
+
+ if ($this->dry_run) {
+ $this->output->writeln('[DRY-RUN] Would create branch: ' . $releaseBranch);
+ $this->output->writeln('[DRY-RUN] Would commit ' . self::PACKAGE_XML_PATH . ' and ' . self::HEADER_PATH);
+ $this->output->writeln('[DRY-RUN] Would open PR against ' . $baseBranch);
+
+ return Command::SUCCESS;
+ }
+
+ // Get SHA of base branch
+ $baseSha = $this->get_sha_for_branch($repository, $baseBranch);
+
+ // Create release branch
+ $this->create_branch($releaseBranch, $baseSha);
+
+ // Commit package.xml
+ $this->commit_file(
+ self::PACKAGE_XML_PATH,
+ $xmlContent,
+ "chore: update package.xml for PECL release {$version}",
+ $releaseBranch
+ );
+
+ // Commit php_opentelemetry.h
+ $this->commit_file(
+ self::HEADER_PATH,
+ $headerContent,
+ "chore: update PHP_OPENTELEMETRY_VERSION to {$version}",
+ $releaseBranch
+ );
+
+ // Open PR
+ $prUrl = $this->create_pull_request($version, $releaseBranch, $baseBranch);
+ if ($prUrl) {
+ $this->output->writeln("[CREATED] PR: {$prUrl}");
+ }
+
+ return Command::SUCCESS;
+ }
+
+ private function fetch_and_convert_package_xml(Repository $repository, string $version, string $stability): ?string
+ {
+ $url = sprintf('https://raw.githubusercontent.com/%s/main/%s', self::REPOSITORY, self::PACKAGE_XML_PATH);
+ $response = $this->fetch($url);
+ if ($response->getStatusCode() !== 200) {
+ $this->output->writeln("Failed to fetch {$url}: {$response->getStatusCode()}");
+
+ return null;
+ }
+
+ $xml = new SimpleXMLElement($response->getBody()->getContents());
+
+ $release = [
+ 'date' => date('Y-m-d'),
+ 'time' => date('H:i:s'),
+ 'version' => [
+ 'release' => $version,
+ 'api' => '1.0',
+ ],
+ 'stability' => [
+ 'release' => $stability,
+ 'api' => 'stable',
+ ],
+ 'notes' => $this->format_notes($version),
+ ];
+
+ return $this->convert_package_xml($xml, $release);
+ }
+
+ private function convert_package_xml(SimpleXMLElement $xml, array $new): string
+ {
+ $release = $xml->changelog->addChild('release');
+ $release->addChild('date', (string) $xml->date);
+ $release->addChild('time', (string) $xml->time);
+ $version = $release->addChild('version');
+ $version->addChild('release', (string) $xml->version->release);
+ $version->addChild('api', (string) $xml->version->api);
+ $stability = $release->addChild('stability');
+ $stability->addChild('release', (string) $xml->stability->release);
+ $stability->addChild('api', (string) $xml->stability->api);
+ $release->addChild('license', (string) $xml->license);
+ $release->addChild('notes', (string) $xml->notes);
+
+ $xml->date = $new['date'];
+ $xml->time = $new['time'];
+ $xml->version->release = $new['version']['release'];
+ $xml->version->api = $new['version']['api'];
+ $xml->stability->release = $new['stability']['release'];
+ $xml->stability->api = $new['stability']['api'];
+ $xml->notes = $new['notes'];
+
+ $pretty = new DOMDocument();
+ $pretty->preserveWhiteSpace = false;
+ $pretty->formatOutput = true;
+ $pretty->loadXML($xml->saveXML());
+
+ return $pretty->saveXML();
+ }
+
+ private function fetch_and_update_header(string $version): ?string
+ {
+ $url = sprintf('https://raw.githubusercontent.com/%s/main/%s', self::REPOSITORY, self::HEADER_PATH);
+ $response = $this->fetch($url);
+ if ($response->getStatusCode() !== 200) {
+ $this->output->writeln("Failed to fetch {$url}: {$response->getStatusCode()}");
+
+ return null;
+ }
+
+ $contents = $response->getBody()->getContents();
+ $updated = preg_replace(
+ '/(#define PHP_OPENTELEMETRY_VERSION ")[^"]+(")/m',
+ '${1}' . $version . '${2}',
+ $contents,
+ );
+
+ if ($updated === $contents) {
+ $this->output->writeln('[WARN] PHP_OPENTELEMETRY_VERSION define not found in header file');
+ }
+
+ return $updated;
+ }
+
+ private function get_file_sha(string $path): string
+ {
+ $url = "https://api.github.com/repos/" . self::REPOSITORY . "/contents/{$path}";
+ $response = $this->fetch($url);
+ if ($response->getStatusCode() !== 200) {
+ throw new Exception("Failed to get file SHA for {$path}: {$response->getStatusCode()}");
+ }
+ $data = json_decode($response->getBody()->getContents());
+
+ return $data->sha;
+ }
+
+ private function create_branch(string $branch, string $sha): void
+ {
+ $url = "https://api.github.com/repos/" . self::REPOSITORY . "/git/refs";
+ $body = json_encode([
+ 'ref' => "refs/heads/{$branch}",
+ 'sha' => $sha,
+ ]);
+ $response = $this->post($url, $body);
+ if ($response->getStatusCode() !== 201) {
+ throw new Exception("Failed to create branch {$branch}: {$response->getStatusCode()} {$response->getBody()->getContents()}");
+ }
+ $this->output->writeln("[CREATED] branch: {$branch}");
+ }
+
+ private function commit_file(string $path, string $content, string $message, string $branch): void
+ {
+ $currentSha = $this->get_file_sha($path);
+ $url = "https://api.github.com/repos/" . self::REPOSITORY . "/contents/{$path}";
+ $body = json_encode([
+ 'message' => $message,
+ 'content' => base64_encode($content),
+ 'sha' => $currentSha,
+ 'branch' => $branch,
+ ]);
+ $response = $this->put($url, $body);
+ if ($response->getStatusCode() !== 200) {
+ throw new Exception("Failed to commit {$path}: {$response->getStatusCode()} {$response->getBody()->getContents()}");
+ }
+ $this->output->writeln("[COMMITTED] {$path}");
+ }
+
+ private function create_pull_request(string $version, string $head, string $base): ?string
+ {
+ $url = "https://api.github.com/repos/" . self::REPOSITORY . "/pulls";
+ $body = json_encode([
+ 'title' => "chore: PECL release {$version}",
+ 'body' => "Automated PR to update `package.xml` and `php_opentelemetry.h` for PECL release {$version}.\n\nSee https://github.com/" . self::REPOSITORY . "/releases/tag/{$version}",
+ 'head' => $head,
+ 'base' => $base,
+ ]);
+ $response = $this->post($url, $body);
+ if ($response->getStatusCode() !== 201) {
+ $this->output->writeln("Failed to create PR: {$response->getStatusCode()} {$response->getBody()->getContents()}");
+
+ return null;
+ }
+ $data = json_decode($response->getBody()->getContents());
+
+ return $data->html_url;
+ }
+
+ private function format_notes(string $version): string
+ {
+ return sprintf('See https://github.com/%s/%s/releases/tag/%s', self::OWNER, self::REPO, $version);
+ }
+}
diff --git a/src/Console/Command/Release/ReleaseCommand.php b/src/Console/Command/Release/ReleaseCommand.php
index a865fba..0e416f0 100644
--- a/src/Console/Command/Release/ReleaseCommand.php
+++ b/src/Console/Command/Release/ReleaseCommand.php
@@ -6,6 +6,7 @@
use Http\Discovery\Psr18ClientDiscovery;
use OpenTelemetry\DevTools\Console\Release\Commit;
+use OpenTelemetry\DevTools\Console\Release\ConventionalCommitVersionDetector;
use OpenTelemetry\DevTools\Console\Release\Diff;
use OpenTelemetry\DevTools\Console\Release\Release;
use OpenTelemetry\DevTools\Console\Release\Repository;
@@ -25,6 +26,8 @@ class ReleaseCommand extends AbstractReleaseCommand
private string $source_branch;
private bool $dry_run;
private bool $force;
+ private bool $non_interactive = false;
+ private ?string $version_bump = null;
#[\Override]
protected function configure(): void
@@ -38,6 +41,9 @@ protected function configure(): void
->addOption('repo', ['r'], InputOption::VALUE_OPTIONAL, 'repo to handle (core, contrib)')
->addOption('filter', null, InputOption::VALUE_OPTIONAL, 'filter by repository prefix')
->addOption('force', ['f'], InputOption::VALUE_NONE, 'force new releases even if no changes')
+ ->addOption('non-interactive', null, InputOption::VALUE_NONE, 'suppress all prompts; auto-detect version from conventional commits')
+ ->addOption('version-bump', null, InputOption::VALUE_OPTIONAL, 'version bump type: patch, minor, major (implies --non-interactive)')
+ ->addOption('draft', null, InputOption::VALUE_NONE, 'create releases as drafts (non-interactive mode)')
;
}
#[\Override]
@@ -62,6 +68,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->source_branch = $input->getOption('branch') ?? 'main';
$this->dry_run = $input->getOption('dry-run');
$this->force = $input->getOption('force');
+ $this->version_bump = $input->getOption('version-bump');
+ $this->non_interactive = $input->getOption('non-interactive') || $this->version_bump !== null;
$source = $input->getOption('repo');
$filter = $input->getOption('filter');
if ($source && !array_key_exists($source, self::AVAILABLE_REPOS)) {
@@ -158,6 +166,12 @@ private function compare_diffs_to_unreleased(Repository $repository): bool
}
$this->output->writeln('Please review these differences before continuing.');
+ if ($this->non_interactive) {
+ $this->output->writeln('[NON-INTERACTIVE] Continuing despite differences.');
+
+ return true;
+ }
+
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Do you want to continue despite these differences? (y/N): ', false);
@@ -225,20 +239,29 @@ private function handle_unreleased(Repository $repository): void
$prev = ($repository->latestRelease === null)
? '-nothing-'
: $repository->latestRelease->version;
- $question = new Question("Latest={$prev}, enter new tag (blank to skip):", null);
- $helper = new QuestionHelper();
- $newVersion = $helper->ask($this->input, $this->output, $question);
- if (!$newVersion) {
- $this->output->writeln("[SKIP] not going to release {$repository->downstream}");
+ if ($this->non_interactive) {
+ $bumpType = $this->version_bump ?? (new ConventionalCommitVersionDetector())->detect($repository->commits);
+ $currentVersion = $repository->latestRelease?->version ?? '0.0.0';
+ $newVersion = $this->bump_version($currentVersion, $bumpType);
+ $this->output->writeln("[AUTO] {$repository->downstream}: {$bumpType} bump {$currentVersion} -> {$newVersion}");
+ $makeLatest = true;
+ $isDraft = $this->input->getOption('draft');
+ } else {
+ $question = new Question("Latest={$prev}, enter new tag (blank to skip):", null);
+ $helper = new QuestionHelper();
+ $newVersion = $helper->ask($this->input, $this->output, $question);
+ if (!$newVersion) {
+ $this->output->writeln("[SKIP] not going to release {$repository->downstream}");
- return;
+ return;
+ }
+ $question = new ConfirmationQuestion('Make this the latest release (Y/n)?', true);
+ $makeLatest = $helper->ask($this->input, $this->output, $question);
+ $question = new ConfirmationQuestion('Make this release a draft (y/N)?', false);
+ $isDraft = $helper->ask($this->input, $this->output, $question);
}
$release->version = $newVersion;
- $question = new ConfirmationQuestion('Make this the latest release (Y/n)?', true);
- $makeLatest = $helper->ask($this->input, $this->output, $question);
- $question = new ConfirmationQuestion('Make this release a draft (y/N)?', false);
- $isDraft = $helper->ask($this->input, $this->output, $question);
$notes = [];
if ($repository->latestRelease === null) {
$notes[] = 'Initial release';
@@ -255,6 +278,21 @@ private function handle_unreleased(Repository $repository): void
$this->do_release($repository, $release, $makeLatest, $isDraft);
}
+ private function bump_version(string $current, string $type): string
+ {
+ $prefix = str_starts_with($current, 'v') ? 'v' : '';
+ $version = ltrim($current, 'v');
+ [$major, $minor, $patch] = array_map('intval', explode('.', $version . '.0.0'));
+
+ match ($type) {
+ ConventionalCommitVersionDetector::BUMP_MAJOR => [$major, $minor, $patch] = [$major + 1, 0, 0],
+ ConventionalCommitVersionDetector::BUMP_MINOR => [$major, $minor, $patch] = [$major, $minor + 1, 0],
+ default => [$major, $minor, $patch] = [$major, $minor, $patch + 1],
+ };
+
+ return "{$prefix}{$major}.{$minor}.{$patch}";
+ }
+
private function do_release(Repository $repository, Release $release, bool $makeLatest, bool $isDraft)
{
$url = "https://api.github.com/repos/{$repository->downstream}/releases";
diff --git a/src/Console/Release/ConventionalCommitVersionDetector.php b/src/Console/Release/ConventionalCommitVersionDetector.php
new file mode 100644
index 0000000..2ac248b
--- /dev/null
+++ b/src/Console/Release/ConventionalCommitVersionDetector.php
@@ -0,0 +1,59 @@
+ 0,
+ self::BUMP_MINOR => 1,
+ self::BUMP_MAJOR => 2,
+ ];
+
+ /**
+ * @param array $commits
+ */
+ public function detect(array $commits): string
+ {
+ $highest = self::BUMP_PATCH;
+ foreach ($commits as $commit) {
+ $detected = $this->detectForMessage($commit->message);
+ if (self::BUMP_ORDER[$detected] > self::BUMP_ORDER[$highest]) {
+ $highest = $detected;
+ }
+ if ($highest === self::BUMP_MAJOR) {
+ break;
+ }
+ }
+
+ return $highest;
+ }
+
+ public function detectForMessage(string $message): string
+ {
+ if (preg_match(self::BREAKING_FOOTER_PATTERN, $message)) {
+ return self::BUMP_MAJOR;
+ }
+ $firstLine = strtok($message, "\n");
+ if ($firstLine !== false) {
+ if (preg_match(self::BREAKING_BANG_PATTERN, $firstLine)) {
+ return self::BUMP_MAJOR;
+ }
+ if (preg_match(self::MINOR_PATTERN, $firstLine)) {
+ return self::BUMP_MINOR;
+ }
+ }
+
+ return self::BUMP_PATCH;
+ }
+}