Release v0.0.0-beta.17 #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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; } |