diff --git a/.github/workflows/ci-pr.yaml b/.github/workflows/ci-pr.yaml index 9b64f6c..1b049ee 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -3,6 +3,10 @@ name: Test PR on: pull_request: +permissions: + # Read repository contents for checkout and dependency resolution only. + contents: read + env: # Please make sure this version is included in the `matrix`, as the # `matrix` section can't use `env`, so it must be entered manually @@ -17,7 +21,7 @@ jobs: steps: - name: Run nox - uses: frequenz-floss/gh-action-nox@v1.1.1 + uses: frequenz-floss/gh-action-nox@80a9845a59ffc71d27b9c41099eb6cb55bc7b671 # v1.1.1 with: python-version: "3.11" nox-session: ci_checks_max @@ -27,15 +31,15 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -44,11 +48,14 @@ jobs: env: MIKE_VERSION: gh-${{ github.job }} run: | - mike deploy $MIKE_VERSION - mike set-default $MIKE_VERSION + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" deploy "$MIKE_VERSION" + python -I "$(command -v mike)" set-default "$MIKE_VERSION" - name: Upload site - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-site path: site/ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2f92f66..d36f4a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,10 @@ on: - "dependabot/**" workflow_dispatch: +permissions: + # Read repository contents for checkout and dependency resolution only. + contents: read + env: # Please make sure this version is included in the `matrix`, as the # `matrix` section can't use `env`, so it must be entered manually @@ -28,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - target: + platform: - ubuntu-24.04 - ubuntu-24.04-arm - windows-latest @@ -44,7 +48,7 @@ jobs: # that uses the same venv to run multiple linting sessions - "ci_checks_max" - "pytest_min" - runs-on: ${{ matrix.target }} + runs-on: ${{ matrix.platform }} steps: - name: Run nox @@ -63,7 +67,9 @@ jobs: needs: ["nox"] # We skip this job only if nox was also skipped if: always() && needs.nox.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim + # Drop token permissions: this job only checks matrix status from `needs`. + permissions: {} env: DEPS_RESULT: ${{ needs.nox.result }} steps: @@ -80,19 +86,19 @@ jobs: - runner: ubuntu-22.04 target: aarch64 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x - name: Build wheels - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-linux-${{ matrix.platform.target }} path: dist @@ -107,19 +113,19 @@ jobs: - runner: ubuntu-22.04 target: aarch64 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x - name: Build wheels - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: musllinux_1_2 - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-musllinux-${{ matrix.platform.target }} path: dist @@ -132,19 +138,19 @@ jobs: - runner: windows-latest target: x64 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x architecture: ${{ matrix.platform.target }} - name: Build wheels - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-windows-${{ matrix.platform.target }} path: dist @@ -159,18 +165,18 @@ jobs: - runner: macos-15 target: aarch64 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x - name: Build wheels - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-macos-${{ matrix.platform.target }} path: dist @@ -179,14 +185,14 @@ jobs: name: Build source distribution packages runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build sdist - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: command: sdist args: --out dist - name: Upload sdist - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-sdist path: dist/*.tar.gz @@ -236,13 +242,13 @@ jobs: steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Print environment (debug) run: env - name: Download package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: wheels-${{ matrix.platform.image }}-${{ matrix.platform.target }} path: dist @@ -263,13 +269,13 @@ jobs: > pyproject.toml - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ matrix.python.semver }} dependencies: dist/*${{ matrix.python.wheelver }}-${{ matrix.platform.tag }}*.whl - name: Print installed packages (debug) - run: python -m pip freeze + run: python -Im pip freeze # This job runs if all the `test-installation` matrix jobs ran and succeeded. # It is only used to have a single job that we can require in branch @@ -281,7 +287,9 @@ jobs: needs: ["test-installation"] # We skip this job only if test-installation was also skipped if: always() && needs.test-installation.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim + # Drop token permissions: this job only checks matrix status from `needs`. + permissions: {} env: DEPS_RESULT: ${{ needs.test-installation.result }} steps: @@ -294,15 +302,15 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -311,11 +319,14 @@ jobs: env: MIKE_VERSION: gh-${{ github.job }} run: | - mike deploy $MIKE_VERSION - mike set-default $MIKE_VERSION + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" deploy "$MIKE_VERSION" + python -I "$(command -v mike)" set-default "$MIKE_VERSION" - name: Upload site - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-site path: site/ @@ -327,18 +338,19 @@ jobs: if: github.event_name == 'push' runs-on: ubuntu-24.04 permissions: + # Push generated documentation updates to the `gh-pages` branch. contents: write steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -351,7 +363,7 @@ jobs: GIT_REF: ${{ github.ref }} GIT_SHA: ${{ github.sha }} run: | - python -m frequenz.repo.config.cli.version.mike.info + python -Im frequenz.repo.config.cli.version.mike.info - name: Fetch the gh-pages branch if: steps.mike-version.outputs.version @@ -372,13 +384,23 @@ jobs: GIT_REF: ${{ github.ref }} GIT_SHA: ${{ github.sha }} run: | - mike deploy --update-aliases --title "$TITLE" "$VERSION" $ALIASES + # Collect aliases into an array to avoid accidental (or malicious) + # shell injection when passing them to mike. + aliases=() + if test -n "$ALIASES"; then + read -r -a aliases <<<"$ALIASES" + fi + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" \ + deploy --update-aliases --title "$TITLE" "$VERSION" "${aliases[@]}" - name: Sort site versions if: steps.mike-version.outputs.version run: | git checkout gh-pages - python -m frequenz.repo.config.cli.version.mike.sort versions.json + python -Im frequenz.repo.config.cli.version.mike.sort versions.json git commit -a -m "Sort versions.json" - name: Publish site @@ -392,14 +414,12 @@ jobs: # Create a release only on tags creation if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') permissions: - # We need write permissions on contents to create GitHub releases and on - # discussions to create the release announcement in the discussion forums + # Create GitHub releases and upload distribution artifacts. contents: write - discussions: write - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim steps: - name: Download distribution files - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist @@ -420,14 +440,14 @@ jobs: - name: Create GitHub release run: | set -ux - extra_opts= - if echo "$REF_NAME" | grep -- -; then extra_opts=" --prerelease"; fi + extra_opts=() + if echo "$REF_NAME" | grep -- -; then extra_opts+=(--prerelease); fi gh release create \ -R "$REPOSITORY" \ --notes-file RELEASE_NOTES.md \ --generate-notes \ - $extra_opts \ - $REF_NAME \ + "${extra_opts[@]}" \ + "$REF_NAME" \ dist/wheels-*/*.whl dist/wheels-sdist/*.tar.gz env: REF_NAME: ${{ github.ref_name }} @@ -444,10 +464,10 @@ jobs: id-token: write steps: - name: Download distribution files - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true - name: Publish the Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 257e738..48c26d5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -15,3 +15,10 @@ ## Bug Fixes + +## Tooling + +* Restored security hardening to GitHub Actions workflows: hash-pinned all + actions, re-added workflow-level `permissions: contents: read`, restored + `python -I` isolated mode on Python invocations, re-added `permissions: {}` + on gate jobs, and fixed shell injection risks in `mike` and release steps.