From a7bc40d51c689cb290c6b2e0980d82dea96af4fe Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:14:31 -0400 Subject: [PATCH 01/11] build: publish to PyPI via trusted publishing Both PyPI publish jobs already request the `id-token: write` permission, so gh-action-pypi-publish can mint its upload credential from the GitHub OIDC token instead of a long-lived API token stored in PYPI_UPLOAD_TOKEN. Dropping the explicit user/password lets the action take that path. This requires a trusted publisher to be configured on PyPI for each project (openedx-plugin-sample and tutor-contrib-sample), pointing at this repository and the release.yml workflow: https://docs.pypi.org/trusted-publishers/adding-a-publisher/ Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f03a32f..dc54ea1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,8 +132,6 @@ jobs: uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: packages-dir: backend-plugin-sample/dist - user: __token__ - password: ${{ secrets.PYPI_UPLOAD_TOKEN }} publish_tutor_plugin_to_pypi: runs-on: ubuntu-latest @@ -155,8 +153,6 @@ jobs: uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: packages-dir: tutor-contrib-sample/dist - user: __token__ - password: ${{ secrets.PYPI_UPLOAD_TOKEN }} publish_to_npm: runs-on: ubuntu-latest From c9723836d94989339546a7e2ec7652237c9ad774 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:24:43 -0400 Subject: [PATCH 02/11] fix: ship a license file with the tutor plugin tutor-contrib-sample/pyproject.toml declares `license-files = ["LICENSE*"]`, but that directory had no LICENSE, so the glob matched nothing and setuptools silently built a wheel with no `License-File:` in METADATA and no `dist-info/licenses/` directory. The published package claimed Apache-2.0 without carrying the text. Copied verbatim from backend-plugin-sample/LICENSE.txt so both distributions carry identical terms. Co-Authored-By: Claude Opus 5 (1M context) --- tutor-contrib-sample/LICENSE.txt | 180 +++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tutor-contrib-sample/LICENSE.txt diff --git a/tutor-contrib-sample/LICENSE.txt b/tutor-contrib-sample/LICENSE.txt new file mode 100644 index 0000000..28ba471 --- /dev/null +++ b/tutor-contrib-sample/LICENSE.txt @@ -0,0 +1,180 @@ + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + From 58e34dfad9d63e2c4289a77e0eceeb8dd0a49563 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:24:48 -0400 Subject: [PATCH 03/11] docs: declare the Python versions the tutor plugin supports The tutor plugin sets `requires-python = ">=3.11"` but only advertised a 3.12 classifier, so PyPI under-reported what it runs on. It works on both 3.11 and 3.12, so list both. Co-Authored-By: Claude Opus 5 (1M context) --- tutor-contrib-sample/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/tutor-contrib-sample/pyproject.toml b/tutor-contrib-sample/pyproject.toml index 8792b69..db18bb5 100644 --- a/tutor-contrib-sample/pyproject.toml +++ b/tutor-contrib-sample/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] keywords = ["tutor", "openedx", "plugin"] From 7a51b4295b3fb28f3d45842a3ed7a11d936408e2 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:24:54 -0400 Subject: [PATCH 04/11] build: drop the dead universal wheel flag `[wheel] universal = 1` is a Python 2 era setting that marks a build as py2.py3 -- meaningless for a package that requires Python >=3.12. It was inert anyway: setuptools reads this option from `[bdist_wheel]`, not `[wheel]`, so nothing had been consuming it. Co-Authored-By: Claude Opus 5 (1M context) --- backend-plugin-sample/setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend-plugin-sample/setup.cfg b/backend-plugin-sample/setup.cfg index d782599..da0309e 100644 --- a/backend-plugin-sample/setup.cfg +++ b/backend-plugin-sample/setup.cfg @@ -5,6 +5,3 @@ line_length = 120 multi_line_output = 3 skip= migrations - -[wheel] -universal = 1 From 4abdba37b6e5f49c79c8c99a4026b1da6b1bce15 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:35:35 -0400 Subject: [PATCH 05/11] build: stop stacking redundant CI runs on pull requests Without a concurrency group GitHub never cancels superseded runs, so every push to a PR branch starts another five-job matrix while the previous five run to completion. Three pushes in a row means fifteen jobs, ten of them for commits nobody will read. The release calls this workflow via workflow_call, and in a called workflow `github.workflow` resolves to the caller's name -- so a naive `cancel-in-progress: true` would let a push to main cancel the tests of an in-flight release, and a shared group key would make concurrent releases queue behind one another. Keying non-pull_request events on `github.run_id` gives every release run a group of its own, leaving that path untouched. https://docs.github.com/en/actions/using-jobs/using-concurrency Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/backend-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 9b36c2a..f52ad4e 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -8,6 +8,14 @@ on: # run CI before doing whatever task they're doing. Like the release workflow. workflow_call: +concurrency: + # On a pull request, group by the PR so a new push supersedes the matrix + # still running for the previous one. Any other event -- notably the + # workflow_call from the release -- keys the group on the unique run id + # instead, so release runs never queue behind or cancel each other. + group: ${{ github.workflow }}-backend-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + defaults: run: working-directory: "./backend-plugin-sample" From 0df15b748a77cc50f5ade08f44d8ee4d6d4532b9 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:35:54 -0400 Subject: [PATCH 06/11] build: publish the repository metadata npm expects npm rewrote this field on every publish, so the metadata on the registry did not match the committed manifest: npm warn publish npm auto-corrected some errors in your package.json npm warn publish "repository" was changed from a string to an object npm warn publish "repository.url" was normalized to "git+https://github.com/openedx/sample-plugin.git" Write it in the shape npm normalizes to, and add `directory` while we are here -- the package lives in a subfolder, and that field is what makes the repository link on npmjs.com point at frontend-plugin-sample rather than the repo root. https://docs.npmjs.com/cli/configuring-npm/package-json#repository Co-Authored-By: Claude Opus 5 (1M context) --- frontend-plugin-sample/package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend-plugin-sample/package.json b/frontend-plugin-sample/package.json index f62a827..4ee0c68 100644 --- a/frontend-plugin-sample/package.json +++ b/frontend-plugin-sample/package.json @@ -3,7 +3,11 @@ "version": "__semantically__released__", "main": "dist/index.js", "files": ["dist"], - "repository": "https://github.com/openedx/sample-plugin", + "repository": { + "type": "git", + "url": "git+https://github.com/openedx/sample-plugin.git", + "directory": "frontend-plugin-sample" + }, "scripts": { "build": "fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.js" }, From 4aedf58b09823b90e7f318ac4c565839d605ed20 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:37:55 -0400 Subject: [PATCH 07/11] build: build the frontend plugin on pull requests The frontend package had no CI at all. Its first and only compile was the `npm run build` inside the release workflow's publish_to_npm job, which runs after the GitHub release is published and after both PyPI uploads. A syntax error in plugin.jsx would therefore be discovered only once two immutable PyPI versions and an immutable GitHub release existed for a version that has no npm package. Compiling is the only check available today -- the package defines no lint or test script -- but it is the one that would have caught that. Uses `npm ci` rather than the release job's `npm install`, so the build is pinned to the committed lockfile and fails loudly if the lockfile and package.json disagree. Aligning the release job is left for a follow-up. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/frontend-ci.yml | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/frontend-ci.yml diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 0000000..4499281 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,48 @@ +name: Frontend CI + +on: + pull_request: + branches: + - "**" + # So the release workflow can run these checks before it publishes anything. + workflow_call: + +concurrency: + # See backend-ci.yml for why non-pull_request events key on the run id. + group: ${{ github.workflow }}-frontend-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + working-directory: "./frontend-plugin-sample" + +jobs: + build: + name: build + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: './frontend-plugin-sample/.nvmrc' + cache: 'npm' + cache-dependency-path: './frontend-plugin-sample/package-lock.json' + + # `npm ci` rather than `npm install`: it installs the lockfile exactly + # and fails if the lockfile and package.json have drifted apart, instead + # of quietly reconciling them. + - name: Install dependencies + run: npm ci + + # The package has no lint or test suite yet, so compiling is the only + # check available. It is still the one that matters most: until now the + # first and only build of this package happened inside the publish job, + # after the GitHub release and both PyPI uploads had already gone out. + - name: Build + run: npm run build From 7d4852ea209cf068666f884b9d223f7d4d7b35a1 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:38:03 -0400 Subject: [PATCH 08/11] build: build the tutor plugin on pull requests Like the frontend package, the tutor plugin was never built until the release workflow needed to publish it, so a broken pyproject.toml surfaced only after the GitHub release and the backend PyPI upload had gone out. Also runs `twine check`, which validates the metadata PyPI will accept and render. The long description is read dynamically from README.md, so it is worth checking that it still renders. The checkout uses fetch-depth 0 because setuptools-scm derives the version from git tags and this project declares no fallback_version, unlike the backend. The release job sidesteps that with SETUPTOOLS_SCM_PRETEND_VERSION, so this exercises a path the release never does: building from a plain checkout, the way a contributor would locally. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/tutor-ci.yml | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/tutor-ci.yml diff --git a/.github/workflows/tutor-ci.yml b/.github/workflows/tutor-ci.yml new file mode 100644 index 0000000..df2a706 --- /dev/null +++ b/.github/workflows/tutor-ci.yml @@ -0,0 +1,54 @@ +name: Tutor Plugin CI + +on: + pull_request: + branches: + - "**" + # So the release workflow can run these checks before it publishes anything. + workflow_call: + +concurrency: + # See backend-ci.yml for why non-pull_request events key on the run id. + group: ${{ github.workflow }}-tutor-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + working-directory: "./tutor-contrib-sample" + +jobs: + build: + name: build + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # setuptools-scm derives the version from git tags, and this project + # sets no fallback_version, so it needs the history and tags rather + # than the default shallow clone. + fetch-depth: 0 + + - name: setup python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + cache-suffix: tutor + + # Until now this package was first built inside the release job, so a + # broken pyproject.toml surfaced only after the GitHub release and the + # backend PyPI upload had already been published. + - name: Build + run: uv run --with build python -m build + + # Catches metadata that PyPI would reject or render badly -- the long + # description is read dynamically from README.md, so it is worth checking. + - name: Check distribution metadata + run: uv run --with twine twine check dist/* From 4add6e4f19f11e49e9c1c352a2ea0f9c1b921bf5 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:38:10 -0400 Subject: [PATCH 09/11] build: gate the release on the frontend and tutor builds The release only required the backend test matrix, so it would tag, publish a GitHub release, and upload to PyPI while the frontend and tutor packages were still unbuilt. Neither the GitHub release nor a PyPI upload can be withdrawn, so a package that fails to build in its own publish job leaves the release permanently half-finished. Requiring all three CI workflows means nothing gets tagged until every package this workflow publishes is known to build. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc54ea1..17088d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,18 @@ jobs: run_backend_tests: uses: ./.github/workflows/backend-ci.yml + run_frontend_tests: + uses: ./.github/workflows/frontend-ci.yml + + run_tutor_tests: + uses: ./.github/workflows/tutor-ci.yml + release: - needs: run_backend_tests + # Every package this workflow publishes has to build before we tag + # anything. The GitHub release and the PyPI uploads cannot be taken back, + # so a package that only fails to build in its publish job would leave the + # release half-finished. + needs: [run_backend_tests, run_frontend_tests, run_tutor_tests] runs-on: ubuntu-latest if: github.ref_name == 'main' concurrency: From ce2bebb6d223cfb7b546a8d52c2a07a107ef27ae Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:50:18 -0400 Subject: [PATCH 10/11] build: install from the lockfile when publishing to npm `npm install` reconciles the lockfile with package.json and carries on; `npm ci` installs the lockfile exactly and fails if the two have drifted apart. For the one build that becomes a published artifact, failing loudly is what we want -- especially here, where every dependency in package.json is `"*"`, so the lockfile is the only thing pinning anything at all. `--include=dev` goes away because `npm ci` installs dev dependencies by default. This matches what frontend-ci.yml already does, so a green PR build and the release build now install the same tree. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17088d7..1e799bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,7 +185,7 @@ jobs: - name: Update the package version and publish run: | - npm install --include=dev + npm ci npm version ${{ needs.release.outputs.version }} npm run build npm publish From bb471ca91964759de7709760d4bc6d659a827fdc Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 1 Sep 2026 09:54:07 -0400 Subject: [PATCH 11/11] build: state that the npm version bump is not a git operation We only want `npm version` to rewrite package.json here; the tag for this release is created by python-semantic-release. This is a no-op today. npm gates all git work behind a single check that stats `.git` in the package directory and does not walk up to the repo root: const isGitDir = newversion === 'from-git' || await git.is(opts) const doGit = gitTagVersion && isGitDir && await enforceClean(opts) frontend-plugin-sample is a subdirectory with no `.git` of its own, so `doGit` is already false and no commit or tag is made. Verified by reproduction: run in a subdirectory package, npm writes the version only; run with package.json at the git root, it also commits and tags. The flag makes the intent explicit rather than leaving it resting on repository layout, which would silently start committing and tagging if this package ever moved to the repo root. https://github.com/npm/cli/blob/latest/workspaces/libnpmversion/lib/version.js Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e799bd..dce5ac3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,7 +186,7 @@ jobs: - name: Update the package version and publish run: | npm ci - npm version ${{ needs.release.outputs.version }} + npm version --no-git-tag-version ${{ needs.release.outputs.version }} npm run build npm publish working-directory: './frontend-plugin-sample'