diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 56c51acc..6cb0adfc 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -4,6 +4,9 @@ on: workflow_call: workflow_dispatch: +permissions: + contents: read + jobs: lint: name: Lint @@ -11,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python 3 uses: actions/setup-python@v5 with: @@ -25,6 +30,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python 3 uses: actions/setup-python@v5 with: @@ -43,6 +50,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -57,6 +66,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python 3 uses: actions/setup-python@v5 with: @@ -72,10 +83,25 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup operator environment - uses: charmed-kubernetes/actions-operator@main with: - provider: lxd - juju-channel: 3.6/stable + persist-credentials: false + - name: Set up Python 3 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install tox + run: pip install tox~=4.2 + - name: Install Concierge + run: sudo snap install --classic concierge + - name: Prepare for deploying machine charms + run: sudo concierge prepare -p machine + - name: Pack test charm + run: | + charm_dir=tests/integration/juju_systemd_notices/notices-charm + cp lib/charms/operator_libs_linux/v1/systemd.py "$charm_dir/lib/charms/operator_libs_linux/v1/" + cp lib/charms/operator_libs_linux/v0/juju_systemd_notices.py "$charm_dir/lib/charms/operator_libs_linux/v0/" + cd "$charm_dir" + charmcraft pack --verbose + echo "CHARM_PATH=$(pwd)/$(ls *.charm)" >> "$GITHUB_ENV" - name: Run integration tests (juju-systemd-notices) run: tox run -e integration-juju-systemd-notices diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 08658380..9f89cd3a 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -6,6 +6,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test: uses: ./.github/workflows/build-and-test.yaml diff --git a/.github/workflows/release-libs.yaml b/.github/workflows/release-libs.yaml index 91941258..c47d8a82 100644 --- a/.github/workflows/release-libs.yaml +++ b/.github/workflows/release-libs.yaml @@ -5,17 +5,21 @@ on: branches: - main +permissions: + contents: write + jobs: release-libs: name: Release any bumped library runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: fetch-depth: 0 + persist-credentials: false - name: Release any bumped charm library - uses: canonical/charming-actions/release-libraries@2.2.3 + uses: canonical/charming-actions/release-libraries@f87f8885aeb69e668b50c6c6095af5eadac457d2 # 2.2.3 with: credentials: "${{ secrets.CHARMHUB_TOKEN }}" github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 999dc92f..c2756321 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,6 +13,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test: uses: ./.github/workflows/build-and-test.yaml @@ -26,14 +29,15 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: fetch-depth: 0 + persist-credentials: false - name: Select charmhub channel - uses: canonical/charming-actions/channel@2.2.3 + uses: canonical/charming-actions/channel@f87f8885aeb69e668b50c6c6095af5eadac457d2 # 2.2.3 id: channel - name: Upload charm to charmhub - uses: canonical/charming-actions/upload-charm@2.2.3 + uses: canonical/charming-actions/upload-charm@f87f8885aeb69e668b50c6c6095af5eadac457d2 # 2.2.3 with: credentials: "${{ secrets.CHARMHUB_TOKEN }}" github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 00000000..6e143adf --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,34 @@ +name: Workflow static checks + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + + - name: Run zizmor + run: uvx zizmor@v1.23.1 --format=sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..5b5b5e74 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +rules: + unpinned-uses: + config: + policies: + "actions/*": ref-pin + "github/*": ref-pin + "pypa/*": ref-pin diff --git a/tests/integration/juju_systemd_notices/conftest.py b/tests/integration/juju_systemd_notices/conftest.py index 10206073..ea10da30 100644 --- a/tests/integration/juju_systemd_notices/conftest.py +++ b/tests/integration/juju_systemd_notices/conftest.py @@ -4,31 +4,62 @@ """Configure integration tests for the juju_systemd_notices library.""" +import logging +import os +import pathlib import shutil -from pathlib import Path +import sys +import time +import jubilant import pytest -from pytest_operator.plugin import OpsTest -test_charm_root = Path("tests/integration/juju_systemd_notices/notices-charm") -lib_root = Path("lib/charms/operator_libs_linux") +logger = logging.getLogger(__name__) + +test_charm_root = pathlib.Path("tests/integration/juju_systemd_notices/notices-charm") +lib_root = pathlib.Path("lib/charms/operator_libs_linux") systemd_path = lib_root / "v1/systemd.py" notices_path = lib_root / "v0/juju_systemd_notices.py" +@pytest.fixture(scope="module") +def juju(request: pytest.FixtureRequest): + """Create a temporary Juju model for running tests.""" + with jubilant.temp_model() as juju: + yield juju + + if request.session.testsfailed: + logger.info("Collecting Juju logs...") + time.sleep(0.5) + log = juju.debug_log(limit=1000) + print(log, end="", file=sys.stderr) + + @pytest.fixture(scope="module", autouse=True) -def copy_machine_libs_into_test_charm(ops_test: OpsTest): +def copy_machine_libs_into_test_charm(): """Copy the systemd and juju_systemd_notices to the test charm.""" shutil.copy(systemd_path, test_charm_root / systemd_path) shutil.copy(notices_path, test_charm_root / notices_path) - - -@pytest.fixture(scope="module") -async def test_charm(ops_test: OpsTest): - return await ops_test.build_charm("tests/integration/juju_systemd_notices/notices-charm") - - -def pytest_sessionfinish(session, exitstatus): - """Clean up integration test after it has completed.""" + yield (test_charm_root / systemd_path).unlink(missing_ok=True) (test_charm_root / notices_path).unlink(missing_ok=True) + + +@pytest.fixture(scope="session") +def test_charm(): + """Return the path of the charm under test.""" + if "CHARM_PATH" in os.environ: + charm_path = pathlib.Path(os.environ["CHARM_PATH"]) + if not charm_path.exists(): + raise FileNotFoundError(f"Charm does not exist: {charm_path}") + return charm_path + charm_paths = list(test_charm_root.glob("*.charm")) + if not charm_paths: + raise FileNotFoundError( + f"No .charm file in {test_charm_root}. " + "Run 'charmcraft pack' first or set CHARM_PATH." + ) + if len(charm_paths) > 1: + path_list = ", ".join(str(p) for p in charm_paths) + raise ValueError(f"More than one .charm file in {test_charm_root}: {path_list}") + return charm_paths[0] diff --git a/tests/integration/juju_systemd_notices/notices-charm/charmcraft.yaml b/tests/integration/juju_systemd_notices/notices-charm/charmcraft.yaml index 4fc0954c..1a8f8a84 100644 --- a/tests/integration/juju_systemd_notices/notices-charm/charmcraft.yaml +++ b/tests/integration/juju_systemd_notices/notices-charm/charmcraft.yaml @@ -8,15 +8,14 @@ summary: | A charm with a minimal daemon for testing the juju-systemd-notices charm library. type: charm -bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" +base: ubuntu@22.04 +platforms: + amd64: + +parts: + charm: + plugin: charm actions: stop-service: description: Stop internal test service inside charm - diff --git a/tests/integration/juju_systemd_notices/test_juju_systemd_notices.py b/tests/integration/juju_systemd_notices/test_juju_systemd_notices.py index 33e0e33d..88fb2755 100644 --- a/tests/integration/juju_systemd_notices/test_juju_systemd_notices.py +++ b/tests/integration/juju_systemd_notices/test_juju_systemd_notices.py @@ -4,11 +4,10 @@ """Integration tests for juju_systemd_notices charm library.""" -import asyncio import logging +import jubilant import pytest -from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) APP_NAME = "test" @@ -17,34 +16,24 @@ @pytest.mark.abort_on_fail @pytest.mark.order(1) -async def test_service_start(ops_test: OpsTest, test_charm) -> None: +def test_service_start(juju: jubilant.Juju, test_charm) -> None: """Test that service_test_started event is properly handled by test charm.""" logger.info("Deploying test charm with internal test daemon") - await asyncio.gather( - ops_test.model.deploy( - str(await test_charm), application_name=APP_NAME, num_units=1, base="ubuntu@22.04" - ) - ) + juju.deploy(test_charm, app=APP_NAME, num_units=1, base="ubuntu@22.04") logger.info("Waiting for test daemon to start...") - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", timeout=1000) - assert ( - ops_test.model.units.get(UNIT_NAME).workload_status_message - == "test service running :)" - ) + status = juju.wait(lambda status: jubilant.all_active(status, APP_NAME), timeout=1000) + unit_status = status.apps[APP_NAME].units[UNIT_NAME] + assert unit_status.workload_status.message == "test service running :)" @pytest.mark.abort_on_fail @pytest.mark.order(2) -async def test_service_stop(ops_test: OpsTest) -> None: +def test_service_stop(juju: jubilant.Juju) -> None: """Test that service_test_stopped event is properly handled by test charm.""" logger.info("Stopping internal test daemon") - action = await ops_test.model.units.get(UNIT_NAME).run_action("stop-service") - await action.wait() + task = juju.run(UNIT_NAME, "stop-service") + task.raise_on_failure() logger.info("Waiting for test daemon to stop...") - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle(apps=[APP_NAME], status="blocked", timeout=1000) - assert ( - ops_test.model.units.get(UNIT_NAME).workload_status_message - == "test service not running :(" - ) + status = juju.wait(lambda status: jubilant.all_blocked(status, APP_NAME), timeout=1000) + unit_status = status.apps[APP_NAME].units[UNIT_NAME] + assert unit_status.workload_status.message == "test service not running :(" diff --git a/tests/integration/test_apt.py b/tests/integration/test_apt.py index 64fd1eda..c552689d 100644 --- a/tests/integration/test_apt.py +++ b/tests/integration/test_apt.py @@ -10,6 +10,7 @@ from typing import List from urllib.request import urlopen +import pytest from charms.operator_libs_linux.v0 import apt from helpers import get_command_path @@ -137,6 +138,7 @@ def test_install_higher_version_package_from_external_repository(): assert not get_command_path("fish") +@pytest.mark.skip(reason="HPE GPG signing key has expired, causing apt-get update to fail") def test_install_hardware_observer_ssacli(): """Test the ability to install a package used by the hardware-observer charm. diff --git a/tox.ini b/tox.ini index a4c516cd..6f0009fd 100644 --- a/tox.ini +++ b/tox.ini @@ -149,14 +149,15 @@ commands = description = Run juju systemd notices integration tests. deps = pytest - pytest-operator pytest-order - juju + jubilant -r {toxinidir}/requirements.txt +passenv = + CHARM_PATH commands = pytest -v \ -s \ --tb native \ --log-cli-level=INFO \ - {[vars]tst_dir}integration/juju_systemd_notices + {[vars]tst_dir}integration/juju_systemd_notices \ {posargs}