Add Temporal-backed Java SDK release automation - #2986
Conversation
Orchestrate immutable sdk-java release candidates with transient GitHub Actions Workers, a no-input approval Update, privileged publication queues, and exact-state external reconciliation.
Make release progress explicitly staged and controllable, bind approval to a locked release-specific GitHub issue, and make Maven and GitHub publication reconciliation safe across ambiguous retries. Add a durable emergency handoff path, frozen Worker routing, prompt Activity cancellation, idempotent native builds, and focused local tests.
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="inspect ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in a0f9655
Fixed in 5a7e272
Fixed in 759f215
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="emergency-inspect-export ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in a0f9655
Fixed in 5a7e272
Fixed in 759f215
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="control handoff-manual ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in a0f9655
Fixed in 5a7e272
Fixed in 759f215
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="emergency-export ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in a0f9655
Fixed in 5a7e272
Fixed in 759f215
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="inspect ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="control ${{ inputs.action }} ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in 5a7e272
Fixed in 759f215
| run: | | ||
| version="$(sed 's/^v//'<<<"$INPUT_TAG")" | ||
| for dir in *; do mv "$dir" "temporal-test-server_${version}_${dir}"; done | ||
|
|
||
| - name: Tar (linux, macOS) | ||
| run: for dir in *{linux,macOS}*; do tar cvzf "${dir}.tar.gz" "$dir"; done | ||
|
|
||
| - name: Zip (windows) | ||
| run: for dir in *windows*; do zip -r "${dir}.zip" "$dir"; done | ||
|
|
||
| - name: Upload release archives | ||
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 | ||
| with: | ||
| name: release-archives | ||
| path: | | ||
| *.zip | ||
| *.tar.gz | ||
| if-no-files-found: error | ||
| retention-days: 1 | ||
|
|
||
| - name: Upload | ||
| bash "$TRUSTED_AUTOMATION_ROOT/.github/scripts/temporal-release/reconcile-publication.sh" | ||
| { | ||
| echo "## Exact emergency release inspection" | ||
| echo | ||
| echo "- Tag: \`${{ inputs.tag }}\`" | ||
| echo "- Full SHA: \`${{ inputs.commit_sha }}\`" | ||
| echo "- Release digest: \`${{ steps.identity.outputs.release_digest }}\`" | ||
| echo "- Manifest SHA-256: \`${{ steps.identity.outputs.manifest_sha256 }}\`" | ||
| echo | ||
| echo '```json' | ||
| jq . "$RELEASE_OUTPUT_FILE" | ||
| echo '```' | ||
| } >>"$GITHUB_STEP_SUMMARY" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in 5a7e272
Fixed in 759f215
Harden the Temporal state machine, publication reconciliation, approval recovery, and transient Worker behavior. Add protected Maven generation authorization, draft-first exact asset handling, maintenance-branch recovery, and both automated and independent emergency paths.
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="control ${{ inputs.action }} ${{ inputs.tag }} ${{ inputs.commit_sha }}" | ||
|
|
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="publication-input ${{ inputs.tag }} ${{ inputs.commit_sha }} ${{ runner.temp }}/publication-input.json" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: | | ||
| current=$(jq -c --argjson generation "$CURRENT_GENERATION" \ | ||
| '.mavenGenerations[]? | select(.generation == $generation)' \ | ||
| "$RUNNER_TEMP/fresh-inspection.json") | ||
| [[ $(jq -r .mavenPresent "$RUNNER_TEMP/fresh-inspection.json") == 0 ]] | ||
| if [[ -n "$current" ]]; then | ||
| repository_state=$(jq -r .repositoryState <<<"$current") | ||
| portal_state=$(jq -r .portalDeploymentState <<<"$current") | ||
| [[ "$repository_state" == absent || "$repository_state" == unavailable || | ||
| "$portal_state" == FAILED ]] | ||
| fi | ||
| generation=$((CURRENT_GENERATION + 1)) | ||
| receipt="$RUNNER_TEMP/maven-retry-authorization.json" | ||
| jq -cn --arg repository temporalio/sdk-java --arg tag '${{ inputs.tag }}' \ | ||
| --arg commitSha '${{ inputs.commit_sha }}' --arg releaseDigest "$RELEASE_DIGEST" \ | ||
| --arg workflowId "$WORKFLOW_ID" --arg runId "$RUN_ID" \ | ||
| --arg githubActor "$GITHUB_TRIGGERING_ACTOR" --argjson githubRunId "$GITHUB_RUN_ID" \ | ||
| --argjson authorizedGeneration "$generation" \ | ||
| '{repository:$repository,tag:$tag,commitSha:$commitSha, | ||
| releaseDigest:$releaseDigest,workflowId:$workflowId,runId:$runId, | ||
| githubActor:$githubActor,githubRunId:$githubRunId, | ||
| authorizedGeneration:$authorizedGeneration,freshInspection:true}' >"$receipt" | ||
| key="sdk-java/$RELEASE_DIGEST/state/maven/retry-authorizations/$generation.json" | ||
| if ! aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" --key "$key" \ | ||
| --body "$receipt" --if-none-match '*' >/dev/null 2>&1; then | ||
| aws s3 cp "s3://$RELEASE_ARTIFACT_BUCKET/$key" \ | ||
| "$RUNNER_TEMP/existing-authorization.json" --no-progress >/dev/null | ||
| jq -e --arg repository temporalio/sdk-java --arg tag '${{ inputs.tag }}' \ | ||
| --arg commit '${{ inputs.commit_sha }}' --arg digest "$RELEASE_DIGEST" \ | ||
| --arg workflow "$WORKFLOW_ID" --arg run "$RUN_ID" \ | ||
| --argjson generation "$generation" \ | ||
| '.repository == $repository and .tag == $tag and .commitSha == $commit and | ||
| .releaseDigest == $digest and .workflowId == $workflow and .runId == $run and | ||
| .authorizedGeneration == $generation and .freshInspection == true' \ | ||
| "$RUNNER_TEMP/existing-authorization.json" >/dev/null | ||
| cp "$RUNNER_TEMP/existing-authorization.json" "$receipt" | ||
| fi | ||
| { | ||
| echo "generation=$generation" | ||
| echo "sha256=$(sha256sum "$receipt" | awk '{print $1}')" | ||
| echo "actor=$(jq -er .githubActor "$receipt")" | ||
| echo "run_id=$(jq -er .githubRunId "$receipt")" | ||
| } >>"$GITHUB_OUTPUT" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in 759f215
| run: >- | ||
| ./gradlew -p .github/release-automation run | ||
| --args="control retry-maven-submission ${{ inputs.tag }} ${{ inputs.commit_sha }}" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: | | ||
| bash "$TRUSTED_AUTOMATION_ROOT/.github/scripts/temporal-release/reconcile-publication.sh" | ||
| { | ||
| echo "## Exact emergency release inspection" | ||
| echo | ||
| echo "- Tag: \`${{ inputs.tag }}\`" | ||
| echo "- Full SHA: \`${{ inputs.commit_sha }}\`" | ||
| echo "- Release digest: \`${{ steps.identity.outputs.release_digest }}\`" | ||
| echo "- Manifest SHA-256: \`${{ steps.identity.outputs.manifest_sha256 }}\`" | ||
| echo | ||
| echo '```json' | ||
| jq . "$RELEASE_OUTPUT_FILE" | ||
| echo '```' | ||
| } >>"$GITHUB_STEP_SUMMARY" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: | | ||
| [[ $(jq -r .state "$REQUEST_FILE") == BLOCKED && | ||
| ( $(jq -r .blockedReason "$REQUEST_FILE") == MavenSubmissionAmbiguous || | ||
| $(jq -r .blockedReason "$REQUEST_FILE") == MavenDeploymentFailed ) && | ||
| $(jq -r '.mavenPresent' "$RUNNER_TEMP/emergency-inspection.json") == 0 ]] || { | ||
| echo "The emergency release is not blocked on an ambiguous Maven intent." >&2 | ||
| exit 1 | ||
| } | ||
| current_generation=$(jq -r .mavenSubmissionGeneration "$REQUEST_FILE") | ||
| current=$(jq -c --argjson generation "$current_generation" \ | ||
| '.mavenGenerations[]? | select(.generation == $generation)' \ | ||
| "$RUNNER_TEMP/emergency-inspection.json") | ||
| if [[ -n "$current" ]]; then | ||
| repository_state=$(jq -r .repositoryState <<<"$current") | ||
| portal_state=$(jq -r .portalDeploymentState <<<"$current") | ||
| [[ "$repository_state" == absent || "$repository_state" == unavailable || | ||
| "$portal_state" == FAILED ]] || { | ||
| echo "The exact prior repository/deployment still exists; it must be reconciled." >&2 | ||
| exit 1 | ||
| } | ||
| fi | ||
| generation=$((current_generation + 1)) | ||
| authorization="$RUNNER_TEMP/emergency-maven-retry-authorization.json" | ||
| jq -cn --arg repository temporalio/sdk-java \ | ||
| --arg tag '${{ inputs.tag }}' --arg commitSha '${{ inputs.commit_sha }}' \ | ||
| --arg releaseDigest '${{ steps.identity.outputs.release_digest }}' \ | ||
| --arg workflowId '${{ steps.identity.outputs.workflow_id }}' \ | ||
| --arg runId '${{ steps.identity.outputs.run_id }}' \ | ||
| --arg githubActor "$GITHUB_TRIGGERING_ACTOR" --argjson githubRunId "$GITHUB_RUN_ID" \ | ||
| --argjson authorizedGeneration "$generation" \ | ||
| '{repository:$repository,tag:$tag,commitSha:$commitSha, | ||
| releaseDigest:$releaseDigest,workflowId:$workflowId,runId:$runId, | ||
| githubActor:$githubActor,githubRunId:$githubRunId, | ||
| authorizedGeneration:$authorizedGeneration,freshInspection:true}' >"$authorization" | ||
| authorization_key="sdk-java/${{ steps.identity.outputs.release_digest }}/state/maven/retry-authorizations/$generation.json" | ||
| if ! aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" \ | ||
| --key "$authorization_key" --body "$authorization" --if-none-match '*' \ | ||
| >/dev/null 2>&1; then | ||
| aws s3 cp "s3://$RELEASE_ARTIFACT_BUCKET/$authorization_key" \ | ||
| "$RUNNER_TEMP/existing-emergency-authorization.json" --no-progress >/dev/null | ||
| jq -e --arg tag '${{ inputs.tag }}' --arg commit '${{ inputs.commit_sha }}' \ | ||
| --arg digest '${{ steps.identity.outputs.release_digest }}' \ | ||
| --argjson generation "$generation" \ | ||
| '.repository == "temporalio/sdk-java" and .tag == $tag and | ||
| .commitSha == $commit and .releaseDigest == $digest and | ||
| .authorizedGeneration == $generation and .freshInspection == true' \ | ||
| "$RUNNER_TEMP/existing-emergency-authorization.json" >/dev/null | ||
| cp "$RUNNER_TEMP/existing-emergency-authorization.json" "$authorization" | ||
| fi | ||
| authorization_sha256=$(sha256sum "$authorization" | awk '{print $1}') | ||
| authorization_actor=$(jq -er .githubActor "$authorization") | ||
| authorization_run=$(jq -er .githubRunId "$authorization") | ||
| jq --arg actor "$GITHUB_TRIGGERING_ACTOR" --argjson run "$GITHUB_RUN_ID" \ | ||
| --arg authorizationActor "$authorization_actor" \ | ||
| --argjson authorizationRun "$authorization_run" \ | ||
| --arg authorizationSha256 "$authorization_sha256" \ | ||
| --argjson generation "$generation" \ | ||
| --arg recordedAt "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | ||
| '.state="READY" | .mavenSubmissionGeneration=$generation | | ||
| .mavenRetryAuthorizedBy=$actor | .mavenRetryAuthorizationRunId=$run | | ||
| .mavenRetryAuthorizationActor=$authorizationActor | | ||
| .mavenRetryAuthorizationRunId=$authorizationRun | | ||
| .mavenRetryAuthorizationSha256=$authorizationSha256 | | ||
| .approvedBy=$actor | .approvalRunId=$run | | ||
| .handoff={actor:$actor,tag:.candidate.tag,commitSha:.candidate.commitSha, | ||
| reason:"Release manager transferred ownership to emergency automation.", | ||
| recordedAt:$recordedAt,githubRunId:$run} | del(.blockedReason)' \ | ||
| "$REQUEST_FILE" >"$RUNNER_TEMP/updated-request.json" | ||
| aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" --key "$REQUEST_KEY" \ | ||
| --body "$RUNNER_TEMP/updated-request.json" --if-match '${{ steps.request.outputs.etag }}' \ | ||
| >/dev/null |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Fixed in 759f215
| run: | | ||
| current=$(jq -c --argjson generation "$CURRENT_GENERATION" \ | ||
| '.mavenGenerations[]? | select(.generation == $generation)' \ | ||
| "$RUNNER_TEMP/fresh-inspection.json") | ||
| [[ $(jq -r .mavenPresent "$RUNNER_TEMP/fresh-inspection.json") == 0 ]] | ||
| if [[ -n "$current" ]]; then | ||
| repository_state=$(jq -r .repositoryState <<<"$current") | ||
| portal_state=$(jq -r .portalDeploymentState <<<"$current") | ||
| [[ ( "$repository_state" == absent || "$repository_state" == unavailable ) && | ||
| ( -z "$portal_state" || "$portal_state" == FAILED ) ]] | ||
| fi | ||
| generation=$((CURRENT_GENERATION + 1)) | ||
| receipt="$RUNNER_TEMP/maven-retry-authorization.json" | ||
| jq -cn --arg repository temporalio/sdk-java --arg tag '${{ inputs.tag }}' \ | ||
| --arg commitSha '${{ inputs.commit_sha }}' --arg releaseDigest "$RELEASE_DIGEST" \ | ||
| --arg workflowId "$WORKFLOW_ID" --arg runId "$RUN_ID" \ | ||
| --arg githubActor "$GITHUB_TRIGGERING_ACTOR" --argjson githubRunId "$GITHUB_RUN_ID" \ | ||
| --argjson authorizedGeneration "$generation" \ | ||
| '{repository:$repository,tag:$tag,commitSha:$commitSha, | ||
| releaseDigest:$releaseDigest,workflowId:$workflowId,runId:$runId, | ||
| githubActor:$githubActor,githubRunId:$githubRunId, | ||
| authorizedGeneration:$authorizedGeneration,freshInspection:true}' >"$receipt" | ||
| key="sdk-java/$RELEASE_DIGEST/state/maven/retry-authorizations/$generation.json" | ||
| if ! aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" --key "$key" \ | ||
| --body "$receipt" --if-none-match '*' >/dev/null 2>&1; then | ||
| aws s3 cp "s3://$RELEASE_ARTIFACT_BUCKET/$key" \ | ||
| "$RUNNER_TEMP/existing-authorization.json" --no-progress >/dev/null | ||
| jq -e --arg repository temporalio/sdk-java --arg tag '${{ inputs.tag }}' \ | ||
| --arg commit '${{ inputs.commit_sha }}' --arg digest "$RELEASE_DIGEST" \ | ||
| --arg workflow "$WORKFLOW_ID" --arg run "$RUN_ID" \ | ||
| --argjson generation "$generation" \ | ||
| '.repository == $repository and .tag == $tag and .commitSha == $commit and | ||
| .releaseDigest == $digest and .workflowId == $workflow and .runId == $run and | ||
| .authorizedGeneration == $generation and .freshInspection == true' \ | ||
| "$RUNNER_TEMP/existing-authorization.json" >/dev/null | ||
| cp "$RUNNER_TEMP/existing-authorization.json" "$receipt" | ||
| fi | ||
| { | ||
| echo "generation=$generation" | ||
| echo "sha256=$(sha256sum "$receipt" | awk '{print $1}')" | ||
| echo "actor=$(jq -er .githubActor "$receipt")" | ||
| echo "run_id=$(jq -er .githubRunId "$receipt")" | ||
| } >>"$GITHUB_OUTPUT" |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
| run: | | ||
| [[ $(jq -r .state "$REQUEST_FILE") == BLOCKED && | ||
| ( $(jq -r .blockedReason "$REQUEST_FILE") == MavenSubmissionAmbiguous || | ||
| $(jq -r .blockedReason "$REQUEST_FILE") == MavenDeploymentFailed ) && | ||
| $(jq -r '.mavenPresent' "$RUNNER_TEMP/emergency-inspection.json") == 0 ]] || { | ||
| echo "The emergency release is not blocked on an ambiguous Maven intent." >&2 | ||
| exit 1 | ||
| } | ||
| current_generation=$(jq -r .mavenSubmissionGeneration \ | ||
| "$RUNNER_TEMP/emergency-inspection.json") | ||
| current=$(jq -c --argjson generation "$current_generation" \ | ||
| '.mavenGenerations[]? | select(.generation == $generation)' \ | ||
| "$RUNNER_TEMP/emergency-inspection.json") | ||
| if [[ -n "$current" ]]; then | ||
| repository_state=$(jq -r .repositoryState <<<"$current") | ||
| portal_state=$(jq -r .portalDeploymentState <<<"$current") | ||
| [[ ( "$repository_state" == absent || "$repository_state" == unavailable ) && | ||
| ( -z "$portal_state" || "$portal_state" == FAILED ) ]] || { | ||
| echo "The exact prior repository/deployment still exists; it must be reconciled." >&2 | ||
| exit 1 | ||
| } | ||
| fi | ||
| generation=$((current_generation + 1)) | ||
| authorization="$RUNNER_TEMP/emergency-maven-retry-authorization.json" | ||
| jq -cn --arg repository temporalio/sdk-java \ | ||
| --arg tag '${{ inputs.tag }}' --arg commitSha '${{ inputs.commit_sha }}' \ | ||
| --arg releaseDigest '${{ steps.identity.outputs.release_digest }}' \ | ||
| --arg workflowId '${{ steps.identity.outputs.workflow_id }}' \ | ||
| --arg runId '${{ steps.identity.outputs.run_id }}' \ | ||
| --arg githubActor "$GITHUB_TRIGGERING_ACTOR" --argjson githubRunId "$GITHUB_RUN_ID" \ | ||
| --argjson authorizedGeneration "$generation" \ | ||
| '{repository:$repository,tag:$tag,commitSha:$commitSha, | ||
| releaseDigest:$releaseDigest,workflowId:$workflowId,runId:$runId, | ||
| githubActor:$githubActor,githubRunId:$githubRunId, | ||
| authorizedGeneration:$authorizedGeneration,freshInspection:true}' >"$authorization" | ||
| authorization_key="sdk-java/${{ steps.identity.outputs.release_digest }}/state/maven/retry-authorizations/$generation.json" | ||
| if ! aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" \ | ||
| --key "$authorization_key" --body "$authorization" --if-none-match '*' \ | ||
| >/dev/null 2>&1; then | ||
| aws s3 cp "s3://$RELEASE_ARTIFACT_BUCKET/$authorization_key" \ | ||
| "$RUNNER_TEMP/existing-emergency-authorization.json" --no-progress >/dev/null | ||
| jq -e --arg tag '${{ inputs.tag }}' --arg commit '${{ inputs.commit_sha }}' \ | ||
| --arg digest '${{ steps.identity.outputs.release_digest }}' \ | ||
| --argjson generation "$generation" \ | ||
| '.repository == "temporalio/sdk-java" and .tag == $tag and | ||
| .commitSha == $commit and .releaseDigest == $digest and | ||
| .authorizedGeneration == $generation and .freshInspection == true' \ | ||
| "$RUNNER_TEMP/existing-emergency-authorization.json" >/dev/null | ||
| cp "$RUNNER_TEMP/existing-emergency-authorization.json" "$authorization" | ||
| fi | ||
| authorization_sha256=$(sha256sum "$authorization" | awk '{print $1}') | ||
| authorization_actor=$(jq -er .githubActor "$authorization") | ||
| authorization_run=$(jq -er .githubRunId "$authorization") | ||
| jq --arg actor "$GITHUB_TRIGGERING_ACTOR" --argjson run "$GITHUB_RUN_ID" \ | ||
| --arg authorizationActor "$authorization_actor" \ | ||
| --argjson authorizationRun "$authorization_run" \ | ||
| --arg authorizationSha256 "$authorization_sha256" \ | ||
| --argjson generation "$generation" \ | ||
| --arg recordedAt "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | ||
| '.state="READY" | .mavenSubmissionGeneration=$generation | | ||
| .mavenRetryAuthorizedBy=$actor | .mavenRetryAuthorizationRunId=$run | | ||
| .mavenRetryAuthorizationActor=$authorizationActor | | ||
| .mavenRetryAuthorizationRunId=$authorizationRun | | ||
| .mavenRetryAuthorizationSha256=$authorizationSha256 | | ||
| .approvedBy=$actor | .approvalRunId=$run | | ||
| .handoff={actor:$actor,tag:.candidate.tag,commitSha:.candidate.commitSha, | ||
| reason:"Release manager transferred ownership to emergency automation.", | ||
| recordedAt:$recordedAt,githubRunId:$run} | del(.blockedReason)' \ | ||
| "$REQUEST_FILE" >"$RUNNER_TEMP/updated-request.json" | ||
| aws s3api put-object --bucket "$RELEASE_ARTIFACT_BUCKET" --key "$REQUEST_KEY" \ | ||
| --body "$RUNNER_TEMP/updated-request.json" --if-match '${{ steps.request.outputs.etag }}' \ | ||
| >/dev/null |
There was a problem hiding this comment.
Opengrep — security.gha.run-shell-injection-inputs (WARNING)
A composite-action / reusable-workflow / workflow_dispatch inputs.* value is interpolated directly into this run/script block. Inputs are supplied by the caller, who may pass attacker-controlled data into them — e.g. a caller invoking this with version: ${{ github.event.pull_request.head.ref }} (a fork branch name). Because the input value is substituted before the shell runs, the callee cannot trust it. Defensive fix: copy the input into a step-level env: entry (e.g. VERSION: ${{ inputs.version }}) and reference the quoted environment variable in the script instead (echo "$VERSION"). This is advisory (WARNING): not every input carries untrusted data, but routing inputs through env: makes the component safe regardless of how callers use it. If an input appears only as a condition selecting between hard-coded string literals (e.g. ${{ inputs.flag && 'a' || 'b' }}), the emitted value is constant and that specific use is not exploitable — but hoist the condition into bash (set a boolean env: var and branch with if) so the run line carries no ${{ }} and the safety stays local and stable. Directly-untrusted github.* contexts are covered separately by security.gha.run-shell-injection.
Suppress this finding
Add a suppression comment on the line before:
# noopengrep: security.gha.run-shell-injection-inputs
What changed?
.github/release-automation, outside the SDK publication set.PREFLIGHT,MAVEN,GITHUB_DRAFT, andPUBLISH_GITHUBphases with status, receipts, Workflow-owned backoff, pause/resume, and permanent handoff.SHA256SUMS, and publish as the final mutation.prepare-release.ymlfallback whose fixed shell implementation does not depend on Temporal Java or S3 control state.Why?
The Actions-only process loses orchestration state with a runner and cannot safely distinguish an ordinary retry from ambiguous external side effects. Temporal Cloud retains orchestration and retry state between jobs, while immutable identities, artifacts, and mutation receipts stay in external storage. Publication credentials remain isolated in protected publication jobs and are released only after approval.
Architecture and security boundary
CandidateWorkflow.ReleaseWorkflowstart.AWAITING_APPROVAL. An active release manager approves by closing the exact locked issue; no text or time window is involved.release-publication. Before source checkout and again inside each Activity, protected code validates the candidate/start receipts, Workflow ID/run ID, repository, tag, full SHA, notes digest, manifest, approval receipt/issue/actor, protected automation SHA, Maven generation, generation-specific queue, and any retry authorization.sdk-java-release-publicationlock, active release-manager authorization, exact tag/SHA ownership, exact Maven identity, and publish-last GitHub behavior.Task Queues are routing, not authorization. Temporal memo state cannot choose privileged code: discovery accepts only identities naming the protected
RELEASE_AUTOMATION_REF, and privileged jobs check out only that reviewed full SHA. Candidate source checkouts do not retain GitHub credentials. Source Gradle receives signing/Central credentials only during Maven work; GitHub and AWS credentials are removed from its environment.Temporal Cloud data-plane roles do not separate Workflow polling/starting from Update submission. This limitation is explicitly accepted: a compromised data-plane key can disrupt orchestration, but it cannot select publication code or independently authorize publication. Approval and Maven-generation evidence are independently re-proven against protected GitHub/S3 state and active team membership.
Approval, retries, and exact-state reconciliation
PUBLISHED.The Sonatype compatibility API cannot atomically create a staging repository and persist its returned ID externally. A runner can die in that narrow interval. Per Edward's direction, this infeasibility is accepted: automation adopts the exact generation description if visible, otherwise blocks for release-manager inspection and protected generation authorization rather than inferring safety from Central absence.
Both-path operating model
The Temporal path is the only release-note push trigger. Keep the independent
prepare-release.ymland dispatchablebuild-native-image.ymlthrough the first supervised release and postmortem.The automated emergency controller preserves the highest observed Maven generation, refuses artifact replacement after Maven evidence exists, fences recovered Temporal Workers through durable ownership, and can finish while Temporal is unavailable. It records completion in S3 and later synchronizes
MANUAL_COMPLETEwhen a Workflow exists.The independent fallback offers only
inspectandresume. It uses a fixed-body, publication-bot-created locked ownership issue bound to the exact tag/SHA, protected controller, authenticated manager, and owning Actions run. Recovery always downloads that run's retained native artifacts, scans both manual and Temporal Sonatype repositories, validates the fixed signed Maven payload locally before mutation, adopts only exact drafts/public releases, and publishes last. It stops rather than guessing when an active repository cannot be identified safely.After one successful supervised release, perform a postmortem and explicitly decide whether to retain either emergency path.
Breaking changes?
No public SDK API changes. Operationally, this remains disabled until the protected environments, credentials, OIDC roles, storage policy, tag rules, and reviewed automation pin below are configured.
Server PR
N/A.
Configuration remaining before enablement
RELEASE_AUTOMATION_REFto this reviewed full commit SHA. Do not rotate it while a candidate/release using the prior pin remains open.TEMPORAL_RELEASE_ADDRESS,TEMPORAL_RELEASE_NAMESPACE, and separate narrowly scoped unprivileged, approval/control, and publication API keys.RELEASE_ARTIFACT_BUCKETand separate build, release-control receipt, and publication OIDC roles (RELEASE_ARTIFACT_ROLE_ARN,RELEASE_CONTROL_ARTIFACT_ROLE_ARN, andRELEASE_PUBLICATION_ARTIFACT_ROLE_ARN). Restrict trust to this repository, exact workflows/environments, andrefs/heads/main.release-controlandrelease-publicationenvironments. Do not add an expiring approval timer.sdk-java-release-managers. The approval token needs team-membership, Actions-run, and issue read/create/lock access. The publication token needs Actions/issue read, manual ownership issue create/lock, and tag/release/asset writes.JAR_SIGNING_KEY,JAR_SIGNING_KEY_ID,JAR_SIGNING_KEY_PASSWORD,RH_USER, andRH_PASSWORDonly inrelease-publication.refs/tags/v*with a ruleset forbidding update/deletion after creation; the publication token must not bypass it.Validation
./gradlew -p .github/release-automation spotlessApply test spotlessCheck— 17 local/mocked tests passed.io.temporalcoordinates, requested version, and fullscm.tagSHA.bash -nfor the independent and Temporal publication scripts.actionlintv1.7.12 for all workflows, filtering only its stale schema diagnostic for the repository'sconcurrency.queueextension.git diff --check.No Temporal Cloud Workflow, release, test release, Maven publication, tag push, or GitHub release creation was performed. No private documentation links are included; setup and operations are documented in
.github/release-automation/README.md.