Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3c732f2
feat: transformer state export, summary and vcs triggers
hllvc Jun 24, 2026
a0c0b17
feat: payload json schema and validator
hllvc Jun 24, 2026
e2ec684
feat: dockerized migration orchestrator
hllvc Jun 24, 2026
2e0242d
docs: update README for the dockerized pipeline
hllvc Jun 24, 2026
5ea39ab
Reformat scripts and example payload
hllvc Sep 7, 2026
1af25eb
fix: fall back to default terraform version above SG ceiling on import
hllvc Sep 7, 2026
339aecb
feat: shell completion subcommand
hllvc Sep 7, 2026
d740eb0
docs: add CLAUDE.md
hllvc Sep 7, 2026
d5e5c4e
feat: show help menu when no command is given
hllvc Sep 7, 2026
784a66c
fix: fail fast without TFC credentials and configure tfe provider host
hllvc Sep 7, 2026
6a43e8a
fix: verify TFC credentials before apply
hllvc Sep 7, 2026
53a48e8
feat: global runner constraints via SGDefaultRunnerConstraints
hllvc Sep 7, 2026
ed666e7
feat: shared libs for prompts, tfvars, TFC and SG API access
hllvc Sep 7, 2026
0d408c0
feat: interactive init wizard with TFC and SG discovery
hllvc Sep 7, 2026
1e9e264
feat: preflight checks before apply and import
hllvc Sep 7, 2026
54e1876
feat: resumable runs and project/workspace filters
hllvc Sep 7, 2026
921e369
feat: show the migration summary after apply and an import plan
hllvc Sep 7, 2026
ec461e0
feat: explain known StackGuardian API errors
hllvc Sep 7, 2026
53c7d6b
feat: post-import checklist and placeholder secrets
hllvc Sep 7, 2026
23371e1
docs: describe the guided init, preflight, resume, dry-run and checklist
hllvc Sep 7, 2026
5e25afe
fix: read connector kind from Settings.kind in the init wizard
hllvc Sep 7, 2026
0be0082
fix: init wrote invalid tfvars for an empty approver list
hllvc Sep 7, 2026
1753eb7
fix: completion follows the shell you are actually running
hllvc Sep 7, 2026
e283233
feat: SG_TFVARS override for the tfvars path
hllvc Sep 7, 2026
b148418
fix: import plan survives an unexpected listall shape; shorter init
hllvc Sep 7, 2026
4f3b86d
fix: 'all' continues after the init wizard instead of stopping
hllvc Sep 7, 2026
329dc9b
fix: parse migrate.sh fully before running it
hllvc Sep 7, 2026
52e5237
feat: strip TFC-specific variables (ignoreVarPatterns)
hllvc Sep 7, 2026
fb20f80
fix: cloud connector picker offered VCS connectors
hllvc Sep 7, 2026
cbd03da
feat: execution preset support; cleaner logs and flow
hllvc Sep 7, 2026
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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.sg
export
out
**/.terraform
**/.terraform.lock.hcl
*.tfstate
*.tfstate.*
*.tfvars
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ eggs/
.eggs/
lib/
lib64/
# The migrator's sourced shell libraries are not Python build output.
!scripts/lib/
parts/
sdist/
var/
Expand Down Expand Up @@ -146,6 +148,8 @@ crash.log
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
# The init wizard keeps the previous file as terraform.tfvars.bak
*.tfvars.bak

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
Expand All @@ -163,4 +167,7 @@ out/*
zip
zip/*

# Migrator local cache + config (downloaded tool binaries, workflow-group map)
.sg/

.DS_Store
88 changes: 88 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Migrator runtime: bundles all pinned tooling so the flow behaves identically
# on Linux/macOS/Windows hosts (anywhere Docker runs). The repo is bind-mounted
# at /app at runtime; this image only provides the tools on PATH.

# yajsv has no linux/arm64 release asset, so build it from source for the
# image's target architecture.
FROM golang:1.22-bookworm AS yajsv
ARG YAJSV_VERSION=v1.4.1
RUN go install "github.com/neilpa/yajsv@${YAJSV_VERSION}"

FROM debian:bookworm-slim

ARG TERRAFORM_VERSION=1.9.8
ARG JQ_VERSION=1.8.1
ARG HCL2JSON_VERSION=0.6.7

RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl ca-certificates git unzip tar coreutils \
&& rm -rf /var/lib/apt/lists/*

# terraform (arch from dpkg: amd64/arm64 — works with or without buildx)
RUN arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${arch}.zip" -o /tmp/tf.zip \
&& unzip /tmp/tf.zip -d /usr/local/bin \
&& rm /tmp/tf.zip

# jq
RUN arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-${arch}" -o /usr/local/bin/jq \
&& chmod +x /usr/local/bin/jq

# hcl2json
RUN arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://github.com/tmccombs/hcl2json/releases/download/v${HCL2JSON_VERSION}/hcl2json_linux_${arch}" -o /usr/local/bin/hcl2json \
&& chmod +x /usr/local/bin/hcl2json

# yajsv (from the build stage above)
COPY --from=yajsv /go/bin/yajsv /usr/local/bin/yajsv

# sg-cli (latest release, Go binary). Assets: sg-cli_<OS>_<ARCH>.tar.gz.
RUN set -eu; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in amd64) arch=x86_64 ;; arm64) arch=arm64 ;; esac; \
curl -fsSL "https://github.com/StackGuardian/sg-cli/releases/latest/download/sg-cli_Linux_${arch}.tar.gz" -o /tmp/sg-cli.tar.gz; \
mkdir -p /tmp/sgcli; \
tar -xzf /tmp/sg-cli.tar.gz -C /tmp/sgcli; \
realcli="$(find /tmp/sgcli -maxdepth 2 -type f -name sg-cli | head -1)"; \
test -n "$realcli"; \
install -m 0755 "$realcli" /usr/local/bin/sg-cli; \
rm -rf /tmp/sg-cli.tar.gz /tmp/sgcli

WORKDIR /app
ENTRYPOINT ["/bin/bash"]
94 changes: 76 additions & 18 deletions README.md

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions schema/sg-payload.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StackGuardian bulk workflow payload",
"description": "Validates the array of workflow objects produced by the transformer (sg-payload.<project>.json). Constraints (ResourceName length, kind/sourceConfigDestKind enums) are derived from schema/sg-openapi.json (#/components/schemas/Workflow and related). Lenient by design: only the fields the transformer emits are checked; unknown fields are allowed so the schema does not need to track every optional API field.",
"type": "array",
"items": { "$ref": "#/definitions/workflow" },
"definitions": {
"workflow": {
"type": "object",
"required": ["ResourceName", "WfType", "VCSConfig"],
"properties": {
"ResourceName": { "type": "string", "minLength": 1, "maxLength": 100 },
"WfType": { "type": "string", "minLength": 1 },
"Description": { "type": "string" },
"Tags": { "type": "array", "items": { "type": "string" } },
"Approvers": { "type": "array", "items": { "type": "string" } },
"UserSchedules": { "type": "array" },
"EnvironmentVariables": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "config"],
"properties": {
"kind": { "type": "string" },
"config": {
"type": "object",
"required": ["varName"],
"properties": {
"varName": { "type": "string", "minLength": 1 },
"textValue": { "type": "string" }
}
}
}
}
},
"DeploymentPlatformConfig": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "config"],
"properties": {
"kind": {
"enum": ["AWS_STATIC", "AWS_RBAC", "AWS_OIDC", "AZURE_STATIC", "AZURE_OIDC", "AZURE_MANAGED_ID_OIDC", "GCP_STATIC", "GCP_OIDC"]
},
"config": { "type": "object" }
}
}
},
"RunnerConstraints": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "type": "string" },
"names": { "type": "array", "items": { "type": "string" } }
}
},
"VCSConfig": {
"type": "object",
"required": ["iacVCSConfig", "iacInputData"],
"properties": {
"iacVCSConfig": {
"type": "object",
"properties": {
"customSource": {
"type": "object",
"properties": {
"sourceConfigDestKind": {
"enum": ["GITHUB_COM", "GITHUB_APP_CUSTOM", "GIT_OTHER", "INLINE", "BITBUCKET_ORG", "GITLAB_COM", "AZURE_DEVOPS"]
}
}
}
}
},
"iacInputData": {
"type": "object",
"required": ["schemaType", "data"],
"properties": {
"schemaType": { "type": "string" },
"data": { "type": "object" }
}
}
}
},
"TerraformConfig": {
"type": "object",
"properties": {
"terraformVersion": { "type": "string", "minLength": 1 },
"managedTerraformState": { "type": "boolean" },
"approvalPreApply": { "type": "boolean" }
}
},
"VCSTriggers": {
"type": ["object", "null"],
"required": ["type"],
"properties": {
"type": {
"enum": ["GITHUB_COM", "GITHUB_APP_CUSTOM", "GITLAB_OAUTH_SSH", "BITBUCKET_ORG", "GITLAB_COM", "AZURE_DEVOPS", "AZURE_DEVOPS_SP"]
},
"tracked_branch": { "type": ["string", "null"] },
"approval_pre_apply": { "type": "boolean" },
"plan_only": { "type": "boolean" },
"gh_check": { "type": "boolean" },
"gl_pipeline": { "type": "boolean" },
"post_comments": { "type": "boolean" },
"file_triggers_enabled": { "type": "boolean" },
"file_trigger_patterns": { "type": "array", "items": { "type": "string" } },
"tags_regex": { "type": ["string", "null"] },
"push": { "type": "object" },
"pull_request_opened": { "type": "object" },
"pull_request_modified": { "type": "object" },
"all_pull_requests": { "type": "object" },
"create_tag": { "type": "object" }
}
}
}
}
}
}
78 changes: 29 additions & 49 deletions convert_hcl_to_json.sh → scripts/convert_hcl_to_json.sh
Original file line number Diff line number Diff line change
@@ -1,49 +1,14 @@
#!/bin/bash
set -euo pipefail

log() { echo "[convert_hcl_to_json] $*" >&2; }

WORKDIR=$(mktemp -d)
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT

# Normalize OS/arch to the names used by the jq and hcl2json release assets.
OS=$(uname -s)
case "$OS" in
Darwin) OS="macos" ;;
Linux) OS="linux" ;;
*) echo "Unsupported OS: $OS" >&2; exit 1 ;;
esac

ARCH=$(uname -m)
case "$ARCH" in
x86_64 | amd64) ARCH="amd64" ;;
aarch64 | arm64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
esac

JQ_BIN="$WORKDIR/jq"
HCL2JSON_BIN="$WORKDIR/hcl2json"

install_jq() {
local url="https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-${OS}-${ARCH}"
if ! curl -fsSL -o "$JQ_BIN" "$url"; then
echo "Failed to download jq from $url" >&2
exit 1
fi
chmod +x "$JQ_BIN"
}

install_hcl2json() {
# hcl2json uses "darwin" rather than "macos" for the OS segment.
local hcl_os="$OS"
[[ "$hcl_os" == "macos" ]] && hcl_os="darwin"
local url="https://github.com/tmccombs/hcl2json/releases/download/v0.6.7/hcl2json_${hcl_os}_${ARCH}"
if ! curl -fsSL -o "$HCL2JSON_BIN" "$url"; then
echo "Failed to download hcl2json from $url" >&2
exit 1
fi
chmod +x "$HCL2JSON_BIN"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=tools.sh
source "$SCRIPT_DIR/tools.sh"

# Detail lines are shown only in verbose mode; warnings always show.
log() {
[ "${SG_VERBOSE:-0}" = "1" ] || return 0
printf '%s[convert]%s %s\n' "$C_CYAN" "$C_RESET" "$*" >&2
}

INPUT_FILE_JSON="${1:-}"
Expand All @@ -56,24 +21,31 @@ if [ ! -f "$INPUT_FILE_JSON" ]; then
exit 1
fi

log "Downloading jq and hcl2json..."
install_jq
install_hcl2json
WORKDIR=$(mktemp -d)
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT

# Resolve tooling from PATH (Docker image) or download+cache (native).
JQ_BIN=$(sg_resolve jq sg_ensure_jq)
HCL2JSON_BIN=$(sg_resolve hcl2json sg_ensure_hcl2json)

# Read entire JSON array into a variable
json_data=$(cat "$INPUT_FILE_JSON")

# Use jq to get the length of array
length=$($JQ_BIN length <<<"$json_data")
log "Processing $length workflow(s) from $INPUT_FILE_JSON"
log "Processing $length workflow(s) from $(sg_rel "$INPUT_FILE_JSON")"

# Accumulate updated objects as newline-delimited JSON
tmpfile="$WORKDIR/updated.ndjson"
: >"$tmpfile"

JSON_PATH=".VCSConfig.iacInputData.data"
converted=0
touched=0

for ((i = 0; i < length; i++)); do
wf_converted=0
# Extract ith object
obj=$($JQ_BIN ".[$i]" <<<"$json_data")

Expand Down Expand Up @@ -117,13 +89,16 @@ for ((i = 0; i < length; i++)); do
if [[ -n "$parsed" && "$parsed" != "null" ]]; then
log " workflow $((i + 1)): converted '$key' from HCL to JSON"
new_val=$($JQ_BIN --arg k "$key" --argjson v "$parsed" '. + {($k): $v}' <<<"$new_val")
converted=$((converted + 1))
wf_converted=1
else
log " workflow $((i + 1)): parsing failed, keeping original value for '$key'"
sg_warn "$(sg_rel "$INPUT_FILE_JSON") workflow $((i + 1)): could not parse '$key' as HCL; keeping original value"
fi
done < <($JQ_BIN -r 'keys[]' <<<"$val")

# Assign the converted data back at JSON_PATH
updated_obj=$($JQ_BIN --argjson nv "$new_val" "$JSON_PATH = \$nv" <<<"$obj")
touched=$((touched + wf_converted))

echo "$updated_obj" >>"$tmpfile"
done
Expand All @@ -133,4 +108,9 @@ done
outfile="$WORKDIR/output.json"
$JQ_BIN -s '.' "$tmpfile" >"$outfile"
mv "$outfile" "$INPUT_FILE_JSON"
log "Done. Updated $INPUT_FILE_JSON in place."
# One result line per file (the orchestrator shows it as-is).
if [ "$converted" -gt 0 ]; then
sg_log "$(basename "$INPUT_FILE_JSON"): $converted HCL value(s) converted to JSON in $touched of $length workflow(s)"
else
sg_log "$(basename "$INPUT_FILE_JSON"): nothing to convert ($length workflow(s), values already JSON)"
fi
Loading