diff --git a/doc/source/reference/release_models.rst b/doc/source/reference/release_models.rst index 62589d3345..792706c3fb 100644 --- a/doc/source/reference/release_models.rst +++ b/doc/source/reference/release_models.rst @@ -148,6 +148,22 @@ projects. * Release tags for deliverables using this tag are managed without oversight from the Release Management team. +.. _abandoned: + +abandoned +========= + +As time passes, some deliverables are abandoned, as they are +no longer useful, or their functionality is absorbed by another deliverable. +For cycle-tied release models they just disappear in the next cycle. However +deliverables with a cycle-independent model just stay around. + +The 'abandoned' release model describes a formally-independent deliverable +that will no longer be released, because it changed release models or +because it was abandoned. + +* "abandoned" deliverables never produce new releases. + .. _untagged: untagged diff --git a/openstack_releases/cmds/validate.py b/openstack_releases/cmds/validate.py index 197c37eb27..bb3f7e4b0b 100644 --- a/openstack_releases/cmds/validate.py +++ b/openstack_releases/cmds/validate.py @@ -552,11 +552,12 @@ def validate_model(deliv, context): 'no release-model specified', ) - if deliv.model == 'independent' and deliv.series != 'independent': - # If the project is release:independent, make sure - # that's where the deliverable file is. + if (deliv.model in ['independent', 'abandoned'] + and deliv.series != 'independent'): + # If the project is release:independent or abandoned, make sure + # the deliverable file is in _independent. context.error( - 'uses the independent release model ' + 'uses the independent or abandoned release model ' 'and should be in the _independent ' 'directory' ) @@ -566,10 +567,11 @@ def validate_model(deliv, context): # bypass the model property because that always returns # 'independent' for deliverables in that series. model_value = deliv.data.get('release-model', 'independent') - if deliv.series == 'independent' and model_value != 'independent': + if (deliv.series == 'independent' + and model_value not in ['independent', 'abandoned']): context.error( 'deliverables in the _independent directory ' - 'should all use the independent release model' + 'should use either the independent or abandoned release models' ) if deliv.model == 'untagged' and deliv.is_released: @@ -921,6 +923,15 @@ def validate_pypi_permissions(deliv, context): sorted(uploaders), pypi_name)) +@skip_existing_tags +@applies_to_released +def validate_deliverable_is_not_abandoned(deliv, context): + "Ensure the deliverable is not an independent abandoned deliverable." + + if deliv.model == 'abandoned': + context.error('Abandoned deliverables should not see new releases') + + @skip_existing_tags @applies_to_released def validate_release_sha_exists(deliv, context): @@ -1848,6 +1859,7 @@ def main(): # Check readme after sdist build to slightly optimize things validate_pypi_readme, validate_gitreview, + validate_deliverable_is_not_abandoned, validate_release_sha_exists, validate_existing_tags, validate_version_numbers, diff --git a/openstack_releases/deliverable.py b/openstack_releases/deliverable.py index 582cc62d9f..4e97a11a09 100644 --- a/openstack_releases/deliverable.py +++ b/openstack_releases/deliverable.py @@ -453,9 +453,10 @@ class Deliverable(object): @property def model(self): - if self.is_independent: + model = self._data.get('release-model', '') + if self.is_independent and model != 'abandoned': return 'independent' - return self._data.get('release-model', '') + return model @property def is_independent(self): diff --git a/openstack_releases/schema.yaml b/openstack_releases/schema.yaml index 43da386195..9ee07e1212 100644 --- a/openstack_releases/schema.yaml +++ b/openstack_releases/schema.yaml @@ -30,7 +30,7 @@ properties: type: "boolean" release-model: type: "string" - enum: ["cycle-with-intermediary", "cycle-with-milestones", "cycle-trailing", "untagged", "cycle-with-rc", "cycle-automatic"] + enum: ["cycle-with-intermediary", "cycle-with-milestones", "cycle-trailing", "untagged", "cycle-with-rc", "cycle-automatic", "abandoned"] type: type: "string" enum: ["horizon-plugin", "library", "client-library", "service", "tempest-plugin", "other"] diff --git a/openstack_releases/tests/test_validate.py b/openstack_releases/tests/test_validate.py index 8944da3690..b5a5646960 100644 --- a/openstack_releases/tests/test_validate.py +++ b/openstack_releases/tests/test_validate.py @@ -486,6 +486,32 @@ class TestValidateModel(base.BaseTestCase): self.assertEqual(0, len(self.ctx.warnings)) self.assertEqual(1, len(self.ctx.errors)) + def test_with_model_abandoned_match(self): + validate.validate_model( + deliverable.Deliverable( + team='team', + series='independent', + name='name', + data={'release-model': 'abandoned'}, + ), + self.ctx, + ) + self.assertEqual(0, len(self.ctx.warnings)) + self.assertEqual(0, len(self.ctx.errors)) + + def test_with_model_abandoned_nomatch(self): + validate.validate_model( + deliverable.Deliverable( + team='team', + series='ocata', + name='name', + data={'release-model': 'abandoned'}, + ), + self.ctx, + ) + self.assertEqual(0, len(self.ctx.warnings)) + self.assertEqual(1, len(self.ctx.errors)) + def test_with_independent_and_model(self): validate.validate_model( deliverable.Deliverable( @@ -535,6 +561,38 @@ class TestValidateModel(base.BaseTestCase): self.assertEqual(1, len(self.ctx.errors)) +class TestValidateNotAbandoned(base.BaseTestCase): + + def setUp(self): + super().setUp() + self.ctx = validate.ValidationContext() + gitutils.clone_repo(self.ctx.workdir, 'openstack/release-test') + + def test_new_release_on_abandoned_deliverable(self): + deliv = deliverable.Deliverable( + team='team', + series='independent', + name='name', + data={ + 'release-model': 'abandoned', + 'artifact-link-mode': 'none', + 'releases': [ + {'version': '0.8.1', + 'projects': [ + {'repo': 'openstack/release-test', + # hash from master + 'hash': '218c9c82f168f1db681b27842b5a829428c6b5e1', + 'tarball-base': 'openstack-release-test'}, + ]} + ], + } + ) + validate.validate_deliverable_is_not_abandoned(deliv, self.ctx) + self.ctx.show_summary() + self.assertEqual(0, len(self.ctx.warnings)) + self.assertEqual(1, len(self.ctx.errors)) + + class TestValidateReleaseSHAExists(base.BaseTestCase): def setUp(self):