diff --git a/.github/workflows/otto-commitlint.yml b/.github/workflows/otto-commitlint.yml new file mode 100644 index 0000000..5e5e486 --- /dev/null +++ b/.github/workflows/otto-commitlint.yml @@ -0,0 +1,16 @@ +name: Otto Conventional Commits + +on: + pull_request_target: + paths: + - "otto/**" + types: [opened, edited, synchronize, reopened] + +jobs: + semantic-pull-request: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/otto-goreleaser.yml b/.github/workflows/otto-goreleaser.yml new file mode 100644 index 0000000..4806e21 --- /dev/null +++ b/.github/workflows/otto-goreleaser.yml @@ -0,0 +1,119 @@ +name: Otto GoReleaser + +on: + workflow_run: + workflows: ["Release Management"] + types: + - completed + branches: + - main + +permissions: + contents: write + packages: write + +jobs: + release-otto: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Check if we need to run the release for Otto + - name: Download workflow artifact + uses: actions/github-script@v7 + id: download-artifact + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }} + }); + + const matchArtifact = artifacts.data.artifacts.find(artifact => { + return artifact.name === "release-please-outputs" + }); + + if (!matchArtifact) { + console.log('No release-please-outputs artifact found'); + return false; + } + + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip' + }); + + const fs = require('fs'); + fs.writeFileSync('release-please-outputs.zip', Buffer.from(download.data)); + + console.log('Downloaded artifact'); + return true; + + - name: Unzip artifact + if: steps.download-artifact.outputs.result == 'true' + run: unzip release-please-outputs.zip + + - name: Read release information + id: release-info + if: steps.download-artifact.outputs.result == 'true' + run: | + if [ -f "otto--release_created" ]; then + RELEASE_CREATED=$(cat otto--release_created) + echo "otto_release_created=${RELEASE_CREATED}" >> $GITHUB_OUTPUT + if [ "${RELEASE_CREATED}" == "true" ] && [ -f "otto--tag_name" ]; then + TAG_NAME=$(cat otto--tag_name) + echo "otto_tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT + fi + fi + + # Only run the rest of the job if Otto was released + - name: Set up Go + if: steps.release-info.outputs.otto_release_created == 'true' + uses: actions/setup-go@v5 + with: + go-version-file: ./otto/go.mod + cache: true + + - name: Set up QEMU + if: steps.release-info.outputs.otto_release_created == 'true' + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + if: steps.release-info.outputs.otto_release_created == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: steps.release-info.outputs.otto_release_created == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Install cosign for artifact signing + - name: Install cosign + if: steps.release-info.outputs.otto_release_created == 'true' + uses: sigstore/cosign-installer@v3.4.0 + with: + cosign-release: 'v2.2.2' + + - name: Run GoReleaser + if: steps.release-info.outputs.otto_release_created == 'true' + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --clean + workdir: ./otto + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # These secrets need to be set in the repository settings + COSIGN_KEY: ${{ secrets.COSIGN_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml new file mode 100644 index 0000000..4a772b1 --- /dev/null +++ b/.github/workflows/release-management.yml @@ -0,0 +1,52 @@ +name: Release Management + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - name: Run Release Please + uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + release-type: go + monorepo-tags: true + + # Save output variables as artifacts to be used by the GoReleaser workflow + - name: Save release outputs + if: ${{ steps.release.outputs.releases_created }} + run: | + # Save all the release-please outputs to files + echo "${{ steps.release.outputs.releases_created }}" > releases_created + echo "${{ steps.release.outputs.paths_released }}" > paths_released + + # Save Otto-specific outputs + echo "${{ steps.release.outputs['otto--release_created'] }}" > otto--release_created + echo "${{ steps.release.outputs['otto--tag_name'] }}" > otto--tag_name + + - name: Upload release outputs + if: ${{ steps.release.outputs.releases_created }} + uses: actions/upload-artifact@v4 + with: + name: release-please-outputs + path: | + releases_created + paths_released + otto--* + + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + paths_released: ${{ steps.release.outputs.paths_released }} + otto_release_created: ${{ steps.release.outputs['otto--release_created'] }} + otto_tag_name: ${{ steps.release.outputs['otto--tag_name'] }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 95ee56a..a475b5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,13 @@ on: push: branches: [main] paths: - - "otto/**" + - 'otto/**' + - '.github/workflows/test.yml' pull_request: branches: [main] paths: - - "otto/**" + - 'otto/**' + - '.github/workflows/test.yml' jobs: test: @@ -42,15 +44,47 @@ jobs: working-directory: ./otto steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.24.x + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24.x - - name: Run linter - uses: golangci/golangci-lint-action@v7 - with: - version: latest - working-directory: otto + - name: Run linter + uses: golangci/golangci-lint-action@v7 + with: + version: latest + working-directory: otto + build: + name: Build Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./otto + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24.x + cache: true + cache-dependency-path: otto/go.sum + + - name: Build binary + run: make build + + - name: Check GoReleaser config + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: check + workdir: ./otto + + - name: Test Docker build + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: build --snapshot --clean --single-target + workdir: ./otto diff --git a/.gitignore b/.gitignore index 3a68780..9ee53a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ otto/config.yaml otto/secrets.yaml otto/.env -otto/*.pem +otto/dist +.DS_Store diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 0000000..042b749 --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,17 @@ +{ + "packages": { + "otto": { + "release-type": "go", + "component": "otto", + "changelog-path": "CHANGELOG.md", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "draft": false, + "prerelease": false, + "include-v-in-tag": true, + "pull-request-title-pattern": "chore${scope}: release${component} ${version}", + "tag-separator": "/" + } + }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..58b022b --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "otto": "0.1.0" +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 16a0232..e1d994b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,17 @@ cd otto && make build cd otto && make lint ``` +## Release Process + +This repository uses [release-please-action](https://github.com/googleapis/release-please-action) to automate versioning and release management based on [Conventional Commits](https://www.conventionalcommits.org/). + +- Commit messages must follow the Conventional Commits specification +- Use `fix:` prefixes for bug fixes (patch version bump) +- Use `feat:` prefixes for new features (minor version bump) +- Use `feat!:` or `fix!:` for breaking changes (major version bump) +- Releases are created automatically when release PRs are merged +- Each project in the monorepo has its own versioning lifecycle + ## General Guidelines - Create code and configs following the standards in each project's directory diff --git a/otto/.goreleaser.yaml b/otto/.goreleaser.yaml new file mode 100644 index 0000000..6dbbe76 --- /dev/null +++ b/otto/.goreleaser.yaml @@ -0,0 +1,130 @@ +version: 2 + +# Force project name to be "otto" +project_name: otto + +# Signing configuration for binaries +signs: + - cmd: cosign + args: + - "sign-blob" + - "--key=${env.COSIGN_KEY}" + - "--output-signature=${signature}" + - "${artifact}" + artifacts: checksum + stdin: "{{ .Env.COSIGN_PASSWORD }}" + signature: "${artifact}.sig" + +before: + hooks: + - go mod tidy + +builds: + - id: otto + binary: otto + main: ./cmd/otto + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + # Ignore combinations that don't exist + ignore: + - goos: darwin + goarch: "386" + flags: + - -trimpath + ldflags: + - -s -w -X github.com/open-telemetry/sig-project-infra/otto/internal.Version={{.Version}} + +dockers: + - image_templates: + - "ghcr.io/open-telemetry/sig-project-infra/otto:{{ .Version }}-amd64" + - "ghcr.io/open-telemetry/sig-project-infra/otto:latest-amd64" + dockerfile: Dockerfile.goreleaser + use: docker + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title=Otto" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/open-telemetry/sig-project-infra" + - "--label=org.opencontainers.image.licenses=Apache-2.0" + ids: + - otto + + - image_templates: + - "ghcr.io/open-telemetry/sig-project-infra/otto:{{ .Version }}-arm64" + - "ghcr.io/open-telemetry/sig-project-infra/otto:latest-arm64" + dockerfile: Dockerfile.goreleaser + use: docker + build_flag_templates: + - "--pull" + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title=Otto" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source=https://github.com/open-telemetry/sig-project-infra" + - "--label=org.opencontainers.image.licenses=Apache-2.0" + goarch: arm64 + ids: + - otto + +# Add docker manifests as suggested by adrielp +docker_manifests: + - name_template: ghcr.io/open-telemetry/sig-project-infra/otto:{{ .Version }} + image_templates: + - ghcr.io/open-telemetry/sig-project-infra/otto:{{ .Version }}-amd64 + - ghcr.io/open-telemetry/sig-project-infra/otto:{{ .Version }}-arm64 + - name_template: ghcr.io/open-telemetry/sig-project-infra/otto:latest + image_templates: + - ghcr.io/open-telemetry/sig-project-infra/otto:latest-amd64 + - ghcr.io/open-telemetry/sig-project-infra/otto:latest-arm64 + +# Docker image signing configuration +docker_signs: + - cmd: cosign + args: + - "sign" + - "--key=${env.COSIGN_KEY}" + - "${artifact}" + artifacts: images + stdin: "{{ .Env.COSIGN_PASSWORD }}" + +archives: + - id: default + name_template: >- + otto_ + {{- .Version }}_ + {{- .Os }}_ + {{- .Arch }} + formats: [tar.gz] + format_overrides: + - goos: windows + formats: [zip] + files: + - LICENSE* + - README* + - CHANGELOG* + - config.example.yaml + +checksum: + name_template: "checksums.txt" + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" + - "^ci:" diff --git a/otto/.svu.yml b/otto/.svu.yml new file mode 100644 index 0000000..09abbca --- /dev/null +++ b/otto/.svu.yml @@ -0,0 +1,7 @@ +tag.prefix: "v" # Prefix for version tags +pre.prefix: "" # Prefix for pre-release versions +build.prefix: "+" # Prefix for build metadata +metadata: false # Include metadata in the version +pre: false # Pre-release mode +no-metadata: true # Exclude metadata from the version +no-pre: true # Exclude pre-release info from the version \ No newline at end of file diff --git a/otto/CLAUDE.md b/otto/CLAUDE.md index 56ada46..bd97a88 100644 --- a/otto/CLAUDE.md +++ b/otto/CLAUDE.md @@ -10,6 +10,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Lint: `golangci-lint run` - Docker build: `docker build -t otel-otto:latest .` +## Release Process +- Otto uses automated semantic versioning through release-please-action +- Version is managed in `.release-please-manifest.json` at the repository root +- Binary and Docker builds are automated with GoReleaser +- Docker images are published to GitHub Container Registry (ghcr.io) + ## Code Style Guidelines - License: Include SPDX license header (`// SPDX-License-Identifier: Apache-2.0`) in all Go files - Imports: Use standard Go import grouping (stdlib, then external, then internal) @@ -21,3 +27,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Logging: Use slog package for structured logging with appropriate levels - Dependencies: This is an OpenTelemetry project; follow OTel conventions - File formatting: Always include a newline at the end of every file + +## Commit Message Guidelines +Follow [Conventional Commits](https://www.conventionalcommits.org/) format for all commits: +- `fix:` for bug fixes (triggers patch version bump) +- `feat:` for new features (triggers minor version bump) +- `feat!:` or `fix!:` for breaking changes (triggers major version bump) +- `docs:`, `chore:`, `test:`, etc. for non-release changes diff --git a/otto/Dockerfile.goreleaser b/otto/Dockerfile.goreleaser new file mode 100644 index 0000000..0b59e32 --- /dev/null +++ b/otto/Dockerfile.goreleaser @@ -0,0 +1,24 @@ +FROM scratch + +# Copy SSL certificates for HTTPS requests +COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Set up the non-root user (needed for scratch image) +# Since scratch has no shell, we need to create user in /etc/passwd directly +COPY --from=alpine:latest /etc/passwd /etc/passwd +COPY --from=alpine:latest /etc/group /etc/group + +# Copy the pre-built binary from the build stage +COPY otto /usr/local/bin/ + +# Expose the service port +EXPOSE 8080 + +# Set environment variables +ENV OTTO_ADDR=:8080 + +# Use non-root user +USER nobody + +# Set the entry point +ENTRYPOINT ["/usr/local/bin/otto"] \ No newline at end of file diff --git a/otto/Makefile b/otto/Makefile index 180a9e6..8a2b056 100644 --- a/otto/Makefile +++ b/otto/Makefile @@ -1,8 +1,9 @@ # Makefile for Otto BINARY := otto CMD_DIR := ./cmd/otto +VERSION ?= $(shell git describe --tags --always --dirty --match "v*" 2> /dev/null || echo "dev") -.PHONY: all build clean run test lint +.PHONY: all build clean run test lint format import-check license-check sec-check release snapshot all: build @@ -21,5 +22,31 @@ test: lint: golangci-lint run +format: + gofmt -s -w . + go mod tidy + +import-check: + gci write -s standard -s default -s "prefix(github.com/open-telemetry)" . + +license-check: + go run github.com/google/addlicense -c "The OpenTelemetry Authors" -l apache -check . + +license-add: + go run github.com/google/addlicense -c "The OpenTelemetry Authors" -l apache . + +sec-check: + go run github.com/google/osv-scanner:v1 . + +# GoReleaser commands +release: + goreleaser release --clean + +snapshot: + goreleaser release --snapshot --clean + docker-build: - docker build -t otel-otto:latest . + docker build -t ghcr.io/open-telemetry/sig-project-infra/otto:latest . + +goreleaser-check: + goreleaser check \ No newline at end of file diff --git a/otto/RELEASING.md b/otto/RELEASING.md new file mode 100644 index 0000000..7b3c288 --- /dev/null +++ b/otto/RELEASING.md @@ -0,0 +1,79 @@ +# Otto Release Process + +This document outlines the release process for Otto. + +## PR-Based Release Process + +Otto uses a PR-based release process. New releases are created automatically when a PR with the `release` label is merged into the main branch. + +### Release Labels + +When you want to trigger a release, add one of the following labels to your PR: + +- `release` - Creates a patch release (e.g., v1.0.0 → v1.0.1) +- `release:minor` - Creates a minor release (e.g., v1.0.0 → v1.1.0) +- `release:major` - Creates a major release (e.g., v1.0.0 → v2.0.0) + +### Release Workflow + +When a labeled PR is merged: + +1. A GitHub Action will automatically: + - Determine the next version number based on the latest tag + - Create and push a new tag + - Build binaries and Docker images using GoReleaser + - Push Docker images to GitHub Container Registry (ghcr.io) + - Create a GitHub release with release notes + +### Available Artifacts + +Each release produces: + +- Binary releases for multiple platforms (Linux, macOS, AMD64, ARM64) +- Docker images published to `ghcr.io/open-telemetry/sig-project-infra/otto` +- Multi-architecture Docker manifests for convenient image pulling + +### Docker Images + +Docker images can be pulled without specifying architecture: + +```sh +# Pull by version +docker pull ghcr.io/open-telemetry/sig-project-infra/otto:v1.0.0 + +# Pull latest +docker pull ghcr.io/open-telemetry/sig-project-infra/otto:latest +``` + +### Verifying Signatures + +Otto releases are signed using Cosign. You can verify the signatures using the public key. + +To verify binary checksums: + +```sh +# Download the public key +curl -O https://raw.githubusercontent.com/open-telemetry/sig-project-infra/main/otto/cosign.pub + +# Verify checksums +cosign verify-blob --key cosign.pub --signature checksums.txt.sig checksums.txt +``` + +To verify Docker images: + +```sh +# Verify a specific version +cosign verify --key cosign.pub ghcr.io/open-telemetry/sig-project-infra/otto:v1.0.0 + +# Verify latest +cosign verify --key cosign.pub ghcr.io/open-telemetry/sig-project-infra/otto:latest +``` + +### Future Improvements + +Planned improvements to the release process: + +- Further enhance security with SBOM generation and attestations +- Explore OpenTelemetry Collector's GoReleaser Pro features for improved build times +- Implement automatic changelog generation based on PR content +- Add user-friendly release verification and installation instructions \ No newline at end of file diff --git a/otto/internal/app.go b/otto/internal/app.go index 52df8d2..117672a 100644 --- a/otto/internal/app.go +++ b/otto/internal/app.go @@ -98,7 +98,7 @@ func (a *App) Start(ctx context.Context) error { } }() - a.Logger.Info("otto started", "addr", a.Addr) + a.Logger.Info("otto started", "addr", a.Addr, "version", Version) return nil } diff --git a/otto/internal/version.go b/otto/internal/version.go new file mode 100644 index 0000000..39a8b99 --- /dev/null +++ b/otto/internal/version.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 + +package internal + +// Version is set during build time via GoReleaser. +// Source version is managed by release-please-action. +var Version = "dev" + +// GetVersion returns the current Otto version. +func GetVersion() string { + return Version +}