From a06012aea7c4867e36e0df37a44e1898e8e922a8 Mon Sep 17 00:00:00 2001 From: Philip Hartlieb Date: Wed, 28 Oct 2026 15:03:51 -0400 Subject: [PATCH 1/5] Split from CLA: release workflows only Remove the agreement, signatures and the Dependabot auto-merge workflow (D10); they stay in CLA or are retired. Rename the project to release-workflows. Add release.yml (release this repo from stable with its own workflows) and dev-security.yml (dependency review, actionlint, zizmor). Scope the CLA App token to CLA and the calling repository with the permissions it uses. Dependabot cooldown of 7 days; self-repository syntax for local calls. --- .github/dependabot.yml | 2 + .github/workflows/cla-reusable.yml | 8 +- .github/workflows/dependabot-auto-merge.yml | 40 ---- .github/workflows/dev-security.yml | 37 ++++ .github/workflows/python-tests.yml | 3 +- .github/workflows/release.yml | 36 ++++ .github/workflows/tests.yml | 5 +- ICLA.md | 66 ------ README.md | 47 ++++ pyproject.toml | 2 +- signatures/version1/cla.json | 228 -------------------- uv.lock | 52 ++--- 12 files changed, 161 insertions(+), 365 deletions(-) delete mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/dev-security.yml create mode 100644 .github/workflows/release.yml delete mode 100644 ICLA.md create mode 100644 README.md delete mode 100644 signatures/version1/cla.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1a41a08..ec4e629 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,5 @@ updates: - "*" schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/cla-reusable.yml b/.github/workflows/cla-reusable.yml index 4343932..f7bdc5a 100644 --- a/.github/workflows/cla-reusable.yml +++ b/.github/workflows/cla-reusable.yml @@ -23,6 +23,11 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: blacklanternsecurity + # CLA holds the signatures; the calling repository's pull request commits are read for private callers. + repositories: CLA,${{ github.event.repository.name }} + permission-contents: write + permission-members: read + permission-pull-requests: read - name: Check all committers against org and allowlist id: cla-check @@ -90,7 +95,8 @@ jobs: if: | (steps.cla-check.outputs.all_exempt != 'true') && ((github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target') - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 + # Upstream is archived with no maintained successor; replacing it is tracked with the CLA cleanup. + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 # zizmor: ignore[archived-uses] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml deleted file mode 100644 index d9c4f6a..0000000 --- a/.github/workflows/dependabot-auto-merge.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Dependabot auto-merge -on: - workflow_call: - -permissions: {} - -jobs: - auto-merge: - if: github.event.pull_request.user.login == 'dependabot[bot]' && !github.event.repository.fork - runs-on: ubuntu-24.04 - timeout-minutes: 180 - permissions: - contents: write - pull-requests: write - checks: read - steps: - - id: metadata - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 - # dev carries no required checks (#139), so the green gate is here rather than in --auto. - - if: steps.metadata.outputs.update-type != 'version-update:semver-major' - env: - PR_URL: ${{ github.event.pull_request.html_url }} - HEAD: ${{ github.event.pull_request.head.sha }} - GH_TOKEN: ${{ github.token }} - run: | - # Workflow names diverge when the caller isn't on the default branch, run ids don't. - others='[.[] | select((.link // "") | contains("/actions/runs/" + env.GITHUB_RUN_ID + "/") | not)]' - while :; do - sleep 30 - checks=$(gh pr checks "$PR_URL" --json name,bucket,link --jq "$others") || true - [ "$(jq length <<<"$checks")" -gt 0 ] || continue - jq -e 'any(.bucket == "pending")' <<<"$checks" >/dev/null && continue - break - done - jq -r '.[] | "\(.bucket)\t\(.name)"' <<<"$checks" - if jq -e 'any(.bucket == "fail" or .bucket == "cancel")' <<<"$checks" >/dev/null; then - echo "::error::checks failed, not merging" - exit 1 - fi - gh pr merge "$PR_URL" --squash --match-head-commit "$HEAD" diff --git a/.github/workflows/dev-security.yml b/.github/workflows/dev-security.yml new file mode 100644 index 0000000..7d9a67c --- /dev/null +++ b/.github/workflows/dev-security.yml @@ -0,0 +1,37 @@ +# The automated security review that stands in for a second developer on dev (policy rule 3). +name: dev-security +on: + pull_request: + +permissions: {} + +jobs: + dependency-review: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + + workflow-lint: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: actionlint + env: + VERSION: 1.7.12 + SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + run: | + curl -fsSLo actionlint.tgz "https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_linux_amd64.tar.gz" + echo "${SHA256} actionlint.tgz" | sha256sum -c - + tar xzf actionlint.tgz actionlint + ./actionlint -color + - uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + advanced-security: false diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8661036..4ca186b 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -94,7 +94,8 @@ jobs: python-version: ${{ matrix.python-version }} - if: inputs.secret-env != '[]' env: - SECRETS: ${{ toJSON(secrets) }} + # Only the secrets declared above reach a reusable workflow; secret-env picks from those by name. + SECRETS: ${{ toJSON(secrets) }} # zizmor: ignore[overprovisioned-secrets] NAMES: ${{ inputs.secret-env }} run: | python3 - <<'PY' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2fc2c3b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +# Releases this repository on a merge to stable, with its own workflows (publishing policy rules 1-2). +name: Release +on: + push: + branches: [stable] + +permissions: {} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + check: + permissions: + contents: read + actions: read + pull-requests: read + uses: $/.github/workflows/release-check.yml + + tests: + needs: check + if: needs.check.outputs.release == 'true' + permissions: + contents: read + uses: $/.github/workflows/tests.yml + + release: + needs: [check, tests] + if: needs.check.outputs.release == 'true' + permissions: + contents: read + uses: $/.github/workflows/publish.yml + with: + version: ${{ needs.check.outputs.version }} + tag: ${{ needs.check.outputs.tag }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20e39d2..e391be1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,14 +2,15 @@ name: Tests on: pull_request: push: - branches: [main] + branches: [dev] + workflow_call: permissions: contents: read jobs: python: - uses: ./.github/workflows/python-tests.yml + uses: $/.github/workflows/python-tests.yml with: uv-sync-args: --group dev diff --git a/ICLA.md b/ICLA.md deleted file mode 100644 index 1bc7b2a..0000000 --- a/ICLA.md +++ /dev/null @@ -1,66 +0,0 @@ -# Individual Contributor License Agreement - -_In order to clarify the intellectual property license granted with Contributions from any person or entity, BLSOPS, LLC -must have a Contributor License Agreement ("**Agreement**") on file that has been signed by each Contributor, indicating -agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of -BLSOPS, LLC as project manager (the "**Project Manager**") and the Project users; it does not change your rights -to use your own Contributions for any other purpose._ - -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the -Project Manager. Except for the license granted herein to the Project Manager and recipients of software distributed by -the Project Manager, You reserve all right, title, and interest in and to Your Contributions. - -1. **Definitions.** - - "**You**" (or "**Your**") shall mean the copyright owner or legal entity authorized by the - copyright owner that is making this Agreement with the Project Manager. - - "**Contribution**" shall mean the code, documentation or other original works of authorship, including any - modifications or additions to an existing work, that is intentionally submitted by You to the Project Manager for - inclusion in, or documentation of any of the projects owned, managed, distributed or made available by the - Project Manager (the "**Work**"). For the purposes of this definition, "**submitted**" means any form of electronic, - verbal, or written communication sent to the Project Manager 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 Project Manager for the purpose of discussing and improving the Work, but excluding communication - that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -2. **Grant of Copyright License.** Subject to the terms and conditions of this Agreement, You hereby grant to the - Project Manager and to recipients of software distributed by the Project Manager 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 Your Contributions and such derivative works. - -3. **Grant of Patent License.** Subject to the terms and conditions of this Agreement, You hereby grant to the - Project Manager and to recipients of software distributed by the Project Manager 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 You that are necessarily infringed by Your Contribution(s) alone or by combination of - Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent - litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that - Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, - then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as - of the date such litigation is filed. - -4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to - intellectual property that you create that includes your Contributions, You represent that you have received - permission to make Contributions on behalf of that employer, that your employer has waived such rights for your - Contributions to the Project Manager, or that your employer has executed a separate Contributor License Agreement with - the Project Manager. - -5. You represent that each of Your Contributions is Your original creation (see Section 7 for submissions on behalf of - others). You represent that Your Contribution submissions include complete details of any third-party license or other - restriction (including, but not limited to, related patents and trademarks) of which You are personally aware and - which are associated with any part of Your Contributions. - -6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. - You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, - You provide Your 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, NONINFRINGEMENT, MERCHANTABILITY, or - FITNESS FOR A PARTICULAR PURPOSE. - -7. Should You wish to submit work that is not Your original creation, You may submit it to the Project Manager - separately from any Contribution, identifying the complete details of its source and of any license or other - restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are - personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". - -8. You agree to notify the Project Manager of any facts or circumstances of which you become aware that would make these - representations inaccurate in any respect. diff --git a/README.md b/README.md new file mode 100644 index 0000000..929f50c --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# release-workflows + +Reusable GitHub Actions workflows that Black Lantern Security repositories call to test, build, check and +publish releases. Callers execute this code with their own permissions and secrets, so it is held to the +same release controls as the projects it publishes. + +| Workflow | Purpose | +|---|---| +| `python-tests.yml`, `python-versions.yml`, `rust-tests.yml` | Test matrices | +| `stable-guard.yml` | Checks a pull request into `stable` is release-ready: a MAJOR.MINOR.PATCH version, matching chart versions, no git dependencies | +| `release-check.yml` | Runs on `stable` only: reads the version, refuses a partly published release, checks merged-PR provenance and that the `release` environment is protected | +| `maturin-wheels.yml`, `crates.yml`, `docker.yml` | Build and publish artifacts | +| `docker-dev.yml` | `dev` channel images on GHCR (not releases) | +| `publish.yml` | Creates the tag and GitHub release with the release App, after everything is published | +| `cla-reusable.yml` | CLA check. The agreement and signatures live in [`CLA`](https://github.com/blacklanternsecurity/CLA) | + +## Using a workflow + +Pin a release by full commit SHA, with the version as a comment: + +```yaml +jobs: + check: + uses: blacklanternsecurity/release-workflows/.github/workflows/release-check.yml@ # v1.0.0 +``` + +Use the SHA of a `v*` tag of this repository, never a branch. Tags here are created only by the release +App and can't be moved or deleted, so a version comment always matches its SHA. + +## How changes land + +- `dev`: pull request, required tests and security checks (CodeQL, dependency review, `actionlint`, + `zizmor`). No approval needed. +- `stable`: pull request from `dev`, approved by a code owner other than the last pusher, required checks. + Nobody bypasses it. +- A merge to `stable` runs `release.yml`, which waits for a `bbot-leads` approval at the `release` + environment and then tags the merge commit `v` from `pyproject.toml`. + +Versions follow semantic versioning: a change to a workflow's inputs, outputs, secrets or required +permissions that a caller must act on is a major version. + +## Tests + +```bash +uv run --group dev pytest +uv run --group dev actionlint +``` diff --git a/pyproject.toml b/pyproject.toml index 778e6dc..9cad8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "shared-workflows" +name = "release-workflows" version = "0.0.0" requires-python = ">=3.11" dependencies = ["packaging", "pyyaml"] diff --git a/signatures/version1/cla.json b/signatures/version1/cla.json deleted file mode 100644 index 921a7f1..0000000 --- a/signatures/version1/cla.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "signedContributors": [ - { - "name": "corest", - "id": 1071648, - "comment_id": 4160152513, - "created_at": "2026-03-31T06:06:17Z", - "repoId": 468957086, - "pullRequestNo": 3000 - }, - { - "name": "claude", - "id": 81847, - "comment_id": -1, - "created_at": "2026-04-01T00:00:00Z", - "repoId": 0, - "pullRequestNo": 0 - }, - { - "name": "Vasco0x4", - "id": 63432221, - "comment_id": 4224656688, - "created_at": "2026-04-10T14:55:47Z", - "repoId": 248238912, - "pullRequestNo": 88 - }, - { - "name": "R1ckyH", - "id": 24563814, - "comment_id": 4288929010, - "created_at": "2026-04-21T13:32:38Z", - "repoId": 818418812, - "pullRequestNo": 160 - }, - { - "name": "cnnrshd", - "id": 35985695, - "comment_id": 4317508733, - "created_at": "2026-04-25T02:06:57Z", - "repoId": 468957086, - "pullRequestNo": 3060 - }, - { - "name": "TrebledJ", - "id": 39648915, - "comment_id": 4344410247, - "created_at": "2026-04-29T13:57:25Z", - "repoId": 468957086, - "pullRequestNo": 3068 - }, - { - "name": "ChrisJr404", - "id": 11917633, - "comment_id": 4367407250, - "created_at": "2026-05-03T23:08:48Z", - "repoId": 468957086, - "pullRequestNo": 3071 - }, - { - "name": "anupddas", - "id": 149822306, - "comment_id": 4573910744, - "created_at": "2026-05-29T10:30:34Z", - "repoId": 468957086, - "pullRequestNo": 3137 - }, - { - "name": "NeffIsBack", - "id": 61382599, - "comment_id": 4583404402, - "created_at": "2026-05-30T16:19:47Z", - "repoId": 248238912, - "pullRequestNo": 95 - }, - { - "name": "quantumcore", - "id": 34606382, - "comment_id": 4633374269, - "created_at": "2026-06-05T16:08:09Z", - "repoId": 468957086, - "pullRequestNo": 3142 - }, - { - "name": "thunderstornX", - "id": 92374187, - "comment_id": 4639638871, - "created_at": "2026-06-06T16:17:54Z", - "repoId": 468957086, - "pullRequestNo": 3144 - }, - { - "name": "Kazul33", - "id": 293459966, - "comment_id": 4699895072, - "created_at": "2026-06-13T21:49:32Z", - "repoId": 1268728821, - "pullRequestNo": 1 - }, - { - "name": "ocervell", - "id": 9629314, - "comment_id": 4716241200, - "created_at": "2026-06-16T07:57:32Z", - "repoId": 248238912, - "pullRequestNo": 96 - }, - { - "name": "absolomb", - "id": 26633403, - "comment_id": 4927038869, - "created_at": "2026-07-09T15:54:50Z", - "repoId": 293375616, - "pullRequestNo": 56 - }, - { - "name": "kobihikri", - "id": 4434689, - "comment_id": 5110046556, - "created_at": "2026-07-28T21:48:56Z", - "repoId": 468957086, - "pullRequestNo": 3354 - }, - { - "name": "derrouic", - "id": 11162089, - "comment_id": 5247448659, - "created_at": "2026-08-11T00:04:32Z", - "repoId": 1163743455, - "pullRequestNo": 110 - }, - { - "name": "Elymaro", - "id": 124057003, - "comment_id": 5315254122, - "created_at": "2026-08-17T11:09:29Z", - "repoId": 248238912, - "pullRequestNo": 106 - }, - { - "name": "CyberAuth", - "id": 41711509, - "comment_id": 5359703580, - "created_at": "2026-08-20T17:55:45Z", - "repoId": 248238912, - "pullRequestNo": 107 - }, - { - "name": "DatenpflegeNordHL", - "id": 25302183, - "comment_id": 5477601924, - "created_at": "2026-08-31T11:23:44Z", - "repoId": 468957086, - "pullRequestNo": 3415 - }, - { - "name": "repins267", - "id": 100247628, - "comment_id": 5553278661, - "created_at": "2026-09-05T16:44:40Z", - "repoId": 468957086, - "pullRequestNo": 3420 - }, - { - "name": "singlerider", - "id": 6310006, - "comment_id": 5576938808, - "created_at": "2026-09-07T23:54:25Z", - "repoId": 971631473, - "pullRequestNo": 112 - }, - { - "name": "GabKodes", - "id": 182553822, - "comment_id": 5588028971, - "created_at": "2026-09-08T15:56:08Z", - "repoId": 971631473, - "pullRequestNo": 112 - }, - { - "name": "dashitongzhi", - "id": 118106297, - "comment_id": 5611866477, - "created_at": "2026-09-10T02:44:18Z", - "repoId": 468957086, - "pullRequestNo": 3228 - }, - { - "name": "wandering-walnut", - "id": 192715399, - "comment_id": 5618049377, - "created_at": "2026-09-10T11:37:53Z", - "repoId": 468957086, - "pullRequestNo": 3428 - }, - { - "name": "Tsybon", - "id": 124361179, - "comment_id": 5729671745, - "created_at": "2026-09-18T12:00:37Z", - "repoId": 468957086, - "pullRequestNo": 3443 - }, - { - "name": "Tsybon", - "id": 124361179, - "comment_id": 5730395672, - "created_at": "2026-09-18T13:04:37Z", - "repoId": 468957086, - "pullRequestNo": 3443 - }, - { - "name": "OsirisTechnicalInstitute", - "id": 213735593, - "comment_id": 5986224427, - "created_at": "2026-10-05T00:48:03Z", - "repoId": 468957086, - "pullRequestNo": 3496 - }, - { - "name": "GhostInTheShell-444", - "id": 190395338, - "comment_id": 6013576990, - "created_at": "2026-10-06T09:41:50Z", - "repoId": 818418812, - "pullRequestNo": 189 - } - ] -} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 8d4d221..9c0e84c 100644 --- a/uv.lock +++ b/uv.lock @@ -125,32 +125,7 @@ wheels = [ ] [[package]] -name = "ruff" -version = "0.15.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, -] - -[[package]] -name = "shared-workflows" +name = "release-workflows" version = "0.0.0" source = { virtual = "." } dependencies = [ @@ -177,3 +152,28 @@ dev = [ { name = "pytest", specifier = "==9.0.2" }, { name = "ruff", specifier = "==0.15.10" }, ] + +[[package]] +name = "ruff" +version = "0.15.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +] From 99b9dc07a955c0f51aeddd7563f14ed2ddd24de3 Mon Sep 17 00:00:00 2001 From: Philip Hartlieb Date: Wed, 28 Oct 2026 17:21:41 -0400 Subject: [PATCH 2/5] Scope the CLA token to CLA; document the two release approvals The caller's pull request commits are read with its own GITHUB_TOKEN, so the App token no longer needs the calling repository or write access to it. --- .github/workflows/cla-reusable.yml | 8 ++++---- README.md | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cla-reusable.yml b/.github/workflows/cla-reusable.yml index f7bdc5a..c4f4da5 100644 --- a/.github/workflows/cla-reusable.yml +++ b/.github/workflows/cla-reusable.yml @@ -23,16 +23,16 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: blacklanternsecurity - # CLA holds the signatures; the calling repository's pull request commits are read for private callers. - repositories: CLA,${{ github.event.repository.name }} + # Only CLA, which holds the signatures. The caller's pull request is read with its own GITHUB_TOKEN. + repositories: CLA permission-contents: write permission-members: read - permission-pull-requests: read - name: Check all committers against org and allowlist id: cla-check env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "${{ github.event_name }}" = "pull_request_target" ]; then PR_NUM="${{ github.event.pull_request.number }}" @@ -40,7 +40,7 @@ jobs: PR_NUM="${{ github.event.issue.number }}" fi - COMMITTERS=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM/commits" --paginate --jq '.[].author.login' | sort -u) + COMMITTERS=$(GH_TOKEN="$PR_TOKEN" gh api "repos/${{ github.repository }}/pulls/$PR_NUM/commits" --paginate --jq '.[].author.login' | sort -u) ALL_EXEMPT=true EXEMPT_LOGINS="" diff --git a/README.md b/README.md index 929f50c..49e3fdd 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ App and can't be moved or deleted, so a version comment always matches its SHA. `zizmor`). No approval needed. - `stable`: pull request from `dev`, approved by a code owner other than the last pusher, required checks. Nobody bypasses it. -- A merge to `stable` runs `release.yml`, which waits for a `bbot-leads` approval at the `release` - environment and then tags the merge commit `v` from `pyproject.toml`. +- A merge to `stable` runs `release.yml`, which tags the merge commit `v` from `pyproject.toml`. + It stops twice at the `release` environment, once before checking the release credentials and once + before publishing, and each stop needs a `bbot-leads` approval. Versions follow semantic versioning: a change to a workflow's inputs, outputs, secrets or required permissions that a caller must act on is a major version. From 562abaddb34622df7d7babf66ede5e6eb2f05461 Mon Sep 17 00:00:00 2001 From: Philip Hartlieb Date: Wed, 28 Oct 2026 17:32:00 -0400 Subject: [PATCH 3/5] Run CodeQL on this pull request CodeQL default setup was enabled after the last push; reopening the pull request did not start a scan. From 4b46101e309f725fdfeb5e87709985bc13a31f32 Mon Sep 17 00:00:00 2001 From: Philip Hartlieb Date: Wed, 28 Oct 2026 19:34:15 -0400 Subject: [PATCH 4/5] Run CodeQL on this pull request again The code scanning rule was added to dev-review after the previous push; reopening the pull request did not start a scan. From 57946b11852b16436a48cc263044e47f68877d33 Mon Sep 17 00:00:00 2001 From: Philip Hartlieb Date: Thu, 29 Oct 2026 09:40:59 -0400 Subject: [PATCH 5/5] Release 1.0.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9cad8ba..a23a3b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "release-workflows" -version = "0.0.0" +version = "1.0.0" requires-python = ">=3.11" dependencies = ["packaging", "pyyaml"] diff --git a/uv.lock b/uv.lock index 9c0e84c..c54a35c 100644 --- a/uv.lock +++ b/uv.lock @@ -126,7 +126,7 @@ wheels = [ [[package]] name = "release-workflows" -version = "0.0.0" +version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "packaging" },