Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 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
60d4813
Reformat scripts and example payload
hllvc Sep 7, 2026
b42113d
fix: fall back to default terraform version above SG ceiling on import
hllvc Sep 7, 2026
5de7523
feat: shell completion subcommand
hllvc Sep 7, 2026
57e02e0
docs: add CLAUDE.md
hllvc Sep 7, 2026
5ba9edf
feat: show help menu when no command is given
hllvc Sep 7, 2026
7a51823
fix: fail fast without TFC credentials and configure tfe provider host
hllvc Sep 7, 2026
80dbd14
fix: verify TFC credentials before apply
hllvc Sep 7, 2026
573a66f
feat: global runner constraints via SGDefaultRunnerConstraints
hllvc Sep 7, 2026
056f633
feat: shared libs for prompts, tfvars, TFC and SG API access
hllvc Sep 7, 2026
80fb77e
feat: interactive init wizard with TFC and SG discovery
hllvc Sep 7, 2026
aeb8303
feat: preflight checks before apply and import
hllvc Sep 7, 2026
b39c4d7
feat: resumable runs and project/workspace filters
hllvc Sep 7, 2026
08bb39f
feat: show the migration summary after apply and an import plan
hllvc Sep 7, 2026
4976120
feat: explain known StackGuardian API errors
hllvc Sep 7, 2026
797dcb9
feat: post-import checklist and placeholder secrets
hllvc Sep 7, 2026
07296d6
docs: describe the guided init, preflight, resume, dry-run and checklist
hllvc Sep 7, 2026
7346561
fix: read connector kind from Settings.kind in the init wizard
hllvc Sep 7, 2026
1c03650
fix: init wrote invalid tfvars for an empty approver list
hllvc Sep 7, 2026
0f071fa
fix: completion follows the shell you are actually running
hllvc Sep 7, 2026
7c2eb1f
feat: SG_TFVARS override for the tfvars path
hllvc Sep 7, 2026
8274d89
fix: import plan survives an unexpected listall shape; shorter init
hllvc Sep 7, 2026
b7e8f4a
fix: 'all' continues after the init wizard instead of stopping
hllvc Sep 7, 2026
9c3cdde
fix: parse migrate.sh fully before running it
hllvc Sep 7, 2026
04b1230
feat: strip TFC-specific variables (ignoreVarPatterns)
hllvc Sep 7, 2026
a689dd8
fix: cloud connector picker offered VCS connectors
hllvc Sep 7, 2026
49712c1
feat: execution preset support; cleaner logs and flow
hllvc Sep 7, 2026
0140516
fix: split _sg_api so callers can read a 4xx body
hllvc Sep 14, 2026
9662748
fix: import workflows without variables directly; sg-cli drops an emp…
hllvc Sep 14, 2026
1db978e
fix: upload Terraform state ourselves and treat a missing state as a …
hllvc Sep 14, 2026
927f546
feat: import one workflow first and stop if it or its state upload fails
hllvc Sep 14, 2026
e487f37
fix: update existing workflows on re-import; sg-cli never takes its u…
hllvc Sep 14, 2026
1d37135
feat: update command pulls the latest version and rebuilds the image …
hllvc Sep 14, 2026
d360f6d
fix: force LF line endings; document the Windows (WSL 2) setup
hllvc Sep 14, 2026
76bdb85
feat: projectOverrides — per-TFC-project connectors, runners, approve…
hllvc Sep 14, 2026
6a244b4
feat: strip cloud credential env vars the connector replaces (stripCl…
hllvc Sep 14, 2026
c544912
feat: reuse existing workflow groups, refuse moves and name collision…
hllvc Sep 14, 2026
ad0c36a
feat: idempotent VCS triggers; say that existing secrets are kept
hllvc Sep 14, 2026
3e35ac4
feat: init asks per project; hand-written overrides survive a re-run
hllvc Sep 14, 2026
09bc5e7
feat: preflight covers projectOverrides and the workflow-group policy
hllvc Sep 14, 2026
974588f
docs: per-project settings, group reuse, cloud credential stripping, …
hllvc Sep 14, 2026
b9eb8be
feat: init writes a commented, ready-to-use entry per project and per…
hllvc Sep 14, 2026
2e430b2
fix: --workspace globs select the same workflows in every phase
hllvc Sep 14, 2026
ae572cc
feat: exclude workspaces by name (tfWorkspaceIgnoreNames, --exclude-w…
hllvc Sep 14, 2026
e39bd86
feat: --project selects at export time, by TFC name or slug
hllvc Sep 14, 2026
5055406
feat: --tag and --exclude-tag select workspaces by TFC tag for one run
hllvc Sep 14, 2026
4cb90dd
fix: --tfvars / SG_TFVARS is the file terraform, enrich and clean use
hllvc Sep 14, 2026
663fbb1
fix: re-importing an existing workflow without variables failed under…
hllvc Sep 14, 2026
2719811
feat: run result files for CI, created vs updated in the results
hllvc Sep 14, 2026
e0dcec2
docs: run scope flags, --tfvars, run result files, running in CI
hllvc Sep 14, 2026
99e5723
feat: init --upgrade appends new settings to an existing tfvars; re-r…
hllvc Sep 14, 2026
d4688c2
fix: init --upgrade covers every module variable, not only the exampl…
hllvc Sep 14, 2026
bec15ab
feat: inline connectors and settings for one run (--cloud-connector, …
hllvc Sep 14, 2026
df28fc7
feat: --region eu|us picks the StackGuardian API and UI hosts together
hllvc Sep 14, 2026
9e458c1
feat: init asks which TFC projects to migrate and always writes tfPro…
hllvc Sep 14, 2026
2e5a0ec
fix: cloudAuthVarPatterns is an advanced setting; preflight and init …
hllvc Sep 14, 2026
cd9663f
fix: existing workflows are PATCHed directly instead of failing a cre…
hllvc Sep 14, 2026
8f0ed15
fix: preflight reports related checks on one line instead of one per …
hllvc Sep 14, 2026
c93ab68
fix: empty-object defaults expanded to a literal {\} under macOS bash…
hllvc Sep 14, 2026
a5600e8
fix: variable-set phase reports the sets and the files that gained va…
hllvc Sep 14, 2026
8a92d56
fix: the per-file trigger line leaves out zero counts
hllvc Sep 14, 2026
6417463
fix: import prints one line per workflow (created/updated, state) ins…
hllvc Sep 14, 2026
d2d6d4c
fix: checklist wording — preset version line reads plainly, state exp…
hllvc Sep 14, 2026
a7f77f5
fix: migration summary shows one 'Version & runners' row when both ar…
hllvc Sep 14, 2026
7136862
fix: the import plan legend only explains what the table shows
hllvc Sep 14, 2026
ca40a6d
fix: drop the [sg-migrate] prefix — phase lines are plain, ✓, ! or ✗
hllvc Sep 14, 2026
b84572c
fix: fold the remaining repeated lines — skipped phases, clean checkl…
hllvc Sep 14, 2026
6fd9cf9
fix: no stray '(3s)' line for the terraform init step
hllvc Sep 14, 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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keep LF everywhere: a Git-for-Windows clone with core.autocrlf would turn the
# bash scripts into CRLF and break them under WSL/Docker.
* text=auto eol=lf
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
91 changes: 91 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"]
120 changes: 101 additions & 19 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