Antonin Raffin

@araffin.bsky.social

Researcher in robotics and machine learning (Reinforcement Learning). Maintainer of Stable-Baselines (SB3). https://araffin.github.io/

Heads up ruff users - with the release of 0.16.0 this week the number of default rules went up from 59 to to 413. This means that if you're ruff version auto updates, your CI pipelines might start failing on you all of a sudden.

Simon Willison@simonwillison.net · 2w ago

Ruff 0.16.0 - Astral's fast Python linter - came out a few days ago and increased the number of default-enabled rules from 59 to 413, which highlighted all sorts of problems across my projects (1618 in sqlite-utils alone) simonwillison.net/2026/Jul/25/...

New software blog post on migrating continuous integration from GitHub to Codeberg: https://scaron.info/blog/migrating-continuous-integration-from-github-to-codeberg.html After using it for 3-4 months, I found Woodpecker CI simple and to the point. My experience with it has been very positive.

Over the past few months, I've been experimenting with migrating projects from GitHub to Codeberg. Four projects in, I've found that the main non-trivial step is migrating continuous integration (CI) from GitHub Actions to Codeberg's instance of Woodpecker CI. Most other steps are straightforward, as Codeberg is mostly at feature parity with GitHub and follows its main design principles. In this note, we thus focus on CI migration. First, we will summarize the steps to activate Woodpecker CI in your Codeberg repository, and second, we will see template CI pipelines for common use cases like linting, documentation and testing. # Activating Woodpecker CI Codeberg has two continuous integration systems: Woodpecker CI and Forgejo Actions. Woodpecker CI is a standalone system with its own pipeline format, configured in `.woodpecker/*.yml` files. It spins up containers where each step runs in a Docker image. We will go with Woodpecker CI as it is readily available on Codeberg and does not require self-hosted runners. Woodpecker CI access on Codeberg is only provided for open-source projects, and as such it is not granted by default. The first step is to fill out a CI request issue: here is for instance the one I filed for `lpsolvers`, the first project I migrated. This approval is only needed once. Afterwards, CI access will be granted to all your future open-source projects. Once access has been granted, you can activate continuous integration for your repository as follows: * Log into Woodpecker CI at ci.codeberg.org * Click on "Repositories" in the top menu * Click on `+ Add repository`: this displays a list of your repositories * Click on `Enable` next to your repository After activation, any push or pull request that includes pipeline files in `.woodpecker/` will trigger a build. # Woodpecker CI workflows A quick word on terminology, as workflows don't mean the same thing in GitHub Actions and Woodpecker CI. In GitHub Actions, workflows defined in `.github/workflows/*.yml` are the top-level unit of context that run in separate containers. Each workflow holds one or more _jobs_ , each job defining a sequence of _steps_. In Woodpecker CI, the top-level unit is called a _pipeline_. Workflows are defined in `.woodpecker/*.yml`, but a workflow is directly a sequence of steps. The correspondence is then: * **GitHub Actions:** workflows define jobs that execute steps * **Woodpecker CI:** pipelines define workflows that execute steps A Woodpecker _workflow_ is therefore the equivalent of a GitHub Actions _job_. Beyond the naming, the semantics are familiar: workflows run in parallel in separate containers by default, dependencies are opt-in, and files are shared only within a workflow, so that passing artifacts across workflows needs external storage, just as it does across GitHub jobs. Let us now take a look at workflow examples for Python projects using pixi. You should be able to adapt them to your language and tooling of choice. ## Linting On GitHub, a workflow named for instance `.github/workflows/lint.yml` would look like: on: [push, pull_request] jobs: lint: name: "Code style" runs-on: ubuntu-latest steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.44.0 cache: true - name: "Run linting" run: | pixi run -e lint lint On Codeberg, it becomes a similar `.woodpecker/lint.yml`: when: - event: [push, pull_request] steps: - name: lint image: ghcr.io/prefix-dev/pixi:latest commands: - pixi run -e lint lint Note how the `actions/checkout` from GitHub Actions is gone: before running steps, Woodpecker automatically clones the repository (using the `woodpeckerci/plugin-git` image) by default into the workspace. The behavior can be overridden in a `clone:` section of the workflow if needed, for instance to shallow-clone with a specific depth. ## Testing On GitHub, a workflow named for instance `.github/workflows/test.yml` would look like: on: [push, pull_request] jobs: test: name: "Test with ${{ matrix.pyenv }}" runs-on: ubuntu-latest strategy: matrix: pyenv: [py310, py311, py312, py313] steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.44.0 cache: true - name: "Test with pixi" run: | pixi run -e test-${{ matrix.pyenv }} test On Codeberg, it becomes `.woodpecker/test.yml`: when: - event: [push, pull_request] matrix: include: - PIXI_ENV: test-py310 - PIXI_ENV: test-py311 - PIXI_ENV: test-py312 - PIXI_ENV: test-py313 steps: - name: test image: ghcr.io/prefix-dev/pixi:latest commands: - pixi run -e ${PIXI_ENV} test ## Documentation On GitHub, a workflow named for instance `.github/workflows/docs.yml` would look like: on: [push, pull_request] jobs: docs: name: "GitHub Pages" runs-on: ubuntu-latest permissions: contents: write steps: - name: "Checkout Git repository" uses: actions/checkout@v4 - name: "Setup pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.44.0 cache: true - name: "Build documentation" run: | pixi run -e docs docs-build - name: "Deploy to GitHub Pages" uses: peaceiris/actions-gh-pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} with: publish_branch: gh-pages github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: _build/ force_orphan: true On Codeberg, GitHub Pages is replaced by Codeberg Pages, which serves static files from a `pages` branch. The following pipeline builds Sphinx documentation, then deploys the output to that branch. The documentation will then be served at `https://USERNAME.codeberg.page/REPONAME/`. On Codeberg, the pipeline becomes `.woodpecker/docs.yml`: when: - event: [push, pull_request] steps: - name: build image: ghcr.io/prefix-dev/pixi:latest commands: - pixi run -e docs docs-build - name: deploy image: alpine/git commands: - | if git clone --depth 1 --branch pages \ "https://x-token:$$CBTOKEN@codeberg.org/${CI_REPO_OWNER}/${CI_REPO_NAME}.git" \ _pages; then : else git clone --depth 1 \ "https://x-token:$$CBTOKEN@codeberg.org/${CI_REPO_OWNER}/${CI_REPO_NAME}.git" \ _pages git -C _pages checkout --orphan pages fi - git config --global --add safe.directory "$(pwd)/_pages" - git config --global user.email "ci@noreply.codeberg.org" - git config --global user.name "CI" - git -C _pages rm -rf --ignore-unmatch . - cp -r _build/. _pages/ - git -C _pages add -A - git -C _pages diff --cached --quiet || git -C _pages commit -m "Deploy documentation [CI SKIP]" - git -C _pages push origin pages environment: CBTOKEN: from_secret: cbtoken when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} Note the `[CI SKIP]` label in the automated commit: steps that push to branches must include it in their commit messages to prevent triggering new pipeline runs. This template uses a `cbtoken` secret as it requires permissions to push to the `pages` branch. See below for a summary of the steps to follow to configure this secret in Woodpecker CI. ## Coverage GitHub projects commonly use Coveralls or Codecov for coverage reporting, but these services don't integrate with Codeberg. On Codeberg, we will go instead for a self-hosted solution where we generate a `coverage/` subdirectory of the documentation website, populated by `coverage html` and pushed to the `pages` branch alongside the Sphinx docs. We also use genbadge to generate a `badge.svg` displayed in the readme and linking to the coverage report. On GitHub with Coveralls, a workflow named for instance `.github/workflows/coverage.yml` would look like: on: [push, pull_request] jobs: coverage: name: "Coverage" runs-on: ubuntu-latest steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.44.0 cache: true - name: "Install coveralls" run: | pip install coveralls - name: "Check code coverage" run: | pixi run -e coverage coverage - name: "Coveralls" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | coveralls --service=github In the Codeberg variant, we generate a coverage badge using `genbadge` locally from our coverage report: mkdir -p coverage coverage erase && coverage run -m unittest discover coverage report --include='<project-name>/*' # Generate the coverage badge coverage xml --include='<project-name>/*' -o coverage/coverage.xml genbadge coverage -i coverage/coverage.xml -o coverage/badge.svg # Generate the HTML coverage report coverage html --include='<project-name>/*' --directory=coverage/html/ We then push both the badge and HTML coverage reports to a sub-directory of the `pages` branch. On Codeberg, the pipeline becomes `.woodpecker/coverage.yml`: when: - event: [push, pull_request] steps: - name: coverage image: ghcr.io/prefix-dev/pixi:latest commands: - pixi run -e coverage coverage - name: publish-coverage image: alpine/git commands: - git clone --depth 1 --branch pages "https://x-token:$$CBTOKEN@codeberg.org/${CI_REPO_OWNER}/${CI_REPO_NAME}.git" _pages - rm -rf _pages/coverage/ - mkdir -p _pages/coverage - cp -r coverage/html/. _pages/coverage/ - cp coverage/badge.svg _pages/coverage/badge.svg - rm -f _pages/coverage/.gitignore - cd _pages - git config --global --add safe.directory "$(pwd)" - git config --global user.email "ci@noreply.codeberg.org" - git config --global user.name "CI" - git remote set-url origin "https://x-token:$$CBTOKEN@codeberg.org/${CI_REPO_OWNER}/${CI_REPO_NAME}.git" - git add --all - git diff --cached --quiet || git commit -m "Update coverage report [CI SKIP]" - git push -u origin pages environment: CBTOKEN: from_secret: cbtoken when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} depends_on: - docs Note how we made the coverage workflow depend on the documentation one, so that the `pages` branch already exists when it runs. We also used again the `[CI SKIP]` label in the automated commit message so that pushing doesn't trigger an additional pipeline run. This template uses a `cbtoken` secret as it requires permissions to push to the `pages` branch. See below for a summary of the steps to follow to configure this secret in Woodpecker CI. # Secrets If your workflows perform some restricted operations, like pushing to branches for coverage or documentation, Woodpecker will need a Codeberg personal access token. You can set it up as follows: * On Codeberg: * Go to Settings * Create a personal access token with "Repository Read & Write" permissions. * **Note down the generated string carefully.** As far as I understand, you won't be able to display it again later on. * On Woodpecker CI: * Go to the repository's settings (cog icon) * Go to the Secrets tab * Add the copied token as a secret named `cbtoken` In the template workflows above, secrets are exposed to steps via the `environment` block with `from_secret`: environment: CBTOKEN: from_secret: cbtoken The token can then be used in git clone or push URLs as `https://x-token:$$CBTOKEN@codeberg.org/...`, as in the documentation and coverage workflows above. The double `$$` is important: Woodpecker pre-processes `${VAR}` syntax and would resolve the token to an empty string before the shell ever sees it. Using `$$CBTOKEN` escapes Woodpecker's substitution so that the shell resolves the variable from the environment at runtime. This won't cause a leak, as secrets are automatically redacted from logs. # To go further In this post, we used Woodpecker CI for continuous integration. After using it for 3-4 months, this system has given me the impression of being simple and to the point, and my overall experience has been entirely positive. If you are looking for more details not covered in this post, the main page to start from is Working with Codeberg's CI in the Codeberg documentation. You can also check out Woodpecker CI examples for other languages in the Codeberg-CI / examples repository. ## Discussion ¶ Feel free to post a comment by e-mail using the form below. Your e-mail address will not be disclosed. 📝 You can use Markdown with `$\LaTeX$` formulas in your comment. Name Website Comment By clicking the button below, you agree to the publication of your comment on this page. Post by e-mail Opens your e-mail client.

scaron.info

Noodling on some lab documentation for LLM usage but high level: 1. LLMs still make bad design decisions. Do not let them make design choices. 2. You’re still responsible for your code correctness. This means reading a lot of code. Hence, bullet point 1.

folks, please don't submit LLM-generated PRs to open source projects. It makes no sense. If the maintainers want to use an LLM to fix an issue, they can use Claude or whatnot directly. They don't need you as intermediary, that's just silly. If they don't want to use LLMs, they have reasons.

This is something I talk about in my paper, where I suggest being explicit about {\gamma}_train (some methods use multiple gammas during training) and \gamma_eval. One of my students is empirically investigating this and, as one would expect, it can have a huge impact. arxiv.org/abs/2510.16175

The Formalism-Implementation Gap in Reinforcement Learning Research

The last decade has seen an upswing in interest and adoption of reinforcement learning (RL) techniques, in large part due to its demonstrated capabilities at performing certain tasks at "super-human l...

arxiv.org