Skip to content

Release v0.0.0-beta.17 #13

Release v0.0.0-beta.17

Release v0.0.0-beta.17 #13

Workflow file for this run

# SPDX-License-Identifier: Apache-2.0
name: Release
run-name: Release ${{ inputs.tag }}
on:
workflow_dispatch:
inputs:
tag:
description: "Immutable v* tag"
required: true
type: string
prepare_run_id:
description: "Successful Release Prepare run ID"
required: true
type: string
distribution_ref:
description: "Optional ref for distribution workflow/helper fixes"
required: false
type: string
crates:
description: "Publish crates.io packages"
required: true
default: true
type: boolean
pypi:
description: "Publish prepared Python files"
required: true
default: true
type: boolean
npm:
description: "Publish prepared npm tarballs"
required: true
default: true
type: boolean
github:
description: "Create/update GitHub release"
required: true
default: true
type: boolean
concurrency:
group: release-distribution-${{ inputs.tag }}
cancel-in-progress: false
permissions:
contents: read
actions: read
jobs:
validate:
uses: ./.github/workflows/release-validate.yml
with:
tag: ${{ inputs.tag }}
provenance:
needs: validate
runs-on: ubuntu-latest
outputs:
artifact: ${{ steps.check.outputs.artifact }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.distribution_ref || inputs.tag }}
- id: check
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.prepare_run_id }}
TAG: ${{ inputs.tag }}
SHA: ${{ needs.validate.outputs.source_sha }}
PUBLISH_CRATES: ${{ inputs.crates }}
PUBLISH_PYPI: ${{ inputs.pypi }}
PUBLISH_NPM: ${{ inputs.npm }}
PUBLISH_GITHUB: ${{ inputs.github }}
IS_PRERELEASE: ${{ needs.validate.outputs.is_prerelease }}
run: |
set -euo pipefail
[[ "$RUN" =~ ^[0-9]+$ ]] || { echo "invalid prepare run ID" >&2; exit 1; }
[[ "$PUBLISH_CRATES$PUBLISH_PYPI$PUBLISH_NPM$PUBLISH_GITHUB" == *true* ]] || { echo "enable at least one distribution target" >&2; exit 1; }
if [[ "$IS_PRERELEASE" = true && "$PUBLISH_CRATES$PUBLISH_PYPI$PUBLISH_NPM" != *true* ]]; then
echo "GitHub Releases are created only for stable versions; enable a registry target" >&2
exit 1
fi
RUN_JSON="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$RUN")"
test "$(jq -r .event <<<"$RUN_JSON")" = push
test "$(jq -r .conclusion <<<"$RUN_JSON")" = success
test "$(jq -r .head_sha <<<"$RUN_JSON")" = "$SHA"
RUN_PATH="$(jq -r .path <<<"$RUN_JSON")"
case "$RUN_PATH" in
.github/workflows/release-prepare.yml|.github/workflows/release-prepare.yml@*) ;;
*) echo "prepare run used unexpected workflow path: $RUN_PATH" >&2; exit 1 ;;
esac
test "$(jq -r .repository.full_name <<<"$RUN_JSON")" = "$GITHUB_REPOSITORY"
test "$(jq -r .head_repository.full_name <<<"$RUN_JSON")" = "$GITHUB_REPOSITORY"
ARTIFACT="release-bundle-${{ needs.validate.outputs.version }}"
COUNT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$RUN/artifacts" --paginate --jq ".artifacts[] | select(.name == \"$ARTIFACT\" and .expired == false) | .id" | wc -l)"
test "$COUNT" -eq 1 || { echo "expected exactly one live $ARTIFACT artifact, got $COUNT" >&2; exit 1; }
echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v8
with:
name: ${{ steps.check.outputs.artifact }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: bundle
- name: Verify exact prepared bundle before protected environments
shell: bash
run: |
set -euo pipefail
python3 scripts/release/artifact_manifest.py verify --directory bundle --manifest bundle/release-manifest.json --repository "$GITHUB_REPOSITORY" --tag "${{ inputs.tag }}" --source-sha "${{ needs.validate.outputs.source_sha }}" --prepare-run-id "${{ inputs.prepare_run_id }}" --workflow release-prepare.yml --version "${{ needs.validate.outputs.version }}"
python3 scripts/release/artifact_manifest.py verify-checksums --directory bundle --manifest bundle/release-manifest.json --checksums bundle/SHA256SUMS
publish-crates:
needs: [validate, provenance]
if: inputs.crates
runs-on: ubuntu-latest
environment: crates.io
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.distribution_ref || inputs.tag }}
- uses: actions/checkout@v6
with:
ref: ${{ needs.validate.outputs.source_sha }}
path: tagged-source
- uses: dtolnay/rust-toolchain@stable
- run: python3 scripts/release/version_contract.py stamp "${{ needs.validate.outputs.version }}" --source tagged-source --destination release-source
- shell: bash
working-directory: release-source
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ needs.validate.outputs.version }}"
USER_AGENT="code2graph-release/$VERSION ($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)"
available() { curl -fsS -A "$USER_AGENT" "https://crates.io/api/v1/crates/$1/$VERSION" >/dev/null; }
verify_remote() {
local package="$1" remote
remote="$(mktemp)"
curl -fsSL -A "$USER_AGENT" "https://crates.io/api/v1/crates/$package/$VERSION/download" -o "$remote"
cmp --silent "target/package/$package-$VERSION.crate" "$remote" || { echo "crates.io archive differs for $package@$VERSION" >&2; return 1; }
}
publish() {
local package="$1"
cargo package -p "$package" --allow-dirty --no-verify
if available "$package"; then
verify_remote "$package"
return
fi
if ! cargo publish -p "$package" --allow-dirty --no-verify; then
echo "cargo publish returned an error; reconciling against crates.io before failing" >&2
fi
for _ in {1..30}; do
if available "$package"; then verify_remote "$package" && return; fi
sleep 10
done
echo "$package@$VERSION did not become available after publish reconciliation" >&2
return 1
}
publish code2graph
publish code2graph-query
publish code2graph-cli
publish-pypi:
needs: [validate, provenance]
if: inputs.pypi
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
actions: read
contents: read
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.distribution_ref || inputs.tag }}
- uses: actions/download-artifact@v8
with:
name: ${{ needs.provenance.outputs.artifact }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: bundle
- name: Reject different existing files and verify eventual registry hashes
shell: bash
run: |
set -euo pipefail
project=code2graph-rs
version="${{ needs.validate.outputs.python_version }}"
release_json() { curl -fsSL "https://pypi.org/pypi/$project/$version/json"; }
check_hashes() {
local json="$1" file name local_hash remote_hash
mapfile -t local_names < <(find bundle/python -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' \) -printf '%f\n' | sort)
mapfile -t remote_names < <(jq -r '.urls[].filename' <<<"$json" | sort)
for name in "${remote_names[@]}"; do
printf '%s\n' "${local_names[@]}" | grep -Fxq "$name" || { echo "unexpected PyPI artifact $name" >&2; return 1; }
file="bundle/python/$name"
local_hash="$(sha256sum "$file" | awk '{print $1}')"
remote_hash="$(jq -r --arg name "$name" '.urls[] | select(.filename == $name) | .digests.sha256' <<<"$json")"
test "$remote_hash" = "$local_hash" || { echo "PyPI artifact mismatch for $name" >&2; return 1; }
done
}
if json="$(release_json 2>/dev/null)"; then check_hashes "$json"; fi
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: bundle/python
skip-existing: true
- name: Poll and re-verify published Python files
shell: bash
run: |
set -euo pipefail
for attempt in {1..90}; do
if json="$(curl -fsSL -H 'Cache-Control: no-cache' "https://pypi.org/pypi/code2graph-rs/${{ needs.validate.outputs.python_version }}/json?attempt=$attempt" 2>/dev/null)"; then
mapfile -t local_names < <(find bundle/python -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' \) -printf '%f\n' | sort)
mapfile -t remote_names < <(jq -r '.urls[].filename' <<<"$json" | sort)
complete=true
test "${local_names[*]}" = "${remote_names[*]}" || complete=false
for name in "${local_names[@]}"; do
hash="$(sha256sum "bundle/python/$name" | awk '{print $1}')"
test "$(jq -r --arg name "$name" '.urls[] | select(.filename == $name) | .digests.sha256' <<<"$json")" = "$hash" || complete=false
done
"$complete" && exit 0
fi
sleep 5
done
exit 1
publish-npm:
needs: [validate, provenance]
if: inputs.npm
runs-on: ubuntu-latest
environment: npm
permissions:
id-token: write
actions: read
contents: read
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.distribution_ref || inputs.tag }}
- uses: actions/setup-node@v5
with:
node-version: "22"
registry-url: https://registry.npmjs.org
- uses: actions/download-artifact@v8
with:
name: ${{ needs.provenance.outputs.artifact }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: bundle
- name: Publish prepared platform packages before the root package
shell: bash
env:
# A token is required to bootstrap previously unpublished platform packages;
# provenance remains attached and subsequent releases stay idempotent.
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
publish() {
local tarball="$1" name version remote
name=$(tar -xOf "$tarball" package/package.json | jq -r .name)
version=$(tar -xOf "$tarball" package/package.json | jq -r .version)
if npm view "$name@$version" version >/dev/null 2>&1; then
remote="$(mktemp -d)"
npm pack "$name@$version" --pack-destination "$remote"
python3 scripts/release/artifact_manifest.py compare-npm --local "$tarball" --remote "$(find "$remote" -name '*.tgz' -print -quit)"
else
npm publish "$tarball" --access public --provenance --tag "${{ needs.validate.outputs.is_prerelease == 'true' && 'next' || 'latest' }}"
fi
for _ in {1..30}; do
if npm view "$name@$version" version >/dev/null 2>&1; then
remote="$(mktemp -d)"
npm pack "$name@$version" --pack-destination "$remote"
python3 scripts/release/artifact_manifest.py compare-npm --local "$tarball" --remote "$(find "$remote" -name '*.tgz' -print -quit)"
return
fi
sleep 2
done
return 1
}
shopt -s nullglob
platform_tarballs=(bundle/npm/*linux-*.tgz bundle/npm/*darwin-*.tgz bundle/npm/*win32-*.tgz)
test "${#platform_tarballs[@]}" -eq 6
for file in "${platform_tarballs[@]}"; do publish "$file"; done
root_tarballs=(bundle/npm/nodedb-lab-code2graph-*.tgz)
roots=()
for file in "${root_tarballs[@]}"; do [[ "$file" == *linux-* || "$file" == *darwin-* || "$file" == *win32-* ]] || roots+=("$file"); done
test "${#roots[@]}" -eq 1
publish "${roots[0]}"
github-release:
needs: [validate, provenance]
if: inputs.github && needs.validate.outputs.is_prerelease != 'true'
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
- uses: actions/download-artifact@v8
with:
name: ${{ needs.provenance.outputs.artifact }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: bundle
- name: Verify exact GitHub release input set
id: release-files
shell: bash
run: |
set -euo pipefail
mapfile -t files < <(find bundle -type f | sort)
test "${#files[@]}" -eq 16
# It consumes the job-scoped contents:write GITHUB_TOKEN directly.
- name: Create or update GitHub release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
name: code2graph ${{ needs.validate.outputs.version }}
target_commitish: ${{ needs.validate.outputs.source_sha }}
generate_release_notes: true
prerelease: false
make_latest: true
files: bundle/**
fail_on_unmatched_files: true
overwrite_files: true
- name: Verify exact GitHub release output
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
SHA: ${{ needs.validate.outputs.source_sha }}
run: |
set -euo pipefail
mapfile -t expected < <(find bundle -type f -printf '%f\n' | sort)
release_json="$(gh api "/repos/$GITHUB_REPOSITORY/releases/tags/$TAG")"
test "$(jq -r .tag_name <<<"$release_json")" = "$TAG"
test "$(jq -r .target_commitish <<<"$release_json")" = "$SHA"
test "$(jq -r .prerelease <<<"$release_json")" = false
mapfile -t actual < <(jq -r '.assets[].name' <<<"$release_json" | sort)
test "${actual[*]}" = "${expected[*]}" || { echo "GitHub release attachment set mismatch" >&2; exit 1; }