Skip to content

Add Temporal-backed Java SDK release automation - #2986

Draft
eamsden wants to merge 5 commits into
mainfrom
eamsden/temporal-release-automation
Draft

Add Temporal-backed Java SDK release automation#2986
eamsden wants to merge 5 commits into
mainfrom
eamsden/temporal-release-automation

Conversation

@eamsden

@eamsden eamsden commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What changed?

  • Added an opinionated Java SDK Temporal orchestrator under .github/release-automation, outside the SDK publication set.
  • Froze each candidate as repository, tag/version, full source SHA, release-note path/hash, protected automation SHA, and one fixed sdk-java Maven profile. The release identity additionally freezes the exact six-object native manifest.
  • Added immutable-identity-derived Workflow/build queues and generation-specific publication queues, with transient GitHub Actions Workers that poll only one release queue.
  • Split privileged work into durable PREFLIGHT, MAVEN, GITHUB_DRAFT, and PUBLISH_GITHUB phases with status, receipts, Workflow-owned backoff, pause/resume, and permanent handoff.
  • Added a zero-input approval issue showing the exact release identity. Its node, creator, body hash, Workflow/run, and protected automation commit are persisted in an immutable S3 request receipt; scheduled recovery handles close-before-binding and runner-loss races.
  • Added protected Maven generation authorization, append-only generation/repository/Portal receipts, a content-addressed signed Maven payload, exact staged/Central byte reconciliation, and Publisher Portal lifecycle reconciliation.
  • Made GitHub publication draft-first: create/adopt the exact draft, reconcile and checksum six native archives plus SHA256SUMS, and publish as the final mutation.
  • Retained two recovery paths:
    • a durable automated emergency controller with S3 compare-and-swap state and recovery even when Temporal is unavailable;
    • an independent dispatch-only prepare-release.yml fallback 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

  1. A release-note push—or scheduled default-branch discovery for an older maintenance branch—creates a protected, create-if-absent candidate receipt before starting CandidateWorkflow.
  2. Six release-specific build queues create or adopt checksum-verified S3 objects. The candidate freezes their manifest and waits for Temporal to acknowledge ReleaseWorkflow start.
  3. The release waits indefinitely in AWAITING_APPROVAL. An active release manager approves by closing the exact locked issue; no text or time window is involved.
  4. Publication credentials appear only in 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.
  5. Each Activity performs one reconciliation attempt. Temporal Workflow timers own durable retry/backoff and scheduled Actions jobs automatically restore Worker capacity after runner loss.
  6. Both mutation paths use the repository-wide sdk-java-release-publication lock, 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

  • Approval is non-expiring and zero-input. Closing the issue before its first Update is safe: scheduled recovery adopts the same closed bot-created issue and immutable request receipt.
  • Activities heartbeat every minute, and pause/handoff waits for cancellation acknowledgement while terminating the subprocess tree.
  • Native retries first validate and adopt an existing immutable object. Emergency replacement is one explicit generation and cannot mix build attempts.
  • Every Maven generation has immutable intent, frozen-payload, repository-ID, and Portal-deployment receipts. Retries download the exact signed bytes, validate the allowlisted sdk-java coordinate/file set, upload only missing bytes, and reconcile the exact Portal deployment through PUBLISHED.
  • Existing Central coordinates are accepted only when all frozen files and signatures match and every POM contains the exact group/artifact/version/full source SHA.
  • Existing Git tags/releases/assets are checked before Maven mutation. GitHub publication adopts only exact metadata/assets, removes only safe zero-byte interrupted starters from a draft, and makes the release public last.
  • Retry authorization authenticates the manager before creating its immutable receipt. Any live Portal state vetoes a new generation. The generation is part of the publication Task Queue, so a stale runner cannot consume newly authorized work.

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.yml and dispatchable build-native-image.yml through 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_COMPLETE when a Workflow exists.

The independent fallback offers only inspect and resume. 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

  • Set RELEASE_AUTOMATION_REF to this reviewed full commit SHA. Do not rotate it while a candidate/release using the prior pin remains open.
  • Configure TEMPORAL_RELEASE_ADDRESS, TEMPORAL_RELEASE_NAMESPACE, and separate narrowly scoped unprivileged, approval/control, and publication API keys.
  • Configure a private versioned/object-locked RELEASE_ARTIFACT_BUCKET and separate build, release-control receipt, and publication OIDC roles (RELEASE_ARTIFACT_ROLE_ARN, RELEASE_CONTROL_ARTIFACT_ROLE_ARN, and RELEASE_PUBLICATION_ARTIFACT_ROLE_ARN). Restrict trust to this repository, exact workflows/environments, and refs/heads/main.
  • Create protected release-control and release-publication environments. Do not add an expiring approval timer.
  • Create 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.
  • Configure JAR_SIGNING_KEY, JAR_SIGNING_KEY_ID, JAR_SIGNING_KEY_PASSWORD, RH_USER, and RH_PASSWORD only in release-publication.
  • Protect release workflows/fixed Gradle hooks with CODEOWNERS and 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.
  • Focused publication-model check — required Nexus tasks exist and all 17 generated POMs have exact io.temporal coordinates, requested version, and full scm.tag SHA.
  • bash -n for the independent and Temporal publication scripts.
  • Ruby/Psych parsing for all workflow YAML.
  • actionlint v1.7.12 for all workflows, filtering only its stale schema diagnostic for the repository's concurrency.queue extension.
  • 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.

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.
Comment thread .github/workflows/prepare-release.yml Outdated
Comment on lines +61 to +63
run: >-
./gradlew -p .github/release-automation run
--args="inspect ${{ inputs.tag }} ${{ inputs.commit_sha }}"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment thread .github/workflows/prepare-release.yml Outdated
Comment on lines +96 to +98
run: >-
./gradlew -p .github/release-automation run
--args="emergency-inspect-export ${{ inputs.tag }} ${{ inputs.commit_sha }}"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment thread .github/workflows/prepare-release.yml Outdated
Comment on lines +150 to +152
run: >-
./gradlew -p .github/release-automation run
--args="control handoff-manual ${{ inputs.tag }} ${{ inputs.commit_sha }}"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment thread .github/workflows/prepare-release.yml Outdated
Comment on lines +159 to +161
run: >-
./gradlew -p .github/release-automation run
--args="emergency-export ${{ inputs.tag }} ${{ inputs.commit_sha }}"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +60 to +62
run: >-
./gradlew -p .github/release-automation run
--args="inspect ${{ inputs.tag }} ${{ inputs.commit_sha }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +78 to +80
run: >-
./gradlew -p .github/release-automation run
--args="control ${{ inputs.action }} ${{ inputs.tag }} ${{ inputs.commit_sha }}"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines 191 to +204
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"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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.
Comment on lines +111 to +114
run: >-
./gradlew -p .github/release-automation run
--args="control ${{ inputs.action }} ${{ inputs.tag }} ${{ inputs.commit_sha }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +155 to +157
run: >-
./gradlew -p .github/release-automation run
--args="publication-input ${{ inputs.tag }} ${{ inputs.commit_sha }} ${{ runner.temp }}/publication-input.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +193 to +235
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"

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +245 to +247
run: >-
./gradlew -p .github/release-automation run
--args="control retry-maven-submission ${{ inputs.tag }} ${{ inputs.commit_sha }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +285 to +298
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +355 to +425
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

@github-actions github-actions Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +200 to +242
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Comment on lines +365 to +436
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opengrepsecurity.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant