From 7b1de7ec0a8d62b55777f4be167bed48c4be4ba6 Mon Sep 17 00:00:00 2001 From: Denis Drobyshev Date: Mon, 10 Aug 2026 15:05:51 +0300 Subject: [PATCH] Bring the repository up to the standard of its siblings stadion is the newest repository here and inherited the least. It had ci.yml and publish.yml and nothing else: no dependency updates, no code scanning, no aggregate check, and until now no branch protection, so anything could be pushed straight to main. Four things: An aggregate `CI` job. The matrix jobs carry their operating system and Python version in their names, so each is a separate status context that changes the moment the matrix does. Requiring those in branch protection means a required check silently stops matching the day a Python version is added. The aggregate gives one stable name to require. dependabot.yml, weekly, on the same schedule as the rest of the organisation so the Monday triage sees this repository too. torch, numpy and scipy are ignored for the same reason as elsewhere: their lower bounds describe what a user may already have, not what CI runs. dependabot-auto-merge.yml, copied unchanged from the version that already has both of its bugs fixed - reading the pull request author rather than the actor, and not calling `gh pr review --approve`, which this organisation does not permit Actions to do. codeql.yml, retargeted from master to main. --- .github/dependabot.yml | 33 ++++++ .github/workflows/ci.yml | 19 ++++ .github/workflows/codeql.yml | 41 +++++++ .github/workflows/dependabot-auto-merge.yml | 115 ++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7d1cd21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + commit-message: + prefix: "ci" + groups: + actions: + patterns: ["*"] + + - package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + day: monday + commit-message: + prefix: "deps" + # torch, numpy and scipy are pinned by a lower bound on purpose: it says + # which versions a user may already have, not which version CI runs. pip + # installs the newest match either way, so raising the floor buys nothing + # and locks users out. + ignore: + - dependency-name: torch + - dependency-name: numpy + - dependency-name: scipy + groups: + dev-tooling: + patterns: ["ruff", "mypy", "pytest*"] + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83256db..4931fc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,3 +85,22 @@ jobs: stadion run "$task" --agent "$agent" --instances 10 --episodes 8 done done + + ci: + name: CI + runs-on: ubuntu-latest + if: always() + needs: [lint-and-test, verify-optima, reference-controls] + steps: + # One aggregate check to require in branch protection. The matrix jobs + # carry their operating system and Python version in the name, so every + # one of them is a separate context that changes the moment the matrix + # does - and a required check that silently stops matching is a required + # check that stopped requiring anything. + - name: Fail if any job did not succeed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + echo "One or more jobs failed:" + echo '${{ toJSON(needs) }}' + exit 1 + - run: echo "All checks passed." diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..14c5ab2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly, because advisories land between pushes: a rule added after the + # last commit would otherwise never run against this code. + - cron: "17 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + analyze: + name: Analyze Python + runs-on: ubuntu-latest + # Code scanning on a private repository needs GitHub Advanced Security. + # Skipped rather than left permanently red, and it starts running by itself + # if the repository ever goes private and comes back. + if: github.event.repository.visibility == 'public' + permissions: + security-events: write + actions: read + contents: read + steps: + - uses: actions/checkout@v7 + + - uses: github/codeql-action/init@v4 + with: + languages: python + # security-and-quality over the default: this service parses untrusted + # documents and serves HTTP, so the extra checks earn their noise. + queries: security-and-quality + + - uses: github/codeql-action/analyze@v4 + with: + category: "/language:python" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..813d67d --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,115 @@ +name: Dependabot auto-merge + +# What this does and does not do: +# +# auto-merged - GitHub Actions bumps, and patch bumps of anything else +# left for you - anything with a minor or major in it +# +# A patch release and an action bump are the updates that pile up unread until +# the queue is too long to review honestly. A minor bump can change behaviour, +# so it keeps a human. Auto-merge is queued, not immediate: GitHub still waits +# for the required checks to pass, and a red build leaves the PR open. +# +# Dependabot watches actions and pip here. The file is kept identical to the +# other repositories so they stay comparable. + +on: pull_request_target + +permissions: + contents: read + +jobs: + auto-merge: + # Who opened the pull request, not who triggered the event. `github.actor` + # is whoever caused this run, so the moment a person touches a Dependabot + # pull request - reopening it, or nudging it after a base change - the job + # skips and the update sits there looking merged-ready and never merging. + # The author never changes, which is the thing actually being asserted. + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + # Reads the update metadata from the PR that Dependabot opened. Nothing + # from the branch is checked out or executed, which is what makes + # pull_request_target safe to use here. + - id: metadata + uses: dependabot/fetch-metadata@v3 + + - id: verdict + name: Decide whether this one can merge itself + env: + ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + UPDATED: ${{ steps.metadata.outputs.updated-dependencies-json }} + # A grouped pull request has no single update type: fetch-metadata + # leaves `update-type` empty and puts one entry per dependency in + # `updated-dependencies-json`. Reading only `update-type` sent every + # grouped bump to a human, including a group where all seven were + # patches - which is exactly the group worth merging unattended, and the + # reason the groups exist at all. + run: | + PATCH="version-update:semver-patch" + + if [ "$ECOSYSTEM" = "github_actions" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=an actions bump" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ -n "$UPDATE_TYPE" ] && [ "$UPDATE_TYPE" != "null" ]; then + if [ "$UPDATE_TYPE" = "$PATCH" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=a patch bump" >> "$GITHUB_OUTPUT" + else + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=${UPDATE_TYPE#version-update:semver-} is not a patch" >> "$GITHUB_OUTPUT" + fi + exit 0 + fi + + total=$(jq 'length' <<<"$UPDATED") + if [ "$total" -eq 0 ]; then + # No metadata to read. Refusing is the only safe reading of silence. + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=no update metadata to read" >> "$GITHUB_OUTPUT" + exit 0 + fi + + patches=$(jq --arg p "$PATCH" '[.[] | select(.updateType == $p)] | length' <<<"$UPDATED") + if [ "$total" -eq "$patches" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=a group of $total, every one a patch" >> "$GITHUB_OUTPUT" + else + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=a group of $total, $((total - patches)) beyond patch" >> "$GITHUB_OUTPUT" + fi + + - name: Queue the merge + if: steps.verdict.outputs.auto == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.html_url }} + REASON: ${{ steps.verdict.outputs.reason }} + # No `gh pr review --approve`. This organisation does not permit Actions + # to approve pull requests, so that call fails with + # + # GitHub Actions is not permitted to approve pull requests + # + # and under `bash -e` it took the whole step down before the merge was + # ever queued - which is how auto-merge came to be broken in every + # repository at once. + # + # The approval was never needed: branch protection here requires the CI + # check and no reviews. If a review requirement is ever added, this needs + # a token that is not GITHUB_TOKEN, not a retry. + run: | + echo "auto-merging: $REASON" + gh pr merge --auto --squash "$PR" + + - name: Explain why this one was left alone + if: steps.verdict.outputs.auto != 'true' + env: + REASON: ${{ steps.verdict.outputs.reason }} + run: echo "not auto-merged ($REASON); this pull request needs a human."