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
13 changes: 11 additions & 2 deletions e2e/scenarios/31-native-deployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ steps:
# A dry-run run skips its deploys, so it must not create a real
# Deployment: every lifecycle step is also gated on a non-dry-run run.
- " if: ${{ github.server_url == 'https://github.com' && github.event.inputs.dry_run != 'true' }}"
- " deployment_id=$(gh api repos/${{ github.repository }}/deployments \\"
- " --field auto_inactive=false \\"
# The create body is assembled as JSON and piped in via --input so
# required_contexts is a real, explicit empty array. An empty array
# means "no required contexts"; sending the literal string "[]"
# (via --field/--raw-field) made GitHub reject the create with 422.
- ' deployment_id=$(printf ''{"ref":"%s","environment":"%s","auto_merge":false,"required_contexts":[],"auto_inactive":false}'' "${{ github.sha }}" "$ENV_NAME" \'
- " | gh api repos/${{ github.repository }}/deployments \\"
- " --input - \\"
- " - name: Set deployment in_progress"
- " --field state=in_progress"
- " - name: Set deployment status"
- " if: ${{ github.server_url == 'https://github.com' && github.event.inputs.dry_run != 'true' && always() }}"
- " production) environment_url='https://app.example.com' ;;"
not_contains:
# The string form GitHub rejected with HTTP 422 must never reappear.
- "--raw-field required_contexts='[]'"
- "required_contexts='[]'"

- name: "Regenerate and confirm no drift"
action: verify
Expand Down
20 changes: 11 additions & 9 deletions e2e/scenarios/57-component-partial-inherit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ description: |
enabled defaulting back to false: api's orchestrate workflow would have emitted
no deployment reporting at all. Under deep-merge api keeps the inherited
enabled: true and layers its keep_prior_active on top, so its finalize job
reports the deployment with auto_inactive=false, while web inherits the plain
enabled: true and reports with the default auto_inactive=true. The scenario
reports the deployment with auto_inactive false, while web inherits the plain
enabled: true and reports with the default auto_inactive true. The scenario
generates the per-component set, proves the roundtrip is drift-free, and asserts
each component's orchestrate workflow carries the correctly merged deployment
reporting rather than the sibling's.
Expand Down Expand Up @@ -70,21 +70,23 @@ steps:
expect_exit: 0
# api set only keep_prior_active; it inherits the top-level enabled: true, so
# its orchestrate workflow still creates a deployment and, because
# keep_prior_active merged in, reports it with auto_inactive=false. web
# keep_prior_active merged in, reports it with auto_inactive false. web
# inherits the bare enabled: true and reports with the default
# auto_inactive=true. The not_contains cross-checks that neither component
# picked up the other's auto_inactive value.
# auto_inactive true. auto_inactive rides in the create-deployment JSON body
# ("auto_inactive":<bool>), so the assertions match that form. The
# not_contains cross-checks that neither component picked up the other's
# auto_inactive value.
expect:
workflow_files:
- path: ".github/workflows/orchestrate-api.yaml"
contains:
- "Create deployment"
- "auto_inactive=false"
- '"auto_inactive":false'
not_contains:
- "auto_inactive=true"
- '"auto_inactive":true'
- path: ".github/workflows/orchestrate-web.yaml"
contains:
- "Create deployment"
- "auto_inactive=true"
- '"auto_inactive":true'
not_contains:
- "auto_inactive=false"
- '"auto_inactive":false'
20 changes: 12 additions & 8 deletions internal/generate/native_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,18 @@ func writeNativeDeploymentSteps(sb *strings.Builder, cfg *config.TrunkConfig, en
sb.WriteString(body + " GH_TOKEN: ${{ github.token }}\n")
sb.WriteString(body + "run: |\n")
fmt.Fprintf(sb, "%s ENV_NAME=\"%s\"\n", body, envExpr)
fmt.Fprintf(sb, "%s deployment_id=$(gh api repos/${{ github.repository }}/deployments \\\n", body)
sb.WriteString(body + " --method POST \\\n")
sb.WriteString(body + " --field ref=${{ github.sha }} \\\n")
sb.WriteString(body + " --field environment=\"$ENV_NAME\" \\\n")
sb.WriteString(body + " --field auto_merge=false \\\n")
sb.WriteString(body + " --raw-field required_contexts='[]' \\\n")
fmt.Fprintf(sb, "%s --field auto_inactive=%t \\\n", body, deploymentAutoInactive(cfg))
sb.WriteString(body + " --jq '.id')\n")
// Assemble the create-deployment request body as JSON and pipe it in via
// --input so required_contexts is a real empty array. An explicit [] means
// the deployment is not gated on any status contexts; omitting the field
// would instead make GitHub apply the repository's default required
// contexts, a different and potentially blocking behavior. gh api --field
// and --raw-field always send scalar strings, so an empty JSON array cannot
// be expressed through them; the body is built here and read from stdin.
fmt.Fprintf(sb, "%s deployment_id=$(printf '{\"ref\":\"%%s\",\"environment\":\"%%s\",\"auto_merge\":false,\"required_contexts\":[],\"auto_inactive\":%t}' \"${{ github.sha }}\" \"$ENV_NAME\" \\\n", body, deploymentAutoInactive(cfg))
sb.WriteString(body + " | gh api repos/${{ github.repository }}/deployments \\\n")
sb.WriteString(body + " --method POST \\\n")
sb.WriteString(body + " --input - \\\n")
sb.WriteString(body + " --jq '.id')\n")
sb.WriteString(body + " echo \"deployment_id=${deployment_id}\" >> \"$GITHUB_OUTPUT\"\n")

// Mark the deployment in_progress.
Expand Down
23 changes: 23 additions & 0 deletions internal/generate/native_deployments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ func TestNativeDeployments_Enabled(t *testing.T) {
"terminal status must derive from the deploy job result")
}

// TestNativeDeployments_RequiredContextsIsJSONArray proves the create-deployment
// step sends required_contexts as a real JSON array, not the literal string
// "[]". gh api --field/--raw-field always transmit scalar strings, so the prior
// `--raw-field required_contexts='[]'` made GitHub receive "[]" and reject the
// create with HTTP 422 (not an array or null). The explicit empty array must be
// preserved, not dropped: omitting the field would let GitHub apply the repo's
// default required contexts instead of "no required contexts".
func TestNativeDeployments_RequiredContextsIsJSONArray(t *testing.T) {
cfg, tmpDir := nativeDeploymentsConfig(t)

out, err := NewGenerator(cfg, tmpDir).Generate()
require.NoError(t, err)

assert.NotContains(t, out, "--raw-field required_contexts='[]'",
"required_contexts must not be sent via --raw-field (transmits the literal string \"[]\", which GitHub rejects with HTTP 422)")
assert.NotContains(t, out, "required_contexts='[]'",
"required_contexts must not be sent as a shell string in any form")
assert.Contains(t, out, `"required_contexts":[]`,
"required_contexts must be sent as a real, explicit empty JSON array")
assert.Contains(t, out, "--input -",
"the create-deployment body must be piped in as JSON via --input so the empty array survives")
}

// TestNativeDeployments_Disabled proves none of the Deployments API wiring is
// emitted when the toggle is absent, keeping the OFF-state output unchanged.
func TestNativeDeployments_Disabled(t *testing.T) {
Expand Down