From 3621c340b0fd64a2bad4af6d06a0e0d68b96c59d Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Mon, 28 Sep 2026 12:35:27 -0700 Subject: [PATCH 1/6] ci: pin the runner image ubuntu-latest is a label GitHub moves. The image changes under us and a green pipeline turns red on a morning nobody touched the code. Pinning to ubuntu-24.04 means the runner moves when we move it. Action refs are untouched; the version-tagged ones stay as they are and Dependabot keeps watching them. --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f20c6d6..81a01ac 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ concurrency: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - run: | @@ -21,7 +21,7 @@ jobs: ruff format test: needs: lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: # if one python version fails, let the others finish fail-fast: false @@ -52,7 +52,7 @@ jobs: verbose: true publish: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: From 0b5d899ca21a57fe56f61e2b83de49a33cc88a18 Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Thu, 1 Oct 2026 08:37:38 -0700 Subject: [PATCH 2/6] ci: pin ruff to 0.11.10 The lint job installed ruff with no version, so it picked up 0.16.9, which flags 70 errors in existing code. Lint failed and the test matrix never ran on the new ubuntu-24.04 runner. Pin ruff with == in the workflow and in the dev dependency group, at the 0.11.10 already locked in poetry.lock. That version passes ruff check and ruff format clean on this branch. Bump it deliberately in a commit that also fixes what the new version flags. --- .github/workflows/tests.yml | 2 +- poetry.lock | 4 ++-- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 81a01ac..85e1ead 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: | - pipx install ruff + pipx install ruff==0.11.10 ruff check ruff format test: diff --git a/poetry.lock b/poetry.lock index 3da6135..afa41ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1445,4 +1445,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "13c16c3b1c6b7ba5138c7945cca0fe84bdd1e08c51d6f6a582a7b7c89849791f" +content-hash = "e80b7f3dae508bdf2312a8002e46c6f1204d78d7a8e3a743cb8379e5ed632535" diff --git a/pyproject.toml b/pyproject.toml index 25ca767..a64dfee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ line-length = 119 lint.ignore = ["E402", "E721", "E741", "F401", "F403", "F405", "E713"] [tool.poetry.group.dev.dependencies] -ruff = ">=0.9.1,<0.12.0" +ruff = "==0.11.10" pytest-httpserver = "^1.1.0" pytest-asyncio = "^0.25.2" poetry-dynamic-versioning = "^1.4.1" From 8cf7ce4151e05217d14b9665cd600f12f1d4d617 Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Sat, 3 Oct 2026 00:58:46 -0700 Subject: [PATCH 3/6] ci: use the runner's google-chrome instead of snap chromium On ubuntu-24.04 chromium-browser is a snap transitional package whose install fails in the runner, which broke every test job once the runner was pinned. The image already ships google-chrome, which webcap's binary detection finds. --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85e1ead..8f3b2bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,12 +36,13 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install -y tesseract-ocr chromium-browser + sudo apt install -y tesseract-ocr pip install poetry poetry install + # chromium-browser on ubuntu-24.04 is a snap shim that fails to install. The image ships google-chrome. + - name: Chrome is available + run: google-chrome --version - name: Run tests - env: - CHROME_DEVEL_SANDBOX: /usr/lib/chromium/chrome-sandbox run: | poetry run pytest -s -vv --disable-warnings --cov-report xml:cov.xml --cov=webcap . - name: Upload Code Coverage From f33bc75407d4ebadfb2c099c8e44161fd7e9a639 Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Sat, 3 Oct 2026 01:03:58 -0700 Subject: [PATCH 4/6] ci: allow the Chrome sandbox's user namespaces on ubuntu-24.04 AppArmor restricts unprivileged user namespaces on 24.04, so Chrome aborted with 'No usable sandbox'. Lift the restriction on the runner rather than running Chrome with --no-sandbox. --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f3b2bf..adfbd62 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,9 +39,10 @@ jobs: sudo apt install -y tesseract-ocr pip install poetry poetry install - # chromium-browser on ubuntu-24.04 is a snap shim that fails to install. The image ships google-chrome. - - name: Chrome is available - run: google-chrome --version + # chromium-browser on ubuntu-24.04 is a snap shim that fails to install. The image ships chrome, but AppArmor + # blocks the unprivileged user namespaces its sandbox needs. + - name: Allow the Chrome sandbox + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Run tests run: | poetry run pytest -s -vv --disable-warnings --cov-report xml:cov.xml --cov=webcap . From 07d98c53446c41b4652fb7f45d687d66b2c581ad Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Sat, 3 Oct 2026 01:33:37 -0700 Subject: [PATCH 5/6] ci(lint): gate on formatting, take ruff's version from the lockfile `ruff format` without --check rewrote files in the runner and always passed, so formatting was never enforced. The ruff pin was also repeated in the workflow; install the locked dev group instead. --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index adfbd62..7f166a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,9 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 - run: | - pipx install ruff==0.11.10 - ruff check - ruff format + pipx install poetry + poetry install --only dev --no-root + poetry run ruff check + poetry run ruff format --check test: needs: lint runs-on: ubuntu-24.04 From f59ae4f5977c7491908d32a5b6ce4007420efad1 Mon Sep 17 00:00:00 2001 From: Shane Engelman Date: Sat, 3 Oct 2026 04:42:43 -0700 Subject: [PATCH 6/6] ci: publish on version tags via OIDC instead of auto-tagging master Remove the publish job from tests.yml. On every push to master it published to PyPI with a long-lived token and then ran git tag and git push of the tag from CI. Add publish.yml from the asndb template: runs only on v* tags, checks the tag matches the poetry-dynamic-versioning version from a full history checkout, reuses tests.yml through workflow_call, publishes with PyPI trusted publishing in the pypi environment, and cuts a GitHub release with an SPDX SBOM. Drop the v0.1.{distance} format override so an exact vX.Y.Z tag yields version X.Y.Z instead of a distance-derived number. tests.yml now triggers on pushes to master only plus pull_request and workflow_call, runs with contents: read, and pins every action to a SHA. Refs: blacklanternsecurity/bbot-enterprise#139 Refs: blacklanternsecurity/bbot-enterprise#155 Refs: blacklanternsecurity/bbot-enterprise#150 --- .github/workflows/publish.yml | 89 +++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 79 +++++-------------------------- pyproject.toml | 1 - 3 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6220de7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: publish + +on: + push: + tags: ["v*", "!archive/**"] + +permissions: + contents: read + +concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + +jobs: + version: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + prerelease: ${{ steps.version.outputs.prerelease }} + major: ${{ steps.version.outputs.major }} + minor: ${{ steps.version.outputs.minor }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - name: Tag matches the declared version + id: version + run: | + uv run --no-project --with packaging python - <<'EOF' >> "$GITHUB_OUTPUT" + import os, subprocess, sys + from packaging.version import Version + declared = Version(subprocess.check_output(["uvx", "--from", "poetry", "--with", "poetry-dynamic-versioning[plugin]", "poetry", "version", "--short"], text=True).strip()) + # org tag convention: vX.Y.Z or vX.Y.Z-rc.N, which must spell exactly the declared version + base = declared.base_version + expected = f"v{base}-rc.{declared.pre[1]}" if declared.pre and declared.pre[0] == "rc" else f"v{declared}" + if os.environ["GITHUB_REF_NAME"] != expected or declared.dev is not None or declared.local: + sys.exit(f"tag {os.environ['GITHUB_REF_NAME']} does not match poetry-dynamic-versioning {declared} (expected {expected})") + print(f"version={declared}") + print(f"prerelease={str(declared.is_prerelease).lower()}") + print(f"major={declared.major}") + print(f"minor={declared.major}.{declared.minor}") + EOF + + tests: + needs: version + uses: ./.github/workflows/tests.yml + secrets: inherit + + pypi: + environment: pypi + needs: [version, tests] + runs-on: ubuntu-24.04 + permissions: + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - run: uv build + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + release: + needs: [pypi] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + path: . + format: spdx-json + output-file: ${{ github.event.repository.name }}-${{ github.ref_name }}.spdx.json + upload-artifact: false + upload-release-assets: false + - name: GitHub Release with SBOM + env: + GH_TOKEN: ${{ github.token }} + run: | + PRERELEASE=() + if [[ "$GITHUB_REF_NAME" == *-rc.* ]]; then PRERELEASE=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" "${{ github.event.repository.name }}-$GITHUB_REF_NAME.spdx.json" \ + --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes "${PRERELEASE[@]}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f166a8..87d2382 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,8 +3,11 @@ on: push: branches: - master - - dev pull_request: + workflow_call: + +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} @@ -14,7 +17,9 @@ jobs: lint: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - run: | pipx install poetry poetry install --only dev --no-root @@ -29,9 +34,11 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -48,71 +55,9 @@ jobs: run: | poetry run pytest -s -vv --disable-warnings --cov-report xml:cov.xml --cov=webcap . - name: Upload Code Coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./cov.xml verbose: true - publish: - runs-on: ubuntu-24.04 - needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install poetry build - poetry self add "poetry-dynamic-versioning[plugin]" - - name: Get current version from Poetry - id: get_version - run: | - VERSION=$(poetry version --short) - echo "VERSION=$VERSION" | tee -a $GITHUB_ENV - - name: Fetch latest tag - run: | - git fetch --tags - LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null || echo "v0.0.0") - echo "LATEST_TAG=$LATEST_TAG" | tee -a $GITHUB_ENV - - name: Detect version change - run: | - # Retrieve and strip "v" prefix if present - PREVIOUS_VERSION="${{ env.LATEST_TAG }}" - CURRENT_VERSION="${{ env.VERSION }}" - - echo "PREVIOUS_VERSION=$PREVIOUS_VERSION" - echo "CURRENT_VERSION=$CURRENT_VERSION" - - # Extract major.minor for comparison - CURRENT_MAJOR_MINOR=$(echo "${CURRENT_VERSION#v}" | cut -d '.' -f 1-2) - PREVIOUS_MAJOR_MINOR=$(echo "${PREVIOUS_VERSION#v}" | cut -d '.' -f 1-2) - - # Compare versions - if [ "$CURRENT_MAJOR_MINOR" == "$PREVIOUS_MAJOR_MINOR" ]; then - echo "VERSION_CHANGE=false" | tee -a $GITHUB_ENV - else - echo "VERSION_CHANGE=true" | tee -a $GITHUB_ENV - fi - shell: bash - - name: Build PyPi package - if: github.ref == 'refs/heads/master' - run: poetry build - - name: Publish PyPi package - if: github.ref == 'refs/heads/master' - uses: pypa/gh-action-pypi-publish@release/v1.12 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - name: Tag the release - if: github.ref == 'refs/heads/master' && env.VERSION_CHANGE == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ env.VERSION }}" -m "Release ${{ env.VERSION }}" - git push origin "refs/tags/${{ env.VERSION }}" diff --git a/pyproject.toml b/pyproject.toml index a64dfee..1e89c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,4 +49,3 @@ build-backend = "poetry_dynamic_versioning.backend" [tool.poetry-dynamic-versioning] enable = true metadata = true -format = "v0.1.{distance}"