From 7084308e39c48457ada24111aecec272aac4e5f9 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 26 Dec 2025 16:11:09 -0500 Subject: [PATCH 1/3] build: Invert version fetching. A lot of systems expect the version number to be in the package metadata so make that the source of truth and update the code to pull the version variable from there instead of the other way around. --- backend/docs/conf.py | 19 ++----------------- backend/pyproject.toml | 4 ++-- backend/sample_plugin/__init__.py | 6 +++++- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/backend/docs/conf.py b/backend/docs/conf.py index 6d2d8fa..798bfce 100644 --- a/backend/docs/conf.py +++ b/backend/docs/conf.py @@ -18,28 +18,13 @@ from subprocess import check_call from django import setup as django_setup - - -def get_version(*file_paths): - """ - Extract the version string from the file. - - Input: - - file_paths: relative path fragments to file with - version string - """ - filename = os.path.join(os.path.dirname(__file__), *file_paths) - version_file = open(filename, encoding="utf8").read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError('Unable to find version string.') +from importlib.metadata import version as get_version REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) -VERSION = get_version('../sample_plugin', '__init__.py') +VERSION = get_version('openedx-sample-plugin') # Configure Django for autodoc usage os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' django_setup() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4c3b515..e0178d3 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,6 +7,7 @@ name = "openedx-sample-plugin" description = "A sample backend plugin for the Open edX Platform" requires-python = ">=3.11" license="Apache-2.0" +version = "0.1.0" authors = [ {name = "Open edX Project", email = "oscm@openedx.org"}, ] @@ -24,7 +25,7 @@ keywords= [ "edx", ] -dynamic = ["version", "readme", "dependencies"] +dynamic = ["readme", "dependencies"] [project.entry-points."lms.djangoapp"] sample_plugin = "sample_plugin.apps:SamplePluginConfig" @@ -37,7 +38,6 @@ Homepage = "https://openedx.org/openedx/sample-plugin" Repository = "https://openedx.org/openedx/sample-plugin" [tool.setuptools.dynamic] -version = {attr = "sample_plugin.__version__"} readme = {file = ["README.rst", "CHANGELOG.rst"]} dependencies = {file = "requirements/base.in"} diff --git a/backend/sample_plugin/__init__.py b/backend/sample_plugin/__init__.py index 8ff2f9e..000bb65 100644 --- a/backend/sample_plugin/__init__.py +++ b/backend/sample_plugin/__init__.py @@ -2,4 +2,8 @@ A sample backend plugin for the Open edX Platform. """ -__version__ = "0.1.0" +from importlib.metadata import version as get_version + +# The name of the package is `openedx-sample-plugin` but __package__ is `sample_plugin` so we hardcode the name of the +# package here so that the version fetching works correctly. A lot of examples will show using `__package__`. +__version__ = get_version('openedx-sample-plugin') From a14a91cc9816d454d391faf266da4718d8e382f2 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 26 Dec 2025 16:27:26 -0500 Subject: [PATCH 2/3] build: Add minimal config needed for python-semantic-release --- backend/pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e0178d3..5434df0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -44,3 +44,7 @@ dependencies = {file = "requirements/base.in"} [tool.setuptools.packages.find] include = ["sample_plugin*"] exclude = ["sample_plugin.tests*"] + +[tool.semantic_release.changelog.default_templates] +changelog_file = "CHANGELOG.rst" +output_format = "rst" From 457eac5ba2398c459d708b7b9b35e07b135fa595 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 29 Dec 2025 14:25:10 -0500 Subject: [PATCH 3/3] build: Add a github workflow for python-semantic-release This should release the plugin to PyPI on new merges to main. --- .github/workflows/ci.yml | 5 +- .github/workflows/release.yml | 93 +++++++++++++++++++++++++++++++++++ backend/pyproject.toml | 3 +- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9de30b..a315c78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,12 @@ name: Python CI on: - push: - branches: [main] pull_request: branches: - "**" + # This is so we can call CI locally from other workflows that might want to + # run CI before doing whatever task they're doing. Like the release workflow. + workflow_call: defaults: run: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..60466e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,93 @@ +name: Python Semantic Release + +on: + push: + branches: [main] + +jobs: + run_tests: + uses: ./.github/workflows/ci.yml + + release: + needs: run_tests + runs-on: ubuntu-latest + if: github.ref_name == 'main' + concurrency: + group: ${{ github.workflow }}-release-${{ github.ref_name }} + cancel-in-progress: false + + permissions: + contents: write + + steps: + # Note: We checkout the repository at the branch that triggered the workflow. + # Python Semantic Release will automatically convert shallow clones to full clones + # if needed to ensure proper history evaluation. However, we forcefully reset the + # branch to the workflow sha because it is possible that the branch was updated + # while the workflow was running, which prevents accidentally releasing un-evaluated + # changes. + - name: Setup | Checkout Repository on Release Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Setup | Force release branch to be at workflow sha + run: | + git reset --hard ${{ github.sha }} + + - name: Action | Semantic Version Release + id: release + # Adjust tag with desired version if applicable. + uses: python-semantic-release/python-semantic-release@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: "github-actions" + git_committer_email: "actions@users.noreply.github.com" + working-directory: './backend' + + - name: Publish | Upload to GitHub Release Assets + uses: python-semantic-release/publish-action@v10.5.3 + if: steps.release.outputs.released == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.release.outputs.tag }} + working-directory: './backend' + + - name: Upload | Distribution Artifacts + uses: actions/upload-artifact@v4 + with: + name: distribution-artifacts + path: dist + if-no-files-found: error + working-directory: './backend' + + outputs: + released: ${{ steps.release.outputs.released || 'false' }} + + deploy: + # 1. Separate out the deploy step from the publish step to run each step at + # the least amount of token privilege + # 2. Also, deployments can fail, and its better to have a separate job if you need to retry + # and it won't require reversing the release. + runs-on: ubuntu-latest + needs: release + if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + + permissions: + contents: read + id-token: write + + steps: + - name: Setup | Download Build Artifacts + uses: actions/download-artifact@v4 + id: artifact-download + with: + name: distribution-artifacts + path: dist + + - name: Publish to PyPi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + user: __token__ + password: ${{ secrets.PYPI_UPLOAD_TOKEN }} diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 5434df0..166b189 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -46,5 +46,4 @@ include = ["sample_plugin*"] exclude = ["sample_plugin.tests*"] [tool.semantic_release.changelog.default_templates] -changelog_file = "CHANGELOG.rst" -output_format = "rst" +changelog_file = "../CHANGELOG.md"