conda-store Backwards Compatibility Policy
Introduction
In software development, it is essential to strike a balance between progress and maintaining a stable environment for existing users. This policy guides how the conda-store project will handle changes to software and services, ensuring that they do not disrupt the workflows of current users, while still enabling innovation and forward progress.
Breaking versus non-breaking changes
Breaking code changes refer to modifications or updates made to software that have the potential to disrupt the functionality of existing applications, integrations, or systems. Breaking changes involve:
- removing existing functionality
- altering existing functionality
- adding new requirements such as making a previously optional parameter required.
These changes can lead to compatibility issues, causing frustration for end-users, higher maintenance costs, and even system downtime, thus undermining the trust and reputation of the software or service provider. Changes are only breaking if they impact users through the REST API, the Python API, or the Database.
In contrast, non-breaking changes can add functionality or reduce requirements such as making a previously required parameter optional. These changes allow software to evolve and grow without negatively impacting existing users.
conda-store users should be able to upgrade to newer versions without worrying about breaking existing integrations. Newer features will be available for users to adopt when needed, while all existing code should continue to work.
Specific implementation guidance
Database changes
Databases are one of the most critical areas to ensure there are no breaking changes. Databases hold the state for the application. Introducing breaking changes to the database can be destructive to data and prevent rolling back to earlier versions of conda-store. To maintain backwards compatibility we follow these principles:
- New columns or tables should be added instead of removing or altering existing ones.
- Columns and tables should not be renamed. Aliases should be used for poorly named existing columns or tables.
REST API endpoints
REST API endpoints are versioned on a per-endpoint basis. New endpoints will
start at v1
.
Non-breaking changes do not require a new version of an endpoint. For REST API endpoints, examples on non-breaking changes are:
- adding a parameter to the return value of an endpoint
- making a previously mandatory input optional.
These changes can be done without a new endpoint version.
However, changes such as:
- removing a parameter from the return value
- altering the meaning of a value
- making a formerly optional parameter mandatory
are breaking changes and require a new endpoint version.
When a new version of an endpoint is created, then all new features will be added to the new version.
Older versions of API endpoints are still considered supported and will receive bug fixes and security updates to their features but new features will not be backported to them.
Experimental changes
conda-store will expose experimental features within the experimental
namespace.
For example, if a new version of the example.com/api/v1/user
endpoint is being
tested, but not yet considered stable, it can be made available at the
example.com/api/experimental/user
route. This allows conda-store contributors
to test new changes and get community feedback without committing to supporting
a new version of an API endpoint. Using the experimental
namespace is not
mandatory. However, deploying a versioned endpoint expresses a commitment to
support that code going forward, so it is highly recommended that developers use
the experimental
namespace to test new endpoints and features before marking
them as stable.
Experimental routes have no guarantees attached to them, they can be removed or changed at any time without warning. This allows testing features with users in real-world scenarios without needing to commit to support that feature as is.
Once endpoints are determined to be stable and functional, they will be moved into the existing latest version of the API endpoint for non-breaking changes or a new version for breaking changes.
Example HTTP routes
Versioned stable route
https://example.com/api/v1/user
https://example.com/api/v2/user
Explanation: This route has breaking changes between v1
and v2
. Code
written for the v1
endpoint will continue to function, but all new features
will only be available in the v2
version of the endpoint. This empowers users
to upgrade when they wish to rather than forcing them to do so.
Route that has never had breaking changes
https://example.com/api/v1/user
Explanation: This route has never had breaking changes introduced. Any code written against this endpoint will function regardless of when it was written.
New experimental
route
https://example.com/api/experimental/user
Explanation: This is an experimental route. It can be changed or removed at any moment without prior notice. This route should be used to test new features and get community feedback.
Removing versions of API endpoints
It is not recommended to remove versions of API endpoints. Removing API endpoints, or versions of endpoints, breaks backwards compatibility and should only be done under exceptional circumstances such as a security vulnerability.
If the desire is to prevent a developer from relying on an API endpoint, adding a warning to the API documentation along with a recommended alternative should be used rather than a deprecation or removal.
In the case of a removed endpoint, or endpoint version, conda-store should
return a status code of 410 Gone
to indicate the endpoint has been removed
along with a json object stating when and why the endpoint was removed and what
version of the endpoint is available currently (if any).
{
# the pull request that removed the endpoint
"reference_pull_request": "https://github.com/conda-incubator/conda-store/pull/0000",
# the date the endpoint was removed
"removal_date": "2021-06-24",
# the reason for the removal, ideally with a link to a CVE if one is available
"removal_reason": "Removed to address CVE-2021-32677 (https://nvd.nist.gov/vuln/detail/CVE-2021-32677)",
# the endpoint that developers should use as a replacement
"new_endpoint": "api/v3/this/should/be/used/instead",
}
If an API endpoint must be deprecated, a deprecation warning should be added for at least one release before the endpoint is removed. This requirement may be waived in the case of a serious security vulnerability.
It should always be clearly communicated in release notes and documentation when an API endpoint is deprecated or removed. This should include:
- version number of the release where this was deprecated
- provide suggestions for alternatives (if possible)
- provide justification for the removal (such as a link to the issue or CVE that necessitated the removal).
Python API
Public Python modules, classes, functions, methods, and variables are considered public APIs and subject to the same considerations as REST API endpoints. Any object with a leading underscore is not considered to be public. This convention is used in the Python community to designate an object as private.
Private examples:
_private_func_or_method
_PrivateClass
_PRIVATE_VAR
.
Public examples:
public_func_or_method
PublicClass
PUBLIC_VAR
.
The highest-level entity determines the visibility level.
For example:
class _Private:
# everything is private here even without underscores
def this_is_also_private(self): pass
or
def _foo():
def inner():
# inner is also private - no way to call it without calling _foo, which is
# private.
Tests are never considered to be part of the public API. Any code within the
tests/
directory is always considered to be private.
Developers are encouraged to make code private by default and only expose objects as public if there is an explicit need to do so. Keeping code private by default limits the public API that the conda-store project developers are committing to supporting.
Build keys
conda-store ships with several build key versions. The build key determines the
location of environment builds and build artifacts. Build key versions marked as
experimental can be changed at any time, see BuildKey
and the FAQ for more
information.
Deprecating Python APIs
Under exceptional circumstances such as a serious security vulnerability which can't be fixed without breaking changes, it may be necessary to deprecate, remove, or introduce breaking changes to objects in the public Python API. This should be avoided if possible.
If the desire is to prevent a developer from relying on a part of the Python API, adding a warning to the documentation along with a recommended alternative and a comment in the code should be used rather than a deprecation or removal.
"""
This function is deprecated [reason/details], use [replacement] instead
"""
If part of the Python API must be deprecated or removed, a deprecation warning should be added for at least one release before the endpoint is removed. This requirement may be waived in the case of a serious security vulnerability.
The deprecation or removal should always be clearly communicated in release notes and documentation. This should include:
- version number of the release where this was deprecated
- provide suggestions for alternatives (if possible)
- provide a reason for the deprecation or removal (such as a link to a CVE or issue that necessitated the removal).
Types of objects
Modules
A breaking change for a module means that any element of the module's public API has a breaking change.
Classes
In a public class, a breaking change is one that changes or removes attributes
or methods or alters their meanings. Rather than changing methods or
attributes, new methods or attributes should be added if needed. For example,
if you wanted to change a user id from an int
to a uuid
, a new attribute
User.uuid
should be added, and all new code should use User.uuid
. Existing
methods can use the new attribute or methods as well as long as that doesn't
introduce breaking changes for the method.
Functions and methods
For a function or a method, breaking changes alter its signature or the meaning of the return value.
This means that the parameters (inputs) and return values (outputs) must be the same. Internal logic may be changed as long as it does not change return values. Extra care should be taken when making these changes however. Changing the way a return value is calculated may result in subtle changes which are not obvious. For example, rounding a decimal versus truncating it may return different results even though the function signature remains the same.
The function signature also includes whether the function is an async function. Changing this is a breaking change.
For example, if there is a function list_envs
, which is synchronous, and
it should be asynchronous, a new function called list_envs_async
should be
added and list_envs
should be kept as a synchronous call.
Optional parameters may be added as long as they have a specified default value and additional fields may be added to return types if you are returning an object like a dict. These are considered non-breaking.
Variables and constants
Public variables should not have their type changed.
Public constants should not have their type or their value changed.