diff --git a/draft-release/README.md b/draft-release/README.md index 4af6c8a..1df3c11 100644 --- a/draft-release/README.md +++ b/draft-release/README.md @@ -24,7 +24,11 @@ Input files: chronological order. The newest entry should contain a header with the phrase “development version”. - `VERSION` - a single-source version file. -- `CITATION.cff` - a citation file. (optional) +- `CITATION.cff` - a citation file. + +For a repository’s first release, set `version-tag` manually (for +example `v0.1.0`), because conventional-commit version detection may not +have a previous release to compare against. When you’re ready to draft a new release, [run the workflow manually](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow). diff --git a/draft-release/README.qmd b/draft-release/README.qmd index 27ed29f..437f19e 100644 --- a/draft-release/README.qmd +++ b/draft-release/README.qmd @@ -30,7 +30,11 @@ Input files: - `CHANGELOG.md` - a changelog or news file with entries in reverse chronological order. The newest entry should contain a header with the phrase “development version”. - `VERSION` - a single-source version file. -- `CITATION.cff` - a citation file. (optional) +- `CITATION.cff` - a citation file. + +For a repository's first release, set `version-tag` manually (for example +`v0.1.0`), because conventional-commit version detection may not have a +previous release to compare against. When you're ready to draft a new release, [run the workflow manually](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow). After the workflow completes, there will be a new draft release that you can review and choose to publish. diff --git a/draft-release/action.yml b/draft-release/action.yml index d0fb1d8..0439e84 100644 --- a/draft-release/action.yml +++ b/draft-release/action.yml @@ -70,6 +70,7 @@ runs: - name: Get current and next versions id: semver uses: ietf-tools/semver-action@v1 + continue-on-error: true with: token: ${{ inputs.github-token }} branch: ${{ github.ref_name }} diff --git a/src/ccbr_actions/release.py b/src/ccbr_actions/release.py index e7b82ac..ac0ec76 100644 --- a/src/ccbr_actions/release.py +++ b/src/ccbr_actions/release.py @@ -222,7 +222,23 @@ def prepare_draft_release( changelog_filepath = path_resolve(changelog_filepath) version_filepath = path_resolve(version_filepath) citation_filepath = path_resolve(citation_filepath) - assert all([f.is_file() for f in (changelog_filepath, version_filepath)]) + required_files = { + "changelog": changelog_filepath, + "version": version_filepath, + "citation": citation_filepath, + } + missing_required_files = [ + f"{name} ({filepath})" + for name, filepath in required_files.items() + if not filepath.is_file() + ] + if missing_required_files: + missing_required_files_str = ", ".join(missing_required_files) + raise FileNotFoundError( + "Missing required release file(s): " + f"{missing_required_files_str}. " + "Please create these files or update draft-release inputs." + ) next_version = get_release_version( next_version_manual=next_version_manual, @@ -234,7 +250,7 @@ def prepare_draft_release( set_output("NEXT_VERSION", next_version) changelog_lines, next_release_lines = get_changelog_lines( - latest_version_strict=current_version.lstrip("v"), + latest_version_strict=current_version.lstrip("v") if current_version else "", next_version_strict=next_version_strict, changelog_filepath=changelog_filepath, dev_header=dev_header, @@ -480,14 +496,27 @@ def get_release_version( ) else: next_version = next_version_convco + if not next_version: + raise ValueError( + "Unable to determine next release version. " + "If this is the first release for this repository, provide a manual next version " + "(draft-release input: version-tag)." + ) if not is_strict_semver(next_version, with_leading_v=with_leading_v): raise ValueError( f"Tag {next_version} does not match semantic versioning guidelines.\nView the guidelines here: https://semver.org/" ) # assert semantic version pattern - check_version_increments_by_one( - current_version, next_version, with_leading_v=with_leading_v - ) + if current_version: + if not is_strict_semver(current_version, with_leading_v=with_leading_v): + raise ValueError( + f"Current version {current_version} does not match semantic versioning guidelines.\n" + "If this is the first release for this repository, set current version to blank " + "and provide a manual next version (draft-release input: version-tag)." + ) + check_version_increments_by_one( + current_version, next_version, with_leading_v=with_leading_v + ) return next_version @@ -546,13 +575,18 @@ def get_changelog_lines( - next_release_lines (list): The list of lines that pertain to the next release. Raises: - ValueError: If any of the provided version strings do not match the semantic versioning pattern. + ValueError: If next_version_strict is blank or if any of the provided version strings do not match the semantic versioning pattern. """ - for version in [latest_version_strict, next_version_strict]: - if not match_semver(version): - raise ValueError( - f"Version {version} does not match semantic versioning pattern" - ) + if not next_version_strict: + raise ValueError("next_version_strict must not be blank") + if not match_semver(next_version_strict): + raise ValueError( + f"Version {next_version_strict} does not match semantic versioning pattern" + ) + if latest_version_strict and not match_semver(latest_version_strict): + raise ValueError( + f"Version {latest_version_strict} does not match semantic versioning pattern" + ) changelog_lines = list() next_release_lines = list() for_next = True @@ -560,7 +594,11 @@ def get_changelog_lines( for line in infile: if line.startswith("#") and dev_header in line: line = line.replace(dev_header, next_version_strict) - elif latest_version_strict in line: + elif ( + latest_version_strict + and line.startswith("#") + and latest_version_strict in line + ): for_next = False changelog_lines.append(line) diff --git a/tests/test_release.py b/tests/test_release.py index 0fd4335..f99524a 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -70,25 +70,25 @@ def test_prepare_draft_release(tmp_path, github_output_file, data_dir): ) -def test_prepare_draft_release_no_citation(github_output_file, data_dir_rel): - output = exec_in_context( - prepare_draft_release, - next_version_manual="v1.0.0", - next_version_convco="v1.0.0", - current_version="v0.9.10", - gh_event_name="push", - changelog_filepath=str(data_dir_rel / "example_changelog.md"), - dev_header="development version", - release_notes_filepath=str(data_dir_rel / "latest-release.md"), - version_filepath=str(data_dir_rel / "VERSION"), - citation_filepath="not/a/file.cff", - release_branch="release-draft", - pr_ref_name="PR_BRANCH_NAME", - repo="CCBR/actions", - debug=True, - ) - assert "git add" in output - assert ".cff" not in output +def test_prepare_draft_release_missing_required_file(github_output_file, data_dir_rel): + with pytest.raises(FileNotFoundError) as exc_info: + prepare_draft_release( + next_version_manual="v1.0.0", + next_version_convco="v1.0.0", + current_version="v0.9.10", + gh_event_name="push", + changelog_filepath=str(data_dir_rel / "example_changelog.md"), + dev_header="development version", + release_notes_filepath=str(data_dir_rel / "latest-release.md"), + version_filepath=str(data_dir_rel / "VERSION"), + citation_filepath="not/a/file.cff", + release_branch="release-draft", + pr_ref_name="PR_BRANCH_NAME", + repo="CCBR/actions", + debug=True, + ) + assert "Missing required release file(s)" in str(exc_info.value) + assert "citation" in str(exc_info.value) def test_create_release_draft(data_dir_rel): @@ -136,6 +136,17 @@ def test_get_changelog_lines(data_dir_rel): assert release_notes == ["\n", "development version notes go here\n", "\n"] +def test_get_changelog_lines_first_release(data_dir_rel): + new_changelog, release_notes = get_changelog_lines( + "", + "0.2.0", + changelog_filepath=str(data_dir_rel / "example_changelog.md"), + ) + assert new_changelog[0] == "## actions 0.2.0\n" + assert release_notes[:3] == ["\n", "development version notes go here\n", "\n"] + assert "## actions 0.1.0\n" in release_notes + + def test_get_changelog_lines_sinclair(data_dir_rel): new_changelog, release_notes = get_changelog_lines( "0.3.0", @@ -159,12 +170,19 @@ def test_get_changelog_lines_error(data_dir_rel): "alpha-0.2.0", changelog_filepath=str(data_dir_rel / "example_changelog.md"), ) + with pytest.raises(ValueError) as exc_info3: + get_changelog_lines( + "0.1.0", + "", + changelog_filepath=str(data_dir_rel / "example_changelog.md"), + ) assert "Version 0.1..9000 does not match semantic versioning pattern" in str( exc_info1.value ) assert "Version alpha-0.2.0 does not match semantic versioning pattern" in str( exc_info2.value ) + assert "next_version_strict must not be blank" in str(exc_info3.value) def test_get_release_version(): @@ -176,6 +194,22 @@ def test_get_release_version(): ) == "v1.10.0" ) + assert ( + get_release_version( + next_version_manual="v0.1.0", + next_version_convco="", + current_version="", + ) + == "v0.1.0" + ) + assert ( + get_release_version( + next_version_manual="", + next_version_convco="v0.1.0", + current_version="", + ) + == "v0.1.0" + ) assert ( get_release_version( next_version_manual="v1.10.0", @@ -186,6 +220,16 @@ def test_get_release_version(): ) +def test_get_release_version_first_release_missing_manual(): + with pytest.raises(ValueError) as exc_info: + get_release_version( + next_version_manual="", + next_version_convco="", + current_version="", + ) + assert "Unable to determine next release version" in str(exc_info.value) + + def test_get_release_version_warning(): with warnings.catch_warnings(): warnings.simplefilter("error") @@ -351,15 +395,17 @@ def test_prepare_draft_release_r_package(github_output_file, tmp_path, data_dir) def test_prepare_draft_release_warns_on_autoformat_trigger_failure( - github_output_file, tmp_path, monkeypatch + github_output_file, tmp_path, monkeypatch, data_dir ): changelog_file = tmp_path / "CHANGELOG.md" version_file = tmp_path / "VERSION" + citation_file = tmp_path / "CITATION.cff" notes_file = tmp_path / "latest-release.md" changelog_file.write_text( "## actions development version\n\nnotes\n\n## actions 0.1.0\n" ) version_file.write_text("0.1.0\n") + citation_file.write_text((data_dir / "CITATION.cff").read_text()) monkeypatch.setattr("ccbr_actions.release.precommit_run", lambda *_: None) monkeypatch.setattr( @@ -388,7 +434,7 @@ def _trigger_workflow_raises(**_): changelog_filepath=str(changelog_file), release_notes_filepath=str(notes_file), version_filepath=str(version_file), - citation_filepath="not/a/file/CITATION.cff", + citation_filepath=str(citation_file), release_branch="release-draft", pr_ref_name="feature/branch", repo="CCBR/actions",