Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
self-hosted-runner:
labels:
# The ARC runner scale set label every ordinary job in this repo (and every consumer of ExaDev/runner-fallback-action) targets - see the project README's Architecture section.
# The ARC runner scale set label the fleet smoke tests (test-arc-runner.yml, test-autoscaler.yml) target - see the project README's Architecture section.
- exadev-runners
# The dedicated, separately-labelled, Docker-in-Docker scale-set profile build-runner-image.yml runs on - see ExaDev/github-runner-fleet's host_vars/node-a.yml and values/exadev-runners-values-builder.yaml.
- exadev-image-builder

# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.
Expand Down
33 changes: 0 additions & 33 deletions .github/workflows/build-runner-image.yml

This file was deleted.

66 changes: 65 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,73 @@ jobs:
# playbooks/collections/ansible_collections/exadev/github_runner is a symlink back to this checkout's own root, for local FQCN resolution (see ansible/ansible.cfg's comment on roles_path). Excluding it stops ansible-lint's own directory walk from following that symlink back into the tree it is already linting, which otherwise relints the whole collection a second time under a growing path on every recursion.
run: ansible-lint --profile production --exclude playbooks/collections

collection-build:
name: Collection build check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Install Ansible
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq ansible
- name: Build the collection tarball
run: ansible-galaxy collection build --force
- name: Install the built tarball and verify its content
run: |
set -euo pipefail
tarball=$(ls exadev-github_runner-*.tar.gz)
install_path="$(mktemp -d)"
ansible-galaxy collection install "$tarball" -p "$install_path"
export ANSIBLE_COLLECTIONS_PATH="$install_path"
echo "Roles shipped in the built collection:"
ls "$install_path/ansible_collections/exadev/github_runner/roles"
echo "Filter plugins shipped in the built collection:"
ansible-doc -t filter -l exadev.github_runner | cat

commitlint:
name: commitlint
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
# Runs the repo's own pinned commitlint rather than a Docker-based action: container actions need the runner to pull an image, which the self-hosted runners can't always do.
- name: Lint every commit in the pull request
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npx --no-install commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --verbose

commitlint-pr-title:
name: Lint PR title
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
# Rebase-merges keep each commit's own message, so the title isn't what lands on main; it's still checked so the PR list reads consistently. Passed through env, never interpolated into the script.
- name: Lint the pull request title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE" | npx --no-install commitlint --verbose

required-checks:
name: Required Checks
needs: [shellcheck, actionlint, yamllint, hadolint, ansible-syntax-check, ansible-lint]
needs: [shellcheck, actionlint, yamllint, hadolint, ansible-syntax-check, ansible-lint, collection-build, commitlint, commitlint-pr-title]
# Always runs, even if a dependency failed or was skipped, so it can report that failure itself rather than being skipped - a required check that's silently absent blocks a PR indefinitely instead of failing it visibly.
if: always()
runs-on: ubuntu-latest
Expand Down
104 changes: 104 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Release

# Runs semantic-release against every push to main. Almost every conventional-commit type triggers at least a patch release (see .releaserc.json's commit-analyzer releaseRules), so in practice this fires on nearly every push, stamping the collection version into galaxy.yml and the arc role's heartbeat/autoscaler image defaults, building the collection tarball, and, if a release was actually published, building and pushing all three container images (runner, heartbeat, autoscaler) tagged with the exact new version alongside floating major and latest tags on the same manifest.
#
# The release commit and tag are pushed over SSH with a write-enabled deploy key (secret RELEASE_DEPLOY_KEY): actions/checkout loads it for every later git command, and .releaserc.json's repositoryUrl is the SSH remote, so @semantic-release/git pushes with it. A deploy key is scoped to this one repository and can be listed as a ruleset bypass actor. It can't call the GitHub API, so the semantic-release process uses the ordinary secrets.GITHUB_TOKEN for creating the Release and uploading the collection tarball, which need no bypass.
#
# The release commit message carries [skip ci]: the image build happens in this same job/run immediately after semantic-release publishes, using the exact version it just decided, so there is nothing useful for a second, separately-triggered run of this workflow (or of ci.yml) to do against that commit - ci.yml's own lint jobs already ran against every commit before it reached main, and re-running them again against a commit that only changed a version stamp and a changelog is pure cost.

on:
push:
branches: [main]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # to push the release commit/tag and create the GitHub Release
issues: write # to comment on released issues
pull-requests: write # to comment on released pull requests
outputs:
published: ${{ steps.before.outputs.version != steps.after.outputs.version }}
version: ${{ steps.after.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
# semantic-release analyses the full commit history since the last release.
fetch-depth: 0
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
- name: Read pre-release version
id: before
run: echo "version=$(grep '^version:' galaxy.yml | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GALAXY_API_KEY: ${{ secrets.GALAXY_API_KEY }}
run: npx semantic-release
- name: Read post-release version
id: after
run: echo "version=$(grep '^version:' galaxy.yml | awk '{print $2}')" >> "$GITHUB_OUTPUT"

build-images:
name: Build and push (${{ matrix.name }})
needs: [release]
if: needs.release.outputs.published == 'true'
# GitHub-hosted, like the rest of this repository's CI, so a release never depends on the fleet it ships images for. QEMU builds the arm64 half of each multi-arch image.
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- name: runner
dockerfile: Dockerfile
image: ghcr.io/exadev/github-runner
- name: heartbeat
dockerfile: heartbeat/Dockerfile
image: ghcr.io/exadev/github-runner-heartbeat
- name: autoscaler
dockerfile: autoscaler/Dockerfile
image: ghcr.io/exadev/github-runner-autoscaler
steps:
- uses: actions/checkout@v7
with:
ref: main
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Derive tags
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ matrix.image }}
# value overrides the automatic ref-derived tag: this workflow triggers on a push to main, not a tag push, so there is no git tag for these patterns to derive from on their own. needs.release.outputs.version is the exact version semantic-release just published (no leading v), so it's prefixed here to satisfy each pattern's own parser.
tags: |
type=semver,pattern={{version}},value=v${{ needs.release.outputs.version }}
type=semver,pattern={{major}},value=v${{ needs.release.outputs.version }}
type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/arm64,linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ logs/
__pycache__/
docker-compose.mesh.yml
/headscale/
node_modules/
*.tar.gz
.ansible/
57 changes: 57 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"repositoryUrl": "git@github.com:ExaDev/github-runner.git",
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"releaseRules": [
{ "breaking": true, "release": "major" },
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "revert", "release": "patch" },
{ "type": "refactor", "release": "patch" },
{ "type": "docs", "release": "patch" },
{ "type": "style", "release": "patch" },
{ "type": "test", "release": "patch" },
{ "type": "build", "release": "patch" },
{ "type": "ci", "release": "patch" },
{ "type": "chore", "release": "patch" }
]
}
],
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "python3 scripts/release/stamp_version.py ${nextRelease.version} && ansible-galaxy collection build --force",
"publishCmd": "if [ -n \"$GALAXY_API_KEY\" ]; then ansible-galaxy collection publish exadev-github_runner-${nextRelease.version}.tar.gz --api-key \"$GALAXY_API_KEY\"; else echo 'GALAXY_API_KEY is not set, skipping Ansible Galaxy publish.'; fi"
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "galaxy.yml", "roles/github_runner_arc/defaults/main.yml"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "exadev-github_runner-${nextRelease.version}.tar.gz",
"label": "Ansible collection tarball"
}
]
}
]
]
}
Loading
Loading