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
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.git
.env
rootfs.ext4

# Nothing below is COPYed by the Dockerfile. Excluding it keeps the build
# context small and makes it structurally impossible for a docs, test, or CI
# edit to reach the builder at all.
.github
.gitignore
CHANGELOG.md
IMAGE-SIZE.md
Makefile
README.md
RELEASES.md
VERSION
tests
tools
127 changes: 127 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: CI

# Building and smoke-testing the image costs many minutes, so prose-only
# changes skip it. The ignore list is deliberately explicit rather than a
# `*.md` glob: VERSION and CHANGELOG.md must keep triggering CI. They are the
# release intent, a release commit usually touches nothing else, and
# tag-release.yaml only fires after a successful CI run on main.
#
# This is an ignore list, not a `paths` allowlist, so that a new directory
# nobody remembered to register still gets built and tested.
on:
push:
branches:
- main
- next
tags-ignore:
- 'v*'
paths-ignore:
- 'README.md'
- 'RELEASES.md'
- 'IMAGE-SIZE.md'
- '.gitignore'
pull_request:
branches:
- main
- next
paths-ignore:
- 'README.md'
- 'RELEASES.md'
- 'IMAGE-SIZE.md'
- '.gitignore'

permissions:
contents: read

jobs:
build:
name: Build and smoke test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Validate release metadata
shell: bash
run: |
set -euo pipefail

VERSION="$(tr -d '[:space:]' < VERSION)"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "VERSION is not valid semantic versioning: $VERSION" >&2
exit 1
fi

if ! grep -q "^## \\[$VERSION\\]" CHANGELOG.md; then
echo "CHANGELOG.md has no section for $VERSION" >&2
exit 1
fi

# Seconds, and needs no Docker. Running it before the build means a typo
# in a shell script or a Makefile pin that drifted from the Dockerfile
# fails here instead of after a full image build.
- name: Validate scripts and pinned metadata
run: make check

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build image
uses: docker/build-push-action@v7
with:
context: .
load: true
platforms: linux/amd64
push: false
tags: pdparchitect/buzznode:ci
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke test
shell: bash
run: |
set -euo pipefail

container="buzznode-ci"
cleanup() {
docker rm --force "$container" >/dev/null 2>&1 || true
}
trap cleanup EXIT

docker run --detach \
--name "$container" \
--platform linux/amd64 \
--shm-size 1g \
--publish 127.0.0.1:16903:6901 \
pdparchitect/buzznode:ci

ready=false
for attempt in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:16903/index.html \
>/dev/null; then
ready=true
break
fi
sleep 2
done

if [ "$ready" != "true" ]; then
echo "Buzznode did not become ready within 120 seconds" >&2
docker logs --tail 150 "$container" >&2 || true
exit 1
fi

docker exec "$container" bash -ec '
for command in agent-runtime-login buzznode buzz buzz-acp \
buzz-agent buzz-dev-mcp codex codex-acp claude \
claude-agent-acp goose; do
command -v "$command" >/dev/null
done
! command -v buzz-desktop >/dev/null
! command -v buzz-relay >/dev/null
! command -v postgres >/dev/null
curl -fsS http://127.0.0.1:6901/ >/dev/null
'

echo "Buzznode is ready with its desktop and agent runtimes"
126 changes: 126 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Release

on:
push:
tags:
- 'v*'
# Dispatched by tag-release.yaml because tags pushed with GITHUB_TOKEN do not
# trigger another workflow.
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

permissions:
contents: write
packages: write

jobs:
validate:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Validate tag and changelog
shell: bash
run: |
set -euo pipefail

VERSION="$(tr -d '[:space:]' < VERSION)"
if [[ "$GITHUB_REF_NAME" != "v${VERSION}" ]]; then
echo "Tag $GITHUB_REF_NAME does not match VERSION v${VERSION}" >&2
exit 1
fi

if ! grep -q "^## \\[$VERSION\\]" CHANGELOG.md; then
echo "CHANGELOG.md has no section for $VERSION" >&2
exit 1
fi

publish:
needs: validate
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=raw,value=${{ github.ref_name }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
labels: |
org.opencontainers.image.title=Buzznode
org.opencontainers.image.description=One persistent browser-accessible computer for one Buzz agent

- name: Build and publish image
id: build
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true

- name: Generate release notes
id: notes
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail

VERSION="${GITHUB_REF_NAME#v}"
{
echo "content<<RELEASE_EOF"
echo "Container image: \`${IMAGE}:${GITHUB_REF_NAME}\`"
echo
echo "Digest: \`${DIGEST}\`"
echo
awk -v version="$VERSION" '
$0 ~ "^## \\[" version "\\]" {
found = 1
next
}
found && /^## \[/ {
exit
}
found {
print
}
' CHANGELOG.md
echo "RELEASE_EOF"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
body: ${{ steps.notes.outputs.content }}
prerelease: ${{ contains(github.ref_name, '-') }}
86 changes: 86 additions & 0 deletions .github/workflows/tag-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Create a release tag only after CI succeeds on main.
#
# The VERSION file is the release intent. If its matching tag does not exist,
# this workflow creates an annotated tag at the exact CI-tested commit and
# dispatches the release workflow at that tag.
name: Tag Release

on:
workflow_run:
workflows:
- CI
types:
- completed

jobs:
tag:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
permissions:
contents: write
actions: write

steps:
- name: Checkout tested commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0

- name: Validate release metadata
id: version
shell: bash
run: |
set -euo pipefail

VERSION="$(tr -d '[:space:]' < VERSION)"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "VERSION is not valid semantic versioning: $VERSION" >&2
exit 1
fi

if ! grep -q "^## \\[$VERSION\\]" CHANGELOG.md; then
echo "CHANGELOG.md has no section for $VERSION" >&2
exit 1
fi

echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Check whether tag exists
id: tag
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if git rev-parse "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Create and push tag
if: steps.tag.outputs.exists == 'false'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push origin "v${VERSION}"

- name: Trigger release workflow
if: steps.tag.outputs.exists == 'false'
uses: actions/github-script@v8
env:
VERSION: ${{ steps.version.outputs.version }}
with:
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release.yaml',
ref: `v${process.env.VERSION}`
})
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
rootfs.ext4
Loading