diff --git a/.github/workflows/core-integration.yml b/.github/workflows/core-integration.yml index 44ec3ff..afeaf9c 100644 --- a/.github/workflows/core-integration.yml +++ b/.github/workflows/core-integration.yml @@ -3,12 +3,20 @@ name: Core integration on: workflow_call: inputs: + stack_sha: + description: Full NeKiro Stack commit SHA that owns this workflow + required: true + type: string core_sha: description: Full NeKiro Core commit SHA to assemble required: true type: string workflow_dispatch: inputs: + stack_sha: + description: Full NeKiro Stack commit SHA to assemble + required: true + type: string core_sha: description: Full NeKiro Core commit SHA to assemble required: true @@ -18,7 +26,7 @@ permissions: contents: read concurrency: - group: core-stack-${{ inputs.core_sha }} + group: core-stack-${{ inputs.stack_sha }}-${{ inputs.core_sha }} cancel-in-progress: true jobs: @@ -30,7 +38,18 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: NeKiro-project/NeKiro-Stack - ref: main + ref: ${{ inputs.stack_sha }} + + - name: Validate exact Stack revision + shell: bash + env: + STACK_SHA: ${{ inputs.stack_sha }} + run: | + if [[ ! "$STACK_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo 'stack_sha must be one full lowercase commit SHA.' >&2 + exit 1 + fi + test "$(git rev-parse HEAD)" = "$STACK_SHA" - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 @@ -65,12 +84,16 @@ jobs: run: | ./scripts/prepare.sh "$NEKIRO_STACK_MANIFEST" "$WORK_ROOT" "$PREPARED_ENV" source "$PREPARED_ENV" - for name in NEKIRO_CONTROL_PLANE_IMAGE NEKIRO_A2A_ROUTER_IMAGE NEKIRO_RUNTIME_A_IMAGE NEKIRO_RUNTIME_B_IMAGE NEKIRO_STACK_CORE_DIR NEKIRO_STACK_CONSOLE_DIR NEKIRO_STACK_SDK_GO_DIR NEKIRO_STACK_SAMPLES_DIR NEKIRO_STACK_TRANSPORT_GO_DIR; do + for name in NEKIRO_CONTROL_PLANE_IMAGE NEKIRO_A2A_ROUTER_IMAGE NEKIRO_RUNTIME_A_IMAGE NEKIRO_RUNTIME_B_IMAGE NEKIRO_NACOS_SECURE_PROXY_IMAGE NEKIRO_STACK_CORE_DIR NEKIRO_STACK_CONSOLE_DIR NEKIRO_STACK_SDK_GO_DIR NEKIRO_STACK_SAMPLES_DIR NEKIRO_STACK_TRANSPORT_GO_DIR; do printf '%s=%s\n' "$name" "${!name}" >>"$GITHUB_ENV" done + printf 'NEKIRO_STACK_PREPARED_ENV=%s\n' "$PREPARED_ENV" >>"$GITHUB_ENV" + + - name: Generate ephemeral Nacos PKI + run: go run ./cmd/nacos-secure-fixture generate "$NEKIRO_E2E_TLS_ROOT" - name: Start exact backend assembly - run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml up --detach --wait --wait-timeout 120 + run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml --file "$NEKIRO_E2E_COMPOSE_OVERRIDE_FILE" --profile router-nacos-secure up --detach --wait --wait-timeout 120 - name: Run trusted publication Invoke-to-Record acceptance run: go test -tags=e2e -count=1 ./tests/backend @@ -83,11 +106,11 @@ jobs: - name: Capture sanitized backend logs if: always() && steps.prepare.outcome == 'success' shell: bash - run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml --profile runtime-registration --profile watch-refresh logs --no-color 2>&1 | perl scripts/sanitize-logs.pl + run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml --file "$NEKIRO_E2E_COMPOSE_OVERRIDE_FILE" --profile router-nacos-secure --profile runtime-registration --profile watch-refresh logs --no-color 2>&1 | perl scripts/sanitize-logs.pl - name: Tear down backend assembly if: always() && steps.prepare.outcome == 'success' - run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml --profile runtime-registration --profile watch-refresh down --volumes --remove-orphans + run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file compose.yaml --file "$NEKIRO_E2E_COMPOSE_OVERRIDE_FILE" --profile router-nacos-secure --profile runtime-registration --profile watch-refresh down --volumes --remove-orphans browser: runs-on: ubuntu-latest @@ -97,7 +120,18 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: NeKiro-project/NeKiro-Stack - ref: main + ref: ${{ inputs.stack_sha }} + + - name: Validate exact Stack revision + shell: bash + env: + STACK_SHA: ${{ inputs.stack_sha }} + run: | + if [[ ! "$STACK_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo 'stack_sha must be one full lowercase commit SHA.' >&2 + exit 1 + fi + test "$(git rev-parse HEAD)" = "$STACK_SHA" - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 @@ -143,7 +177,7 @@ jobs: run: | ./scripts/prepare.sh "$NEKIRO_STACK_MANIFEST" "$WORK_ROOT" "$PREPARED_ENV" source "$PREPARED_ENV" - for name in NEKIRO_CONTROL_PLANE_IMAGE NEKIRO_A2A_ROUTER_IMAGE NEKIRO_RUNTIME_A_IMAGE NEKIRO_RUNTIME_B_IMAGE NEKIRO_STACK_CORE_DIR NEKIRO_STACK_CONSOLE_DIR NEKIRO_STACK_SDK_GO_DIR NEKIRO_STACK_SAMPLES_DIR NEKIRO_STACK_TRANSPORT_GO_DIR; do + for name in NEKIRO_CONTROL_PLANE_IMAGE NEKIRO_A2A_ROUTER_IMAGE NEKIRO_RUNTIME_A_IMAGE NEKIRO_RUNTIME_B_IMAGE NEKIRO_NACOS_SECURE_PROXY_IMAGE NEKIRO_STACK_CORE_DIR NEKIRO_STACK_CONSOLE_DIR NEKIRO_STACK_SDK_GO_DIR NEKIRO_STACK_SAMPLES_DIR NEKIRO_STACK_TRANSPORT_GO_DIR; do printf '%s=%s\n' "$name" "${!name}" >>"$GITHUB_ENV" done diff --git a/go.mod b/go.mod index 1ff6e7a..4eda837 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.0 require ( github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da github.com/jackc/pgx/v5 v5.10.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -14,6 +15,8 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.16.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.36.0 // indirect diff --git a/go.sum b/go.sum index 6cec3fb..79abff3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da h1:jZSoKwcJe github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da/go.mod h1:JCIEeiLu52WC/Q5QlcAKmWKRtW7CNLkZ3lV3BAn92Oo= github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+NiI= github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,12 +28,18 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY= github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU= github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -59,6 +66,8 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/workflow/core_integration_test.go b/internal/workflow/core_integration_test.go new file mode 100644 index 0000000..0f6372d --- /dev/null +++ b/internal/workflow/core_integration_test.go @@ -0,0 +1,92 @@ +package workflow + +import ( + "os" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +const ( + stackRevisionExpression = "${{ inputs.stack_sha }}" + secureProxyImage = "NEKIRO_NACOS_SECURE_PROXY_IMAGE" +) + +type coreIntegrationWorkflow struct { + On struct { + WorkflowCall workflowTrigger `yaml:"workflow_call"` + Dispatch workflowTrigger `yaml:"workflow_dispatch"` + } `yaml:"on"` + Jobs map[string]workflowJob `yaml:"jobs"` +} + +type workflowTrigger struct { + Inputs map[string]workflowInput `yaml:"inputs"` +} + +type workflowInput struct { + Required bool `yaml:"required"` +} + +type workflowJob struct { + Steps []workflowStep `yaml:"steps"` +} + +type workflowStep struct { + Name string `yaml:"name"` + With map[string]string `yaml:"with"` + Run string `yaml:"run"` +} + +func TestCoreIntegrationPinsStackSourceAndExportsPreparedImages(t *testing.T) { + content, err := os.ReadFile("../../.github/workflows/core-integration.yml") + if err != nil { + t.Fatal(err) + } + var workflow coreIntegrationWorkflow + if err := yaml.Unmarshal(content, &workflow); err != nil { + t.Fatal(err) + } + for triggerName, trigger := range map[string]workflowTrigger{ + "workflow_call": workflow.On.WorkflowCall, + "workflow_dispatch": workflow.On.Dispatch, + } { + if !trigger.Inputs["stack_sha"].Required { + t.Errorf("%s stack_sha input must be required", triggerName) + } + } + for _, jobName := range []string{"backend", "browser"} { + job, ok := workflow.Jobs[jobName] + if !ok { + t.Fatalf("missing %s job", jobName) + } + checkout := namedStep(t, job.Steps, "Check out canonical Stack") + if checkout.With["ref"] != stackRevisionExpression { + t.Errorf("%s checkout ref = %q, want exact stack_sha input", jobName, checkout.With["ref"]) + } + prepare := namedStep(t, job.Steps, "Resolve exact components and build images") + if !strings.Contains(prepare.Run, secureProxyImage) { + t.Errorf("%s prepare step does not export %s", jobName, secureProxyImage) + } + } + backend := workflow.Jobs["backend"] + namedStep(t, backend.Steps, "Generate ephemeral Nacos PKI") + start := namedStep(t, backend.Steps, "Start exact backend assembly") + for _, required := range []string{"NEKIRO_E2E_COMPOSE_OVERRIDE_FILE", "--profile router-nacos-secure"} { + if !strings.Contains(start.Run, required) { + t.Errorf("backend start step does not contain %q", required) + } + } +} + +func namedStep(t *testing.T, steps []workflowStep, name string) workflowStep { + t.Helper() + for _, step := range steps { + if step.Name == name { + return step + } + } + t.Fatalf("missing workflow step %q", name) + return workflowStep{} +}