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

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.x"
cache: true

- name: Test
run: go test ./...

- name: Race test
run: go test -race ./...

- name: Vet
run: go vet ./...

build:
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
needs: test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.x"
cache: true

- name: Build archive
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "0"
run: |
version="${GITHUB_REF_NAME}"
commit="$(git rev-parse --short HEAD)"
build_date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
mkdir -p package dist
binary="stackdome"
if [ "${GOOS}" = "windows" ]; then
binary="stackdome.exe"
fi
go build -trimpath \
-ldflags "-s -w -X main.Version=${version} -X main.GitCommit=${commit} -X main.BuildDate=${build_date}" \
-o "package/${binary}" ./cmd/stackdome
cp LICENSE package/LICENSE
if [ "${GOOS}" = "windows" ]; then
(cd package && zip -q "../dist/stackdome_${version}_${GOOS}_${GOARCH}.zip" "${binary}" LICENSE)
else
tar -C package -czf "dist/stackdome_${version}_${GOOS}_${GOARCH}.tar.gz" "${binary}" LICENSE
fi

- name: Verify release metadata
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
env:
EXPECTED_VERSION: ${{ github.ref_name }}
run: |
expected_commit="$(git rev-parse --short HEAD)"
version_json="$(./package/stackdome version -o json)"
jq -e \
--arg version "${EXPECTED_VERSION}" \
--arg commit "${expected_commit}" \
'.version == $version and .commit == $commit and (.built | length > 0)' \
<<<"${version_json}"

- name: Upload archive
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: stackdome-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/*
if-no-files-found: error

publish:
name: Publish GitHub release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Download archives
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: stackdome-*
path: dist
merge-multiple: true

- name: Generate checksum manifest
working-directory: dist
run: sha256sum *.tar.gz *.zip > checksums.txt

- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${GITHUB_REF_NAME}" dist/* --verify-tag --generate-notes --title "Stackdome CLI ${GITHUB_REF_NAME}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ bin/
/stackfile.yaml
/docs/superpowers/
HANDOVER.md
.vscode/
91 changes: 85 additions & 6 deletions cmd/stackdome/addon.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package main

import (
"context"
"fmt"
"os"
"time"

"github.com/Stackdome/stackdome-cli/internal/cmdutil"
clierrors "github.com/Stackdome/stackdome-cli/internal/errors"
"github.com/Stackdome/stackdome-cli/internal/output"
openapi "github.com/Stackdome/stackdome/pkg/api/openapi"
"github.com/spf13/cobra"
"github.com/stackdome/cli/internal/cmdutil"
clierrors "github.com/stackdome/cli/internal/errors"
"github.com/stackdome/cli/internal/output"
)

func newAddonCmd() *cobra.Command {
Expand Down Expand Up @@ -43,6 +45,8 @@ func newPostgresCreateCmd() *cobra.Command {
flagVersion int32
flagInstances int32
flagStorage string
flagWait bool
flagTimeout time.Duration
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -73,12 +77,29 @@ func newPostgresCreateCmd() *cobra.Command {
if err != nil {
return err
}
if flagWait {
if created.Id == nil || *created.Id == "" {
return clierrors.New("Created postgres addon response had no ID; cannot wait for readiness.")
}
waitCtx, cancel := waitContext(cmd.Context(), flagTimeout)
defer cancel()
waitCmd := *cmd
waitCmd.SetContext(waitCtx)
created, err = waitForPostgresAddon(ctx, &waitCmd, *created.Id, created.Name)
if err := postgresWaitCommandError(cmd.Context(), waitCtx, createdName(addon, created), err); err != nil {
return err
}
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(created)
}

fmt.Fprintf(os.Stderr, "Postgres addon %q created.\n", created.Name)
if flagWait {
fmt.Fprintf(os.Stderr, "Postgres addon %q is %s.\n", created.Name, created.Status.GetState())
} else {
fmt.Fprintf(os.Stderr, "Postgres addon %q created.\n", created.Name)
}
return nil
})),
}
Expand All @@ -88,9 +109,63 @@ func newPostgresCreateCmd() *cobra.Command {
cmd.Flags().Int32Var(&flagVersion, "version", 16, "PostgreSQL major version (13-17)")
cmd.Flags().Int32Var(&flagInstances, "instances", 1, "Number of instances (1-5)")
cmd.Flags().StringVar(&flagStorage, "storage", "10Gi", "Storage size")
cmd.Flags().BoolVarP(&flagWait, "wait", "w", false, "Wait for the PostgreSQL addon to become ready")
cmd.Flags().DurationVar(&flagTimeout, "timeout", defaultWaitTimeout, "Maximum time to wait for the PostgreSQL addon")
return cmd
}

const postgresWaitPollInterval = 2 * time.Second

func waitForPostgresAddon(ctx *cmdutil.CommandContext, cmd *cobra.Command, addonID, name string) (*openapi.PostgresAddon, error) {
ticker := time.NewTicker(postgresWaitPollInterval)
defer ticker.Stop()

for {
addon, err := ctx.Client.GetPostgresAddon(cmd.Context(), addonID)
if err != nil {
return nil, err
}
state := ""
message := ""
if addon.Status != nil {
state = addon.Status.GetState()
message = addon.Status.GetMessage()
}
switch state {
case "Ready", "Running":
return addon, nil
case "Failed", "Error":
if message != "" {
return nil, clierrors.Newf("Postgres addon %q entered terminal state %s: %s", name, state, message)
}
return nil, clierrors.Newf("Postgres addon %q entered terminal state %s", name, state)
}

select {
case <-cmd.Context().Done():
return nil, cmd.Context().Err()
case <-ticker.C:
}
}
}

func postgresWaitCommandError(parent, wait context.Context, name string, err error) error {
if parent.Err() != nil {
return clierrors.ErrUserCanceled
}
if wait.Err() == context.DeadlineExceeded {
return clierrors.Newf("Timed out waiting for postgres addon %q to become ready.", name).WithCode("TIMEOUT")
}
return err
}

func createdName(request openapi.PostgresAddon, response *openapi.PostgresAddon) string {
if response != nil && response.Name != "" {
return response.Name
}
return request.Name
}

func newPostgresListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Expand Down Expand Up @@ -189,8 +264,12 @@ func newPostgresDeleteCmd() *cobra.Command {
return err
}

fmt.Fprintf(os.Stderr, "Postgres addon %q deleted.\n", args[0])
return nil
return printMutationResult(ctx, mutationResult{
Status: "deleted",
Resource: "postgres_addon",
Name: args[0],
ID: addon.GetId(),
}, fmt.Sprintf("Postgres addon %q deleted.", args[0]))
})),
}

Expand Down
Loading