Skip to content
Open
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
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,41 @@ current root directory the whole command simplifies to:
./vendor/bin/tailor ter:publish 1.2.0
```

The version can be skipped as well, since Tailor is able to
determine it from your extension:

```bash
./vendor/bin/tailor ter:publish
```

The following sources are used, in this order:

1. The **version argument**, if given.
2. The **tag of the checked out commit**, no matter whether it's
prefixed with `v` (e.g. `v1.2.0`) or not.
3. The version in **`ext_emconf.php`**.
4. The version in **`composer.json`**, either on root level or at
`[extra][typo3/cms][version]`.

The last two are the ones written by the
[`set-version`](#update-the-version-in-your-extension-files)
command. Tailor tells you which version it uses and where it
comes from, e.g.
`Using version 1.2.0 from the tag of the checked out commit.`

Since an extension key never looks like a version, it can still
be passed as only argument:

```bash
./vendor/bin/tailor ter:publish my_extension
```

> [!NOTE]
> The version has to be stated as argument if the checked out
> commit is tagged with more than one version, or if you publish
> an `--artefact` which does not belong to the extension in your
> current working directory.

> [!IMPORTANT]
> A couple of directories and files are excluded from packaging
> by default. Read more about
Expand Down Expand Up @@ -469,9 +504,14 @@ git push origin --tags
**Step 4: Push this version to TER**

```bash
./vendor/bin/tailor ter:publish 1.5.0
./vendor/bin/tailor ter:publish
```

Since **Step 1** wrote the version into your extension files and
**Step 2** tagged the commit with it, the version does not have to
be repeated here. State it as argument, e.g. `ter:publish 1.5.0`,
if you want to publish another version.

> [!NOTE]
> Both `set-version` and `ter:publish` provide options
> to specify the location of your extension. If, like in the example
Expand Down
17 changes: 14 additions & 3 deletions src/Command/Extension/UploadExtensionVersionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,30 @@ protected function configure(): void
$this
->setDescription('Publishes a new version of an extension to TER')
->setResultFormat(ConsoleFormatter::FORMAT_DETAIL)
->addArgument('version', InputArgument::REQUIRED, 'The version to publish, e.g. 1.2.3')
->addArgument('version', InputArgument::OPTIONAL, 'The version to publish, e.g. 1.2.3. Defaults to the tag of the checked out commit.')
->addArgument('extensionkey', InputArgument::OPTIONAL, 'The extension key')
->addOption('path', '', InputOption::VALUE_OPTIONAL, 'Path to the extension folder')
->addOption('path', '', InputOption::VALUE_OPTIONAL, 'Path to the extension folder. Defaults to the current working directory.')
->addOption('artefact', '', InputOption::VALUE_OPTIONAL, 'Path or URL to a zip file')
->addOption('comment', '', InputOption::VALUE_OPTIONAL, 'Upload comment of the new version (e.g. release notes)');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->version = $input->getArgument('version');
CommandHelper::normalizeVersionAndExtensionKeyArguments($input);

$resolvedVersion = CommandHelper::getVersionFromInput($input);
$this->version = $resolvedVersion->getVersion();
$this->extensionKey = CommandHelper::getExtensionKeyFromInput($input);
$this->transactionPath = rtrim(realpath(getcwd() ?: './'), '/') . '/tailor-version-upload';

if (!$resolvedVersion->isFromArgument() && $input->getOption('raw') === false) {
$output->writeln(sprintf(
'<info>Using version %s from %s.</info>',
$resolvedVersion->getVersion(),
$resolvedVersion->getSource()
));
}

if (!(new Filesystem\Directory())->create($this->transactionPath)) {
throw new \RuntimeException(sprintf('Directory could not be created.'));
}
Expand Down
54 changes: 54 additions & 0 deletions src/Dto/ResolvedVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 project - inspiring people to share!
* (c) 2020 Oliver Bartsch & Benni Mack
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace TYPO3\Tailor\Dto;

/**
* A version together with the source it was taken from.
*
* Since the version does not have to be stated on the command line,
* commands can tell the user where the version they work with comes from.
*/
class ResolvedVersion
{
public const SOURCE_ARGUMENT = 'argument';
public const SOURCE_GIT_TAG = 'the tag of the checked out commit';
public const SOURCE_EMCONF = 'ext_emconf.php';
public const SOURCE_COMPOSER = 'composer.json';

/** @var string */
protected $version;

/** @var string */
protected $source;

public function __construct(string $version, string $source)
{
$this->version = $version;
$this->source = $source;
}

public function getVersion(): string
{
return $this->version;
}

public function getSource(): string
{
return $this->source;
}

public function isFromArgument(): bool
{
return $this->source === self::SOURCE_ARGUMENT;
}
}
15 changes: 15 additions & 0 deletions src/Exception/VersionMissingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 project - inspiring people to share!
* (c) 2020 Oliver Bartsch & Benni Mack
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace TYPO3\Tailor\Exception;

class VersionMissingException extends \InvalidArgumentException {}
13 changes: 13 additions & 0 deletions src/Filesystem/ComposerReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,17 @@ public function getExtensionKey(): string
{
return $this->composerSchema['extra']['typo3/cms']['extension-key'] ?? '';
}

/**
* The version of the extension, as maintained by the `set-version` command.
* Both the root level and the TYPO3 specific section are taken into account.
*/
public function getVersion(): string
{
$version = $this->composerSchema['version']
?? $this->composerSchema['extra']['typo3/cms']['version']
?? '';

return trim((string)$version);
}
}
47 changes: 47 additions & 0 deletions src/Filesystem/EmConfReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 project - inspiring people to share!
* (c) 2020 Oliver Bartsch & Benni Mack
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace TYPO3\Tailor\Filesystem;

/**
* Reading information from ext_emconf.php
*/
class EmConfReader
{
/** @var array */
protected $configuration = [];

public function __construct(string $path = '')
{
$filename = rtrim($path ?: (string)(getcwd() ?: '.'), '/') . '/ext_emconf.php';
if (!file_exists($filename)) {
return;
}

$_EXTKEY = 'dummy';
@include $filename;

if (!isset($EM_CONF) || !is_array($EM_CONF)) {
return;
}

$configuration = reset($EM_CONF);
if (is_array($configuration)) {
$this->configuration = $configuration;
}
}

public function getVersion(): string
{
return trim((string)($this->configuration['version'] ?? ''));
}
}
94 changes: 92 additions & 2 deletions src/Helper/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
namespace TYPO3\Tailor\Helper;

use Symfony\Component\Console\Input\InputInterface;
use TYPO3\Tailor\Dto\ResolvedVersion;
use TYPO3\Tailor\Environment\Variables;
use TYPO3\Tailor\Exception\ExtensionKeyMissingException;
use TYPO3\Tailor\Exception\VersionMissingException;
use TYPO3\Tailor\Filesystem\ComposerReader;
use TYPO3\Tailor\Filesystem\EmConfReader;
use TYPO3\Tailor\Service\GitService;
use TYPO3\Tailor\Validation\VersionValidator;

/**
* Helper class for console commands.
Expand Down Expand Up @@ -47,6 +52,91 @@ final class CommandHelper
'typo3extension',
];

/**
* The version is looked up in the extension itself if not given as argument,
* so a release does not have to repeat its version on the command line.
*/
public static function getVersionFromInput(InputInterface $input): ResolvedVersion
{
// 1. CLI argument has highest priority
$version = (string)($input->getArgument('version') ?? '');
if ($version !== '') {
return new ResolvedVersion($version, ResolvedVersion::SOURCE_ARGUMENT);
}

$path = self::getPathFromInput($input);

// 2. The tag of the checked out commit marks the released version
$versions = (new GitService())->getVersionsFromTagsOfHead($path);

if (count($versions) > 1) {
throw new VersionMissingException(
sprintf(
'The checked out commit is tagged with more than one version (%s). Please state the version to use as argument.',
implode(', ', $versions)
),
1786492801
);
}
Comment on lines +72 to +80

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if we should really fail here if more than one version tag points to HEAD (which is actually a quite uncommon circumstance), since we probably still read it from composer.json or ext_emconf.php.


if ($versions !== []) {
return new ResolvedVersion($versions[0], ResolvedVersion::SOURCE_GIT_TAG);
}

// 3. The version files, as written by the `set-version` command
$versionValidator = new VersionValidator();

$version = (new EmConfReader($path))->getVersion();
if ($versionValidator->isValid($version)) {
return new ResolvedVersion($version, ResolvedVersion::SOURCE_EMCONF);
}

$version = (new ComposerReader($path))->getVersion();
if ($versionValidator->isValid($version)) {
return new ResolvedVersion($version, ResolvedVersion::SOURCE_COMPOSER);
}
Comment on lines +89 to +97

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd give composer.json a higher priority than ext_emconf.php, since this reflects the new source of truth.


throw new VersionMissingException(
'The version must either be set as argument, or be available in the tag of the checked out commit, '
. 'in `ext_emconf.php` or in `composer.json`.',
1786492802
);
}

/**
* Move the extension key to its argument if it was passed as only argument.
*
* Since the version argument comes first and is optional, a single argument
* is ambiguous. Extension keys never look like a version though, so
* `ter:publish my_extension` can safely be told apart from `ter:publish 1.2.3`.
*/
public static function normalizeVersionAndExtensionKeyArguments(InputInterface $input): void
{
if (!$input->hasArgument('version') || !$input->hasArgument('extensionkey')) {
return;
}

$version = (string)($input->getArgument('version') ?? '');
$extensionKey = (string)($input->getArgument('extensionkey') ?? '');

if ($extensionKey !== '' || !preg_match('/^[a-z][a-z0-9_]+$/', $version)) {
return;
}

$input->setArgument('extensionkey', $version);
$input->setArgument('version', null);
}

/**
* The path of the extension to work with. Defaults to the current working directory.
*/
public static function getPathFromInput(InputInterface $input): string
{
$path = $input->hasOption('path') ? (string)($input->getOption('path') ?? '') : '';

return $path !== '' ? $path : (string)(getcwd() ?: '.');
}

public static function getExtensionKeyFromInput(InputInterface $input): string
{
// 1. CLI argument has highest priority
Expand All @@ -56,8 +146,8 @@ public static function getExtensionKeyFromInput(InputInterface $input): string
return $key;
}

// 2. composer.json is the recommended source
$extensionKeyFromComposer = (new ComposerReader())->getExtensionKey();
// 2. composer.json of the extension is the recommended source
$extensionKeyFromComposer = (new ComposerReader(self::getPathFromInput($input)))->getExtensionKey();
if ($extensionKeyFromComposer !== '') {
return $extensionKeyFromComposer;
}
Expand Down
Loading
Loading