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
Original file line number Diff line number Diff line change
@@ -1,83 +1,63 @@
# opencoder - CI/CD Pipeline
# Builds and tests the container on every push and pull request

name: CI
name: Build & Publish Image

on:
push:
branches: [main, master]
paths:
- 'build/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/ci.yml'
- '.pre-commit-config.yaml'
schedule:
- cron: "0 3 * * 1" # Every Monday at 03:00 UTC
workflow_dispatch:
pull_request:
branches: [main, master]
paths:
- 'build/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/ci.yml'
- '.pre-commit-config.yaml'
workflow_dispatch:
- "build/**"
- "scripts/**"
- ".github/workflows/build-and-publish-image.yaml"
push:
branches: [main]
paths:
- "build/**"
- "scripts/**"
- ".github/workflows/build-and-publish-image.yaml"

permissions:
contents: read
packages: write

env:
IMAGE_NAME: opencoder
IMAGE_TAG: ${{ github.sha }}

jobs:
# Lint and validate before building
validate:
name: Validate Configuration
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
env:
CONTAINER_CMD: docker

- name: Install validation tools
steps:
- name: Free disk space on runner
run: |
sudo apt-get update
sudo apt-get install -y jq
pip install pre-commit
# Install hadolint for Containerfile linting
HADOLINT_VERSION="2.14.0"
curl -fsSL "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-linux-x86_64" \
-o /usr/local/bin/hadolint
chmod +x /usr/local/bin/hadolint
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true

- name: Run validation suite
run: ./scripts/validate.sh
sudo docker system prune -af || true
sudo docker image prune -af || true
sudo docker volume prune -f || true

- name: Run pre-commit hooks
run: pre-commit run --all-files
sudo rm -rf /tmp/* /var/tmp/* || true

# Build and test container
build-and-test:
name: Build & Test Container
runs-on: ubuntu-latest
needs: validate
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Set container runtime
run: |
echo "Using Docker (pre-installed on GitHub runners)"
echo "CONTAINER_CMD=docker" >> $GITHUB_ENV

- name: Resolve OpenCode version
run: |
OPENCODE_VERSION=$(cat build/.opencode-version | tr -d '[:space:]')
echo "OPENCODE_VERSION=${OPENCODE_VERSION}" >> $GITHUB_ENV
echo "OpenCode version: ${OPENCODE_VERSION}"

- name: Run validation suite
run: ./scripts/validate.sh

- name: Build container image
run: |
./scripts/build.sh \
Expand All @@ -104,33 +84,21 @@ jobs:
exit-code: '1'
format: table

# Publish only on main pushes, scheduled rebuilds, and manual dispatches.
# Pull request runs build + test + scan but never publish.
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: tankdonut/github-actions/actions/ghcr-login@v1
with:
runtime: docker

- name: Push to GitHub Container Registry
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name != 'pull_request'
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | ${{ env.CONTAINER_CMD }} login ghcr.io -u ${{ github.actor }} --password-stdin
${{ env.CONTAINER_CMD }} tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} ghcr.io/${{ github.repository_owner }}/opencoder:${{ env.IMAGE_TAG }}
${{ env.CONTAINER_CMD }} tag ${{ env.IMAGE_NAME }}:${{ env.OPENCODE_VERSION }} ghcr.io/${{ github.repository_owner }}/opencoder:${{ env.OPENCODE_VERSION }}
${{ env.CONTAINER_CMD }} tag ${{ env.IMAGE_NAME }}:latest ghcr.io/${{ github.repository_owner }}/opencoder:latest
${{ env.CONTAINER_CMD }} push ghcr.io/${{ github.repository_owner }}/opencoder:${{ env.IMAGE_TAG }}
${{ env.CONTAINER_CMD }} push ghcr.io/${{ github.repository_owner }}/opencoder:${{ env.OPENCODE_VERSION }}
${{ env.CONTAINER_CMD }} push ghcr.io/${{ github.repository_owner }}/opencoder:latest
echo "✓ Image pushed to GitHub Container Registry with tags: ${{ env.IMAGE_TAG }}, ${{ env.OPENCODE_VERSION }}, latest"

# Summary job
ci-status:
name: CI Status
runs-on: ubuntu-latest
needs: [validate, build-and-test]
if: always()
steps:
- name: Check overall status
run: |
if [[ "${{ needs.validate.result }}" == "success" && "${{ needs.build-and-test.result }}" == "success" ]]; then
echo "✅ All CI checks passed!"
exit 0
else
echo "❌ CI checks failed"
echo "Validate: ${{ needs.validate.result }}"
echo "Build & Test: ${{ needs.build-and-test.result }}"
exit 1
fi
21 changes: 21 additions & 0 deletions .github/workflows/lint-and-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Lint & Test

on:
pull_request:
push:
branches: [main]

jobs:
pre-commit:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v7
with:
# full history needed by pre-commit hooks (e.g. check-added-large-files)
fetch-depth: 0

- name: Run pre-commit
uses: tankdonut/github-actions/actions/pre-commit@v1
19 changes: 19 additions & 0 deletions .github/workflows/prune-ghcr-images.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Prune GHCR Images

on:
workflow_dispatch:
schedule:
- cron: "30 1 * * *"

permissions:
packages: write

jobs:
prune:
uses: tankdonut/github-actions/.github/workflows/prune-ghcr.yaml@v1
with:
packages: '["opencoder"]'
exclude-latest: true
exclude-semver: true
keep-n-tagged: "10"
secrets: inherit
27 changes: 27 additions & 0 deletions .github/workflows/renovate-auto-approve.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Renovate Auto-Approve

on:
pull_request:
types: [opened, ready_for_review, synchronize]

# Prevent concurrent approvals on the same PR.
concurrency:
group: renovate-auto-approve-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
pull-requests: write

jobs:
approve:
# Only approve non-draft PRs opened by Renovate. Drafts (e.g. major bumps)
# remain pending human review until marked ready.
if: |
github.event.pull_request.draft == false &&
(github.actor == 'renovate[bot]' || github.actor == 'renovate-config[bot]')
runs-on: ubuntu-latest
steps:
- name: Approve Renovate pull request
uses: tankdonut/github-actions/actions/renovate-auto-approve@v1
with:
github-token: ${{ secrets.AUTO_APPROVE_PAT }}
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hadolint 2.14.0
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ opencoder/
│ ├── CODEOWNERS # Review auto-assignment
│ ├── dependabot.yml # Dependabot (docker + github-actions ecosystems)
│ └── workflows/
│ └── ci.yml # 3-job pipeline: validate → build-and-test → ci-status
│ ├── lint-and-test.yaml # Pre-commit via tankdonut/github-actions@v1
│ ├── build-and-publish-image.yaml # Build, scan, publish to GHCR (cron + push + PR + dispatch)
│ ├── prune-ghcr-images.yaml # Daily GHCR tag pruning (reusable workflow)
│ └── renovate-auto-approve.yaml # Auto-approve non-draft Renovate PRs
├── .pre-commit-config.yaml # Pre-commit hooks (hadolint, shellcheck, hygiene)
├── .node-version # Node 24 (CI setup-node reads this)
├── .tool-versions # asdf tools (hadolint) — installed by CI's pre-commit action
├── .node-version # Node 24 (kept for tooling; not required by CI)
├── renovate.json # Renovate bot (config:recommended, 7-day min release age)
├── build/ # Container build context ( sole context for `podman build` )
│ ├── .containerignore # Container build exclusions
Expand Down Expand Up @@ -144,10 +148,10 @@ jq . build/skills-lock.json
### CI/CD

```bash
# Run pre-build validation (same as CI validate job)
# Run pre-build validation (same as CI build-and-publish-image workflow)
./scripts/validate.sh

# Run container test suite (same as CI test job)
# Run container test suite (same as CI build-and-publish-image workflow)
./scripts/container-test.sh opencoder:latest

# Run container test suite with Docker
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ podman run -it --rm ghcr.io/tankdonut/opencoder:latest

### CI Configuration

- Workflow: `.github/workflows/ci.yml`
- Workflows: `.github/workflows/` (4 files — `lint-and-test.yaml`, `build-and-publish-image.yaml`, `prune-ghcr-images.yaml`, `renovate-auto-approve.yaml`); consume centralized actions from `tankdonut/github-actions@v1`
- Test script: `scripts/container-test.sh`
- Validation: `scripts/validate.sh`

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# opencoder

[![CI](https://github.com/tankdonut/opencoder/actions/workflows/ci.yml/badge.svg)](https://github.com/tankdonut/opencoder/actions/workflows/ci.yml)
[![Build & Publish Image](https://github.com/tankdonut/opencoder/actions/workflows/build-and-publish-image.yaml/badge.svg)](https://github.com/tankdonut/opencoder/actions/workflows/build-and-publish-image.yaml)
[![Container](https://img.shields.io/badge/container-ghcr.io-blue)](https://github.com/tankdonut/opencoder/pkgs/container/opencoder)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

Expand Down
21 changes: 20 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,24 @@
"extends": [
"config:recommended"
],
"minimumReleaseAge": "7 days"
"minimumReleaseAge": "7 days",
"platformAutomerge": true,
"automergeStrategy": "squash",
"packageRules": [
{
"description": "Automerge non-major updates after passing CI and approval.",
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"description": "Automerge development dependencies.",
"matchDepTypes": ["devDependencies"],
"automerge": true
},
{
"description": "Major updates require manual review.",
"matchUpdateTypes": ["major"],
"automerge": false
}
]
}
27 changes: 17 additions & 10 deletions scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

## Purpose

Host-side bash scripts that **build, validate, test, and bump** the OpenCode container image. Invoked by CI (`.github/workflows/ci.yml`) and by developers locally. No script sources another — coupling is data-level (version/checksum files) and CI-level (job sequencing).
Host-side bash scripts that **build, validate, test, and bump** the OpenCode container image. Invoked by CI (`.github/workflows/build-and-publish-image.yaml`) and by developers locally. No script sources another — coupling is data-level (version/checksum files) and CI-level (job sequencing).

## File Inventory

| Script | LOC | CI-run? | Role |
|--------|-----|---------|------|
| `validate.sh` | 395 | ✅ validate job | Pre-build validation (7 checks + `--fix` mode) |
| `build.sh` | 204 | ✅ build-and-test job | Container build driver (podman/docker auto-detect) |
| `container-test.sh` | 594 | ✅ build-and-test job | Post-build integration suite (15 test groups) |
| `validate.sh` | 395 | ✅ build-and-publish-image | Pre-build validation (7 checks + `--fix` mode) |
| `build.sh` | 204 | ✅ build-and-publish-image | Container build driver (podman/docker auto-detect) |
| `container-test.sh` | 594 | ✅ build-and-publish-image | Post-build integration suite (15 test groups) |
| `bump-version.sh` | 284 | ❌ manual | Atomic OpenCode version + checksum updater |
| `local-setup.sh` | 303 | ❌ manual | Host bootstrap (non-container path) |
| `opencode-sandbox.sh` | 392 | ❌ manual | Linux sandbox wrapper (bwrap / gVisor / nspawn modes) |
Expand Down Expand Up @@ -104,14 +104,21 @@ verify_installation / print_summary / main

## CI Integration

CI consumes centralized actions from [`tankdonut/github-actions@v1`](https://github.com/tankdonut/github-actions):

```
ci.yml
├─ validate job: validate.sh + pre-commit run --all-files + hadolint
└─ build-and-test: build.sh → tag (sha, version, latest) → container-test.sh → push ghcr.io (main only)
.github/workflows/
├─ lint-and-test.yaml: pre-commit@v1 (all-files)
├─ build-and-publish-image.yaml: validate.sh → build.sh → tag (sha, version, latest)
│ → container-test.sh → trivy (advisory)
│ → ghcr-login@v1 → push ghcr.io (non-PR only)
├─ prune-ghcr-images.yaml: reusable prune-ghcr.yaml@v1 (daily cron)
└─ renovate-auto-approve.yaml: renovate-auto-approve@v1 (on Renovate PRs)
```
- Triggers: push to `main`, PR to `main`, `workflow_dispatch`; path-filtered on `build/**`, `scripts/**`, `ci.yml`, `.pre-commit-config.yaml` (note: a `tests/**` filter exists but tests/ has no CI-wired tests → filter matches nothing)
- `ci-status` job (`if: always()`) fails pipeline if upstream failed
- Vulnerability scan: `aquasecurity/trivy-action@0.28.0` (ci.yml), image-ref scan, severity CRITICAL/HIGH, `continue-on-error: true` (non-blocking advisory). Docker-compatible — runs in CI and locally.
- Build triggers: `schedule (Mon 03:00 UTC)`, `workflow_dispatch`, PR + push to `main` (path-filtered on `build/**`, `scripts/**`, the workflow file itself); PR runs build+test+scan but never publish
- Pre-commit runs on every PR + push to `main` (no path filter, `fetch-depth: 0`)
- No `ci-status` aggregator job — set branch-protection required checks on `Lint & Test / pre-commit` + `Build & Publish Image / release`
- Vulnerability scan: `aquasecurity/trivy-action@v0.36.0`, image-ref scan, severity CRITICAL/HIGH, `continue-on-error: true` (non-blocking advisory). Docker-compatible — runs in CI and locally.

## Anti-Patterns (script-specific)

Expand Down
2 changes: 1 addition & 1 deletion tests/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TDD-style **unit tests** for `build/entrypoint.sh` bootstrap helper functions. U

## ⚠️ CRITICAL: Not Wired Into CI

`tests/test_bootstrap.sh` is **NOT invoked by `.github/workflows/ci.yml`**. CI runs only `scripts/container-test.sh` (the integration suite). This file is **dev-only** — run it manually:
`tests/test_bootstrap.sh` is **NOT invoked by `.github/workflows/build-and-publish-image.yaml`**. CI runs only `scripts/container-test.sh` (the integration suite). This file is **dev-only** — run it manually:

```bash
bash tests/test_bootstrap.sh
Expand Down
Loading