Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7c76376
Add Temporal-backed release automation
eamsden Jul 31, 2026
a6cb4ce
Fix release automation handoff safety
eamsden Aug 1, 2026
a0f9655
Harden Temporal release recovery
eamsden Aug 1, 2026
5a7e272
Address release automation review findings
eamsden Aug 1, 2026
759f215
Harden release automation trust boundaries
eamsden Aug 1, 2026
45fa42c
Harden Temporal release recovery
eamsden Aug 3, 2026
e84a550
Use setup-java with GraalVM Community support
eamsden Aug 3, 2026
e497bee
Fix native build arguments on macOS
eamsden Aug 3, 2026
94b3c1b
Stabilize native build containers
eamsden Aug 3, 2026
8ef8abc
Build native images on a clean toolchain
eamsden Aug 3, 2026
2368e9b
Align native release builds with Java 23
eamsden Aug 3, 2026
757082f
Normalize Windows paths for trusted Bash
eamsden Aug 3, 2026
577c4b9
Stabilize trusted release toolchains
eamsden Aug 3, 2026
e0fa9f9
Harden release automation recovery boundaries
eamsden Aug 3, 2026
711f3f5
Isolate native release builds
eamsden Aug 3, 2026
bef71b8
Use SDK team for release approvals
eamsden Aug 3, 2026
6935dde
Move release docs to workflow directory
eamsden Aug 3, 2026
a3081df
Remove checked-in release documentation
eamsden Aug 3, 2026
b73198a
Fix release recovery authorization
eamsden Aug 5, 2026
2c92411
Restore workflow documentation
eamsden Aug 5, 2026
2428caa
Remove stale workflow documentation
eamsden Aug 5, 2026
afcac58
Fix Maven retry takeover
eamsden Aug 5, 2026
79c9d42
Simplify release fallback and policy
eamsden Aug 5, 2026
6d703f4
Use SDK environment config for releases
eamsden Aug 6, 2026
765d641
Replace S3 release state with GitHub artifacts
eamsden Aug 6, 2026
2ca3cdc
Harden release takeover and native builds
eamsden Aug 6, 2026
97d0f67
Avoid interpolating release inputs in shell
eamsden Aug 6, 2026
950708b
Isolate manual Maven authorization
eamsden Aug 6, 2026
8b9c351
Use variables for Temporal connection metadata
eamsden Aug 6, 2026
07b1c23
Use a GitHub App for release automation
eamsden Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/release-automation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.gradle/
build/
63 changes: 63 additions & 0 deletions .github/release-automation/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
plugins {
id 'application'
id 'com.diffplug.spotless' version '7.0.2'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'io.temporal:temporal-envconfig'
implementation 'io.temporal:temporal-sdk'
implementation 'com.google.code.gson:gson:2.10.1'
runtimeOnly 'org.slf4j:slf4j-simple:1.7.36'

testImplementation 'io.temporal:temporal-testing'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.14.2'
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

application {
mainClass = 'io.temporal.releaseautomation.ReleaseAutomationMain'
}

tasks.named('run') {
// The trusted scripts live at repository-relative paths. Do not let Gradle's
// application plugin make the standalone build directory the trust root.
systemProperty 'releaseAutomation.repositoryRoot', file('../..').canonicalPath
}

tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.compilerArgs.addAll([
'-Xlint:all',
'-Werror',
'-parameters'
])
}

tasks.withType(Test).configureEach {
systemProperty 'releaseAutomation.repositoryRoot', file('../..').canonicalPath
useJUnit()
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat 'full'
}
}

spotless {
java {
googleJavaFormat('1.28.0')
target 'src/**/*.java'
}
groovyGradle {
greclipse()
target '*.gradle'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM ghcr.io/graalvm/native-image-community:23@sha256:5a28da013d97ac66033b6c836015c7ee7f5d3e07c705d51f1e6c26a04fd17b8e AS graalvm
FROM ubuntu:18.04@sha256:152dc042452c496007f07ca9127571cb9c29697f42acbfad72324b2bb2e43c98
ENV JAVA_HOME=/usr/lib64/graalvm/graalvm-community-java23
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=graalvm ${JAVA_HOME} ${JAVA_HOME}
# Launchpad PPA fingerprint: C8EC952E2A0E1FBDC5090F6A2C277A0A352154E5.
COPY ubuntu-toolchain-r-test.asc /usr/share/keyrings/ubuntu-toolchain-r-test.asc
RUN apt-get update --allow-releaseinfo-change && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates && \
printf '%s\n' \
'deb [signed-by=/usr/share/keyrings/ubuntu-toolchain-r-test.asc] https://ppa.launchpadcontent.net/ubuntu-toolchain-r/test/ubuntu bionic main' \
> /etc/apt/sources.list.d/ubuntu-toolchain-r-test.list && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
git build-essential zlib1g-dev gcc-10 g++-10 && \
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 60 \
--slave /usr/bin/g++ g++ /usr/bin/g++-10 && \
rm -rf /var/lib/apt/lists/*
RUN git config --global --add safe.directory '*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Hostname:
Version: Hockeypuck 2.2

xsFNBGYzeoEBEADEVzm0fPTo1KfunTjKV5ZP0Mb86Z6izCXSYbeYt/iAaIfK7a8x
c13I/uAOxzjcuZHD4sQh7/22I74ignCSexoJD3PnytpZtAxNjyj3jkJpIrdFOVd4
Cmua5tnEI1Av+B21YLlYYftK2wR+Z6hpsz3NyQ8URR1SCX04BOV9AGxcWb7izkV9
RHu6xR4x8kAjuTObhhqw/ZR73+U9mig785qmrtapQkF/IanYj2OofqUJwosDo8YA
shi01735JP9s5do4nnsb6w6xcYQ0HlGn3AxR1k4WA2CK/90QybOmVbLz7IOF3Ou3
cPfoDzSxAiQemge2msJWPaQLSy/HPUivPB2uNPxkcNIHUVwbjgIig0yaN0EOsveg
o1m6QvS8sm1MEA2ySl+9+q8wdqTnXbSgA3hq/5OoG4rvHD9GUAd6oRxbz/wdOU3J
7BlAhEMAHgeQ9DyIzRP9lGeU3QnWNA+WFiiDrLljf+6xewe5iyqLxPqvCkFUDPCF
emp2oIqeWf6l04IMtM4SJz2AnU1EvytprEEvntMvHMXlBBinw1Q5r3ES/QlPfWr4
o5CUpu2P3GpZtflDe7jvccyJUMDtlqOVv7wBZPMZykjTAPQ16clGdtIqWdQSk3/b
UA96RSdpM4gMGXFBGLBoAd7CptafRvPjpRnbMxs2jIxicnASfwJALEFgbQARAQAB
zT9MYXVuY2hwYWQgUFBBIGZvciBQUEEgZm9yIFVidW50dSBUb29sY2hhaW4gVXBs
b2FkcyAocmVzdHJpY3RlZCnCwY4EEwEKADgWIQTI7JUuKg4fvcUJD2osJ3oKNSFU
5QUCZjN6gQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAsJ3oKNSFU5TkT
EACpFJ+q0xqfAmRF1886cgtVw+ruJTqfsxdj7ms1os3/I5VXLhPMbUg56mtK9Okj
0Nf9rY5bnDagJobjtS2JgzMbYWXocPJuwK6ZvmMD5tFSsTov19R3SyIT2x3JAy7S
/LNWqVeZS41RtM8uSu56PhghE7aAP9PigFTI3CUargpx1DQh6xiHKRR57ZMrLcd4
l6tj8yb4Hl+OeFRQMcoim696lPMSqzVtJ0epP7yNsz2LFaRmkRP/hf+S4JlSs8uC
wzAl9eJR2h1v1WoBr0pAW2gjjd8VyKWhaKjneicJPqo5GDvhDl+lbrpqSwSHENlH
DVyC+XvAjvTf4JhTQE/M1L/sGrqqa65n+WF4ZqauiDvn1bUw0vnL0oaiK/hyH1YG
XW7khJ7JDlw/6LFNZ6Ih0UuVRTzVBBBz3Sipw5hgM9ztKEYLMI5zlwWGlhEjMnxD
X5WauIUWjPv3dKQp1qTthN7Fgv3/2J6j+2/tMAfWeWB1tL8H4lVPUCfcpQpkTP/E
HTnB3oaRiYy6GDuGKsNkhEUvlV2AnVAxO17IN5QQ2lMQ261zaPj9y9v1oCQlzGfm
UKRJWFGbsooapdm7DTrqrfO+zyRXWPe6GRnYlmeXTRWi8CEIXpCkhhf8rma8KNnW
S96jsTSU97Lo0hnIbL+4o3qf2BiYcfIwfoIXzUaj5ajGEA==
=CwJW
-----END PGP PUBLIC KEY BLOCK-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM ghcr.io/graalvm/native-image-community:23@sha256:5a28da013d97ac66033b6c836015c7ee7f5d3e07c705d51f1e6c26a04fd17b8e AS graalvm
FROM ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5abfea8f4c3c0c849ce512b5ef8982
ENV JAVA_HOME=/usr/lib64/graalvm/graalvm-community-java23
ENV PATH="${JAVA_HOME}/bin:${PATH}"
COPY --from=graalvm ${JAVA_HOME} ${JAVA_HOME}
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
git build-essential curl ca-certificates binutils make tar gzip && \
rm -rf /var/lib/apt/lists/*
COPY install-musl.sh /opt/install-musl.sh
RUN chmod +x /opt/install-musl.sh && cd /opt && ./install-musl.sh
ENV MUSL_HOME=/opt/musl-toolchain
ENV PATH="/opt/musl-toolchain/bin:${PATH}"
RUN git config --global --add safe.directory '*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
MUSL_VERSION=1.2.5
ZLIB_VERSION=1.2.13
export MUSL_HOME=/opt/musl-toolchain
curl --fail --location --retry 5 --retry-all-errors \
--output "musl-$MUSL_VERSION.tar.gz" \
"https://musl.libc.org/releases/musl-$MUSL_VERSION.tar.gz"
curl --fail --location --retry 5 --retry-all-errors \
--output "zlib-$ZLIB_VERSION.tar.gz" \
"https://github.com/madler/zlib/releases/download/v$ZLIB_VERSION/zlib-$ZLIB_VERSION.tar.gz"
tar -xzf "musl-$MUSL_VERSION.tar.gz"
cd "musl-$MUSL_VERSION"
./configure --prefix="$MUSL_HOME" --static
make -j"$(nproc)"
make install
cd ..
ln -sf "$MUSL_HOME/bin/musl-gcc" "$MUSL_HOME/bin/x86_64-linux-musl-gcc"
export PATH="$MUSL_HOME/bin:$PATH"
tar -xzf "zlib-$ZLIB_VERSION.tar.gz"
cd "zlib-$ZLIB_VERSION"
CC=musl-gcc ./configure --prefix="$MUSL_HOME" --static
make -j"$(nproc)"
make install
16 changes: 16 additions & 0 deletions .github/release-automation/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}

rootProject.name = 'temporal-release-automation'

includeBuild('../..') {
dependencySubstitution {
substitute module('io.temporal:temporal-envconfig') using project(':temporal-envconfig')
substitute module('io.temporal:temporal-sdk') using project(':temporal-sdk')
substitute module('io.temporal:temporal-testing') using project(':temporal-testing')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.temporal.releaseautomation;

import java.util.Locale;
import java.util.regex.Pattern;

public final class ApprovalEvidence {
private static final Pattern SHA = Pattern.compile("[0-9a-f]{40}");
private static final Pattern ACTOR = Pattern.compile("[A-Za-z0-9-]{1,39}");
private static final Pattern WORKFLOW_ID = Pattern.compile("sdk-java-release/[0-9a-f]{64}");
private static final Pattern RUN_ID = Pattern.compile("[0-9a-fA-F-]{16,64}");

public String releaseDigest;
public String workflowId;
public String runId;
public long githubApprovalRunId;
public String githubActor;
public long githubIssueNumber;
public String githubIssueNodeId;
public String githubIssueBodySha256;
public String trustedWorkerCommit;

public ApprovalEvidence() {}

public ApprovalEvidence(
String releaseDigest,
String workflowId,
String runId,
long githubApprovalRunId,
String githubActor,
long githubIssueNumber,
String githubIssueNodeId,
String githubIssueBodySha256,
String trustedWorkerCommit) {
this.releaseDigest = releaseDigest.toLowerCase(Locale.ROOT);
this.workflowId = workflowId;
this.runId = runId;
this.githubApprovalRunId = githubApprovalRunId;
this.githubActor = githubActor;
this.githubIssueNumber = githubIssueNumber;
this.githubIssueNodeId = githubIssueNodeId;
this.githubIssueBodySha256 = githubIssueBodySha256;
this.trustedWorkerCommit = trustedWorkerCommit.toLowerCase(Locale.ROOT);
validate();
}

public void validate() {
if (releaseDigest == null || !releaseDigest.matches("[0-9a-f]{64}")) {
throw new IllegalArgumentException("Approval release digest is invalid.");
}
if (workflowId == null || !WORKFLOW_ID.matcher(workflowId).matches()) {
throw new IllegalArgumentException("Approval workflow ID is invalid.");
}
if (runId == null || !RUN_ID.matcher(runId).matches()) {
throw new IllegalArgumentException("Approval run ID is invalid.");
}
if (githubApprovalRunId <= 0) {
throw new IllegalArgumentException("GitHub approval run ID is invalid.");
}
if (githubActor == null || !ACTOR.matcher(githubActor).matches()) {
throw new IllegalArgumentException("GitHub approval actor is invalid.");
}
if (githubIssueNumber <= 0
|| githubIssueNodeId == null
|| !githubIssueNodeId.matches("[A-Za-z0-9_=-]{8,128}")
|| githubIssueBodySha256 == null
|| !githubIssueBodySha256.matches("[0-9a-f]{64}")) {
throw new IllegalArgumentException("GitHub approval issue identity is invalid.");
}
if (trustedWorkerCommit == null || !SHA.matcher(trustedWorkerCommit).matches()) {
throw new IllegalArgumentException("Trusted worker commit must be a full SHA.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.temporal.releaseautomation;

public final class ApprovalRequest {
public String releaseDigest;
public String workflowId;
public String runId;
public long githubRunId;
public long githubIssueNumber;
public String githubIssueNodeId;
public String githubIssueBodySha256;
public String githubIssueCreator;
public String trustedWorkerCommit;

public ApprovalRequest() {}

public ApprovalRequest(
String releaseDigest,
String workflowId,
String runId,
long githubRunId,
long githubIssueNumber,
String githubIssueNodeId,
String githubIssueBodySha256,
String githubIssueCreator,
String trustedWorkerCommit) {
this.releaseDigest = releaseDigest;
this.workflowId = workflowId;
this.runId = runId;
this.githubRunId = githubRunId;
this.githubIssueNumber = githubIssueNumber;
this.githubIssueNodeId = githubIssueNodeId;
this.githubIssueBodySha256 = githubIssueBodySha256;
this.githubIssueCreator = githubIssueCreator;
this.trustedWorkerCommit = trustedWorkerCommit;
validate();
}

public void validate() {
if (releaseDigest == null
|| !releaseDigest.matches("[0-9a-f]{64}")
|| workflowId == null
|| !workflowId.matches("sdk-java-release/[0-9a-f]{64}")
|| runId == null
|| !runId.matches("[0-9a-fA-F-]{16,64}")
|| githubRunId <= 0
|| githubIssueNumber <= 0
|| githubIssueNodeId == null
|| !githubIssueNodeId.matches("[A-Za-z0-9_=-]{8,128}")
|| githubIssueBodySha256 == null
|| !githubIssueBodySha256.matches("[0-9a-f]{64}")
|| githubIssueCreator == null
|| !githubIssueCreator.matches("[A-Za-z0-9-]{1,39}")
|| trustedWorkerCommit == null
|| !trustedWorkerCommit.matches("[0-9a-f]{40}")) {
throw new IllegalArgumentException("Invalid release-specific approval request.");
}
}

public boolean matches(ApprovalEvidence evidence) {
validate();
evidence.validate();
return releaseDigest.equals(evidence.releaseDigest)
&& workflowId.equals(evidence.workflowId)
&& runId.equals(evidence.runId)
&& githubIssueNumber == evidence.githubIssueNumber
&& githubIssueNodeId.equals(evidence.githubIssueNodeId)
&& githubIssueBodySha256.equals(evidence.githubIssueBodySha256)
&& trustedWorkerCommit.equals(evidence.trustedWorkerCommit);
}

public boolean sameIssue(ApprovalRequest other) {
validate();
other.validate();
return releaseDigest.equals(other.releaseDigest)
&& workflowId.equals(other.workflowId)
&& runId.equals(other.runId)
&& githubIssueNumber == other.githubIssueNumber
&& githubIssueNodeId.equals(other.githubIssueNodeId)
&& githubIssueBodySha256.equals(other.githubIssueBodySha256)
&& githubIssueCreator.equals(other.githubIssueCreator)
&& trustedWorkerCommit.equals(other.trustedWorkerCommit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.temporal.releaseautomation;

import java.util.Locale;
import java.util.regex.Pattern;

public final class ArtifactEntry implements Comparable<ArtifactEntry> {
private static final Pattern NAME = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]*");
private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}");

public String name;
public String sha256;
public long size;

public ArtifactEntry() {}

public ArtifactEntry(String name, String sha256, long size) {
this.name = name;
this.sha256 = sha256.toLowerCase(Locale.ROOT);
this.size = size;
validate();
}

public void validate() {
if (name == null || !NAME.matcher(name).matches()) {
throw new IllegalArgumentException("Artifact name must be a basename.");
}
if (sha256 == null || !HASH.matcher(sha256).matches()) {
throw new IllegalArgumentException("Artifact hash must be SHA-256.");
}
if (size <= 0) {
throw new IllegalArgumentException("Artifact size must be positive.");
}
}

String canonicalForm() {
validate();
return name + "\t" + sha256 + "\t" + size;
}

@Override
public int compareTo(ArtifactEntry other) {
return name.compareTo(other.name);
}
}
Loading
Loading