name: Release
on:
workflow_run:
workflows:
- CI
types:
- completed
branches:
- main
workflow_dispatch:
inputs:
publish-existing-tag:
description: Existing vX.Y.Z tag to validate or re-publish.
required: true
type: string
artifact-source:
description: Promote retained CI artifacts or rebuild all targets when any are missing.
required: true
type: choice
default: promote
options:
- promote
- rebuild-if-missing
dry-run:
description: Validate the full release without publishing GitHub or Cargo assets.
required: true
type: boolean
default: false
concurrency:
group: release-${{ github.event_name == 'workflow_run' && 'main' || inputs.publish-existing-tag }}
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_HTTP_MULTIPLEXING: "false"
CARGO_NET_RETRY: "10"
jobs:
release-please:
name: release please
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
outputs:
release-created: ${{ steps.release.outputs.release_created }}
tag-name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
ci-run-id: ${{ steps.trigger.outputs.ci-run-id }}
ci-head-sha: ${{ steps.trigger.outputs.ci-head-sha }}
steps:
- name: Verify successful CI is for the current main tip
id: trigger
shell: bash
env:
GH_TOKEN: ${{ github.token }}
CI_RUN_ID: ${{ github.event.workflow_run.id }}
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
main_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
if [[ "$main_sha" != "$CI_HEAD_SHA" ]]; then
echo "Skipping stale CI completion for $CI_HEAD_SHA; current main is $main_sha."
echo 'current-tip=false' >> "$GITHUB_OUTPUT"
exit 0
fi
associated_pulls="$(gh api \
-H 'Accept: application/vnd.github+json' \
"repos/$GITHUB_REPOSITORY/commits/$CI_HEAD_SHA/pulls")"
release_merge_count="$(jq -r \
--arg repository "$GITHUB_REPOSITORY" \
'[.[]
| select(.merged_at != null)
| select(.base.ref == "main")
| select(.head.repo.full_name == $repository)
| select(.head.ref == "release-please--branches--main--components--codebase-graph")
| select([.labels[].name] | index("autorelease: pending") != null)]
| length' <<<"$associated_pulls")"
[[ "$release_merge_count" -le 1 ]] || {
echo "Expected at most one release-please pull request for $CI_HEAD_SHA; found $release_merge_count." >&2
exit 1
}
if [[ "$release_merge_count" == '1' ]]; then
release_merge=true
else
release_merge=false
fi
{
echo 'current-tip=true'
echo "ci-run-id=$CI_RUN_ID"
echo "ci-head-sha=$CI_HEAD_SHA"
echo "release-merge=$release_merge"
} >> "$GITHUB_OUTPUT"
- name: Create release pull request or GitHub release
id: release
if: steps.trigger.outputs.current-tip == 'true'
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: ${{ steps.trigger.outputs.release-merge != 'true' }}
- name: Recheck current main tip after release-please
if: steps.trigger.outputs.current-tip == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RELEASE_MERGE: ${{ steps.trigger.outputs.release-merge }}
RELEASE_CREATED: ${{ steps.release.outputs.release_created }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
set -euo pipefail
main_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
[[ "$main_sha" == "$CI_HEAD_SHA" ]] || {
echo "Main advanced to $main_sha while release-please was running; stopping publication for $CI_HEAD_SHA." >&2
exit 1
}
if [[ "$RELEASE_CREATED" == 'true' && "$RELEASE_MERGE" != 'true' ]]; then
echo "Release-please created a release outside a verified release merge." >&2
exit 1
fi
if [[ "$RELEASE_CREATED" == 'true' && "$RELEASE_SHA" != "$CI_HEAD_SHA" ]]; then
echo "Release-please created a release for $RELEASE_SHA, not triggering CI SHA $CI_HEAD_SHA." >&2
exit 1
fi
release-target:
name: resolve release target
needs: release-please
if: ${{ always() && (github.event_name == 'workflow_dispatch' || needs.release-please.result == 'success') }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
should-publish: ${{ steps.resolve.outputs.should-publish }}
automatic: ${{ steps.resolve.outputs.automatic }}
tag-name: ${{ steps.resolve.outputs.tag-name }}
version: ${{ steps.resolve.outputs.version }}
source-sha: ${{ steps.resolve.outputs.source-sha }}
publish_assets: ${{ steps.resolve.outputs.publish_assets }}
artifact-source: ${{ steps.resolve.outputs.artifact-source }}
ci-run-id: ${{ steps.resolve.outputs.ci-run-id }}
steps:
- name: Resolve tag and source SHA
id: resolve
shell: bash
env:
GH_TOKEN: ${{ github.token }}
MANUAL_TAG: ${{ inputs.publish-existing-tag }}
MANUAL_SOURCE: ${{ inputs.artifact-source }}
MANUAL_DRY_RUN: ${{ inputs.dry-run }}
RELEASE_CREATED: ${{ needs.release-please.outputs.release-created }}
RELEASE_TAG: ${{ needs.release-please.outputs.tag-name }}
RELEASE_CI_RUN_ID: ${{ needs.release-please.outputs.ci-run-id }}
RELEASE_CI_HEAD_SHA: ${{ needs.release-please.outputs.ci-head-sha }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == 'workflow_dispatch' ]]; then
tag="$MANUAL_TAG"
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Invalid release tag: $tag" >&2; exit 1; }
source_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha')"
object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.type')"
if [[ "$object_type" == 'tag' ]]; then
source_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$source_sha" --jq '.object.sha')"
fi
automatic=false
should_publish=true
dry_run="$MANUAL_DRY_RUN"
if [[ "$dry_run" == 'true' ]]; then
publish_assets=false
else
publish_assets=true
fi
artifact_source="$MANUAL_SOURCE"
ci_run_id=''
elif [[ "$RELEASE_CREATED" == 'true' ]]; then
tag="$RELEASE_TAG"
source_sha="$RELEASE_CI_HEAD_SHA"
ci_run_id="$RELEASE_CI_RUN_ID"
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Invalid triggering CI SHA: $source_sha" >&2; exit 1; }
[[ "$ci_run_id" =~ ^[0-9]+$ ]] || { echo "Invalid triggering CI run ID: $ci_run_id" >&2; exit 1; }
tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha')"
object_type="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.type')"
if [[ "$object_type" == 'tag' ]]; then
tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha" --jq '.object.sha')"
fi
[[ "$tag_sha" == "$source_sha" ]] || { echo "Release tag $tag resolves to $tag_sha, not triggering CI SHA $source_sha." >&2; exit 1; }
automatic=true
should_publish=true
dry_run=false
publish_assets=true
artifact_source=promote
else
tag=''
source_sha=''
automatic=false
should_publish=false
dry_run=true
publish_assets=false
artifact_source=promote
ci_run_id=''
fi
{
echo "should-publish=$should_publish"
echo "automatic=$automatic"
echo "tag-name=$tag"
echo "version=${tag#v}"
echo "source-sha=$source_sha"
echo "publish_assets=$publish_assets"
echo "artifact-source=$artifact_source"
echo "ci-run-id=$ci_run_id"
} >> "$GITHUB_OUTPUT"
ci-gate:
name: exact-SHA CI gate
needs: release-target
if: needs.release-target.outputs.should-publish == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
actions: read
contents: read
outputs:
ci-run-id: ${{ steps.wait.outputs.ci-run-id }}
artifacts-available: ${{ steps.wait.outputs.artifacts-available }}
steps:
- name: Validate exact-SHA CI push run
id: wait
shell: bash
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ needs.release-target.outputs.source-sha }}
AUTOMATIC: ${{ needs.release-target.outputs.automatic }}
REQUESTED_CI_RUN_ID: ${{ needs.release-target.outputs.ci-run-id }}
run: |
set -euo pipefail
if [[ "$AUTOMATIC" == 'true' ]]; then
run="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$REQUESTED_CI_RUN_ID")"
jq -e \
--arg sha "$SOURCE_SHA" \
--arg run_id "$REQUESTED_CI_RUN_ID" \
'.id == ($run_id | tonumber)
and .path == ".github/workflows/ci.yml"
and .event == "push"
and .head_branch == "main"
and .head_sha == $sha
and .status == "completed"
and .conclusion == "success"' <<<"$run" >/dev/null || {
echo "Triggering workflow run does not satisfy the exact-SHA main CI contract." >&2
exit 1
}
main_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')"
[[ "$main_sha" == "$SOURCE_SHA" ]] || {
echo "Refusing to release stale CI SHA $SOURCE_SHA; current main is $main_sha." >&2
exit 1
}
artifact_count="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$REQUESTED_CI_RUN_ID/artifacts?per_page=100" --jq '[.artifacts[] | select(.expired == false and (.name | startswith("native-")))] | length')"
{
echo "ci-run-id=$REQUESTED_CI_RUN_ID"
if [[ "$artifact_count" == '4' ]]; then
echo 'artifacts-available=true'
else
echo 'artifacts-available=false'
fi
} >> "$GITHUB_OUTPUT"
exit 0
fi
deadline=$((SECONDS + 1800))
while (( SECONDS < deadline )); do
runs="$(gh api --paginate "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?event=push&head_sha=$SOURCE_SHA&per_page=100")"
run_id="$(jq -r --arg sha "$SOURCE_SHA" '[.workflow_runs[] | select(.head_sha == $sha and .event == "push" and .status == "completed" and .conclusion == "success")] | sort_by(.run_number) | last | .id // empty' <<<"$runs")"
if [[ -n "$run_id" ]]; then
artifact_count="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/artifacts?per_page=100" --jq '[.artifacts[] | select(.expired == false and (.name | startswith("native-")))] | length')"
{
echo "ci-run-id=$run_id"
if [[ "$artifact_count" == '4' ]]; then
echo "artifacts-available=true"
else
echo "artifacts-available=false"
fi
} >> "$GITHUB_OUTPUT"
exit 0
fi
failed="$(jq -r --arg sha "$SOURCE_SHA" '[.workflow_runs[] | select(.head_sha == $sha and .event == "push" and .status == "completed" and .conclusion != "success")] | length' <<<"$runs")"
if [[ "$failed" != '0' ]]; then
echo "Exact-SHA CI completed without success for $SOURCE_SHA." >&2
exit 1
fi
sleep 20
done
echo "Timed out waiting for successful CI for $SOURCE_SHA." >&2
exit 1
production-gate:
name: production release gate
needs:
- release-target
- ci-gate
if: needs.release-target.outputs.should-publish == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: cargo
permissions:
contents: read
steps:
- name: Check out exact release SHA
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: ${{ needs.release-target.outputs.source-sha }}
- name: Verify package version matches tag
run: cargo run -p xtask -- verify-release-version "${{ needs.release-target.outputs.tag-name }}"
- name: Run production repository gate
shell: bash
env:
CODEBASE_GRAPH_CONFIRM_RELEASE_ENVIRONMENT: ${{ vars.CODEBASE_GRAPH_CONFIRM_RELEASE_ENVIRONMENT }}
CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING: ${{ vars.CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING }}
CODEBASE_GRAPH_REQUIRE_CONDA: ${{ vars.CODEBASE_GRAPH_REQUIRE_CONDA }}
run: |
set -euo pipefail
args=(release-gate --production)
[[ "$CODEBASE_GRAPH_CONFIRM_RELEASE_ENVIRONMENT" == 'true' ]] && args+=(--confirm release-environment)
[[ "$CODEBASE_GRAPH_CONFIRM_PRIVATE_VULNERABILITY_REPORTING" == 'true' ]] && args+=(--confirm private-vulnerability-reporting)
[[ "$CODEBASE_GRAPH_REQUIRE_CONDA" == 'true' ]] && args+=(--require-conda)
cargo run -p xtask -- "${args[@]}"
select-artifacts:
name: select artifact source
needs:
- release-target
- ci-gate
- production-gate
if: needs.release-target.outputs.should-publish == 'true'
environment:
name: cargo
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
rebuild: ${{ steps.select.outputs.rebuild }}
steps:
- name: Select promotion or all-target rebuild
id: select
shell: bash
env:
ARTIFACT_SOURCE: ${{ needs.release-target.outputs.artifact-source }}
ARTIFACTS_AVAILABLE: ${{ needs.ci-gate.outputs.artifacts-available }}
AUTOMATIC: ${{ needs.release-target.outputs.automatic }}
run: |
set -euo pipefail
if [[ "$ARTIFACTS_AVAILABLE" == 'true' ]]; then
echo 'rebuild=false' >> "$GITHUB_OUTPUT"
elif [[ "$AUTOMATIC" == 'false' && "$ARTIFACT_SOURCE" == 'rebuild-if-missing' ]]; then
echo 'rebuild=true' >> "$GITHUB_OUTPUT"
else
echo 'Exact-SHA CI artifacts are missing or expired; select rebuild-if-missing for manual recovery.' >&2
exit 1
fi
rebuild-artifacts:
name: rebuild (${{ matrix.target }})
needs:
- release-target
- select-artifacts
if: ${{ needs.select-artifacts.outputs.rebuild == 'true' }}
strategy:
fail-fast: false
matrix:
target:
- linux-x86_64
- macos-arm64
- macos-x86_64
- windows-x86_64
uses: ./.github/workflows/native.yml
with:
target: ${{ matrix.target }}
source-sha: ${{ needs.release-target.outputs.source-sha }}
run-tests: false
upload-artifact: true
artifact-retention-days: 7
validate-artifacts:
name: validate complete native artifact set
needs:
- release-target
- ci-gate
- select-artifacts
- rebuild-artifacts
if: ${{ always() && needs.select-artifacts.result == 'success' && (needs.rebuild-artifacts.result == 'success' || needs.rebuild-artifacts.result == 'skipped') }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read
environment:
name: cargo
steps:
- name: Check out exact release SHA
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: ${{ needs.release-target.outputs.source-sha }}
- name: Download promoted CI artifacts
if: needs.select-artifacts.outputs.rebuild == 'false'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
run-id: ${{ needs.ci-gate.outputs.ci-run-id }}
pattern: native-*
path: dist/native
github-token: ${{ github.token }}
- name: Download rebuilt artifacts
if: needs.select-artifacts.outputs.rebuild == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
pattern: native-*
path: dist/native
- name: Validate checksums, provenance, and archive contract
run: cargo run -p xtask -- validate-native-artifacts --input dist/native --tag "${{ needs.release-target.outputs.tag-name }}" --commit-sha "${{ needs.release-target.outputs.source-sha }}"
- name: Stage complete validated set
shell: bash
run: |
set -euo pipefail
mkdir -p dist/release-assets
while IFS= read -r -d '' artifact; do cp "$artifact" dist/release-assets/; done < <(find dist/native -type f -name 'codebase-graph-*.tar.gz' -print0)
while IFS= read -r -d '' checksum; do cp "$checksum" dist/release-assets/; done < <(find dist/native -type f -name 'codebase-graph-*.tar.gz.sha256' -print0)
test "$(find dist/release-assets -type f | wc -l | tr -d ' ')" = '8'
- name: Upload validated release set
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: validated-release-assets
path: dist/release-assets/
if-no-files-found: error
retention-days: 7
publish-release-assets:
name: publish release assets
needs:
- release-target
- validate-artifacts
if: >-
${{
always()
&& needs.release-target.result == 'success'
&& needs.validate-artifacts.result == 'success'
&& needs.release-target.outputs.publish_assets == 'true'
}}
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: cargo
permissions:
actions: read
contents: write
steps:
- name: Download validated release set
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: validated-release-assets
path: dist/release-assets
- name: Upload complete native asset set
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ needs.release-target.outputs.tag-name }}" dist/release-assets/codebase-graph-*.tar.gz dist/release-assets/codebase-graph-*.tar.gz.sha256 --clobber --repo "$GITHUB_REPOSITORY"
publish-crate:
name: publish crates.io package
needs:
- release-please
- release-target
- publish-release-assets
if: >-
${{
always()
&& needs.release-please.result == 'success'
&& needs.release-target.result == 'success'
&& needs.publish-release-assets.result == 'success'
&& needs.release-please.outputs.release-created == 'true'
&& needs.release-target.outputs.publish_assets == 'true'
}}
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
name: cargo
permissions:
contents: read
steps:
- name: Check out exact release SHA
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: ${{ needs.release-target.outputs.source-sha }}
- name: Verify crates.io package
run: cargo publish --dry-run --locked
- name: Publish crates.io package
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked