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
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
## Checklist

- [ ] I have read `CONTRIBUTING.md`.
- [ ] The PR title follows Conventional Commits (`feat:`, `fix:`, ...); it becomes the changelog entry.
- [ ] Every commit in this PR includes a valid DCO `Signed-off-by` line.
- [ ] I have the right to submit all material in this PR.
- [ ] I have not included secrets or confidential data.
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ jobs:
- name: Vulnerability scan
run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./...

# The release pipeline only runs once a release pull request merges, so a broken
# .goreleaser.yaml would otherwise surface at the worst moment, after the tag
# exists. Same version as `task release:check`.
- name: GoReleaser config
run: go run github.com/goreleaser/goreleaser/v2@v2.12.7 check

# Both installers are fetched raw from this branch by users, so a syntax
# error ships the moment it merges.
- name: Install scripts
run: |
shellcheck --shell=sh install/install.sh
pwsh -NoProfile -Command '
$tokens = $null; $errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile("install/install.ps1", [ref]$tokens, [ref]$errors)
if ($errors) { $errors | ForEach-Object { Write-Error $_.ToString() }; exit 1 }
'

build:
name: build
runs-on: ubuntu-latest
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: PR title

# release-please computes every version and changelog entry from the commit messages
# that land on main, so they must follow Conventional Commits: `feat:` cuts a minor
# version, `fix:` a patch, `feat!:` or a `BREAKING CHANGE:` footer a major one (a minor
# one before 1.0.0). A squash merge makes the pull request title the commit message,
# which is why the title is the thing checked here, and a single-commit pull request
# must agree with it, since GitHub uses that commit's message instead.
#
# pull_request_target, like dco.yml, so the check runs as it is on main. It is safe for
# the same reason: nothing here checks out or runs the pull request's code.
on:
pull_request_target:
branches: ["main"]
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read

jobs:
title:
name: PR title
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Title follows Conventional Commits
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ github.token }}
with:
validateSingleCommit: true
validateSingleCommitMatchesPrTitle: true
111 changes: 106 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,84 @@
name: Release

# Releases are cut from main, never by hand-pushing a tag.
#
# Every push to main runs release-please, which reads the Conventional Commit messages
# since the last release and keeps one pull request open, "chore: release X.Y.Z", that
# bumps the version and writes CHANGELOG.md. Merging that pull request is the release:
# on that push release-please tags vX.Y.Z and creates the GitHub release, and the
# goreleaser job builds and attaches everything the README's Installation section
# points at.
#
# workflow_dispatch rebuilds the artifacts of a tag that already exists, for when the
# goreleaser job failed after release-please had tagged.
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
branches: [ "main" ]
workflow_dispatch:
inputs:
tag:
description: Existing tag to build and publish, e.g. v0.1.0
required: true

permissions:
contents: write
permissions: {}

# Two merges in quick succession must not race to tag or publish.
concurrency:
group: release
cancel-in-progress: false

jobs:
release-please:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
# The release pull request must run CI: the ruleset requires lint, build, test and
# DCO before anything merges. GitHub starts no workflow for a pull request opened
# with the job's GITHUB_TOKEN, so release-please acts as a GitHub App instead. The
# App needs Contents, Pull requests and Issues (for labels) read and write on this
# repository; README's Releasing section says how to set it up.
- name: Create GitHub App token
id: app
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Run release-please
id: release
uses: googleapis/release-please-action@v4
with:
token: ${{ steps.app.outputs.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

goreleaser:
needs: release-please
# release-please is skipped on workflow_dispatch, and a skipped need skips this job
# unless the condition says otherwise.
if: >-
!cancelled() && (
needs.release-please.outputs.release_created == 'true' ||
github.event_name == 'workflow_dispatch'
)
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
attestations: write
env:
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
ref: ${{ env.TAG }}
fetch-depth: 0

# 1.26.x, the oldest Go still patched: a released binary carries the
Expand All @@ -32,11 +96,48 @@ jobs:
check-latest: true
cache-dependency-path: go.sum

# The arm64 image is built on an amd64 runner.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

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

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# release-please has already created the release; GoReleaser finds it by tag and
# attaches the artifacts (release.mode: append in .goreleaser.yaml).
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# SLSA build provenance for every archive and package, and for the image, so
# `gh attestation verify <file> --repo kaitencloud/cli` proves an artifact was
# built here, from this tag.
- name: Attest release artifacts
uses: actions/attest-build-provenance@v3
with:
subject-checksums: dist/checksums.txt

- name: Resolve image digest
id: image
run: |
digest=$(docker buildx imagetools inspect "ghcr.io/kaitencloud/cli:${TAG#v}" --format '{{json .Manifest}}' | jq -r .digest)
echo "digest=$digest" >> "$GITHUB_OUTPUT"

- name: Attest container image
uses: actions/attest-build-provenance@v3
with:
subject-name: ghcr.io/kaitencloud/cli
subject-digest: ${{ steps.image.outputs.digest }}
push-to-registry: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test-report.json
# Build and release output
/bin/
/dist/
/completions/

# Dependency directories
vendor/
Expand Down
135 changes: 117 additions & 18 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2

project_name: kaiten

before:
hooks:
- go mod tidy
# Completions ship in every archive and Linux package, so they are
# generated once here from the binary that is about to be released.
- sh -c 'mkdir -p completions && for shell in bash zsh fish; do go run ./cmd/kaiten completion "$shell" > "completions/kaiten.$shell"; done'

builds:
- id: kaiten
Expand All @@ -25,10 +29,12 @@ builds:
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}

# install/install.sh and install/install.ps1 download these by name, so the
# template is part of the install contract: kaiten_<os>_<arch>.tar.gz|zip.
archives:
- formats: [tar.gz]
name_template: >-
{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}
- id: archives
formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
formats: [zip]
Expand All @@ -39,28 +45,121 @@ archives:
- LICENSE
- NOTICE
- README.md
- completions/*

checksum:
name_template: "checksums.txt"

# .deb, .rpm and .apk, attached to the release next to the archives. Named
# like the archives, without the version, so that
# releases/latest/download/kaiten_linux_amd64.deb is a stable URL; the version
# lives in the package metadata, where dpkg, rpm and apk read it.
nfpms:
- id: packages
package_name: kaiten
file_name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
vendor: Kaiten
homepage: https://kaiten.sh
maintainer: Kaiten <support@kaiten.sh>
description: Command-line interface for the Kaiten API.
license: Apache-2.0
formats:
- deb
- rpm
- apk
bindir: /usr/bin
section: utils
contents:
- src: completions/kaiten.bash
dst: /usr/share/bash-completion/completions/kaiten
- src: completions/kaiten.zsh
dst: /usr/share/zsh/site-functions/_kaiten
- src: completions/kaiten.fish
dst: /usr/share/fish/vendor_completions.d/kaiten.fish
# The doc/license content types are rpm-only; deb and apk silently skip
# them, so each packager gets the license file in its own convention.
- src: LICENSE
dst: /usr/share/licenses/kaiten/LICENSE
type: license
packager: rpm
- src: NOTICE
dst: /usr/share/licenses/kaiten/NOTICE
type: license
packager: rpm
- src: LICENSE
dst: /usr/share/doc/kaiten/copyright
packager: deb
- src: NOTICE
dst: /usr/share/doc/kaiten/NOTICE
packager: deb
- src: LICENSE
dst: /usr/share/licenses/kaiten/LICENSE
packager: apk
- src: NOTICE
dst: /usr/share/licenses/kaiten/NOTICE
packager: apk

# ghcr.io/kaitencloud/cli:<version> and :latest, one multi-platform image. The
# workflow logs in to GHCR with the job's GITHUB_TOKEN. Built in the publish
# phase, so `--snapshot` produces per-platform local images and `--skip=docker`
# none at all.
dockers_v2:
- id: kaiten
ids:
- kaiten
images:
- ghcr.io/kaitencloud/cli
tags:
- "{{ .Version }}"
# A pre-release tag (v1.2.0-rc.1) does not move latest.
- "{{ if not .Prerelease }}latest{{ end }}"
platforms:
- linux/amd64
- linux/arm64
dockerfile: Dockerfile
extra_files:
- LICENSE
- NOTICE
labels:
org.opencontainers.image.title: "{{ .ProjectName }}"
org.opencontainers.image.description: Command-line interface for the Kaiten API
org.opencontainers.image.url: https://github.com/kaitencloud/cli
org.opencontainers.image.source: https://github.com/kaitencloud/cli
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.licenses: Apache-2.0

# release-please writes the release notes, from the same commits, into CHANGELOG.md
# and the GitHub release; GoReleaser adds only the install footer below.
changelog:
sort: asc
groups:
- title: Features
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Bug fixes
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Other
order: 999
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
disable: true

release:
github:
owner: kaitencloud
name: cli
# release-please has created the release by the time GoReleaser runs: append the
# footer to its notes and attach the artifacts, rather than replace them. This is
# also why the repository must not turn on immutable releases, which forbid adding
# assets to a published release.
mode: append
# A pre-release version (1.2.0-rc.1) is published as a GitHub pre-release, which
# also keeps install.sh's "latest" pointing at the last stable one.
prerelease: auto
footer: |
## Install

```shell
# macOS and Linux
curl -fsSL https://raw.githubusercontent.com/kaitencloud/cli/main/install/install.sh | sh -s -- {{ .Version }}

# Windows
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/kaitencloud/cli/main/install/install.ps1))) -Version {{ .Version }}

# Container
docker run --rm ghcr.io/kaitencloud/cli:{{ .Version }} version
```

Every archive and package above is listed in `checksums.txt`. See the
[README](https://github.com/kaitencloud/cli#installation) for the other options.
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Loading
Loading