-
Notifications
You must be signed in to change notification settings - Fork 61
docs: feature flag handling #217
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
Closed
MaximilianSoerenPollak
wants to merge
4
commits into
eclipse-score:main
from
MaximilianSoerenPollak:MaximilianSoerenPollak-packaging-concept
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
4c0b2e2
docs: Added ability to disable requirements
MaximilianSoerenPollak bb8671f
docs: Added tests to packaging
MaximilianSoerenPollak 6ddf262
docs: Configuring of requirements
MaximilianSoerenPollak a234a0f
docs: Feature Flag handling
MaximilianSoerenPollak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
docs/_tooling/extensions/score_feature_flag_handling/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
|
||
# Modularity Sphinx Extension | ||
|
||
A Sphinx extension that enables conditional documentation rendering based on feature flags. | ||
|
||
## Overview | ||
|
||
The extension consists of two main components: | ||
|
||
1. `feature_flags.bzl`: A Bazel translation layer that converts commands into tags | ||
2. `score_feature_flag_handling`: A Sphinx extension that filters documentation based on feature flags | ||
|
||
The list of tags to filter is built in 'reverse' from the features enabled. In an empty configuration all tags will be added to the 'disable' list. By adding a feature you remove the corresponding tags from the list, therefore enabling requirements with those tags. | ||
|
||
**The extension is set up so it only disables requirements where *all* tags are contained within the filtered ones.** | ||
|
||
|
||
## Usage | ||
|
||
### Command Line | ||
|
||
Configure features when building documentation: | ||
|
||
```bash | ||
docs build //docs:docs --//docs:feature1=true | ||
``` | ||
|
||
## Configuration | ||
|
||
### Feature Flags (feature_flags.bzl) | ||
|
||
The `feature_flags.bzl` file takes care of the following things: | ||
|
||
- Feature-to-tag translation | ||
- Feature name to flag mappings | ||
- Default values for flags | ||
- Temporary file generation for flag storage | ||
|
||
|
||
Example configuration: | ||
|
||
Mapping of features to tags is defined as follows: | ||
|
||
```bzl | ||
FEATURE_TAG_MAPPING = { | ||
"feature1": ["some-ip", "tag2"], # --//docs:feature1=true -> will display requirements with those tags | ||
"second-feature": ["test-feat", "tag6"], | ||
} | ||
``` | ||
|
||
```bzl | ||
def define_feature_flags(name): | ||
bool_flag( | ||
name = "feature1", | ||
build_setting_default = False, | ||
) | ||
bool_flag( | ||
name = "second-feature", | ||
build_setting_default = False, | ||
) | ||
|
||
feature_flag_translator( | ||
name = name, | ||
flags = {":feature1": "True", ":second-feature": "True"}, | ||
) | ||
``` | ||
As can be seen here, each flag needs to be registered as well as have a default value defined. | ||
After adding new flags they can be added to the build command like so: `--//docs:<flag-name>=<value>` | ||
|
||
If a feature flag is enabled requirements with corresponding tags are now **enabled**. | ||
|
||
|
||
### Sphinx Extension (score_feature_flag_handling) | ||
|
||
The extension processes the temporary flag file and disables documentation sections tagged with disabled features. | ||
|
||
#### Use in requirements | ||
|
||
All requirements which tags are **all** contained within the tags that we look for, will be disabled. | ||
Here an example to illustrate the point. | ||
We will disable the `test-feat` tag via feature flags. If we take the following example rst: | ||
```rst | ||
.. tool_req:: Test_TOOL | ||
:id: TEST_TOOL_REQ | ||
:tags: feature1, test-feat | ||
:satisfies: TEST_STKH_REQ_1, TEST_STKH_REQ_20 | ||
|
||
We will see that this should still be rendered but 'TEST_STKH_REQ_1' will be missing from the 'satisfies' option | ||
|
||
.. stkh_req:: Test_REQ disable | ||
:id: TEST_STKH_REQ_1 | ||
:tags: test-feat | ||
|
||
This is a requirement that we would want to disable via the feature flag | ||
|
||
.. stkh_req:: Test_REQ do not disable | ||
:id: TEST_STKH_REQ_20 | ||
:tags: feature1 | ||
|
||
This requirement will not be disabled. | ||
``` | ||
We can then build it with our feature flag enabled via `bazel build //docs:docs --//docs:second-feature=true` | ||
This will expand via our translation layer `feature_flag.bzl` into the tags `test-feat` and `tag5`. | ||
|
||
**The extension is set up so it only disables requirements where *all* tags are contained within the filtered ones.** | ||
In the rst above `TEST_TOOL_REQ` has the `test-feat` tag but it also has the `feature1` tag, therefore it won't be disabled. | ||
In contrast, TEST_STKH_REQ_20 has only the `test-feat` tag, therefore it will be disabled and removed from any links as well. | ||
|
||
If we now look at the rendered HTML: | ||
 | ||
|
||
We can confirm that the `TEST_STKH_REQ_1` requirement is gone and so is the reference to it from `TEST_TOOL_REQ`. | ||
|
||
|
||
*However, keep in mind that in the source code the actual underlying RST has not changed, it's just the HTML.* | ||
This means that one can still see all the requirements there and also if searching for it will still find the document where it was mentioned. | ||
|
||
|
||
### How the extension achieves this? | ||
|
||
The extension uses a sphinx-needs built-in option called `hide`. If a need has the `hide=True` it will not be shown in the final HTML. | ||
It also gathers a list of all 'hidden' requirements as it then in a second iteration removes these from any of the possible links. | ||
|
||
#### Special cases | ||
|
||
Needpie, Needtable etc. are special cases as these are not requirements. There are two wrappers inside `filter_overwrite.py` that add the general functionality of hiding all requirements that have `hide==True`. Therefore enabling normal use, without special restrictions needed to be adhered to. | ||
|
||
### Decision Record | ||
|
||
Please see [Decision Record](/docs/_tooling/extensions/modularity/decision_record.md) for more information. |
115 changes: 115 additions & 0 deletions
115
docs/_tooling/extensions/score_feature_flag_handling/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# ******************************************************************************* | ||
# Copyright (c) 2025 Contributors to the Eclipse Foundation | ||
# | ||
# See the NOTICE file(s) distributed with this work for additional | ||
# information regarding copyright ownership. | ||
# | ||
# This program and the accompanying materials are made available under the | ||
# terms of the Apache License Version 2.0 which is available at | ||
# https://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# ******************************************************************************* | ||
from pprint import pprint | ||
from sphinx.application import Sphinx | ||
from sphinx_needs.data import SphinxNeedsData | ||
from sphinx_needs.config import NeedsSphinxConfig | ||
from sphinx.environment import BuildEnvironment | ||
from sphinx.util import logging | ||
from .filter_overwrite import wrap_filter_common | ||
import os | ||
import json | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def read_filter_tags(app: Sphinx) -> list: | ||
""" | ||
Helper function to read in the 'filter_tags' provided by `feature_flag.bzl`. | ||
|
||
Args: | ||
app: The current running Sphinx-Build application | ||
|
||
Returns: | ||
- List of tags e.g. ['some-ip','another-tag'] | ||
|
||
Errors: | ||
- If 'filter_tags_file_path' can't be found in the config | ||
- If something goes wrong when reading the file | ||
""" | ||
# asserting our worldview | ||
assert hasattr( | ||
app.config, "filter_tags_file_path" | ||
), "Config missing filter_tags_file_path, this is mandatory." | ||
filter_file = app.config.filter_tags_file_path | ||
logger.debug(f"Found the following filter_tags_file_path: {filter_file}") | ||
try: | ||
with open(filter_file, "r") as f: | ||
content = f.read().strip() | ||
filter_tags = [tag.strip() for tag in content.split(",")] if content else [] | ||
logger.debug(f"found: {len(filter_tags)} filter tag.") | ||
logger.debug(f"filter tags found: {filter_tags}") | ||
return filter_tags | ||
except Exception as e: | ||
logger.error(f"could not read file: {filter_file}. Error: {e}") | ||
raise e | ||
|
||
|
||
def hide_needs(app: Sphinx, env: BuildEnvironment) -> None: | ||
""" | ||
Function that hides needs (requirements) if *all* of their tags are in the specified tags to hide. | ||
Also deletes any references to hidden requirements, e.g. in 'satisfies' option. | ||
|
||
Args: | ||
app: The current running Sphinx-Build application, this will be supplied automatically | ||
env: The current running BuildEnvironment, this will be supplied automatically | ||
""" | ||
filter_tags = read_filter_tags(app) | ||
|
||
# Early return if no work needs to be done | ||
if not filter_tags: | ||
logger.debug("filter_tags was empty, no work to be done.") | ||
return | ||
need_data = SphinxNeedsData(env) | ||
needs = need_data.get_needs_mutable() | ||
extra_links = [x["option"] for x in NeedsSphinxConfig(env.config).extra_links] | ||
rm_needs = [] | ||
for need_id, need in needs.items(): | ||
if need["hide"]: | ||
rm_needs.append(need_id) | ||
if ( | ||
need["tags"] | ||
and all(tag in filter_tags for tag in need["tags"]) | ||
and not need["hide"] | ||
): | ||
rm_needs.append(need_id) | ||
need["hide"] = True | ||
|
||
needs_to_disable = set(rm_needs) | ||
|
||
logger.debug(f"found {len(needs_to_disable)} requirements to be disabled.") | ||
logger.debug(f"requirements found: {needs_to_disable}") | ||
|
||
# Remove references | ||
for need_id in needs: | ||
for opt in extra_links: | ||
needs[need_id][opt] = [ | ||
x for x in needs[need_id][opt] if x not in needs_to_disable | ||
] | ||
needs[need_id][opt + "_back"] = [ | ||
x for x in needs[need_id][opt + "_back"] if x not in needs_to_disable | ||
] | ||
|
||
|
||
def setup(app): | ||
logger.debug("score_feature_flag_handling extension loaded") | ||
app.add_config_value("filter_tags_file_path", None, "env") | ||
app.connect("env-updated", hide_needs) | ||
|
||
wrap_filter_common() | ||
return { | ||
"version": "1.0", | ||
"parallel_read_safe": True, | ||
"parallel_write_safe": True, | ||
} |
47 changes: 47 additions & 0 deletions
47
docs/_tooling/extensions/score_feature_flag_handling/decision_record.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Decision Record: How to disabale requirements | ||
|
||
## Context | ||
[Feature Flags](https://eclipse-score.github.io/score/process/guidance/feature_flags/index.html#feature-flags) require the functionality | ||
of disable or enabeling requirements based on which feature flags were provided, in order to build relevant documentation only. | ||
This decision record will contain what implementation was choosen to enable this feature. | ||
|
||
## Decision | ||
Use the 'hide' option from sphinx-needs objects to remove/disable requirements. | ||
|
||
## Chosen Solution: 'hide' Option | ||
The 'hide' option was selected as the primary implementation method. | ||
|
||
### Advantages | ||
- Can be set programmatically with minimal complexity | ||
- Maintains document structure integrity | ||
- Provides clear control over element visibility | ||
- Predictable behavior in programmatic contexts | ||
|
||
### Negatives | ||
- Requirements still appear in the rst via 'view source code' | ||
- Information only saved inside the `NeedsInfoType` objects | ||
|
||
## Alternatives Considered | ||
|
||
### Alternative 1: `:delete:` Option | ||
**Why Not Chosen:** | ||
- Lacks programmatic control capabilities. Can only be set hardcoded inside rst files | ||
|
||
More info: [Delete option docs](https://sphinx-needs.readthedocs.io/en/latest/directives/need.html#delete) | ||
|
||
### Alternative 2: `del_need` Method | ||
**Why Not Chosen:** | ||
- Introduces complexity in document node management | ||
- Creates challenges in locating correct manipulation points | ||
- Risk of document structure corruption during build | ||
- Increases maintenance overhead due to complex node relationships | ||
|
||
More info: [del_need docs](https://sphinx-needs.readthedocs.io/en/latest/api.html#sphinx_needs.api.need.del_need) | ||
|
||
### Alternative 3: `variations` Approach | ||
**Why Not Chosen:** | ||
- Filtering behavior appears inconsistent or non functional when used programmatically | ||
- Requires hardcoding in configuration files for reliable operation | ||
|
||
More info: [variations docs](https://sphinx-needs.readthedocs.io/en/latest/configuration.html#needs-variants) | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
There is a specific event for this
needs-before-sealing
;func(app, needs)
, as mentioned in https://sphinx-needs.readthedocs.io/en/latest/changelog.html#improvements-to-filtering-at-scaleThere 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.
Hmm interesting, I must have made a mistake then. I tried this in the beginning but decided not go go with it as something didn't quite work. I will check it out again, as if we have such specific events it would be nicer to call them correctly.
Thanks for pointing it out.