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
32 changes: 32 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Lint

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]

permissions:
contents: read

jobs:
lint:
name: Python and shell lint
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.14"

- name: Install pinned Ruff
run: python -m pip install "ruff==$(make --no-print-directory -s lint-version)"

- name: Run repository lint target
run: make lint
153 changes: 153 additions & 0 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Publish OpenCode image

on:
push:
branches: [main]

permissions:
contents: read

jobs:
publish:
name: Build, verify, and promote OpenCode image
runs-on: ubuntu-latest
concurrency:
group: opencode-image-release-${{ github.repository }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: read
id-token: write
steps:
- name: Check out main with full history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0
persist-credentials: false

- name: Install Podman
run: |
if ! command -v podman >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y podman
fi
podman --version

- name: Build and push unprocessed squash-merge candidates
id: publish
env:
GH_TOKEN: ${{ github.token }}
QUAY_REPOSITORY_PATH: ${{ vars.QUAY_REPOSITORY_PATH }}
QUAY_ROBOT_USERNAME: ${{ vars.QUAY_ROBOT_USERNAME }}
QUAY_PUSH_TOKEN: ${{ secrets.QUAY_PUSH_TOKEN }}
run: python3 scripts/publish_image.py --stage publish

- name: Install KinD
if: ${{ success() && steps.publish.outputs.published == 'true' }}
run: |
curl -fsSLo kind-linux-amd64 https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-amd64
# Expected digest is the GitHub Release API digest for the v0.32.0
# linux/amd64 asset, pinned independently of kind.sigs.k8s.io.
echo "50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54 kind-linux-amd64" | sha256sum --check -
chmod +x kind-linux-amd64
sudo mv kind-linux-amd64 /usr/local/bin/kind
kind version
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Run SCA audit for every candidate digest
if: ${{ success() && steps.publish.outputs.published == 'true' }}
env:
IMAGE_REFS: ${{ steps.publish.outputs.image_refs }}
run: |
set -euo pipefail
[[ -n "$IMAGE_REFS" ]]
candidate=0
while IFS= read -r image_ref; do
[ -n "$image_ref" ] || continue
candidate=$((candidate + 1))
echo "::group::SCA audit candidate $candidate — $image_ref"
podman run --rm --pull=always --entrypoint /bin/sh "$image_ref" -ec 'pip-audit --local'
echo "::endgroup::"
done <<< "$IMAGE_REFS"

- name: Run standalone container E2E for every candidate
if: ${{ success() && steps.publish.outputs.published == 'true' }}
env:
IMAGE_REFS: ${{ steps.publish.outputs.image_refs }}
run: |
[[ -n "$IMAGE_REFS" ]]
candidate=0
while IFS= read -r image_ref; do
[ -n "$image_ref" ] || continue
candidate=$((candidate + 1))
echo "::group::Standalone E2E candidate $candidate — $image_ref"
if python3 scripts/e2e_container.py --image-ref "$image_ref"; then
echo "::endgroup::"
else
result=$?
echo "::endgroup::"
exit "$result"
fi
done <<< "$IMAGE_REFS"

- name: Run KinD Pod E2E for every candidate
if: ${{ success() && steps.publish.outputs.published == 'true' }}
env:
IMAGE_REFS: ${{ steps.publish.outputs.image_refs }}
run: |
[[ -n "$IMAGE_REFS" ]]
candidate=0
while IFS= read -r image_ref; do
[ -n "$image_ref" ] || continue
candidate=$((candidate + 1))
echo "::group::KinD Pod E2E candidate $candidate — $image_ref"
if python3 scripts/e2e_kind_pod.py --image-ref "$image_ref" --cluster-name "agent-containers-$candidate"; then
echo "::endgroup::"
else
result=$?
echo "::endgroup::"
exit "$result"
fi
done <<< "$IMAGE_REFS"

- name: Install pinned cosign
if: ${{ success() && steps.publish.outputs.published == 'true' }}
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v3.1.3

- name: Sign each tested candidate digest with GitHub OIDC
if: ${{ success() && steps.publish.outputs.published == 'true' }}
env:
IMAGE_REFS: ${{ steps.publish.outputs.image_refs }}
QUAY_REPOSITORY_PATH: ${{ vars.QUAY_REPOSITORY_PATH }}
QUAY_ROBOT_USERNAME: ${{ vars.QUAY_ROBOT_USERNAME }}
QUAY_PUSH_TOKEN: ${{ secrets.QUAY_PUSH_TOKEN }}
run: |
set -euo pipefail
authfile="${RUNNER_TEMP}/cosign-quay-auth.json"
docker_config="${RUNNER_TEMP}/cosign-docker-config"
mkdir -p "$docker_config"
printf '%s' "$QUAY_PUSH_TOKEN" | podman login \
--authfile "$authfile" \
--username "$QUAY_ROBOT_USERNAME" \
--password-stdin quay.io
cp "$authfile" "$docker_config/config.json"
chmod 600 "$docker_config/config.json"
export DOCKER_CONFIG="$docker_config"
[[ -n "$IMAGE_REFS" ]]
while IFS= read -r image_ref; do
[ -n "$image_ref" ] || continue
echo "::group::Sign tested candidate — $image_ref"
cosign sign --yes "$image_ref"
echo "::endgroup::"
done <<< "$IMAGE_REFS"

- name: Promote tested candidate and publish release metadata
if: ${{ success() && steps.publish.outputs.published == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
QUAY_REPOSITORY_PATH: ${{ vars.QUAY_REPOSITORY_PATH }}
QUAY_ROBOT_USERNAME: ${{ vars.QUAY_ROBOT_USERNAME }}
QUAY_PUSH_TOKEN: ${{ secrets.QUAY_PUSH_TOKEN }}
run: python3 scripts/publish_image.py --stage promote-and-push
Comment thread
coderabbitai[bot] marked this conversation as resolved.
46 changes: 46 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Security analysis

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

jobs:
sast:
name: Semgrep SAST
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"

- name: Install pinned Semgrep
run: python -m pip install semgrep==1.179.0

- name: Scan Python source
run: semgrep scan --config p/python --error scripts tests

dependency-review:
name: Dependency review SCA
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Review dependency changes
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
69 changes: 69 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: PR tests

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]

permissions:
contents: read

jobs:
tests:
name: Unit and contract tests
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.14"

- name: Install test dependency
run: python -m pip install pytest

- name: Run Python tests
run: python -m pytest -q
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Check pinned toolchain wiring
run: bash tests/test_lsp_version_pinning.sh

- name: Check standalone provider secrets
run: bash tests/test_standalone_provider_secrets.sh

trusted-image-build:
name: Build image for trusted PRs
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Check out PR source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Install Podman
run: |
if ! command -v podman >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y podman
fi
podman --version

- name: Build image locally without pushing
run: >-
make build-opencode REGISTRY=localhost
IMAGE_TAG=pr-${{ github.event.number }} NOPROMPT=1 SAVE_DEFAULTS=0

- name: Audit built image dependencies
run: >-
podman run --rm --entrypoint /bin/sh
localhost/opencode:pr-${{ github.event.number }}
-ec 'pip-audit --local'
Empty file added IMAGE_DIGEST
Empty file.
1 change: 1 addition & 0 deletions IMAGE_PUBLISH_STATE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3b2b440b80f891d2e0ef5d552f53f35e45b08554
35 changes: 30 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@
# ../agent-swarm/.push-defaults: REGISTRY + IMAGE_TAG (source of truth) — checked in.
-include .push-defaults
AC_DEFAULTS := $(firstword $(wildcard ../agent-swarm/.push-defaults) .push-defaults)
ifneq ($(AC_DEFAULTS),.push-defaults)
-include $(AC_DEFAULTS)
endif

IMAGES := opencode
REGISTRY ?=
IMAGE_TAG ?= latest
SAVE_DEFAULTS ?= 1
PUSH_LATEST ?= 1
PUSH_DIGEST_FILE ?=

# Toolchain versions — update all with: make update-deps
GO_VERSION ?= 1.27.1
Expand All @@ -18,11 +25,12 @@ FZF_VERSION ?= 0.74.4
RG_VERSION ?= 15.2.0
YQ_VERSION ?= 4.54.1
JIRA_MCP_VERSION ?= 0.2.1
AGENT_SWARM_MCP_VERSION ?= 0.1.0
AGENT_SWARM_MCP_REVISION ?= 292c2128164a237aa59e04b11f03630a61113639
GOPLS_VERSION ?= 0.23.0
PYRIGHT_VERSION ?= 1.1.414
PIP_AUDIT_VERSION ?= 2.10.1
GOVULNCHECK_VERSION ?= 1.8.0
RUFF_VERSION ?= 0.15.22

# Per-image build targets
TARGET_opencode := opencode
Expand Down Expand Up @@ -56,15 +64,19 @@ build-$(1):
RG_VERSION=$(RG_VERSION) \
YQ_VERSION=$(YQ_VERSION) \
JIRA_MCP_VERSION=$(JIRA_MCP_VERSION) \
AGENT_SWARM_MCP_VERSION=$(AGENT_SWARM_MCP_VERSION) \
AGENT_SWARM_MCP_REVISION=$(AGENT_SWARM_MCP_REVISION) \
GOPLS_VERSION=$(GOPLS_VERSION) \
PYRIGHT_VERSION=$(PYRIGHT_VERSION) \
PIP_AUDIT_VERSION=$(PIP_AUDIT_VERSION) \
GOVULNCHECK_VERSION=$(GOVULNCHECK_VERSION) \
REGISTRY="$(REGISTRY)" \
IMAGE_TAG="$(IMAGE_TAG)" \
SAVE_DEFAULTS="$(SAVE_DEFAULTS)" \
NOPROMPT=$(NOPROMPT) \
bash scripts/build.sh $(1) $(CONTAINERFILE)
push-$(1):
@bash scripts/push.sh $(1)
@REGISTRY="$(REGISTRY)" IMAGE_TAG="$(IMAGE_TAG)" PUSH_LATEST="$(PUSH_LATEST)" \
PUSH_DIGEST_FILE="$(PUSH_DIGEST_FILE)" bash scripts/push.sh $(1)
publish-$(1):
@$(MAKE) build-$(1) NOPROMPT=1
@$(MAKE) push-$(1)
Expand Down Expand Up @@ -102,6 +114,15 @@ build: ## Build all images (first image prompts for registry/tag, rest reuse sa
push: $(addprefix push-,$(IMAGES)) ## Push all images
publish: $(addprefix publish-,$(IMAGES)) ## Build + push all images (no prompts)

.PHONY: lint lint-version
lint: ## Lint Python and validate shell syntax
@command -v ruff >/dev/null 2>&1 || { echo "ruff $(RUFF_VERSION) is required; install it with: python3 -m pip install ruff==$(RUFF_VERSION)" >&2; exit 1; }
@test "$$(ruff --version)" = "ruff $(RUFF_VERSION)" || { echo "Expected ruff $(RUFF_VERSION), got $$(ruff --version)" >&2; exit 1; }
ruff check scripts tests
@set -eu; for script in scripts/*.sh tests/*.sh; do bash -n "$$script"; done
lint-version:
@printf '%s\n' "$(RUFF_VERSION)"

# --------------------------------------------------------------------------
# Secrets
# --------------------------------------------------------------------------
Expand All @@ -127,6 +148,8 @@ update-deps: ## Fetch latest versions of all dependencies and update Makefile
$(eval LATEST_PYRIGHT := $(shell curl -fsSL 'https://pypi.org/pypi/pyright/json' | jq -r '.info.version // empty'))
$(eval LATEST_PIP_AUDIT := $(shell curl -fsSL 'https://pypi.org/pypi/pip-audit/json' | jq -r '[.releases | keys[] | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$$"))] | sort_by(split(".") | map(tonumber)) | last // empty'))
$(eval LATEST_GOVULNCHECK := $(shell curl -fsSL 'https://proxy.golang.org/golang.org/x/vuln/@latest' | jq -r '.Version // empty' | sed 's/^v//'))
$(eval LATEST_AGENT_SWARM_MCP_REVISION := $(shell curl -fsSL 'https://api.github.com/repos/stolostron/agent-swarm/commits?path=mcp-server&per_page=1' | jq -r '.[0].sha // empty'))
$(eval LATEST_RUFF := $(shell curl -fsSL 'https://pypi.org/pypi/ruff/json' | jq -r '.info.version // empty'))
$(if $(strip $(LATEST_GO)),,$(error Failed to fetch latest Go version - aborting without modifying Makefile))
$(if $(strip $(LATEST_BUILD)),,$(error Failed to fetch latest Python build tag - aborting without modifying Makefile))
$(if $(strip $(LATEST_PY)),,$(error Failed to fetch latest Python version - aborting without modifying Makefile))
Expand All @@ -140,10 +163,12 @@ update-deps: ## Fetch latest versions of all dependencies and update Makefile
$(if $(strip $(LATEST_PYRIGHT)),,$(error Failed to fetch latest pyright version - aborting without modifying Makefile))
$(if $(strip $(LATEST_PIP_AUDIT)),,$(error Failed to fetch latest pip-audit version - aborting without modifying Makefile))
$(if $(strip $(LATEST_GOVULNCHECK)),,$(error Failed to fetch latest govulncheck version - aborting without modifying Makefile))
@echo "Go: $(LATEST_GO) Python: $(LATEST_PY) (build: $(LATEST_BUILD)) opencode: $(LATEST_OC) gh: $(LATEST_GH) fzf: $(LATEST_FZF) rg: $(LATEST_RG) yq: $(LATEST_YQ) jira-mcp: $(LATEST_JIRA_MCP) gopls: $(LATEST_GOPLS) pyright: $(LATEST_PYRIGHT) pip-audit: $(LATEST_PIP_AUDIT) govulncheck: $(LATEST_GOVULNCHECK)"
$(if $(strip $(LATEST_AGENT_SWARM_MCP_REVISION)),,$(error Failed to fetch latest Agent Swarm MCP source revision - aborting without modifying Makefile))
$(if $(strip $(LATEST_RUFF)),,$(error Failed to fetch latest Ruff version - aborting without modifying Makefile))
@echo "Go: $(LATEST_GO) Python: $(LATEST_PY) (build: $(LATEST_BUILD)) opencode: $(LATEST_OC) gh: $(LATEST_GH) fzf: $(LATEST_FZF) rg: $(LATEST_RG) yq: $(LATEST_YQ) jira-mcp: $(LATEST_JIRA_MCP) agent-swarm-mcp: $(LATEST_AGENT_SWARM_MCP_REVISION) gopls: $(LATEST_GOPLS) pyright: $(LATEST_PYRIGHT) pip-audit: $(LATEST_PIP_AUDIT) govulncheck: $(LATEST_GOVULNCHECK) ruff: $(LATEST_RUFF)"
@set -eu; tmpfile=$$(mktemp Makefile.XXXXXX); trap 'rm -f "$$tmpfile"' EXIT; \
cp Makefile "$$tmpfile"; \
sed -i 's/^GO_VERSION\s*?= .*/GO_VERSION ?= $(LATEST_GO)/; s/^PYTHON_VERSION\s*?= .*/PYTHON_VERSION ?= $(LATEST_PY)/; s/^PYTHON_BUILD\s*?= .*/PYTHON_BUILD ?= $(LATEST_BUILD)/; s/^OPENCODE_VERSION\s*?= .*/OPENCODE_VERSION ?= $(LATEST_OC)/; s/^GH_VERSION\s*?= .*/GH_VERSION ?= $(LATEST_GH)/; s/^FZF_VERSION\s*?= .*/FZF_VERSION ?= $(LATEST_FZF)/; s/^RG_VERSION\s*?= .*/RG_VERSION ?= $(LATEST_RG)/; s/^YQ_VERSION\s*?= .*/YQ_VERSION ?= $(LATEST_YQ)/; s/^JIRA_MCP_VERSION\s*?= .*/JIRA_MCP_VERSION ?= $(LATEST_JIRA_MCP)/; s/^GOPLS_VERSION\s*?= .*/GOPLS_VERSION ?= $(LATEST_GOPLS)/; s/^PYRIGHT_VERSION\s*?= .*/PYRIGHT_VERSION ?= $(LATEST_PYRIGHT)/; s/^PIP_AUDIT_VERSION\s*?= .*/PIP_AUDIT_VERSION ?= $(LATEST_PIP_AUDIT)/; s/^GOVULNCHECK_VERSION\s*?= .*/GOVULNCHECK_VERSION ?= $(LATEST_GOVULNCHECK)/' "$$tmpfile"; \
sed -i 's/^GO_VERSION\s*?= .*/GO_VERSION ?= $(LATEST_GO)/; s/^PYTHON_VERSION\s*?= .*/PYTHON_VERSION ?= $(LATEST_PY)/; s/^PYTHON_BUILD\s*?= .*/PYTHON_BUILD ?= $(LATEST_BUILD)/; s/^OPENCODE_VERSION\s*?= .*/OPENCODE_VERSION ?= $(LATEST_OC)/; s/^GH_VERSION\s*?= .*/GH_VERSION ?= $(LATEST_GH)/; s/^FZF_VERSION\s*?= .*/FZF_VERSION ?= $(LATEST_FZF)/; s/^RG_VERSION\s*?= .*/RG_VERSION ?= $(LATEST_RG)/; s/^YQ_VERSION\s*?= .*/YQ_VERSION ?= $(LATEST_YQ)/; s/^JIRA_MCP_VERSION\s*?= .*/JIRA_MCP_VERSION ?= $(LATEST_JIRA_MCP)/; s/^AGENT_SWARM_MCP_REVISION\s*?= .*/AGENT_SWARM_MCP_REVISION ?= $(LATEST_AGENT_SWARM_MCP_REVISION)/; s/^GOPLS_VERSION\s*?= .*/GOPLS_VERSION ?= $(LATEST_GOPLS)/; s/^PYRIGHT_VERSION\s*?= .*/PYRIGHT_VERSION ?= $(LATEST_PYRIGHT)/; s/^PIP_AUDIT_VERSION\s*?= .*/PIP_AUDIT_VERSION ?= $(LATEST_PIP_AUDIT)/; s/^GOVULNCHECK_VERSION\s*?= .*/GOVULNCHECK_VERSION ?= $(LATEST_GOVULNCHECK)/; s/^RUFF_VERSION\s*?= .*/RUFF_VERSION ?= $(LATEST_RUFF)/' "$$tmpfile"; \
grep -q '^PIP_AUDIT_VERSION\s*?=' "$$tmpfile" && grep -q '^GOVULNCHECK_VERSION\s*?=' "$$tmpfile"; \
mv "$$tmpfile" Makefile; trap - EXIT

Expand Down
Loading
Loading