Re-usable GitHub Actions and Workflows
GitHub Actions
Retry Command
This GitHub Action retries a shell command if it fails. It is useful for handling flaky tests in CI/CD pipelines.
Inputs
command(required): The shell command to run.max_attempts(optional): The number of retry attempts. Defaults to3.delay_seconds(optional): The delay between retries in seconds. Defaults to5.
Usage Example
You can use this action in your workflow as follows:
name: Retry Example
on:
push:
branches:
- main
jobs:
retry-command-example:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Test
uses: openwisp/openwisp-utils/.github/actions/retry-command@master
with:
delay_seconds: 30
max_attempts: 5
command: ./runtests.py --parallel
env:
SELENIUM_HEADLESS: 1
This example retries the ./runtests.py --parallel command up to 5
times with a 30 second delay between attempts.
Note
If the command continues to fail after the specified number of attempts, the action will exit with a non-zero status, causing the workflow to fail.
Auto-Assignment Bot
A collection of Python scripts that automate issue and PR management for OpenWISP repositories. The bot provides the following features:
Issue auto-assignment: When a contributor opens a PR referencing an issue (e.g.,
Fixes #123), the issue is automatically assigned to the PR author.Assignment request responses: When someone comments asking to be assigned, the bot responds with contributing guidelines explaining that no assignment is needed — just open a PR.
Stale PR management: Warns PR authors after 7 days of inactivity, marks stale and unassigns after 14 days, and closes after 60 days.
PR reopen reassignment: When a stale PR is reopened, linked issues are reassigned back to the author.
Secrets
These secrets are used by the workflow to generate a GITHUB_TOKEN via
the actions/create-github-app-token action. The bot itself consumes
the following environment variables at runtime: GITHUB_TOKEN,
REPOSITORY, and GITHUB_EVENT_NAME.
OPENWISP_BOT_APP_ID(required): OpenWISP Bot GitHub App ID.OPENWISP_BOT_PRIVATE_KEY(required): OpenWISP Bot GitHub App private key.
Setup for Other Repositories
To enable the auto-assignment bot in another OpenWISP repository, you must
create four workflow files under .github/workflows/ that call the
reusable GitHub Workflow. This reusable workflow automatically handles
token generation, environment setup, and executing the bot scripts.
Note
Each caller workflow must declare its own permissions block.
GitHub Actions reusable workflows inherit permissions from the caller,
so the reusable workflow cannot set them on its own.
Create the following workflow files in your repository.
1. Issue Assignment Bot
(.github/workflows/bot-autoassign-issue.yml)
name: Issue Assignment Bot
on:
issue_comment:
types: [created]
permissions:
contents: read
issues: write
concurrency:
group: bot-autoassign-issue-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
respond-to-assign-request:
if: github.event.issue.pull_request == null
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-autoassign.yml@master
with:
bot_command: issue_assignment
secrets:
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
2. PR Issue Link
(.github/workflows/bot-autoassign-pr-issue-link.yml)
name: PR Issue Auto-Assignment
on:
pull_request_target:
types: [opened, reopened, closed]
permissions:
contents: read
issues: write
pull-requests: read
concurrency:
group: bot-autoassign-pr-link-${{ github.repository }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
auto-assign-issue:
if: github.event.action != 'closed' || github.event.pull_request.merged == false
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-autoassign.yml@master
with:
bot_command: issue_assignment
secrets:
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
3. PR Reopen (.github/workflows/bot-autoassign-pr-reopen.yml)
name: PR Reopen Reassignment
on:
pull_request_target:
types: [reopened]
issue_comment:
types: [created]
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: bot-autoassign-pr-reopen-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
jobs:
reassign-on-reopen:
if: github.event_name == 'pull_request_target' && github.event.action == 'reopened'
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-autoassign.yml@master
with:
bot_command: pr_reopen
secrets:
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
handle-pr-activity:
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-autoassign.yml@master
with:
bot_command: pr_reopen
secrets:
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
Note
Both jobs use bot_command: pr_reopen. The pr_reopen command
dispatches to PRReopenBot on pull_request_target events (to
reassign issues when a PR is reopened) and to PRActivityBot on
issue_comment events (to remove the stale label when the PR author
comments on their stale PR).
4. Stale PR (.github/workflows/bot-autoassign-stale-pr.yml)
name: Stale PR Management
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: bot-autoassign-stale-pr-${{ github.repository }}
cancel-in-progress: false
jobs:
manage-stale-prs-python:
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-autoassign.yml@master
with:
bot_command: stale_pr
secrets:
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
GitHub Workflows
Replicate Commits to Version Branch
This re-usable workflow replicates commits from the master branch to a
version branch. The version branch name is derived from the version of the
Python package specified in the workflow.
Version branches are essential during development to ensure that each
OpenWISP module depends on compatible versions of its OpenWISP
dependencies. Without version branches, modules depending on the
master branch of other modules may encounter errors, as the master
branch could include future changes that are incompatible with previous
versions. This makes it impossible to build a specific commit reliably
after such changes.
To address this, we use version branches so that each module can depend on
a compatible version of its dependencies during development. Managing
these version branches manually is time-consuming, which is why this
re-usable GitHub workflow automates the process of keeping version
branches synchronized with the master branch.
You can invoke this workflow from another workflow using the following example:
name: Replicate Commits to Version Branch
on:
push:
branches:
- master
jobs:
version-branch:
uses: openwisp/openwisp-utils/.github/workflows/reusable-version-branch.yml@master
with:
# The name of the Python package (required)
module_name: openwisp_utils
# Whether to install the Python package. Defaults to false.
install_package: true
Note
If the master branch is force-pushed, this workflow will fail due
to conflicts. To resolve this, you must manually synchronize the
version branch with the master branch. You can use the following
commands to perform this synchronization:
VERSION=<enter-version-number> # e.g. 1.2
git fetch origin
git checkout $VERSION
git reset --hard origin/master
git push origin $VERSION --force-with-lease
Backport Fixes to Stable Branch
This re-usable workflow automates cherry-picking fixes from master or
main to stable release branches.
It supports two triggers:
Commit message: Add
[backport X.Y]or[backport: X.Y]to the squash merge commit body to automatically backport when merged tomasterormain.Comment: Comment
/backport X.Yon a merged PR (org members only).
If the cherry-pick fails due to conflicts, the bot comments on the PR with manual resolution steps. If the target branch does not exist or the PR is not yet merged, the workflow exits safely without failing.
name: Backport fixes to stable branch
on:
push:
branches:
- master
- main
issue_comment:
types: [created]
concurrency:
group: backport-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
backport-on-push:
if: github.event_name == 'push'
uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master
with:
commit_sha: ${{ github.sha }}
secrets:
app_id: ${{ secrets.OPENWISP_BOT_APP_ID }}
private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
backport-on-comment:
if: >
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.issue.pull_request.merged_at != null &&
github.event.issue.state == 'closed' &&
contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) &&
startsWith(github.event.comment.body, '/backport')
uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master
with:
pr_number: ${{ github.event.issue.number }}
comment_body: ${{ github.event.comment.body }}
secrets:
app_id: ${{ secrets.OPENWISP_BOT_APP_ID }}
private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
Automated CI Failure Bot
To assist contributors with debugging, this reusable workflow leverages Google's Gemini API to analyze continuous integration failures in real-time. Upon detecting a failed CI run, it intelligently gathers the relevant source code context (safely bypassing unnecessary assets) alongside the raw error logs. It then posts a concise summary and an actionable remediation plan directly to the Pull Request.
When the bot detects that all failures are transient (e.g., network
errors, browser crashes, Coveralls flakiness), it automatically re-runs
the failed jobs up to 3 times and posts a short notification instead of
the full analysis. This requires actions: write permission in the
caller workflow and the GitHub App must have the Actions permission
enabled. If the permission is not granted (e.g., in repositories that
haven't updated their caller workflow yet), the auto-retry is skipped
gracefully and the full analysis is posted instead.
This workflow is intended to be triggered via the workflow_run event
after your primary test suite concludes. It features strict
cross-repository concurrency locks and token limits to prevent PR spam on
rapid, consecutive commits.
Usage Example
Set up a caller workflow in your repository (e.g.,
.github/workflows/bot-ci-failure.yml) that monitors your primary CI
job:
name: CI Failure Bot (Caller)
on:
workflow_run:
workflows: ["CI Build"]
types:
- completed
permissions:
pull-requests: read
actions: read
contents: read
concurrency:
group: ci-failure-${{ github.repository }}-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
find-pr:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }}
outputs:
pr_number: ${{ steps.pr.outputs.number }}
pr_author: ${{ steps.pr.outputs.author }}
steps:
- name: Find PR Number
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER_PAYLOAD: ${{ github.event.workflow_run.pull_requests[0].number }}
EVENT_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
emit_pr() {
local pr_number="$1"
local pr_author
pr_author=$(gh pr view "$pr_number" --repo "$REPO" --json author --jq '.author.login // empty' 2>/dev/null || echo "")
if [ -z "$pr_author" ] || [ "$pr_author" = "null" ]; then
echo "::warning::Could not fetch PR author for PR #$pr_number"
fi
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
echo "author=$pr_author" >> "$GITHUB_OUTPUT"
}
PR_NUMBER="$PR_NUMBER_PAYLOAD"
if [ -n "$PR_NUMBER" ]; then
echo "Found PR #$PR_NUMBER from workflow payload."
emit_pr "$PR_NUMBER"
exit 0
fi
HEAD_SHA="$EVENT_HEAD_SHA"
echo "Payload empty. Searching for PR via Commits API..."
PR_NUMBER=$(gh api repos/$REPO/commits/$HEAD_SHA/pulls -q '.[0].number' 2>/dev/null || true)
if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then
echo "Found PR #$PR_NUMBER using Commits API."
emit_pr "$PR_NUMBER"
exit 0
fi
echo "API lookup failed/empty. Scanning open PRs for matching head SHA..."
PR_NUMBER=$(gh pr list --repo "$REPO" --state open --limit 100 --json number,headRefOid --jq ".[] | select(.headRefOid == \"$HEAD_SHA\") | .number" | head -n 1)
if [ -n "$PR_NUMBER" ]; then
echo "Found PR #$PR_NUMBER by scanning open PRs."
emit_pr "$PR_NUMBER"
exit 0
fi
echo "::warning::No open PR found. This workflow run might not be attached to an open PR."
exit 0
call-ci-failure-bot:
needs: find-pr
if: ${{ needs.find-pr.outputs.pr_number != '' }}
permissions:
pull-requests: write
actions: write
contents: read
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-ci-failure.yml@master
with:
pr_number: ${{ needs.find-pr.outputs.pr_number }}
head_sha: ${{ github.event.workflow_run.head_sha }}
head_repo: ${{ github.event.workflow_run.head_repository.full_name }}
base_repo: ${{ github.repository }}
run_id: ${{ github.event.workflow_run.id }}
pr_author: ${{ needs.find-pr.outputs.pr_author }}
actor: ${{ github.event.workflow_run.actor.login }}
secrets:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
Changelog Bot
This workflow automatically generates changelog entry suggestions for Pull
Requests using Google Gemini. It gets triggered when a PR with a title
prefixed with [feature], [fix], or [change] is approved by a
maintainer. It analyzes the PR's title, description, code changes, and
linked issues, then posts a properly formatted changelog entry as a
comment on the PR.
The bot uses a two-workflow pattern to support PRs from forks. The first
workflow is triggered by pull_request_review: it checks whether the PR
qualifies and uploads the PR number as an artifact, but requires no
secrets. The second workflow executes via workflow_run once the first
completes: it runs in the context of the base repository, has full access
to secrets, and is the one that generates and posts the changelog comment.
Secrets
GEMINI_API_KEY(required): Google Gemini API key.OPENWISP_BOT_APP_ID(required): OpenWISP Bot GitHub App ID.OPENWISP_BOT_PRIVATE_KEY(required): OpenWISP Bot GitHub App private key.
Setup for Other Repositories
To enable the changelog bot in any OpenWISP repository, create the following two workflow files.
1. Trigger (.github/workflows/bot-changelog-trigger.yml)
This workflow fires on PR approval, checks if the PR is noteworthy, and uploads the PR number as an artifact for the runner to pick up.
name: Changelog Bot Trigger
on:
pull_request_review:
types: [submitted]
jobs:
check:
if: |
github.event.review.state == 'approved' &&
(github.event.review.author_association == 'OWNER' ||
github.event.review.author_association == 'MEMBER' ||
github.event.review.author_association == 'COLLABORATOR')
runs-on: ubuntu-latest
steps:
- name: Check for noteworthy PR
id: check
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if echo "$PR_TITLE" | grep -qiE '^\[(feature|fix|change)\]'; then
echo "has_noteworthy=true" >> $GITHUB_OUTPUT
fi
- name: Save PR metadata
if: steps.check.outputs.has_noteworthy == 'true'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: echo "$PR_NUMBER" > pr_number
- name: Upload PR metadata
if: steps.check.outputs.has_noteworthy == 'true'
uses: actions/upload-artifact@v4
with:
name: changelog-metadata
path: pr_number
2. Runner (.github/workflows/bot-changelog-runner.yml)
This workflow triggers after the trigger completes, downloads the artifact, and calls the reusable workflow with full secret access.
name: Changelog Bot Runner
on:
workflow_run:
workflows: ["Changelog Bot Trigger"]
types:
- completed
permissions:
actions: read
contents: read
pull-requests: write
issues: write
jobs:
fetch-metadata:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
outputs:
pr_number: ${{ steps.metadata.outputs.pr_number }}
steps:
- name: Download PR metadata
id: download
uses: actions/download-artifact@v4
with:
name: changelog-metadata
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
continue-on-error: true
- name: Read PR metadata
if: steps.download.outcome == 'success'
id: metadata
run: |
PR_NUMBER=$(cat pr_number)
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: $PR_NUMBER"
exit 1
fi
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
changelog:
needs: fetch-metadata
if: needs.fetch-metadata.outputs.pr_number != ''
uses: openwisp/openwisp-utils/.github/workflows/reusable-bot-changelog.yml@master
with:
pr_number: ${{ needs.fetch-metadata.outputs.pr_number }}
secrets:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENWISP_BOT_APP_ID: ${{ secrets.OPENWISP_BOT_APP_ID }}
OPENWISP_BOT_PRIVATE_KEY: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
Note
The name field in the trigger workflow must be exactly Changelog
Bot Trigger. The runner watches for this name via workflow_run.
Changing it will silently break the connection between the two
workflows.
Both bot-changelog-trigger.yml and bot-changelog-runner.yml
must be committed to the default branch of the repository. GitHub
only activates workflow_run listeners that exist on the default
branch: adding the runner only to a feature branch will cause it to
silently do nothing.