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
82 changes: 82 additions & 0 deletions .github/workflows/iac-host-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: IaC Host Conformance
on:
push:
branches: [main]
pull_request:
branches: [main]

env:
GOPRIVATE: github.com/GoCodeAlone/*

jobs:
legacy-module-engine-range:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Configure Git for private repos
env:
RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}
run: |
if [ -n "${RELEASES_TOKEN}" ]; then
git config --global url."https://x-access-token:${RELEASES_TOKEN}@github.com/".insteadOf "https://github.com/"
fi

- name: Determine Workflow engine versions
id: versions
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_CURRENT_VERSION: ${{ vars.WORKFLOW_CURRENT_VERSION }}
run: |
set -euo pipefail
min="$(jq -r '.minEngineVersion // empty' plugin.json)"
if [ -z "${min}" ]; then
echo "::error::plugin.json must declare minEngineVersion"
exit 1
fi
case "${min}" in
v*) min_version="${min}" ;;
*) min_version="v${min}" ;;
esac
current="${WORKFLOW_CURRENT_VERSION}"
if [ -z "${current}" ]; then
current="$(gh release view --repo GoCodeAlone/workflow --json tagName --jq '.tagName')"
fi
if [ -z "${current}" ]; then
echo "::error::could not determine current Workflow engine release"
exit 1
fi
first="$(printf '%s\n%s\n' "${current}" "${min_version}" | sort -V | head -n1)"
if [ "${first}" = "${current}" ] && [ "${current}" != "${min_version}" ]; then
echo "::notice::current Workflow release ${current} is older than declared minimum ${min_version}; testing minimum as current"
current="${min_version}"
fi
echo "min=${min_version}" >> "${GITHUB_OUTPUT}"
echo "current=${current}" >> "${GITHUB_OUTPUT}"
echo "Declared min engine: ${min_version}"
echo "Current engine release: ${current}"

# Current-release probing is intentional: this plugin should keep loading
# on newly released Workflow hosts. Pin WORKFLOW_CURRENT_VERSION at the
# repo/org level when a release is under investigation.
- name: Conformance against declared minimum engine
run: ./scripts/workflow-iac-host-conformance.sh "${{ steps.versions.outputs.min }}" min

- name: Conformance against current engine release
if: steps.versions.outputs.current != steps.versions.outputs.min
run: ./scripts/workflow-iac-host-conformance.sh "${{ steps.versions.outputs.current }}" current

- name: Remove private repo Git credential rewrite
if: always()
env:
RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}
run: |
if [ -n "${RELEASES_TOKEN}" ]; then
git config --global --unset-all url."https://x-access-token:${RELEASES_TOKEN}@github.com/".insteadOf || true
fi
118 changes: 118 additions & 0 deletions internal/host_conformance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package internal

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/GoCodeAlone/workflow/plugin/external"
pb "github.com/GoCodeAlone/workflow/plugin/external/proto"
)

func TestWorkflowHostConformance_LoadsLegacyIaCModulePlugin(t *testing.T) {
if os.Getenv("WORKFLOW_IAC_HOST_CONFORMANCE") != "1" {
t.Skip("set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run host compatibility smoke")
}

// AWS is still a legacy sdk.Serve module plugin, not a strict-cutover
// sdk.ServeIaCPlugin provider. This gate validates the host/plugin boundary
// it actually ships today: external plugin load, iac.provider discovery, and
// strict module contract registry exposure across engine versions.
repoRoot := hostConformanceRepoRoot(t)
pluginName := hostConformancePluginName(t, filepath.Join(repoRoot, "plugin.json"))

pluginsDir := filepath.Join(t.TempDir(), "data", "plugins")
pluginDir := filepath.Join(pluginsDir, pluginName)
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
t.Fatalf("mkdir plugin dir: %v", err)
}
hostConformanceCopyFile(t, filepath.Join(repoRoot, "plugin.json"), filepath.Join(pluginDir, "plugin.json"))
hostConformanceCopyFile(t, filepath.Join(repoRoot, "plugin.contracts.json"), filepath.Join(pluginDir, "plugin.contracts.json"))

build := exec.Command("go", "build", "-o", filepath.Join(pluginDir, pluginName), "./cmd/workflow-plugin-aws")
build.Dir = repoRoot
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("build plugin binary: %v\n%s", err, out)
}

mgr := external.NewExternalPluginManager(pluginsDir, nil)
t.Cleanup(mgr.Shutdown)

adapter, err := mgr.LoadPlugin(pluginName)
if err != nil {
t.Fatalf("load plugin through Workflow external host: %v", err)
}
if adapter.Name() != pluginName {
t.Fatalf("host adapter name = %q, want %q", adapter.Name(), pluginName)
}
if !hasString(adapter.EngineManifest().ModuleTypes, moduleTypeIaCProvider) {
t.Fatalf("host adapter module types = %v, want %q", adapter.EngineManifest().ModuleTypes, moduleTypeIaCProvider)
}

registry := adapter.ContractRegistry()
if registry == nil {
t.Fatal("contract registry is nil")
}
if !registryHasModule(registry, moduleTypeIaCProvider) {
t.Fatalf("contract registry missing module %q: %v", moduleTypeIaCProvider, registry.GetContracts())
}
}

func hostConformanceRepoRoot(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return filepath.Clean(filepath.Join(filepath.Dir(file), ".."))
}

func hostConformancePluginName(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read plugin manifest: %v", err)
}
var manifest struct {
Name string `json:"name"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
t.Fatalf("parse plugin manifest: %v", err)
}
if manifest.Name == "" {
t.Fatal("plugin manifest missing name")
}
return manifest.Name
}

func hostConformanceCopyFile(t *testing.T, src, dst string) {
t.Helper()
data, err := os.ReadFile(src)
if err != nil {
t.Fatalf("read %s: %v", src, err)
}
if err := os.WriteFile(dst, data, 0o644); err != nil {
t.Fatalf("write %s: %v", dst, err)
}
}

func registryHasModule(registry *pb.ContractRegistry, moduleType string) bool {
for _, contract := range registry.GetContracts() {
if contract.GetKind() == pb.ContractKind_CONTRACT_KIND_MODULE && contract.GetModuleType() == moduleType {
return true
}
}
return false
}

func hasString(values []string, want string) bool {
for _, value := range values {
if value == want {
return true
}
}
return false
}
31 changes: 31 additions & 0 deletions scripts/workflow-iac-host-conformance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail

engine_version="${1:?usage: workflow-iac-host-conformance.sh <workflow-engine-version> [label]}"
label="${2:-${engine_version}}"

case "${engine_version}" in
v*) ;;
*) engine_version="v${engine_version}" ;;
esac

repo_root="$(git rev-parse --show-toplevel)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT

work_dir="${tmp_dir}/repo"
mkdir -p "${work_dir}"

rsync -a \
--exclude '.git' \
--exclude '.worktrees' \
--exclude '_worktrees' \
--exclude 'data' \
"${repo_root}/" "${work_dir}/"

cd "${work_dir}"

echo "==> workflow IaC host conformance (${label}): github.com/GoCodeAlone/workflow@${engine_version}"
go mod edit -require "github.com/GoCodeAlone/workflow@${engine_version}"
GOWORK=off go mod tidy
WORKFLOW_IAC_HOST_CONFORMANCE=1 GOWORK=off go test ./internal -run TestWorkflowHostConformance_LoadsLegacyIaCModulePlugin -count=1 -v
Loading