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
167 changes: 167 additions & 0 deletions .github/actions/setup-stackctl/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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
github-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:
INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}
run: |
set -euo pipefail

# ------------------------------------------------------------------
# Map GitHub Actions runner context to target triples
# ------------------------------------------------------------------
case "${RUNNER_OS}-${RUNNER_ARCH}" in
Linux-X64) target_triple="x86_64-unknown-linux-gnu" ;;
Linux-ARM64) target_triple="aarch64-unknown-linux-gnu" ;;
macOS-X64) target_triple="x86_64-apple-darwin" ;;
macOS-ARM64) target_triple="aarch64-apple-darwin" ;;
*)
echo "::error::Unsupported runner: ${RUNNER_OS} ${RUNNER_ARCH}"
exit 1
;;
esac

echo "Platform: ${RUNNER_OS} ${RUNNER_ARCH} → target triple: ${target_triple}"

# ------------------------------------------------------------------
# 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=$(curl -sSfL \
-H "Accept: application/vnd.github+json" \
-H "Authorization: token ${INPUT_GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/AniTrend/stackctl/releases/latest \
| tr -d '\n\r' \
| grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 \
| sed 's/.*"\(.*\)"/\1/') || {
echo "::error::Failed to resolve latest release"
exit 1
}

if [ -z "$resolved" ]; then
echo "::error::Failed to parse tag_name from GitHub API response"
exit 1
fi

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}/${RUNNER_ARCH}"
mkdir -p "$install_dir"

# ------------------------------------------------------------------
# Download tarball and checksums.txt from GitHub Releases
# ------------------------------------------------------------------
tarball="stackctl-${tag}-${target_triple}.tar.gz"
base_url="https://github.com/AniTrend/stackctl/releases/download/${tag}"
tarball_url="${base_url}/${tarball}"
checksum_url="${base_url}/checksums.txt"

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

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

# ------------------------------------------------------------------
# Verify SHA256 checksum
# ------------------------------------------------------------------
echo "Verifying SHA256 checksum..."
cd "$install_dir"

# Extract expected checksum for this tarball from checksums.txt
expected=$(grep -F "${tarball}" checksums.txt | awk '{print $1}')
if [ -z "$expected" ]; then
echo "::error::Could not find checksum for ${tarball} in checksums.txt"
exit 1
fi

# Use sha256sum if available, otherwise fall back to shasum -a 256 (macOS)
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "${tarball}" | awk '{print $1}')
else
actual=$(shasum -a 256 "${tarball}" | awk '{print $1}')
fi

if [ "$expected" != "$actual" ]; then
echo "::error::SHA256 checksum verification failed for ${tarball}"
echo "Expected: ${expected}"
echo "Got: ${actual}"
exit 1
fi
echo "Checksum OK"

# ------------------------------------------------------------------
# Extract tarball
# ------------------------------------------------------------------
echo "Extracting ${tarball}..."
tar -xzf "${tarball}" || {
echo "::error::Failed to extract ${tarball}"
exit 1
}

# Ensure the stackctl binary exists after extraction
if [ ! -f "stackctl" ]; then
echo "::error::stackctl binary not found after extracting ${tarball}"
exit 1
fi

# ------------------------------------------------------------------
# Make binary executable
# ------------------------------------------------------------------
chmod +x stackctl

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

echo "stackctl ${tag} (${target_triple}) 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
90 changes: 90 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest

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-sys --allow-run=git,docker,sops,age,age-keygen,shred,rm --target ${{ matrix.target }} --output dist/stackctl src/main.ts

- name: Copy README and LICENSE
run: cp README.md LICENSE dist/

- name: Package tarball
working-directory: dist
run: tar -czf "stackctl-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" stackctl README.md LICENSE

- name: Generate checksum
working-directory: dist
run: shasum -a 256 "stackctl-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" > "stackctl-${{ github.ref_name }}-${{ matrix.target }}.tar.gz.sha256"

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: |
dist/stackctl-${{ github.ref_name }}-${{ matrix.target }}.tar.gz
dist/stackctl-${{ github.ref_name }}-${{ matrix.target }}.tar.gz.sha256

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: "*"
path: dist/
merge-multiple: true

- name: Generate combined checksums
working-directory: dist
run: shasum -a 256 stackctl-*.tar.gz > checksums.txt

# NOTE: Homebrew tap auto-update is a follow-up task.
# The release workflow currently does not trigger a Homebrew tap PR.

- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/stackctl-*.tar.gz
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/
Loading