Release Project workflow (google/closure-compiler)
The Release Project workflow from google/closure-compiler, explained and optimized by Latchkey.
CI health: D - needs work
Point runs-on at Latchkey and get caching, run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Release Project workflow from the google/closure-compiler repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.0rg/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Release Project
permissions:
contents: read
# This workflow is triggered by pushes to the `master` branch that modify '.github/piper-cut-cl.json'.
# When `piper-cut-cl.json` changes, indicating a new release cut, the workflow proceeds to:
# 1. Tag the repository at the current commit with the new version ID.
# 2. Run the test suite.
# 3. If tests pass, publish the release artifacts to Sonatype.
on:
push:
branches: [ master ]
paths:
- '.github/piper-cut-cl.json'
jobs:
tag_release_version_id:
name: Tag Release Version ID
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ fromJson(steps.set_piper_json_vars.outputs.piperJson).version_id }}
steps:
# Checkout the piper-cut-cl.json from the most recent commit
- name: Checkout Current closure-compiler Commit
# https://github.com/marketplace/actions/checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
sparse-checkout: |
.github/piper-cut-cl.json
.github/ci_support/tag_release_version.sh
sparse-checkout-cone-mode: false
- id: set_piper_json_vars
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
run: |
{
echo 'piperJson<<EOF'
cat ./.github/piper-cut-cl.json
echo EOF
} >> "$GITHUB_OUTPUT"
# Fetch all historical commits
- name: Checkout all closure-compiler commits and tags
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# a depth of 0 triggers a fetch of all historical commits/branches/tags
# https://github.com/marketplace/actions/checkout#Fetch-all-history-for-all-tags-and-branches
fetch-depth: 0
- name: Tag cut CL
id: tag_cut_cl
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
.github/ci_support/tag_release_version.sh \
"${{ fromJson(env.piperJson).piper_cut_cl }}" \
"${{ fromJson(env.piperJson).version_id }}";
env:
piperJson: ${{ steps.set_piper_json_vars.outputs.piperJson }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run-tests:
uses: ./.github/workflows/run_tests.yaml
needs: tag_release_version_id
with:
ref: ${{ needs.tag_release_version_id.outputs.tag }}
publish-to-sonatype:
runs-on: ubuntu-latest
# Only publish if tests passed.
needs: [tag_release_version_id, run-tests]
steps:
- name: Check secrets are provided
run: |
check_secret() {
if [[ -z "${2:-}" ]]; then
echo "Error: secret ${1} is required but not provided."
exit 1
fi
}
check_secret "SONATYPE_USERNAME_TOKEN" "${{ secrets.SONATYPE_USERNAME_TOKEN }}"
check_secret "SONATYPE_PASSWORD_TOKEN" "${{ secrets.SONATYPE_PASSWORD_TOKEN }}"
check_secret "MAVEN_GPG_PRIVATE_KEY" "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}"
check_secret "MAVEN_GPG_PASSPHRASE" "${{ secrets.MAVEN_GPG_PASSPHRASE }}"
- name: Checkout the repo at the release tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ needs.tag_release_version_id.outputs.tag }}
persist-credentials: false
# Maven and Bazel require Java to be available.
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
java-version: '21'
distribution: 'zulu'
java-package: jdk
# This will trigger the import of the private key in the GPG secret keyring.
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
# This will create the maven settings.xml file needed for deploying to Sonatype.
server-id: central
# Configure Maven settings to use environment variables. These environment variables are
# populated from GitHub secrets in the publication steps.
server-username: SONATYPE_USERNAME_TOKEN
server-password: SONATYPE_PASSWORD_TOKEN
gpg-passphrase: MAVEN_GPG_PASSPHRASE
# https://github.com/bazel-contrib/setup-bazel
- uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
with:
# Avoid downloading Bazel every time.
bazelisk-cache: true
# Store build cache per workflow.
disk-cache: ${{ github.workflow }}
# Share repository cache between workflows
repository-cache: true
- name: Upload and Publish to Sonatype
run: |
./release.sh --sonatype-auto-publish
env:
RELEASE_NUM: ${{ needs.tag_release_version_id.outputs.tag }}
SONATYPE_USERNAME_TOKEN: ${{ secrets.SONATYPE_USERNAME_TOKEN }}
SONATYPE_PASSWORD_TOKEN: ${{ secrets.SONATYPE_PASSWORD_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.0rg/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Release Project permissions: contents: read # This workflow is triggered by pushes to the `master` branch that modify '.github/piper-cut-cl.json'. # When `piper-cut-cl.json` changes, indicating a new release cut, the workflow proceeds to: # 1. Tag the repository at the current commit with the new version ID. # 2. Run the test suite. # 3. If tests pass, publish the release artifacts to Sonatype. on: push: branches: [ master ] paths: - '.github/piper-cut-cl.json' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: tag_release_version_id: timeout-minutes: 30 name: Tag Release Version ID runs-on: latchkey-small permissions: contents: write outputs: tag: ${{ fromJson(steps.set_piper_json_vars.outputs.piperJson).version_id }} steps: # Checkout the piper-cut-cl.json from the most recent commit - name: Checkout Current closure-compiler Commit # https://github.com/marketplace/actions/checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 sparse-checkout: | .github/piper-cut-cl.json .github/ci_support/tag_release_version.sh sparse-checkout-cone-mode: false - id: set_piper_json_vars # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context run: | { echo 'piperJson<<EOF' cat ./.github/piper-cut-cl.json echo EOF } >> "$GITHUB_OUTPUT" # Fetch all historical commits - name: Checkout all closure-compiler commits and tags uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # a depth of 0 triggers a fetch of all historical commits/branches/tags # https://github.com/marketplace/actions/checkout#Fetch-all-history-for-all-tags-and-branches fetch-depth: 0 - name: Tag cut CL id: tag_cut_cl run: | git config --global user.name "${GITHUB_ACTOR}" git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" .github/ci_support/tag_release_version.sh \ "${{ fromJson(env.piperJson).piper_cut_cl }}" \ "${{ fromJson(env.piperJson).version_id }}"; env: piperJson: ${{ steps.set_piper_json_vars.outputs.piperJson }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run-tests: timeout-minutes: 30 uses: ./.github/workflows/run_tests.yaml needs: tag_release_version_id with: ref: ${{ needs.tag_release_version_id.outputs.tag }} publish-to-sonatype: timeout-minutes: 30 runs-on: latchkey-small # Only publish if tests passed. needs: [tag_release_version_id, run-tests] steps: - name: Check secrets are provided run: | check_secret() { if [[ -z "${2:-}" ]]; then echo "Error: secret ${1} is required but not provided." exit 1 fi } check_secret "SONATYPE_USERNAME_TOKEN" "${{ secrets.SONATYPE_USERNAME_TOKEN }}" check_secret "SONATYPE_PASSWORD_TOKEN" "${{ secrets.SONATYPE_PASSWORD_TOKEN }}" check_secret "MAVEN_GPG_PRIVATE_KEY" "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}" check_secret "MAVEN_GPG_PASSPHRASE" "${{ secrets.MAVEN_GPG_PASSPHRASE }}" - name: Checkout the repo at the release tag uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.tag_release_version_id.outputs.tag }} persist-credentials: false # Maven and Bazel require Java to be available. - name: Setup Java uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: cache: 'maven' java-version: '21' distribution: 'zulu' java-package: jdk # This will trigger the import of the private key in the GPG secret keyring. gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} # This will create the maven settings.xml file needed for deploying to Sonatype. server-id: central # Configure Maven settings to use environment variables. These environment variables are # populated from GitHub secrets in the publication steps. server-username: SONATYPE_USERNAME_TOKEN server-password: SONATYPE_PASSWORD_TOKEN gpg-passphrase: MAVEN_GPG_PASSPHRASE # https://github.com/bazel-contrib/setup-bazel - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 with: # Avoid downloading Bazel every time. bazelisk-cache: true # Store build cache per workflow. disk-cache: ${{ github.workflow }} # Share repository cache between workflows repository-cache: true - name: Upload and Publish to Sonatype run: | ./release.sh --sonatype-auto-publish env: RELEASE_NUM: ${{ needs.tag_release_version_id.outputs.tag }} SONATYPE_USERNAME_TOKEN: ${{ secrets.SONATYPE_USERNAME_TOKEN }} SONATYPE_PASSWORD_TOKEN: ${{ secrets.SONATYPE_PASSWORD_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.