Skip to content
Draft
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
131 changes: 131 additions & 0 deletions .github/actions/setup-stackctl/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Setup stackctl
description: Install the stackctl binary for use in GitHub Actions workflows
author: AniTrend

branding:
icon: layers
color: green

inputs:
version:
description: >
Version of stackctl to install (without the 'v' prefix).
Use 'latest' to resolve the latest GitHub Release.
Example: '0.1.0'
required: false
default: latest
token:
description: GitHub token for API requests (defaults to github.token)
required: false
default: ${{ github.token }}

runs:
using: composite
steps:
- name: Install stackctl
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail

# ------------------------------------------------------------------
# Map GitHub Actions runner context to stackctl platform identifiers
# ------------------------------------------------------------------
case "$RUNNER_OS" in
Linux) os="linux" ;;
macOS) os="macos" ;;
*)
echo "::error::Unsupported runner OS: $RUNNER_OS"
exit 1
;;
esac

case "$RUNNER_ARCH" in
X64) arch="x64" ;;
ARM64) arch="arm64" ;;
*)
echo "::error::Unsupported runner architecture: $RUNNER_ARCH"
exit 1
;;
esac

target="stackctl-${os}-${arch}"
echo "Platform: ${os} ${arch} → artifact: ${target}"

# ------------------------------------------------------------------
# Resolve version (latest via GitHub API, or explicit tag)
# ------------------------------------------------------------------
version_raw="${{ inputs.version }}"

if [ "$version_raw" = "latest" ]; then
echo "Resolving latest release from AniTrend/stackctl..."
resolved=$(gh api repos/AniTrend/stackctl/releases/latest --jq '.tag_name') || {
echo "::error::Failed to resolve latest release"
exit 1
}
echo "Latest release resolved: ${resolved}"
tag="$resolved"
else
# Normalize: if version already starts with 'v', use as-is;
# otherwise prepend 'v'
if [[ "$version_raw" == v* ]]; then
tag="$version_raw"
else
tag="v${version_raw}"
fi
fi

# Derive a clean version string for the cache path (strip leading v)
cache_version="${tag#v}"

# ------------------------------------------------------------------
# Install directory (RUNNER_TOOL_CACHE / stackctl / version / arch)
# ------------------------------------------------------------------
install_dir="${RUNNER_TOOL_CACHE}/stackctl/${cache_version}/${arch}"
mkdir -p "$install_dir"

# ------------------------------------------------------------------
# Download binary and checksum from GitHub Releases
# ------------------------------------------------------------------
base_url="https://github.com/AniTrend/stackctl/releases/download/${tag}"
binary_url="${base_url}/${target}"
checksum_url="${base_url}/${target}.sha256"

echo "Downloading ${target} (${tag})..."
curl -fsSL --retry 3 --retry-delay 1 -o "${install_dir}/${target}" "$binary_url" || {
echo "::error::Failed to download ${binary_url}"
exit 1
}

echo "Downloading checksum..."
curl -fsSL --retry 3 --retry-delay 1 -o "${install_dir}/${target}.sha256" "$checksum_url" || {
echo "::error::Failed to download ${checksum_url}"
exit 1
}

# ------------------------------------------------------------------
# Verify SHA256 checksum
# ------------------------------------------------------------------
echo "Verifying SHA256 checksum..."
cd "$install_dir"
sha256sum -c "${target}.sha256" > /dev/null 2>&1 || {
echo "::error::SHA256 checksum verification failed for ${target}"
echo "Expected: $(cat ${target}.sha256)"
echo "Got: $(sha256sum ${target})"
exit 1
}
echo "Checksum OK"

# ------------------------------------------------------------------
# Rename to canonical binary name and make executable
# ------------------------------------------------------------------
mv "$target" stackctl
chmod +x stackctl

# ------------------------------------------------------------------
# Add to PATH for subsequent workflow steps
# ------------------------------------------------------------------
echo "$install_dir" >> "$GITHUB_PATH"

echo "stackctl ${tag} (${os}-${arch}) installed to ${install_dir}"
71 changes: 71 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* CI workflow for stackctl.
*
* Runs on every push and PR to main/dev branches.
* Validates format, linting, type checking, tests, and coverage.
*/
name: CI

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

env:
DENO_VERSION: "2.x"

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: ${{ env.DENO_VERSION }}

- name: Cache dependencies
run: deno task cache

- name: Check formatting
run: deno task fmt:check

- name: Lint
run: deno task lint

- name: Type check
run: deno task check

- name: Run tests
run: deno task test

- name: Generate coverage report
if: success()
run: |
deno test --allow-read --allow-write --allow-env --allow-run --allow-sys --coverage=.coverage
deno coverage --detailed .coverage

build:
runs-on: ubuntu-latest
needs: check
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: ${{ env.DENO_VERSION }}

- name: Build Linux x64
run: deno task build:linux-x64

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: stackctl-linux-x64
path: dist/stackctl-linux-x64
80 changes: 80 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release

on:
push:
tags:
- 'v*'

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
artifact: stackctl-linux-x64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
artifact: stackctl-linux-arm64
- target: x86_64-apple-darwin
os: macos-latest
artifact: stackctl-macos-x64
- target: aarch64-apple-darwin
os: macos-latest
artifact: stackctl-macos-arm64

steps:
- uses: actions/checkout@v4

- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Cache Deno dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/deno
deno.lock
key: ${{ runner.os }}-deno-${{ hashFiles('deno.lock') }}

- name: Build ${{ matrix.target }}
run: deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --target ${{ matrix.target }} --output dist/${{ matrix.artifact }} src/main.ts

- name: Generate checksum
run: cd dist && sha256sum ${{ matrix.artifact }} > ${{ matrix.artifact }}.sha256

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: dist/${{ matrix.artifact }}*

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
pattern: stackctl-*
path: dist/
merge-multiple: true

- name: Generate combined checksums
run: cat dist/*.sha256 > dist/checksums.txt

- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/stackctl-*
dist/checksums.txt
generate_release_notes: true
draft: false
prerelease: false
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# stackctl
.stackctl.local
.stackctl.local.*

# secrets
*.env
!.env.example
*.env.enc
age-key.txt
age.key

# build output
dist/

# rendered stacks
.rendered/

# OS
.DS_Store
Thumbs.db

# editor
.vscode/settings.json
*.swp
*.swo
*~

# test coverage
.coverage/
54 changes: 54 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@anitrend/stackctl",
"version": "0.1.0-dev",
"exports": "./src/main.ts",
"tasks": {
"cache": "deno cache src/main.ts",
"check": "deno check src/main.ts",
"fmt": "deno fmt",
"fmt:check": "deno fmt --check",
"lint": "deno lint",
"test": "deno test --allow-read --allow-write --allow-env --allow-run --allow-sys",
"coverage": "deno coverage --detailed",
"build": "deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --output dist/stackctl src/main.ts",
"build:release": "deno task build:linux-x64 && deno task build:linux-arm64 && deno task build:macos-x64 && deno task build:macos-arm64",
"build:linux-x64": "deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --target x86_64-unknown-linux-gnu --output dist/stackctl-linux-x64 src/main.ts",
"build:linux-arm64": "deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --target aarch64-unknown-linux-gnu --output dist/stackctl-linux-arm64 src/main.ts",
"build:macos-x64": "deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --target x86_64-apple-darwin --output dist/stackctl-macos-x64 src/main.ts",
"build:macos-arm64": "deno compile --allow-read --allow-write --allow-env --allow-run --allow-sys --target aarch64-apple-darwin --output dist/stackctl-macos-arm64 src/main.ts"
},
"imports": {
"@cliffy/command": "jsr:@cliffy/command@^1.0.0",
"@cliffy/command/completions": "jsr:@cliffy/command@^1.0.0/completions",
"@std/assert": "jsr:@std/assert@^1.0.18",
"@std/dotenv": "jsr:@std/dotenv@^0.225.6",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@std/path": "jsr:@std/path@^1.1.4",
"@std/testing": "jsr:@std/testing@^1.0.17",
"@std/yaml": "jsr:@std/yaml@^1.1.1",
"@std/fmt": "jsr:@std/fmt@^1.0.5"
},
"lint": {
"include": ["src/"],
"rules": {
"tags": ["recommended"],
"exclude": ["no-unused-vars"]
}
},
"fmt": {
"include": ["src/"],
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": false,
"proseWrap": "always"
},
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": false
},
"lock": true,
"nodeModulesDir": "none"
}
Loading