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..3ad1a82 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Python CI + +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" + + - 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 }} + + - name: Upload | Distribution Artifacts + uses: actions/upload-artifact@v4 + with: + name: distribution-artifacts + path: dist + if-no-files-found: error + + 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/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..5434df0 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,10 +38,13 @@ 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"} [tool.setuptools.packages.find] include = ["sample_plugin*"] exclude = ["sample_plugin.tests*"] + +[tool.semantic_release.changelog.default_templates] +changelog_file = "CHANGELOG.rst" +output_format = "rst" diff --git a/backend/sample_plugin/__init__.py b/backend/sample_plugin/__init__.py index 8ff2f9e..e84b710 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 `opnedx-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')