-
-
Notifications
You must be signed in to change notification settings - Fork 22
Automatically generate changelog revision text if the changelog was not hand-crafted - do not commit if unchanged #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automatically generate changelog revision text if the changelog was not hand-crafted - do not commit if unchanged #58
Conversation
- Use a third-level heading for the opening statement, and chnage from "Release ..." to "Release Notes for ..." - Rewrite '----' and '====' style headings to appropriate `#` style headings (consistency) Signed-off-by: Matthew Weier O'Phinney <[email protected]>
This patch accomplishes several goals: - When a changelog entry from a CHANGELOG.md file does not contain any changes, its contents are removed. - The contents from jwage changelog generator either replace those in for the entry in CHANGELOG.md (if it had no changes), or append to it, ALWAYS. These are then COMMITTED to the file. To accomplish this, I needed to do the following: - I created the class `ChangelogReleaseNotes`. It aggregates changelog contents, and, optionally, a phly/keep-a-changelog `ChangelogEntry` instance. It defines three behavior methods in addition to its `contents()` value method: - `requiresUpdatingChangelogFile()`, which returns true if there is a changelog entry instance AND its contents differ from those of the `ChangelogReleaseNotes` instance itself. - `writeChangelogFile(string $changelogFile)` will write the contents aggregated in the instance to the specified changelog file, per the composed `$changelogEntry` instance - `merge(ChangelogReleaseNotes $next): ChangelogReleaseNotes` will create a new instance that merges the current with the one provided. The contents of the one provided are appended to the original using a separator, and if either compose a `ChangelogEntry` instance, it will be used. (If both define one, this is considered undefined behavior.) - I updated the CreateReleaseText interface to return a `ChangelogReleaseNotes` instance. - I updated the `CreateReleaseTextViaKeepAChangelog` to compose a `Clock` instance, and to inline logic for creating a `ChangelogEntry` instance when parsing the `CHANGELOG.md` file for the related version. Once it has, it removes the entry if it is unchanged since initialization, and sets the date for the release before seeding a `ChangelogReleaseNotes` instance to return. - I updated `CreateReleaseTextThroughChangelog` to create and return a `ChangelogReleaseNotes` instance with the text it creates. - I renamed `ConcatenateMultipleReleaseTexts` to `MergeMultipleReleaseNotes`, and have it now using `array_reduce` on its aggregated items to `merge()` each result with the previous, returning the merged `ChangelogReleaseNotes` instance. - I updated the `CommitReleaseChangelog` interface to accept a `ChangelogReleaseNotes` instance as the first argument. It then test to see if it has changes via `requiresUpdatingChangelogFile()`, and, if so, it uses its `writeChangelogFile()` method to write the changes before committing them. - In `ReleaseCommand`, I inverted the release text creation and commit operations, and capture the `ChangelogReleaseNotes` for use with both the commit and the release operations. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
Adds information detailing user-facing changes of laminas#58. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One potential bug, some space for refactoring, otherwise requires commenting on tricky logic that parses release notes.
I think this needs only quick adjustments, and then we can release it as 1.3.0
$milestone, | ||
$repositoryName, | ||
$releaseVersion, | ||
$releaseBranch, | ||
$repositoryPath | ||
); | ||
|
||
($this->commitChangelog)($changelogReleaseNotes, $repositoryPath, $releaseVersion, $releaseBranch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like how this now receives a pre-computed changelog 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you might. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This made me happy as well. The "commit, then create" from before worked, and was necessary based on what we were doing, but this makes sooooo much more sense.
$lines = explode("\n", trim($changelog)); | ||
$lineCount = count($lines); | ||
$linesToRemove = []; | ||
for ($i = 0; $i < $lineCount; $i += 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code block is really hard to understand. I suppose most of it is tested well, according to mutation testing results, but it is really making the reader's head spin.
Refactoring can be deferred, since the code is functionally pure, but we need comments about what is being done here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about using foreach
instead ?
for ($i = 0; $i < $lineCount; $i += 1) { | |
foreach ($lines as $i => $line) { |
We can eliminate usage of $lineCount = count($lines);
); | ||
|
||
Assert::isInstanceOf($releaseNotes, ChangelogReleaseNotes::class); | ||
Assert::notEmpty($releaseNotes->contents()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this assertion needed? I think ->contents()
already returns a non-empty-string
use PHPUnit\Framework\TestCase; | ||
|
||
final class CreateChangelogTextTest extends TestCase | ||
{ | ||
public function testGeneratedReleaseText(): void | ||
{ | ||
/** @psalm-var GenerateChangelog&MockObject $generateChangelog */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This inline type shouldn't be needed: phpunit mocking utilities already have the correct annotations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was in part for intelephense code completion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't intelephense read something like .phpstorm.meta.php
?
private function normalizeChangelogHeadings(string $changelog): string | ||
{ | ||
$lines = explode("\n", trim($changelog)); | ||
$lineCount = count($lines); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be removed if next for
changed to foreach
*/ | ||
public function __invoke( | ||
Milestone $milestone, | ||
RepositoryName $repositoryName, | ||
SemVerVersion $semVerVersion, | ||
BranchName $sourceBranch, | ||
string $repositoryDirectory | ||
): string; | ||
): ChangelogReleaseNotes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BC Break 😱 Technically then this feature should go to v2.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package has "type": "application"
and is not published to packagist either - we can break internals as much as we want (and feel free to do so!)
Requires making the `writeChangelogFile()` method static. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
Details the algorithms for each, and what they are expected to accomplish. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
Already guaranteed by `ChangelogReleaseNotes` method definition. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
- Adds `@covers` annotation to `ChangelogReleaseNotesTest` - Removes unnecessary variable annotation in `CreateChangelogTextTest` Signed-off-by: Matthew Weier O'Phinney <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚢
Ah, can't merge this due to mutation score going down quite a lot. This is most likely due to the complex logic in the changelog text generation/extraction code: we can either test it more carefully (more string examples, effectively), or rewrite bits of it to be more functional-oriented, and removing prodcedural/iteration bits that are more mutation-prone (yes, FP has the advantage of reducing code paths :D ) |
Requires fully qualified class name, not one derived from import statements. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
Updates test cases to ensure all paths of parsers and normalizers are tested. Signed-off-by: Matthew Weier O'Phinney <[email protected]>
Mutation score fixed, @Ocramius 😄 |
To simplify setup in our own repos, I'm adding another example workflow file that uses the organization admin token in the release step, to allow triggering a release workflow event (which will be necessary for us to trigger documenation builds). Signed-off-by: Matthew Weier O'Phinney <[email protected]>
@@ -0,0 +1,57 @@ | |||
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example needs a better comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - the documentation of the new example can wait a bit
Description
When rolling out automatic-releases to various repositories, we noticed an issue with the workflow for documentation, mainly that our docs-build action only builds on pushes to the "master" branch, we are trying to eliminate. This has meant manually pushing the current release branch to the "master" branch on the repository whenever there are changes.
@Ocramius suggested that we merge documentation changes just like bugfixes: assign them to milestones, and build on release. The objections raised were:
We would have unchanged packages from one version to the next, as docs are excluded via
.gitattributes
. We decided this was not really an issue; most people are not updating regularly, and a package that introduces zero code changes is generally welcome as it represents no risk.We would have empty changelogs and release announcements.
This latter was something I realized we could address in the automatic-releases tooling, as:
entry:*
commands; the only requirements are the version headings.As such, this patch refactors how changelogs are generated for release notes, and then committed back to the repository.
In particular:
It introduces
Laminas\AutomaticReleases\Changelog\ChangelogReleaseNotes
, which is an entity that composes the changelog contents, and optionally a phly/keep-a-changelogChangelogEntry
instance. It has behavior:requiresUpdatingChangelogFile()
returns true if aChangelogEntry
is composed AND its contents differ from those stored in theChangelogReleaseNotes
instance.writeChangelogFile(string $changelogFile)
will write the release notes contents to the specified changelog file, per the metadata in theChangelogEntry
instance composed.merge(ChangelogReleaseNotes $next): ChangelogReleaseNotes
will clone itself and merge$next
into it, returning the clone.I updated
CreateReleaseText
to have it return aChangelogReleaseNotes
instance.I updated
CreateReleaseTextViaKeepAChangelog
to now mimics how the phly/keep-a-changelog library parses a changelog file when parsing string contents representing a full changelog as pulled from the originating branch, in order to create aChangelogEntry
that details where the original is found in the file, and how much space it takes. Additionally, it then sets the date for the version, and checks to see if the contents have changed from the template; if not, it clears them before passing them to theChangelogReleaseNotes
instance it creates.I updated
CreateReleaseThroughChangelog
to perform some minor markdown formatting changes to better accommodate being injected in a CHANGELOG.md file; it also now returns aChangelogReleaseNotes
instance (without aChangelogEntry
composed).I updated
CommitReleaseChangelog
to accept aChangelogReleaseNotes
as the first argument; implementations now use it to actually write theCHANGELOG.md
file prior to committing and pushing it.I renamed
ConcatenateMultipleReleaseTexts
toMergeMultipleReleaseNotes
to better describe what it now does (it uses themerge()
operation of returnedChangelogReleaseNotes
instances, returning the final product).I updated the logic of
ReleaseCommand
to generate release text first, then commit it.tl;dr
When the "release" command of automatic-releases now runs, it will:
CHANGELOG.md
file is present, it then:The upshot is that documentation maintainers can:
and get a release issued, which will then trigger builds for documentation.
Final note
We will need to use the
ORGANIZATION_ADMIN_TOKEN
for the value of theGITHUB_TOKEN
on theRelease
step of the workflow in order to trigger the release workflow event, and thus the documentation build.